Most broken Model Context Protocol (MCP) authorization flows fail in the same place. The server gets built as if it issues tokens, when its actual job is to validate them, and that mistake accounts for most of the confused-deputy bugs, token-passthrough leaks, and discovery failures that surface once a server starts seeing real traffic.
On our AI Gateway network, tool-call requests grew from 31.6% to 58.9% of tokens between October 2025 and April 2026, so most token volume now comes from agents calling tools. The servers behind those tools need authorization that holds up under that load.
We operate one of them, mcp.vercel.com, and we maintain mcp-handler, the package teams use to build their own on Vercel. This guide walks the build that ships today, current to the 2026-07-28 spec revision, the largest since MCP launched. It covers the resource-server model, the discovery endpoint, bearer-token validation, client registration after the Dynamic Client Registration deprecation, the failure modes that break real deployments, and how to verify the whole chain before a client connects.
Key takeaways:
An MCP server is an OAuth 2.1 resource server. It validates access tokens and serves resources, and it never issues tokens or logs users in.
The 2025-06-18 spec reclassified MCP servers as resource servers and made RFC 9728 Protected Resource Metadata the mandatory discovery mechanism.
mcp-handlersupplies the resource-server surface (protectedResourceHandlerfor discovery,withMcpAuthfor bearer-token validation), but you write the token verification against your identity provider.The 2026-07-28 spec formally deprecated Dynamic Client Registration (DCR) in favor of Client ID Metadata Documents, though DCR stays available for backward compatibility for at least 12 months.
Forwarding a client's bearer token to an upstream API is forbidden by the spec and causes the confused-deputy problem. Your server obtains its own upstream tokens through delegation.
Copy link to headingStep 1: Treat your MCP server as an OAuth resource server
Your server validates tokens. It never issues them. In OAuth terms, an MCP server is a resource server rather than an authorization server, and getting that boundary right is what the rest of the build depends on.
The distinction was not always this clean. The 2025-03-26 spec expected an MCP server to act as its own authorization server, handling user login and minting access tokens directly. The 2025-06-18 revision reclassified the MCP server as a pure OAuth resource server and added RFC 9728 Protected Resource Metadata, so the server now advertises which authorization server it trusts and delegates login, consent, and token issuance to it.
That leaves your server two jobs. It publishes where the authorization server lives, and it validates that every incoming token was issued specifically for it. Our own mcp.vercel.com works this way. It validates tokens from our auth infrastructure and keeps an allowlist of approved clients.
Copy link to headingWhy the split happened
Combining both roles is where the security bugs live. Aaron Parecki, who pushed for the change, argued that a dedicated authorization server should own login and token minting, which leaves the resource server the narrower task of checking tokens and returning resources. Fewer responsibilities on the server that faces untrusted tool traffic means fewer places for a token to leak or a scope to be misjudged.
Copy link to headingStep 2: Install mcp-handler and pin a secure SDK version
With the model clear, set up the packages. mcp-handler gives you the resource-server surface on the Next.js App Router. It provides the discovery handler and the auth wrapper, and the token verification logic stays yours to write. You need a Node.js project on the App Router, plus a Vercel project or a local environment running vercel dev.
Install mcp-handler with a pinned SDK version:
npm install mcp-handler@1.1.0 @modelcontextprotocol/sdk@1.26.0 zod@^3Pin the SDK deliberately. Versions of @modelcontextprotocol/sdk before 1.26.0 carry a known security vulnerability, so 1.26.0 is the floor. This guide builds on the surface in mcp-handler's authorization guide, which reads auth from extra.authInfo. The v2 line tracks the MCP SDK v2 packages and serves both protocol versions from one endpoint, and step 6 covers what changes when you move to it.
Copy link to headingStep 3: Expose the protected resource metadata endpoint
Clients discover your authorization server through RFC 9728 Protected Resource Metadata, and the spec requires it. Without this endpoint, a compliant client cannot start the flow.
Create app/.well-known/oauth-protected-resource/route.ts:
import { protectedResourceHandler, metadataCorsOptionsRequestHandler,} from 'mcp-handler';
const handler = protectedResourceHandler({ authServerUrls: ['https://your_auth_server_issuer_url_here'],});const corsHandler = metadataCorsOptionsRequestHandler();
export { handler as GET, corsHandler as OPTIONS };This endpoint publishes two values a client needs. resource is the URL of your MCP server, and authorization_servers is an array of authorization server issuer URLs. From there the client fetches the authorization server's own metadata and begins the Proof Key for Code Exchange (PKCE) flow. The 2026-07-28 spec accepts either discovery path, a WWW-Authenticate header carrying resource_metadata on 401 responses or this well-known URI, and mcp-handler covers both.
Copy link to headingMatch resourceMetadataPath to your well-known path
withMcpAuth accepts a resourceMetadataPath option, and it has to match the well-known path from the previous step. The default is /.well-known/oauth-protected-resource. When the wrapper issues a 401, this is the path it points clients to, so a mismatch here breaks discovery even when both endpoints work in isolation.
Copy link to headingStep 4: Validate bearer tokens with withMcpAuth
With discovery in place, validate the tokens themselves. withMcpAuth wraps your base MCP handler and runs a verifyToken function you supply on every request. The function receives the request and the bearer token, and it returns an AuthInfo object or undefined. Verify the signature against your identity provider (IdP), using its JSON Web Key Set (JWKS) or introspection endpoint. Do not compare against a hardcoded string.
Write verifyToken and wrap the handler:
import { withMcpAuth } from 'mcp-handler';import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
const verifyToken = async ( req: Request, bearerToken?: string,): Promise<AuthInfo | undefined> => { if (!bearerToken) return undefined; // Verify the signature against your IdP's JWKS endpoint, // confirm the token was issued for this server, and read its claims. const payload = await verifyWithYourIdp(bearerToken); if (!payload) return undefined; return { token: bearerToken, scopes: payload.scopes, clientId: payload.client_id, extra: { userId: payload.sub }, };};
const authHandler = withMcpAuth(handler, verifyToken, { required: true, requiredScopes: ['read:stuff'], resourceMetadataPath: '/.well-known/oauth-protected-resource',});AuthInfo carries token, scopes, clientId, and an arbitrary extra object that flows through to your tool handlers. With required set to true, the wrapper rejects unauthenticated requests with a 401. When a token is present but its scopes do not cover requiredScopes, the wrapper returns a 403.
Export the wrapped handler for both verbs:
export { authHandler as GET, authHandler as POST };The wrapped handler now serves both the GET and POST routes for your MCP endpoint.
Copy link to headingRead auth info in your tools
Inside a tool handler, the authenticated context arrives on extra.authInfo, which is where the user ID and client ID you returned from verifyToken become available. For Clerk, the wiring collapses to a single verifyToken argument. Parse the incoming token with Clerk's auth() helper, then pass it to verifyClerkToken() from @clerk/mcp-tools/next, which validates the token and exposes the current user's ID to your tools. Clerk documents the full setup in its MCP server guide.
Copy link to headingStep 5: Register OAuth clients with Client ID Metadata Documents
Once tokens validate, decide how clients register. The 2026-07-28 spec formally deprecated Dynamic Client Registration (DCR) in favor of Client ID Metadata Documents (CIMD). Build new implementations on CIMD.
CIMD uses an HTTPS URL as the client_id. The authorization server fetches that URL to read the client's metadata, so there is no registration endpoint and no per-instance client records piling up on your authorization server.
Provider support for these mechanisms varies, so confirm your identity provider before you commit. Check OAuth 2.1 with PKCE, backward-compatible DCR, CIMD, RFC 8707 Resource Indicators, and the quality of the Vercel or Next.js path:
Every provider in the table keeps DCR available for backward compatibility, and you will need it for a while. Client adoption of the 2026-07-28 spec lags its release. The four Tier 1 SDKs shipped support on the day the spec published, but the broader field of clients has not caught up. Support both registration paths and default to CIMD.
Copy link to headingStep 6: Fix the failure modes that break MCP server authorization in production
The spec-compliant path above is not where deployments fail. These are the failure modes that show up in real traffic, and each has a specific cause worth naming before you hit it:
Token passthrough: Forwarding a client's bearer token to an upstream API is forbidden. Your server is an OAuth client to those upstream services and obtains its own tokens through delegation, such as RFC 8693 token exchange. Skip that step and downstream APIs trust the forwarded token as if your server had authorized the request, which is the confused-deputy problem.
Sub-path discovery failures: Clients such as Gemini CLI construct the well-known discovery URL incorrectly when a server lives at a sub-path like
/api/mcpinstead of the domain root, so authentication fails with a resource mismatch. If you deploy at a sub-path, test discovery explicitly before pointing a client at it.RFC 8707 gaps at major IdPs: Resource Indicators are mandatory for MCP clients, but only Amazon Cognito and Ping Identity support them natively. Auth0, Okta, and Microsoft Entra use proprietary audience parameters that predate the standard, so confirm your provider's approach rather than assuming the resource parameter works.
Scope-string mismatches: An authorization server issuing
org:writewhile the MCP server checksorgs:writebreaks authorization on a plain string mismatch. Audit the exact scope strings across both systems.Missing DCR at enterprise IdPs: Okta and Microsoft Entra do not implement RFC 7591, and some clients treat a missing DCR endpoint as fatal rather than falling back to static credentials. Register clients through CIMD or pre-provision credentials in the IdP dashboard.
The v1 to v2 upgrade:
mcp-handler2.x moves to the MCP SDK v2 packages and changes how tool handlers read authentication, so a handler written against the 1.xextra.authInfosurface needs updating when you move. Plan the upgrade as its own change, not a version bump.
The lesson for anyone running an MCP server is the one the spec already encodes. An over-scoped OAuth grant is a path into everything it can reach, so scope tokens narrowly and validate their audience on every request.
Copy link to headingStep 7: Verify the OAuth authorization chain end-to-end
Before pointing any client at your deployment, run the chain manually. Each check confirms one link, and a failure tells you exactly where discovery or validation broke.
Run this sequence against your server:
Request the metadata endpoint with
curl -i https://your-server.example.com/.well-known/oauth-protected-resource, and expect200 OKwith a JSON body containingresourceandauthorization_servers.Send an unauthenticated POST to the MCP endpoint, and expect
401 Unauthorizedwith aWWW-Authenticate: Bearerheader that includesresource_metadata.Complete the PKCE flow through your identity provider and retrieve a bearer token with the required scopes.
Repeat the POST with an
Authorization: Bearerheader, and expect a valid JSON-RPC response.Repeat once more with a token missing a required scope, and expect
403 Forbidden.
MCP Inspector automates most of this, with one caveat worth knowing before you rely on it. When protected resource metadata is unavailable, a documented Inspector bug has it fall back to the bare origin and drop the server's mount path, so discovery against a sub-path server can fail in the tool itself rather than in your server. Test sub-path deployments with a raw curl request as well, so a client bug does not read as a server bug.
Copy link to headingHarden your MCP server beyond the OAuth spec on Vercel
A spec-compliant flow is the floor, not the finish line. The teams whose MCP servers hold up in production treat the authorization spec as the baseline and add the operational controls it leaves optional, because the failure modes above are about deployment posture, not protocol conformance.
When we launched mcp.vercel.com, we made it read-only, kept an allowlist of approved clients, and required an OAuth consent screen on every connection. None of those are spec requirements. The allowlist costs us support work, since third-party platforms regularly file to get their redirect URIs approved, and we accept that friction because the alternative is worse. Vercel gives you the primitives to make the same choices without assembling them by hand:
mcp-handler: The resource-server surface for Next.js, with
protectedResourceHandlerfor RFC 9728 discovery andwithMcpAuthfor bearer-token validation, kept current as the MCP spec revises.Vercel MCP: A working reference for the operational posture worth copying, running read-only with a client allowlist and a mandatory consent screen on
mcp.vercel.com.AI Gateway: Governs the model half of an agent's request path with routing, failover, and spend controls, which pairs with the MCP server that governs the tool half.
Environment variables and secrets: Sensitive-marked variables stay encrypted and support rotation, so the credentials your server uses to reach upstream APIs are not sitting in plaintext.
Fluid compute: Runs the MCP server itself with concurrency on shared instances, so an authenticated tool call does not pay a cold start on every request.
Start a new project and deploy your MCP server on your first git push, or follow the MCP deployment docs to wire authorization into a server you already run.
Copy link to headingFrequently asked questions about MCP server authorization
Copy link to headingDoes withMcpAuth work with STDIO transport?
No. The MCP spec states that STDIO transport should not use the OAuth authorization flow. For a STDIO server, read credentials from the environment instead, and reserve withMcpAuth and the discovery endpoint for HTTP transports.
Copy link to headingMy identity provider does not support Dynamic Client Registration. What do I do?
Register clients through Client ID Metadata Documents, or pre-provision credentials in your provider's dashboard. Okta and Microsoft Entra do not implement RFC 7591, so a static, pre-registered client is the standard path there. DCR is deprecated as of the 2026-07-28 spec, not required.
Copy link to headingCan I pass a user's bearer token to a third-party API my tool calls?
No. The spec forbids it. Your server acts as an OAuth client to the upstream API and gets its own token through delegation, such as RFC 8693 token exchange. Forwarding the original token causes the confused-deputy problem and breaks audience isolation.
Copy link to headingWhich clients support the 2026-07-28 spec?
Support is still rolling out. The four Tier 1 SDKs (TypeScript, Python, Go, and C#) shipped support on the day the spec published, but many clients still run older revisions. Serve both protocol versions, which mcp-handler 2.x does from one endpoint.
Copy link to headingWhat changed in mcp-handler v2 that could break my deployment?
mcp-handler 2.x moves to the MCP SDK v2 packages and updates how tool handlers read authentication context, so handlers written against the 1.x extra.authInfo surface need changes. It also serves both the 2026-07-28 and older protocol versions from one endpoint.