-
Notifications
You must be signed in to change notification settings - Fork 589
Description
OAuth Resource URI Validation Too Strict - Fails When MCP Server Uses Subpath
Environment
- SDK Version: v0.5.0-preview.1
- Target Framework: .NET 8.0
- Identity Provider: Azure AD (Microsoft Entra ID)
Description
The SDK's OAuth client throws a validation error when the resource field in OAuth protected resource metadata returns the base URL instead of the full MCP endpoint path, even though this appears to be spec-compliant behavior per MCP specification and RFC 9728.
Error Message
Connection failed: Resource URI in metadata (https://{url}.com/)
does not match the expected URI (https://{url}.com/mcp)
Current Behavior
- Client connects to MCP server at
https://api.example.com/mcp - SDK derives base URL:
https://api.example.com - SDK fetches metadata from
https://api.example.com/.well-known/oauth-protected-resource - Metadata returns:
{ "resource": "https://api.example.com/", "authorization_servers": [ "https://login.microsoftonline.com/{tenant}/v2.0" ], "bearer_methods_supported": ["header"], "scopes_supported": ["mcp:tools"] } - SDK validates:
metadata.resourcemust exactly matchserverUrl - Validation fails because
"https://api.example.com/"≠"https://api.example.com/mcp"
Expected Behavior
The SDK should accept the OAuth metadata when resource field identifies the base URL, as this appears to be the correct behavior per MCP specification.
Specification Analysis
MCP Specification - Authorization Discovery
From the MCP specification:
The authorization base URL MUST be derived by discarding the path component from the MCP server URL
This explicitly states that OAuth operates at the base URL level, not the endpoint path level.
Example from spec:
- MCP Server URL:
https://api.example.com/v1/mcp - Authorization Base URL:
https://api.example.com← path discarded
This suggests the server returning base URL in resource field is correct per MCP spec.
RFC 9728 - OAuth 2.0 Protected Resource Metadata
From RFC 9728:
resource: The resource URI as defined in [RFC8707]. This field uniquely identifies the protected resource.
The standard allows flexibility in what URI identifies the resource - it doesn't mandate exact endpoint path matching. Both base URL and full path are valid representations.
Related Issues
- #643 - Well-known URL path issues behind ingress
- #860 - Inconsistent resource_metadata handling (closed)
- #1052 - Path ignored in metadata URL construction
- SEP-985 - Align with RFC 9728 (merged)
Minimal Reproduction
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
var endpoint = new Uri("https://api.example.com/mcp");
var transport = new HttpClientTransport(new HttpClientTransportOptions
{
Endpoint = endpoint,
OAuth = new ClientOAuthOptions
{
ClientId = "your-client-id",
RedirectUri = new Uri("http://localhost:3000/callback"),
AuthorizationRedirectDelegate = async (authUri, redirectUri, ct) =>
{
// Delegate implementation...
return "authorization-code";
}
}
});
var client = await McpClientFactory.CreateAsync(transport);
// Throws: "Resource URI in metadata (.../) does not match expected URI (.../mcp)"Impact
This strict validation prevents OAuth from working with:
- MCP servers deployed behind reverse proxies/ingress controllers with path-based routing
- MCP servers that correctly implement the spec by operating OAuth at base URL level
- Multi-tenant hosting scenarios where multiple MCP servers share a base domain
Proposed Solutions
Option 1: Relax Validation (Recommended)
Accept metadata when resource field is either:
- Exact match:
https://example.com/mcp - Base URL match:
https://example.comorhttps://example.com/
// Current (strict)
if (metadata.Resource != expectedUri)
throw new Exception($"Resource URI mismatch");
// Proposed (flexible)
var baseUri = new Uri(expectedUri.GetLeftPart(UriPartial.Authority));
if (metadata.Resource != expectedUri &&
metadata.Resource.TrimEnd('/') != baseUri.ToString().TrimEnd('/'))
throw new Exception($"Resource URI mismatch");Option 2: Follow MCP Spec Exactly
Since MCP spec says authorization operates at base URL level, only validate that resource matches the base URL (with path discarded).
// Derive base URL per MCP spec (discard path)
var authBaseUrl = expectedUri.GetLeftPart(UriPartial.Authority);
// Validate against base URL, not full endpoint
if (metadata.Resource.TrimEnd('/') != authBaseUrl.TrimEnd('/'))
throw new Exception($"Resource URI mismatch");Option 3: Make Validation Configurable
Add option to disable or customize resource URI validation:
var transport = new HttpClientTransport(new HttpClientTransportOptions
{
OAuth = new ClientOAuthOptions
{
// ...
ValidateResourceUri = false // or custom validator delegate
}
});Workaround
Currently, the only workaround is to configure the MCP server to return the full endpoint path in the resource field, but this contradicts the MCP spec guidance:
{
"resource": "https://api.example.com/mcp"
}Additional Context
This issue particularly affects Azure AD (Entra ID) OAuth flows where the resource parameter typically represents the application scope at the base URL level, not individual endpoints.
The TypeScript SDK appears to handle this more flexibly based on issue #860, creating an inconsistency between SDK implementations.
Questions
- Is the SDK's strict validation intentional or an oversight?
- Should the SDK follow the MCP spec's guidance that OAuth operates at base URL level?
- Would accepting either base URL or full path as valid
resourcevalues break any existing use cases?
Additional Information:
- Using custom OAuth implementation works fine, suggesting server configuration is correct
- Server OAuth metadata is accessible and properly formatted
- Issue only occurs with SDK's built-in OAuth, not with manual Bearer token auth