June 21, 2026/11 min read

MCP in production

What changes when an MCP server moves from localhost to real users, real accounts, and real permissions.

MCP gets interesting the moment it touches real users, real accounts, and real data.

At that point, the hard part is not just exposing a tool. The hard part is deciding who can use it, on whose behalf, with which permissions, and against which server.

Think of remote MCP like a busy conference venue. There are rooms inside: search, files, tickets, calendars, reports, payments, databases. MCP is the map that tells an assistant which rooms exist and how to ask for help inside them.

OAuth is the badge desk at the entrance.

The assistant does not get a master key. It gets a badge, for one user, with specific rooms printed on it.

Keep this model in your head:

  • MCP tells the assistant what exists.
  • OAuth decides whether this assistant can use it for this user.
  • Protected Resource Metadata tells the assistant where the badge desk is.
  • Authorization Server Metadata tells the assistant how that badge desk works.
  • CIMD and DCR explain how unknown clients introduce themselves.
  • Scopes, audience, and resource indicators keep the badge narrow enough to be safe.

That is the production boundary.

The production boundary

Localhost hides the hard part

Local MCP can be wonderfully direct. A desktop app starts a local server over stdio. Secrets can come from environment variables or local config. The trust boundary is mostly the user's machine.

Remote MCP is different. Now an HTTP server is receiving requests for user-owned data. It may be called by clients it has never seen before. It has to know whether the user approved the call, whether the token is meant for this server, and whether the requested tool is inside the user's permission.

The MCP authorization spec says HTTP-based transports should follow its authorization profile when auth is supported, while stdio should generally use credentials from the environment.1

The players

In OAuth terms, the badge desk is the authorization server.

The MCP server is the protected venue. It has the rooms and tools. The MCP client is the visitor's assistant. The user is the person who owns the badgeable access.

The authorization server handles consent and issues the access token. The MCP server validates that token before letting the request reach the tool.

So the shape is:

  • MCP client: "I want to help this user."
  • Authorization server: "Let me ask the user and mint a badge."
  • MCP server: "Let me inspect the badge before opening the room."

Discovery: where auth lives

Finding the badge desk

A client should not guess where authorization happens.

When a client reaches a protected MCP server without a valid token, the server can return 401 Unauthorized and point the client to its Protected Resource Metadata. That metadata tells the client which authorization server can issue tokens for this MCP server.

In protocol terms, MCP uses OAuth 2.0 Protected Resource Metadata. Discovery can happen through a WWW-Authenticate header with a resource_metadata URL, or through a well-known metadata URL. The current draft requires clients to support both paths.2

The well-known route is worth spelling out because this is where a lot of integrations quietly break:

  • root metadata: https://example.com/.well-known/oauth-protected-resource
  • path-specific metadata for https://example.com/public/mcp: https://example.com/.well-known/oauth-protected-resource/public/mcp

So a client can first follow the resource_metadata URL from WWW-Authenticate when the server gives one. If not, it can fall back to constructing the well-known route for the exact MCP endpoint, and then the root well-known route.

The sign on the locked door

Protected Resource Metadata is the MCP server publishing the auth facts a client needs before it can start the OAuth flow.

It may look roughly like this:

JSON
{
  "resource": "https://mcp.example.com",
  "authorization_servers": ["https://auth.example.com"],
  "scopes_supported": ["search.read", "items.write", "admin"]
}

The exact fields depend on the server, but the idea is simple:

  • this is the protected resource
  • this is where tokens come from
  • these are the kinds of permissions I understand

That one sign saves every new client from hardcoded setup.

The badge desk has its own map

The authorization server publishes metadata too. The client fetches Authorization Server Metadata to learn the authorization endpoint, token endpoint, supported scopes, supported grant types, supported response types, PKCE methods, and whether CIMD or DCR is available.

Here too, there are well-known routes. For a root issuer like https://auth.example.com, the client tries:

TEXT
https://auth.example.com/.well-known/oauth-authorization-server
https://auth.example.com/.well-known/openid-configuration

For a path-based issuer like https://auth.example.com/tenant1, the path stays part of discovery:

TEXT
https://auth.example.com/.well-known/oauth-authorization-server/tenant1
https://auth.example.com/.well-known/openid-configuration/tenant1
https://auth.example.com/tenant1/.well-known/openid-configuration

The client should reject metadata whose issuer does not exactly match the issuer it was trying to discover. Otherwise a malicious or misconfigured metadata document can point the flow at the wrong badge desk.

The MCP stable auth spec builds on OAuth 2.1, OAuth Authorization Server Metadata, Dynamic Client Registration, Protected Resource Metadata, and Resource Indicators.1

Client identity: CIMD and DCR

Unknown clients need an introduction

Traditional OAuth often assumes the client has already registered with the authorization server. MCP cannot always assume that.

