From 6e605b506caa83b076eb7e12bf00e8ec7d5e1851 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Thu, 4 Jun 2026 20:33:26 +0300 Subject: [PATCH 1/4] =?UTF-8?q?refactor(config):=20rename=20Teams=E2=86=92?= =?UTF-8?q?ServerEdition;=20internal/teams=E2=86=92serveredition;=20teams?= =?UTF-8?q?=E2=86=92server=5Fedition=20key=20(alias)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames the server-edition surface for clarity and to disambiguate from any "teams" collaboration concept (MCP-1086). Go: - config.TeamsConfig→ServerEditionConfig, TeamsOAuthConfig→ServerEditionOAuthConfig, DefaultTeamsConfig→DefaultServerEditionConfig; Config.Teams→Config.ServerEdition. - Config key/tag teams→server_edition. Legacy "teams" key still loads onto ServerEdition via normalize-on-load in loadConfigFile (new key wins). Compiles in both editions (ServerEditionConfig is a struct{} stub in personal builds). - Move internal/teams/→internal/serveredition/ (package teams→serveredition, incl. broker/ from #588); update all imports and selectors. - Rename exported symbols: TeamsAuthMiddleware→ServerEditionAuthMiddleware, TeamsStatusInfo→ServerEditionStatusInfo, TeamsInfo→ServerEditionInfo, wireTeamsOAuth→wireServerEditionOAuth. - `mcpproxy status -o json` server-edition block key teams→server_edition (aligns output with the rename). Edition strings ("personal"/"server"), the `server` build tag, and the edition_teams.go build-tag filename are unchanged per scope. Tests: - New file-load back-compat tests: new server_edition key, legacy teams key, and both-keys-new-wins (server-tagged). Docs: - CLAUDE.md path/config-key references (kept under the 40k char gate); active feature docs; specs config-key + code-path references. Spec directory slugs (029-mcpproxy-teams, 024-teams-multiuser-oauth) and historical narrative are preserved intact. swagger: teams config is swaggerignore — no REST surface change (verified). Related MCP-1086 --- CLAUDE.md | 18 +- cmd/mcpproxy/edition.go | 2 +- cmd/mcpproxy/serveredition_register.go | 11 + cmd/mcpproxy/status_cmd.go | 44 ++-- cmd/mcpproxy/status_serveredition.go | 18 ++ ...s_stub.go => status_serveredition_stub.go} | 2 +- cmd/mcpproxy/status_teams.go | 18 -- cmd/mcpproxy/teams_register.go | 11 - docs/features/settings-page.md | 2 +- internal/auth/agent_token.go | 2 +- internal/config/config.go | 8 +- internal/config/loader.go | 17 ++ ...ams_config.go => server_edition_config.go} | 44 ++-- internal/config/server_edition_config_stub.go | 6 + ..._test.go => server_edition_config_test.go} | 197 ++++++++++++------ internal/config/teams_config_stub.go | 6 - internal/config/teams_credential_test.go | 26 +-- internal/httpapi/server.go | 2 +- internal/runtime/activity_service.go | 4 +- internal/server/mcp.go | 2 +- internal/server/server.go | 4 +- .../{teams_wire.go => serveredition_wire.go} | 10 +- internal/server/serveredition_wire_stub.go | 10 + internal/server/teams_wire_stub.go | 10 - .../api/admin_handlers.go | 6 +- .../api/admin_handlers_test.go | 6 +- .../api/admin_integration_test.go | 2 +- .../api/auth_endpoints.go | 8 +- .../api/auth_endpoints_test.go | 6 +- .../api/user_activity.go | 4 +- .../api/user_activity_test.go | 4 +- .../api/user_handlers.go | 2 +- .../api/user_handlers_test.go | 2 +- .../auth/idp_subject_token.go | 2 +- .../auth/idp_subject_token_test.go | 8 +- .../auth/integration_test.go | 24 +-- .../auth/jwt_tokens.go | 0 .../auth/jwt_tokens_test.go | 0 .../auth/middleware.go | 28 +-- .../auth/middleware_test.go | 14 +- .../auth/oauth_handler.go | 8 +- .../auth/oauth_handler_test.go | 30 +-- .../auth/oauth_providers.go | 0 .../auth/oauth_providers_test.go | 0 .../auth/session_store.go | 2 +- .../auth/session_store_test.go | 2 +- .../broker/bbolt_aes.go | 2 +- .../broker/credential_store.go | 2 +- .../broker/credential_store_test.go | 0 .../broker/token_exchanger.go | 0 .../broker/token_exchanger_test.go | 0 internal/{teams => serveredition}/doc.go | 4 +- .../multiuser/activity.go | 0 .../multiuser/activity_test.go | 0 .../multiuser/isolation_test.go | 0 .../multiuser/router.go | 2 +- .../multiuser/router_test.go | 4 +- .../multiuser/tool_filter.go | 0 internal/{teams => serveredition}/registry.go | 2 +- .../{teams => serveredition}/registry_test.go | 2 +- internal/{teams => serveredition}/setup.go | 16 +- .../{teams => serveredition}/setup_test.go | 16 +- .../{teams => serveredition}/users/models.go | 0 .../users/models_test.go | 0 .../users/server_store.go | 0 .../{teams => serveredition}/users/store.go | 2 +- .../users/store_test.go | 0 .../workspace/integration_test.go | 0 .../workspace/manager.go | 2 +- .../workspace/manager_test.go | 0 .../workspace/workspace.go | 2 +- .../workspace/workspace_test.go | 2 +- internal/storage/async_ops_test.go | 4 +- specs/029-mcpproxy-teams/data-model.md | 2 +- specs/029-mcpproxy-teams/plan.md | 4 +- specs/029-mcpproxy-teams/spec.md | 2 +- specs/029-mcpproxy-teams/tasks.md | 16 +- .../verification/report.md | 2 +- 78 files changed, 407 insertions(+), 313 deletions(-) create mode 100644 cmd/mcpproxy/serveredition_register.go create mode 100644 cmd/mcpproxy/status_serveredition.go rename cmd/mcpproxy/{status_teams_stub.go => status_serveredition_stub.go} (60%) delete mode 100644 cmd/mcpproxy/status_teams.go delete mode 100644 cmd/mcpproxy/teams_register.go rename internal/config/{teams_config.go => server_edition_config.go} (57%) create mode 100644 internal/config/server_edition_config_stub.go rename internal/config/{teams_config_test.go => server_edition_config_test.go} (62%) delete mode 100644 internal/config/teams_config_stub.go rename internal/server/{teams_wire.go => serveredition_wire.go} (71%) create mode 100644 internal/server/serveredition_wire_stub.go delete mode 100644 internal/server/teams_wire_stub.go rename internal/{teams => serveredition}/api/admin_handlers.go (98%) rename internal/{teams => serveredition}/api/admin_handlers_test.go (97%) rename internal/{teams => serveredition}/api/admin_integration_test.go (99%) rename internal/{teams => serveredition}/api/auth_endpoints.go (94%) rename internal/{teams => serveredition}/api/auth_endpoints_test.go (96%) rename internal/{teams => serveredition}/api/user_activity.go (97%) rename internal/{teams => serveredition}/api/user_activity_test.go (97%) rename internal/{teams => serveredition}/api/user_handlers.go (99%) rename internal/{teams => serveredition}/api/user_handlers_test.go (99%) rename internal/{teams => serveredition}/auth/idp_subject_token.go (98%) rename internal/{teams => serveredition}/auth/idp_subject_token_test.go (96%) rename internal/{teams => serveredition}/auth/integration_test.go (97%) rename internal/{teams => serveredition}/auth/jwt_tokens.go (100%) rename internal/{teams => serveredition}/auth/jwt_tokens_test.go (100%) rename internal/{teams => serveredition}/auth/middleware.go (85%) rename internal/{teams => serveredition}/auth/middleware_test.go (97%) rename internal/{teams => serveredition}/auth/oauth_handler.go (98%) rename internal/{teams => serveredition}/auth/oauth_handler_test.go (94%) rename internal/{teams => serveredition}/auth/oauth_providers.go (100%) rename internal/{teams => serveredition}/auth/oauth_providers_test.go (100%) rename internal/{teams => serveredition}/auth/session_store.go (98%) rename internal/{teams => serveredition}/auth/session_store_test.go (99%) rename internal/{teams => serveredition}/broker/bbolt_aes.go (98%) rename internal/{teams => serveredition}/broker/credential_store.go (98%) rename internal/{teams => serveredition}/broker/credential_store_test.go (100%) rename internal/{teams => serveredition}/broker/token_exchanger.go (100%) rename internal/{teams => serveredition}/broker/token_exchanger_test.go (100%) rename internal/{teams => serveredition}/doc.go (64%) rename internal/{teams => serveredition}/multiuser/activity.go (100%) rename internal/{teams => serveredition}/multiuser/activity_test.go (100%) rename internal/{teams => serveredition}/multiuser/isolation_test.go (100%) rename internal/{teams => serveredition}/multiuser/router.go (98%) rename internal/{teams => serveredition}/multiuser/router_test.go (98%) rename internal/{teams => serveredition}/multiuser/tool_filter.go (100%) rename internal/{teams => serveredition}/registry.go (98%) rename internal/{teams => serveredition}/registry_test.go (98%) rename internal/{teams => serveredition}/setup.go (85%) rename internal/{teams => serveredition}/setup_test.go (92%) rename internal/{teams => serveredition}/users/models.go (100%) rename internal/{teams => serveredition}/users/models_test.go (100%) rename internal/{teams => serveredition}/users/server_store.go (100%) rename internal/{teams => serveredition}/users/store.go (99%) rename internal/{teams => serveredition}/users/store_test.go (100%) rename internal/{teams => serveredition}/workspace/integration_test.go (100%) rename internal/{teams => serveredition}/workspace/manager.go (98%) rename internal/{teams => serveredition}/workspace/manager_test.go (100%) rename internal/{teams => serveredition}/workspace/workspace.go (98%) rename internal/{teams => serveredition}/workspace/workspace_test.go (98%) diff --git a/CLAUDE.md b/CLAUDE.md index 02d62d7cb..bafa08cf0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,13 +45,13 @@ MCPProxy is built in two editions from the same codebase using Go build tags: |-----------|---------| | `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) | @@ -70,7 +70,7 @@ Server edition supports OAuth-based multi-user authentication with Google, GitHu ```json { - "teams": { + "server_edition": { "enabled": true, "admin_emails": ["admin@company.com"], "oauth": { @@ -117,7 +117,7 @@ Server edition supports OAuth-based multi-user authentication with Google, GitHu ### 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 ``` diff --git a/cmd/mcpproxy/edition.go b/cmd/mcpproxy/edition.go index 9d143d222..7d95bab33 100644 --- a/cmd/mcpproxy/edition.go +++ b/cmd/mcpproxy/edition.go @@ -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" diff --git a/cmd/mcpproxy/serveredition_register.go b/cmd/mcpproxy/serveredition_register.go new file mode 100644 index 000000000..d77217a63 --- /dev/null +++ b/cmd/mcpproxy/serveredition_register.go @@ -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" diff --git a/cmd/mcpproxy/status_cmd.go b/cmd/mcpproxy/status_cmd.go index 81df7ed87..f1a5c4f44 100644 --- a/cmd/mcpproxy/status_cmd.go +++ b/cmd/mcpproxy/status_cmd.go @@ -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"` } @@ -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) @@ -247,7 +247,7 @@ func collectStatusFromConfig(cfg *config.Config, socketPath, configPath string) ConfigPath: configPath, } - info.TeamsInfo = collectTeamsInfo(cfg) + info.ServerEditionInfo = collectServerEditionInfo(cfg) return info } @@ -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, ", ")) } } diff --git a/cmd/mcpproxy/status_serveredition.go b/cmd/mcpproxy/status_serveredition.go new file mode 100644 index 000000000..b2f271362 --- /dev/null +++ b/cmd/mcpproxy/status_serveredition.go @@ -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 +} diff --git a/cmd/mcpproxy/status_teams_stub.go b/cmd/mcpproxy/status_serveredition_stub.go similarity index 60% rename from cmd/mcpproxy/status_teams_stub.go rename to cmd/mcpproxy/status_serveredition_stub.go index 5cc415079..794a2974a 100644 --- a/cmd/mcpproxy/status_teams_stub.go +++ b/cmd/mcpproxy/status_serveredition_stub.go @@ -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 } diff --git a/cmd/mcpproxy/status_teams.go b/cmd/mcpproxy/status_teams.go deleted file mode 100644 index 298a900d3..000000000 --- a/cmd/mcpproxy/status_teams.go +++ /dev/null @@ -1,18 +0,0 @@ -//go:build server - -package main - -import "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - -func collectTeamsInfo(cfg *config.Config) *TeamsStatusInfo { - if cfg.Teams == nil || !cfg.Teams.Enabled { - return nil - } - info := &TeamsStatusInfo{ - AdminEmails: cfg.Teams.AdminEmails, - } - if cfg.Teams.OAuth != nil { - info.OAuthProvider = cfg.Teams.OAuth.Provider - } - return info -} diff --git a/cmd/mcpproxy/teams_register.go b/cmd/mcpproxy/teams_register.go deleted file mode 100644 index 3fa7ae7bf..000000000 --- a/cmd/mcpproxy/teams_register.go +++ /dev/null @@ -1,11 +0,0 @@ -//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 -// teams.SetupAll() during HTTP server initialization (see internal/server/teams_wire.go). -// -// This file imports the teams package for its init() side effects, -// which register feature modules in the server registry. -import _ "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams" diff --git a/docs/features/settings-page.md b/docs/features/settings-page.md index a82d67f00..01dcf9bb0 100644 --- a/docs/features/settings-page.md +++ b/docs/features/settings-page.md @@ -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 build only. ## How saving works diff --git a/internal/auth/agent_token.go b/internal/auth/agent_token.go index 5d4aee694..04bd10261 100644 --- a/internal/auth/agent_token.go +++ b/internal/auth/agent_token.go @@ -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. diff --git a/internal/config/config.go b/internal/config/config.go index 98fb1d68e..5cb29a683 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 @@ -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"` } diff --git a/internal/config/loader.go b/internal/config/loader.go index 5a38f251c..40175d7f0 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -245,6 +245,23 @@ func loadConfigFile(path string, cfg *Config) error { return fmt.Errorf("failed to parse config file: %w", err) } + // Back-compat (MCP-1086): the server-edition block was renamed from the + // legacy "teams" key to "server_edition". An existing config that still uses + // "teams" is normalized onto ServerEdition on read. The new key always wins; + // only fall back to the legacy key when "server_edition" is absent. This + // compiles in both editions because ServerEditionConfig is a struct{} stub + // in the personal build (it simply unmarshals to an empty value there). + if _, hasNew := rawConfig["server_edition"]; !hasNew { + if legacy, hasLegacy := rawConfig["teams"]; hasLegacy { + if raw, err := json.Marshal(legacy); err == nil { + var se ServerEditionConfig + if err := json.Unmarshal(raw, &se); err == nil { + cfg.ServerEdition = &se + } + } + } + } + // Set created time if not specified for _, server := range cfg.Servers { if server.Created.IsZero() { diff --git a/internal/config/teams_config.go b/internal/config/server_edition_config.go similarity index 57% rename from internal/config/teams_config.go rename to internal/config/server_edition_config.go index 5d02dd8dd..168b5fb63 100644 --- a/internal/config/teams_config.go +++ b/internal/config/server_edition_config.go @@ -9,15 +9,15 @@ import ( "time" ) -// TeamsConfig holds configuration for the server edition multi-user features. -type TeamsConfig struct { - Enabled bool `json:"enabled" mapstructure:"enabled"` - AdminEmails []string `json:"admin_emails" mapstructure:"admin-emails"` - OAuth *TeamsOAuthConfig `json:"oauth,omitempty" mapstructure:"oauth"` - SessionTTL Duration `json:"session_ttl,omitempty" mapstructure:"session-ttl"` - BearerTokenTTL Duration `json:"bearer_token_ttl,omitempty" mapstructure:"bearer-token-ttl"` - WorkspaceIdleTimeout Duration `json:"workspace_idle_timeout,omitempty" mapstructure:"workspace-idle-timeout"` - MaxUserServers int `json:"max_user_servers,omitempty" mapstructure:"max-user-servers"` +// ServerEditionConfig holds configuration for the server edition multi-user features. +type ServerEditionConfig struct { + Enabled bool `json:"enabled" mapstructure:"enabled"` + AdminEmails []string `json:"admin_emails" mapstructure:"admin-emails"` + OAuth *ServerEditionOAuthConfig `json:"oauth,omitempty" mapstructure:"oauth"` + SessionTTL Duration `json:"session_ttl,omitempty" mapstructure:"session-ttl"` + BearerTokenTTL Duration `json:"bearer_token_ttl,omitempty" mapstructure:"bearer-token-ttl"` + WorkspaceIdleTimeout Duration `json:"workspace_idle_timeout,omitempty" mapstructure:"workspace-idle-timeout"` + MaxUserServers int `json:"max_user_servers,omitempty" mapstructure:"max-user-servers"` // CredentialEncryptionKey encrypts per-user upstream credentials at rest // (spec 074). When empty, it falls back to the MCPPROXY_CRED_KEY env var. @@ -27,8 +27,8 @@ type TeamsConfig struct { StoreIDPTokens bool `json:"store_idp_tokens" mapstructure:"store-idp-tokens"` } -// TeamsOAuthConfig holds OAuth identity provider configuration for the server edition. -type TeamsOAuthConfig struct { +// ServerEditionOAuthConfig holds OAuth identity provider configuration for the server edition. +type ServerEditionOAuthConfig struct { Provider string `json:"provider" mapstructure:"provider"` // "google", "github", "microsoft" ClientID string `json:"client_id" mapstructure:"client-id"` ClientSecret string `json:"client_secret" mapstructure:"client-secret"` @@ -36,9 +36,9 @@ type TeamsOAuthConfig struct { AllowedDomains []string `json:"allowed_domains,omitempty" mapstructure:"allowed-domains"` } -// DefaultTeamsConfig returns a TeamsConfig with sensible defaults. -func DefaultTeamsConfig() *TeamsConfig { - return &TeamsConfig{ +// DefaultServerEditionConfig returns a ServerEditionConfig with sensible defaults. +func DefaultServerEditionConfig() *ServerEditionConfig { + return &ServerEditionConfig{ Enabled: false, SessionTTL: Duration(24 * time.Hour), BearerTokenTTL: Duration(24 * time.Hour), @@ -48,7 +48,7 @@ func DefaultTeamsConfig() *TeamsConfig { } // IsAdminEmail checks if the given email is in the admin list (case-insensitive). -func (c *TeamsConfig) IsAdminEmail(email string) bool { +func (c *ServerEditionConfig) IsAdminEmail(email string) bool { for _, admin := range c.AdminEmails { if strings.EqualFold(admin, email) { return true @@ -57,8 +57,8 @@ func (c *TeamsConfig) IsAdminEmail(email string) bool { return false } -// Validate checks that the TeamsConfig is valid for operation. -func (c *TeamsConfig) Validate() error { +// Validate checks that the ServerEditionConfig is valid for operation. +func (c *ServerEditionConfig) Validate() error { if !c.Enabled { return nil // disabled, no validation needed } @@ -68,20 +68,20 @@ func (c *TeamsConfig) Validate() error { c.CredentialEncryptionKey = os.Getenv("MCPPROXY_CRED_KEY") } if len(c.AdminEmails) == 0 { - return fmt.Errorf("teams.admin_emails must contain at least one admin email") + return fmt.Errorf("server_edition.admin_emails must contain at least one admin email") } if c.OAuth == nil { - return fmt.Errorf("teams.oauth configuration is required when teams is enabled") + return fmt.Errorf("server_edition.oauth configuration is required when server_edition is enabled") } validProviders := map[string]bool{"google": true, "github": true, "microsoft": true} if !validProviders[c.OAuth.Provider] { - return fmt.Errorf("teams.oauth.provider must be one of: google, github, microsoft (got: %s)", c.OAuth.Provider) + return fmt.Errorf("server_edition.oauth.provider must be one of: google, github, microsoft (got: %s)", c.OAuth.Provider) } if c.OAuth.ClientID == "" { - return fmt.Errorf("teams.oauth.client_id is required") + return fmt.Errorf("server_edition.oauth.client_id is required") } if c.OAuth.ClientSecret == "" { - return fmt.Errorf("teams.oauth.client_secret is required") + return fmt.Errorf("server_edition.oauth.client_secret is required") } if c.OAuth.Provider == "microsoft" && c.OAuth.TenantID == "" { // Default to "common" for multi-tenant diff --git a/internal/config/server_edition_config_stub.go b/internal/config/server_edition_config_stub.go new file mode 100644 index 000000000..16565b2b6 --- /dev/null +++ b/internal/config/server_edition_config_stub.go @@ -0,0 +1,6 @@ +//go:build !server + +package config + +// ServerEditionConfig is a stub for personal edition. Server edition features are not available. +type ServerEditionConfig struct{} diff --git a/internal/config/teams_config_test.go b/internal/config/server_edition_config_test.go similarity index 62% rename from internal/config/teams_config_test.go rename to internal/config/server_edition_config_test.go index 61b5256f0..cc409154e 100644 --- a/internal/config/teams_config_test.go +++ b/internal/config/server_edition_config_test.go @@ -4,6 +4,8 @@ package config import ( "encoding/json" + "os" + "path/filepath" "testing" "time" @@ -11,8 +13,8 @@ import ( "github.com/stretchr/testify/require" ) -func TestTeamsDefaultTeamsConfig(t *testing.T) { - cfg := DefaultTeamsConfig() +func TestTeamsDefaultServerEditionConfig(t *testing.T) { + cfg := DefaultServerEditionConfig() assert.False(t, cfg.Enabled, "teams should be disabled by default") assert.Empty(t, cfg.AdminEmails, "admin emails should be empty by default") @@ -24,7 +26,7 @@ func TestTeamsDefaultTeamsConfig(t *testing.T) { } func TestTeamsIsAdminEmail(t *testing.T) { - cfg := &TeamsConfig{ + cfg := &ServerEditionConfig{ AdminEmails: []string{"admin@example.com", "Boss@Corp.io"}, } @@ -52,21 +54,21 @@ func TestTeamsIsAdminEmail(t *testing.T) { } func TestTeamsIsAdminEmail_EmptyList(t *testing.T) { - cfg := &TeamsConfig{AdminEmails: nil} + cfg := &ServerEditionConfig{AdminEmails: nil} assert.False(t, cfg.IsAdminEmail("anyone@example.com")) - cfg2 := &TeamsConfig{AdminEmails: []string{}} + cfg2 := &ServerEditionConfig{AdminEmails: []string{}} assert.False(t, cfg2.IsAdminEmail("anyone@example.com")) } func TestTeamsValidate_DisabledSkipsValidation(t *testing.T) { - cfg := &TeamsConfig{Enabled: false} + cfg := &ServerEditionConfig{Enabled: false} err := cfg.Validate() assert.NoError(t, err, "disabled teams config should pass validation") } func TestTeamsValidate_MissingAdminEmails(t *testing.T) { - cfg := &TeamsConfig{ + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: nil, } @@ -76,7 +78,7 @@ func TestTeamsValidate_MissingAdminEmails(t *testing.T) { } func TestTeamsValidate_MissingOAuth(t *testing.T) { - cfg := &TeamsConfig{ + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, OAuth: nil, @@ -87,10 +89,10 @@ func TestTeamsValidate_MissingOAuth(t *testing.T) { } func TestTeamsValidate_InvalidProvider(t *testing.T) { - cfg := &TeamsConfig{ + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "facebook", ClientID: "id", ClientSecret: "secret", @@ -103,10 +105,10 @@ func TestTeamsValidate_InvalidProvider(t *testing.T) { } func TestTeamsValidate_MissingClientID(t *testing.T) { - cfg := &TeamsConfig{ + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "google", ClientID: "", ClientSecret: "secret", @@ -118,10 +120,10 @@ func TestTeamsValidate_MissingClientID(t *testing.T) { } func TestTeamsValidate_MissingClientSecret(t *testing.T) { - cfg := &TeamsConfig{ + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "google", ClientID: "my-client-id", ClientSecret: "", @@ -133,10 +135,10 @@ func TestTeamsValidate_MissingClientSecret(t *testing.T) { } func TestTeamsValidate_ValidGoogleConfig(t *testing.T) { - cfg := &TeamsConfig{ + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "google", ClientID: "my-client-id.apps.googleusercontent.com", ClientSecret: "GOCSPX-secret", @@ -151,10 +153,10 @@ func TestTeamsValidate_ValidGoogleConfig(t *testing.T) { } func TestTeamsValidate_ValidGitHubConfig(t *testing.T) { - cfg := &TeamsConfig{ + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "github", ClientID: "Iv1.abc123", ClientSecret: "secret123", @@ -165,10 +167,10 @@ func TestTeamsValidate_ValidGitHubConfig(t *testing.T) { } func TestTeamsValidate_MicrosoftDefaultsTenantID(t *testing.T) { - cfg := &TeamsConfig{ + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "microsoft", ClientID: "my-client-id", ClientSecret: "my-client-secret", @@ -181,10 +183,10 @@ func TestTeamsValidate_MicrosoftDefaultsTenantID(t *testing.T) { } func TestTeamsValidate_MicrosoftExplicitTenantID(t *testing.T) { - cfg := &TeamsConfig{ + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "microsoft", ClientID: "my-client-id", ClientSecret: "my-client-secret", @@ -197,10 +199,10 @@ func TestTeamsValidate_MicrosoftExplicitTenantID(t *testing.T) { } func TestTeamsValidate_DefaultsAppliedForZeroValues(t *testing.T) { - cfg := &TeamsConfig{ + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "google", ClientID: "id", ClientSecret: "secret", @@ -219,10 +221,10 @@ func TestTeamsValidate_AllProviders(t *testing.T) { providers := []string{"google", "github", "microsoft"} for _, provider := range providers { t.Run(provider, func(t *testing.T) { - cfg := &TeamsConfig{ + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: provider, ClientID: "id", ClientSecret: "secret", @@ -234,11 +236,11 @@ func TestTeamsValidate_AllProviders(t *testing.T) { } } -func TestTeamsConfig_JSONRoundTrip(t *testing.T) { - original := &TeamsConfig{ +func TestServerEditionConfig_JSONRoundTrip(t *testing.T) { + original := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com", "boss@corp.io"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "google", ClientID: "my-client-id.apps.googleusercontent.com", ClientSecret: "GOCSPX-secret", @@ -253,7 +255,7 @@ func TestTeamsConfig_JSONRoundTrip(t *testing.T) { data, err := json.Marshal(original) require.NoError(t, err) - var restored TeamsConfig + var restored ServerEditionConfig err = json.Unmarshal(data, &restored) require.NoError(t, err) @@ -270,11 +272,11 @@ func TestTeamsConfig_JSONRoundTrip(t *testing.T) { assert.Equal(t, original.MaxUserServers, restored.MaxUserServers) } -func TestTeamsConfig_JSONRoundTrip_MicrosoftWithTenant(t *testing.T) { - original := &TeamsConfig{ +func TestServerEditionConfig_JSONRoundTrip_MicrosoftWithTenant(t *testing.T) { + original := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@contoso.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "microsoft", ClientID: "my-client-id", ClientSecret: "my-secret", @@ -286,7 +288,7 @@ func TestTeamsConfig_JSONRoundTrip_MicrosoftWithTenant(t *testing.T) { data, err := json.Marshal(original) require.NoError(t, err) - var restored TeamsConfig + var restored ServerEditionConfig err = json.Unmarshal(data, &restored) require.NoError(t, err) @@ -294,13 +296,13 @@ func TestTeamsConfig_JSONRoundTrip_MicrosoftWithTenant(t *testing.T) { assert.Equal(t, "contoso.onmicrosoft.com", restored.OAuth.TenantID) } -func TestTeamsConfig_JSONRoundTrip_MinimalDisabled(t *testing.T) { - original := &TeamsConfig{Enabled: false} +func TestServerEditionConfig_JSONRoundTrip_MinimalDisabled(t *testing.T) { + original := &ServerEditionConfig{Enabled: false} data, err := json.Marshal(original) require.NoError(t, err) - var restored TeamsConfig + var restored ServerEditionConfig err = json.Unmarshal(data, &restored) require.NoError(t, err) @@ -309,13 +311,13 @@ func TestTeamsConfig_JSONRoundTrip_MinimalDisabled(t *testing.T) { assert.Empty(t, restored.AdminEmails) } -func TestTeamsConfig_EmbeddedInConfig(t *testing.T) { +func TestServerEditionConfig_EmbeddedInConfig(t *testing.T) { cfg := &Config{ Listen: "127.0.0.1:8080", - Teams: &TeamsConfig{ + ServerEdition: &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "google", ClientID: "id", ClientSecret: "secret", @@ -332,35 +334,35 @@ func TestTeamsConfig_EmbeddedInConfig(t *testing.T) { err = json.Unmarshal(data, &restored) require.NoError(t, err) - require.NotNil(t, restored.Teams) - assert.True(t, restored.Teams.Enabled) - assert.Equal(t, []string{"admin@example.com"}, restored.Teams.AdminEmails) - assert.Equal(t, "google", restored.Teams.OAuth.Provider) - assert.Equal(t, Duration(12*time.Hour), restored.Teams.SessionTTL) - assert.Equal(t, 30, restored.Teams.MaxUserServers) + require.NotNil(t, restored.ServerEdition) + assert.True(t, restored.ServerEdition.Enabled) + assert.Equal(t, []string{"admin@example.com"}, restored.ServerEdition.AdminEmails) + assert.Equal(t, "google", restored.ServerEdition.OAuth.Provider) + assert.Equal(t, Duration(12*time.Hour), restored.ServerEdition.SessionTTL) + assert.Equal(t, 30, restored.ServerEdition.MaxUserServers) } -func TestTeamsConfig_OmittedFromConfig(t *testing.T) { +func TestServerEditionConfig_OmittedFromConfig(t *testing.T) { cfg := &Config{ Listen: "127.0.0.1:8080", - // Teams is nil + // ServerEdition is nil } data, err := json.Marshal(cfg) require.NoError(t, err) - // Verify "teams" key is not present in JSON output + // Verify "server_edition" key is not present in JSON output var raw map[string]interface{} err = json.Unmarshal(data, &raw) require.NoError(t, err) - _, hasTeams := raw["teams"] - assert.False(t, hasTeams, "nil Teams should be omitted from JSON") + _, hasServerEdition := raw["server_edition"] + assert.False(t, hasServerEdition, "nil ServerEdition should be omitted from JSON") } -func TestTeamsConfig_UnmarshalFromJSON(t *testing.T) { +func TestServerEditionConfig_UnmarshalFromJSON(t *testing.T) { jsonStr := `{ "listen": "0.0.0.0:8080", - "teams": { + "server_edition": { "enabled": true, "admin_emails": ["admin@example.com"], "oauth": { @@ -379,12 +381,85 @@ func TestTeamsConfig_UnmarshalFromJSON(t *testing.T) { err := json.Unmarshal([]byte(jsonStr), &cfg) require.NoError(t, err) - require.NotNil(t, cfg.Teams) - assert.True(t, cfg.Teams.Enabled) - assert.Equal(t, "github", cfg.Teams.OAuth.Provider) - assert.Equal(t, "Iv1.abc", cfg.Teams.OAuth.ClientID) - assert.Equal(t, Duration(4*time.Hour), cfg.Teams.SessionTTL) - assert.Equal(t, Duration(30*time.Minute), cfg.Teams.BearerTokenTTL) - assert.Equal(t, Duration(10*time.Minute), cfg.Teams.WorkspaceIdleTimeout) - assert.Equal(t, 5, cfg.Teams.MaxUserServers) + require.NotNil(t, cfg.ServerEdition) + assert.True(t, cfg.ServerEdition.Enabled) + assert.Equal(t, "github", cfg.ServerEdition.OAuth.Provider) + assert.Equal(t, "Iv1.abc", cfg.ServerEdition.OAuth.ClientID) + assert.Equal(t, Duration(4*time.Hour), cfg.ServerEdition.SessionTTL) + assert.Equal(t, Duration(30*time.Minute), cfg.ServerEdition.BearerTokenTTL) + assert.Equal(t, Duration(10*time.Minute), cfg.ServerEdition.WorkspaceIdleTimeout) + assert.Equal(t, 5, cfg.ServerEdition.MaxUserServers) +} + +// writeServerEditionConfigFile writes a config JSON to a temp file and returns +// its path. Each test gets an isolated data_dir so LoadFromFile does not touch +// the real ~/.mcpproxy. +func writeServerEditionConfigFile(t *testing.T, body string) string { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "mcp_config.json") + content := `{ + "listen": "127.0.0.1:8080", + "data_dir": "` + dir + `", + ` + body + ` + }` + require.NoError(t, os.WriteFile(path, []byte(content), 0600)) + return path +} + +// TestLoadFromFile_ServerEditionKey verifies the new canonical "server_edition" +// key loads onto Config.ServerEdition (MCP-1086). +func TestLoadFromFile_ServerEditionKey(t *testing.T) { + path := writeServerEditionConfigFile(t, `"server_edition": { + "enabled": true, + "admin_emails": ["new@example.com"], + "oauth": {"provider": "google", "client_id": "id", "client_secret": "secret"} + }`) + + cfg, err := LoadFromFile(path) + require.NoError(t, err) + require.NotNil(t, cfg.ServerEdition) + assert.True(t, cfg.ServerEdition.Enabled) + assert.Equal(t, []string{"new@example.com"}, cfg.ServerEdition.AdminEmails) + assert.Equal(t, "google", cfg.ServerEdition.OAuth.Provider) +} + +// TestLoadFromFile_LegacyTeamsKey verifies a config that still uses the legacy +// "teams" key is normalized onto Config.ServerEdition on load (back-compat, +// MCP-1086). +func TestLoadFromFile_LegacyTeamsKey(t *testing.T) { + path := writeServerEditionConfigFile(t, `"teams": { + "enabled": true, + "admin_emails": ["legacy@example.com"], + "oauth": {"provider": "github", "client_id": "Iv1.abc", "client_secret": "ghp_x"} + }`) + + cfg, err := LoadFromFile(path) + require.NoError(t, err) + require.NotNil(t, cfg.ServerEdition, "legacy 'teams' key must load onto ServerEdition") + assert.True(t, cfg.ServerEdition.Enabled) + assert.Equal(t, []string{"legacy@example.com"}, cfg.ServerEdition.AdminEmails) + assert.Equal(t, "github", cfg.ServerEdition.OAuth.Provider) +} + +// TestLoadFromFile_BothKeysNewWins verifies that when both keys are present the +// new "server_edition" key takes precedence over the legacy "teams" key. +func TestLoadFromFile_BothKeysNewWins(t *testing.T) { + path := writeServerEditionConfigFile(t, `"server_edition": { + "enabled": true, + "admin_emails": ["new@example.com"], + "oauth": {"provider": "google", "client_id": "id", "client_secret": "secret"} + }, + "teams": { + "enabled": true, + "admin_emails": ["legacy@example.com"], + "oauth": {"provider": "github", "client_id": "Iv1.abc", "client_secret": "ghp_x"} + }`) + + cfg, err := LoadFromFile(path) + require.NoError(t, err) + require.NotNil(t, cfg.ServerEdition) + assert.Equal(t, []string{"new@example.com"}, cfg.ServerEdition.AdminEmails, + "new server_edition key must win over legacy teams key") + assert.Equal(t, "google", cfg.ServerEdition.OAuth.Provider) } diff --git a/internal/config/teams_config_stub.go b/internal/config/teams_config_stub.go deleted file mode 100644 index 43a3db618..000000000 --- a/internal/config/teams_config_stub.go +++ /dev/null @@ -1,6 +0,0 @@ -//go:build !server - -package config - -// TeamsConfig is a stub for personal edition. Teams features are not available. -type TeamsConfig struct{} diff --git a/internal/config/teams_credential_test.go b/internal/config/teams_credential_test.go index 847cf4e10..d513cbcac 100644 --- a/internal/config/teams_credential_test.go +++ b/internal/config/teams_credential_test.go @@ -10,35 +10,35 @@ import ( "github.com/stretchr/testify/require" ) -func TestTeamsConfig_StoreIDPTokensDefaultsFalse(t *testing.T) { +func TestServerEditionConfig_StoreIDPTokensDefaultsFalse(t *testing.T) { // Default config: privacy-preserving default (FR-006). - cfg := DefaultTeamsConfig() + cfg := DefaultServerEditionConfig() assert.False(t, cfg.StoreIDPTokens) // Absent from JSON => false. - var parsed TeamsConfig + var parsed ServerEditionConfig require.NoError(t, json.Unmarshal([]byte(`{"enabled":false}`), &parsed)) assert.False(t, parsed.StoreIDPTokens) } -func TestTeamsConfig_StoreIDPTokensParsed(t *testing.T) { - var parsed TeamsConfig +func TestServerEditionConfig_StoreIDPTokensParsed(t *testing.T) { + var parsed ServerEditionConfig require.NoError(t, json.Unmarshal([]byte(`{"store_idp_tokens":true}`), &parsed)) assert.True(t, parsed.StoreIDPTokens) } -func TestTeamsConfig_CredentialEncryptionKeyParsed(t *testing.T) { - var parsed TeamsConfig +func TestServerEditionConfig_CredentialEncryptionKeyParsed(t *testing.T) { + var parsed ServerEditionConfig require.NoError(t, json.Unmarshal([]byte(`{"credential_encryption_key":"abc123"}`), &parsed)) assert.Equal(t, "abc123", parsed.CredentialEncryptionKey) } -func TestTeamsConfig_CredentialEncryptionKeyEnvFallback(t *testing.T) { +func TestServerEditionConfig_CredentialEncryptionKeyEnvFallback(t *testing.T) { t.Setenv("MCPPROXY_CRED_KEY", "from-env-key") - cfg := &TeamsConfig{ + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "google", ClientID: "cid", ClientSecret: "csec", @@ -48,13 +48,13 @@ func TestTeamsConfig_CredentialEncryptionKeyEnvFallback(t *testing.T) { assert.Equal(t, "from-env-key", cfg.CredentialEncryptionKey, "env MCPPROXY_CRED_KEY should fill an empty key") } -func TestTeamsConfig_CredentialEncryptionKeyConfigWins(t *testing.T) { +func TestServerEditionConfig_CredentialEncryptionKeyConfigWins(t *testing.T) { t.Setenv("MCPPROXY_CRED_KEY", "from-env-key") - cfg := &TeamsConfig{ + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, CredentialEncryptionKey: "from-config", - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "google", ClientID: "cid", ClientSecret: "csec", diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index a257cc551..342564cb5 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -1079,7 +1079,7 @@ func (s *Server) buildWebUIURLWithAPIKey(listenAddr string, r *http.Request) str // buildVersion is set during build using -ldflags var buildVersion = "development" -// editionValue identifies the MCPProxy edition (personal or teams). +// editionValue identifies the MCPProxy edition (personal or server). var editionValue = "personal" // GetBuildVersion returns the build version from build-time variables. diff --git a/internal/runtime/activity_service.go b/internal/runtime/activity_service.go index c5bb652b8..012367d53 100644 --- a/internal/runtime/activity_service.go +++ b/internal/runtime/activity_service.go @@ -329,7 +329,7 @@ func (s *ActivityService) handleToolCallCompleted(evt Event) { ResponseBytes: responseBytes, } - // Extract user identity from auth metadata injected into arguments (teams edition) + // Extract user identity from auth metadata injected into arguments (server edition) if arguments != nil { if userID, ok := arguments["_auth_user_id"].(string); ok && userID != "" { record.UserID = userID @@ -569,7 +569,7 @@ func (s *ActivityService) handleInternalToolCall(evt Event) { RequestID: requestID, } - // Extract user identity from auth metadata injected into arguments (teams edition) + // Extract user identity from auth metadata injected into arguments (server edition) if arguments != nil { if userID, ok := arguments["_auth_user_id"].(string); ok && userID != "" { record.UserID = userID diff --git a/internal/server/mcp.go b/internal/server/mcp.go index d4ef270ac..a391fe8c6 100644 --- a/internal/server/mcp.go +++ b/internal/server/mcp.go @@ -350,7 +350,7 @@ func getAuthMetadata(ctx context.Context) map[string]string { meta["agent_name"] = authCtx.AgentName meta["token_prefix"] = authCtx.TokenPrefix } - // Include user identity for teams session-based auth + // Include user identity for server edition session-based auth if authCtx.UserID != "" { meta["user_id"] = authCtx.UserID } diff --git a/internal/server/server.go b/internal/server/server.go index 1197616bc..6b5cee686 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1845,8 +1845,8 @@ func (s *Server) startCustomHTTPServer(ctx context.Context, streamableServer *se } s.securityScanner = secService } - // Wire teams multi-user OAuth (no-op in personal edition) - wireTeamsOAuth(s, httpAPIServer) + // Wire server edition multi-user OAuth (no-op in personal edition) + wireServerEditionOAuth(s, httpAPIServer) mux.Handle("/api/", httpAPIServer) mux.Handle("/events", httpAPIServer) diff --git a/internal/server/teams_wire.go b/internal/server/serveredition_wire.go similarity index 71% rename from internal/server/teams_wire.go rename to internal/server/serveredition_wire.go index 15aced546..622ef2e58 100644 --- a/internal/server/teams_wire.go +++ b/internal/server/serveredition_wire.go @@ -6,12 +6,12 @@ import ( "go.uber.org/zap" "github.com/smart-mcp-proxy/mcpproxy-go/internal/httpapi" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition" ) -// wireTeamsOAuth sets up server edition multi-user OAuth routes on the HTTP API server. +// wireServerEditionOAuth sets up server edition multi-user OAuth routes on the HTTP API server. // This is called during server initialization after the HTTP API server is created. -func wireTeamsOAuth(s *Server, httpAPIServer *httpapi.Server) { +func wireServerEditionOAuth(s *Server, httpAPIServer *httpapi.Server) { cfg := s.runtime.Config() if cfg == nil { s.logger.Debug("Server OAuth wiring skipped: no config available") @@ -24,7 +24,7 @@ func wireTeamsOAuth(s *Server, httpAPIServer *httpapi.Server) { return } - deps := teams.Dependencies{ + deps := serveredition.Dependencies{ Router: httpAPIServer.Router(), DB: sm.GetDB(), Logger: s.logger.Sugar(), @@ -34,7 +34,7 @@ func wireTeamsOAuth(s *Server, httpAPIServer *httpapi.Server) { StorageManager: sm, } - if err := teams.SetupAll(deps); err != nil { + if err := serveredition.SetupAll(deps); err != nil { s.logger.Error("Failed to initialize server features", zap.Error(err)) } } diff --git a/internal/server/serveredition_wire_stub.go b/internal/server/serveredition_wire_stub.go new file mode 100644 index 000000000..bce39274b --- /dev/null +++ b/internal/server/serveredition_wire_stub.go @@ -0,0 +1,10 @@ +//go:build !server + +package server + +import ( + "github.com/smart-mcp-proxy/mcpproxy-go/internal/httpapi" +) + +// wireServerEditionOAuth is a no-op in the personal edition. +func wireServerEditionOAuth(_ *Server, _ *httpapi.Server) {} diff --git a/internal/server/teams_wire_stub.go b/internal/server/teams_wire_stub.go deleted file mode 100644 index 2f3aa7bb1..000000000 --- a/internal/server/teams_wire_stub.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !server - -package server - -import ( - "github.com/smart-mcp-proxy/mcpproxy-go/internal/httpapi" -) - -// wireTeamsOAuth is a no-op in the personal edition. -func wireTeamsOAuth(_ *Server, _ *httpapi.Server) {} diff --git a/internal/teams/api/admin_handlers.go b/internal/serveredition/api/admin_handlers.go similarity index 98% rename from internal/teams/api/admin_handlers.go rename to internal/serveredition/api/admin_handlers.go index 0d531f197..6bcefc6d8 100644 --- a/internal/teams/api/admin_handlers.go +++ b/internal/serveredition/api/admin_handlers.go @@ -16,9 +16,9 @@ import ( "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" "github.com/smart-mcp-proxy/mcpproxy-go/internal/contracts" - teamsauth "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/auth" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/multiuser" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + teamsauth "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/auth" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/multiuser" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) // AdminHandlers provides admin-only REST endpoints. diff --git a/internal/teams/api/admin_handlers_test.go b/internal/serveredition/api/admin_handlers_test.go similarity index 97% rename from internal/teams/api/admin_handlers_test.go rename to internal/serveredition/api/admin_handlers_test.go index 992023d25..5ccd0d3fd 100644 --- a/internal/teams/api/admin_handlers_test.go +++ b/internal/serveredition/api/admin_handlers_test.go @@ -17,10 +17,10 @@ import ( "go.uber.org/zap" "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" + teamsauth "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/auth" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/multiuser" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" "github.com/smart-mcp-proxy/mcpproxy-go/internal/storage" - teamsauth "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/auth" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/multiuser" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" ) const testAdminUserID = "01HTEST000000000000000ADMN" diff --git a/internal/teams/api/admin_integration_test.go b/internal/serveredition/api/admin_integration_test.go similarity index 99% rename from internal/teams/api/admin_integration_test.go rename to internal/serveredition/api/admin_integration_test.go index 7943675a6..1bb0afbe2 100644 --- a/internal/teams/api/admin_integration_test.go +++ b/internal/serveredition/api/admin_integration_test.go @@ -13,8 +13,8 @@ import ( "github.com/stretchr/testify/require" "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" "github.com/smart-mcp-proxy/mcpproxy-go/internal/storage" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" ) func TestIntegration_AdminViewsAllUsersActivity(t *testing.T) { diff --git a/internal/teams/api/auth_endpoints.go b/internal/serveredition/api/auth_endpoints.go similarity index 94% rename from internal/teams/api/auth_endpoints.go rename to internal/serveredition/api/auth_endpoints.go index cc3c1f7d0..873fbf68d 100644 --- a/internal/teams/api/auth_endpoints.go +++ b/internal/serveredition/api/auth_endpoints.go @@ -11,15 +11,15 @@ import ( "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - teamsauth "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/auth" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + teamsauth "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/auth" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) // AuthEndpoints provides authentication-related REST endpoints. type AuthEndpoints struct { userStore *users.UserStore sessionManager *teamsauth.SessionManager - teamsConfig *config.TeamsConfig + teamsConfig *config.ServerEditionConfig hmacKey []byte logger *zap.SugaredLogger } @@ -28,7 +28,7 @@ type AuthEndpoints struct { func NewAuthEndpoints( userStore *users.UserStore, sessionManager *teamsauth.SessionManager, - teamsConfig *config.TeamsConfig, + teamsConfig *config.ServerEditionConfig, hmacKey []byte, logger *zap.SugaredLogger, ) *AuthEndpoints { diff --git a/internal/teams/api/auth_endpoints_test.go b/internal/serveredition/api/auth_endpoints_test.go similarity index 96% rename from internal/teams/api/auth_endpoints_test.go rename to internal/serveredition/api/auth_endpoints_test.go index 59d8234dd..6a1262ab9 100644 --- a/internal/teams/api/auth_endpoints_test.go +++ b/internal/serveredition/api/auth_endpoints_test.go @@ -18,8 +18,8 @@ import ( "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - teamsauth "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/auth" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + teamsauth "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/auth" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) // authTestSetup creates AuthEndpoints backed by a temporary BBolt database. @@ -35,7 +35,7 @@ func authTestSetup(t *testing.T) (*AuthEndpoints, *users.UserStore) { require.NoError(t, store.EnsureBuckets()) sessionManager := teamsauth.NewSessionManager(store, 24*time.Hour, false) - teamsConfig := config.DefaultTeamsConfig() + teamsConfig := config.DefaultServerEditionConfig() hmacKey := []byte("test-hmac-key-32-bytes-long!!!!!!") logger := zap.NewNop().Sugar() diff --git a/internal/teams/api/user_activity.go b/internal/serveredition/api/user_activity.go similarity index 97% rename from internal/teams/api/user_activity.go rename to internal/serveredition/api/user_activity.go index ba30fbcee..04621f0c7 100644 --- a/internal/teams/api/user_activity.go +++ b/internal/serveredition/api/user_activity.go @@ -10,8 +10,8 @@ import ( "go.uber.org/zap" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/multiuser" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/multiuser" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) // UserActivityHandlers provides endpoints for user activity and diagnostics. diff --git a/internal/teams/api/user_activity_test.go b/internal/serveredition/api/user_activity_test.go similarity index 97% rename from internal/teams/api/user_activity_test.go rename to internal/serveredition/api/user_activity_test.go index 1fb2e11e6..d89e61759 100644 --- a/internal/teams/api/user_activity_test.go +++ b/internal/serveredition/api/user_activity_test.go @@ -18,9 +18,9 @@ import ( "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/multiuser" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" "github.com/smart-mcp-proxy/mcpproxy-go/internal/storage" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/multiuser" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" ) // mockActivityProvider implements multiuser.ActivityStorageProvider for tests. diff --git a/internal/teams/api/user_handlers.go b/internal/serveredition/api/user_handlers.go similarity index 99% rename from internal/teams/api/user_handlers.go rename to internal/serveredition/api/user_handlers.go index 19eb23d78..b7101b39e 100644 --- a/internal/teams/api/user_handlers.go +++ b/internal/serveredition/api/user_handlers.go @@ -14,7 +14,7 @@ import ( "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) // UserHandlers provides REST endpoints for user server management. diff --git a/internal/teams/api/user_handlers_test.go b/internal/serveredition/api/user_handlers_test.go similarity index 99% rename from internal/teams/api/user_handlers_test.go rename to internal/serveredition/api/user_handlers_test.go index 747fed271..4ec119745 100644 --- a/internal/teams/api/user_handlers_test.go +++ b/internal/serveredition/api/user_handlers_test.go @@ -19,7 +19,7 @@ import ( "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) const testUserID = "01HTEST000000000000000USER" diff --git a/internal/teams/auth/idp_subject_token.go b/internal/serveredition/auth/idp_subject_token.go similarity index 98% rename from internal/teams/auth/idp_subject_token.go rename to internal/serveredition/auth/idp_subject_token.go index a5e5c637e..47d22a5f7 100644 --- a/internal/teams/auth/idp_subject_token.go +++ b/internal/serveredition/auth/idp_subject_token.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/broker" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/broker" ) // idpSubjectTokenType is the credential Type recorded for persisted IdP subject diff --git a/internal/teams/auth/idp_subject_token_test.go b/internal/serveredition/auth/idp_subject_token_test.go similarity index 96% rename from internal/teams/auth/idp_subject_token_test.go rename to internal/serveredition/auth/idp_subject_token_test.go index 7502ee47e..1d160c896 100644 --- a/internal/teams/auth/idp_subject_token_test.go +++ b/internal/serveredition/auth/idp_subject_token_test.go @@ -14,8 +14,8 @@ import ( "time" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/broker" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/broker" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.etcd.io/bbolt" @@ -113,10 +113,10 @@ func setupIDPTestHandler(t *testing.T, storeIDPTokens bool) (*OAuthHandler, *use t.Cleanup(func() { providerRegistry["google"] = originalFactory }) sessionMgr := NewSessionManager(userStore, time.Hour, false) - teamsCfg := &config.TeamsConfig{ + teamsCfg := &config.ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &config.TeamsOAuthConfig{Provider: "google", ClientID: "cid", ClientSecret: "secret"}, + OAuth: &config.ServerEditionOAuthConfig{Provider: "google", ClientID: "cid", ClientSecret: "secret"}, SessionTTL: config.Duration(time.Hour), BearerTokenTTL: config.Duration(time.Hour), StoreIDPTokens: storeIDPTokens, diff --git a/internal/teams/auth/integration_test.go b/internal/serveredition/auth/integration_test.go similarity index 97% rename from internal/teams/auth/integration_test.go rename to internal/serveredition/auth/integration_test.go index 081cb935b..ad145a7b5 100644 --- a/internal/teams/auth/integration_test.go +++ b/internal/serveredition/auth/integration_test.go @@ -20,7 +20,7 @@ import ( coreauth "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) // ---------- mock OAuth provider ---------- @@ -88,17 +88,17 @@ type integrationSetup struct { oauthHandler *OAuthHandler sessionManager *SessionManager userStore *users.UserStore - teamsConfig *config.TeamsConfig + teamsConfig *config.ServerEditionConfig hmacKey []byte } // setupIntegration creates the full stack: // - temp BBolt DB + user store // - mock OAuth provider -// - OAuthHandler + SessionManager + TeamsAuthMiddleware +// - OAuthHandler + SessionManager + ServerEditionAuthMiddleware // - chi router wired up with login / callback / logout / me / token endpoints // - httptest.Server serving the chi router -func setupIntegration(t *testing.T, email, name, userSub string, oauthCfg *config.TeamsOAuthConfig, adminEmails []string) *integrationSetup { +func setupIntegration(t *testing.T, email, name, userSub string, oauthCfg *config.ServerEditionOAuthConfig, adminEmails []string) *integrationSetup { t.Helper() // -- Database -- @@ -135,7 +135,7 @@ func setupIntegration(t *testing.T, email, name, userSub string, oauthCfg *confi } sessionTTL := 24 * time.Hour bearerTTL := 24 * time.Hour - teamsCfg := &config.TeamsConfig{ + teamsCfg := &config.ServerEditionConfig{ Enabled: true, AdminEmails: adminEmails, OAuth: oauthCfg, @@ -149,7 +149,7 @@ func setupIntegration(t *testing.T, email, name, userSub string, oauthCfg *confi // -- Components -- sessionMgr := NewSessionManager(store, sessionTTL, false) oauthH := NewOAuthHandler(store, sessionMgr, teamsCfg, hmacKey, logger) - authMW := NewTeamsAuthMiddleware(sessionMgr, store, teamsCfg, hmacKey, logger) + authMW := NewServerEditionAuthMiddleware(sessionMgr, store, teamsCfg, hmacKey, logger) // -- Router -- r := chi.NewRouter() @@ -227,7 +227,7 @@ func setupIntegration(t *testing.T, email, name, userSub string, oauthCfg *confi func TestIntegration_FullOAuthFlow(t *testing.T) { s := setupIntegration(t, "alice@example.com", "Alice Test", "google-sub-alice", - &config.TeamsOAuthConfig{ + &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "mock-client-id", ClientSecret: "mock-client-secret", @@ -381,7 +381,7 @@ func TestIntegration_FullOAuthFlow(t *testing.T) { func TestIntegration_AdminUser(t *testing.T) { s := setupIntegration(t, "admin@test.com", "Admin Boss", "google-sub-admin", - &config.TeamsOAuthConfig{ + &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "mock-client-id", ClientSecret: "mock-client-secret", @@ -442,7 +442,7 @@ func TestIntegration_AdminUser(t *testing.T) { func TestIntegration_DomainRestriction(t *testing.T) { s := setupIntegration(t, "user@forbidden.org", "Forbidden User", "google-sub-forbidden", - &config.TeamsOAuthConfig{ + &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "mock-client-id", ClientSecret: "mock-client-secret", @@ -487,7 +487,7 @@ func TestIntegration_ExpiredSession(t *testing.T) { // past expiry so the test does not need to sleep. s := setupIntegration(t, "alice@example.com", "Alice", "google-sub-alice", - &config.TeamsOAuthConfig{ + &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "mock-client-id", ClientSecret: "mock-client-secret", @@ -550,7 +550,7 @@ func TestIntegration_ExpiredSession(t *testing.T) { func TestIntegration_UnauthenticatedAccess(t *testing.T) { s := setupIntegration(t, "alice@example.com", "Alice", "google-sub-alice", - &config.TeamsOAuthConfig{ + &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "mock-client-id", ClientSecret: "mock-client-secret", @@ -580,7 +580,7 @@ func TestIntegration_BearerTokenSurvivesSessionLogout(t *testing.T) { // signature + user existence, not by session. s := setupIntegration(t, "bob@example.com", "Bob Builder", "google-sub-bob", - &config.TeamsOAuthConfig{ + &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "mock-client-id", ClientSecret: "mock-client-secret", diff --git a/internal/teams/auth/jwt_tokens.go b/internal/serveredition/auth/jwt_tokens.go similarity index 100% rename from internal/teams/auth/jwt_tokens.go rename to internal/serveredition/auth/jwt_tokens.go diff --git a/internal/teams/auth/jwt_tokens_test.go b/internal/serveredition/auth/jwt_tokens_test.go similarity index 100% rename from internal/teams/auth/jwt_tokens_test.go rename to internal/serveredition/auth/jwt_tokens_test.go diff --git a/internal/teams/auth/middleware.go b/internal/serveredition/auth/middleware.go similarity index 85% rename from internal/teams/auth/middleware.go rename to internal/serveredition/auth/middleware.go index 408f70680..aae321e6e 100644 --- a/internal/teams/auth/middleware.go +++ b/internal/serveredition/auth/middleware.go @@ -11,32 +11,32 @@ import ( coreauth "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) // agentTokenPrefix is the prefix for agent tokens, which should not be // treated as JWT bearer tokens. const agentTokenPrefix = "mcp_agt_" -// TeamsAuthMiddleware validates user authentication via session cookies +// ServerEditionAuthMiddleware validates user authentication via session cookies // or JWT bearer tokens (server edition). -type TeamsAuthMiddleware struct { +type ServerEditionAuthMiddleware struct { sessionManager *SessionManager userStore *users.UserStore - teamsConfig *config.TeamsConfig + teamsConfig *config.ServerEditionConfig hmacKey []byte logger *zap.SugaredLogger } -// NewTeamsAuthMiddleware creates a new TeamsAuthMiddleware. -func NewTeamsAuthMiddleware( +// NewServerEditionAuthMiddleware creates a new ServerEditionAuthMiddleware. +func NewServerEditionAuthMiddleware( sessionManager *SessionManager, userStore *users.UserStore, - teamsConfig *config.TeamsConfig, + teamsConfig *config.ServerEditionConfig, hmacKey []byte, logger *zap.SugaredLogger, -) *TeamsAuthMiddleware { - return &TeamsAuthMiddleware{ +) *ServerEditionAuthMiddleware { + return &ServerEditionAuthMiddleware{ sessionManager: sessionManager, userStore: userStore, teamsConfig: teamsConfig, @@ -53,7 +53,7 @@ func NewTeamsAuthMiddleware( // // If neither method yields a valid identity, a 401 JSON error is returned. // On success, the request context is enriched with an AuthContext. -func (m *TeamsAuthMiddleware) Middleware() func(http.Handler) http.Handler { +func (m *ServerEditionAuthMiddleware) Middleware() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 1. Try session cookie @@ -86,7 +86,7 @@ func (m *TeamsAuthMiddleware) Middleware() func(http.Handler) http.Handler { // AdminOnly returns middleware that requires an admin AuthContext. // It must be chained after Middleware() so that the AuthContext is already set. -func (m *TeamsAuthMiddleware) AdminOnly() func(http.Handler) http.Handler { +func (m *ServerEditionAuthMiddleware) AdminOnly() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ac := coreauth.AuthContextFromContext(r.Context()) @@ -105,7 +105,7 @@ func (m *TeamsAuthMiddleware) AdminOnly() func(http.Handler) http.Handler { // authenticateFromSession attempts to validate a session cookie and returns // an AuthContext if successful. Returns (nil, nil) if no session cookie is present. -func (m *TeamsAuthMiddleware) authenticateFromSession(r *http.Request) (*coreauth.AuthContext, error) { +func (m *ServerEditionAuthMiddleware) authenticateFromSession(r *http.Request) (*coreauth.AuthContext, error) { session, err := m.sessionManager.GetSessionFromRequest(r) if err != nil { return nil, err @@ -133,7 +133,7 @@ func (m *TeamsAuthMiddleware) authenticateFromSession(r *http.Request) (*coreaut // authenticateFromBearer attempts to validate a Bearer token from the // Authorization header and returns an AuthContext if successful. // Returns (nil, nil) if no Bearer token is present. -func (m *TeamsAuthMiddleware) authenticateFromBearer(r *http.Request) (*coreauth.AuthContext, error) { +func (m *ServerEditionAuthMiddleware) authenticateFromBearer(r *http.Request) (*coreauth.AuthContext, error) { authHeader := r.Header.Get("Authorization") if !strings.HasPrefix(authHeader, "Bearer ") { return nil, nil @@ -177,7 +177,7 @@ func (m *TeamsAuthMiddleware) authenticateFromBearer(r *http.Request) (*coreauth // buildAuthContext creates an AuthContext for the given user, determining the // role from the server config admin email list. -func (m *TeamsAuthMiddleware) buildAuthContext(user *users.User) *coreauth.AuthContext { +func (m *ServerEditionAuthMiddleware) buildAuthContext(user *users.User) *coreauth.AuthContext { if m.teamsConfig.IsAdminEmail(user.Email) { return coreauth.AdminUserContext(user.ID, user.Email, user.DisplayName, user.Provider) } diff --git a/internal/teams/auth/middleware_test.go b/internal/serveredition/auth/middleware_test.go similarity index 97% rename from internal/teams/auth/middleware_test.go rename to internal/serveredition/auth/middleware_test.go index a14c48208..b31adef79 100644 --- a/internal/teams/auth/middleware_test.go +++ b/internal/serveredition/auth/middleware_test.go @@ -17,7 +17,7 @@ import ( coreauth "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) // testMiddlewareSetup holds all components needed for middleware tests. @@ -25,8 +25,8 @@ type testMiddlewareSetup struct { db *bbolt.DB userStore *users.UserStore sessionManager *SessionManager - middleware *TeamsAuthMiddleware - teamsConfig *config.TeamsConfig + middleware *ServerEditionAuthMiddleware + teamsConfig *config.ServerEditionConfig hmacKey []byte testUser *users.User adminUser *users.User @@ -34,7 +34,7 @@ type testMiddlewareSetup struct { } // setupMiddlewareTest creates a temporary BBolt DB, user store, session manager, -// test users, and a TeamsAuthMiddleware instance. +// test users, and a ServerEditionAuthMiddleware instance. func setupMiddlewareTest(t *testing.T) *testMiddlewareSetup { t.Helper() @@ -70,8 +70,8 @@ func setupMiddlewareTest(t *testing.T) *testMiddlewareSetup { t.Fatalf("failed to create disabled user: %v", err) } - // Teams config with admin@example.com as admin - teamsConfig := &config.TeamsConfig{ + // Server edition config with admin@example.com as admin + teamsConfig := &config.ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, } @@ -86,7 +86,7 @@ func setupMiddlewareTest(t *testing.T) *testMiddlewareSetup { // Logger (discard output in tests) logger := zap.NewNop().Sugar() - mw := NewTeamsAuthMiddleware(sessionManager, userStore, teamsConfig, hmacKey, logger) + mw := NewServerEditionAuthMiddleware(sessionManager, userStore, teamsConfig, hmacKey, logger) return &testMiddlewareSetup{ db: db, diff --git a/internal/teams/auth/oauth_handler.go b/internal/serveredition/auth/oauth_handler.go similarity index 98% rename from internal/teams/auth/oauth_handler.go rename to internal/serveredition/auth/oauth_handler.go index fcdbbb113..07723e1d3 100644 --- a/internal/teams/auth/oauth_handler.go +++ b/internal/serveredition/auth/oauth_handler.go @@ -17,15 +17,15 @@ import ( "go.uber.org/zap" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/broker" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/broker" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) // OAuthHandler handles the OAuth login/callback/logout HTTP endpoints. type OAuthHandler struct { userStore *users.UserStore sessionManager *SessionManager - config *config.TeamsConfig + config *config.ServerEditionConfig hmacKey []byte logger *zap.SugaredLogger @@ -58,7 +58,7 @@ const stateMaxAge = 10 * time.Minute func NewOAuthHandler( userStore *users.UserStore, sessionManager *SessionManager, - cfg *config.TeamsConfig, + cfg *config.ServerEditionConfig, hmacKey []byte, logger *zap.SugaredLogger, ) *OAuthHandler { diff --git a/internal/teams/auth/oauth_handler_test.go b/internal/serveredition/auth/oauth_handler_test.go similarity index 94% rename from internal/teams/auth/oauth_handler_test.go rename to internal/serveredition/auth/oauth_handler_test.go index c5169b3fa..37591df42 100644 --- a/internal/teams/auth/oauth_handler_test.go +++ b/internal/serveredition/auth/oauth_handler_test.go @@ -13,7 +13,7 @@ import ( "time" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.etcd.io/bbolt" @@ -22,7 +22,7 @@ import ( // setupTestOAuthHandler creates an OAuthHandler with a real BBolt store // and a mock OAuth provider server. -func setupTestOAuthHandler(t *testing.T, oauthCfg *config.TeamsOAuthConfig) (*OAuthHandler, *users.UserStore) { +func setupTestOAuthHandler(t *testing.T, oauthCfg *config.ServerEditionOAuthConfig) (*OAuthHandler, *users.UserStore) { t.Helper() tmpFile := filepath.Join(t.TempDir(), "test.db") @@ -35,7 +35,7 @@ func setupTestOAuthHandler(t *testing.T, oauthCfg *config.TeamsOAuthConfig) (*OA sessionMgr := NewSessionManager(store, time.Hour, false) - teamsCfg := &config.TeamsConfig{ + teamsCfg := &config.ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, OAuth: oauthCfg, @@ -121,7 +121,7 @@ func TestHandleLogin_Redirects(t *testing.T) { mockServer := mockOAuthProviderServer(t, "user@example.com", "Test User", "sub-123") registerMockProvider(t, mockServer) - handler, _ := setupTestOAuthHandler(t, &config.TeamsOAuthConfig{ + handler, _ := setupTestOAuthHandler(t, &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "test-client-id", ClientSecret: "test-client-secret", @@ -166,7 +166,7 @@ func TestHandleLogin_RequestsOfflineAccess(t *testing.T) { mockServer := mockOAuthProviderServer(t, "user@example.com", "Test User", "sub-123") registerMockProvider(t, mockServer) - handler, _ := setupTestOAuthHandler(t, &config.TeamsOAuthConfig{ + handler, _ := setupTestOAuthHandler(t, &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "test-client-id", ClientSecret: "test-client-secret", @@ -195,7 +195,7 @@ func TestHandleLogin_StateInURL(t *testing.T) { mockServer := mockOAuthProviderServer(t, "user@example.com", "Test User", "sub-123") registerMockProvider(t, mockServer) - handler, _ := setupTestOAuthHandler(t, &config.TeamsOAuthConfig{ + handler, _ := setupTestOAuthHandler(t, &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "test-client-id", ClientSecret: "test-client-secret", @@ -235,7 +235,7 @@ func TestHandleCallback_Success(t *testing.T) { mockServer := mockOAuthProviderServer(t, "user@example.com", "Test User", "sub-123") registerMockProvider(t, mockServer) - handler, store := setupTestOAuthHandler(t, &config.TeamsOAuthConfig{ + handler, store := setupTestOAuthHandler(t, &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "test-client-id", ClientSecret: "test-client-secret", @@ -297,7 +297,7 @@ func TestHandleCallback_InvalidState(t *testing.T) { mockServer := mockOAuthProviderServer(t, "user@example.com", "Test User", "sub-123") registerMockProvider(t, mockServer) - handler, _ := setupTestOAuthHandler(t, &config.TeamsOAuthConfig{ + handler, _ := setupTestOAuthHandler(t, &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "test-client-id", ClientSecret: "test-client-secret", @@ -324,7 +324,7 @@ func TestHandleCallback_MissingCode(t *testing.T) { mockServer := mockOAuthProviderServer(t, "user@example.com", "Test User", "sub-123") registerMockProvider(t, mockServer) - handler, _ := setupTestOAuthHandler(t, &config.TeamsOAuthConfig{ + handler, _ := setupTestOAuthHandler(t, &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "test-client-id", ClientSecret: "test-client-secret", @@ -351,7 +351,7 @@ func TestHandleCallback_DomainNotAllowed(t *testing.T) { mockServer := mockOAuthProviderServer(t, "user@unauthorized.com", "Test User", "sub-123") registerMockProvider(t, mockServer) - handler, _ := setupTestOAuthHandler(t, &config.TeamsOAuthConfig{ + handler, _ := setupTestOAuthHandler(t, &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "test-client-id", ClientSecret: "test-client-secret", @@ -390,7 +390,7 @@ func TestHandleCallback_ExistingUser(t *testing.T) { mockServer := mockOAuthProviderServer(t, "existing@example.com", "Updated Name", "sub-existing") registerMockProvider(t, mockServer) - handler, store := setupTestOAuthHandler(t, &config.TeamsOAuthConfig{ + handler, store := setupTestOAuthHandler(t, &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "test-client-id", ClientSecret: "test-client-secret", @@ -451,7 +451,7 @@ func TestHandleCallback_ExistingUser(t *testing.T) { } func TestHandleLogout_ClearsCookie(t *testing.T) { - handler, store := setupTestOAuthHandler(t, &config.TeamsOAuthConfig{ + handler, store := setupTestOAuthHandler(t, &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "test-client-id", ClientSecret: "test-client-secret", @@ -505,7 +505,7 @@ func TestHandleLogout_ClearsCookie(t *testing.T) { } func TestHandleLogout_NoSession(t *testing.T) { - handler, _ := setupTestOAuthHandler(t, &config.TeamsOAuthConfig{ + handler, _ := setupTestOAuthHandler(t, &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "test-client-id", ClientSecret: "test-client-secret", @@ -529,7 +529,7 @@ func TestHandleLogout_NoSession(t *testing.T) { } func TestCleanupStaleStates(t *testing.T) { - handler, _ := setupTestOAuthHandler(t, &config.TeamsOAuthConfig{ + handler, _ := setupTestOAuthHandler(t, &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "test-client-id", ClientSecret: "test-client-secret", @@ -614,7 +614,7 @@ func TestIsDomainAllowed(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - handler, _ := setupTestOAuthHandler(t, &config.TeamsOAuthConfig{ + handler, _ := setupTestOAuthHandler(t, &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "test-client-id", ClientSecret: "test-client-secret", diff --git a/internal/teams/auth/oauth_providers.go b/internal/serveredition/auth/oauth_providers.go similarity index 100% rename from internal/teams/auth/oauth_providers.go rename to internal/serveredition/auth/oauth_providers.go diff --git a/internal/teams/auth/oauth_providers_test.go b/internal/serveredition/auth/oauth_providers_test.go similarity index 100% rename from internal/teams/auth/oauth_providers_test.go rename to internal/serveredition/auth/oauth_providers_test.go diff --git a/internal/teams/auth/session_store.go b/internal/serveredition/auth/session_store.go similarity index 98% rename from internal/teams/auth/session_store.go rename to internal/serveredition/auth/session_store.go index fce3c4b74..b01a2fb7f 100644 --- a/internal/teams/auth/session_store.go +++ b/internal/serveredition/auth/session_store.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) const ( diff --git a/internal/teams/auth/session_store_test.go b/internal/serveredition/auth/session_store_test.go similarity index 99% rename from internal/teams/auth/session_store_test.go rename to internal/serveredition/auth/session_store_test.go index f63601cde..fb9f22584 100644 --- a/internal/teams/auth/session_store_test.go +++ b/internal/serveredition/auth/session_store_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.etcd.io/bbolt" diff --git a/internal/teams/broker/bbolt_aes.go b/internal/serveredition/broker/bbolt_aes.go similarity index 98% rename from internal/teams/broker/bbolt_aes.go rename to internal/serveredition/broker/bbolt_aes.go index ba88588cc..afa3e052c 100644 --- a/internal/teams/broker/bbolt_aes.go +++ b/internal/serveredition/broker/bbolt_aes.go @@ -60,7 +60,7 @@ func NewBBoltAESStore(db *bbolt.DB, base64Key string, logger *zap.Logger) (*BBol if strings.TrimSpace(base64Key) == "" { logger.Warn("upstream credential broker disabled: no encryption key configured " + - "(set MCPPROXY_CRED_KEY or teams.credential_encryption_key to enable)") + "(set MCPPROXY_CRED_KEY or server_edition.credential_encryption_key to enable)") return &BBoltAESStore{db: db, enabled: false, logger: logger}, nil } diff --git a/internal/teams/broker/credential_store.go b/internal/serveredition/broker/credential_store.go similarity index 98% rename from internal/teams/broker/credential_store.go rename to internal/serveredition/broker/credential_store.go index e2d6b24dd..48e88a425 100644 --- a/internal/teams/broker/credential_store.go +++ b/internal/serveredition/broker/credential_store.go @@ -128,7 +128,7 @@ var _ CredentialStore = (*BBoltAESStore)(nil) // ResolveMasterKey returns the base64-encoded master key, preferring the // MCPPROXY_CRED_KEY environment variable over the supplied configuration value -// (e.g. teams.credential_encryption_key). Returns "" when neither is set, in +// (e.g. server_edition.credential_encryption_key). Returns "" when neither is set, in // which case the store is disabled. func ResolveMasterKey(configKey string) string { if v := os.Getenv(MasterKeyEnvVar); v != "" { diff --git a/internal/teams/broker/credential_store_test.go b/internal/serveredition/broker/credential_store_test.go similarity index 100% rename from internal/teams/broker/credential_store_test.go rename to internal/serveredition/broker/credential_store_test.go diff --git a/internal/teams/broker/token_exchanger.go b/internal/serveredition/broker/token_exchanger.go similarity index 100% rename from internal/teams/broker/token_exchanger.go rename to internal/serveredition/broker/token_exchanger.go diff --git a/internal/teams/broker/token_exchanger_test.go b/internal/serveredition/broker/token_exchanger_test.go similarity index 100% rename from internal/teams/broker/token_exchanger_test.go rename to internal/serveredition/broker/token_exchanger_test.go diff --git a/internal/teams/doc.go b/internal/serveredition/doc.go similarity index 64% rename from internal/teams/doc.go rename to internal/serveredition/doc.go index d93b83a16..bbc244188 100644 --- a/internal/teams/doc.go +++ b/internal/serveredition/doc.go @@ -1,6 +1,6 @@ //go:build server -// Package teams provides multi-user server edition features for MCPProxy. +// Package serveredition provides multi-user server edition features for MCPProxy. // This package and all sub-packages are only compiled when the "server" build tag is set. // The personal edition of MCPProxy does not include any server edition code. -package teams +package serveredition diff --git a/internal/teams/multiuser/activity.go b/internal/serveredition/multiuser/activity.go similarity index 100% rename from internal/teams/multiuser/activity.go rename to internal/serveredition/multiuser/activity.go diff --git a/internal/teams/multiuser/activity_test.go b/internal/serveredition/multiuser/activity_test.go similarity index 100% rename from internal/teams/multiuser/activity_test.go rename to internal/serveredition/multiuser/activity_test.go diff --git a/internal/teams/multiuser/isolation_test.go b/internal/serveredition/multiuser/isolation_test.go similarity index 100% rename from internal/teams/multiuser/isolation_test.go rename to internal/serveredition/multiuser/isolation_test.go diff --git a/internal/teams/multiuser/router.go b/internal/serveredition/multiuser/router.go similarity index 98% rename from internal/teams/multiuser/router.go rename to internal/serveredition/multiuser/router.go index c7a151108..bff27e015 100644 --- a/internal/teams/multiuser/router.go +++ b/internal/serveredition/multiuser/router.go @@ -12,7 +12,7 @@ import ( "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/workspace" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/workspace" ) // ServerOwnership indicates who owns a server. diff --git a/internal/teams/multiuser/router_test.go b/internal/serveredition/multiuser/router_test.go similarity index 98% rename from internal/teams/multiuser/router_test.go rename to internal/serveredition/multiuser/router_test.go index 072e79876..ccce95f6d 100644 --- a/internal/teams/multiuser/router_test.go +++ b/internal/serveredition/multiuser/router_test.go @@ -10,8 +10,8 @@ import ( "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/workspace" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/workspace" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.etcd.io/bbolt" diff --git a/internal/teams/multiuser/tool_filter.go b/internal/serveredition/multiuser/tool_filter.go similarity index 100% rename from internal/teams/multiuser/tool_filter.go rename to internal/serveredition/multiuser/tool_filter.go diff --git a/internal/teams/registry.go b/internal/serveredition/registry.go similarity index 98% rename from internal/teams/registry.go rename to internal/serveredition/registry.go index c2e1e5261..7fefd1cf5 100644 --- a/internal/teams/registry.go +++ b/internal/serveredition/registry.go @@ -1,6 +1,6 @@ //go:build server -package teams +package serveredition import ( "fmt" diff --git a/internal/teams/registry_test.go b/internal/serveredition/registry_test.go similarity index 98% rename from internal/teams/registry_test.go rename to internal/serveredition/registry_test.go index 95bebc64b..ea44353eb 100644 --- a/internal/teams/registry_test.go +++ b/internal/serveredition/registry_test.go @@ -1,6 +1,6 @@ //go:build server -package teams +package serveredition import ( "errors" diff --git a/internal/teams/setup.go b/internal/serveredition/setup.go similarity index 85% rename from internal/teams/setup.go rename to internal/serveredition/setup.go index ffb6a1943..7f684cd3b 100644 --- a/internal/teams/setup.go +++ b/internal/serveredition/setup.go @@ -1,6 +1,6 @@ //go:build server -package teams +package serveredition import ( "fmt" @@ -10,10 +10,10 @@ import ( "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - teamsapi "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/api" - teamsauth "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/auth" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/broker" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + teamsapi "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/api" + teamsauth "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/auth" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/broker" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) func init() { @@ -24,12 +24,12 @@ func init() { } func setupMultiUserOAuth(deps Dependencies) error { - if deps.Config == nil || deps.Config.Teams == nil || !deps.Config.Teams.Enabled { + if deps.Config == nil || deps.Config.ServerEdition == nil || !deps.Config.ServerEdition.Enabled { deps.Logger.Debug("Server multi-user OAuth: not enabled, skipping setup") return nil } - cfg := deps.Config.Teams + cfg := deps.Config.ServerEdition // Validate server config if err := cfg.Validate(); err != nil { @@ -69,7 +69,7 @@ func setupMultiUserOAuth(deps Dependencies) error { oauthHandler.SetCredentialStore(credStore) // Create auth middleware - authMiddleware := teamsauth.NewTeamsAuthMiddleware(sessionManager, userStore, cfg, hmacKey, deps.Logger) + authMiddleware := teamsauth.NewServerEditionAuthMiddleware(sessionManager, userStore, cfg, hmacKey, deps.Logger) // Register OAuth routes on the router. // Login and callback are public (no auth required). diff --git a/internal/teams/setup_test.go b/internal/serveredition/setup_test.go similarity index 92% rename from internal/teams/setup_test.go rename to internal/serveredition/setup_test.go index dd7375561..e33a579d9 100644 --- a/internal/teams/setup_test.go +++ b/internal/serveredition/setup_test.go @@ -1,6 +1,6 @@ //go:build server -package teams +package serveredition import ( "net/http" @@ -17,7 +17,7 @@ import ( ) func TestSetupMultiUserOAuth_Disabled(t *testing.T) { - // When teams is not enabled, setup should be a no-op + // When server edition is not enabled, setup should be a no-op logger := zap.NewNop().Sugar() router := chi.NewRouter() @@ -25,7 +25,7 @@ func TestSetupMultiUserOAuth_Disabled(t *testing.T) { Router: router, Logger: logger, Config: &config.Config{ - Teams: &config.TeamsConfig{ + ServerEdition: &config.ServerEditionConfig{ Enabled: false, }, }, @@ -53,7 +53,7 @@ func TestSetupMultiUserOAuth_NilConfig(t *testing.T) { } } -func TestSetupMultiUserOAuth_NilTeamsConfig(t *testing.T) { +func TestSetupMultiUserOAuth_NilServerEditionConfig(t *testing.T) { logger := zap.NewNop().Sugar() router := chi.NewRouter() @@ -61,7 +61,7 @@ func TestSetupMultiUserOAuth_NilTeamsConfig(t *testing.T) { Router: router, Logger: logger, Config: &config.Config{ - Teams: nil, + ServerEdition: nil, }, } @@ -80,7 +80,7 @@ func TestSetupMultiUserOAuth_InvalidConfig(t *testing.T) { Router: router, Logger: logger, Config: &config.Config{ - Teams: &config.TeamsConfig{ + ServerEdition: &config.ServerEditionConfig{ Enabled: true, AdminEmails: nil, // Missing admin emails }, @@ -118,12 +118,12 @@ func TestSetupMultiUserOAuth_RegistersRoutes(t *testing.T) { Logger: logger, DataDir: tmpDir, Config: &config.Config{ - Teams: &config.TeamsConfig{ + ServerEdition: &config.ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, SessionTTL: config.Duration(24 * time.Hour), BearerTokenTTL: config.Duration(24 * time.Hour), - OAuth: &config.TeamsOAuthConfig{ + OAuth: &config.ServerEditionOAuthConfig{ Provider: "google", ClientID: "test-client-id", ClientSecret: "test-client-secret", diff --git a/internal/teams/users/models.go b/internal/serveredition/users/models.go similarity index 100% rename from internal/teams/users/models.go rename to internal/serveredition/users/models.go diff --git a/internal/teams/users/models_test.go b/internal/serveredition/users/models_test.go similarity index 100% rename from internal/teams/users/models_test.go rename to internal/serveredition/users/models_test.go diff --git a/internal/teams/users/server_store.go b/internal/serveredition/users/server_store.go similarity index 100% rename from internal/teams/users/server_store.go rename to internal/serveredition/users/server_store.go diff --git a/internal/teams/users/store.go b/internal/serveredition/users/store.go similarity index 99% rename from internal/teams/users/store.go rename to internal/serveredition/users/store.go index e42d16c0e..146b8e30a 100644 --- a/internal/teams/users/store.go +++ b/internal/serveredition/users/store.go @@ -11,7 +11,7 @@ import ( "go.etcd.io/bbolt" ) -// Bucket names for teams user and session storage. +// Bucket names for server edition user and session storage. const ( BucketUsers = "users" BucketUsersByEmail = "users_by_email" diff --git a/internal/teams/users/store_test.go b/internal/serveredition/users/store_test.go similarity index 100% rename from internal/teams/users/store_test.go rename to internal/serveredition/users/store_test.go diff --git a/internal/teams/workspace/integration_test.go b/internal/serveredition/workspace/integration_test.go similarity index 100% rename from internal/teams/workspace/integration_test.go rename to internal/serveredition/workspace/integration_test.go diff --git a/internal/teams/workspace/manager.go b/internal/serveredition/workspace/manager.go similarity index 98% rename from internal/teams/workspace/manager.go rename to internal/serveredition/workspace/manager.go index 3b71d47e1..e97932117 100644 --- a/internal/teams/workspace/manager.go +++ b/internal/serveredition/workspace/manager.go @@ -8,7 +8,7 @@ import ( "go.uber.org/zap" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) // Manager manages user workspaces, creating them on demand and cleaning up idle ones. diff --git a/internal/teams/workspace/manager_test.go b/internal/serveredition/workspace/manager_test.go similarity index 100% rename from internal/teams/workspace/manager_test.go rename to internal/serveredition/workspace/manager_test.go diff --git a/internal/teams/workspace/workspace.go b/internal/serveredition/workspace/workspace.go similarity index 98% rename from internal/teams/workspace/workspace.go rename to internal/serveredition/workspace/workspace.go index cb0e9743b..f76ca74a0 100644 --- a/internal/teams/workspace/workspace.go +++ b/internal/serveredition/workspace/workspace.go @@ -11,7 +11,7 @@ import ( "go.uber.org/zap" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) // UserWorkspace holds a user's personal server configurations and state. diff --git a/internal/teams/workspace/workspace_test.go b/internal/serveredition/workspace/workspace_test.go similarity index 98% rename from internal/teams/workspace/workspace_test.go rename to internal/serveredition/workspace/workspace_test.go index 28577fce7..070815bd1 100644 --- a/internal/teams/workspace/workspace_test.go +++ b/internal/serveredition/workspace/workspace_test.go @@ -8,7 +8,7 @@ import ( "time" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams/users" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.etcd.io/bbolt" diff --git a/internal/storage/async_ops_test.go b/internal/storage/async_ops_test.go index f70b69b22..cd3f2ddf7 100644 --- a/internal/storage/async_ops_test.go +++ b/internal/storage/async_ops_test.go @@ -242,7 +242,7 @@ func TestSaveServerSyncFieldCoverage(t *testing.T) { "Created": true, "Updated": true, // Updated is set by saveServerSync, not copied "Isolation": true, - "Shared": true, // Teams-only: persisted in JSON config, not in BBolt + "Shared": true, // Server-edition-only: persisted in JSON config, not in BBolt "SkipQuarantine": true, // Spec 032: runtime-only field, not persisted to BBolt "ReconnectOnUse": true, // Spec 354: persisted to BBolt for on-demand reconnection "LauncherWaitTimeout": true, // Spec 046: persisted to BBolt so REST-API-added launcher servers survive restarts @@ -287,7 +287,7 @@ func TestSaveServerSyncFieldCoverage(t *testing.T) { continue } if fieldName == "Shared" { - // Teams-only field, persisted in JSON config not BBolt + // Server-edition-only field, persisted in JSON config not BBolt continue } if fieldName == "SkipQuarantine" { diff --git a/specs/029-mcpproxy-teams/data-model.md b/specs/029-mcpproxy-teams/data-model.md index c6d22c999..3c7c07d77 100644 --- a/specs/029-mcpproxy-teams/data-model.md +++ b/specs/029-mcpproxy-teams/data-model.md @@ -28,7 +28,7 @@ The `/api/v1/status` response gains one field: The teams edition uses a registration pattern for feature modules: ```go -// internal/teams/registry.go +// internal/serveredition/registry.go type Feature struct { Name string Setup func(deps Dependencies) error diff --git a/specs/029-mcpproxy-teams/plan.md b/specs/029-mcpproxy-teams/plan.md index 4e1317358..e358db5eb 100644 --- a/specs/029-mcpproxy-teams/plan.md +++ b/specs/029-mcpproxy-teams/plan.md @@ -37,7 +37,7 @@ Restructure the MCPProxy repository to support two editions (Personal and Teams) |------------|--------|-------| | Core + Tray Split | PASS | Tray split preserved; native/ skeleton added | | Event-Driven Updates | PASS | No event changes | -| DDD Layering | PASS | internal/teams/ follows existing layer patterns | +| DDD Layering | PASS | internal/serveredition/ follows existing layer patterns | | Upstream Client Modularity | PASS | No upstream changes | ## Project Structure @@ -89,7 +89,7 @@ Makefile # Modify: add build-teams, build-docker, build- .github/workflows/release.yml # Modify: add teams build matrix entries ``` -**Structure Decision**: Existing Go project structure preserved. Teams code isolated in `internal/teams/` with build tags. No `pkg/` migration. Native tray apps in `native/` at repo root. +**Structure Decision**: Existing Go project structure preserved. Teams code isolated in `internal/serveredition/` with build tags. No `pkg/` migration. Native tray apps in `native/` at repo root. ## Complexity Tracking diff --git a/specs/029-mcpproxy-teams/spec.md b/specs/029-mcpproxy-teams/spec.md index b286ad416..d64b8aa3c 100644 --- a/specs/029-mcpproxy-teams/spec.md +++ b/specs/029-mcpproxy-teams/spec.md @@ -272,7 +272,7 @@ The proxy provides a web interface where users log in via their identity provide - Agent token maximum per user follows the same limit as the personal edition (10 tokens per user). - Admin designation is static per configuration (email list or IdP claim) — there is no in-app role assignment UI in v1. - Server template definitions are static (shipped with the binary + admin-defined in config) — there is no template marketplace or community sharing. -- Both editions are built from the same repository (`mcpproxy-go`) using Go build tags. Teams-only code lives in `internal/teams/` with `//go:build teams` guards. No `pkg/` migration is needed. +- Both editions are built from the same repository (`mcpproxy-go`) using Go build tags. Teams-only code lives in `internal/serveredition/` with `//go:build teams` guards. No `pkg/` migration is needed. - Data retention for activity logs follows the same policy as the personal edition — configurable, with no default auto-purge. ## Scope Boundaries diff --git a/specs/029-mcpproxy-teams/tasks.md b/specs/029-mcpproxy-teams/tasks.md index 33f76a7ce..6b2e0bc97 100644 --- a/specs/029-mcpproxy-teams/tasks.md +++ b/specs/029-mcpproxy-teams/tasks.md @@ -25,17 +25,17 @@ --- -## Phase 2: Teams Skeleton (internal/teams/) +## Phase 2: Teams Skeleton (internal/serveredition/) **Purpose**: Create the teams feature registry and package skeleton with build tags -- [x] T006 Create `internal/teams/doc.go` with `//go:build teams` tag and package documentation -- [x] T007 Create `internal/teams/registry.go` with Feature struct, Register(), SetupAll() functions (build-tagged) -- [x] T008 Create `internal/teams/registry_test.go` with tests verifying registration and setup (build-tagged) -- [x] T009 Create teams registration entry point `cmd/mcpproxy/teams_register.go` with `//go:build teams` that imports `internal/teams` and calls `SetupAll()` during init +- [x] T006 Create `internal/serveredition/doc.go` with `//go:build teams` tag and package documentation +- [x] T007 Create `internal/serveredition/registry.go` with Feature struct, Register(), SetupAll() functions (build-tagged) +- [x] T008 Create `internal/serveredition/registry_test.go` with tests verifying registration and setup (build-tagged) +- [x] T009 Create teams registration entry point `cmd/mcpproxy/teams_register.go` with `//go:build teams` that imports `internal/serveredition` and calls `SetupAll()` during init - [x] T010 Verify both builds compile: `go build ./cmd/mcpproxy` (no teams code) and `go build -tags teams ./cmd/mcpproxy` (with teams skeleton) -**Checkpoint**: `go test -tags teams ./internal/teams/...` passes; personal build has zero teams code compiled in +**Checkpoint**: `go test -tags teams ./internal/serveredition/...` passes; personal build has zero teams code compiled in --- @@ -92,10 +92,10 @@ **Purpose**: Update project documentation to reflect dual-edition architecture -- [x] T024 Update `CLAUDE.md` — add Build & Distribution section documenting `build-teams`, `build-docker`, edition detection, `internal/teams/` structure +- [x] T024 Update `CLAUDE.md` — add Build & Distribution section documenting `build-teams`, `build-docker`, edition detection, `internal/serveredition/` structure - [x] T025 Update `Makefile` help target to include new build-teams, build-docker, build-deb targets - [x] T026 Verify all existing tests pass: `go test ./internal/... -v` (personal build) — all pass except pre-existing `internal/server` timeout -- [x] T027 Verify teams build tests pass: `go test -tags teams ./internal/teams/... -v` +- [x] T027 Verify teams build tests pass: `go test -tags teams ./internal/serveredition/... -v` - [x] T028 Verify E2E tests pass: `./scripts/test-api-e2e.sh` — 61/71 pass, 10 failures are pre-existing (same on clean branch) - [x] T029 Verify linter passes: `./scripts/run-linter.sh` diff --git a/specs/047-cpu-hotpath-fix/verification/report.md b/specs/047-cpu-hotpath-fix/verification/report.md index 7edce2cf0..77a597d04 100644 --- a/specs/047-cpu-hotpath-fix/verification/report.md +++ b/specs/047-cpu-hotpath-fix/verification/report.md @@ -69,7 +69,7 @@ The Swift tray's existing periodic 30 s refresh and the `status`-event-driven re ```text go test -race ./internal/... ok (all packages) -go test -tags server ./internal/teams/... ok +go test -tags server ./internal/serveredition/... ok go vet ./... ok gofmt -l ok golangci-lint 0 issues From bd4f2b9cd9ee51765274de81eedeff71a53da46a Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Tue, 9 Jun 2026 09:48:12 +0300 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20finish=20Teams=E2=86=92ServerEditio?= =?UTF-8?q?n=20rename=20leftovers=20(MCP-1795)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review follow-up for PR #603 / MCP-1086: - generate-release-notes.sh: server-edition code path internal/teams/ → internal/serveredition/ - specs/029-mcpproxy-teams/tasks.md: build-tag docs //go:build teams / -tags teams → server, matching the accepted scope (build tag remains "server", current code uses //go:build server). Package paths were already renamed by PR #603; only the build-tag references were stale. Edition-name/wording strings left untouched (owned by MCP-1087). Related #603 --- scripts/generate-release-notes.sh | 2 +- specs/029-mcpproxy-teams/tasks.md | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/generate-release-notes.sh b/scripts/generate-release-notes.sh index 0b18e3016..e699b71cb 100755 --- a/scripts/generate-release-notes.sh +++ b/scripts/generate-release-notes.sh @@ -128,7 +128,7 @@ build_prompt() { cat << EOF Generate concise release notes for version ${version} of MCPProxy (Smart MCP Proxy). -MCPProxy is built in two editions from the same codebase: a default Personal edition (desktop app, distributed as DMG / Windows installer / Linux tar.gz / .deb / .rpm) and a Server edition built with the \`server\` Go build tag (Docker image + multi-user OAuth, code under \`internal/teams/\`). Most changes ship in BOTH editions because shared code (\`internal/runtime/\`, \`internal/server/\`, \`internal/upstream/\`, \`frontend/\`, etc.) is compiled into both binaries. +MCPProxy is built in two editions from the same codebase: a default Personal edition (desktop app, distributed as DMG / Windows installer / Linux tar.gz / .deb / .rpm) and a Server edition built with the \`server\` Go build tag (Docker image + multi-user OAuth, code under \`internal/serveredition/\`). Most changes ship in BOTH editions because shared code (\`internal/runtime/\`, \`internal/server/\`, \`internal/upstream/\`, \`frontend/\`, etc.) is compiled into both binaries. Commits since last release: ${commits} diff --git a/specs/029-mcpproxy-teams/tasks.md b/specs/029-mcpproxy-teams/tasks.md index 6b2e0bc97..721c80ba3 100644 --- a/specs/029-mcpproxy-teams/tasks.md +++ b/specs/029-mcpproxy-teams/tasks.md @@ -16,12 +16,12 @@ **Purpose**: Establish the build tag architecture that separates personal and teams editions - [x] T001 [P] Create edition detection file `cmd/mcpproxy/edition.go` with `var Edition = "personal"` and `GetEdition()` function -- [x] T002 [P] Create teams edition override file `cmd/mcpproxy/edition_teams.go` with `//go:build teams` tag that sets `Edition = "teams"` +- [x] T002 [P] Create teams edition override file `cmd/mcpproxy/edition_teams.go` with `//go:build server` tag that sets `Edition = "teams"` - [x] T003 Update `cmd/mcpproxy/main.go` to log edition on startup and pass edition to server initialization - [x] T004 Add edition field to `/api/v1/status` response in `internal/httpapi/server.go` - [x] T005 Add edition to `mcpproxy version` CLI output in `cmd/mcpproxy/main.go` -**Checkpoint**: `go build ./cmd/mcpproxy && ./mcpproxy version` shows "personal"; `go build -tags teams ./cmd/mcpproxy && ./mcpproxy version` shows "teams" +**Checkpoint**: `go build ./cmd/mcpproxy && ./mcpproxy version` shows "personal"; `go build -tags server ./cmd/mcpproxy && ./mcpproxy version` shows "teams" --- @@ -29,13 +29,13 @@ **Purpose**: Create the teams feature registry and package skeleton with build tags -- [x] T006 Create `internal/serveredition/doc.go` with `//go:build teams` tag and package documentation +- [x] T006 Create `internal/serveredition/doc.go` with `//go:build server` tag and package documentation - [x] T007 Create `internal/serveredition/registry.go` with Feature struct, Register(), SetupAll() functions (build-tagged) - [x] T008 Create `internal/serveredition/registry_test.go` with tests verifying registration and setup (build-tagged) -- [x] T009 Create teams registration entry point `cmd/mcpproxy/teams_register.go` with `//go:build teams` that imports `internal/serveredition` and calls `SetupAll()` during init -- [x] T010 Verify both builds compile: `go build ./cmd/mcpproxy` (no teams code) and `go build -tags teams ./cmd/mcpproxy` (with teams skeleton) +- [x] T009 Create teams registration entry point `cmd/mcpproxy/teams_register.go` with `//go:build server` that imports `internal/serveredition` and calls `SetupAll()` during init +- [x] T010 Verify both builds compile: `go build ./cmd/mcpproxy` (no teams code) and `go build -tags server ./cmd/mcpproxy` (with teams skeleton) -**Checkpoint**: `go test -tags teams ./internal/serveredition/...` passes; personal build has zero teams code compiled in +**Checkpoint**: `go test -tags server ./internal/serveredition/...` passes; personal build has zero teams code compiled in --- @@ -43,7 +43,7 @@ **Purpose**: Create Docker distribution for teams edition and extend Makefile -- [x] T011 [P] Create `Dockerfile` at repo root — multi-stage build with `golang:1.24` builder, `gcr.io/distroless/base` runtime, builds with `-tags teams`, embeds frontend, exposes 8080, entrypoint `mcpproxy serve --listen 0.0.0.0:8080` +- [x] T011 [P] Create `Dockerfile` at repo root — multi-stage build with `golang:1.24` builder, `gcr.io/distroless/base` runtime, builds with `-tags server`, embeds frontend, exposes 8080, entrypoint `mcpproxy serve --listen 0.0.0.0:8080` - [x] T012 [P] Create `.dockerignore` excluding `.git`, `node_modules`, `native/`, `*.md`, test files - [x] T013 Add Makefile targets: `build-teams` (Go binary with teams tag), `build-docker` (Docker image), `build-deb` (placeholder echoing "TODO") - [x] T014 Verify `make build` still produces personal edition (no regression) @@ -79,7 +79,7 @@ **Purpose**: Extend GitHub Actions release workflow to build teams assets alongside personal -- [x] T020 Add teams Linux matrix entries (amd64, arm64) to `.github/workflows/release.yml` build job — uses `-tags teams` flag, produces `mcpproxy-teams-*` archives +- [x] T020 Add teams Linux matrix entries (amd64, arm64) to `.github/workflows/release.yml` build job — uses `-tags server` flag, produces `mcpproxy-teams-*` archives - [x] T021 Add `build-docker` job to `.github/workflows/release.yml` — builds and pushes `ghcr.io/smart-mcp-proxy/mcpproxy-teams:$VERSION` on tag push - [x] T022 Update release notes prompt in `.github/workflows/release.yml` to mention both editions - [x] T023 Update release asset upload to include teams archives with `mcpproxy-teams-` prefix @@ -95,7 +95,7 @@ - [x] T024 Update `CLAUDE.md` — add Build & Distribution section documenting `build-teams`, `build-docker`, edition detection, `internal/serveredition/` structure - [x] T025 Update `Makefile` help target to include new build-teams, build-docker, build-deb targets - [x] T026 Verify all existing tests pass: `go test ./internal/... -v` (personal build) — all pass except pre-existing `internal/server` timeout -- [x] T027 Verify teams build tests pass: `go test -tags teams ./internal/serveredition/... -v` +- [x] T027 Verify teams build tests pass: `go test -tags server ./internal/serveredition/... -v` - [x] T028 Verify E2E tests pass: `./scripts/test-api-e2e.sh` — 61/71 pass, 10 failures are pre-existing (same on clean branch) - [x] T029 Verify linter passes: `./scripts/run-linter.sh` From e06cd9eee35649cc6e08081232fb2c6363261cde Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Wed, 10 Jun 2026 06:57:08 +0300 Subject: [PATCH 3/4] docs: fix stale build-tag references (teams -> server) in specs/029 and related docs All //go:build teams, -tags teams, internal/teams/ references updated to //go:build server, -tags server, internal/serveredition/ to match the actual build tags used throughout the codebase. Related #603 --- docs/features/settings-page.md | 2 +- docs/plans/2026-03-08-repo-restructure-design.md | 6 +++--- specs/029-mcpproxy-teams/data-model.md | 2 +- specs/029-mcpproxy-teams/plan.md | 8 ++++---- specs/029-mcpproxy-teams/quickstart.md | 4 ++-- specs/029-mcpproxy-teams/research.md | 2 +- specs/029-mcpproxy-teams/spec.md | 8 ++++---- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/features/settings-page.md b/docs/features/settings-page.md index 01dcf9bb0..e421628b3 100644 --- a/docs/features/settings-page.md +++ b/docs/features/settings-page.md @@ -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. -- **Server edition** — server build only. +- **Server Edition** — server edition only. ## How saving works diff --git a/docs/plans/2026-03-08-repo-restructure-design.md b/docs/plans/2026-03-08-repo-restructure-design.md index 093732748..295b1d2d6 100644 --- a/docs/plans/2026-03-08-repo-restructure-design.md +++ b/docs/plans/2026-03-08-repo-restructure-design.md @@ -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` @@ -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 diff --git a/specs/029-mcpproxy-teams/data-model.md b/specs/029-mcpproxy-teams/data-model.md index 3c7c07d77..d41b808c3 100644 --- a/specs/029-mcpproxy-teams/data-model.md +++ b/specs/029-mcpproxy-teams/data-model.md @@ -6,7 +6,7 @@ This restructure introduces no new data entities. It adds build-time metadata on | Field | Type | Source | Description | |-------|------|--------|-------------| -| `Edition` | `string` | Build tag | `"personal"` (default) or `"teams"` (with `-tags teams`) | +| `Edition` | `string` | Build tag | `"personal"` (default) or `"teams"` (with `-tags server`) | | `Version` | `string` | ldflags | Semantic version from git tag | | `Commit` | `string` | ldflags | Short git commit hash | | `BuildDate` | `string` | ldflags | ISO 8601 UTC build timestamp | diff --git a/specs/029-mcpproxy-teams/plan.md b/specs/029-mcpproxy-teams/plan.md index e358db5eb..d772222b4 100644 --- a/specs/029-mcpproxy-teams/plan.md +++ b/specs/029-mcpproxy-teams/plan.md @@ -5,7 +5,7 @@ ## Summary -Restructure the MCPProxy repository to support two editions (Personal and Teams) built from the same codebase using Go build tags. Personal is the default build; Teams requires `-tags teams`. Add Dockerfile for teams, `native/` directory skeleton for future Swift/C# tray apps, extended Makefile, and edition self-identification. +Restructure the MCPProxy repository to support two editions (Personal and Teams) built from the same codebase using Go build tags. Personal is the default build; Teams requires `-tags server`. Add Dockerfile for teams, `native/` directory skeleton for future Swift/C# tray apps, extended Makefile, and edition self-identification. ## Technical Context @@ -60,13 +60,13 @@ specs/029-mcpproxy-teams/ ```text cmd/mcpproxy/ ├── main.go # Shared entry point (modify: add edition variable) -├── teams_register.go # NEW: //go:build teams — registers teams features +├── teams_register.go # NEW: //go:build server — registers teams features └── edition.go # NEW: default edition = "personal" - edition_teams.go # NEW: //go:build teams — overrides to "teams" + edition_teams.go # NEW: //go:build server — overrides to "teams" internal/ ├── teams/ # NEW: teams-only skeleton -│ ├── doc.go # Package doc, //go:build teams +│ ├── doc.go # Package doc, //go:build server │ ├── registry.go # Feature registry (init pattern) │ └── registry_test.go # Verify registration works ├── httpapi/ diff --git a/specs/029-mcpproxy-teams/quickstart.md b/specs/029-mcpproxy-teams/quickstart.md index 075b7616c..ed21837e6 100644 --- a/specs/029-mcpproxy-teams/quickstart.md +++ b/specs/029-mcpproxy-teams/quickstart.md @@ -15,7 +15,7 @@ go build -ldflags "..." -o mcpproxy ./cmd/mcpproxy ```bash make build-teams # or directly: -go build -tags teams -ldflags "..." -o mcpproxy-teams ./cmd/mcpproxy +go build -tags server -ldflags "..." -o mcpproxy-teams ./cmd/mcpproxy ./mcpproxy-teams version # MCPProxy v0.21.0 (teams) linux/amd64 ``` @@ -45,7 +45,7 @@ curl http://localhost:8080/api/v1/status | jq .edition ```bash # Run tests (both editions) go test ./internal/... -v # personal (default) -go test -tags teams ./internal/... -v # teams (includes teams tests) +go test -tags server ./internal/... -v # teams (includes teams tests) # Lint ./scripts/run-linter.sh diff --git a/specs/029-mcpproxy-teams/research.md b/specs/029-mcpproxy-teams/research.md index 24698e3c8..a0d405fe3 100644 --- a/specs/029-mcpproxy-teams/research.md +++ b/specs/029-mcpproxy-teams/research.md @@ -2,7 +2,7 @@ ## Go Build Tags Pattern -**Decision**: Use `//go:build teams` file-level tags to isolate teams-only code. +**Decision**: Use `//go:build server` file-level tags to isolate teams-only code. **Rationale**: Go build tags are the idiomatic way to compile different feature sets from the same codebase. The `init()` registration pattern allows teams packages to self-register without modifying shared code paths. diff --git a/specs/029-mcpproxy-teams/spec.md b/specs/029-mcpproxy-teams/spec.md index d64b8aa3c..a0ccb6248 100644 --- a/specs/029-mcpproxy-teams/spec.md +++ b/specs/029-mcpproxy-teams/spec.md @@ -224,11 +224,11 @@ The proxy provides a web interface where users log in via their identity provide **Backward Compatibility** - **FR-037**: When operating in personal mode (default), the system MUST behave identically to the current single-user version with no team features active. -- **FR-038**: The system MUST be built as a separate binary from the personal edition using Go build tags (`-tags teams`), from the same repository and `cmd/mcpproxy` entry point. +- **FR-038**: The system MUST be built as a separate binary from the personal edition using Go build tags (`-tags server`), from the same repository and `cmd/mcpproxy` entry point. **Build & Distribution** -- **FR-039**: The personal edition MUST be the default build output (`go build ./cmd/mcpproxy`). The teams edition MUST require an explicit build tag (`go build -tags teams ./cmd/mcpproxy`). +- **FR-039**: The personal edition MUST be the default build output (`go build ./cmd/mcpproxy`). The teams edition MUST require an explicit build tag (`go build -tags server ./cmd/mcpproxy`). - **FR-040**: The teams edition MUST be distributed as a Docker image (`ghcr.io/smart-mcp-proxy/mcpproxy-teams`), a `.deb` package for Ubuntu/Debian, and a Linux binary tarball. - **FR-041**: The personal edition MUST be distributed as macOS DMG installer (with native Swift tray app), Windows MSI/EXE installer (with native C# tray app), Linux binary tarball, and via Homebrew. - **FR-042**: Both editions MUST be released under a single GitHub release tag (e.g., `v0.21.0`) with assets clearly named by edition (`mcpproxy-*` for personal, `mcpproxy-teams-*` for teams). @@ -272,7 +272,7 @@ The proxy provides a web interface where users log in via their identity provide - Agent token maximum per user follows the same limit as the personal edition (10 tokens per user). - Admin designation is static per configuration (email list or IdP claim) — there is no in-app role assignment UI in v1. - Server template definitions are static (shipped with the binary + admin-defined in config) — there is no template marketplace or community sharing. -- Both editions are built from the same repository (`mcpproxy-go`) using Go build tags. Teams-only code lives in `internal/serveredition/` with `//go:build teams` guards. No `pkg/` migration is needed. +- Both editions are built from the same repository (`mcpproxy-go`) using Go build tags. Teams-only code lives in `internal/serveredition/` with `//go:build server` guards. No `pkg/` migration is needed. - Data retention for activity logs follows the same policy as the personal edition — configurable, with no default auto-purge. ## Scope Boundaries @@ -291,7 +291,7 @@ The proxy provides a web interface where users log in via their identity provide - Activity log with user identity and filtering - Web UI: login, dashboard, admin panel - Backward-compatible personal mode -- Same-repo build tag architecture (`-tags teams`) +- Same-repo build tag architecture (`-tags server`) - Single GitHub release with labeled assets per edition - Docker image and deb package for teams distribution - Native tray apps: Swift (macOS), C# (Windows) for personal distribution From c0a758e18795094bc4413fcd42e612e96667d193 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Fri, 12 Jun 2026 09:04:10 +0300 Subject: [PATCH 4/4] fix(frontend): rebind server-edition settings + docs to server_edition key (MCP-1086) Address CodexReviewer REQUEST_CHANGES on PR #603. The backend renamed the canonical config key teams -> server_edition; the settings UI was still bound to the legacy teams object, so after the rename the Server Edition tab would disappear and edits would reintroduce the legacy key. - Settings.vue: gate the Server Edition tab on server_edition (fallback to the legacy teams key), and alias a legacy teams-keyed config onto server_edition at load so old configs still hydrate the form. - settings/fields.ts: write/read the canonical server_edition.* dot-paths; the shared SettingsSection read (getPath) and PATCH partial (buildPartial) follow the field keys, so both now target server_edition. - idp-token-storage.md + idp_subject_token.go: rename operator-facing refs to server_edition, noting teams is still accepted as a back-compat alias. Verified: vitest (server-edition wording spec), frontend build (vue-tsc), go build -tags server ./cmd/mcpproxy, go build ./cmd/mcpproxy. Related #603 --- docs/features/idp-token-storage.md | 5 +-- frontend/src/views/Settings.vue | 31 +++++++++++++++---- frontend/src/views/settings/fields.ts | 17 +++++----- .../settings-server-edition-wording.spec.ts | 16 +++++----- .../serveredition/auth/idp_subject_token.go | 4 +-- 5 files changed, 47 insertions(+), 26 deletions(-) diff --git a/docs/features/idp-token-storage.md b/docs/features/idp-token-storage.md index 1fede5c9c..e558ba58a 100644 --- a/docs/features/idp-token-storage.md +++ b/docs/features/idp-token-storage.md @@ -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": "", diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index c7008bdc9..b7b77375b 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -120,7 +120,7 @@ -
+

{{ serverEditionTitle }}

@@ -212,7 +212,12 @@ const loadError = ref('') const activeTab = ref('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('') @@ -220,7 +225,7 @@ const allFields = computed(() => [ ...securityFields, ...generalFields, ...advancedAccordions.flatMap((a) => a.fields), - ...(hasTeams.value ? serverEditionFields : []), + ...(hasServerEdition.value ? serverEditionFields : []), ]) const filteredFields = computed(() => { const q = search.value.trim().toLowerCase() @@ -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 }) @@ -282,6 +287,16 @@ function clone(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 = '' @@ -289,8 +304,12 @@ async function loadConfig() { 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 diff --git a/frontend/src/views/settings/fields.ts b/frontend/src/views/settings/fields.ts index 769799bcc..715a13450 100644 --- a/frontend/src/views/settings/fields.ts +++ b/frontend/src/views/settings/fields.ts @@ -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) ---- diff --git a/frontend/tests/unit/settings-server-edition-wording.spec.ts b/frontend/tests/unit/settings-server-edition-wording.spec.ts index dcbcaad33..6e9af2c76 100644 --- a/frontend/tests/unit/settings-server-edition-wording.spec.ts +++ b/frontend/tests/unit/settings-server-edition-wording.spec.ts @@ -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') @@ -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\./) } }) }) diff --git a/internal/serveredition/auth/idp_subject_token.go b/internal/serveredition/auth/idp_subject_token.go index 47d22a5f7..57eac9d9b 100644 --- a/internal/serveredition/auth/idp_subject_token.go +++ b/internal/serveredition/auth/idp_subject_token.go @@ -38,8 +38,8 @@ func (h *OAuthHandler) persistIDPSubjectToken(userID string, tokenResp *TokenRes return } if h.credStore == nil || !h.credStore.Enabled() { - h.logger.Warnw("teams.store_idp_tokens is enabled but the credential store is disabled; "+ - "IdP subject token not persisted (set MCPPROXY_CRED_KEY or teams.credential_encryption_key)", + h.logger.Warnw("server_edition.store_idp_tokens is enabled but the credential store is disabled; "+ + "IdP subject token not persisted (set MCPPROXY_CRED_KEY or server_edition.credential_encryption_key)", "user_id", userID) return }