OAuth & Dynamic Client Registration
How auth.mode: oauth triggers RFC 9728 → 8414 → 7591 discovery and manages per-user tokens.
When an MCP server requires its own OAuth flow — separate from the identity that authenticated with Orchid — set auth.mode: oauth in agents.yaml. The framework handles the entire discovery and token lifecycle automatically.
The passthrough vs oauth distinction
Both modes attach auth credentials to MCP calls, but they draw those credentials from different sources:
| Mode | Where the token comes from |
|---|---|
passthrough | The graph's OrchidAuthContext bearer token — obtained once at the API entry point via OrchidIdentityResolver and passed through the LangGraph run configuration (never stored in graph state) |
oauth | A per-user, per-MCP-server token stored in OrchidMCPTokenStore — resolved independently of the graph auth |
Use passthrough when the MCP server shares your identity provider. Use oauth when the MCP server has its own independent OAuth server.
Discovery on demand
When auth.mode: oauth is configured and no stored token exists for the user yet, the sequence is:
agent calls tool
→ MCP client finds no stored token → raises OrchidMCPAuthRequiredError
→ orchid-api surfaces the auth requirement (per-server status via GET /mcp/auth/servers)
GET /mcp/auth/servers/{name}/authorize:
→ probe the MCP server → 401 + WWW-Authenticate: Bearer resource_metadata="…"
→ RFC 9728: fetch Protected Resource Metadata → names the auth server
→ RFC 8414: fetch Authorization Server Metadata → endpoints + grant types
→ RFC 7591: POST Dynamic Client Registration → receive client_id (+ optional client_secret)
→ persist OrchidMCPClientRegistration (endpoints + DCR credentials, one row per MCP server)
→ return the browser authorization URL
→ user completes the OAuth dance → GET /mcp/auth/callback
→ per-user access + refresh tokens stored in OrchidMCPTokenStoreSubsequent calls resolve the token from OrchidMCPTokenStore and auto-refresh using the stored credentials — no re-discovery required.
Token and registration stores
| Store | Contents | Scope |
|---|---|---|
OrchidMCPTokenStore | Per-user access + refresh tokens | One row per (tenant_id, user_id, server_name) |
OrchidMCPClientRegistrationStore | Discovered endpoints + DCR credentials | One row per MCP server (shared across all users) |
Both ABCs are defined in orchid_ai/core/mcp_tokens.py and orchid_ai/core/mcp_registration.py and re-exported from the orchid_ai/core/mcp.py umbrella. The framework ships dependency-free in-memory stores; durable implementations ship in plugin packages — orchid-storage-sqlite (OrchidSQLiteMCPTokenStore, OrchidSQLiteMCPClientRegistrationStore) and orchid-storage-postgres (OrchidPostgresMCPTokenStore, OrchidPostgresMCPClientRegistrationStore). orchid-api and orchid-cli default to the SQLite stores.
No credentials in YAML
The MCP 2025-03-26 authorization model is zero-config by design. The only field in config is:
mcp_servers:
- name: external-crm
url: https://crm.example.com/mcp
auth:
mode: oauthFor MCP-2025-03-26-compliant servers, no client_id, client_secret, authorization_endpoint, or token_endpoint is needed — the framework discovers them. The authorization server must advertise RFC 7591 dynamic client registration.
For non-compliant servers (a bare 401 without resource_metadata), seed the endpoints and credentials explicitly with auth.manual_registration; the framework then skips auto-discovery:
mcp_servers:
- name: legacy-crm
url: https://crm.example.com/mcp
auth:
mode: oauth
manual_registration:
authorization_endpoint: https://auth.example.com/authorize
token_endpoint: https://auth.example.com/oauth/token
client_id: ${CRM_CLIENT_ID}
client_secret: ${CRM_CLIENT_SECRET}
scopes: "read:records"OrchidAuthContext entry point
OrchidAuthContext — the graph's identity token — is resolved once per HTTP request by OrchidIdentityResolver at the API entry point. It travels in the LangGraph run configuration (config["configurable"]["auth_context"]) and is re-injected for every entry point — it is never written to graph state or a checkpoint. MCP servers with auth.mode: passthrough receive its bearer_header; MCP servers with auth.mode: oauth bypass it entirely and use their own per-user token.