Presso Network
Note08 of 08

Building on MCP

MCP OAuth 2.1: your server is the bouncer, not the ticket office

Under MCP OAuth 2.1 your remote server checks tokens and never issues them. The part tutorials skip is refusal: the audience check, and guarding every route.

MCP OAuth 2.1 makes your remote MCP server a resource server. It does not log anyone in and it does not issue tokens. A separate authorisation server does that.

Your server’s whole job is to look at the token on each request and decide whether to let it in. Every tutorial teaches the half of that job that says yes. The half that says no is where servers get hurt.

You have probably already built the yes half. The client gets a 401, finds your metadata, sends the user off to log in, and comes back with a token. Your tools appear, and it feels finished.

That is where most people stop.

What does MCP OAuth 2.1 actually require?

The MCP authorization specification (opens in a new tab) (revision 2025-06-18, re-read on 05/10/2026) splits the work into two roles. Authorisation is optional for MCP servers in general. For a remote HTTP server that holds anything worth protecting, this is the shape:

  • The authorisation server issues tokens. WorkOS, Auth0, Keycloak, or your own. OAuth 2.1 rules apply, so clients must use PKCE, a one-time secret that stops a stolen login code being swapped for a token (draft-ietf-oauth-v2-1 (opens in a new tab)).
  • Your MCP server is the resource server. It publishes a small metadata file at /.well-known/oauth-protected-resource that tells clients which authorisation server to use (RFC 9728 (opens in a new tab)).
  • Requests with no token get a 401 with a WWW-Authenticate header pointing at that metadata. That 401 is how discovery starts.
  • Clients name your server when they ask for a token, using a resource parameter (RFC 8707 (opens in a new tab)). That way the token is made for you specifically.

That last point is the hinge for everything below.

What is the bit every MCP OAuth tutorial skips?

The audience check. In plain words: is this token addressed to me? The spec is blunt about it:

“MCP servers MUST validate that access tokens were issued specifically for them as the intended audience, according to RFC 8707 Section 2.” - MCP authorization specification, 2025-06-18

Most token-checking code checks three things. The signature is valid, the issuer is one you trust, and the token has not expired.

That is where a tutorial leaves you. It feels complete, because every token you test with passes.

Here is the problem. Your authorisation server probably issues tokens for more than one thing: another API, another MCP server, an internal dashboard.

Every one of those tokens has a valid signature, the right issuer and a future expiry. Without checking the audience (the aud field in the token), your server accepts all of them. You built a lock that opens for any key cut by the same locksmith.

Check Typical tutorial What a resource server must do
Signature valid Yes Yes
Issuer is trusted Yes Yes
Not expired Yes Yes
aud is this server Often missing Required by the MCP spec
Token never forwarded upstream Rarely mentioned Required: no token passthrough
Every route checks, not just /mcp Rarely mentioned Your problem, not the spec’s

The passthrough row is the same idea from the other side. If your MCP server calls another API, it gets its own token for that API. It never forwards the one the client gave it.

The spec forbids passthrough. A token that works in two places means a breach in one is a breach in both.

Why does “every route” matter if OAuth is on?

Because OAuth protects the routes you put it in front of, and nothing else. The spec covers the MCP endpoint.

Your server, meanwhile, has picked up other routes. A health check. A webhook receiver. A setup route added so a new account can get going without the full login flow.

That last kind is the dangerous one. Picture a server with the OAuth work done properly: metadata published, correct 401s, PKCE, the lot. A few routes along sits POST /onboard, which creates an account and hands out a token without asking who you are.

The front door has a bouncer checking IDs. The side door is handing out wristbands.

The fix is small. Make the route demand a setup secret. If that secret is not configured, return 404 rather than 401, so a stranger cannot even confirm the route exists.

Closed should be the default. Then add a CI check that fails any deploy where that route is open. A comment in a config file gets read once; a failing build gets read every time.

How do you check your own MCP server today?

Three tests, ten minutes, nothing beyond curl:

  1. Send no token to every route, not just /mcp. List your routes from the router, not from memory. Anything that returns 200 and is not meant to be public is your side door.
  2. Send a real token made for a different service. Same authorisation server, different audience. If your server accepts it, the audience check is missing.
  3. Read your outgoing calls. If the Authorization header from an incoming request ever shows up on an outgoing one, that is token passthrough.

None of this is exotic. It is just the refusing half of the job.

Refusing is less fun to write a tutorial about. The demo is a 401, and nobody screenshots a 401.

The resource-server model is a good model. It keeps login, consent and token issuing out of your code, which is where most OAuth bugs live.

What it leaves you is one narrow job: say no to everything that was not made for you. It is worth doing on every door.

Where this stops working

Required field, min 1 / 4 entries
  1. 01

    Local stdio servers. The MCP spec says they should take credentials from the environment, so none of this applies until the server is reachable over HTTP.

  2. 02

    Servers that only expose public, read-only data. If there is nothing to protect, an authorisation server is ceremony.

  3. 03

    Opaque tokens. The audience check here assumes JWTs you can read; with opaque tokens you need token introspection (RFC 7662), and each uncached check becomes a network call to the authorisation server.

  4. 04

    The spec is moving. This was checked on 05/10/2026 against the 2025-06-18 revision and the current 2026-07-28 revision, which keeps the audience and passthrough rules word for word. Check the current version before you copy a header name.