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
- Variables
- func BuildTLSConfig(srv config.MCPServer) (*tls.Config, error)
- func CanonicalResourceURI(raw string) (string, error)
- func HTTPClientForTLS(tlsCfg *tls.Config, timeout time.Duration) *http.Client
- func IsAuthorizationRequired(err error) bool
- func IsInteractive() bool
- func NonInteractiveLoginError(serverName string) error
- func TransportForTLS(tlsCfg *tls.Config) http.RoundTripper
- type CallbackServer
- type Challenge
- type ClientCredentialsParams
- type ClientInfo
- type Entry
- type ErrInsufficientScope
- type LoginPrompt
- type Manager
- func (m *Manager) ClientCredentialsTransport(ctx context.Context, serverName string, resolved config.MCPServer, ...) (http.RoundTripper, error)
- func (m *Manager) Do401(serverName string, token string, fn func() error) error
- func (m *Manager) HandleInsufficientScope(serverName string, challenged []string) error
- func (m *Manager) HasTokens(serverName, serverURL string) bool
- func (m *Manager) InvalidateClientRegistration(serverName string) error
- func (m *Manager) InvalidateIfDiskChanged(serverName string) error
- func (m *Manager) Login(ctx context.Context, serverName string, srv config.MCPServer, p LoginPrompt) error
- func (m *Manager) LoginClientCredentials(ctx context.Context, serverName string, srv config.MCPServer) error
- func (m *Manager) Logout(serverName string) error
- func (m *Manager) OAuthConfig(serverName string, srv config.MCPServer) (transport.OAuthConfig, error)
- func (m *Manager) PersistClientRegistration(serverName, serverURL string, h *transport.OAuthHandler) error
- func (m *Manager) RecordGrantedScopes(serverName string, scopes []string) error
- func (m *Manager) Status(serverName string, srv config.MCPServer) StatusInfo
- func (m *Manager) StepUpScopes(serverName string, challenged []string) []string
- func (m *Manager) Store() *Store
- type Metadata
- type ScopeCapturingTransport
- type ServerTokenStore
- type StatusInfo
- type Store
- func (s *Store) All() (map[string]Entry, error)
- func (s *Store) Get(name string) (*Entry, bool, error)
- func (s *Store) GetForURL(name, serverURL string) (*Entry, bool, error)
- func (s *Store) ModTime() (time.Time, error)
- func (s *Store) Path() string
- func (s *Store) Remove(name string) error
- func (s *Store) Set(name string, e *Entry) error
- func (s *Store) UpdateClientInfo(name string, ci *ClientInfo, serverURL string) error
- func (s *Store) UpdateMetadata(name string, md *Metadata, serverURL string) error
- func (s *Store) UpdateRequestedScopes(name string, scopes []string, serverURL string) error
- func (s *Store) UpdateTokens(name string, t *Tokens, serverURL string) error
- type Tokens
Constants ¶
const DefaultCallbackPath = "/mcp/oauth/callback"
DefaultCallbackPath is the path component of the local OAuth redirect URI.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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.
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.
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 ¶
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) GetForURL ¶
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 ¶
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) Remove ¶
Remove deletes the stored entry for name, if any. It is not an error to remove an entry that does not exist.
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 ¶
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 ¶
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.
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.