An MCP client may discover a remote server today and its authorization server ten seconds later. There may be no prior relationship. No shared dashboard setup. No pre-created OAuth app.

So the client needs a way to say, "this is who I am," without forcing manual setup for every server-client combination.

There are three practical paths:

  • use pre-registered client details when the relationship already exists
  • use CIMD when the authorization server supports it
  • use DCR as a fallback or backwards-compatible path

The MCP draft now lists that priority explicitly: pre-registration first when available, Client ID Metadata Documents next, Dynamic Client Registration as fallback, then manual client details if nothing else works.3

CIMD: a public visitor card

CIMD means Client ID Metadata Document.

It turns the OAuth client_id into an HTTPS URL. That URL points to a JSON document describing the client.

JSON
{
  "client_id": "https://client.example.com/oauth/client-metadata.json",
  "client_name": "Example MCP Client",
  "client_uri": "https://client.example.com",
  "redirect_uris": [
    "http://127.0.0.1:3000/callback",
    "http://localhost:3000/callback"
  ],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

The authorization server sees the URL-shaped client_id, fetches the document, checks that the document's client_id exactly matches the URL, and uses the metadata inside.

The visitor card is not a secret. It tells the badge desk the visitor's name, homepage, redirect addresses, and the kind of OAuth flow it wants to use.

This avoids creating a fresh registration record for every unknown client.

CIMD is not free trust

CIMD is discovery, not blind trust.

The authorization server is fetching a URL supplied by an unknown client. That means it has to defend itself.

Good production behavior includes:

  • prevent SSRF when fetching metadata
  • limit response size
  • require HTTPS client IDs
  • validate JSON structure
  • require exact client_id match
  • validate redirect URIs against the metadata
  • display hostnames clearly on consent screens
  • apply allowlists or reputation rules when the deployment needs them

The IETF Client ID Metadata Document draft calls out SSRF, phishing, client metadata changes, and display risks as security considerations.4

The badge desk still decides whom to admit.

DCR: registration at the desk

DCR means Dynamic Client Registration.

Instead of the client hosting a metadata document, the client sends registration metadata to the authorization server and receives a client_id in response. RFC 7591 defines this flow.5

DCR was central in earlier MCP authorization guidance. The stable 2025-06-18 spec says MCP clients and authorization servers should support DCR because clients may not know every MCP server and authorization server ahead of time.1

The draft direction is different: new implementations should prefer CIMD when available and keep DCR for fallback and compatibility.3

That distinction matters if you are shipping now. Stable clients and servers may still expect DCR. Newer implementations should be ready for CIMD.

Tokens: audience, scopes, and PKCE

The token must name the right venue

A badge for one venue should not open another.

OAuth Resource Indicators let the client include a resource parameter in both the authorization request and the token request.

For MCP, the resource is the MCP server the client wants to access:

TEXT
resource=https%3A%2F%2Fmcp.example.com

The stable MCP auth spec says clients must include resource and servers must validate that tokens were issued for them.1

This is audience binding in everyday language. The badge says "Main Hall" on it. The person at the door checks that it says "Main Hall", not "Parking Garage", not "Another Conference", not "Some API from the same company."

PKCE is the claim ticket

Before the client sends the user to the authorization server, it creates a private code_verifier and a derived code_challenge.

The authorization server sees the challenge. Later, when the client exchanges the authorization code for tokens, it must present the verifier.

If someone steals the code but not the verifier, the code is not useful.

The venue version: when you entered the badge queue, you tore off half of a claim ticket and kept it. A stolen pickup slip is not enough without the matching half.

The MCP draft says clients must implement PKCE and verify PKCE support before proceeding.6

Scopes are the rooms on the badge

Bad scope design looks like this:

TEXT
all

Better scope design looks like this:

TEXT
search.read
items.create
items.update
comments.delete
admin

The exact names depend on the product, but the shape should match risk. A read-only search tool should not need the same permission as a destructive delete tool.

For MCP servers, scopes can map to tool families, read vs write actions, sensitive resources, tenant boundaries, admin operations, or workflow stages.

The useful question is not "how many scopes can we invent?" It is "what permission boundary would a user actually understand?"

The badge can grow when the work changes

The server should ask for a better badge when the work crosses a permission boundary, not fail mysteriously.

Imagine the user first approves search.read. Later the assistant tries to create something and needs items.create.

The MCP server can return 403 Forbidden with an insufficient_scope challenge that tells the client which scope is missing. The client can then take the user back through authorization for the additional permission.

That is step-up authorization.

The draft guidance recommends returning all scopes needed for the current operation in one challenge, instead of making the client discover them one painful retry at a time.7

Usually, the client should not ask for everything at the start. Broad first-run consent screens train users to click through scary prompts. A better pattern is to start with the smallest useful read scope, request write scopes when the user begins a write workflow, group scopes that naturally belong together, and step up at meaningful risk boundaries.

The badge should grow with the user's intent.

Failure modes

Do not pass tokens through

An MCP server should not pass through the token it received from the MCP client.

Token passthrough is dangerous because the downstream API may interpret the token differently. That can create a confused deputy problem: the MCP server accidentally helps the caller use authority somewhere else.

The safer pattern is:

  • the client gets a token for the MCP server
  • the MCP server validates that token
  • if the MCP server needs an upstream API, it uses a separate credential or obtains a separate upstream token

One badge per venue.

The MCP draft is explicit that servers must validate tokens for themselves and must not pass through tokens received from MCP clients.6

Refresh tokens need extra care

An access token is today's badge.

A refresh token is permission to ask the badge desk for another badge later.

That can be useful. It can also be risky. If a refresh token leaks, the attacker may keep coming back.

Production defaults:

  • store refresh tokens carefully
  • rotate refresh tokens for public clients
  • do not assume every authorization server will issue one
  • request offline access only when the experience needs it
  • do not make refresh tokens a resource requirement of the MCP server

Long-lived access should feel earned, not accidental.

Fail closed, but leave a sign

Production MCP auth should be generous with recovery hints and strict with trust.

MomentResponseWhy
No token, expired token, or invalid token401The client needs authorization or a fresh token.
Valid token, missing permission403The user is known, but this tool needs more scope.
Bad authorization request400The request is malformed and should be fixed.
Token for another resource401The badge is real, but it is not for this venue.

The best error is not just a wall. It is a locked door with the correct sign next to it.

The whole flow

Here is the compact version:

TEXT
Client tries MCP server.
Server says: 401, get auth metadata here.
Client fetches protected resource metadata.
Client finds the authorization server.
Client fetches authorization server metadata.
Client identifies itself with pre-registration, CIMD, or DCR.
Client starts authorization code flow with PKCE.
Client includes resource=https://mcp.example.com.
User approves scopes.
Client exchanges code for tokens.
Client calls MCP server with Authorization: Bearer <token>.
Server validates issuer, expiry, audience, resource, and scopes.
Tool runs.
If a later tool needs more scope, server triggers step-up.

That is the bridge from "the model can call my tool" to "the model can safely use my tool for the right user."

Production checklist

For the MCP server:

  • Protected Resource Metadata exists
  • authorization servers are advertised clearly
  • tokens are accepted only through the Authorization header
  • issuer, expiry, audience, resource, and scopes are validated
  • token passthrough is rejected
  • 401 and 403 responses carry useful challenges
  • scopes match real tool risk
  • logs do not contain tokens

For the MCP client:

  • WWW-Authenticate challenges are parsed
  • well-known resource metadata discovery works
  • authorization server metadata discovery works
  • pre-registration is used when configured
  • CIMD is used when supported
  • DCR fallback still works where needed
  • PKCE is mandatory
  • resource is sent in authorization and token requests
  • token and registration state are bound to the authorization server
  • step-up authorization feels understandable

For the authorization server:

  • metadata is published
  • CIMD or DCR support is advertised accurately
  • redirect URIs are exact-match validated
  • PKCE support is visible and enforced
  • unknown clients go through trust policy
  • CIMD fetches are SSRF-protected
  • tokens are audience-bound
  • refresh tokens are rotated for public clients
  • consent screens show the client and requested access clearly

The quiet part

MCP makes the rooms discoverable.

OAuth decides who can enter them.

Protected Resource Metadata is the sign on the locked door.

Authorization Server Metadata is the badge desk manual.

CIMD is a public visitor card.

DCR is on-the-spot visitor registration.

PKCE is the matching claim ticket.

The resource parameter prints the venue address on the badge.

Audience validation keeps badges from wandering into the wrong venue.

Scopes are the rooms on the badge.

Step-up authorization adds a room only when the user actually walks toward it.

That is the part that turns MCP into a production doorway.


Footnotes

  1. Model Context Protocol, "Authorization", version 2025-06-18, https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization 2 3 4

  2. Model Context Protocol draft, "Authorization Server Discovery", https://modelcontextprotocol.io/specification/draft/basic/authorization/authorization-server-discovery

  3. Model Context Protocol draft, "Client Registration", https://modelcontextprotocol.io/specification/draft/basic/authorization/client-registration 2

  4. IETF Internet-Draft, "OAuth Client ID Metadata Document", https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/

  5. RFC 7591, "OAuth 2.0 Dynamic Client Registration Protocol", https://datatracker.ietf.org/doc/rfc7591/

  6. Model Context Protocol draft, "Authorization Security Considerations", https://modelcontextprotocol.io/specification/draft/basic/authorization/security-considerations 2

  7. Model Context Protocol draft, "Authorization", https://modelcontextprotocol.io/specification/draft/basic/authorization