Prerequisites
- An Authdog OIDC issuer for the environment that issues access tokens.
- An MCP resource URL, normally the public server URL plus
/mcp. - JWT access tokens and either an issuer or direct JWKS URI.
- Required MCP scopes chosen for the service and for sensitive tools.
- Bun and Wrangler when running the included Cloudflare Worker sample.
The sample uses @modelcontextprotocol/sdk, agents, and a Durable Object class named MyMCP. It exposes authenticated Streamable HTTP at /mcp and protected-resource metadata at /.well-known/oauth-protected-resource.
Useful environment bindings implemented by packages/mcp-sdk include:
AUTHDOG_OIDC_ISSUERJWKS_URIAUTHDOG_OIDC_AUDIENCEMCP_REQUIRED_SCOPESMCP_SCOPES_SUPPORTEDMCP_PROTECTED_RESOURCEMCP_JWT_ONLY
Opaque-token introspection is opt-in through MCP_ALLOW_OPAQUE_INTROSPECTION=true and requires an introspection client ID and secret.
Implementation
From the repository root, start the sample:
cd examples/mcp-auth-sample
bun install
bun run devCopy .dev.vars.example to .dev.vars and set values for your environment. A production-shaped JWT configuration sets the issuer, pins or discovers JWKS, configures the MCP URL as audience and protected resource, and requires a baseline scope such as mcp:invoke.
The current sample calls the exported authenticateRequest(request, env) before forwarding /mcp to MyMCP.serve. Failed authentication becomes an HTTP error response. Successful authentication is passed to attachAuthdogContext(ctx, auth), which places { token, claims } under ctx.props.authdog.
authenticateRequest reads Authorization: Bearer <token>. It discovers OIDC metadata when an issuer is configured, derives {issuer}/.well-known/jwks.json when needed, verifies the JWT signature, and checks configured issuer, audience, and required scopes. Missing or invalid credentials produce 401; missing required scopes produce 403.
Apply authorization again inside privileged tools. The sample’s admin_echo handler uses:
requireClaimScopes(auth.claims, ["mcp:admin"])The public metadata helper, buildProtectedResourceMetadata(request, env), returns the resource URL, authorization servers, supported scopes, and bearer_methods_supported: ["header"]. Publish that response at /.well-known/oauth-protected-resource so clients can learn how the MCP resource is protected.
Test the metadata and authentication boundary locally:
curl http://localhost:8787/.well-known/oauth-protected-resource
curl -H "Authorization: Bearer $TOKEN" http://localhost:8787/mcpSecurity considerations
Prefer bearer tokens in the Authorization header. MCP_ALLOW_QUERY_TOKEN=true enables an access_token query fallback, but the sample marks it as not recommended. The protected-resource metadata advertises header transport only.
Use MCP_JWT_ONLY=true when the deployment accepts only JWT access tokens. This prevents introspection fallback. The included production deployment workflow uses JWT-only mode and derives JWKS from the issuer.
Do not enable opaque introspection merely because stale credentials exist. The SDK requires both valid introspection credentials and explicit MCP_ALLOW_OPAQUE_INTROSPECTION=true. Introspection uses HTTP Basic authentication and must remain server-side. Never expose its client secret to MCP clients.
Set AUTHDOG_OIDC_AUDIENCE to the actual MCP resource URL. The sample deployment aligns both AUTHDOG_OIDC_AUDIENCE and MCP_PROTECTED_RESOURCE with {public_base_url}/mcp. Audience validation limits a token minted for another resource from being reused here.
Distinguish service-wide and tool-level scopes. MCP_REQUIRED_SCOPES protects every request; requireClaimScopes protects a particular tool. Authentication alone does not authorize a tool’s side effects.
The sample includes permissive CORS (Access-Control-Allow-Origin: *). That does not replace token verification. Review origin policy for your deployment, especially if browser clients are not required.
Validation checklist
- Metadata endpoint returns the intended resource and authorization server.
- Metadata advertises only header bearer transport.
/mcprejects a request withoutAuthorization: Bearer.- Valid JWT succeeds only with matching issuer and audience.
- Missing baseline scope returns
403. - Sensitive tool rejects a token lacking its tool-specific scope.
MCP_JWT_ONLY=trueprevents introspection.- Opaque introspection works only when explicitly enabled with valid server secrets.
- Query-token fallback remains disabled.
- Logs and error handling do not expose bearer tokens or introspection secrets.
Troubleshooting
“Missing token verification settings” means neither JWT verification nor enabled introspection is fully configured. Set an issuer or JWKS URI for JWTs.
“Token audience mismatch” usually means the OAuth client requested a token for a different resource. Match AUTHDOG_OIDC_AUDIENCE, MCP_PROTECTED_RESOURCE, and the public /mcp URL.
A 403 reporting missing scopes means signature and claims verification succeeded, but either MCP_REQUIRED_SCOPES or a tool’s requireClaimScopes requirement was not granted.
When JWT verification fails, do not enable introspection unless tokens are genuinely opaque. Confirm issuer discovery and JWKS first.
Next steps
- Inspect
examples/mcp-auth-sample/src/index.tsfor request ordering and tool checks. - Inspect
packages/mcp-sdk/src/auth.tsfor exact exported configuration and verification behavior. - Read Machine identity for non-human OAuth clients.
- Apply Authorization to each state-changing MCP tool.