Skip to content

feat(mcp): add .well-known OAuth discovery endpoints - #8263

Open
andypalmi wants to merge 2 commits into
mainfrom
feat/7431-mcp-wellknown-discovery
Open

feat(mcp): add .well-known OAuth discovery endpoints#8263
andypalmi wants to merge 2 commits into
mainfrom
feat/7431-mcp-wellknown-discovery

Conversation

@andypalmi

@andypalmi andypalmi commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the OAuth discovery endpoints an MCP client reads before it can authenticate.

  • GET /.well-known/oauth-authorization-server (RFC 8414): advertises the authorize, token and registration endpoints, the supported grant types (authorization_code, refresh_token), code_challenge_methods_supported: ["S256"], and token_endpoint_auth_methods_supported: ["none"].
  • GET /.well-known/oauth-protected-resource (RFC 9728): advertises the protected resource (/mcp) and its authorization server.

Both endpoints are anonymous and derive their URLs from base_url.

First of a stack of three PRs adding OAuth2 for MCP agents.

Part of #7423. Closes #7431.

@andypalmi andypalmi changed the title feat/7431 mcp wellknown discovery feat(mcp): add .well-known OAuth discovery endpoints Aug 24, 2026
@andypalmi
andypalmi marked this pull request as ready for review August 24, 2026 19:26
@andypalmi
andypalmi requested a review from cstns August 24, 2026 19:29
@andypalmi
andypalmi force-pushed the feat/7431-mcp-wellknown-discovery branch from 842ebdb to 7bcc293 Compare August 24, 2026 19:33
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.11%. Comparing base (0dbcac5) to head (691aa4b).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8263      +/-   ##
==========================================
+ Coverage   76.08%   76.11%   +0.02%     
==========================================
  Files         445      447       +2     
  Lines       23994    24011      +17     
  Branches     6405     6405              
==========================================
+ Hits        18255    18275      +20     
+ Misses       5739     5736       -3     
Flag Coverage Δ
backend 76.11% <100.00%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Add RFC 8414 authorization-server metadata and RFC 9728 protected-resource
metadata under /.well-known so MCP clients can auto-discover the OAuth
endpoints and the MCP resource URL. Public, license-tier independent.

Ref #7431
@cstns
cstns force-pushed the feat/7431-mcp-wellknown-discovery branch from 7bcc293 to f67ce0f Compare August 25, 2026 07:52
@cstns
cstns deployed to staging August 25, 2026 07:55 — with GitHub Actions Active

@cstns cstns left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

oauth-protected-resource won't scale past the one resource

Two things about this one, they're related and I think the same fix sorts out both.

1. One document can only describe one resource. RFC 9728 has no "multiple resources" shape, so the document describes exactly one resource identifier, which
here is hardcoded to ${base_url}/mcp. As soon as there's a second protected resource (per team MCP endpoint, a separate REST resource, whatever) there's nowhere
to put it and the root document has to pick a winner.

2. The document is at the wrong path for the resource it describes. RFC 9728 §3.1 uses path insertion, so metadata for https://host/mcp belongs at
https://host/.well-known/oauth-protected-resource/mcp. The bare root path is reserved for a resource whose identifier is exactly https://host. Right now a
client fetching the root gets back resource: "https://host/mcp", which doesn't match what it asked for, and stricter clients reject that mismatch. Current MCP
clients will probably still work because the spec has them try the path inserted URL first and fall back to the root, so we're leaning on the fallback rather than
on being correct.

Might be worth having each protected resource serve its own document at its own path:

/.well-known/oauth-protected-resource/mcp     # owned by the mcp plugin
/.well-known/oauth-protected-resource         # optional alias, if we want the fallback to keep working

That also happens to fix a third thing: this plugin is registered unconditionally in forge/routes/index.js, but /mcp only exists under EE with an active license
(forge/ee/routes/mcp/index.js:11). On OSS or an unlicensed install we're advertising a resource that 404s. If the document moves into the EE mcp plugin it gets
the right path and the right gating for free. The oauth-authorization-server half can stay where it is, one issuer per deployment, so the root path is correct
there and stays correct.

One more, since it's the bit clients actually rely on: RFC 9728 §5.1 and the MCP spec have the resource answer an unauthenticated request with

401 WWW-Authenticate: Bearer resource_metadata="https://host/.well-known/oauth-protected-resource/mcp"

so the client never has to guess a path at all. I couldn't find WWW-Authenticate anywhere in the stack. Happy for it to land in a later PR, just flagging it so it
doesn't fall through the gaps.

So, roughly

  • Serve the doc at /.well-known/oauth-protected-resource/mcp, keep the root path as an alias if we want the fallback
  • Move it into the EE mcp plugin so it's license gated alongside the resource it describes
  • Have /mcp return 401 + WWW-Authenticate: Bearer resource_metadata="..." (later PR is fine)

Move the RFC 9728 protected-resource document out of the root
.well-known handler into the license-gated EE mcp plugin, so it is only
advertised where the /mcp resource exists. Serve it at the path-inserted
/.well-known/oauth-protected-resource/mcp (RFC 9728 3.1) with the bare
path kept as an alias, and challenge unauthenticated /mcp requests with a
WWW-Authenticate header pointing at that metadata.
@andypalmi

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review, all three make sense and I have pushed a commit addressing them.

  1. Path insertion: the protected resource document is now served at /.well-known/oauth-protected-resource/mcp per RFC 9728 section 3.1, with the bare /.well-known/oauth-protected-resource kept as an alias.

  2. Licensing: I moved the protected resource document out of the root well-known handler into a new plugin under forge/ee/routes/mcp, so it is only advertised where the /mcp resource actually exists. The authorization server metadata stays at the root since the issuer is per deployment.

  3. WWW-Authenticate: I went ahead and added it here rather than deferring. Unauthenticated requests to /mcp now return 401 with WWW-Authenticate: Bearer resource_metadata="..." pointing at the path inserted document (RFC 9728 section 5.1). This meant marking the route anonymous at the auth layer so the handler emits the challenge instead of the generic session check.

Tests cover the new metadata paths, the alias, and the challenge header. Happy to split point 3 into its own PR if you would prefer.

@andypalmi
andypalmi requested a review from cstns August 25, 2026 10:40
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.

.well-known Discovery Endpoints for MCP OAuth

2 participants