A minimal Model Context Protocol server that demonstrates OAuth 2.1 protected Streamable HTTP transport — the pattern for exposing an MCP server to remote clients (Claude Code, the claude.ai web app, custom agents) with per-request bearer-token auth instead of stdio + local secrets.
It plays the resource server role. Token issuance is delegated to an external OAuth 2.1 authorization server (e.g. Auth0, Keycloak, Okta, WorkOS); this server only validates the tokens it receives.
Following the MCP authorization spec:
- Client hits
POST /mcpwith no token → server replies 401 with aWWW-Authenticateheader carrying theresource_metadataURL. - Client fetches
/.well-known/oauth-protected-resource(RFC 9728) to discover the authorization server. - Client runs the OAuth 2.1 code + PKCE flow against that authorization
server and obtains an access token scoped to this resource
(RFC 8707
resourcebinding). - Client retries with
Authorization: Bearer <token>. The server validates signature (JWKS), issuer, expiry, and audience before serving the tool.
Audience validation is what stops token passthrough — a token minted for some other service is rejected here.
pnpm install
cp .env.example .env # point AUTH_* at your authorization server
pnpm dev # tsx watch on http://localhost:3000Verify the public metadata endpoint (no auth):
curl -s localhost:3000/.well-known/oauth-protected-resource | jqCall the protected endpoint without a token to see the 401 challenge:
curl -i -X POST localhost:3000/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'With a valid token, tools/list returns the whoami tool, which echoes the
identity from the access token.
| command | what it does |
|---|---|
pnpm dev |
run with hot reload (tsx watch) |
pnpm build |
compile TypeScript to dist/ |
pnpm start |
run the compiled server |
pnpm typecheck |
type-check without emitting |
pnpm inspect |
launch the MCP Inspector against it |