Auth is the reason most people end up needing a remote server even when a local one would be simpler. OAuth redirects, token storage, and refresh all work cleanly when there's a real hosted endpoint to redirect back to.
Claude's MCP client supports a specific set of auth types — not every spec-compliant flow works. Full reference: https://claude.com/docs/connectors/building/authentication
| Type | Notes |
|---|---|
oauth_dcr |
Supported. For high-volume directory entries, prefer CIMD or Anthropic-held creds — DCR registers a new client on every fresh connection. |
oauth_cimd |
Supported, recommended over DCR for directory entries. |
oauth_anthropic_creds |
Partner provides client_id/client_secret to Anthropic; user-consent-gated. Contact mcp-review@anthropic.com. |
custom_connection |
User supplies URL/creds at connect time (Snowflake-style). Contact mcp-review@anthropic.com. |
none |
Authless. |
Not supported: user-pasted bearer tokens (static_bearer); pure machine-to-machine client_credentials grant without user consent.
Callback URL (single, all surfaces): https://claude.ai/api/mcp/auth_callback
Server reads a key from env. User provides it once at setup. Done.
const apiKey = process.env.UPSTREAM_API_KEY;
if (!apiKey) throw new Error("UPSTREAM_API_KEY not set");
Works for local stdio, MCPB, and remote servers alike. If this is all you need, stop here.
Client ID Metadata Document. The MCP host publishes its client metadata at an HTTPS URL and uses that URL as its client_id. Your authorization server fetches the document, validates it, and proceeds with the auth-code flow. No registration endpoint, no stored client records.
Spec 2025-11-25 promoted CIMD to SHOULD (preferred). Advertise support via client_id_metadata_document_supported: true in your OAuth AS metadata.
Server responsibilities:
/.well-known/oauth-authorization-server with client_id_metadata_document_supported: trueclient_id as an HTTPS URL, validate the returned client metadata, proceedValidate bearer tokens on incoming /mcp requests
┌─────────┐ client_id=https://... ┌──────────────┐ upstream OAuth ┌──────────┐
│ MCP host│ ──────────────────────> │ Your MCP srv │ ─────────────────> │ Upstream │
└─────────┘ <─── bearer token ───── └──────────────┘ <── access token ──└──────────┘
Backward-compat fallback — spec 2025-11-25 demoted DCR to MAY. The host discovers your registration_endpoint, POSTs its metadata to register itself as a client, gets back a client_id, then runs the auth-code flow.
Implement DCR if you need to support hosts that haven't moved to CIMD yet. Same server responsibilities as CIMD, but instead of fetching the client_id URL you run a registration endpoint that stores client records.
Client priority order: pre-registered → CIMD (if AS advertises client_id_metadata_document_supported) → DCR (if AS has registration_endpoint) → prompt user.
Several MCP-focused hosting providers handle the OAuth plumbing for you — you implement tool logic, they run the authorization server. Check their docs for current capabilities. If the user doesn't have strong hosting preferences, this is usually the fastest path to a working OAuth-protected server.
Local stdio servers can do OAuth (open a browser, catch the redirect on a localhost port, stash the token in the OS keychain). It's fragile:
If OAuth is required, lean hard toward remote HTTP. If you must ship local + OAuth, the @modelcontextprotocol/sdk includes a localhost-redirect helper, and MCPB is the right packaging so at least the runtime is predictable.
| Deployment | Store tokens in |
|---|---|
| Remote, stateless | Nowhere — host sends bearer each request |
| Remote, stateful | Session store keyed by MCP session ID (Redis, etc.) |
| MCPB / local | OS keychain (keytar on Node, keyring on Python). Never plaintext on disk. |
Validating "is this a valid bearer token" isn't enough. The spec requires validating "was this token minted for this server" — RFC 8707 audience. A token issued for api.other-service.com must be rejected even if the signature checks out.
Token passthrough is explicitly forbidden. Don't accept a token, then forward it upstream. If your server needs to call another service, exchange the token or use its own credentials.
@modelcontextprotocol/sdk/server/auth ships:
mcpAuthRouter() — Express router for the full OAuth AS surface (metadata, authorize, token)bearerAuth — middleware that validates bearer tokens against your verifierproxyProvider — forward auth to an upstream IdPIf you're wiring auth from scratch, check these first.