mcp

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 4, 2026 License: MIT Imports: 21 Imported by: 0

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:

  1. 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.

  2. 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.

  3. Generate PKCE verifier + challenge (S256), generate state.

  4. Bind a localhost callback listener and open the user's browser at `<authorization_endpoint>?response_type=code&...&code_challenge=...`.

  5. Receive the redirect with `code` + `state`, validate state.

  6. Exchange code at `<token_endpoint>` for {access_token, refresh_token, expires_in}.

  7. 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

Constants

This section is empty.

Variables

View Source
var ErrUnauthorized = errors.New("mcp: unauthorized (401)")

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

func DeleteServerToken(s secure.Storage, serverName string) error

DeleteServerToken removes the persisted OAuthTokens bundle. Idempotent — returns nil if no bundle was present.

func DiscoverProtectedResource

func DiscoverProtectedResource(ctx context.Context, serverURL string) (string, error)

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

func IsDisabled(name, cwd string) bool

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):

  1. user — ~/.claude.json → mcpServers (global)
  2. local — ~/.claude.json → projects[cwd].mcpServers (per-project)
  3. project — every .mcp.json from filesystem root down to cwd (closer wins)

Mirrors getMcpConfigsByScope() in src/services/mcp/config.ts.

func NormalizeServerName

func NormalizeServerName(name string) string

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

func SetDisabled(name, cwd string, disabled bool) error

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):

  1. 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).

  2. RFC 8414 metadata at the (resolved) AS origin's `/.well-known/oauth-authorization-server`.

  3. 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

func NewHTTPClient(url string, headers map[string]string) Client

NewHTTPClient creates a Client that sends JSON-RPC requests to url via HTTP POST.

func NewStdioClient

func NewStdioClient(command string, args []string, env map[string]string) (Client, error)

NewStdioClient creates a Client that runs cmd with the given args and env, communicating over stdio.

func NewWebSocketClient

func NewWebSocketClient(url string, headers map[string]string) Client

NewWebSocketClient creates a Client that communicates via WebSocket.

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

type ContentBlock struct {
	Type string `json:"type"`
	Text string `json:"text,omitempty"`
}

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 NewManager

func NewManager() *Manager

NewManager returns an empty Manager.

func (*Manager) AllTools

func (m *Manager) AllTools() []NamedTool

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) Close

func (m *Manager) Close()

Close shuts down all server connections.

func (*Manager) ConnectAll

func (m *Manager) ConnectAll(ctx context.Context, cwd string) error

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

func (m *Manager) DisconnectServer(name string)

DisconnectServer closes and removes a server from the live connection map. Used when disabling a server at runtime.

func (*Manager) ListResources

func (m *Manager) ListResources(ctx context.Context, serverName string) ([]ResourceDef, error)

ListResources fetches resources from the named server.

func (*Manager) PendingApprovals

func (m *Manager) PendingApprovals() []string

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

func (m *Manager) PendingNeedsAuth() []string

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

func (m *Manager) Reconnect(ctx context.Context, name, cwd string) error

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

func (m *Manager) SecureStore() secure.Storage

SecureStore returns the configured secure store (nil if unset). Used by McpAuthTool to persist newly-obtained tokens.

func (*Manager) ServerInstructions

func (m *Manager) ServerInstructions() map[string]string

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

func (m *Manager) SetSecureStore(s secure.Storage)

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

func (m *Manager) SyncPluginServers(ctx context.Context, cwd string)

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 NamedTool

type NamedTool struct {
	ServerName    string
	Prefix        string
	Def           ToolDef
	QualifiedName string
}

NamedTool is a tool from an MCP server with its qualified name.

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"
)

type ToolDef

type ToolDef struct {
	Name        string          `json:"name"`
	Description string          `json:"description"`
	InputSchema json.RawMessage `json:"inputSchema,omitempty"`
}

ToolDef is the tool shape returned by tools/list.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL