Reference · AI agents on social media

MCP server OAuth: how a remote server authenticates an agent, and what breaks

By Antoine Blanco··7 min read

A remote MCP server authenticates an agent as an OAuth 2.1 resource server. It answers an unauthenticated call with 401 and a WWW-Authenticate header, the client follows that to the server's metadata, runs an authorization code flow with PKCE, and sends the token as a bearer header on every request. Here is each step, run live today.

Authorization is optional in MCP, and only for HTTP transports: a local stdio server reads credentials from the environment instead. Everything below is from the current specification revision, 2026-07-28, and every response is a real call to https://api.plugkit.co/mcp made on October 16, 2026.

The handshake, in four responses

Step 1. Call without a token and read the challenge. The server must return 401 with a WWW-Authenticate header pointing at its metadata.

curl -i -X POST https://api.plugkit.co/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
HTTP/2 401
www-authenticate: Bearer resource_metadata="https://api.plugkit.co/.well-known/oauth-protected-resource", scope="mcp:read mcp:write"
{"error":"Missing credentials. Send `Authorization: Bearer <api key or session token>`.","type":"authentication_error","code":"unauthorized"}

The scope hint is a SHOULD in the spec, not a MUST, and it is the difference between a client that asks for the right permissions on the first try and one that guesses. Clients must treat the challenged scopes as authoritative for the operation.

Step 2. Fetch the protected resource metadata. This is RFC 9728, and it is a MUST for every MCP server.

{"resource":"https://api.plugkit.co/mcp",
 "authorization_servers":["https://api.plugkit.co"],
 "scopes_supported":["mcp:read","mcp:write"],
 "bearer_methods_supported":["header"],
 "resource_documentation":"https://docs.plugkit.co"}

Step 3. Fetch the authorization server metadata. RFC 8414, or OpenID Connect Discovery; the server must offer at least one, and the client must support both.

{"issuer":"https://api.plugkit.co",
 "authorization_endpoint":"https://api.plugkit.co/oauth/authorize",
 "token_endpoint":"https://api.plugkit.co/oauth/token",
 "registration_endpoint":"https://api.plugkit.co/oauth/register",
 "revocation_endpoint":"https://api.plugkit.co/oauth/revoke",
 "code_challenge_methods_supported":["S256"],
 "token_endpoint_auth_methods_supported":["none"],
 "client_id_metadata_document_supported":true,
 "authorization_response_iss_parameter_supported":true}

Step 4. Run the code flow and call again with the token. Authorization code with PKCE, the resource parameter on both requests, then Authorization: Bearer <token> on every single HTTP request, including requests in the same session.

MCP server authentication: where the client ID comes from

This is the part that changed, and the part most tutorials still get wrong. The current revision lists three mechanisms and ranks them.

Mechanism Status in the 2026-07-28 revision What the client sends as client_id
Client ID Metadata Documents SHOULD support An HTTPS URL the server fetches for the client's metadata
Pre-registration Allowed A client ID you were issued out of band
Dynamic Client Registration (RFC 7591) MAY support, deprecated A client ID from POST /register

Dynamic registration was a SHOULD in the 2025-06-18 revision. It is now explicitly "deprecated and retained for backwards compatibility with authorization servers that do not support Client ID Metadata Documents". CIMD replaces it with something simpler: the client uses a URL as its identifier, the authorization server fetches that URL, reads the metadata and validates the redirect URIs. No registration call, no stored client record per client.

The PlugKit server above advertises both, client_id_metadata_document_supported: true and a live registration_endpoint, which is the right posture while clients catch up.

MCP OAuth 2.1: PKCE, the resource parameter and audience binding

Three requirements do the security work, and all three are MUSTs for clients.

  1. PKCE. Required on every client. S256 is the method the server above publishes. It stops an intercepted authorization code from being redeemed by anyone but the client that started the flow.
  2. The resource parameter. RFC 8707, on both the authorization request and the token request, carrying the canonical URI of the MCP server: &resource=https%3A%2F%2Fapi.plugkit.co%2Fmcp. Clients must send it whether or not the server supports it.
  3. Audience validation. The server must verify the token was issued for it, and must reject anything else. The spec is categorical: an MCP server "MUST NOT accept or transit any other tokens".

That last one has a name. If a server takes a token meant for someone else and forwards it upstream, you get the confused deputy problem: the downstream API trusts the call because it arrived from the server. Token passthrough is forbidden, which is why a server calling an upstream API must hold its own separate token for it. That is the "server to platform" half of the problem, and it has its own failure modes, covered in social media access tokens.

What breaks

Five failures, in the order you are likely to meet them.

Symptom Status Cause Fix
Client never starts the flow 401 No WWW-Authenticate, or no resource metadata Implement RFC 9728, return the header on every 401
Tool call refused, token looks fine 403 Token lacks a scope the operation needs Return error="insufficient_scope" with scope, let the client step up
Loop of browser consent prompts 401 Refresh token expired or revoked, client retries blindly Cap retries, treat repeated failure as permanent
Token rejected by a second server 401 Token sent to a server it was not issued for Send the resource parameter, validate the audience
Flow completes against the wrong server none Mix-up attack, iss not checked Record the issuer before redirecting, compare on return

The scope one deserves detail because it is new and easy to get wrong. When a token is valid but too narrow, the server should answer 403 with error="insufficient_scope" and the scopes the operation needs. The client then re-authorizes with the union of what it already had and what was just challenged, not just the new scope, or it loses the permissions it was using before. Servers should send every scope the operation needs in one challenge rather than drip them one per round trip.

The iss check is the quietest of the five, because nothing fails visibly. The client must record the issuer from the validated metadata before opening the browser, then compare the iss that comes back on the callback, with no normalisation: no case folding, no trailing slash, no percent-encoding cleanup. The server above sets authorization_response_iss_parameter_supported: true, so a missing iss on the response must be rejected outright.

Remote MCP server auth: OAuth or an API key?

Both work, and the trade-off is scope and revocation, not security theatre.

OAuth 2.1 API key
Granularity Scopes, read only is possible Full access, no scopes
Who holds the secret The client, refreshable You, in a config file or env var
Revocation Per connected app, by the user Rotate the key, breaks every consumer
Good for Interactive clients, third parties Scripts, cron, your own backend

On the server above, the two scopes split cleanly: mcp:read covers profiles, accounts, posts, conversations and analytics; mcp:write adds publishing, sending messages, replying to comments and connecting accounts. An API key carries full access and no scopes, which is the honest reason to prefer OAuth for anything a model drives interactively. Run an agent on mcp:read for a week before you let it write, as in Claude as your social media manager.

The same server is reachable from Claude, ChatGPT, Claude Code, Cursor and VS Code, and costs $49 a month for 5 connected accounts, cancel anytime, with 7 days free and no card. Whether an MCP server is even the right shape for your integration is a separate question, answered in MCP or REST API for your agent; for a worked example of one in a client, see the Instagram MCP server.

Full normative text: the MCP authorization specification, read October 16, 2026.

FAQ

Does an MCP server need OAuth?

Only if it is remote and you want per-user authorization. Authorization is optional in the spec, and local stdio servers should not use it at all, taking credentials from the environment instead. An HTTP server that serves several users should implement it, because an API key gives every holder the same full access.

What OAuth version does MCP use?

OAuth 2.1, with PKCE required on all clients, protected resource metadata (RFC 9728) required on all servers, and the resource parameter from RFC 8707 required on authorization and token requests. The current revision is 2026-07-28, which also brings in issuer identification from RFC 9207.

Is dynamic client registration still required for MCP?

No. It moved from SHOULD in the 2025-06-18 revision to MAY in the current one, and the spec calls it deprecated, kept for servers that do not support Client ID Metadata Documents. CIMD is the preferred path: the client uses an HTTPS URL as its client ID and the authorization server fetches the metadata from it.

Why does my MCP client keep asking me to log in again?

Usually an expired or revoked refresh token with a client that retries the flow instead of stopping. The spec tells clients to cap retries and treat repeated failure as permanent. Check whether the user revoked the app, and whether your authorization server rotates refresh tokens, which it must do for public clients.

What does a 403 from an MCP server mean?

The token is valid but lacks a scope the operation needs. The server should return error="insufficient_scope" along with the required scopes, and the client should re-authorize asking for those plus everything it already had. A 401 means missing or invalid credentials instead, and a 400 means the authorization request itself was malformed.

AB
Antoine BlancoFounder of PlugKit. Runs his own Instagram, YouTube and X accounts through the API and MCP server described here.

One plug for every platform your agent touches

$49 a month for 5 connected accounts, cancel anytime. Every feature in every plan.

Get your API key →
English ▾