mcp-rune 0.107.0
Be star #1 Get started
SECTION VII · GUIDE 31 OF 49
Reading
9 min
Topic
auth · rfc
Spec
v0.107.0
Source
07-auth-and-transport/oauth2-discovery.md
On this page7 sections

OAuth 2.0 Discovery Flow

This document describes the OAuth 2.0 flow implemented by servers built on mcp-rune’s HttpServer, with explicit citations of the RFCs each route implements. It is the canonical, current reference for the HTTP transport’s authentication story.

Overview

Any server built on mcp-rune’s HttpServer (src/mcp/http-server.ts) with OAuth enabled acts as an OAuth 2.0 Resource Server. It:

  • Exposes RFC 9728 Protected Resource Metadata so clients can discover the authorization server.
  • Proxies authorization-server metadata (RFC 8414), Dynamic Client Registration (RFC 7591), authorize/token endpoints (RFC 6749), and forwards PKCE (RFC 7636) and resource indicators (RFC 8707) parameters transparently to an upstream authorization server.
  • Validates bearer tokens on /mcp via token introspection against the authorization server.

The MCP server never issues tokens itself (except via the M2M /mcp/m2m/token convenience endpoint, which is a thin wrapper around the authorization server’s Client Credentials grant). All user authentication and token issuance happens on the authorization server; the MCP server is a thin, spec-compliant façade.

RFC Map

RFCTitleRoute(s) exposedImplementation
RFC 6749The OAuth 2.0 Authorization FrameworkGET /oauth/authorize, POST /oauth/tokensrc/mcp/middleware/oauth-router.ts — authorize handler, token proxy
RFC 7591OAuth 2.0 Dynamic Client Registration Protocol (DCR)POST /oauth/registersrc/mcp/middleware/oauth-router.ts
RFC 7636Proof Key for Code Exchange (PKCE)Forwarded through /oauth/authorize and /oauth/tokenClient provides code_challenge/code_verifier; parameters pass through unchanged
RFC 8414OAuth 2.0 Authorization Server MetadataGET /.well-known/oauth-authorization-server, GET /.well-known/openid-configurationsrc/mcp/middleware/oauth-router.ts — server metadata, openid alias
RFC 8707Resource Indicators for OAuth 2.0resource query/body param forwarded on authorize + tokensrc/oauth2/service.ts
RFC 9728OAuth 2.0 Protected Resource MetadataGET /.well-known/oauth-protected-resource, GET /.well-known/oauth-protected-resource/mcpsrc/mcp/middleware/oauth-router.ts — both forms share one handler
MCP Authorization spec 2025-06-18MCP-specific framing of the aboveWWW-Authenticate on /mcp with resource_metadata parametersrc/mcp/middleware/oauth-router.tssendUnauthorized()

End-to-End Discovery Flow

OAUTH2 DISCOVERY · PROXIED FLOWClientMCP ServerAuth ServerDISCOVERY · 401POST /mcp (no token)401 · WWW-Authenticate: Bearer · RFC 9728METADATA DISCOVERYGET protected-resource metadata · 9728{ resource, authorization_servers }GET authorization-server metadata · 8414fetch + rewrite upstreamDYNAMIC CLIENT REGISTRATIONPOST /oauth/register · RFC 7591proxied{ client_id, client_secret }AUTHORIZATION · PKCEGET /oauth/authorize · 6749 + PKCE + 8707302 redirectuser authenticates on auth server → redirect to /oauth/callback with auth codeTOKEN EXCHANGEPOST /oauth/token · PKCE + resourceproxied{ access_token, refresh_token }AUTHENTICATED SESSIONPOST /mcp + Authorization: Bearerintrospect (cached)Session created · MCP tools served

RFC 9728 §3.1 Path-Insertion and the “Why Two Routes?” Quirk

RFC 9728 §3.1 defines how to build the Protected Resource Metadata URL for a resource whose URL has a non-root path:

The well-known path component (.well-known/oauth-protected-resource) is inserted between the origin and the resource path, not appended at the root.

Example for this repo:

Resource URL (the /mcp endpoint)Canonical metadata URL (RFC 9728 §3.1)
http://localhost:4100/mcphttp://localhost:4100/.well-known/oauth-protected-resource/mcp
https://example.com/my-mcp-server/mcphttps://example.com/.well-known/oauth-protected-resource/my-mcp-server/mcp

buildResourceMetadataUrl() in src/mcp/middleware/oauth-router.ts constructs this form, and sendUnauthorized() advertises it in the WWW-Authenticate: Bearer resource_metadata="…" header whenever /mcp is hit without a token.

Both forms are served

The router registers the same handler on two paths:

  1. /.well-known/oauth-protected-resource — origin-only, legacy/fallback form.
  2. /.well-known/oauth-protected-resource/mcp — RFC 9728 §3.1 canonical form for the ${baseUrl}/mcp resource.

Why serve both? Because the two groups of clients in the wild behave differently:

  • Strict clients (aligned with MCP spec 2025-06-18) parse the resource_metadata parameter from the WWW-Authenticate header and fetch it verbatim. They hit the /mcp-suffixed URL and expect a 200. If the server did not serve this form, these clients would fail discovery entirely.
  • MCP Inspector (up to current versions) implements an extra-spec fallback: if the path-suffixed URL 404s, it retries the origin-only URL. This behavior is not required by RFC 9728 and must not be relied upon in new client implementations. mcp-rune serves the origin-only form so the existing Inspector workflow keeps working.

Diagnostic: the two log lines

If your logs show this pair:

GET /.well-known/oauth-protected-resource       → 200 / 304
GET /.well-known/oauth-protected-resource/mcp   → 404

…the server is advertising the §3.1 URL in WWW-Authenticate but not serving it. MCP Inspector hides the problem via its fallback, but strict clients break. The fix is exactly what mcp-rune does: register both forms with the same handler. See src/mcp/middleware/oauth-router.ts around the protectedResourceHandler definition and the __tests__/lib/mcp/middleware/oauth-router.spec.ts spec that covers the §3.1 path.

Path-Prefixed Deployments (/my-mcp-server/mcp rather than /mcp)

.well-known URIs are origin-scoped by RFC 8615 and re-affirmed by RFC 9728 §3.1: they live at the root of the host, with the resource path appended after the well-known segment, never nested inside a sub-path. That means a server reverse-proxied under a non-root path cannot serve its own Protected Resource Metadata at the canonical URL — the framework’s HTTP listener simply never sees requests for /.well-known/... once an upstream proxy is routing only /my-mcp-server/* to it.

When HttpServer is constructed with a non-empty pathPrefix, the OAuth router auto-skips registering the PRM endpoints (serveProtectedResourceMetadata: false). The WWW-Authenticate header continues to advertise the correct origin-rooted URL via buildResourceMetadataUrl(), and the operator is responsible for serving that URL from the reverse proxy.

Example for a deployment at https://example.com/my-mcp-server/mcp:

ElementValue
Resource URLhttps://example.com/my-mcp-server/mcp
Canonical PRM URL (RFC 9728 §3.1)https://example.com/.well-known/oauth-protected-resource/my-mcp-server/mcp
WWW-Authenticate (emitted by mcp-rune on 401)Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource/my-mcp-server/mcp"
Served byUpstream reverse proxy, not mcp-rune

Minimal nginx snippet to serve the PRM JSON at the origin root:

location = /.well-known/oauth-protected-resource/my-mcp-server/mcp {
    default_type application/json;
    add_header Access-Control-Allow-Origin *;
    return 200 '{"resource":"https://example.com/my-mcp-server/mcp","authorization_servers":["https://example.com"]}';
}

For the root-mount case (pathPrefix empty / unset), nothing changes: mcp-rune serves both PRM forms itself.

Unauthorized Response Contract

Every request to /mcp without a valid bearer token receives:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="${origin}/.well-known/oauth-protected-resource/mcp"
Content-Type: application/json

{ "error": "unauthorized", "error_description": "Authentication required. See WWW-Authenticate header for authorization server details." }

This matches MCP spec 2025-06-18 (§ Authorization) and RFC 9728 §5.1. The resource_metadata URL is always the §3.1 path-inserted form.

FileRole
src/mcp/middleware/oauth-router.tsExpress router with all OAuth routes + buildResourceMetadataUrl / sendUnauthorized helpers
src/mcp/http-server.tsHTTP server entry point; mounts the OAuth router and invokes sendUnauthorized on unauthenticated /mcp requests
src/oauth2/service.tsOAuth client logic: token introspection, client-credentials grant, resource indicator handling
__tests__/lib/mcp/middleware/oauth-router.spec.tsRoute-level unit tests, including RFC 9728 §3.1 coverage