Skip to content

Conversation

@sd2k
Copy link
Contributor

@sd2k sd2k commented Nov 19, 2025

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:

  • 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

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • MCP spec compatibility implementation
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring (no functional changes)
  • Performance improvement
  • Tests only (no functional changes)
  • Other (please describe):

Checklist

  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the documentation accordingly

MCP Spec Compliance

  • This PR implements a feature defined in the MCP specification
  • Link to relevant spec section: Authorization server location
  • Implementation follows the specification exactly

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

    • Added RFC 9728 OAuth Protected Resource Metadata discovery support
    • Client now automatically extracts OAuth metadata URLs from server WWW-Authenticate headers
    • Enhanced error handling to propagate resource metadata information
  • Tests

    • Added test coverage for OAuth metadata discovery and error handling scenarios

✏️ Tip: You can customize this high-level summary in your review settings.

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
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

Walkthrough

This 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

Cohort / File(s) Summary
Core OAuth Client API
client/oauth.go
Added public type alias AuthorizationRequiredError for transport errors and helper functions IsAuthorizationRequiredError() and GetResourceMetadataURL() to check error type and extract metadata URLs.
OAuth Client Tests
client/oauth_test.go
Added TestGetResourceMetadataURL and expanded TestIsAuthorizationRequiredError to verify metadata URL extraction across OAuth and non-OAuth error paths and both base/wrapped error types.
OAuth Configuration
client/transport/oauth.go
Added ProtectedResourceMetadataURL field to OAuthConfig to explicitly specify RFC9728 metadata URL; adjusted server discovery to prefer explicit URL over constructed well-known endpoint.
Transport Authorization Error Infrastructure
client/transport/streamable_http.go
Introduced base AuthorizationRequiredError type with ResourceMetadataURL field, extended OAuthAuthorizationRequiredError to embed it, added extractResourceMetadataURL() helper to parse WWW-Authenticate headers, and refactored 401 error handling across SendRequest and SendNotification.
Transport RFC9728 Metadata Handling
client/transport/sse.go
Enhanced Start, SendRequest, and SendNotification paths to extract metadata URLs from WWW-Authenticate headers on 401 responses and propagate via OAuthAuthorizationRequiredError (with OAuth) or AuthorizationRequiredError (without OAuth).
Transport Tests
client/transport/sse_oauth_test.go, client/transport/streamable_http_oauth_test.go
Added TestSSE_OAuthMetadataDiscovery and TestStreamableHTTP_OAuthMetadataDiscovery to verify metadata extraction from 401 responses; added parameterized TestExtractResourceMetadataURL for WWW-Authenticate parsing edge cases.
Documentation and Examples
examples/oauth_client/README.md, examples/oauth_client/main.go
Updated documentation to describe automatic RFC9728 metadata discovery from WWW-Authenticate headers, expanded OAuth flow steps, and added usage example calling GetResourceMetadataURL() on authorization errors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • AuthorizationRequiredError embedding into OAuthAuthorizationRequiredError — verify the embedding pattern maintains backward compatibility and error propagation
  • WWW-Authenticate header parsing for resource_metadata extraction — validate extractResourceMetadataURL() handles quoted values, multiple parameters, and malformed headers correctly
  • Error propagation paths in SendRequest, SendNotification, and SSE transports — ensure metadata URLs are consistently threaded through both OAuth and non-OAuth branches across all three methods

Possibly related issues

  • #411: Implements RFC9728 protected resource metadata discovery and related OAuth error/utility changes directly addressing the same code-level objectives

Possibly related PRs

  • #581: Modifies server metadata discovery in client/transport/oauth.go affecting well-known endpoint preference
  • #296: Earlier OAuth authorization error foundation that this PR extends with base error type and metadata propagation
  • #340: Concurrent OAuth integration changes in same client transport code paths (client/oauth.go, SSE, HTTP transports)

Suggested labels

type: enhancement, area: mcp spec

Suggested reviewers

  • giridhar-murthy-glean
  • ezynda3
  • pottekkat
  • rwjblue-glean

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing RFC9728 OAuth Protected Resource Metadata discovery, which is the primary focus of the changeset.
Description check ✅ Passed The description comprehensively covers the feature implementation, provides context for why it's needed, lists all major changes, explains MCP spec compliance, includes a link to the RFC, and completes the provided template with appropriate selections.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sd2k sd2k marked this pull request as ready for review November 20, 2025 10:34
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 API

