Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Expand Down Expand Up @@ -45,13 +45,13 @@
|-----------|---------|
| `cmd/mcpproxy/edition.go` | Default edition = "personal" |
| `cmd/mcpproxy/edition_teams.go` | Build-tagged override for server edition |
| `cmd/mcpproxy/teams_register.go` | Server feature registration entry point |
| `internal/teams/` | Server-only code (all files have `//go:build server`) |
| `internal/teams/auth/` | OAuth authentication, session management, JWT tokens, middleware |
| `internal/teams/users/` | User/session models, BBolt store, user server management |
| `internal/teams/workspace/` | Per-user workspace manager for personal upstream servers |
| `internal/teams/multiuser/` | Multi-user router, tool filtering, activity isolation |
| `internal/teams/api/` | Server REST API endpoints (user, admin, auth) |
| `cmd/mcpproxy/serveredition_register.go` | Server feature registration entry point |
| `internal/serveredition/` | Server-only code (all files have `//go:build server`) |
| `internal/serveredition/auth/` | OAuth, sessions, JWT tokens, middleware |
| `internal/serveredition/users/` | User/session models, BBolt store |
| `internal/serveredition/workspace/` | Per-user workspace for personal upstreams |
| `internal/serveredition/multiuser/` | Multi-user router, tool filtering, activity isolation |
| `internal/serveredition/api/` | Server REST API endpoints (user, admin, auth) |
| `native/macos/MCPProxy/` | Swift macOS tray app (SwiftUI, macOS 13+) |
| `native/macos/MCPProxyUITest/` | Swift MCP server for UI testing (accessibility + screenshots) |
| `native/windows/` | Future C# tray app (placeholder) |
Expand All @@ -70,7 +70,7 @@

