-
Notifications
You must be signed in to change notification settings - Fork 720
feat: implement RFC9728 OAuth Protected Resource Metadata discovery #637
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: implement RFC9728 OAuth Protected Resource Metadata discovery #637
Conversation
Add support for discovering OAuth authorization server metadata from WWW-Authenticate headers per RFC9728 Section 5.1. The MCP spec indicates that servers should return a 401 Unauthorized response with a WWW-Authenticate header containing the resource_metadata parameter. This parameter is used to discover the OAuth authorization server metadata. This change adds support for this discovery, allowing clients to automatically extract the OAuth metadata URL from the WWW-Authenticate header and use it to discover the OAuth authorization server configuration, rather than relying on it being on the /.well-known path of the base URL, which is not always the case (for example, https://mcp.linear.app/mcp/.well-known/oauth-protected-resource vs https://mcp.honeycomb.io/.well-known/oauth-protected-resource - note the lack of /mcp in one of these, even though both servers expect the /mcp path in the base URL). Changes: - Add AuthorizationRequiredError base error type with ResourceMetadataURL field - Add OAuthAuthorizationRequiredError that embeds AuthorizationRequiredError - Add ProtectedResourceMetadataURL to OAuthConfig for explicit configuration - Extract resource_metadata parameter from WWW-Authenticate headers in both streamable_http and sse transports - Update getServerMetadata() to use ProtectedResourceMetadataURL when provided - Add helper functions: IsAuthorizationRequiredError(), GetResourceMetadataURL() - Add comprehensive tests for metadata URL extraction and usage - Update OAuth example to demonstrate RFC9728 discovery This allows clients to properly discover OAuth endpoints when servers return 401 responses with WWW-Authenticate headers containing resource_metadata URLs, enabling correct OAuth flows without requiring well-known URL assumptions. RFC9728: https://datatracker.ietf.org/doc/html/rfc9728
WalkthroughThis PR introduces RFC9728 protected resource metadata discovery support for OAuth flows. Changes include a new public error type alias and helper functions at the client package level, explicit configuration for metadata URLs in the OAuth transport layer, extraction and propagation of metadata URLs from WWW-Authenticate headers in HTTP/SSE error responses, and corresponding test coverage and documentation updates. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Tip 📝 Customizable high-level summaries are now available in beta!You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.
Example instruction:
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
examples/oauth_client/README.md (1)
7-12: Fix helper name in README example to match exported APIThe RFC9728 section shows:
if metadataURL := client.GetDiscoveredMetadataURL(err); metadataURL != "" { fmt.Printf("Server provided OAuth metadata URL: %s\n", metadataURL) }but the exported helper is
client.GetResourceMetadataURL(err), and the main example file uses that name. Please rename the README snippet toGetResourceMetadataURL(or add a real alias) so documentation matches the actual API.Also applies to: 29-41, 63-76
🧹 Nitpick comments (4)
client/transport/sse.go (2)
166-187: 401 handling and metadata propagation look correct; optionally clean up response channels on errorThe new 401 branches correctly:
- Extract
resource_metadatafromWWW-AuthenticateviaextractResourceMetadataURL.- Return
OAuthAuthorizationRequiredError(whenoauthHandleris present) embeddingAuthorizationRequiredErrorwith the discoveredResourceMetadataURL.- Fall back to
AuthorizationRequiredErrorfor non-OAuth SSE usage.That matches the intended RFC9728 behavior and is consistent across
Start,SendRequest, andSendNotification.One optional improvement for
SendRequest: in the non-success status paths (including this new 401 branch),responses[idKey]remains registered inc.responses, sincedeleteResponseChan()is not called before returning. This was already true for non-401 errors but now applies to 401 as well. If you expect many failed requests, you may want to invokedeleteResponseChan()before returning to avoid leaving stale entries in the map.Also applies to: 443-461, 580-597
143-159: Replace string comparisons witherrors.Is()for sentinel error checkingYou correctly identified that
SendNotificationuseserrors.Is(err, ErrOAuthAuthorizationRequired)at line 551, but bothStart(line 148) andSendRequest(line 394) still use brittle string comparison:if err.Error() == "no valid token available, authorization required" {The sentinel error
ErrOAuthAuthorizationRequiredis defined instreamable_http.go:238and is available insse.go(which imports theerrorspackage). Switching toerrors.Is()makes the code robust to wrapped errors, consistent with theSendNotificationpattern, and aligns with the coding guidelines mandate to useerrors.Is/Aswith sentinel errors.Update lines 148 and 394 to use:
if errors.Is(err, ErrOAuthAuthorizationRequired) {client/oauth_test.go (1)
129-211: Migrate new tests to testify for consistency with codebase testing patternsThe codebase has established testify v1.9.0 as the standard testing library across
_test.gofiles (imports found in 30+ test files includingserver/,mcp/, andclient/packages).
client/oauth_test.gocurrently uses manualif/t.Errorf()assertions in the newTestGetResourceMetadataURLandTestIsAuthorizationRequiredErrortests. Refactor these to usetestify/assertand/ortestify/requirefor consistency:
- Replace
if condition { t.Errorf(...) }patterns withassert.Equal(),assert.Empty(), etc.- Use
requirefor assertions that must pass to continue the test safely.This aligns with the coding guideline and improves readability of test failures across the codebase.
client/transport/streamable_http_oauth_test.go (1)
287-334: Migrate test file to testify for consistency with codebase patternsThe codebase extensively uses
testify/assertandtestify/requireacross test files, butclient/transport/streamable_http_oauth_test.gousest.Errorf()throughout. The newTestExtractResourceMetadataURLtest follows the existing pattern in this file, but to align with the coding guidelines and the established convention across the codebase, you should migrate this test file (or at minimum the new test) to use testify assertions.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
client/oauth.go(2 hunks)client/oauth_test.go(1 hunks)client/transport/oauth.go(2 hunks)client/transport/sse.go(6 hunks)client/transport/sse_oauth_test.go(1 hunks)client/transport/streamable_http.go(4 hunks)client/transport/streamable_http_oauth_test.go(1 hunks)examples/oauth_client/README.md(3 hunks)examples/oauth_client/main.go(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*.go: Order imports: standard library first, then third-party, then local packages (goimports enforces this)
Follow Go naming conventions: exported identifiers in PascalCase; unexported in camelCase; acronyms uppercase (HTTP, JSON, MCP)
Error handling: return sentinel errors, wrap with fmt.Errorf("context: %w", err), and check with errors.Is/As
Prefer explicit types and strongly-typed structs; avoid using any except where protocol flexibility is required (e.g., Arguments any)
All exported types and functions must have GoDoc comments starting with the identifier name; avoid inline comments unless necessary
Functions that are handlers or long-running must accept context.Context as the first parameter
Ensure thread safety for shared state using sync.Mutex and document thread-safety requirements in comments
For JSON: use json struct tags with omitempty for optional fields; use json.RawMessage for flexible/deferred parsing
Files:
client/oauth.goexamples/oauth_client/main.goclient/oauth_test.goclient/transport/sse.goclient/transport/streamable_http_oauth_test.goclient/transport/sse_oauth_test.goclient/transport/streamable_http.goclient/transport/oauth.go
**/*_test.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*_test.go: Testing: use testify/assert and testify/require
Write table-driven tests using a tests := []struct{ name, ... } pattern
Go test files must end with _test.go
Files:
client/oauth_test.goclient/transport/streamable_http_oauth_test.goclient/transport/sse_oauth_test.go
🧠 Learnings (1)
📚 Learning: 2025-10-13T09:35:20.180Z
Learnt from: CR
Repo: mark3labs/mcp-go PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-13T09:35:20.180Z
Learning: Applies to **/*_test.go : Testing: use testify/assert and testify/require
Applied to files:
client/oauth_test.go
🧬 Code graph analysis (7)
client/oauth.go (1)
client/transport/streamable_http.go (2)
AuthorizationRequiredError(266-268)OAuthAuthorizationRequiredError(280-283)
examples/oauth_client/main.go (1)
client/oauth.go (1)
GetResourceMetadataURL(91-105)
client/oauth_test.go (3)
client/oauth.go (6)
OAuthAuthorizationRequiredError(64-64)OAuthConfig(11-11)AuthorizationRequiredError(61-61)GetResourceMetadataURL(91-105)IsAuthorizationRequiredError(67-70)IsOAuthAuthorizationRequiredError(73-76)client/transport/streamable_http.go (2)
OAuthAuthorizationRequiredError(280-283)AuthorizationRequiredError(266-268)client/transport/oauth.go (2)
NewOAuthHandler(157-169)OAuthConfig(21-44)
client/transport/sse.go (2)
client/oauth.go (2)
AuthorizationRequiredError(61-61)OAuthAuthorizationRequiredError(64-64)client/transport/streamable_http.go (2)
AuthorizationRequiredError(266-268)OAuthAuthorizationRequiredError(280-283)
client/transport/streamable_http_oauth_test.go (3)
client/oauth.go (5)
NewMemoryTokenStore(23-23)Token(14-14)OAuthConfig(11-11)TokenStore(17-17)OAuthAuthorizationRequiredError(64-64)client/transport/oauth.go (4)
NewMemoryTokenStore(99-101)Token(69-82)OAuthConfig(21-44)TokenStore(55-66)client/transport/streamable_http.go (3)
NewStreamableHTTP(131-161)WithHTTPOAuth(65-69)OAuthAuthorizationRequiredError(280-283)
client/transport/sse_oauth_test.go (3)
client/transport/oauth.go (4)
NewMemoryTokenStore(99-101)Token(69-82)OAuthConfig(21-44)TokenStore(55-66)client/transport/sse.go (2)
NewSSE(85-112)WithOAuth(77-81)client/transport/streamable_http.go (1)
OAuthAuthorizationRequiredError(280-283)
client/transport/streamable_http.go (2)
client/oauth.go (2)
AuthorizationRequiredError(61-61)OAuthAuthorizationRequiredError(64-64)client/transport/oauth.go (1)
OAuthHandler(144-154)
🔇 Additional comments (9)
client/transport/sse_oauth_test.go (1)
243-304: SSE OAuth metadata discovery test exercises the right 401/error pathThe test setup and assertions correctly mirror the SSE
Start401 handling (WWW-Authenticate →ResourceMetadataURLonOAuthAuthorizationRequiredError), and the use of a valid token plus forced 401 ensures the discovery path is actually hit. No changes needed here.client/transport/streamable_http_oauth_test.go (1)
222-285: Streamable HTTP OAuth metadata discovery test aligns with 401 handlingThe server fixture, 401 response, and assertion against
OAuthAuthorizationRequiredError.ResourceMetadataURLline up with the newSendRequest401 logic and give solid coverage of the RFC9728 path. Looks good.client/transport/streamable_http.go (2)
240-277: AuthorizationRequiredError and metadata helper are well-structured for RFC9728Defining
ErrAuthorizationRequired,AuthorizationRequiredError(withResourceMetadataURL), and the smallextractResourceMetadataURLhelper gives a clear, reusable surface for 401 handling. TheError/Unwrapmethods follow the sentinel-error pattern, so callers can use botherrors.Is(err, ErrAuthorizationRequired)and higher-level helpers likeIsAuthorizationRequiredError/GetResourceMetadataURL.Given the tests in
client/transport/streamable_http_oauth_test.go, this looks solid.
330-349: Streamable HTTP 401 handling and OAuth error propagation look consistentFor both
SendRequestandSendNotification, the new 401 branches:
- Extract
resource_metadatafromWWW-Authenticate.- Return
OAuthAuthorizationRequiredErrorwith populatedResourceMetadataURLwhenoauthHandleris set.- Otherwise, return
AuthorizationRequiredErrorwith the same URL.Additionally,
sendHTTPnow short-circuits whenGetAuthorizationHeaderreturnsErrOAuthAuthorizationRequiredby constructing anOAuthAuthorizationRequiredError(with empty metadata, since there’s no response yet). This matches how higher layers detect and drive the OAuth flow while still allowingGetResourceMetadataURLto succeed when a real 401 response provided metadata.The behavior is coherent across request and notification paths; I don’t see issues here.
Also applies to: 433-445, 618-637
examples/oauth_client/main.go (1)
111-115: Example correctly surfaces discovered metadata URL via helperUsing
client.GetResourceMetadataURL(err)inside theIsOAuthAuthorizationRequiredErrorbranch is the right way to expose the RFC9728resource_metadataURL to users, and the messaging makes the new behavior clear without changing the control flow. No further changes needed here.client/transport/oauth.go (1)
35-38: ProtectedResourceMetadataURL wiring and discovery priority look correctAdding
ProtectedResourceMetadataURLtoOAuthConfigwith the updatedgetServerMetadatalogic gives a clear precedence:
- Use
AuthServerMetadataURLwhen explicitly configured.- Otherwise, if
ProtectedResourceMetadataURLis set (e.g., from RFC9728resource_metadata), use that.- Otherwise, fall back to
baseURL + "/.well-known/oauth-protected-resource".The subsequent fallbacks to authorization-server discovery and default endpoints remain intact. This aligns with the described RFC9728 behavior and should integrate cleanly with the new error-surfaced metadata.
Also applies to: 358-375
client/oauth.go (3)
60-61: LGTM! Clean exposure of transport-level error.The type alias appropriately exposes the
AuthorizationRequiredErrorat the client package boundary, following the same pattern as other convenience types in this file. The GoDoc comment is clear and properly formatted.
66-70: LGTM! Standard error checking helper.The implementation correctly uses
errors.Asto check forAuthorizationRequiredError, following Go idioms and maintaining consistency with the existingIsOAuthAuthorizationRequiredErrorhelper on lines 72-76.
87-105: LGTM! Well-implemented metadata URL extraction.The function correctly extracts the
ResourceMetadataURLfrom authorization errors with proper handling of both error types. The order of checking (OAuth-specific first, then base) is correct sinceOAuthAuthorizationRequiredErrorembedsAuthorizationRequiredError. The comprehensive GoDoc comment clearly explains the purpose and RFC9728 context. The function safely handles nil errors and missing metadata URLs by returning an empty string.
Description
Add support for discovering OAuth authorization server metadata from WWW-Authenticate headers per RFC9728 Section 5.1.
The MCP spec indicates that servers should return a 401 Unauthorized response with a WWW-Authenticate header containing the resource_metadata parameter. This parameter is used to discover the OAuth authorization server metadata.
This change adds support for this discovery, allowing clients to automatically extract the OAuth metadata URL from the WWW-Authenticate header and use it to discover the OAuth authorization server configuration, rather than relying on it being on the /.well-known path of the base URL, which is not always the case (for example,
https://mcp.linear.app/mcp/.well-known/oauth-protected-resource vs https://mcp.honeycomb.io/.well-known/oauth-protected-resource - note the lack of /mcp in one of these, even though both servers expect the /mcp path in the base URL).
Changes:
This allows clients to properly discover OAuth endpoints when servers return 401 responses with WWW-Authenticate headers containing resource_metadata URLs, enabling correct OAuth flows without requiring well-known URL assumptions.
RFC9728: https://datatracker.ietf.org/doc/html/rfc9728
Type of Change
Checklist
MCP Spec Compliance
Additional Information
I'm using this fork locally, will mark as ready-for-review once I'm sure it works correctly.
Summary by CodeRabbit
New Features
Tests
✏️ Tip: You can customize this high-level summary in your review settings.