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

heskew merged 1 commit into
mainfrom
mcp/well-known-metadata

Conversation

@heskew

@heskew heskew commented May 21, 2026 •

Copy link
Copy Markdown
Contributor

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

claude Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@heskew heskew left a comment

Copy link
Copy Markdown
Contributor Author

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
heskew added a commit that referenced this pull request Aug 26, 2026
…review trigger

Draft PRs skip review until flipped ready (label still opts one in),
mechanical diffs skip pre-run, reasoning effort scales with diff size
(60/high, 1500/xhigh, else max), synchronize runs debounce 120s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S94XethbGXpAb4DRKMD4kt
heskew added a commit that referenced this pull request Aug 26, 2026
…#218)

* ci: adopt ai-review-prompts fleet defaults and new review lenses

Bump every ai-review-prompts pin to 4632c5d: claude-sonnet-5 model,
--effort xhigh, 96-turn review budget (issue-to-pr 100), 30m timeouts
(#89), the sibling-implementations and cannot-fail-test lenses (#88),
and the week-of-08-10/08-17 calibration prompt edits (#85/#87).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S94XethbGXpAb4DRKMD4kt

* ci: adopt ai-review-prompts #90 cost gates (pin be549ad) + ready_for_review trigger

Draft PRs skip review until flipped ready (label still opts one in),
mechanical diffs skip pre-run, reasoning effort scales with diff size
(60/high, 1500/xhigh, else max), synchronize runs debounce 120s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S94XethbGXpAb4DRKMD4kt

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
heskew added a commit that referenced this pull request Aug 26, 2026
* ci(v1.x): sync bot caller workflows with main

claude-code-action validates a PR's caller workflow against the
default branch, so drifted copies on v1.x fail every review on
v1.x-targeted PRs. Content matches the bump-ai-review-prompts-pin
branch (oauth#216) — identical to main once that merges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S94XethbGXpAb4DRKMD4kt

* ci: adopt ai-review-prompts #90 cost gates (pin be549ad) + ready_for_review trigger

Draft PRs skip review until flipped ready (label still opts one in),
mechanical diffs skip pre-run, reasoning effort scales with diff size
(60/high, 1500/xhigh, else max), synchronize runs debounce 120s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S94XethbGXpAb4DRKMD4kt

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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