```json
{
"teams": {
"server_edition": {
"enabled": true,
"admin_emails": ["admin@company.com"],
"oauth": {
Expand Down Expand Up @@ -117,7 +117,7 @@
### Server Testing

```bash
go test -tags server ./internal/teams/... -v -race # All server unit + integration tests
go test -tags server ./internal/serveredition/... -v -race # All server unit + integration tests
go build -tags server ./cmd/mcpproxy # Build server edition
go build ./cmd/mcpproxy # Verify personal edition unaffected
```
Expand Down
2 changes: 1 addition & 1 deletion cmd/mcpproxy/edition.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package main

// Edition identifies which MCPProxy edition this binary is.
// This is the default value; teams edition overrides it via build tags.
// This is the default value; server edition overrides it via build tags.
var Edition = "personal"
11 changes: 11 additions & 0 deletions cmd/mcpproxy/serveredition_register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build server

package main

// Server edition features are registered via init() functions in their
// respective packages. The actual setup happens when the server calls
// serveredition.SetupAll() during HTTP server initialization (see internal/server/serveredition_wire.go).
//
// This file imports the serveredition package for its init() side effects,
// which register feature modules in the server registry.
import _ "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition"
44 changes: 22 additions & 22 deletions cmd/mcpproxy/status_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,24 @@ import (

// StatusInfo holds the collected status data for display.
type StatusInfo struct {
State string `json:"state"`
Edition string `json:"edition"`
ListenAddr string `json:"listen_addr"`
Uptime string `json:"uptime,omitempty"`
UptimeSeconds float64 `json:"uptime_seconds,omitempty"`
APIKey string `json:"api_key"`
WebUIURL string `json:"web_ui_url"`
RoutingMode string `json:"routing_mode"`
Endpoints map[string]string `json:"endpoints"`
Servers *ServerCounts `json:"servers,omitempty"`
SocketPath string `json:"socket_path,omitempty"`
ConfigPath string `json:"config_path,omitempty"`
Version string `json:"version,omitempty"`
TeamsInfo *TeamsStatusInfo `json:"teams,omitempty"`
State string `json:"state"`
Edition string `json:"edition"`
ListenAddr string `json:"listen_addr"`
Uptime string `json:"uptime,omitempty"`
UptimeSeconds float64 `json:"uptime_seconds,omitempty"`
APIKey string `json:"api_key"`
WebUIURL string `json:"web_ui_url"`
RoutingMode string `json:"routing_mode"`
Endpoints map[string]string `json:"endpoints"`
Servers *ServerCounts `json:"servers,omitempty"`
SocketPath string `json:"socket_path,omitempty"`
ConfigPath string `json:"config_path,omitempty"`
Version string `json:"version,omitempty"`
ServerEditionInfo *ServerEditionStatusInfo `json:"server_edition,omitempty"`
}

// TeamsStatusInfo holds teams-specific status information.
type TeamsStatusInfo struct {
// ServerEditionStatusInfo holds server-edition-specific status information.
type ServerEditionStatusInfo struct {
OAuthProvider string `json:"oauth_provider"`
AdminEmails []string `json:"admin_emails"`
}
Expand Down Expand Up @@ -173,8 +173,8 @@ func collectStatusFromDaemon(cfg *config.Config, socketPath, configPath string)
info.RoutingMode = config.RoutingModeRetrieveTools
}

// Add teams info if available
info.TeamsInfo = collectTeamsInfo(cfg)
// Add server edition info if available
info.ServerEditionInfo = collectServerEditionInfo(cfg)

// Get status data (running, listen_addr, upstream_stats)
statusData, err := client.GetStatus(ctx)
Expand Down Expand Up @@ -247,7 +247,7 @@ func collectStatusFromConfig(cfg *config.Config, socketPath, configPath string)
ConfigPath: configPath,
}

info.TeamsInfo = collectTeamsInfo(cfg)
info.ServerEditionInfo = collectServerEditionInfo(cfg)

return info
}
Expand Down Expand Up @@ -416,11 +416,11 @@ func printStatusTable(info *StatusInfo) {
}
}

if info.TeamsInfo != nil {
if info.ServerEditionInfo != nil {
fmt.Println()
fmt.Println("Server Edition")
fmt.Printf(" %-12s %s\n", "OAuth:", info.TeamsInfo.OAuthProvider)
fmt.Printf(" %-12s %s\n", "Admins:", strings.Join(info.TeamsInfo.AdminEmails, ", "))
fmt.Printf(" %-12s %s\n", "OAuth:", info.ServerEditionInfo.OAuthProvider)
fmt.Printf(" %-12s %s\n", "Admins:", strings.Join(info.ServerEditionInfo.AdminEmails, ", "))
}
}

Expand Down
18 changes: 18 additions & 0 deletions cmd/mcpproxy/status_serveredition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//go:build server

package main

import "github.com/smart-mcp-proxy/mcpproxy-go/internal/config"

func collectServerEditionInfo(cfg *config.Config) *ServerEditionStatusInfo {
if cfg.ServerEdition == nil || !cfg.ServerEdition.Enabled {
return nil
}
info := &ServerEditionStatusInfo{
AdminEmails: cfg.ServerEdition.AdminEmails,
}
if cfg.ServerEdition.OAuth != nil {
info.OAuthProvider = cfg.ServerEdition.OAuth.Provider
}
return info
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ package main

import "github.com/smart-mcp-proxy/mcpproxy-go/internal/config"

func collectTeamsInfo(_ *config.Config) *TeamsStatusInfo {
func collectServerEditionInfo(_ *config.Config) *ServerEditionStatusInfo {
return nil
}
18 changes: 0 additions & 18 deletions cmd/mcpproxy/status_teams.go

This file was deleted.

11 changes: 0 additions & 11 deletions cmd/mcpproxy/teams_register.go

This file was deleted.

5 changes: 3 additions & 2 deletions docs/features/idp-token-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ feature is **off by default** and requires an encryption key to activate.

## Configuration

Two settings control the feature, both under the `teams` block:
Two settings control the feature, both under the `server_edition` block (configs
that still use the legacy `teams` key are accepted as a back-compat alias):

```json
{
"teams": {
"server_edition": {
"enabled": true,
"store_idp_tokens": true,
"credential_encryption_key": "<base64-encoded 32-byte AES key>",
Expand Down
2 changes: 1 addition & 1 deletion docs/features/settings-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ friendly, prioritized form sections instead of raw JSON:
isolation, sensitive-data detection, output validation, output sanitisation,
activity retention, logging, TLS, …).
- **Raw JSON** — the full Monaco editor, kept as an escape hatch.
- **Teams** — server edition only.
- **Server Edition** — server edition only.

## How saving works

Expand Down
6 changes: 3 additions & 3 deletions docs/plans/2026-03-08-repo-restructure-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ MCPProxy personal and teams editions will be built from the **same repository**
## Binary Architecture

- `go build ./cmd/mcpproxy` → **Personal edition** (default)
- `go build -tags teams ./cmd/mcpproxy` → **Teams edition**
- Teams-only code lives in `internal/teams/` with `//go:build teams` guards
- `go build -tags server ./cmd/mcpproxy` → **Teams edition**
- Teams-only code lives in `internal/serveredition/` with `//go:build server` guards
- Teams features self-register via `init()` pattern
- Binary self-identifies edition in version output, startup logs, `/api/v1/status`

Expand All @@ -24,7 +24,7 @@ MCPProxy personal and teams editions will be built from the **same repository**
mcpproxy-go/
├── cmd/mcpproxy/
│ ├── main.go ← shared entry point
│ └── teams_register.go ← //go:build teams
│ └── teams_register.go ← //go:build server
├── internal/
│ ├── teams/ ← teams-only code (all build-tagged)
│ │ ├── auth/ ← OAuth authorization server
Expand Down
31 changes: 25 additions & 6 deletions frontend/src/views/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
</div>

<!-- Server edition / multi-user settings (server edition only) -->
<div v-if="hasTeams" v-show="activeTab === 'teams'" class="card bg-base-100 shadow-md">
<div v-if="hasServerEdition" v-show="activeTab === 'teams'" class="card bg-base-100 shadow-md">
<div class="card-body">
<h2 class="card-title text-lg" data-test="settings-server-edition-title">{{ serverEditionTitle }}</h2>
<SettingsSection section-id="teams" :fields="serverEditionFields" :working="state.working" :original="state.original" />
Expand Down Expand Up @@ -212,15 +212,20 @@ const loadError = ref('')
const activeTab = ref<string>('security')
const showConnect = ref(false)
const state = reactive<{ working: any; original: any }>({ working: {}, original: {} })
const hasTeams = computed(() => state.working && state.working.teams != null)
// Server-edition (multi-user) config lives under `server_edition` (MCP-1086).
// Gate on the canonical key, falling back to the legacy `teams` key so a config
// written before the rename still surfaces the Server Edition tab.
const hasServerEdition = computed(
() => state.working && (state.working.server_edition != null || state.working.teams != null)
)

// cross-section search: type to find any setting across all tabs
const search = ref('')
const allFields = computed<SettingField[]>(() => [
...securityFields,
...generalFields,
...advancedAccordions.flatMap((a) => a.fields),
...(hasTeams.value ? serverEditionFields : []),
...(hasServerEdition.value ? serverEditionFields : []),
])
const filteredFields = computed<SettingField[]>(() => {
const q = search.value.trim().toLowerCase()
Expand Down Expand Up @@ -255,7 +260,7 @@ const tabs = computed(() => {
{ id: 'general', label: 'General', icon: '⚙️' },
{ id: 'advanced', label: 'Advanced', icon: '🧰' },
] as Array<{ id: string; label: string; icon: string }>
if (hasTeams.value) base.push({ id: 'teams', label: SERVER_EDITION_TAB_LABEL, icon: '👥' })
if (hasServerEdition.value) base.push({ id: 'teams', label: SERVER_EDITION_TAB_LABEL, icon: '👥' })
base.push({ id: 'raw', label: 'Raw JSON', icon: '{ }' })
return base
})
Expand All @@ -282,15 +287,29 @@ function clone<T>(v: T): T {
return JSON.parse(JSON.stringify(v))
}

// Back-compat for the teams -> server_edition rename (MCP-1086): if a config
// only carries the legacy `teams` key, mirror it onto `server_edition` so the
// form (which binds to `server_edition.*`) hydrates. Mutates and returns cfg.
function aliasServerEdition(cfg: any): any {
if (cfg && cfg.server_edition == null && cfg.teams != null) {
cfg.server_edition = cfg.teams
}
return cfg
}

async function loadConfig() {
loading.value = true
loadError.value = ''
try {
const response = await api.getConfig()
if (response.success && response.data) {
const cfg = response.data.config
state.working = clone(cfg)
state.original = clone(cfg)
// The server-edition form binds to `server_edition.*` (MCP-1086). The
// backend loader already normalizes a legacy `teams` key to
// `server_edition`, but alias it here too so a config still carrying the
// old key hydrates the form (edits always save under `server_edition`).
state.working = aliasServerEdition(clone(cfg))
state.original = aliasServerEdition(clone(cfg))
configJson.value = JSON.stringify(cfg, null, 2)
configStatus.value = { valid: true }
loaded.value = true
Expand Down
17 changes: 9 additions & 8 deletions frontend/src/views/settings/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,17 +262,18 @@ export const GENERAL_FIELDS: SettingField[] = [
]

// ---- Server edition (multi-user) section ----
// User-facing wording is "Server Edition" (MCP-1087). The config dot-paths
// deliberately stay on the legacy `teams.*` key: the backend rename of the
// top-level config key (`teams` -> `server_edition`, MCP-1085 / PR #607) is
// not merged, so a live config is still `teams`-keyed. Flip these to
// `server_edition.*` in the follow-up only once that backend change lands.
// User-facing wording is "Server Edition" (MCP-1087). The backend rename of the
// top-level config key (`teams` -> `server_edition`, MCP-1086) has landed, so
// these dot-paths write/read the canonical `server_edition.*` key. Legacy
// `teams`-keyed configs still populate the form: the backend loader normalizes
// `teams` -> `server_edition` on load, and Settings.vue aliases it defensively
// so old configs hydrate the form while edits always save under `server_edition`.
export const SERVER_EDITION_TAB_LABEL = 'Server Edition'
export const SERVER_EDITION_SECTION_TITLE = '👥 Server Edition'
export const SERVER_EDITION_FIELDS: SettingField[] = [
{ key: 'teams.enabled', label: 'Enable multi-user mode', control: 'toggle', restart: true },
{ key: 'teams.oauth.provider', label: 'OAuth provider', control: 'select', options: ['', 'google', 'github', 'microsoft'].map((v) => ({ value: v, label: v || '(none)' })) },
{ key: 'teams.max_user_servers', label: 'Max servers per user', control: 'number', min: 0 },
{ key: 'server_edition.enabled', label: 'Enable multi-user mode', control: 'toggle', restart: true },
{ key: 'server_edition.oauth.provider', label: 'OAuth provider', control: 'select', options: ['', 'google', 'github', 'microsoft'].map((v) => ({ value: v, label: v || '(none)' })) },
{ key: 'server_edition.max_user_servers', label: 'Max servers per user', control: 'number', min: 0 },
]

// ---- Section 3: Advanced (subsystem accordions) ----
Expand Down
16 changes: 8 additions & 8 deletions frontend/tests/unit/settings-server-edition-wording.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import {
} from '../../src/views/settings/fields'

// MCP-1087: the Settings server-edition surface must read "Server Edition",
// not the legacy "Teams" wording. The config *keys*, however, deliberately
// stay on the legacy `teams.*` dot-paths until the backend rename of the
// config key (`teams` -> `server_edition`, MCP-1085 / PR #607, currently
// unmerged) lands. Flipping the keys early would make `hasTeams` read
// `state.working.server_edition`, which a live `teams`-keyed config doesn't
// have -> the whole server-edition tab silently disappears.
// not the legacy "Teams" wording. MCP-1086: the backend config-key rename
// (`teams` -> `server_edition`) has landed, so the config *keys* are now on the
// canonical `server_edition.*` dot-paths. `Settings.vue` gates the tab on
// `server_edition` (with a `teams` fallback) and aliases a legacy `teams`-keyed
// config onto `server_edition` at load, so old configs still hydrate the form
// while edits always save under `server_edition`.
describe('Settings server-edition wording (MCP-1087)', () => {
it('uses "Server Edition" wording with no "Teams" left in user-facing labels', () => {
expect(SERVER_EDITION_TAB_LABEL).toBe('Server Edition')
Expand All @@ -20,10 +20,10 @@ describe('Settings server-edition wording (MCP-1087)', () => {
expect(SERVER_EDITION_SECTION_TITLE).toMatch(/Server Edition/)
})

it('keeps the config field keys on the legacy `teams.*` contract (backend rename pending)', () => {
it('binds the config field keys to the canonical `server_edition.*` contract (MCP-1086)', () => {
expect(SERVER_EDITION_FIELDS.length).toBeGreaterThan(0)
for (const f of SERVER_EDITION_FIELDS) {
expect(f.key).toMatch(/^teams\./)
expect(f.key).toMatch(/^server_edition\./)
}
})
})
2 changes: 1 addition & 1 deletion internal/auth/agent_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type AgentToken struct {
CreatedAt time.Time `json:"created_at"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
Revoked bool `json:"revoked"`
UserID string `json:"user_id,omitempty"` // Owner user ID (teams edition)
UserID string `json:"user_id,omitempty"` // Owner user ID (server edition)
}

// IsExpired returns true if the token has passed its expiry time.
Expand Down
8 changes: 5 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,10 @@ type Config struct {
// downstream tool genuinely needs raw values in the response.
RevealSecretHeaders bool `json:"reveal_secret_headers,omitempty" mapstructure:"reveal-secret-headers"`

// Server edition multi-user configuration (only meaningful with -tags server)
Teams *TeamsConfig `json:"teams,omitempty" mapstructure:"teams" swaggerignore:"true"`
// Server edition multi-user configuration (only meaningful with -tags server).
// Renamed from the legacy "teams" key (MCP-1086); an existing config that
// still uses "teams" is normalized onto this field on load (see loader.go).
ServerEdition *ServerEditionConfig `json:"server_edition,omitempty" mapstructure:"server_edition" swaggerignore:"true"`
}

// TLSConfig represents TLS configuration
Expand Down Expand Up @@ -348,7 +350,7 @@ type ServerConfig struct {
// subject token for an upstream-scoped credential and injects it into the
// outbound request. The concrete type is build-tagged: a full struct in the
// server edition, an empty stub in the personal edition (which ignores it),
// so personal-edition behavior is unaffected. swaggerignore mirrors Teams.
// so personal-edition behavior is unaffected. swaggerignore mirrors ServerEdition.
AuthBroker *AuthBrokerConfig `json:"auth_broker,omitempty" mapstructure:"auth_broker" swaggerignore:"true"`
}

Expand Down
Loading
Loading