MCP OAuth Explained: Agent Email Without API Keys

When you click "connect" on a remote MCP server in Claude, Cursor, or Grok Bot and a consent page opens in your browser, five RFCs just executed in about two seconds. The Model Context Protocol standardized its authorization layer on OAuth 2.1, and it is the difference between pasting API keys into every client and clicking one Allow button that you can revoke later. We just shipped the full flow on Dead Simple's hosted email MCP server, so this guide walks through how MCP OAuth actually works, with real payloads from a production server rather than pseudocode, plus the implementation mistakes that quietly break it.

If you just want the result: add https://api.deadsimple.email/mcp to any OAuth-capable MCP client with no credentials, approve the consent screen, and your agent has 14 email tools. The rest of this post is what happens underneath.

Why MCP Needed OAuth

The first generation of remote MCP servers authenticated with static bearer tokens: get an API key from a dashboard, paste it into a config file or an install dialog. That works, and it is still the right answer for CI and server-side agents. But it has three problems at the "end user connects a tool to their chat client" scale MCP now operates at. First, friction: every key paste is a place where a non-developer gives up. Second, sprawl: a key pasted into four clients is four copies of a permanent credential nobody remembers to rotate. Third, scope: most API keys are account-wide, so the calendar plugin you tried once holds the same power as your production backend.

The MCP specification's answer, introduced in the March 2025 revision and refined since, is an optional authorization layer built on OAuth 2.1 for HTTP transports. The design goal: a client that has never seen your server before can discover how to authenticate, register itself, and obtain a scoped, revocable, refreshable token, all without the server operator pre-registering anything.

The Moving Parts

MCP OAuth composes five standards. Knowing which does what makes the flow legible:

Standard Role in MCP
RFC 9728 (Protected Resource Metadata)The MCP server says "here is who can issue tokens for me"
RFC 8414 (AS Metadata)The authorization server publishes its endpoints and capabilities
RFC 7591 (Dynamic Client Registration)The client creates its own client_id on first contact
OAuth 2.1 + PKCE (RFC 7636)Authorization-code flow, safe for public clients with no secret
RFC 7009 (Revocation)Kill a token before it expires

The Flow, With Real Payloads

Step 1: The 401 challenge

Everything starts with a request that has no credentials. A spec-compliant server must answer 401 and point at its metadata:

POST /mcp (no Authorization header)
HTTP/2 401
WWW-Authenticate: Bearer resource_metadata=
  "https://api.deadsimple.email/.well-known/oauth-protected-resource/mcp"

This header is the trigger. A client that sees it starts the OAuth dance; a client that never sees it assumes the server is open. Which leads to the first implementation trap: if your server answers unauthenticated initialize calls with 200, OAuth-capable clients will connect "successfully" and then fail on every tool call. The 401 has to come first.

Step 2: Discovery

The client fetches the resource metadata, learns which authorization server covers this resource, then fetches that server's own metadata:

