Skip to content

MCP OAuth Stage 2: well-known PRM, AS metadata, JWKS endpoints#90

Merged
heskew merged 1 commit into
mainfrom
mcp/well-known-metadata
May 21, 2026
Merged

MCP OAuth Stage 2: well-known PRM, AS metadata, JWKS endpoints#90
heskew merged 1 commit into
mainfrom
mcp/well-known-metadata

Conversation

@heskew
Copy link
Copy Markdown
Member

@heskew heskew commented May 21, 2026

Drafted by Claude (Opus 4.7, 1M context) on Nathan's behalf, following the harper-engineering-guidelines development lifecycle. Code, tests, the pre-push Gemini cross-review, and this description are all agent-authored; pushed under Nathan's account because the agent runs locally on his machine. Human review expected before merge.

Closes #92. Sub-issue of #86 (parent roll-up).

Summary

Stage 2 of MCP OAuth support. Mounts three discovery documents so MCP clients can locate the authorization server, see what it supports, and (eventually) verify the tokens it issues:

  • /.well-known/oauth-protected-resource (RFC 9728 PRM)
  • /.well-known/oauth-authorization-server (RFC 8414 AS metadata)
  • /.well-known/jwks.json (placeholder, empty key set until Stage 4)

Why

Without these documents an MCP client has no way to discover the authorization server from a 401 response — the discovery contract is the entry point to the entire MCP OAuth flow. The PRM is what withMCPAuth (Stage 5) will point at via WWW-Authenticate: Bearer resource_metadata="...". The AS metadata is what the client reads to find /oauth/mcp/register (already shipped in #89), /oauth/mcp/authorize (Stage 3), /oauth/mcp/token (Stage 4), and /.well-known/jwks.json.

Where to look

  • src/lib/mcp/wellKnown.ts — three builder functions + a registration helper. Note the getter pattern for mcpConfig: handlers read current config at request time so live config changes apply without re-registering routes (Harper's server.http has no deregistration). Each handler also exact-checks request.pathname — Harper's urlPath is prefix-matched, so a request to /.well-known/oauth-protected-resource/extra would otherwise reach the handler and be served.
  • src/index.ts — one call to registerWellKnownHandlers(server, () => OAuthResource.mcpConfig, logger) after updateConfiguration(). Routes mounted once at boot.
  • src/lib/mcp/wellKnown.ts:resolveIssuer — comment explicitly flags that the request-derived issuer trusts the Host header and MUST be pinned via mcp.issuer in production once Stage 4 signs tokens with iss. Also surfaced in docs/configuration.md with strong wording.
  • AS metadata advertising: code_challenge_methods_supported: ["S256"] (S256 only, no plain), id_token_signing_alg_values_supported: ["RS256", "EdDSA"] (both, per the Stage 4 plan), resource_parameter_supported: true (RFC 8707), token_endpoint_auth_methods_supported includes none for public clients.

What was reviewed

Gemini CLI 0.42.0 reviewed the diff. Six items surfaced; two acted on before push:

  1. CORS headers missing — added Access-Control-Allow-Origin: * + Access-Control-Allow-Methods: GET, OPTIONS to all three documents. Browser-based MCP clients can now fetch cross-origin. Test coverage added.
  2. Host-header spoofing of issuer — added security note inline at resolveIssuer and a "STRONGLY recommended in production" notice in docs/configuration.md. Stage 4 will need this pinned to prevent attacker-influenced iss claims on signed tokens.

Three dismissed: localhost scheme default (Harper's request.protocol is socket-derived and reliable in production), JWKS algorithm-advertise vs empty content mismatch (acknowledged by Gemini as acceptable; Stage 4 populates), local HarperRequest interface redundancy (the type is narrow and accurate for this surface — unifying with the session-bearing Request would actively reduce clarity).

Test plan

  • 552 unit tests pass locally; +25 from this PR covering URI resolution, each document's required fields, CORS headers, handler-registration shape, fall-through paths, and exact-path matching
  • Lint clean
  • Format clean
  • End-to-end verification deferred to Stage 7 — mcp-remote or Claude Desktop hitting the discovery chain is the conformance proof

Out of scope

  • /oauth/mcp/authorize — Stage 3
  • /oauth/mcp/token + JWT signing — Stage 4
  • withMCPAuth wrapper that issues the 401 + WWW-Authenticate pointing at PRM — Stage 5
  • Populated JWKS — Stage 4 generates and persists the signing keypair in harper_oauth_mcp_keys

🤖 Generated with Claude Code

Stage 2 of MCP OAuth support (issue #86). Mounts three discovery
documents under /.well-known/* so MCP clients (Claude Desktop, Cursor,
mcp-remote) can find the authorization server, the supported methods,
and the public keys to verify tokens against.

- /.well-known/oauth-protected-resource — RFC 9728 PRM document. Becomes
  the entry point once Stage 5's withMCPAuth issues 401 + WWW-Authenticate
  pointing here.
- /.well-known/oauth-authorization-server — RFC 8414 AS metadata.
  Advertises authorize / token / register / JWKS endpoints, S256-only
  PKCE, code-only response type, both client secret and public-client
  auth methods, RS256 + EdDSA signing algs, RFC 8707 resource parameter
  support.
- /.well-known/jwks.json — placeholder returning an empty key set until
  Stage 4 generates and persists the signing keypair in the
  harper_oauth_mcp_keys table.

All three documents include Access-Control-Allow-Origin: * so browser-
based MCP clients can fetch them cross-origin (Gemini's blocker on the
draft of this PR — adding it before push).

Two new MCP config fields: mcp.issuer (authorization-server identity)
and mcp.resource (canonical resource URI for RFC 8707 audience binding).
Both default to derive-from-request, but the issuer derivation trusts
the Host header and so MUST be pinned in production once Stage 4 starts
signing tokens with `iss` — documented in docs/configuration.md.

Handlers registered via Harper's server.http(handler, { urlPath })
middleware. urlPath matching is prefix-based with segment-boundary
awareness, so each handler additionally exact-checks request.pathname
to reject sub-paths. Config is read at request time through a getter so
options changes apply without re-registering routes (Harper's server.http
has no deregistration mechanism).

552 unit tests pass locally (+25 from this PR).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@heskew heskew requested a review from kriszyp May 21, 2026 14:46
@github-actions
Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@claude
Copy link
Copy Markdown

claude Bot commented May 21, 2026

Reviewed; no blockers found.

Copy link
Copy Markdown
Member Author

@heskew heskew left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Deep Review of OAuth PR #90: MCP Discovery Endpoints

I have conducted a thorough review of the changes in PR #90 (mcp/well-known-metadata), which implements Stage 2 of the MCP OAuth integration (RFC 8414, RFC 9728, and JWKS placeholders). The implementation is exceptionally clean, robust, and highly spec-compliant.

Here is a breakdown of the key strengths and design decisions in this PR:


1. ⚡ Dynamic Config & Hot-Reload Support

  • Design Decision: The use of a getter function () => OAuthResource.mcpConfig when registering middleware handlers is a brilliant design pattern.
  • Why it matters: Harper's server.http router does not support endpoint deregistration. Passing a live config-resolver closure ensures that configuration updates (such as changing the mcp configuration block or toggling enabled) take effect instantly at request time without duplicate or stale route registrations.

2. 🛡️ Defensive Prefix-Routing Guard

  • Design Decision: In makeHandler(), checking req.pathname !== match.exactPath before serving metadata is an excellent defensive measure.
  • Why it matters: Harper's urlPath router matches prefixes at segment boundaries. Without this exact-path check, sub-paths like /.well-known/oauth-protected-resource/extra would have erroneously matched and been served. This guard ensures they fall through correctly (e.g., to a 404).

3. 🌐 Complete CORS Support

  • Design Decision: Adding Access-Control-Allow-Origin: * and Access-Control-Allow-Methods: GET, OPTIONS to all discovery documents.
  • Why it matters: This ensures browser-based MCP clients (and browser extension developer tools) can successfully fetch discovery metadata without origin restrictions. Since these documents contain entirely public metadata and do not require user credentials, a wildcard CORS origin is secure and fully appropriate here.

4. 🔒 Proactive Security Guarding (Issuer Resolution)

  • Design Decision: Resolving the issuer dynamically using the request's Host and protocol headers with clear security warnings.
  • Why it matters: Dynamic resolution is excellent for development (e.g., local development on localhost), but in production, trusting the client-controlled Host header can lead to host-header spoofing. You have explicitly warned about this risk in resolveIssuer and added prominent, strong guidance in docs/configuration.md to pin mcp.issuer in production. This is highly responsible engineering.

5. 📖 Spec-Compliant Metadata Advertising

  • RFC 8414 Compliance: Correctly advertising code_challenge_methods_supported: ["S256"] enforces secure PKCE (no insecure plain method allowed).
  • RFC 8707 Compliance: Correctly advertising resource_parameter_supported: true flags downstream support for resource indicators on token requests.
  • JWKS Placeholder: The empty JWKS placeholder { keys: [] } is perfectly aligned with the phased approach (to be populated in Stage 4 once signing keys are implemented).

6. 🧪 Comprehensive Unit Tests

  • The 25 newly added tests in test/lib/mcp/wellKnown.test.js provide robust coverage. They thoroughly test fall-throughs, path-prefix matching, localhost fallbacks, and CORS presence across all well-known handlers.

🏁 Verdict

This PR is Approved / LGTM! The code quality is top-tier, and the architecture is well-prepared for the subsequent stages (Stage 3 authorize and Stage 4 token endpoints).


🤖 Posted by Antigravity on Nathan's behalf

@heskew heskew merged commit fa214cf into main May 21, 2026
10 checks passed
@heskew heskew deleted the mcp/well-known-metadata branch May 21, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP OAuth Stage 2: well-known PRM, AS metadata, JWKS endpoints

1 participant