Documentation
¶
Overview ¶
MCP OAuth 2.0 + PKCE flow for HTTP/SSE servers that gate tool calls behind a bearer token. Mirrors src/services/mcp/auth.ts (which leans on the official @modelcontextprotocol/sdk; we implement the spec directly).
The flow conduit performs:
Discover authorization server metadata (RFC 8414) by GETting `<server-base>/.well-known/oauth-authorization-server`. Falls back to `<server-base>/.well-known/openid-configuration` for AS that ship the OIDC discovery doc only.
If the metadata advertises a registration_endpoint and we don't have a client_id cached, perform Dynamic Client Registration (RFC 7591). Many MCPs do require DCR — they don't pre-register clients.
Generate PKCE verifier + challenge (S256), generate state.
Bind a localhost callback listener and open the user's browser at `<authorization_endpoint>?response_type=code&...&code_challenge=...`.
Receive the redirect with `code` + `state`, validate state.
Exchange code at `<token_endpoint>` for {access_token, refresh_token, expires_in}.
Persist tokens in secure storage under a per-server key.
On a subsequent 401, RefreshServerToken() exchanges the refresh_token for a fresh access_token without prompting the user again.
Package mcp implements the MCP (Model Context Protocol) host. It supports stdio, SSE, and HTTP transports — the three transports used by real Claude Code (decoded/client.ts, types.ts).
Index ¶
- Variables
- func AuthorizeURL(metadata *AuthServerMetadata, ...) string
- func AuthorizeURLForFlow(ctx context.Context, serverName, serverURL string, scopes []string) (authURL string, listener *auth.CallbackListener, ...)
- func DeleteServerToken(s secure.Storage, serverName string) error
- func DiscoverProtectedResource(ctx context.Context, serverURL string) (string, error)
- func IsDisabled(name, cwd string) bool
- func LoadConfigs(cwd string) (map[string]ServerConfig, error)
- func NormalizeServerName(name string) string
- func SaveServerToken(s secure.Storage, serverName string, tokens *OAuthTokens) error
- func SetDisabled(name, cwd string, disabled bool) error
- type AuthServerMetadata
- type CallResult
- type Client
- type ClientRegistration
- type ConnectedServer
- type ContentBlock
- type Manager
- func (m *Manager) AllTools() []NamedTool
- func (m *Manager) CallTool(ctx context.Context, qualifiedName string, input []byte) (CallResult, error)
- func (m *Manager) Close()
- func (m *Manager) ConnectAll(ctx context.Context, cwd string) error
- func (m *Manager) DisconnectServer(name string)
- func (m *Manager) ListResources(ctx context.Context, serverName string) ([]ResourceDef, error)
- func (m *Manager) PendingApprovals() []string
- func (m *Manager) PendingNeedsAuth() []string
- func (m *Manager) ReadResource(ctx context.Context, serverName, uri string) ([]ResourceContent, error)
- func (m *Manager) Reconnect(ctx context.Context, name, cwd string) error
- func (m *Manager) SecureStore() secure.Storage
- func (m *Manager) ServerInstructions() map[string]string
- func (m *Manager) Servers() []*ConnectedServer
- func (m *Manager) SetSecureStore(s secure.Storage)
- func (m *Manager) SyncPluginServers(ctx context.Context, cwd string)
- type McpJSON
- type NamedTool
- type OAuthTokens
- func ExchangeCode(ctx context.Context, ...) (*OAuthTokens, error)
- func LoadServerToken(s secure.Storage, serverName string) (*OAuthTokens, error)
- func PerformOAuthFlow(ctx context.Context, serverName, serverURL string, scopes []string, ...) (*OAuthTokens, error)
- func RefreshToken(ctx context.Context, tokenEndpoint, refreshToken, clientID string) (*OAuthTokens, error)
- type ProtectedResourceMetadata
- type ResourceContent
- type ResourceDef
- type ServerConfig
- type ServerStatus
- type ToolDef
Constants ¶
This section is empty.
Variables ¶
ErrUnauthorized is returned by HTTP/SSE client calls when the server responds with HTTP 401. Callers — typically Manager — branch on this to mark the server as StatusNeedsAuth and surface McpAuthTool.
Functions ¶
func AuthorizeURL ¶
func AuthorizeURL(metadata *AuthServerMetadata, clientID, redirectURI, state, codeChallenge string, scopes []string) string
AuthorizeURL builds the URL the user opens in their browser. State and codeChallenge are provided by the caller (the listener side already has them). scopes may be empty — many MCPs use the metadata's scopes_supported, others ignore the parameter entirely.
func AuthorizeURLForFlow ¶
func AuthorizeURLForFlow(ctx context.Context, serverName, serverURL string, scopes []string) ( authURL string, listener *auth.CallbackListener, state, verifier, redirectURI string, client *ClientRegistration, md *AuthServerMetadata, err error, )
AuthorizeURLForFlow returns the URL to open along with the listener so callers (like McpAuthTool) that want to surface the URL to the LLM before opening a browser can do so. The caller must Close the listener after exchange (or on error).
This is a lower-level alternative to PerformOAuthFlow. The full flow is:
url, listener, state, verifier, client, md, _ := AuthorizeURLForFlow(...)
defer func() { _ = listener.Close() }()
// surface url to the user/LLM
code, _ := listener.Wait(ctx, state)
tokens, _ := ExchangeCode(ctx, md.TokenEndpoint, code, redirectURI, client.ClientID, verifier)
func DeleteServerToken ¶
DeleteServerToken removes the persisted OAuthTokens bundle. Idempotent — returns nil if no bundle was present.
func DiscoverProtectedResource ¶
DiscoverProtectedResource probes the MCP server for an RFC 9728 `/.well-known/oauth-protected-resource` document and returns the first authorization_servers entry. Mirrors what CC's MCP SDK does on a 401.
Returns ("", nil) when the server doesn't expose this document — the caller can then fall through to the legacy "discover at server origin" path for MCPs where the server and the AS share an origin.
func IsDisabled ¶
IsDisabled returns true if the named server is in disabledMcpServers for cwd. Mirrors isMcpServerDisabled() in src/services/mcp/config.ts.
func LoadConfigs ¶
func LoadConfigs(cwd string) (map[string]ServerConfig, error)
LoadConfigs loads MCP server configs from all config sources, in priority order (later entries override earlier ones with the same name):
- user — ~/.claude.json → mcpServers (global)
- local — ~/.claude.json → projects[cwd].mcpServers (per-project)
- project — every .mcp.json from filesystem root down to cwd (closer wins)
Mirrors getMcpConfigsByScope() in src/services/mcp/config.ts.
func NormalizeServerName ¶
NormalizeServerName converts an MCP server name to a safe tool-name prefix. "my-server" → "my_server__" (double underscore separator matches TS convention)
func SaveServerToken ¶
func SaveServerToken(s secure.Storage, serverName string, tokens *OAuthTokens) error
SaveServerToken persists an OAuthTokens bundle for the named MCP server.
func SetDisabled ¶
SetDisabled adds or removes name from disabledMcpServers in ~/.claude.json → projects[cwd]. Mirrors setMcpServerEnabled() in src/services/mcp/config.ts.
Types ¶
type AuthServerMetadata ¶
type AuthServerMetadata struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
ScopesSupported []string `json:"scopes_supported,omitempty"`
ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`
}
AuthServerMetadata is the subset of RFC 8414 / RFC 8615 metadata fields we use. JSON tags match the spec.
func DiscoverAuthServer ¶
func DiscoverAuthServer(ctx context.Context, serverURL string) (*AuthServerMetadata, error)
DiscoverAuthServer fetches the authorization server metadata for the MCP server at serverURL.
Discovery order (matches CC's MCP SDK):
RFC 9728 protected-resource document at `<server-origin>/.well-known/oauth-protected-resource`. If present, it points at the real authorization server origin (which may be different from the MCP server's own origin — e.g. github.com as the AS for an MCP at api.githubcopilot.com).
RFC 8414 metadata at the (resolved) AS origin's `/.well-known/oauth-authorization-server`.
OIDC discovery at the (resolved) AS origin's `/.well-known/openid-configuration`.
If protected-resource discovery fails or returns no AS, we fall back to treating the MCP server's own origin as the AS — that's the right behavior for MCPs where the two are colocated.
type CallResult ¶
type CallResult struct {
Content []ContentBlock `json:"content"`
IsError bool `json:"isError,omitempty"`
}
CallResult is the shape returned by tools/call.
type Client ¶
type Client interface {
// Initialize sends the MCP initialize handshake and returns the server's
// instructions string (empty if none) for injection into the system prompt.
Initialize(ctx context.Context) (string, error)
// ListTools fetches the server's tool list.
ListTools(ctx context.Context) ([]ToolDef, error)
// CallTool invokes a tool and returns its result.
CallTool(ctx context.Context, name string, input json.RawMessage) (CallResult, error)
// ListResources fetches the server's resource list (MCP resources/list).
ListResources(ctx context.Context) ([]ResourceDef, error)
// ReadResource reads the contents of one resource (MCP resources/read).
ReadResource(ctx context.Context, uri string) ([]ResourceContent, error)
// Close shuts down the transport.
Close() error
}
Client is the interface both stdio and HTTP/SSE clients implement.
func NewHTTPClient ¶
NewHTTPClient creates a Client that sends JSON-RPC requests to url via HTTP POST.
func NewStdioClient ¶
NewStdioClient creates a Client that runs cmd with the given args and env, communicating over stdio.
type ClientRegistration ¶
type ClientRegistration struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"`
ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"`
RedirectURIs []string `json:"redirect_uris,omitempty"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
}
ClientRegistration is the response from a Dynamic Client Registration call (RFC 7591 §3.2.1). We store the client_id (and secret if any) per-server so subsequent token refreshes don't re-register.
func RegisterClient ¶
func RegisterClient(ctx context.Context, registrationEndpoint, clientName string, redirectURIs []string) (*ClientRegistration, error)
RegisterClient performs Dynamic Client Registration (RFC 7591). Called when AuthServerMetadata.RegistrationEndpoint is set and we don't already have a cached client_id. clientName/redirectURI must be supplied.
type ConnectedServer ¶
type ConnectedServer struct {
Name string
Config ServerConfig
Status ServerStatus
Disabled bool // true when the server is in disabledMcpServers
Tools []ToolDef
Instructions string // server-provided instructions from initialize response
Error string // set when Status == StatusFailed
// contains filtered or unexported fields
}
ConnectedServer holds a live connection to one MCP server.
type ContentBlock ¶
ContentBlock is a single block in a tools/call result.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager connects to all configured MCP servers and keeps them alive. It is the single source of truth for which servers are connected and what tools they expose. Mirrors MCPConnectionManager.tsx.
func (*Manager) AllTools ¶
AllTools returns all tools across all connected servers, with names prefixed by "<serverName>__" to avoid collisions.
func (*Manager) CallTool ¶
func (m *Manager) CallTool(ctx context.Context, qualifiedName string, input []byte) (CallResult, error)
CallTool dispatches a tool call to the appropriate server. qualifiedName is "<prefix><toolName>" as returned by AllTools.
func (*Manager) ConnectAll ¶
ConnectAll loads configs for cwd and connects to every server in parallel. Errors from individual servers are captured in the server's Error field; ConnectAll itself only returns an error if config loading fails.
func (*Manager) DisconnectServer ¶
DisconnectServer closes and removes a server from the live connection map. Used when disabling a server at runtime.
func (*Manager) ListResources ¶
ListResources fetches resources from the named server.
func (*Manager) PendingApprovals ¶
PendingApprovals returns the names of project-scope MCP servers that are awaiting user approval (StatusNeedsApproval), in deterministic order. Used by the TUI to drive the approval dialog on startup.
func (*Manager) PendingNeedsAuth ¶
PendingNeedsAuth returns the names of HTTP/SSE/WS MCP servers that returned 401 on connect — the caller (TUI) surfaces these via the /mcp panel and the McpAuthTool pseudo-tool.
func (*Manager) ReadResource ¶
func (m *Manager) ReadResource(ctx context.Context, serverName, uri string) ([]ResourceContent, error)
ReadResource reads a resource from the named server.
func (*Manager) Reconnect ¶
Reconnect closes and re-connects a single named server. It re-reads the config so any edits to .claude.json take effect.
func (*Manager) SecureStore ¶
SecureStore returns the configured secure store (nil if unset). Used by McpAuthTool to persist newly-obtained tokens.
func (*Manager) ServerInstructions ¶
ServerInstructions returns a map of serverName → instructions for all connected servers that provided instructions in their initialize response. These are injected into the system prompt as additional context.
func (*Manager) Servers ¶
func (m *Manager) Servers() []*ConnectedServer
Servers returns a snapshot of all server states.
func (*Manager) SetSecureStore ¶
SetSecureStore wires a secure.Storage so the manager can load persisted OAuth bearer tokens for HTTP/SSE/WS servers. Callers that don't need MCP OAuth can leave this unset.
func (*Manager) SyncPluginServers ¶
SyncPluginServers re-reads configs and reconciles plugin-scoped MCP servers: new plugin servers are connected, removed plugin servers are disconnected. Non-plugin servers (user/project/local scope) are left untouched. Called after /plugin install or /plugin uninstall.
type McpJSON ¶
type McpJSON struct {
McpServers map[string]ServerConfig `json:"mcpServers"`
}
McpJSON is the shape of ~/.claude/mcp.json and .mcp.json.
type OAuthTokens ¶
type OAuthTokens struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
TokenType string `json:"token_type"`
ExpiresAt time.Time `json:"expires_at"`
Scope string `json:"scope,omitempty"`
// Embedded so token refresh can re-auth without another DCR roundtrip.
Client ClientRegistration `json:"client"`
TokenEndpoint string `json:"token_endpoint"`
}
OAuthTokens is the bundle persisted per-server. ExpiresAt is computed from the response's expires_in at issuance time so we don't need a server clock.
func ExchangeCode ¶
func ExchangeCode(ctx context.Context, tokenEndpoint, code, redirectURI, clientID, codeVerifier string) (*OAuthTokens, error)
ExchangeCode trades an authorization code for tokens at token_endpoint. codeVerifier is the original PKCE verifier (the AS will hash it and compare against the challenge it received in the authorize request).
func LoadServerToken ¶
func LoadServerToken(s secure.Storage, serverName string) (*OAuthTokens, error)
LoadServerToken returns the persisted OAuthTokens bundle for serverName. Returns secure.ErrNotFound when no bundle is present.
func PerformOAuthFlow ¶
func PerformOAuthFlow(ctx context.Context, serverName, serverURL string, scopes []string, browser auth.BrowserOpener) (*OAuthTokens, error)
PerformOAuthFlow drives the whole flow: discovery → optional DCR → PKCE + browser → callback → token exchange. Returns the OAuthTokens bundle ready to persist.
browser may be nil to use the default system browser launcher.
The returned tokens have Client and TokenEndpoint populated so a later RefreshToken call can be made without re-running discovery.
func RefreshToken ¶
func RefreshToken(ctx context.Context, tokenEndpoint, refreshToken, clientID string) (*OAuthTokens, error)
RefreshToken trades a refresh_token for fresh tokens. Used on 401 when the access token has expired. Returns the new bundle (refresh_token may be rotated; persist the returned value).
func (OAuthTokens) String ¶
func (OAuthTokens) String() string
Redact prevents accidental logging of bearer tokens.
type ProtectedResourceMetadata ¶
type ProtectedResourceMetadata struct {
Resource string `json:"resource"`
AuthorizationServers []string `json:"authorization_servers"`
ScopesSupported []string `json:"scopes_supported,omitempty"`
}
ProtectedResourceMetadata is the subset of RFC 9728 fields we use. The MCP spec requires servers that gate tools behind OAuth to expose this document so clients can find the right authorization server (which may be a different origin from the MCP server itself — e.g. GitHub Copilot's MCP at api.githubcopilot.com points at github.com as its AS).
type ResourceContent ¶
type ResourceContent struct {
URI string `json:"uri"`
MimeType string `json:"mimeType,omitempty"`
Text string `json:"text,omitempty"`
Blob string `json:"blob,omitempty"` // base64
}
ResourceContent is one content item from resources/read.
type ResourceDef ¶
type ResourceDef struct {
URI string `json:"uri"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
MimeType string `json:"mimeType,omitempty"`
}
ResourceDef is one resource entry from resources/list.
type ServerConfig ¶
type ServerConfig struct {
// Type is "stdio" | "sse" | "http". Empty means "stdio".
Type string `json:"type,omitempty"`
// Stdio fields
Command string `json:"command,omitempty"`
Args []string `json:"args,omitempty"`
Env map[string]string `json:"env,omitempty"`
// SSE / HTTP fields
URL string `json:"url,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
// Source is the config file path this entry was loaded from.
// Not serialized — set at load time by LoadConfigs.
Source string `json:"-"`
// Scope is "user" | "local" | "project" | "plugin".
// Not serialized — set at load time by LoadConfigs.
Scope string `json:"-"`
// PluginName is set when the server was defined by an installed plugin.
// Not serialized.
PluginName string `json:"-"`
}
ServerConfig describes how to connect to one MCP server. The discriminant field is "type" (stdio/sse/http); stdio omits it for backwards-compat with older .mcp.json files.
type ServerStatus ¶
type ServerStatus string
ServerStatus tracks connection state.
const ( StatusPending ServerStatus = "pending" StatusConnected ServerStatus = "connected" StatusFailed ServerStatus = "failed" StatusDisconnected ServerStatus = "disconnected" // StatusNeedsApproval is set on a project-scope server (loaded from // .mcp.json) that the user hasn't explicitly approved or denied. // Mirrors CC's MCPServerApprovalDialog gate. StatusNeedsApproval ServerStatus = "needs-approval" // StatusNeedsAuth is set on an HTTP/SSE server that returned 401 on // connect — the user must complete an OAuth flow (McpAuthTool or // /mcp auth <name>) before the server's tools become available. StatusNeedsAuth ServerStatus = "needs-auth" )