Skip to content

Security and Authorization

An MCP deployment is never just “a client and a server”. A real request crosses several trust boundaries, and each one is a place where credentials can leak, be reused, or be forged:

flowchart LR
  U["User"] -->|"approves actions"| H["Host (AI app)"]
  H -->|"one client per server"| C["MCP Client"]
  C -->|"MCP over stdio or HTTP"| S["MCP Server"]
  S -->|"its own credentials"| T["Third-party API"]
  A["Authorization Server"] -.->|"issues tokens for S"| C
The trust boundaries a single tool call crosses
BoundaryWhat crosses itThe question to ask
User ↔ HostConsent to run tools, see elicitation forms, open URLsDoes the user know which server is asking, and for what?
Host ↔ ClientTool definitions and results going into the modelAre tool descriptions and results treated as untrusted input?
Client ↔ ServerRequests, plus a bearer token on HTTPWas this token issued for this server?
Server ↔ Third-party APIThe server’s own credentials for an upstream serviceIs the server reusing the client’s token instead of its own?

This module is about the last three boundaries. The first one is a UX obligation covered in Server Primitives and Client Interaction.

The authorization spec is transport-scoped. It is OPTIONAL overall, and when a server supports it:

  • Streamable HTTP servers SHOULD implement the OAuth 2.1 flow described in this module.
  • stdio servers SHOULD NOT implement it. A stdio server is a subprocess the host launched on the user’s own machine; it takes credentials from the environment (env in the host’s config, a keychain, a config file) exactly as any local CLI would.
  • Custom transports MUST follow the established security practices for their protocol.

The reasoning is simple: a stdio server has no network endpoint to protect, and the host already decided to trust it enough to execute it. A remote HTTP server, by contrast, is reachable by anyone who can send a POST, so it needs a way to know who is asking and what they were granted.

// Host config for a stdio server: credentials come from the environment
{
"mcpServers": {
"github": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@example/github-mcp"],
"env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }
}
}
}

MCP does not invent an authentication protocol. It assigns OAuth 2.1 roles to the participants it already has:

MCP participantOAuth 2.1 roleResponsibility
MCP server (HTTP)Resource serverAccepts bearer tokens, validates them (signature, expiry, audience, scopes), answers 401/403 when they fail
MCP clientOAuth clientDiscovers the authorization server, registers or identifies itself, runs the authorization-code flow with PKCE on the user’s behalf, sends the token on every request
Authorization serverAuthorization serverAuthenticates the user, shows consent, issues access (and optionally refresh) tokens for a specific resource
UserResource ownerGrants or denies the client access to the server

The authorization server may be hosted next to the MCP server or be a separate identity provider. The spec deliberately says nothing about how it is implemented; it only requires that the MCP server point to it through Protected Resource Metadata, covered in the next lesson.

Most of OAuth is “SHOULD”. Three rules are “MUST”, and they are the ones that keep tokens from wandering across boundaries:

  1. Audience binding. The client requests a token for this server (resource= parameter), and the server MUST verify the token was issued to it. A token for https://mail.example.com is worthless at https://mcp.example.com, even if the same authorization server signed both.
  2. No token passthrough. A server MUST NOT accept tokens that were not issued for it, and MUST NOT forward a client’s token to any downstream API. Upstream calls use the server’s own credentials.
  3. Bearer header only. Tokens travel in Authorization: Bearer … on every request, never in the URL query string where they end up in logs and browser history.

Everything else in this module follows from those three.

The MCP authorization spec composes a subset of existing IETF and OpenID work rather than defining new crypto. You do not need to read them all, but you should recognise the names:

StandardWhat MCP uses it for
OAuth 2.1 (draft-ietf-oauth-v2-1)The base flow: authorization code + PKCE, no implicit grant
RFC 6750Bearer token usage and the WWW-Authenticate challenge format
RFC 9728Protected Resource Metadata: how a server advertises its authorization server(s)
RFC 8414Authorization Server Metadata discovery (/.well-known/oauth-authorization-server)
OpenID Connect Discovery 1.0The alternative discovery document (/.well-known/openid-configuration)
RFC 8707Resource indicators: the resource= parameter that binds a token to one server
RFC 9207The iss parameter on the redirect, which defeats mix-up attacks
Client ID Metadata Documents (draft)HTTPS URLs as client identifiers, replacing dynamic registration
RFC 7591Dynamic Client Registration, now deprecated but kept for compatibility
LessonYou will learn
The Authorization FlowThe full sequence from a 401 to a working bearer token, with exact headers and documents
Client RegistrationPre-registration, Client ID Metadata Documents, and why dynamic registration was deprecated
Protecting Your ServerVerifying tokens in the TypeScript and Python SDKs, scopes, and step-up challenges
Threats and MitigationsToken passthrough, confused deputy, SSRF, handle hijacking, prompt injection, and what stops each
Third-Party AuthorizationWhen your server needs its own access to another API, and how URL elicitation keeps that safe
Which transport is the MCP authorization spec written for?
In OAuth 2.1 terms, what role does an HTTP MCP server play?
A server receives a valid, unexpired token that was issued for a different service by the same authorization server. What must it do?