mcpauth

package
v0.631.1 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2026 License: MIT Imports: 34 Imported by: 0

Documentation

Overview

Package mcpauth implements Phase 2 of the MCP client authentication plan: persistent OAuth 2.1 credential storage and a process-wide manager that builds mcp-go transport.OAuthConfig values from Pando's declarative config.MCPAuth blocks. It does not drive the interactive browser/callback flow (that is Phase 3); it only stores tokens and dynamically-registered client credentials so that Phase 3 (and refresh) can reuse them.

Index

Constants

View Source
const DefaultCallbackPath = "/mcp/oauth/callback"

DefaultCallbackPath is the path component of the local OAuth redirect URI.

View Source
const DefaultCallbackPort = 19876

DefaultCallbackPort is the local port the Phase 3 callback server listens on when a server's config does not set Auth.OAuth.CallbackPort.

View Source
const DefaultCallbackTimeout = 5 * time.Minute

DefaultCallbackTimeout is how long CallbackServer.Wait blocks for the browser redirect to arrive before giving up, unless the caller passes a different timeout.

Variables

View Source
var ErrAuthorizationRequired = errors.New("mcp: oauth authorization required")

ErrAuthorizationRequired is returned (or wrapped) by Manager methods when a server needs an interactive OAuth authorization step that this phase does not perform. Phase 3 uses IsAuthorizationRequired to detect this condition (from this error or from mcp-go's own client.IsOAuthAuthorizationRequiredError) and drive the browser/callback flow.

Functions

func BuildTLSConfig

func BuildTLSConfig(srv config.MCPServer) (*tls.Config, error)

BuildTLSConfig builds a *tls.Config for srv's TLS/mTLS options (Auth.ClientCert/ClientKey/ClientKeyPassword/CACert/SkipTLSVerify), or returns (nil, nil) when none of those fields are set — the common case, meaning "use whatever default transport the caller already has".

srv should already have its secrets resolved via config.ResolveMCPServerSecrets so that Auth.ClientKeyPassword is plaintext; BuildTLSConfig does no decryption of its own.

This lives in internal/mcpauth rather than internal/mcpclient so that internal/mcpauth/login.go (the interactive OAuth flow) can also apply it to the OAuthHandler's own HTTP client without creating an import cycle — internal/mcpclient already imports internal/mcpauth, so the reverse dependency is not available.

func CanonicalResourceURI

func CanonicalResourceURI(raw string) (string, error)

CanonicalResourceURI implements the RFC 8707 canonical resource-server URI this package uses as the "resource" parameter on client_credentials token requests: lowercase scheme and host, default ports (80 for http, 443 for https) stripped, fragment dropped, trailing slash on a non-root path dropped, path otherwise preserved as-is. The query string is dropped too (a resource identifier is scheme+host+path, not a request URI).

func HTTPClientForTLS

func HTTPClientForTLS(tlsCfg *tls.Config, timeout time.Duration) *http.Client

HTTPClientForTLS returns an *http.Client using TransportForTLS(tlsCfg) (a nil Transport falls back to http.DefaultTransport, matching the zero-value http.Client behavior) with the given timeout.

func IsAuthorizationRequired

func IsAuthorizationRequired(err error) bool

IsAuthorizationRequired reports whether err indicates that an MCP server needs interactive OAuth authorization, whether that error originated in this package (ErrAuthorizationRequired) or in mcp-go itself (client.IsOAuthAuthorizationRequiredError, transport.ErrOAuthAuthorizationRequired).

func IsInteractive

func IsInteractive() bool

IsInteractive reports whether stdin/stdout look like a usable terminal, so callers can decide between driving the interactive browser flow and failing fast with actionable instructions.

func NonInteractiveLoginError

func NonInteractiveLoginError(serverName string) error

NonInteractiveLoginError is returned by callers (agent/tool layer) that detect a server needs authorization but cannot drive an interactive login themselves (no TTY, running as a background tool call, etc.).

func TransportForTLS

func TransportForTLS(tlsCfg *tls.Config) http.RoundTripper

TransportForTLS returns an *http.Transport configured with tlsCfg, cloned from http.DefaultTransport so connection pooling/proxy/timeouts behave exactly like the library defaults elsewhere. It returns nil when tlsCfg is nil, so callers can treat a nil result as "no custom transport needed" (e.g. NewScopeCapturingTransport(nil) already falls back to http.DefaultTransport, and an http.Client with a nil Transport already uses http.DefaultTransport).

Types

type CallbackServer

type CallbackServer struct {
	// contains filtered or unexported fields
}

CallbackServer is a short-lived local HTTP server that receives the OAuth authorization redirect (the "authorization code" leg of the flow) on 127.0.0.1. It is deliberately narrow: it only ever answers the configured callback path, only ever binds to loopback, and resolves at most one authorization attempt before it must be closed and recreated.

func StartCallbackServer

func StartCallbackServer(redirectURI string) (*CallbackServer, error)

StartCallbackServer parses redirectURI to determine which loopback port and path to listen on (falling back to DefaultCallbackPort / DefaultCallbackPath when redirectURI is empty or fails to parse as an absolute http(s) URL), starts listening, and returns the running server.

Only 127.0.0.1 (or localhost, normalized to 127.0.0.1) is ever bound; a redirectURI pointing anywhere else is rejected rather than silently listening on an unexpected interface.

func (*CallbackServer) Close

func (cs *CallbackServer) Close() error

Close shuts down the HTTP server and releases the listener. It is idempotent: calling it more than once (or after the server already shut itself down) is a no-op.

func (*CallbackServer) RedirectURI

func (cs *CallbackServer) RedirectURI() string

RedirectURI returns the actual redirect_uri this server answers on, derived from the bound listener's port so the caller (Manager.Login) uses the port actually acquired rather than the one requested.

func (*CallbackServer) Wait

func (cs *CallbackServer) Wait(ctx context.Context, state string, timeout time.Duration) (code string, iss string, err error)

Wait blocks until the callback server receives a matching authorization response, an error response, ctx is cancelled, or timeout elapses (defaulting to DefaultCallbackTimeout when timeout <= 0). Only one Wait call may be in flight at a time.

It returns the authorization code and, per RFC 9207 §3, the "iss" query parameter the authorization server included on the redirect (empty if the server didn't send one). Callers MUST validate iss against the issuer recorded from AS metadata before exchanging code for tokens — Wait itself only captures the raw value; it does not know the expected issuer.

type Challenge

type Challenge struct {
	Scheme           string
	Error            string
	ErrorDescription string
	Scope            []string
	ResourceMetadata string
}

Challenge is a parsed WWW-Authenticate challenge (RFC 7235 §4.1), extended with the two parameters the MCP authorization spec cares about: the RFC 6750 §3.1 "insufficient_scope" error (with its space-separated "scope" value) and the RFC 9728 §5.1 "resource_metadata" parameter.

func ParseChallenge

func ParseChallenge(header string) Challenge

ParseChallenge parses a WWW-Authenticate header value into a single Challenge. The header value may itself contain more than one challenge — RFC 7235's "1#challenge" grammar allows several challenges separated by top-level commas in one header line, and RFC 7230 §3.2.2 separately allows a header field to be repeated across multiple lines (which HTTP semantics permit combining into one comma-joined value). Callers with several raw header lines (resp.Header.Values("WWW-Authenticate")) should join them with ", " before calling ParseChallenge, or call it once per line and keep whichever result has Scheme=="Bearer" — see ParseChallenges for a helper that does this automatically.

When the header describes several challenges, ParseChallenge prefers the "Bearer" scheme challenge (the one the MCP OAuth flows and step-up re-authorization care about); if none is a Bearer challenge, the first parsed challenge is returned. An empty or unparseable header yields a zero-value Challenge.

func ParseChallenges

func ParseChallenges(headers []string) Challenge

ParseChallenges joins multiple WWW-Authenticate header lines (as returned by http.Header.Values) and parses them with ParseChallenge. It is safe to call with an empty slice (returns a zero-value Challenge).

type ClientCredentialsParams

type ClientCredentialsParams struct {
	// TokenEndpoint is the authorization server's token endpoint, normally
	// discovered via discoverTokenEndpoint (RFC 8414 metadata).
	TokenEndpoint string
	ClientID      string
	ClientSecret  string
	Scopes        []string
	// Resource is the RFC 8707 canonical resource-server URI to send as the
	// "resource" form parameter, or empty to omit it.
	Resource string
	// HTTPClient is used for the token request. A nil value falls back to a
	// plain client with discoveryHTTPTimeout.
	HTTPClient *http.Client
}

ClientCredentialsParams holds everything needed to mint (or re-mint) an OAuth 2.1 client_credentials access token (RFC 6749 §4.4) for one MCP server.

type ClientInfo

type ClientInfo struct {
	ClientID        string `json:"clientID,omitempty"`
	ClientSecret    string `json:"clientSecret,omitempty"`
	IssuedAt        int64  `json:"issuedAt,omitempty"`
	SecretExpiresAt int64  `json:"secretExpiresAt,omitempty"`
}

ClientInfo is the persisted form of a dynamically-registered (RFC 7591) OAuth client. mcp-go performs dynamic client registration but does not persist its result across process restarts; this fills that gap.

type Entry

type Entry struct {
	// ServerURL is the server's base URL at the time these credentials were
	// stored. If a later config points the same server name at a different
	// URL, the credentials are treated as invalid (see Store.GetForURL) —
	// mirroring opencode's getForUrl invalidation-on-change behavior.
	ServerURL  string      `json:"serverURL,omitempty"`
	Tokens     *Tokens     `json:"tokens,omitempty"`
	ClientInfo *ClientInfo `json:"clientInfo,omitempty"`
	Metadata   *Metadata   `json:"metadata,omitempty"`
	// RequestedScopes is the accumulated set of OAuth scopes ever requested
	// for this server, across the initial login and any subsequent 403
	// insufficient_scope step-up re-authorizations (see stepup.go). The MCP
	// authorization spec requires clients to accumulate scopes on step-up
	// rather than replace them, so a later, narrower request never drops a
	// permission the user already granted.
	RequestedScopes []string  `json:"requestedScopes,omitempty"`
	UpdatedAt       time.Time `json:"updatedAt,omitempty"`
}

Entry is the persisted OAuth state for a single named MCP server.

type ErrInsufficientScope

type ErrInsufficientScope struct {
	ServerName string
	Scopes     []string
}

ErrInsufficientScope is returned when an MCP server rejects a request with HTTP 403 and a WWW-Authenticate challenge carrying error="insufficient_scope" (RFC 6750 §3.1): the currently-held access token is valid but lacks a scope the server now requires for this operation. Scopes is the accumulated set (previously requested union challenged) that a fresh login should request.

func IsInsufficientScope

func IsInsufficientScope(err error) (*ErrInsufficientScope, bool)

IsInsufficientScope reports whether err is (or wraps) an *ErrInsufficientScope, returning it for callers that want the challenged scope list.

func (*ErrInsufficientScope) Error

func (e *ErrInsufficientScope) Error() string

type LoginPrompt

type LoginPrompt struct {
	// OnAuthorizationURL is called once the authorization URL is ready,
	// whether or not the browser is opened automatically. Callers should
	// always print/display it so the user has a fallback if OpenBrowser
	// fails or is disabled.
	OnAuthorizationURL func(url string)

	// OpenBrowser, when true, makes Login attempt to open the authorization
	// URL in the user's default browser. A failure to open is reported via
	// OnBrowserError (if set) but never aborts the flow: OnAuthorizationURL
	// already gave the user the URL to open manually.
	OpenBrowser bool

	// OnBrowserError is called when OpenBrowser is true but launching the
	// browser failed. Optional.
	OnBrowserError func(err error)

	// ManualCode, when non-nil, is used instead of waiting on the local
	// callback server: it should block until the user has pasted back the
	// authorization code (or the full redirect URL) and the state Pando
	// generated, returning them so Login can validate and exchange them.
	// This is the fallback for headless/remote sessions where the local
	// callback server is unreachable from the browser that completes the
	// login.
	//
	// iss is the RFC 9207 §3 authorization-server issuer, when the caller was
	// able to parse it out of a pasted full redirect URL (its `iss` query
	// parameter); it is fed into the same validateIssuer check the HTTP
	// callback path uses. Return an empty iss (never an error) when only a
	// bare code was pasted and no iss is available to check — validateIssuer
	// treats that as "nothing to compare" unless the authorization server is
	// known (from cached metadata) to declare
	// authorization_response_iss_parameter_supported=true, in which case an
	// empty iss is correctly rejected rather than silently skipped.
	ManualCode func(ctx context.Context) (code string, state string, iss string, err error)

	// Timeout bounds how long Login waits for the callback (or ManualCode)
	// before giving up. Zero uses DefaultCallbackTimeout.
	Timeout time.Duration
}

LoginPrompt lets the caller (CLI today, TUI/WebUI later) drive the interactive parts of Login without Manager needing to know anything about terminals, browsers, or GUI dialogs.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager is a process-wide, concurrency-safe cache of OAuth configuration built from Pando's declarative config plus the persisted Store. It does not perform the interactive authorization-code exchange itself (Phase 3 owns that, driving the OAuthHandler obtained from mcp-go directly); it exists so that:

  • repeated calls to acquire an MCP client for the same server reuse the same TokenStore/OAuthConfig instead of re-reading disk every time,
  • dynamically-registered client credentials survive process restarts,
  • external changes to the credential store (another Pando process completing a login, a cron rotating tokens) are picked up,
  • concurrent 401s against the same access token trigger exactly one recovery instead of a thundering herd.

func Default

func Default() *Manager

Default returns the process-wide Manager singleton, backed by DefaultStore().

func NewManager

func NewManager(store *Store) *Manager

New401Manager-style constructors: NewManager builds a Manager over store. Pass mcpauth.DefaultStore() to share the default on-disk location, or a test-local Store (New(tempPath)) in tests.

func (*Manager) ClientCredentialsTransport

func (m *Manager) ClientCredentialsTransport(ctx context.Context, serverName string, resolved config.MCPServer, base http.RoundTripper) (http.RoundTripper, error)

ClientCredentialsTransport builds the http.RoundTripper internal/mcpclient wires into the plain (non-OAuth-handler) SSE/streamable-http transports for a server whose Auth.Type is MCPAuthOAuthClientCredentials: it discovers the token endpoint, then returns a bearerRoundTripper that mints and caches tokens through the Store on demand, wrapping base (the TLS-configured transport, or nil for the library default).

resolved must already have its secrets resolved via config.ResolveMCPServerSecrets.

func (*Manager) Do401

func (m *Manager) Do401(serverName string, token string, fn func() error) error

Do401 runs fn to recover from a 401 response that was observed with the given (now presumably stale) access token, ensuring that concurrent callers reporting a 401 for the *same* server and the *same* stale token share a single recovery attempt instead of each independently retrying (or worse, each independently kicking off an interactive re-authorization in Phase 3). This mirrors hermes' pending_401 deduplication.

The token parameter is part of the dedup key specifically so that once one recovery succeeds and rotates the token, a *new* 401 (with the new, different stale token) starts its own recovery rather than being incorrectly folded into a stale in-flight one.

func (*Manager) HandleInsufficientScope

func (m *Manager) HandleInsufficientScope(serverName string, challenged []string) error

HandleInsufficientScope is the single entry point transport-layer code (internal/mcpclient) calls when it detects a 403 insufficient_scope response for serverName: it accumulates challenged with any previously requested scopes (StepUpScopes), enforces maxStepUpAttempts for that accumulated set, and returns either a fresh *ErrInsufficientScope (for the caller to surface as "run pando mcp login <name>") or a terminal error once the cap is hit.

func (*Manager) HasTokens

func (m *Manager) HasTokens(serverName, serverURL string) bool

HasTokens reports whether a valid-looking (non-empty access token, URL matching) token set is persisted for serverName. It does not check expiry: an expired-but-refreshable token still counts, since mcp-go's OAuthHandler transparently refreshes it on next use.

func (*Manager) InvalidateClientRegistration

func (m *Manager) InvalidateClientRegistration(serverName string) error

InvalidateClientRegistration drops the persisted ClientInfo (dynamically registered client_id/client_secret) for serverName, without touching its tokens or requested-scope history. It is called when the authorization server rejects a request with the OAuth error invalid_client, which indicates the persisted registration is stale, revoked, or was never valid at this server — forcing a fresh RegisterClient on the next Login rather than repeating the same rejected credentials forever.

Static config (an explicit Auth.OAuth.ClientID) is never invalidated this way; callers must only invoke this when the server's config does not pin a ClientID, since a config-pinned value can't be "wrong" in the same sense — see Login and mcpclient's invalid_client handling for the guard.

func (*Manager) InvalidateIfDiskChanged

func (m *Manager) InvalidateIfDiskChanged(serverName string) error

InvalidateIfDiskChanged compares the store's on-disk modification time against the last one this Manager observed for serverName and, if it has changed, drops the cached state so the next OAuthConfig call rebuilds it from the current disk contents. This lets an external process (another Pando instance completing `pando mcp login`, a cron rotating a secret) take effect without a restart.

A stat failure is treated as "no change" rather than propagated, since missing-file is the common case (no credentials stored yet) and must not prevent building a fresh OAuthConfig.

func (*Manager) Login

func (m *Manager) Login(ctx context.Context, serverName string, srv config.MCPServer, p LoginPrompt) error

Login drives the full interactive OAuth 2.1 authorization-code + PKCE flow for serverName and persists the resulting tokens (and, if dynamic client registration happened, the client credentials) so future calls need no further interaction until the refresh token itself stops working.

srv must be the server's config entry with secrets already resolved via config.ResolveMCPServerSecrets (the caller may pass the raw config from config.Get().MCPServers[serverName]; Login resolves it itself if not).

func (*Manager) LoginClientCredentials

func (m *Manager) LoginClientCredentials(ctx context.Context, serverName string, srv config.MCPServer) error

LoginClientCredentials performs the non-interactive OAuth 2.1 client_credentials grant for serverName (Auth.Type == MCPAuthOAuthClientCredentials): discovers the token endpoint, mints an access token, and persists it through the Store — no browser, no local callback server, suitable for headless/CI use from `pando mcp login`.

func (*Manager) Logout

func (m *Manager) Logout(serverName string) error

Logout removes all persisted OAuth state (tokens and client registration) for serverName and drops any cached in-memory state.

func (*Manager) OAuthConfig

func (m *Manager) OAuthConfig(serverName string, srv config.MCPServer) (transport.OAuthConfig, error)

OAuthConfig builds (or returns the cached) transport.OAuthConfig for serverName using the already-decrypted srv (i.e. the result of config.ResolveMCPServerSecrets). Persisted ClientInfo (dynamic client registration results saved by PersistClientRegistration) is used when the static config does not set an explicit ClientID/ClientSecret; an explicit config value always takes precedence over what was previously persisted, so an operator can override a stale or misbehaving DCR registration by editing the config.

func (*Manager) PersistClientRegistration

func (m *Manager) PersistClientRegistration(serverName, serverURL string, h *transport.OAuthHandler) error

PersistClientRegistration reads the client ID/secret that h (an *transport.OAuthHandler obtained from an in-flight OAuth attempt, e.g. via client.GetOAuthHandler) ended up using — whether that came from static config or from a fresh RFC 7591 dynamic registration — and saves it to the Store under serverName/serverURL. This closes the gap where mcp-go performs dynamic client registration but never persists its result, which would otherwise force a fresh registration (and, for servers with a registration rate limit, failure) on every process restart.

It also drops any cached serverState for serverName so the next OAuthConfig call picks up the newly-persisted ClientInfo.

func (*Manager) RecordGrantedScopes

func (m *Manager) RecordGrantedScopes(serverName string, scopes []string) error

RecordGrantedScopes persists scopes as the server's accumulated RequestedScopes, for use by a later StepUpScopes call. It is normally called by Manager.Login on success (with the scope set it actually used), not by callers directly.

func (*Manager) Status

func (m *Manager) Status(serverName string, srv config.MCPServer) StatusInfo

Status reports the current OAuth state for serverName, for `pando mcp status` and any future UI. srv should be the (unresolved is fine; only Auth.Type/OAuth.ClientID and URL are read) server config entry.

func (*Manager) StepUpScopes

func (m *Manager) StepUpScopes(serverName string, challenged []string) []string

StepUpScopes returns the union of serverName's previously-requested scopes (if any are persisted) and challenged, preserving the order previously-requested scopes were first seen in and then appending any new challenged scopes not already present. The MCP authorization spec requires clients accumulate scopes on a 403 insufficient_scope step-up rather than replace the previous grant, so a permission the user already approved is never silently dropped by a later, narrower request.

func (*Manager) Store

func (m *Manager) Store() *Store

Store returns the Manager's backing Store, so Phase 3's login/logout/ status commands and the mcpclient wiring can share exactly one on-disk view without importing internals.

type Metadata

type Metadata struct {
	Issuer                                     string `json:"issuer,omitempty"`
	AuthorizationResponseIssParameterSupported bool   `json:"authorizationResponseIssParameterSupported,omitempty"`
}

Metadata caches authorization-server metadata discovered for a server, so later phases (step-up auth, iss validation, disk-watch invalidation) don't need to re-discover it.

AuthorizationResponseIssParameterSupported is populated in Phase 4 from a raw fetch of the AS metadata document (see issuer.go): mcp-go v0.57.0's client/transport.AuthServerMetadata struct (RFC 8414) does not expose the RFC 9207 §3 authorization_response_iss_parameter_supported flag, so mcpauth decodes that one field itself rather than relying on mcp-go's parsed struct.

type ScopeCapturingTransport

type ScopeCapturingTransport struct {
	// contains filtered or unexported fields
}

ScopeCapturingTransport wraps an http.RoundTripper and records the most recent 403 Forbidden response's WWW-Authenticate challenge it observes.

This exists to work around a real gap in mcp-go v0.57.0: its request path (client/transport/streamable_http.go SendRequest, and the equivalent SSE codepath) only preserves response headers for a 401 Unauthorized — anything else, including a 403 with a WWW-Authenticate: Bearer error="insufficient_scope" challenge, falls into the generic non-2xx branch that returns just fmt.Errorf("request failed with status %d: %s", resp.StatusCode, body) and discards resp.Header entirely. There is no exported hook to observe the raw *http.Response for a non-401 status. Installing this transport as the http.Client mcp-go actually issues the tool-call request through (not the OAuthConfig.HTTPClient, which is a separate client instance used only by the OAuthHandler's own metadata/token/refresh requests) is the only place headers are still visible before mcp-go discards them.

func NewScopeCapturingTransport

func NewScopeCapturingTransport(base http.RoundTripper) *ScopeCapturingTransport

NewScopeCapturingTransport returns a ScopeCapturingTransport delegating to base. A nil base uses http.DefaultTransport.

func (*ScopeCapturingTransport) LastChallenge

func (t *ScopeCapturingTransport) LastChallenge() (Challenge, bool)

LastChallenge returns (and clears) the most recently captured 403 challenge, if any. Clearing on read means a later call that hits neither a 403 nor a WWW-Authenticate header does not accidentally reuse a stale challenge from an earlier, unrelated request made through the same client.

func (*ScopeCapturingTransport) RoundTrip

func (t *ScopeCapturingTransport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper, delegating to the wrapped transport and recording a 403 response's WWW-Authenticate challenge (if any) before returning.

type ServerTokenStore

type ServerTokenStore struct {
	// contains filtered or unexported fields
}

ServerTokenStore adapts a Store to transport.TokenStore for one named MCP server. mcp-go's OAuthHandler calls GetToken/SaveToken without knowing anything about Pando's multi-server config, so one ServerTokenStore is constructed per server name (see Manager.OAuthConfig).

func NewServerTokenStore

func NewServerTokenStore(store *Store, serverName, serverURL string) *ServerTokenStore

NewServerTokenStore returns a transport.TokenStore backed by store for the given server name and URL. serverURL is used both to invalidate credentials on URL change (see Store.GetForURL) and to stamp newly-saved tokens.

func (*ServerTokenStore) GetToken

func (s *ServerTokenStore) GetToken(ctx context.Context) (*transport.Token, error)

GetToken implements transport.TokenStore.

func (*ServerTokenStore) SaveToken

func (s *ServerTokenStore) SaveToken(ctx context.Context, token *transport.Token) error

SaveToken implements transport.TokenStore.

type StatusInfo

type StatusInfo struct {
	ServerName            string
	ServerURL             string
	AuthType              string
	HasTokens             bool
	ExpiresAt             time.Time
	Expired               bool
	ClientID              string
	DynamicallyRegistered bool
}

StatusInfo summarizes one server's OAuth state for `pando mcp status` and any future TUI/WebUI surface.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is a persistent, file-backed collection of per-server OAuth Entry values. It is safe for concurrent use from multiple goroutines in this process (via an internal mutex) and from multiple Pando processes (via a best-effort cross-process file lock around each read-modify-write).

Store degrades gracefully: a missing or corrupt file is treated as an empty store rather than a hard error, since losing cached OAuth state is recoverable (re-authorize) while crashing on it is not acceptable.

func DefaultStore

func DefaultStore() *Store

DefaultStore returns the process-wide Store using the default path resolution rules described in New.

func New

func New(path string) *Store

New returns a Store backed by path. An empty path resolves to the default location: config.GlobalConfigDir()/mcp-auth.json, unless the PANDO_MCP_AUTH_FILE environment variable is set, in which case that value wins (used by tests and headless setups that redirect the global config dir elsewhere).

func (*Store) All

func (s *Store) All() (map[string]Entry, error)

All returns a snapshot of every stored entry, keyed by server name.

func (*Store) Get

func (s *Store) Get(name string) (*Entry, bool, error)

Get returns the stored entry for name, if any.

func (*Store) GetForURL

func (s *Store) GetForURL(name, serverURL string) (*Entry, bool, error)

GetForURL returns the stored entry for name only if its recorded ServerURL matches serverURL (or the stored entry has no ServerURL recorded yet, e.g. from an older schema version). If the server's configured URL has changed since the credentials were stored, the entry is treated as not found — this invalidates stale credentials the way opencode's getForUrl does, since an OAuth token/registration is scoped to a specific resource server.

func (*Store) ModTime

func (s *Store) ModTime() (time.Time, error)

ModTime returns the last modification time of the store file. It returns the zero time (no error) if the file does not yet exist, so callers can use it for cheap external-change detection without treating "never written" as an error.

func (*Store) Path

func (s *Store) Path() string

Path returns the file path this store reads from and writes to.

func (*Store) Remove

func (s *Store) Remove(name string) error

Remove deletes the stored entry for name, if any. It is not an error to remove an entry that does not exist.

func (*Store) Set

func (s *Store) Set(name string, e *Entry) error

Set stores (replacing) the entry for name, stamping UpdatedAt.

func (*Store) UpdateClientInfo

func (s *Store) UpdateClientInfo(name string, ci *ClientInfo, serverURL string) error

UpdateClientInfo merges ci into the stored entry for name (creating one if absent), stamping its ServerURL and UpdatedAt. This is how dynamic client registration results are persisted (see manager.go PersistClientRegistration), since mcp-go itself does not persist them.

func (*Store) UpdateMetadata

func (s *Store) UpdateMetadata(name string, md *Metadata, serverURL string) error

UpdateMetadata merges md into the stored entry for name (creating one if absent), stamping its ServerURL and UpdatedAt. This is how the Phase 4 issuer-validation flow caches discovered AS metadata (issuer string plus the raw authorization_response_iss_parameter_supported flag) so that a later callback can validate iss without re-discovering metadata.

func (*Store) UpdateRequestedScopes

func (s *Store) UpdateRequestedScopes(name string, scopes []string, serverURL string) error

UpdateRequestedScopes replaces the stored entry's accumulated RequestedScopes for name (creating an entry if absent), stamping its ServerURL and UpdatedAt. Callers are expected to have already computed the union with any previously requested scopes (see Manager.StepUpScopes) before calling this — Store itself just persists whatever slice it is given.

func (*Store) UpdateTokens

func (s *Store) UpdateTokens(name string, t *Tokens, serverURL string) error

UpdateTokens merges t into the stored entry for name (creating one if absent), stamping its ServerURL and UpdatedAt.

type Tokens

type Tokens struct {
	AccessToken  string    `json:"accessToken,omitempty"`
	TokenType    string    `json:"tokenType,omitempty"`
	RefreshToken string    `json:"refreshToken,omitempty"`
	Scope        string    `json:"scope,omitempty"`
	ExpiresAt    time.Time `json:"expiresAt,omitzero"`
}

Tokens is the persisted form of an OAuth token set for one MCP server.

Jump to

Keyboard shortcuts

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