This guide puts that conversation, and the checks behind it, in front of the transport: authenticate the caller, consult the trust store, authorize the specific tool, then record what happened. In production, set both the trust store and AuthZEN to enforce. off and monitor are for staging, while you are still learning what would have been denied. The SDK is not on npm yet. Install it from packages/mcp-sdk in this monorepo. Do not run npm install @authdog/mcp-sdk and expect a production package.
Console: Connect MCP server
For an MCP project environment, open MCP Auth → Connect in the console. It copies your environment’s issuer, JWKS URI, token/authorize endpoints, a suggested AUTHDOG_OIDC_ISSUER env block, and a starter protected-resource metadata JSON document. Set Default resource under MCP Auth → Settings when OAuth clients omit the RFC 8707 resource parameter.
Validation design (JWT-first vs opaque introspection): specs/024-mcp-auth-platform/contracts/validation-credential.md.
AuthdogMcpAuthProvider (Wave 2)
For a thin validation entry point in your MCP server:
import { AuthdogMcpAuthProvider } from "@authdog/mcp-sdk"
const auth = new AuthdogMcpAuthProvider({
env: process.env as AuthdogMcpEnv,
})
const result = await auth.validateAccessToken(request.headers.get("authorization"))
if (!result.ok) {
return result.response // 401/403 with MCP metadata when configured
}The class delegates to authenticateRequest; use either surface. Prefer JWT verification (MCP_JWT_ONLY=true) for tokens minted to desktop MCP clients.
Prerequisites
You need an issuer that already mints tokens, and a public URL clients can reach.
- An Authdog OIDC issuer for the environment that issues access tokens.
- The MCP resource URL, normally the public server URL plus
/mcp. - JWT access tokens, plus either that issuer or a JWKS URI you trust.
- The scopes this service requires, and any tighter scopes for sensitive tools.
- Bun and Wrangler if you are running the included Cloudflare Worker sample.
Set these bindings before you start: AUTHDOG_OIDC_ISSUER, JWKS_URI, AUTHDOG_OIDC_AUDIENCE, MCP_REQUIRED_SCOPES, MCP_PROTECTED_RESOURCE, MCP_JWT_ONLY. Leave opaque-token introspection off unless you explicitly set MCP_ALLOW_OPAQUE_INTROSPECTION=true.
Implementation
1. Put four checks in front of every tool
A request should not reach a tool until all four have had a say.
Authenticate with authenticateRequest. It checks the bearer token against your JWKS, and it serves the protected-resource metadata (RFC 9728) clients fetch before they ask for a token. Trust store with checkTrustStore. It refuses a non-human identity that is unverified or revoked, and it caps the scopes that identity may use (MCP_TRUST_STORE_MODE). Existing servers still default to off, so an upgrade does not lock them out. Switch to enforce when you are ready to deny. Those denials show up as MCP_TRUST_VIOLATION when MCP_TELEMETRY=true. Authorize with withAuthdogToolGuard. It asks Authdog's AuthZEN policy decision point whether this identity may call this specific tool (MCP_AUTHZ_MODE). Set that to enforce on the product path too. Telemetry records MCP_TOOL_INVOKED, MCP_TOOL_DENIED, and MCP_TOOL_FAILED. The event carries a digest of the arguments, never the raw arguments. Denied tools land in Audit/SIEM as MCP_TOOL_DENIED when MCP_TELEMETRY=true (see specs/024-mcp-auth-platform/contracts/mcp-audit-siem-export.md and MCP authentication).
2. Run the sample until a missing token is rejected
cd examples/mcp-auth-sample
bun install
bun run devCopy .dev.vars.example to .dev.vars. Fill in the issuer, JWKS, audience, and a required scope such as mcp:invoke. Set MCP_TRUST_STORE_MODE=enforce and MCP_AUTHZ_MODE=enforce. Register the caller in Agents — that page is sales-gated — and verify the identity before you expect a token to pass.
Call /mcp with no Authorization header. You want a 401. That is the signal the boundary is actually in front of the transport.
3. Authenticate before the transport runs
The sample calls authenticateRequest(request, env) before it forwards /mcp. That call loads OIDC metadata, verifies the JWT, and checks issuer, audience, and scopes. A missing or invalid credential is 401. A valid token that lacks the baseline scope is 403. If you see 403 on a token you just minted, the signature was fine and the scope was not.
4. Ask again inside a privileged tool
Getting into the server is not permission to change something. The sample's admin_echo handler runs a second, narrower check:
requireClaimScopes(auth.claims, ["mcp:admin"])Give the test caller mcp:invoke and withhold mcp:admin, then invoke admin_echo. The tool should refuse the call even though the same token can reach /mcp.
AuthZEN vs OAuth scopes (024)
OAuth scopes from MCP Auth → Scopes control what users may consent to
and what appears on access tokens. AuthZEN controls individual tools after
authentication. A token with mcp:invoke can still be denied on admin_echo
when MCP_AUTHZ_MODE=enforce. Map tool names to AuthZEN MCP_TOOL policies;
see specs/024-mcp-auth-platform/contracts/authzen-mcp-mapping.md.
5. Publish the document clients use to find your issuer
An MCP client does not guess your authorization server. It fetches a small JSON document from your origin, then goes to the issuer named there.
Add a route for /.well-known/oauth-protected-resource and return buildProtectedResourceMetadata(request, env). Open that URL in a browser. You should see three things you already configured: the resource URL of this server, the authorization servers allowed to mint tokens for it, and the scopes you accept. If any of those are empty or point at the wrong host, fix the bindings before you debug the client.
Security considerations
- Put the bearer token in the
Authorizationheader. The query-token fallback (MCP_ALLOW_QUERY_TOKEN) leaks into logs and referrers. Leave it off. - Set
MCP_JWT_ONLY=truewhen this deployment should accept only JWT access tokens. - Do not turn on opaque introspection because an old credential is lying around. It needs an explicit opt-in, and the introspection call stays on the server. When you need opaque validation, rotate the credential under MCP Auth → Connect and copy the secret once.
- Set
AUTHDOG_OIDC_AUDIENCEto this server's real MCP URL. A token minted for a different resource should fail here. - Keep a service-wide scope (
mcp:invoke) separate from tool scopes (mcp:admin). Passing authentication does not authorize a tool's side effects.
Validation checklist
- Metadata endpoint returns the intended resource and authorization server.
/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.- Logs and error handling never expose bearer tokens or introspection secrets.
Troubleshooting
"Missing token verification settings." Neither JWT verification nor introspection is fully configured. Set an issuer or a JWKS URI, then retry the same request.
"Token audience mismatch." AUTHDOG_OIDC_AUDIENCE, MCP_PROTECTED_RESOURCE, and the public /mcp URL have drifted apart. Make all three the same resource, then request a fresh token.
403 that names missing scopes. Signature and claims already checked out. The token is real, and it was not granted a scope this server or tool requires. Fix the grant, not the verifier.
Next steps
- Read
examples/mcp-auth-sample/src/index.tsfor the order of checks and the tool-level scope gate. - Read
packages/mcp-sdk/src/auth.tsfor the configuration the sample is actually reading. - Follow Agents quickstart and Machine identity when the caller is a workload, not a person.
- Apply Authorization to every MCP tool that changes state.