Skip to content

Commit 413a5a1

Browse files
authored
Fix OAuth client to preserve full URL path for metadata discovery (#2577)
1 parent 515e8e8 commit 413a5a1

File tree

2 files changed

+56
-9
lines changed

2 files changed

+56
-9
lines changed

src/fastmcp/client/auth/oauth.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import webbrowser
55
from collections.abc import AsyncGenerator
66
from typing import Any
7-
from urllib.parse import urlparse
87

98
import anyio
109
import httpx
@@ -162,8 +161,8 @@ def __init__(
162161
additional_client_metadata: Extra fields for OAuthClientMetadata
163162
callback_port: Fixed port for OAuth callback (default: random available port)
164163
"""
165-
parsed_url = urlparse(mcp_url)
166-
server_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
164+
# Normalize the MCP URL (strip trailing slashes for consistency)
165+
mcp_url = mcp_url.rstrip("/")
167166

168167
# Setup OAuth client
169168
self.httpx_client_factory = httpx_client_factory or httpx.AsyncClient
@@ -201,16 +200,17 @@ def __init__(
201200
stacklevel=2,
202201
)
203202

203+
# Use full URL for token storage to properly separate tokens per MCP endpoint
204204
self.token_storage_adapter: TokenStorageAdapter = TokenStorageAdapter(
205-
async_key_value=token_storage, server_url=server_base_url
205+
async_key_value=token_storage, server_url=mcp_url
206206
)
207207

208-
# Store server_base_url for use in callback_handler
209-
self.server_base_url = server_base_url
208+
# Store full MCP URL for use in callback_handler display
209+
self.mcp_url = mcp_url
210210

211-
# Initialize parent class
211+
# Initialize parent class with full URL for proper OAuth metadata discovery
212212
super().__init__(
213-
server_url=server_base_url,
213+
server_url=mcp_url,
214214
client_metadata=client_metadata,
215215
storage=self.token_storage_adapter,
216216
redirect_handler=self.redirect_handler,
@@ -256,7 +256,7 @@ async def callback_handler(self) -> tuple[str, str | None]:
256256
# Create server with result tracking
257257
server: Server = create_oauth_callback_server(
258258
port=self.redirect_port,
259-
server_url=self.server_base_url,
259+
server_url=self.mcp_url,
260260
result_container=result,
261261
result_ready=result_ready,
262262
)

tests/client/auth/test_oauth_client.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55

66
from fastmcp.client import Client
7+
from fastmcp.client.auth import OAuth
78
from fastmcp.client.transports import StreamableHttpTransport
89
from fastmcp.server.auth.auth import ClientRegistrationOptions
910
from fastmcp.server.auth.providers.in_memory import InMemoryOAuthProvider
@@ -124,3 +125,49 @@ async def test_oauth_server_metadata_discovery(streamable_http_server: str):
124125
# The endpoints should be properly formed URLs
125126
assert metadata["authorization_endpoint"].startswith(server_base_url)
126127
assert metadata["token_endpoint"].startswith(server_base_url)
128+
129+
130+
class TestOAuthClientUrlHandling:
131+
"""Tests for OAuth client URL handling (issue #2573)."""
132+
133+
def test_oauth_preserves_full_url_with_path(self):
134+
"""OAuth client should preserve the full MCP URL including path components.
135+
136+
This is critical for servers hosted under path-based endpoints like
137+
mcp.example.com/server1/v1.0/mcp where OAuth metadata discovery needs
138+
the full path to find the correct .well-known endpoints.
139+
"""
140+
mcp_url = "https://mcp.example.com/server1/v1.0/mcp"
141+
oauth = OAuth(mcp_url=mcp_url)
142+
143+
# The full URL should be preserved for OAuth discovery
144+
assert oauth.context.server_url == mcp_url
145+
146+
# The stored mcp_url should match
147+
assert oauth.mcp_url == mcp_url
148+
149+
def test_oauth_preserves_root_url(self):
150+
"""OAuth client should work correctly with root-level URLs."""
151+
mcp_url = "https://mcp.example.com"
152+
oauth = OAuth(mcp_url=mcp_url)
153+
154+
assert oauth.context.server_url == mcp_url
155+
assert oauth.mcp_url == mcp_url
156+
157+
def test_oauth_normalizes_trailing_slash(self):
158+
"""OAuth client should normalize trailing slashes for consistency."""
159+
mcp_url_with_slash = "https://mcp.example.com/api/mcp/"
160+
oauth = OAuth(mcp_url=mcp_url_with_slash)
161+
162+
# Trailing slash should be stripped
163+
expected = "https://mcp.example.com/api/mcp"
164+
assert oauth.context.server_url == expected
165+
assert oauth.mcp_url == expected
166+
167+
def test_oauth_token_storage_uses_full_url(self):
168+
"""Token storage should use the full URL to separate tokens per endpoint."""
169+
mcp_url = "https://mcp.example.com/server1/v1.0/mcp"
170+
oauth = OAuth(mcp_url=mcp_url)
171+
172+
# Token storage should key by the full URL, not just the host
173+
assert oauth.token_storage_adapter._server_url == mcp_url

0 commit comments

Comments
 (0)