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](https://vercel.com/i/ai-gateway-vs-mcp), 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`](http://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](https://blog.modelcontextprotocol.io/posts/2026-07-28), 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-handler` supplies the resource-server surface (`protectedResourceHandler` for discovery, `withMcpAuth` for 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 heading](#step-1:-treat-your-mcp-server-as-an-oauth-resource-server)Step 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](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization), 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`](http://mcp.vercel.com) works this way. It validates tokens from our auth infrastructure and keeps an allowlist of approved clients.

### [Copy link to heading](#why-the-split-happened)Why the split happened

Combining both roles is where the security bugs live. [Aaron Parecki](https://aaronparecki.com/2025/04/03/15/oauth-for-model-context-protocol), 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 heading](#step-2:-install-mcp-handler-and-pin-a-secure-sdk-version)Step 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:

Terminal

```
npm install mcp-handler@1.1.0 @modelcontextprotocol/sdk@1.26.0 zod@^3
```

Pin the SDK deliberately. Versions of `@modelcontextprotocol/sdk` before 1.26.0 carry [a known security vulnerability](https://github.com/vercel/mcp-handler), so 1.26.0 is the floor. This guide builds on the surface in [mcp-handler's authorization guide](https://github.com/vercel/mcp-handler/blob/main/docs/AUTHORIZATION.md), 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 heading](#step-3:-expose-the-protected-resource-metadata-endpoint)Step 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 heading](#match-resourcemetadatapath-to-your-well-known-path)Match 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 heading](#step-4:-validate-bearer-tokens-with-withmcpauth)Step 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 heading](#read-auth-info-in-your-tools)Read 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](https://clerk.com/docs/nextjs/guides/ai/mcp/build-mcp-server).

## [Copy link to heading](#step-5:-register-oauth-clients-with-client-id-metadata-documents)Step 5: Register OAuth clients with Client ID Metadata Documents

Once tokens validate, decide how clients register. The 2026-07-28 spec [formally deprecated](https://blog.modelcontextprotocol.io/posts/2026-07-28) Dynamic Client Registration (DCR) in favor of Client ID Metadata Documents (CIMD). Build new implementations on CIMD.

CIMD uses [an HTTPS URL](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-01) 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:

| Provider | OAuth 2.1 + PKCE | DCR (backward compat) | CIMD | RFC 8707 Resource Indicators | Vercel / Next.js integration |
| --- | --- | --- | --- | --- | --- |
| [WorkOS AuthKit](https://workos.com/docs/authkit/mcp) | Yes | Dashboard toggle | Preferred since Nov 2025 | Configurable default | Names Vercel a first-class MCP framework integration |
| Clerk | Yes | Dashboard toggle | Not documented | Not documented | A dedicated Next.js guide built on `withMcpAuth` |
| [Auth0](https://auth0.com/ai/docs/mcp/guides/registering-your-mcp-client-application) | Yes | Tenant-level, needs Resource Parameter Compatibility Profile | Manual CIMD recommended for production | Via Resource Parameter Compatibility Profile | AI SDK listed out of the box, Auth for MCP in Early Access |

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 heading](#step-6:-fix-the-failure-modes-that-break-mcp-server-authorization-in-production)Step 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](https://github.com/google-gemini/gemini-cli/issues/15754) construct the well-known discovery URL incorrectly when a server lives at a sub-path like `/api/mcp` instead 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](https://kane.mx/posts/2025/mcp-authorization-oauth-rfc-deep-dive). 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:write` while the MCP server checks `orgs:write` [breaks authorization](https://www.mcpjam.com/blog/scalekit-oauth) 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-handler` 2.x moves to the MCP SDK v2 packages and changes how tool handlers read authentication, so a handler written against the 1.x `extra.authInfo` surface 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 heading](#step-7:-verify-the-oauth-authorization-chain-end-to-end)Step 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:

1.  Request the metadata endpoint with `curl -i https://your-server.example.com/.well-known/oauth-protected-resource`, and expect `200 OK` with a JSON body containing `resource` and `authorization_servers`.

2.  Send an unauthenticated POST to the MCP endpoint, and expect `401 Unauthorized` with a `WWW-Authenticate: Bearer` header that includes `resource_metadata`.

3.  Complete the PKCE flow through your identity provider and retrieve a bearer token with the required scopes.

4.  Repeat the POST with an `Authorization: Bearer` header, and expect a valid JSON-RPC response.

5.  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](https://github.com/modelcontextprotocol/inspector/issues/1168) 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 heading](#harden-your-mcp-server-beyond-the-oauth-spec-on-vercel)Harden 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`](http://mcp.vercel.com), we [made it read-only](/blog/introducing-vercel-mcp-connect-vercel-to-your-ai-tools), 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 `protectedResourceHandler` for RFC 9728 discovery and `withMcpAuth` for 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`](http://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](https://vercel.com/new) and deploy your MCP server on your first `git push`, or follow the [MCP deployment docs](https://vercel.com/docs/mcp/deploy-mcp-servers-to-vercel) to wire authorization into a server you already run.

## [Copy link to heading](#frequently-asked-questions-about-mcp-server-authorization)Frequently asked questions about MCP server authorization

### [Copy link to heading](#does-withmcpauth-work-with-stdio-transport)Does 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](https://vercel.com/docs/environment-variables/rotating-secrets) instead, and reserve `withMcpAuth` and the discovery endpoint for HTTP transports.

### [Copy link to heading](#my-identity-provider-does-not-support-dynamic-client-registration.-what-do-i-do)My 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 heading](#can-i-pass-a-user's-bearer-token-to-a-third-party-api-my-tool-calls)Can 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 heading](#which-clients-support-the-2026-07-28-spec)Which 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 heading](#what-changed-in-mcp-handler-v2-that-could-break-my-deployment)What 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.