GET /.well-known/oauth-authorization-server
{
  "issuer": "https://api.deadsimple.email",
  "authorization_endpoint": ".../oauth/authorize",
  "token_endpoint": ".../oauth/token",
  "registration_endpoint": ".../oauth/register",
  "revocation_endpoint": ".../oauth/revoke",
  "code_challenge_methods_supported": ["S256"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["none"]
}

Two details worth noticing. token_endpoint_auth_methods_supported: ["none"] declares that clients are public: a desktop app cannot keep a secret, so there are no client secrets at all, and PKCE carries the proof instead. And serve the metadata at the path-suffixed variant too (/.well-known/oauth-authorization-server/mcp): some clients request it, and a 404 there ends the flow.

Step 3: Dynamic client registration

The client has no client_id yet, so it mints one:

POST /oauth/register
{"client_name": "Claude",
 "redirect_uris": ["https://claude.ai/api/mcp/auth_callback"]}

// 201 response
{"client_id": "dse_oc_4f2a91c86d0b7e13a5c8",
 "token_endpoint_auth_method": "none", ...}

Registration is unauthenticated by design, which means it needs its own guardrails: validate redirect URIs at registration time (https, loopback http for native apps, or app schemes; never javascript: or data:), cap the list, and rate limit by IP.

Step 4: Authorization and consent

The client opens a browser at the authorization endpoint with a PKCE challenge. The server validates the client and redirect URI, parks the request, and sends the user to a consent page, in our case on the dashboard, where they sign in with their normal account and see exactly what the client gets: inbox and message operations, and explicitly not billing, team management, or API-key administration. One Allow click redirects the browser back to the client's callback with a single-use authorization code bound to the approving account.

The rule that matters most here comes straight from RFC 6749: if the redirect URI is not an exact match for a registered one, respond 400 and never redirect. Redirecting errors to an unverified URI is how authorization codes get exfiltrated.

Step 5: Token exchange

POST /oauth/token
grant_type=authorization_code&code=dse_oac_...&
code_verifier=<the client's original random string>

// 200 response
{"access_token": "dse_oat_...", "token_type": "Bearer",
 "expires_in": 604800, "refresh_token": "dse_ort_...",
 "scope": "email"}

The server hashes the presented code_verifier with SHA-256 and compares it to the code_challenge from step 4. A stolen code without the verifier is useless. From here the client sends Authorization: Bearer dse_oat_... on every MCP request, exactly as it would an API key, and refreshes silently when the token ages out.

Design Decisions That Keep It Safe

  • PKCE S256 only. The plain method exists in the RFC and has no reason to exist in 2026. Rejecting it costs nothing.
  • Everything is single-use and short-lived by default. Authorization requests expire in 10 minutes, codes in 5 minutes and burn on first exchange, access tokens last 7 days, refresh tokens last 90 days and rotate on every use, so a leaked refresh token dies the next time the legitimate client refreshes.
  • Tokens are hashed at rest. Like passwords and API keys, OAuth tokens should be stored as SHA-256 digests. A database leak then leaks nothing usable.
  • Scope less than the account. An OAuth grant is a stranger your user met through a chat client. Our tokens carry email operations only; API-key administration, billing, and team management are structurally absent from the permission set.
  • Expired means 401, not a sad tool result. The second big implementation trap: validating tokens only inside tool execution returns errors wrapped in HTTP 200, and the client, seeing a healthy transport, never refreshes. An expired or revoked token must produce an HTTP 401 with WWW-Authenticate: Bearer error="invalid_token", which is the signal clients act on. We validate at the transport layer with a short positive cache so revocation takes effect within 30 seconds.

OAuth or API Key? Both, and When to Use Which

Context Use Why
Claude, Cursor, Grok Bot, grok.com connectors OAuth One click, no key handling, revocable per client
CI, cron jobs, server-side agents API key No browser available for consent; keys live in a secret manager anyway
Local stdio MCP server API key (env var) One process per user; the env var never leaves the machine
Multi-tenant SaaS calling on behalf of customers Scoped API keys / workspaces Tenant isolation needs first-class resources, not just tokens

This is why the answer to "OAuth or keys?" for a production MCP server is both, on the same endpoint. Ours accepts Bearer dse_... API keys and Bearer dse_oat_... OAuth tokens interchangeably; the transport does not care which trust path minted the credential.

What This Unlocks for Email Specifically

Email is the sharpest version of the "stranger in your account" problem, because the alternative most people reach for is handing an agent their personal Gmail via Google OAuth. That grants a bot the keys to a human identity, inside a consumer platform that actively suspends automated accounts. MCP OAuth against agent-native infrastructure inverts it: the inboxes are the agent's own identity, the consent shows real scopes, and revoking the grant cannot lock a human out of anything. The connected client gets the full toolset, from create_inbox through verification-code extraction, with webhooks still available server-side on the same account.

In the agent-inbox category, one-click OAuth connect is now the bar: AgentMail ships it, and as of this week Dead Simple does too, on top of the same 14-tool MCP surface that already worked with keys. Most transactional email providers' MCP servers remain key-only.

Frequently Asked Questions

Does MCP require OAuth?

No, but it standardizes it. Authorization is an optional layer in the MCP spec for HTTP transports; servers may accept static bearer credentials instead, and many production servers support both. What the spec buys you is that every OAuth-capable client speaks the same discovery-registration-PKCE sequence with zero server-specific code.

Which MCP clients support OAuth today?

Claude (web, desktop, Claude Code), Cursor, Grok Bot, and grok.com custom connectors all trigger the flow automatically off the 401 challenge. Headless contexts generally do not, which is what API keys remain for.

What actually triggers the flow in a client?

The 401 with a WWW-Authenticate header naming a resource_metadata URL. No 401, no OAuth. The same mechanism handles expiry: a 401 with error="invalid_token" tells the client to refresh or re-authorize.

How is this different from Google OAuth for Gmail?

Google OAuth shares a human's mailbox with an agent. MCP OAuth authorizes an agent against inboxes it owns, with agent-scale rate limits and no consumer account to suspend. Same protocol family, opposite trust model.

Can I still use plain API keys with Dead Simple's MCP server?

Yes, unchanged: Authorization: Bearer dse_your_api_key against the same endpoint, which is what the Grok Build CLI setup and CI environments use. OAuth and keys coexist; use whichever fits the context.

The Bottom Line

MCP OAuth turns "email API for agents" from a developer product into something anyone can connect from a chat window: add a URL, click Allow, done. The plumbing is five RFCs deep, but the properties it buys are simple: no key paste, real scopes, silent refresh, instant revocation. It is live on https://api.deadsimple.email/mcp now, alongside the API keys that servers and CI will always prefer.

Sign up free (5 inboxes, 5,000 emails a month, no card) and try it: add the server to Claude or Cursor with no credentials and watch the consent flow run. The MCP integration page has the endpoint reference, and getting started covers the REST API side.

Connect your agent's inbox with one click

OAuth 2.1 on the hosted MCP server: no key paste, real scopes, instant revocation. Five inboxes free, no card.