// Package mcpgateway aggregates upstream MCP servers behind GoModel's // authenticated /mcp endpoints. The gateway terminates the MCP protocol on // both legs: it is an MCP server to clients and an MCP client to upstreams. // It is also the credential boundary — client bearer tokens never reach an // upstream; upstream credentials come only from server configuration. package mcpgateway import ( "maps" "slices" "time" "github.com/enterpilot/gomodel/config" ) // ServerStatus describes the runtime connection state of one upstream server. type ServerStatus string const ( // StatusDisabled means the server is declared but switched off. StatusDisabled ServerStatus = "disabled" // StatusConnecting means no session has been established yet. StatusConnecting ServerStatus = "connecting" // StatusConnected means the session is live and the catalog is fresh. StatusConnected ServerStatus = "connected" // StatusDegraded means the last connect or listing failed; any previous // catalog is kept (stale carry-forward) and the server is re-probed. StatusDegraded ServerStatus = "degraded" ) // ServerSpec is the runtime-normalized definition of one upstream server, // merged from declarative config (Managed=true) and the admin store. type ServerSpec struct { // Name is the stable ASCII slug used for routing and namespacing. Name string DisplayName string URL string Transport string // Headers are sent to the upstream, secret references resolved. Headers map[string]string // HeaderReferences holds the stored value of each header of an // admin-managed server that holds a secret reference, so the admin API // can show the reference instead of masking the header. HeaderReferences map[string]string Command string Args []string Env map[string]string Description string Enabled bool AllowedTools []string DisallowedTools []string UserPaths []string // DisallowedUserPaths hides the server from these subtrees; it wins over // UserPaths. DisallowedUserPaths []string ToolTimeout time.Duration // Managed marks specs declared in config.yaml / MCP_SERVERS. They override // admin-store rows with the same name and are read-only in the dashboard. Managed bool // oauth supplies the access token of an operator OAuth sign-in; nil // when the server is not signed in. A new sign-in is a new source, so // the spec changes and the manager redials with it. oauth *oauthSource } // SpecFromConfig converts one declarative config entry into a runtime spec. func SpecFromConfig(name string, cfg config.MCPServerConfig) ServerSpec { return ServerSpec{ Name: name, DisplayName: name, URL: cfg.URL, Transport: cfg.Transport, Headers: maps.Clone(cfg.Headers), Command: cfg.Command, Args: slices.Clone(cfg.Args), Env: maps.Clone(cfg.Env), Description: cfg.Description, Enabled: config.MCPServerEnabled(cfg), AllowedTools: slices.Clone(cfg.AllowedTools), DisallowedTools: slices.Clone(cfg.DisallowedTools), UserPaths: normalizeUserPaths(cfg.UserPaths), DisallowedUserPaths: normalizeUserPaths(cfg.DisallowedUserPaths), ToolTimeout: cfg.ToolTimeout, Managed: true, } } // equal reports whether two specs describe the same upstream configuration, // so the manager can keep an existing session across no-op reloads. func (s ServerSpec) equal(other ServerSpec) bool { return s.Name == other.Name && s.DisplayName == other.DisplayName && s.URL == other.URL && s.Transport == other.Transport && maps.Equal(s.Headers, other.Headers) && maps.Equal(s.HeaderReferences, other.HeaderReferences) && s.Command == other.Command && slices.Equal(s.Args, other.Args) && maps.Equal(s.Env, other.Env) && s.Description == other.Description && s.Enabled == other.Enabled && slices.Equal(s.AllowedTools, other.AllowedTools) && slices.Equal(s.DisallowedTools, other.DisallowedTools) && slices.Equal(s.UserPaths, other.UserPaths) && slices.Equal(s.DisallowedUserPaths, other.DisallowedUserPaths) && s.ToolTimeout == other.ToolTimeout && s.Managed == other.Managed && s.oauth == other.oauth } // withoutAccessPolicy clears the gateway-side access policy (tool filters and // user-path scopes), so callers can tell a policy-only edit, applied in place, // from one that changes the upstream connection and needs a redial. func (s ServerSpec) withoutAccessPolicy() ServerSpec { s.AllowedTools = nil s.DisallowedTools = nil s.UserPaths = nil s.DisallowedUserPaths = nil return s } // visibleTo reports whether a caller with userPath may see and use this // server: inside UserPaths (empty means everyone) and outside every // DisallowedUserPaths subtree. func (s ServerSpec) visibleTo(userPath string) bool { return userPathAllowed(userPath, s.UserPaths) && !userPathWithin(userPath, s.DisallowedUserPaths) } // ServerView is a point-in-time snapshot of one upstream for admin and // dashboard consumption. type ServerView struct { Spec ServerSpec Status ServerStatus LastError string ToolCount int // ExcludedToolCount counts discovered tools hidden by the tool filters. ExcludedToolCount int PromptCount int // ResourceCount includes resource templates. ResourceCount int ConnectedAt time.Time // AuthRequired reports that the last connect was answered 401 // Unauthorized: the server needs a credential or an OAuth sign-in. AuthRequired bool // OAuth is the operator sign-in state; nil when never signed in. OAuth *OAuthView }