mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 05:24:18 +02:00
Fix OAuth client to preserve full URL path for metadata discovery (#2577)
This commit is contained in:
parent
515e8e832c
commit
413a5a1319
2 changed files with 56 additions and 9 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue