From 8cdbf6cd6b70800d570d389dff384e0b65074cb1 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Fri, 5 Jun 2026 08:36:19 +0300 Subject: [PATCH] =?UTF-8?q?refactor(server-edition):=20rename=20Teams=20?= =?UTF-8?q?=E2=86=92=20Server=20edition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reconcile the multi-user code/config to the product's "Server edition" naming (editions are Server and Personal; "Teams" was the legacy name). - Go identifiers: TeamsConfig→ServerEditionConfig, TeamsOAuthConfig, TeamsAuthMiddleware, TeamsStatusInfo/Info, and locals → ServerEdition*. - Package: internal/teams/ → internal/serveredition/ (git-mv, history preserved); package teams → serveredition; import paths/aliases updated. - Config key: "teams" → "server_edition" with a backward-compat alias — the legacy key is normalized on read (Config.UnmarshalJSON) so existing deployments keep working; SaveConfig always writes the new key. New key wins when both are present. - Docs/comments/strings: Teams → Server edition (CLAUDE.md, settings-page). - Unchanged by design: the //go:build server tag and the server/personal edition strings (binary identity). Tested: builds (personal + server), config alias unit tests (both editions), full serveredition suite -race, API E2E 65/65, linter clean. Related MCP-1085 --- CLAUDE.md | 26 +-- cmd/mcpproxy/edition.go | 2 +- .../{edition_teams.go => edition_server.go} | 0 cmd/mcpproxy/server_edition_register.go | 11 + cmd/mcpproxy/status_cmd.go | 44 ++-- cmd/mcpproxy/status_server_edition.go | 18 ++ ..._stub.go => status_server_edition_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 | 38 ++- ...ams_config.go => server_edition_config.go} | 44 ++-- internal/config/server_edition_config_stub.go | 6 + .../config/server_edition_config_stub_test.go | 31 +++ ..._test.go => server_edition_config_test.go} | 218 ++++++++++++------ ...t.go => server_edition_credential_test.go} | 26 +-- internal/config/teams_config_stub.go | 6 - internal/httpapi/server.go | 2 +- internal/runtime/activity_service.go | 4 +- internal/server/mcp.go | 2 +- internal/server/server.go | 4 +- .../{teams_wire.go => server_edition_wire.go} | 10 +- internal/server/server_edition_wire_stub.go | 10 + internal/server/teams_wire_stub.go | 10 - .../api/admin_handlers.go | 10 +- .../api/admin_handlers_test.go | 8 +- .../api/admin_integration_test.go | 2 +- .../api/auth_endpoints.go | 32 +-- .../api/auth_endpoints_test.go | 12 +- .../api/user_activity.go | 4 +- .../api/user_activity_test.go | 4 +- .../api/user_handlers.go | 2 +- .../api/user_handlers_test.go | 2 +- .../auth/integration_test.go | 50 ++-- .../auth/jwt_tokens.go | 0 .../auth/jwt_tokens_test.go | 0 .../auth/middleware.go | 48 ++-- .../auth/middleware_test.go | 46 ++-- .../auth/oauth_handler.go | 6 +- .../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 internal/{teams => serveredition}/doc.go | 2 +- .../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 | 26 +-- .../{teams => serveredition}/setup_test.go | 14 +- .../{teams => serveredition}/users/models.go | 0 .../users/models_test.go | 0 .../users/server_store.go | 0 .../{teams => serveredition}/users/store.go | 0 .../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 +- 70 files changed, 502 insertions(+), 371 deletions(-) rename cmd/mcpproxy/{edition_teams.go => edition_server.go} (100%) create mode 100644 cmd/mcpproxy/server_edition_register.go create mode 100644 cmd/mcpproxy/status_server_edition.go rename cmd/mcpproxy/{status_teams_stub.go => status_server_edition_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 create mode 100644 internal/config/server_edition_config_stub_test.go rename internal/config/{teams_config_test.go => server_edition_config_test.go} (57%) rename internal/config/{teams_credential_test.go => server_edition_credential_test.go} (69%) delete mode 100644 internal/config/teams_config_stub.go rename internal/server/{teams_wire.go => server_edition_wire.go} (71%) create mode 100644 internal/server/server_edition_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 (82%) rename internal/{teams => serveredition}/api/auth_endpoints_test.go (92%) 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/integration_test.go (94%) 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 (79%) rename internal/{teams => serveredition}/auth/middleware_test.go (94%) rename internal/{teams => serveredition}/auth/oauth_handler.go (98%) rename internal/{teams => serveredition}/auth/oauth_handler_test.go (93%) 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}/doc.go (92%) 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 (65%) rename internal/{teams => serveredition}/setup_test.go (93%) 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 (100%) 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..29f56fb76 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,14 +44,14 @@ MCPProxy is built in two editions from the same codebase using Go build tags: | Directory | Purpose | |-----------|---------| | `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/edition_server.go` | Build-tagged override for server edition | +| `cmd/mcpproxy/server_edition_register.go` | Server feature registration entry point | +| `internal/serveredition/` | Server-only code (all files have `//go:build server`) | +| `internal/serveredition/auth/` | OAuth authentication, session management, JWT tokens, middleware | +| `internal/serveredition/users/` | User/session models, BBolt store, user server management | +| `internal/serveredition/workspace/` | Per-user workspace manager for personal upstream servers | +| `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) | @@ -68,9 +68,11 @@ Server edition supports OAuth-based multi-user authentication with Google, GitHu ### Server Configuration +The config key is `server_edition` (the legacy `teams` key is still accepted on read and auto-migrated to `server_edition` on next save). + ```json { - "teams": { + "server_edition": { "enabled": true, "admin_emails": ["admin@company.com"], "oauth": { @@ -117,7 +119,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 ``` @@ -731,15 +733,12 @@ See `docs/prerelease-builds.md` for download instructions. - Go 1.24 (toolchain go1.24.10) (001-update-version-display) - In-memory only for version cache (no persistence per clarification) (001-update-version-display) - Go 1.24 (toolchain go1.24.10) + Cobra CLI framework, encoding/json, gopkg.in/yaml.v3 (014-cli-output-formatting) -- N/A (CLI output only) (014-cli-output-formatting) - Go 1.24 (toolchain go1.24.10) + BBolt (storage), Chi router (HTTP), Zap (logging), existing event bus (016-activity-log-backend) - BBolt database (existing `~/.mcpproxy/config.db`) (016-activity-log-backend) - Go 1.24 (toolchain go1.24.10) + Cobra CLI framework, encoding/json, internal/cli/output (spec 014), internal/cliclien (017-activity-cli-commands) -- N/A (CLI layer only - uses REST API from spec 016) (017-activity-cli-commands) - Go 1.24 (toolchain go1.24.10) + Cobra CLI, Chi router, BBolt (storage), Zap (logging), mark3labs/mcp-go (MCP protocol) (018-intent-declaration) - BBolt database (`~/.mcpproxy/config.db`) - ActivityRecord extended with intent metadata (018-intent-declaration) - TypeScript 5.9, Vue 3.5, Go 1.24 (backend already exists) + Vue 3, Vue Router 4, Pinia 2, Tailwind CSS 3, DaisyUI 4, Vite 5 (019-activity-webui) -- N/A (frontend consumes REST API from backend) (019-activity-webui) - Go 1.24 (toolchain go1.24.10) + Cobra (CLI), Chi router (HTTP), Zap (logging), mark3labs/mcp-go (MCP protocol) (020-oauth-login-feedback) - Go 1.24 (toolchain go1.24.10) + Cobra (CLI), Chi router (HTTP), Zap (logging), google/uuid (ID generation) (021-request-id-logging) - BBolt database (`~/.mcpproxy/config.db`) - activity log extended with request_id field (021-request-id-logging) @@ -756,7 +755,6 @@ See `docs/prerelease-builds.md` for download instructions. - Go 1.24 (toolchain go1.24.10) + TypeScript 5.9 / Vue 3.5 + Chi router, BBolt, Zap logging, mcp-go, golang-jwt/jwt/v5, Vue 3, Pinia, DaisyUI (024-teams-multiuser-oauth) - BBolt database (`~/.mcpproxy/config.db`) - new buckets for users, sessions, user servers (024-teams-multiuser-oauth) - Go 1.24 (toolchain go1.24.10) + `github.com/dop251/goja` (existing JS sandbox), `github.com/evanw/esbuild` (new - TypeScript transpilation), `github.com/mark3labs/mcp-go` (MCP protocol), `github.com/spf13/cobra` (CLI) (033-typescript-code-execution) -- N/A (no new storage requirements) (033-typescript-code-execution) - Swift 5.9+ / Xcode 15+ + SwiftUI, AppKit (escape hatches), Sparkle 2.x (SPM), Foundation (URLSession, Process, UNUserNotificationCenter) (037-macos-swift-tray) - N/A (tray reads all state from core via REST API — no local persistence per Constitution III) (037-macos-swift-tray) - Go 1.24 (toolchain go1.24.10) — primary; Swift 5.9 — macOS tray header change only + `github.com/google/uuid` (existing), `github.com/go-chi/chi/v5` (existing, for `RoutePattern()`), `github.com/spf13/cobra` (existing, new subcommand), `go.uber.org/zap` (existing), stdlib `sync/atomic`, `sync`, `os` (042-telemetry-tier2) 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/edition_teams.go b/cmd/mcpproxy/edition_server.go similarity index 100% rename from cmd/mcpproxy/edition_teams.go rename to cmd/mcpproxy/edition_server.go diff --git a/cmd/mcpproxy/server_edition_register.go b/cmd/mcpproxy/server_edition_register.go new file mode 100644 index 000000000..7a8be2d7b --- /dev/null +++ b/cmd/mcpproxy/server_edition_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/server_edition_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..3d7fdf88a 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_server_edition.go b/cmd/mcpproxy/status_server_edition.go new file mode 100644 index 000000000..b2f271362 --- /dev/null +++ b/cmd/mcpproxy/status_server_edition.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_server_edition_stub.go similarity index 60% rename from cmd/mcpproxy/status_teams_stub.go rename to cmd/mcpproxy/status_server_edition_stub.go index 5cc415079..794a2974a 100644 --- a/cmd/mcpproxy/status_teams_stub.go +++ b/cmd/mcpproxy/status_server_edition_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..38c691673 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** — multi-user settings (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 e8d289bf6..7a8b90096 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -199,7 +199,7 @@ type Config struct { 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"` + ServerEdition *ServerEditionConfig `json:"server_edition,omitempty" mapstructure:"server_edition" swaggerignore:"true"` } // TLSConfig represents TLS configuration @@ -275,7 +275,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"` } @@ -1519,6 +1519,16 @@ func (c *Config) MarshalJSON() ([]byte, error) { // UnmarshalJSON implements json.Unmarshaler interface func (c *Config) UnmarshalJSON(data []byte) error { + // Backward-compat (MCP-1085): accept the legacy top-level "teams" key as an + // alias for the canonical "server_edition" key. Normalize on read — mirroring + // the provenance normalize-on-read in #594 — so existing deployments keep + // working. SaveConfig always writes the new key, so this only ever runs once + // per legacy config until it is rewritten. + data, err := normalizeLegacyServerEditionKey(data) + if err != nil { + return err + } + type Alias Config aux := &struct { *Alias @@ -1528,6 +1538,30 @@ func (c *Config) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, aux) } +// normalizeLegacyServerEditionKey rewrites a config payload that still uses the +// legacy top-level "teams" key onto the canonical "server_edition" key +// (MCP-1085). It is a no-op when "teams" is absent — the common case pays no +// cost. When both keys are present, "server_edition" is authoritative and the +// legacy key is dropped. A payload that is not a JSON object is returned +// unchanged so the caller's normal unmarshal surfaces the real error. +func normalizeLegacyServerEditionKey(data []byte) ([]byte, error) { + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + // Not a JSON object (or otherwise invalid): return the input untouched and + // let the caller's typed unmarshal report the real error. + return data, nil + } + legacy, hasLegacy := raw["teams"] + if !hasLegacy { + return data, nil + } + if _, hasNew := raw["server_edition"]; !hasNew { + raw["server_edition"] = legacy + } + delete(raw, "teams") + return json.Marshal(raw) +} + // OAuthConfigChanged checks if OAuth configuration has changed between two configs. // Returns true if any OAuth field differs (ClientID, Scopes, ExtraParams, etc.) func OAuthConfigChanged(old, new *OAuthConfig) bool { 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..bef4fb690 --- /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. ServerEdition features are not available. +type ServerEditionConfig struct{} diff --git a/internal/config/server_edition_config_stub_test.go b/internal/config/server_edition_config_stub_test.go new file mode 100644 index 000000000..347fb34fc --- /dev/null +++ b/internal/config/server_edition_config_stub_test.go @@ -0,0 +1,31 @@ +//go:build !server + +package config + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestServerEditionKeyAlias_PersonalEdition verifies the legacy "teams" -> new +// "server_edition" key alias (MCP-1085) is honored in the default (personal) +// build too: the key is accepted without error and the config always serializes +// with the canonical key. The stub ServerEditionConfig has no fields, so we only +// assert key-level behavior here. +func TestServerEditionKeyAlias_PersonalEdition(t *testing.T) { + // Legacy key is accepted (no error) and maps onto ServerEdition. + var cfg Config + require.NoError(t, json.Unmarshal([]byte(`{"teams": {}}`), &cfg)) + require.NotNil(t, cfg.ServerEdition, "legacy teams key should populate ServerEdition in personal edition") + + // Output always uses the canonical key, never the legacy one. + data, err := json.Marshal(&cfg) + require.NoError(t, err) + var raw map[string]interface{} + require.NoError(t, json.Unmarshal(data, &raw)) + _, hasLegacy := raw["teams"] + assert.False(t, hasLegacy, "legacy teams key must never be written") +} diff --git a/internal/config/teams_config_test.go b/internal/config/server_edition_config_test.go similarity index 57% rename from internal/config/teams_config_test.go rename to internal/config/server_edition_config_test.go index 61b5256f0..a6864ec93 100644 --- a/internal/config/teams_config_test.go +++ b/internal/config/server_edition_config_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/require" ) -func TestTeamsDefaultTeamsConfig(t *testing.T) { - cfg := DefaultTeamsConfig() +func TestServerEditionDefaultServerEditionConfig(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") @@ -23,8 +23,8 @@ func TestTeamsDefaultTeamsConfig(t *testing.T) { assert.Equal(t, 20, cfg.MaxUserServers, "max user servers should default to 20") } -func TestTeamsIsAdminEmail(t *testing.T) { - cfg := &TeamsConfig{ +func TestServerEditionIsAdminEmail(t *testing.T) { + cfg := &ServerEditionConfig{ AdminEmails: []string{"admin@example.com", "Boss@Corp.io"}, } @@ -51,22 +51,22 @@ func TestTeamsIsAdminEmail(t *testing.T) { } } -func TestTeamsIsAdminEmail_EmptyList(t *testing.T) { - cfg := &TeamsConfig{AdminEmails: nil} +func TestServerEditionIsAdminEmail_EmptyList(t *testing.T) { + 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} +func TestServerEditionValidate_DisabledSkipsValidation(t *testing.T) { + 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{ +func TestServerEditionValidate_MissingAdminEmails(t *testing.T) { + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: nil, } @@ -75,8 +75,8 @@ func TestTeamsValidate_MissingAdminEmails(t *testing.T) { assert.Contains(t, err.Error(), "admin_emails") } -func TestTeamsValidate_MissingOAuth(t *testing.T) { - cfg := &TeamsConfig{ +func TestServerEditionValidate_MissingOAuth(t *testing.T) { + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, OAuth: nil, @@ -86,11 +86,11 @@ func TestTeamsValidate_MissingOAuth(t *testing.T) { assert.Contains(t, err.Error(), "oauth configuration is required") } -func TestTeamsValidate_InvalidProvider(t *testing.T) { - cfg := &TeamsConfig{ +func TestServerEditionValidate_InvalidProvider(t *testing.T) { + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "facebook", ClientID: "id", ClientSecret: "secret", @@ -102,11 +102,11 @@ func TestTeamsValidate_InvalidProvider(t *testing.T) { assert.Contains(t, err.Error(), "facebook") } -func TestTeamsValidate_MissingClientID(t *testing.T) { - cfg := &TeamsConfig{ +func TestServerEditionValidate_MissingClientID(t *testing.T) { + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "google", ClientID: "", ClientSecret: "secret", @@ -117,11 +117,11 @@ func TestTeamsValidate_MissingClientID(t *testing.T) { assert.Contains(t, err.Error(), "client_id is required") } -func TestTeamsValidate_MissingClientSecret(t *testing.T) { - cfg := &TeamsConfig{ +func TestServerEditionValidate_MissingClientSecret(t *testing.T) { + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "google", ClientID: "my-client-id", ClientSecret: "", @@ -132,11 +132,11 @@ func TestTeamsValidate_MissingClientSecret(t *testing.T) { assert.Contains(t, err.Error(), "client_secret is required") } -func TestTeamsValidate_ValidGoogleConfig(t *testing.T) { - cfg := &TeamsConfig{ +func TestServerEditionValidate_ValidGoogleConfig(t *testing.T) { + 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", @@ -150,11 +150,11 @@ func TestTeamsValidate_ValidGoogleConfig(t *testing.T) { assert.NoError(t, err) } -func TestTeamsValidate_ValidGitHubConfig(t *testing.T) { - cfg := &TeamsConfig{ +func TestServerEditionValidate_ValidGitHubConfig(t *testing.T) { + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "github", ClientID: "Iv1.abc123", ClientSecret: "secret123", @@ -164,11 +164,11 @@ func TestTeamsValidate_ValidGitHubConfig(t *testing.T) { assert.NoError(t, err) } -func TestTeamsValidate_MicrosoftDefaultsTenantID(t *testing.T) { - cfg := &TeamsConfig{ +func TestServerEditionValidate_MicrosoftDefaultsTenantID(t *testing.T) { + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "microsoft", ClientID: "my-client-id", ClientSecret: "my-client-secret", @@ -180,11 +180,11 @@ func TestTeamsValidate_MicrosoftDefaultsTenantID(t *testing.T) { assert.Equal(t, "common", cfg.OAuth.TenantID, "Microsoft tenant ID should default to 'common'") } -func TestTeamsValidate_MicrosoftExplicitTenantID(t *testing.T) { - cfg := &TeamsConfig{ +func TestServerEditionValidate_MicrosoftExplicitTenantID(t *testing.T) { + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "microsoft", ClientID: "my-client-id", ClientSecret: "my-client-secret", @@ -196,11 +196,11 @@ func TestTeamsValidate_MicrosoftExplicitTenantID(t *testing.T) { assert.Equal(t, "my-tenant-id", cfg.OAuth.TenantID, "explicit tenant ID should be preserved") } -func TestTeamsValidate_DefaultsAppliedForZeroValues(t *testing.T) { - cfg := &TeamsConfig{ +func TestServerEditionValidate_DefaultsAppliedForZeroValues(t *testing.T) { + cfg := &ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, - OAuth: &TeamsOAuthConfig{ + OAuth: &ServerEditionOAuthConfig{ Provider: "google", ClientID: "id", ClientSecret: "secret", @@ -215,14 +215,14 @@ func TestTeamsValidate_DefaultsAppliedForZeroValues(t *testing.T) { assert.Equal(t, 20, cfg.MaxUserServers, "zero MaxUserServers should default to 20") } -func TestTeamsValidate_AllProviders(t *testing.T) { +func TestServerEditionValidate_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 +234,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 +253,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 +270,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 +286,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 +294,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 +309,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 +332,103 @@ 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) { +// TestServerEditionConfig_LegacyTeamsKeyAlias verifies the backward-compat +// alias (MCP-1085): a config that still uses the old top-level "teams" key is +// normalized on read into ServerEdition so existing deployments keep working. +func TestServerEditionConfig_LegacyTeamsKeyAlias(t *testing.T) { + jsonStr := `{ + "listen": "0.0.0.0:8080", + "teams": { + "enabled": true, + "admin_emails": ["admin@example.com"], + "oauth": { + "provider": "github", + "client_id": "Iv1.abc", + "client_secret": "ghp_secret" + }, + "max_user_servers": 5 + } + }` + + var cfg Config + err := json.Unmarshal([]byte(jsonStr), &cfg) + require.NoError(t, err) + + require.NotNil(t, cfg.ServerEdition, "legacy teams key should populate ServerEdition") + assert.True(t, cfg.ServerEdition.Enabled) + assert.Equal(t, []string{"admin@example.com"}, cfg.ServerEdition.AdminEmails) + require.NotNil(t, cfg.ServerEdition.OAuth) + assert.Equal(t, "github", cfg.ServerEdition.OAuth.Provider) + assert.Equal(t, 5, cfg.ServerEdition.MaxUserServers) +} + +// TestServerEditionConfig_NewKeyWinsOverLegacy verifies that if both the new +// "server_edition" key and the legacy "teams" key are present, the new key is +// authoritative and the legacy one is ignored. +func TestServerEditionConfig_NewKeyWinsOverLegacy(t *testing.T) { + jsonStr := `{ + "teams": { "enabled": false, "max_user_servers": 1 }, + "server_edition": { "enabled": true, "max_user_servers": 9 } + }` + + var cfg Config + err := json.Unmarshal([]byte(jsonStr), &cfg) + require.NoError(t, err) + + require.NotNil(t, cfg.ServerEdition) + assert.True(t, cfg.ServerEdition.Enabled, "server_edition key must take precedence over teams") + assert.Equal(t, 9, cfg.ServerEdition.MaxUserServers) +} + +// TestServerEditionConfig_WritesNewKey verifies SaveConfig/Marshal always emits +// the canonical "server_edition" key, never the legacy "teams" key. +func TestServerEditionConfig_WritesNewKey(t *testing.T) { + cfg := &Config{ + ServerEdition: &ServerEditionConfig{Enabled: true, MaxUserServers: 7}, + } + data, err := json.Marshal(cfg) + require.NoError(t, err) + + var raw map[string]interface{} + require.NoError(t, json.Unmarshal(data, &raw)) + _, hasNew := raw["server_edition"] + assert.True(t, hasNew, "config must be written with the server_edition key") + _, hasLegacy := raw["teams"] + assert.False(t, hasLegacy, "config must never be written with the legacy teams key") +} + +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") + // And the legacy "teams" key must never be emitted either. + _, hasLegacy := raw["teams"] + assert.False(t, hasLegacy, "legacy teams key must never be written") } -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 +447,12 @@ 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) } diff --git a/internal/config/teams_credential_test.go b/internal/config/server_edition_credential_test.go similarity index 69% rename from internal/config/teams_credential_test.go rename to internal/config/server_edition_credential_test.go index 847cf4e10..d513cbcac 100644 --- a/internal/config/teams_credential_test.go +++ b/internal/config/server_edition_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/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/httpapi/server.go b/internal/httpapi/server.go index 1eeca736b..4f0c74317 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -1070,7 +1070,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 95d107d5e..1b54b5359 100644 --- a/internal/runtime/activity_service.go +++ b/internal/runtime/activity_service.go @@ -322,7 +322,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 @@ -562,7 +562,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 543643a97..369004c19 100644 --- a/internal/server/mcp.go +++ b/internal/server/mcp.go @@ -349,7 +349,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 5bc95a424..4422fdff6 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1776,8 +1776,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/server_edition_wire.go similarity index 71% rename from internal/server/teams_wire.go rename to internal/server/server_edition_wire.go index 15aced546..622ef2e58 100644 --- a/internal/server/teams_wire.go +++ b/internal/server/server_edition_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/server_edition_wire_stub.go b/internal/server/server_edition_wire_stub.go new file mode 100644 index 000000000..bce39274b --- /dev/null +++ b/internal/server/server_edition_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..2467ca169 100644 --- a/internal/teams/api/admin_handlers.go +++ b/internal/serveredition/api/admin_handlers.go @@ -16,16 +16,16 @@ 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" + seauth "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. type AdminHandlers struct { userStore *users.UserStore activityFilter *multiuser.ActivityFilter - sessionManager *teamsauth.SessionManager + sessionManager *seauth.SessionManager adminEmails []string sharedServers []*config.ServerConfig config *config.Config @@ -38,7 +38,7 @@ type AdminHandlers struct { func NewAdminHandlers( userStore *users.UserStore, activityFilter *multiuser.ActivityFilter, - sessionManager *teamsauth.SessionManager, + sessionManager *seauth.SessionManager, adminEmails []string, sharedServers []*config.ServerConfig, cfg *config.Config, 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..d2ae1dd6e 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" + seauth "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" @@ -39,7 +39,7 @@ func adminTestSetup(t *testing.T, records []*storage.ActivityRecord) (*AdminHand provider := &mockActivityProvider{records: records} activityFilter := multiuser.NewActivityFilter(provider) - sessionManager := teamsauth.NewSessionManager(store, 24*time.Hour, false) + sessionManager := seauth.NewSessionManager(store, 24*time.Hour, false) logger := zap.NewNop().Sugar() handlers := NewAdminHandlers(store, activityFilter, sessionManager, []string{"admin@example.com"}, nil, nil, "", nil, logger) 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 82% rename from internal/teams/api/auth_endpoints.go rename to internal/serveredition/api/auth_endpoints.go index cc3c1f7d0..154618a68 100644 --- a/internal/teams/api/auth_endpoints.go +++ b/internal/serveredition/api/auth_endpoints.go @@ -11,33 +11,33 @@ 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" + seauth "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 - hmacKey []byte - logger *zap.SugaredLogger + userStore *users.UserStore + sessionManager *seauth.SessionManager + serverEditionConfig *config.ServerEditionConfig + hmacKey []byte + logger *zap.SugaredLogger } // NewAuthEndpoints creates a new AuthEndpoints instance. func NewAuthEndpoints( userStore *users.UserStore, - sessionManager *teamsauth.SessionManager, - teamsConfig *config.TeamsConfig, + sessionManager *seauth.SessionManager, + serverEditionConfig *config.ServerEditionConfig, hmacKey []byte, logger *zap.SugaredLogger, ) *AuthEndpoints { return &AuthEndpoints{ - userStore: userStore, - sessionManager: sessionManager, - teamsConfig: teamsConfig, - hmacKey: hmacKey, - logger: logger, + userStore: userStore, + sessionManager: sessionManager, + serverEditionConfig: serverEditionConfig, + hmacKey: hmacKey, + logger: logger, } } @@ -119,12 +119,12 @@ func (h *AuthEndpoints) generateToken(w http.ResponseWriter, r *http.Request) { return } - ttl := h.teamsConfig.BearerTokenTTL.Duration() + ttl := h.serverEditionConfig.BearerTokenTTL.Duration() if ttl <= 0 { ttl = 24 * time.Hour } - token, err := teamsauth.GenerateBearerToken( + token, err := seauth.GenerateBearerToken( h.hmacKey, user.ID, user.Email, diff --git a/internal/teams/api/auth_endpoints_test.go b/internal/serveredition/api/auth_endpoints_test.go similarity index 92% rename from internal/teams/api/auth_endpoints_test.go rename to internal/serveredition/api/auth_endpoints_test.go index 59d8234dd..bd5892d29 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" + seauth "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. @@ -34,12 +34,12 @@ func authTestSetup(t *testing.T) (*AuthEndpoints, *users.UserStore) { store := users.NewUserStore(db) require.NoError(t, store.EnsureBuckets()) - sessionManager := teamsauth.NewSessionManager(store, 24*time.Hour, false) - teamsConfig := config.DefaultTeamsConfig() + sessionManager := seauth.NewSessionManager(store, 24*time.Hour, false) + serverEditionConfig := config.DefaultServerEditionConfig() hmacKey := []byte("test-hmac-key-32-bytes-long!!!!!!") logger := zap.NewNop().Sugar() - endpoints := NewAuthEndpoints(store, sessionManager, teamsConfig, hmacKey, logger) + endpoints := NewAuthEndpoints(store, sessionManager, serverEditionConfig, hmacKey, logger) return endpoints, store } @@ -179,7 +179,7 @@ func TestAuthToken_GeneratesJWT(t *testing.T) { assert.NotEmpty(t, resp.ExpiresAt) // Verify the token is a valid JWT by parsing it. - claims, err := teamsauth.ValidateBearerToken(resp.Token, []byte("test-hmac-key-32-bytes-long!!!!!!")) + claims, err := seauth.ValidateBearerToken(resp.Token, []byte("test-hmac-key-32-bytes-long!!!!!!")) require.NoError(t, err) assert.Equal(t, testUserID, claims.Subject) assert.Equal(t, "test@example.com", claims.Email) 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/integration_test.go b/internal/serveredition/auth/integration_test.go similarity index 94% rename from internal/teams/auth/integration_test.go rename to internal/serveredition/auth/integration_test.go index 081cb935b..ffa583d09 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 ---------- @@ -84,21 +84,21 @@ func newMockOAuthProvider(t *testing.T, email, name, id string) *mockOAuthProvid // ---------- integration test scaffold ---------- type integrationSetup struct { - appServer *httptest.Server - oauthHandler *OAuthHandler - sessionManager *SessionManager - userStore *users.UserStore - teamsConfig *config.TeamsConfig - hmacKey []byte + appServer *httptest.Server + oauthHandler *OAuthHandler + sessionManager *SessionManager + userStore *users.UserStore + serverEditionConfig *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{ + serverEditionCfg := &config.ServerEditionConfig{ Enabled: true, AdminEmails: adminEmails, OAuth: oauthCfg, @@ -148,8 +148,8 @@ 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) + oauthH := NewOAuthHandler(store, sessionMgr, serverEditionCfg, hmacKey, logger) + authMW := NewServerEditionAuthMiddleware(sessionMgr, store, serverEditionCfg, hmacKey, logger) // -- Router -- r := chi.NewRouter() @@ -194,7 +194,7 @@ func setupIntegration(t *testing.T, email, name, userSub string, oauthCfg *confi writeJSONError(w, http.StatusInternalServerError, "User not found") return } - ttl := teamsCfg.BearerTokenTTL.Duration() + ttl := serverEditionCfg.BearerTokenTTL.Duration() token, err := GenerateBearerToken(hmacKey, user.ID, user.Email, user.DisplayName, ac.Role, user.Provider, ttl) if err != nil { writeJSONError(w, http.StatusInternalServerError, "Failed to generate token") @@ -213,12 +213,12 @@ func setupIntegration(t *testing.T, email, name, userSub string, oauthCfg *confi t.Cleanup(srv.Close) return &integrationSetup{ - appServer: srv, - oauthHandler: oauthH, - sessionManager: sessionMgr, - userStore: store, - teamsConfig: teamsCfg, - hmacKey: hmacKey, + appServer: srv, + oauthHandler: oauthH, + sessionManager: sessionMgr, + userStore: store, + serverEditionConfig: serverEditionCfg, + hmacKey: hmacKey, } } @@ -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 79% rename from internal/teams/auth/middleware.go rename to internal/serveredition/auth/middleware.go index 408f70680..0196e9df5 100644 --- a/internal/teams/auth/middleware.go +++ b/internal/serveredition/auth/middleware.go @@ -11,37 +11,37 @@ 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 { - sessionManager *SessionManager - userStore *users.UserStore - teamsConfig *config.TeamsConfig - hmacKey []byte - logger *zap.SugaredLogger +type ServerEditionAuthMiddleware struct { + sessionManager *SessionManager + userStore *users.UserStore + serverEditionConfig *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, + serverEditionConfig *config.ServerEditionConfig, hmacKey []byte, logger *zap.SugaredLogger, -) *TeamsAuthMiddleware { - return &TeamsAuthMiddleware{ - sessionManager: sessionManager, - userStore: userStore, - teamsConfig: teamsConfig, - hmacKey: hmacKey, - logger: logger, +) *ServerEditionAuthMiddleware { + return &ServerEditionAuthMiddleware{ + sessionManager: sessionManager, + userStore: userStore, + serverEditionConfig: serverEditionConfig, + hmacKey: hmacKey, + logger: logger, } } @@ -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,8 +177,8 @@ 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 { - if m.teamsConfig.IsAdminEmail(user.Email) { +func (m *ServerEditionAuthMiddleware) buildAuthContext(user *users.User) *coreauth.AuthContext { + if m.serverEditionConfig.IsAdminEmail(user.Email) { return coreauth.AdminUserContext(user.ID, user.Email, user.DisplayName, user.Provider) } return coreauth.UserContext(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 94% rename from internal/teams/auth/middleware_test.go rename to internal/serveredition/auth/middleware_test.go index a14c48208..9f367f1c7 100644 --- a/internal/teams/auth/middleware_test.go +++ b/internal/serveredition/auth/middleware_test.go @@ -17,24 +17,24 @@ 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. type testMiddlewareSetup struct { - db *bbolt.DB - userStore *users.UserStore - sessionManager *SessionManager - middleware *TeamsAuthMiddleware - teamsConfig *config.TeamsConfig - hmacKey []byte - testUser *users.User - adminUser *users.User - disabledUser *users.User + db *bbolt.DB + userStore *users.UserStore + sessionManager *SessionManager + middleware *ServerEditionAuthMiddleware + serverEditionConfig *config.ServerEditionConfig + hmacKey []byte + testUser *users.User + adminUser *users.User + disabledUser *users.User } // 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{ + // ServerEdition config with admin@example.com as admin + serverEditionConfig := &config.ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, } @@ -86,18 +86,18 @@ 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, serverEditionConfig, hmacKey, logger) return &testMiddlewareSetup{ - db: db, - userStore: userStore, - sessionManager: sessionManager, - middleware: mw, - teamsConfig: teamsConfig, - hmacKey: hmacKey, - testUser: testUser, - adminUser: adminUser, - disabledUser: disabledUser, + db: db, + userStore: userStore, + sessionManager: sessionManager, + middleware: mw, + serverEditionConfig: serverEditionConfig, + hmacKey: hmacKey, + testUser: testUser, + adminUser: adminUser, + disabledUser: disabledUser, } } 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 1baca2ad4..322061d7e 100644 --- a/internal/teams/auth/oauth_handler.go +++ b/internal/serveredition/auth/oauth_handler.go @@ -17,14 +17,14 @@ 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" ) // 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 @@ -46,7 +46,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 93% rename from internal/teams/auth/oauth_handler_test.go rename to internal/serveredition/auth/oauth_handler_test.go index 093f5098f..35030a7fa 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{ + serverEditionCfg := &config.ServerEditionConfig{ Enabled: true, AdminEmails: []string{"admin@example.com"}, OAuth: oauthCfg, @@ -46,7 +46,7 @@ func setupTestOAuthHandler(t *testing.T, oauthCfg *config.TeamsOAuthConfig) (*OA logger := zap.NewNop().Sugar() hmacKey := []byte("test-hmac-key-for-jwt-signing-32b") - handler := NewOAuthHandler(store, sessionMgr, teamsCfg, hmacKey, logger) + handler := NewOAuthHandler(store, sessionMgr, serverEditionCfg, hmacKey, logger) return handler, store } @@ -120,7 +120,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", @@ -155,7 +155,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", @@ -195,7 +195,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", @@ -257,7 +257,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", @@ -284,7 +284,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", @@ -311,7 +311,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", @@ -350,7 +350,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", @@ -411,7 +411,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", @@ -465,7 +465,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", @@ -489,7 +489,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", @@ -574,7 +574,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/doc.go b/internal/serveredition/doc.go similarity index 92% rename from internal/teams/doc.go rename to internal/serveredition/doc.go index d93b83a16..1e18a84dc 100644 --- a/internal/teams/doc.go +++ b/internal/serveredition/doc.go @@ -3,4 +3,4 @@ // Package teams 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 65% rename from internal/teams/setup.go rename to internal/serveredition/setup.go index a68a80ff9..75d923a45 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,9 +10,9 @@ 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/users" + seapi "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/api" + seauth "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/auth" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/serveredition/users" ) func init() { @@ -23,12 +23,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 { @@ -52,13 +52,13 @@ func setupMultiUserOAuth(deps Dependencies) error { if sessionTTL == 0 { sessionTTL = 24 * time.Hour } - sessionManager := teamsauth.NewSessionManager(userStore, sessionTTL, false) // secure=false for localhost + sessionManager := seauth.NewSessionManager(userStore, sessionTTL, false) // secure=false for localhost // Create OAuth handler - oauthHandler := teamsauth.NewOAuthHandler(userStore, sessionManager, cfg, hmacKey, deps.Logger) + oauthHandler := seauth.NewOAuthHandler(userStore, sessionManager, cfg, hmacKey, deps.Logger) // Create auth middleware - authMiddleware := teamsauth.NewTeamsAuthMiddleware(sessionManager, userStore, cfg, hmacKey, deps.Logger) + authMiddleware := seauth.NewServerEditionAuthMiddleware(sessionManager, userStore, cfg, hmacKey, deps.Logger) // Register OAuth routes on the router. // Login and callback are public (no auth required). @@ -71,11 +71,11 @@ func setupMultiUserOAuth(deps Dependencies) error { // All server edition endpoints that require session cookie or JWT authentication. // Mounted outside the API key group so session cookies work. - authEndpoints := teamsapi.NewAuthEndpoints(userStore, sessionManager, cfg, hmacKey, deps.Logger) + authEndpoints := seapi.NewAuthEndpoints(userStore, sessionManager, cfg, hmacKey, deps.Logger) configPath := config.GetConfigPath(deps.Config.DataDir) - adminHandlers := teamsapi.NewAdminHandlers(userStore, nil, sessionManager, cfg.AdminEmails, sharedServers, deps.Config, configPath, deps.ManagementService, deps.Logger) - userHandlers := teamsapi.NewUserHandlers(userStore, sharedServers, deps.StorageManager, hmacKey, deps.Logger) - userActivityHandlers := teamsapi.NewUserActivityHandlers(nil, userStore, sharedServers, deps.Logger) + adminHandlers := seapi.NewAdminHandlers(userStore, nil, sessionManager, cfg.AdminEmails, sharedServers, deps.Config, configPath, deps.ManagementService, deps.Logger) + userHandlers := seapi.NewUserHandlers(userStore, sharedServers, deps.StorageManager, hmacKey, deps.Logger) + userActivityHandlers := seapi.NewUserActivityHandlers(nil, userStore, sharedServers, deps.Logger) deps.Router.Group(func(r chi.Router) { r.Use(authMiddleware.Middleware()) diff --git a/internal/teams/setup_test.go b/internal/serveredition/setup_test.go similarity index 93% rename from internal/teams/setup_test.go rename to internal/serveredition/setup_test.go index dd7375561..dc28f75a5 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" @@ -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 100% rename from internal/teams/users/store.go rename to internal/serveredition/users/store.go 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 9e775d05f..107c509e9 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, // ServerEdition-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 @@ -283,7 +283,7 @@ func TestSaveServerSyncFieldCoverage(t *testing.T) { continue } if fieldName == "Shared" { - // Teams-only field, persisted in JSON config not BBolt + // ServerEdition-only field, persisted in JSON config not BBolt continue } if fieldName == "SkipQuarantine" {