Fix OAuth client to preserve full URL path for metadata discovery (#2577)

This commit is contained in:
Jeremiah Lowin 2025-12-09 09:47:07 -05:00 committed by GitHub
commit 413a5a1319
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 56 additions and 9 deletions

View file

@ -4,7 +4,6 @@ import time
import webbrowser
from collections.abc import AsyncGenerator
from typing import Any
from urllib.parse import urlparse
import anyio
import httpx
@ -162,8 +161,8 @@ class OAuth(OAuthClientProvider):
additional_client_metadata: Extra fields for OAuthClientMetadata
callback_port: Fixed port for OAuth callback (default: random available port)
"""
parsed_url = urlparse(mcp_url)
server_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
# Normalize the MCP URL (strip trailing slashes for consistency)
mcp_url = mcp_url.rstrip("/")
# Setup OAuth client
self.httpx_client_factory = httpx_client_factory or httpx.AsyncClient
@ -201,16 +200,17 @@ class OAuth(OAuthClientProvider):
stacklevel=2,
)
# Use full URL for token storage to properly separate tokens per MCP endpoint
self.token_storage_adapter: TokenStorageAdapter = TokenStorageAdapter(
async_key_value=token_storage, server_url=server_base_url
async_key_value=token_storage, server_url=mcp_url
)
# Store server_base_url for use in callback_handler
self.server_base_url = server_base_url
# Store full MCP URL for use in callback_handler display
self.mcp_url = mcp_url
# Initialize parent class
# Initialize parent class with full URL for proper OAuth metadata discovery
super().__init__(
server_url=server_base_url,
server_url=mcp_url,
client_metadata=client_metadata,
storage=self.token_storage_adapter,
redirect_handler=self.redirect_handler,
@ -256,7 +256,7 @@ class OAuth(OAuthClientProvider):
# Create server with result tracking
server: Server = create_oauth_callback_server(
port=self.redirect_port,
server_url=self.server_base_url,
server_url=self.mcp_url,
result_container=result,
result_ready=result_ready,
)

View file

@ -4,6 +4,7 @@ import httpx
import pytest
from fastmcp.client import Client
from fastmcp.client.auth import OAuth
from fastmcp.client.transports import StreamableHttpTransport
from fastmcp.server.auth.auth import ClientRegistrationOptions
from fastmcp.server.auth.providers.in_memory import InMemoryOAuthProvider
@ -124,3 +125,49 @@ async def test_oauth_server_metadata_discovery(streamable_http_server: str):
# The endpoints should be properly formed URLs
assert metadata["authorization_endpoint"].startswith(server_base_url)
assert metadata["token_endpoint"].startswith(server_base_url)
class TestOAuthClientUrlHandling:
"""Tests for OAuth client URL handling (issue #2573)."""
def test_oauth_preserves_full_url_with_path(self):
"""OAuth client should preserve the full MCP URL including path components.
This is critical for servers hosted under path-based endpoints like
mcp.example.com/server1/v1.0/mcp where OAuth metadata discovery needs
the full path to find the correct .well-known endpoints.
"""
mcp_url = "https://mcp.example.com/server1/v1.0/mcp"
oauth = OAuth(mcp_url=mcp_url)
# The full URL should be preserved for OAuth discovery
assert oauth.context.server_url == mcp_url
# The stored mcp_url should match
assert oauth.mcp_url == mcp_url
def test_oauth_preserves_root_url(self):
"""OAuth client should work correctly with root-level URLs."""
mcp_url = "https://mcp.example.com"
oauth = OAuth(mcp_url=mcp_url)
assert oauth.context.server_url == mcp_url
assert oauth.mcp_url == mcp_url
def test_oauth_normalizes_trailing_slash(self):
"""OAuth client should normalize trailing slashes for consistency."""
mcp_url_with_slash = "https://mcp.example.com/api/mcp/"
oauth = OAuth(mcp_url=mcp_url_with_slash)
# Trailing slash should be stripped
expected = "https://mcp.example.com/api/mcp"
assert oauth.context.server_url == expected
assert oauth.mcp_url == expected
def test_oauth_token_storage_uses_full_url(self):
"""Token storage should use the full URL to separate tokens per endpoint."""
mcp_url = "https://mcp.example.com/server1/v1.0/mcp"
oauth = OAuth(mcp_url=mcp_url)
# Token storage should key by the full URL, not just the host
assert oauth.token_storage_adapter._server_url == mcp_url