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

Filter by extension

Filter by extension

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

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Expand Down Expand Up @@ -44,14 +44,14 @@
| 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) |
Expand All @@ -68,9 +68,11 @@

### 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": {
Expand Down Expand Up @@ -117,7 +119,7 @@
### Server Testing

```bash
go test -tags server ./internal/teams/... -v -race # All server unit + integration tests
go test -tags server ./internal/serveredition/... -v -race # All server unit + integration tests
go build -tags server ./cmd/mcpproxy # Build server edition
go build ./cmd/mcpproxy # Verify personal edition unaffected
```
Expand Down Expand Up @@ -731,15 +733,12 @@
- 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)
Expand All @@ -756,7 +755,6 @@
- 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)
Expand Down
2 changes: 1 addition & 1 deletion cmd/mcpproxy/edition.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package main

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

package main

// Server edition features are registered via init() functions in their
// respective packages. The actual setup happens when the server calls
// serveredition.SetupAll() during HTTP server initialization (see internal/server/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"
44 changes: 22 additions & 22 deletions cmd/mcpproxy/status_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,24 @@ import (

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

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

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

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

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

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

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

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

package main

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

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

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

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

This file was deleted.

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

This file was deleted.

2 changes: 1 addition & 1 deletion docs/features/settings-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ friendly, prioritized form sections instead of raw JSON:
isolation, sensitive-data detection, output validation, output sanitisation,
activity retention, logging, TLS, …).
- **Raw JSON** — the full Monaco editor, kept as an escape hatch.
- **Teams** — server edition only.
- **Server edition** — multi-user settings (server build only).

## How saving works

Expand Down
2 changes: 1 addition & 1 deletion internal/auth/agent_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type AgentToken struct {
CreatedAt time.Time `json:"created_at"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
Revoked bool `json:"revoked"`
UserID string `json:"user_id,omitempty"` // Owner user ID (teams edition)
UserID string `json:"user_id,omitempty"` // Owner user ID (server edition)
}

// IsExpired returns true if the token has passed its expiry time.
Expand Down
38 changes: 36 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`
}

Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
Loading
Loading