Skip to content

OAuth Resource URI Validation Too Strict - Fails When MCP Server Uses Subpath #1119

@hans1512

Description

@hans1512

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

  1. Client connects to MCP server at https://api.example.com/mcp
  2. SDK derives base URL: https://api.example.com
  3. SDK fetches metadata from https://api.example.com/.well-known/oauth-protected-resource
  4. Metadata returns:
    {
      "resource": "https://api.example.com/",
      "authorization_servers": [
        "https://login.microsoftonline.com/{tenant}/v2.0"
      ],
      "bearer_methods_supported": ["header"],
      "scopes_supported": ["mcp:tools"]
    }
  5. SDK validates: metadata.resource must exactly match serverUrl
  6. 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.compath 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:

  1. MCP servers deployed behind reverse proxies/ingress controllers with path-based routing
  2. MCP servers that correctly implement the spec by operating OAuth at base URL level
  3. 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.com or https://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

  1. Is the SDK's strict validation intentional or an oversight?
  2. Should the SDK follow the MCP spec's guidance that OAuth operates at base URL level?
  3. Would accepting either base URL or full path as valid resource values 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions