Psych Runtime
Guides

Authenticate to an MCP server with OAuth 2.1

Psych gives the official MCP Python SDK a scope-isolated token store, resolved client secrets, an egress check, and the host application's browser callback. The SDK handles...

This is the documentation for unreleased changes on main. For the released library, read v0.1.

Psych gives the official MCP Python SDK a scope-isolated token store, resolved client secrets, an egress check, and the host application's browser callback. The SDK handles protected-resource and authorization-server discovery, PKCE, token exchange, refresh, and scope step-up.

Registered client credentials

psych_runtime.McpServer(
    name="records",
    url="https://mcp.example.com/records",
    oauth=psych_runtime.McpOAuth(
        grant="client_credentials",
        preregistered_client_id="psych-service",
        client_secret_credential="records-oauth-secret",
        issuer="https://auth.example.com",
    ),
)

client_secret_credential is a secret name resolved through the configured SecretResolver. It is never the secret value. Set issuer to the authorization server that registered the client. This is the recommended machine-to-machine configuration and uses the SDK's ClientCredentialsOAuthProvider.

Authorization code

psych_runtime.McpServer(
    name="calendar",
    url="https://mcp.example.com/calendar",
    oauth=psych_runtime.McpOAuth(
        grant="authorization_code",
        preregistered_client_id="psych-local",
        redirect_uris=("http://127.0.0.1:8080/oauth/callback",),
    ),
)

Authorization code requires an AuthorizationRedirectPort. Psych owns no HTTP server and opens no browser, so the host application presents the SDK's authorization URL and returns the callback. The SDK creates and validates PKCE, exchanges the code, refreshes tokens, and handles insufficient_scope step-up.

Wire the OAuth adapter

from psych_runtime.tools.mcp import McpPool
from psych_runtime.tools.oauth import OAuthClient

oauth = OAuthClient(transport=oauth_transport, redirect=redirect_port)
pool = McpPool(transport=mcp_transport, secrets=secrets, oauth=oauth)

Construct one OAuthClient per process. Its stores are keyed by tenant, principal, resource, and grant identity. A token refresh never changes the MCP pool key. McpPool.evict_server clears the connection and its stored OAuth state together.

Compatibility configurations

Client credentials obtained through dynamic client registration or a Client ID Metadata Document use Psych's compatibility flow because the SDK's client credentials provider requires an already registered client ID and secret. MCP protocol traffic still runs through the official SDK.

Code execution changes nothing here

A sandboxed program calling an MCP tool uses the pooled connection this Scope already holds, with the authorization that connection already carries. The exchange, the refresh and the retry all happen on the host.

No token, refresh token, client secret, transport object or authenticated URL crosses into the sandbox. A program sends a tool name and JSON arguments and receives a result; it has no way to ask for a credential and nothing to ask.

Rules

  • Put only secret names in a Spec.
  • Use authorization code when access represents a person.
  • Use registered client credentials plus an explicit issuer for service access.
  • Keep the token store isolated by Scope; never pool by URL alone.
  • Treat AuthorizationRedirectPort as a host application boundary.
  • https:// only, except loopback. The resource URI, the metadata URL a 401 challenge names, the authorization servers a resource document lists and every endpoint an authorization server advertises must be https://. A plain http:// anywhere in that chain is InvalidCanonicalUri or DiscoveryError, never a request carrying a token. localhost and loopback addresses are exempt for development servers and test stubs.
  • Dynamic Client Registration asks for token_endpoint_auth_method="none" by default (ClientIdentityConfig.token_endpoint_auth_method), registering a public client and discarding any secret the server returns anyway. A server-side deployment that can keep a secret sets client_secret_basic and the issued secret is then used at the token endpoint.
  • Discovery documents are bounded. Everything read through HttpTransport.request is refused past 16 MiB; see psych-multitenancy.

On this page