The 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 to GetResourceMetadataURL (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 error

The new 401 branches correctly:

  • Extract resource_metadata from WWW-Authenticate via extractResourceMetadataURL.
  • Return OAuthAuthorizationRequiredError (when oauthHandler is present) embedding AuthorizationRequiredError with the discovered ResourceMetadataURL.
  • Fall back to AuthorizationRequiredError for non-OAuth SSE usage.

That matches the intended RFC9728 behavior and is consistent across Start, SendRequest, and SendNotification.

One optional improvement for SendRequest: in the non-success status paths (including this new 401 branch), responses[idKey] remains registered in c.responses, since deleteResponseChan() 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 invoke deleteResponseChan() before returning to avoid leaving stale entries in the map.

Also applies to: 443-461, 580-597


143-159: Replace string comparisons with errors.Is() for sentinel error checking

You correctly identified that SendNotification uses errors.Is(err, ErrOAuthAuthorizationRequired) at line 551, but both Start (line 148) and SendRequest (line 394) still use brittle string comparison:

if err.Error() == "no valid token available, authorization required" {

The sentinel error ErrOAuthAuthorizationRequired is defined in streamable_http.go:238 and is available in sse.go (which imports the errors package). Switching to errors.Is() makes the code robust to wrapped errors, consistent with the SendNotification pattern, and aligns with the coding guidelines mandate to use errors.Is/As with 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 patterns

The codebase has established testify v1.9.0 as the standard testing library across _test.go files (imports found in 30+ test files including server/, mcp/, and client/ packages).

client/oauth_test.go currently uses manual if/t.Errorf() assertions in the new TestGetResourceMetadataURL and TestIsAuthorizationRequiredError tests. Refactor these to use testify/assert and/or testify/require for consistency:

  • Replace if condition { t.Errorf(...) } patterns with assert.Equal(), assert.Empty(), etc.
  • Use require for 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 patterns

The codebase extensively uses testify/assert and testify/require across test files, but client/transport/streamable_http_oauth_test.go uses t.Errorf() throughout. The new TestExtractResourceMetadataURL test 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

📥 Commits

Reviewing files that changed from the base of the PR and between ecc6d8f and 9ce87a0.

📒 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.go
  • examples/oauth_client/main.go
  • client/oauth_test.go
  • client/transport/sse.go
  • client/transport/streamable_http_oauth_test.go
  • client/transport/sse_oauth_test.go
  • client/transport/streamable_http.go
  • client/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.go
  • client/transport/streamable_http_oauth_test.go
  • client/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 path

The test setup and assertions correctly mirror the SSE Start 401 handling (WWW-Authenticate → ResourceMetadataURL on OAuthAuthorizationRequiredError), 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 handling

The server fixture, 401 response, and assertion against OAuthAuthorizationRequiredError.ResourceMetadataURL line up with the new SendRequest 401 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 RFC9728

Defining ErrAuthorizationRequired, AuthorizationRequiredError (with ResourceMetadataURL), and the small extractResourceMetadataURL helper gives a clear, reusable surface for 401 handling. The Error/Unwrap methods follow the sentinel-error pattern, so callers can use both errors.Is(err, ErrAuthorizationRequired) and higher-level helpers like IsAuthorizationRequiredError/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 consistent

For both SendRequest and SendNotification, the new 401 branches:

  • Extract resource_metadata from WWW-Authenticate.
  • Return OAuthAuthorizationRequiredError with populated ResourceMetadataURL when oauthHandler is set.
  • Otherwise, return AuthorizationRequiredError with the same URL.

Additionally, sendHTTP now short-circuits when GetAuthorizationHeader returns ErrOAuthAuthorizationRequired by constructing an OAuthAuthorizationRequiredError (with empty metadata, since there’s no response yet). This matches how higher layers detect and drive the OAuth flow while still allowing GetResourceMetadataURL to 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 helper

Using client.GetResourceMetadataURL(err) inside the IsOAuthAuthorizationRequiredError branch is the right way to expose the RFC9728 resource_metadata URL 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 correct

Adding ProtectedResourceMetadataURL to OAuthConfig with the updated getServerMetadata logic gives a clear precedence:

  1. Use AuthServerMetadataURL when explicitly configured.
  2. Otherwise, if ProtectedResourceMetadataURL is set (e.g., from RFC9728 resource_metadata), use that.
  3. 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 AuthorizationRequiredError at 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.As to check for AuthorizationRequiredError, following Go idioms and maintaining consistency with the existing IsOAuthAuthorizationRequiredError helper on lines 72-76.


87-105: LGTM! Well-implemented metadata URL extraction.

The function correctly extracts the ResourceMetadataURL from authorization errors with proper handling of both error types. The order of checking (OAuth-specific first, then base) is correct since OAuthAuthorizationRequiredError embeds AuthorizationRequiredError. 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.

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.

1 participant