|
4 | 4 | import pytest |
5 | 5 |
|
6 | 6 | from fastmcp.client import Client |
| 7 | +from fastmcp.client.auth import OAuth |
7 | 8 | from fastmcp.client.transports import StreamableHttpTransport |
8 | 9 | from fastmcp.server.auth.auth import ClientRegistrationOptions |
9 | 10 | from fastmcp.server.auth.providers.in_memory import InMemoryOAuthProvider |
@@ -124,3 +125,49 @@ async def test_oauth_server_metadata_discovery(streamable_http_server: str): |
124 | 125 | # The endpoints should be properly formed URLs |
125 | 126 | assert metadata["authorization_endpoint"].startswith(server_base_url) |
126 | 127 | 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