Add OAuth to your MCP server in 10 minutes

This guide walks through adding a full OAuth 2.1 authorization layer — including Dynamic Client Registration, so MCP clients like Claude can connect without any manual setup — to an existing MCP server, using the mcpauth SDK.

Prerequisites

  • Node.js 18 or later.
  • An existing MCP server built on @modelcontextprotocol/sdk, running over HTTP (Streamable HTTP or SSE transport). If your server currently only speaks stdio, get it running over HTTP first — mcpauth protects HTTP-based MCP servers.

Step 1: Install the SDK

mcpauth peer-depends on @modelcontextprotocol/sdk and express, so make sure those are already in your project.

npm install getmcpauth

Step 2: Create a project and get a registration secret

Sign in to the mcpauth dashboard with GitHub and create a project for your MCP server. Creating a project gives you a registration secret— a bearer credential that authenticates your server's calls to the mcpauth API (for example when minting tokens or introspecting them).

The registration secret is shown once, at creation time. Copy it immediately and store it somewhere safe — a secrets manager or your deploy platform's environment variable settings work well. If you lose it, you'll need to generate a new one from the project's dashboard page.

# .env
MCPAUTH_SECRET=mcpauth_sk_...   # your project's registration secret

Step 3: Wrap your server with the auth middleware

mcpAuth()is an Express middleware that wraps the official MCP SDK's requireBearerAuthmiddleware. Mount it in front of your MCP route and every request is verified before it reaches your server's handlers — unauthenticated or invalid requests get rejected automatically with a spec-correct 401.

import express from "express";
import { mcpAuth } from "getmcpauth";
import { yourMcpServerHandler } from "./mcp-server";

const app = express();

app.use(
  "/mcp",
  mcpAuth({
    registrationSecret: process.env.MCPAUTH_SECRET!,
  }),
);

// Your existing MCP server handler — now only reachable with a
// valid access token attached.
app.use("/mcp", yourMcpServerHandler);

app.listen(3000);

Successful token verifications are cached in-process for 30 seconds by default, so a chatty agent conversation doesn't trigger a network round-trip on every single tool call. You can tune this with cacheTtlMs, and restrict access with requiredScopes if your server issues scoped tokens:

mcpAuth({
  registrationSecret: process.env.MCPAUTH_SECRET!,
  cacheTtlMs: 60_000,
  requiredScopes: ["mcp:tools"],
})

Step 4: Connect a client — no manual setup required

This is the part that's normally the hard part. When an MCP client (Claude, or any other client built on the official MCP SDK) connects to your server for the first time, it:

  1. Gets a 401 from your server, with a pointer to the authorization server metadata.
  2. Fetches /.well-known/oauth-authorization-server from mcpauth, discovering every endpoint it needs (authorize, token, registration, revocation, introspection) from a single issuer URL, per RFC 8414.
  3. Calls POST /api/oauth/register to dynamically register itself as an OAuth client (RFC 7591) — no dashboard step, no pre-shared client ID, nothing for you or the end user to configure by hand.
  4. Sends the user through GET /oauth/authorize, where they sign in with GitHub and approve the connection on a consent screen, using PKCE the whole way (mandatory under OAuth 2.1).

This is Dynamic Client Registration — the part of the MCP spec almost no OAuth provider implements, which is why so many MCP servers ship with no auth or a bare API key instead. With mcpauth it's already done.

Step 5: Test it with curl

A request with no Authorization header should be rejected with a 401:

curl -i https://your-server.example.com/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# HTTP/1.1 401 Unauthorized
# WWW-Authenticate: Bearer resource_metadata="https://your-server.example.com/.well-known/oauth-protected-resource"

A request with a valid access token (obtained via the flow above, or minted directly — see token minting for server-to-server issuance) should succeed:

curl -i https://your-server.example.com/mcp \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# HTTP/1.1 200 OK

Troubleshooting

Client registration fails or a client shows up with unexpected settings

Double-check the JSON body sent to POST /api/oauth/register — at minimum it needs a redirect_uris array. If a client was registered with the wrong token_endpoint_auth_method, or an old client is stale, register a fresh one; DCR is meant to be cheap and repeatable, not a one-time manual step.

redirect_uri errors at the authorize step

The redirect_uri passed to GET /oauth/authorize must exactly match one of the URIs the client supplied when it registered via /api/oauth/register. Trailing slashes, ports, and scheme all count — if your client's redirect URI changed (for example a new local dev port), it needs to re-register.

Token requests fail with an invalid code_verifier

POST /api/oauth/token checks the code_verifier against the code_challenge sent to /authorize (PKCE, S256). If they don't match — often because the verifier was regenerated between the authorize and token calls, or lost across a redirect — the exchange is rejected. Generate the verifier once per authorization attempt and hold onto it until the token exchange completes.

Access token stopped working after a while

Access tokens expire (see expires_in in the token response). Use the accompanying refresh_token with grant_type=refresh_token against POST /api/oauth/token to get a new one — the MCP SDK client handles this automatically in most cases.

Next steps

  • SDK reference — every export in the mcpauth package, including McpAuthTokenVerifier and the resource-metadata helpers.
  • Token minting — for servers embedded in a product that already has its own users, skip the browser consent flow entirely with /api/oauth/token/exchange.
  • Endpoint reference — full request/response shapes for every OAuth endpoint.
  • mcpauth vs. bare API keys — why real OAuth is worth the ten minutes.