From 8b0537e295dbe1cff2666f6591a210cac16cfad2 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Thu, 16 Oct 2025 21:19:54 -0400 Subject: [PATCH] Update version badge for azure auth docs --- docs/integrations/asgi.mdx | 62 ++++++ docs/integrations/azure.mdx | 2 +- examples/auth/github_oauth/client.py | 2 +- examples/auth/github_oauth/server.py | 35 +++- pyproject.toml | 2 +- src/fastmcp/server/http.py | 15 +- tests/server/auth/test_auth_provider.py | 10 +- tests/server/auth/test_oauth_mounting.py | 194 ++++++++++++++++++ .../server/auth/test_remote_auth_provider.py | 42 ++-- uv.lock | 10 +- 10 files changed, 335 insertions(+), 39 deletions(-) create mode 100644 tests/server/auth/test_oauth_mounting.py diff --git a/docs/integrations/asgi.mdx b/docs/integrations/asgi.mdx index 66e7e4055..5bb2e41a8 100644 --- a/docs/integrations/asgi.mdx +++ b/docs/integrations/asgi.mdx @@ -152,6 +152,68 @@ app = Starlette( In this setup, the MCP server is accessible at the `/outer/inner/mcp/` path. +## Mounting OAuth-Protected Servers + +When mounting an OAuth-protected FastMCP server under a path prefix, you need to ensure OAuth discovery endpoints are accessible at the root level for RFC 9728 compliance. + +### Understanding `base_url` with Mounting + +The `base_url` parameter represents **where the OAuth endpoints will be publicly accessible**, including any mount path: + +```python +from fastmcp import FastMCP +from fastmcp.server.auth.providers.github import GitHubProvider +from starlette.applications import Starlette +from starlette.routing import Mount + +# Create OAuth provider +auth = GitHubProvider( + client_id="your-client-id", + client_secret="your-client-secret", + base_url="http://localhost:8000/api", # Where OAuth endpoints will be accessible + issuer_url="http://localhost:8000", # Where auth server metadata is located (root level) +) + +mcp = FastMCP("Protected Server", auth=auth) + +# Create MCP app with internal path +mcp_app = mcp.http_app(path="/mcp") + +# Get well-known routes (mounted at root level) +well_known_routes = auth.get_routes(mcp_path="/mcp") + +# Mount everything +app = Starlette( + routes=[ + *well_known_routes, # Well-known discovery routes at root level + Mount("/api", app=mcp_app), # MCP app under /api prefix + ], + lifespan=mcp_app.lifespan, +) +``` + +**Key Parameters:** + +- `base_url`: Where OAuth endpoints (authorize, token, callback) will be accessible. **Includes the mount path.** +- `issuer_url`: Where auth server metadata is located. **Should be root level** when well-known routes are mounted at root. +- `mcp_path`: Internal MCP path that combines with `base_url` to form the resource URL. + +**How it works:** + +1. `base_url="http://localhost:8000/api"` - OAuth endpoints accessible under `/api` +2. `issuer_url="http://localhost:8000"` - Auth server metadata at root level +3. Internal MCP routes created at `/mcp` +4. When mounted under `/api`, they become accessible at `/api/mcp` +5. Well-known routes mounted at root for RFC compliance + +**Result:** +- MCP endpoint: `http://localhost:8000/api/mcp` +- OAuth callback: `http://localhost:8000/api/auth/callback` +- Auth server metadata: `http://localhost:8000/.well-known/oauth-authorization-server` +- Protected resource metadata: `http://localhost:8000/.well-known/oauth-protected-resource/api/mcp` + +For more details on OAuth authentication, see the [Authentication guide](/servers/auth). + ## Custom Middleware diff --git a/docs/integrations/azure.mdx b/docs/integrations/azure.mdx index 2d2595a86..525a674b8 100644 --- a/docs/integrations/azure.mdx +++ b/docs/integrations/azure.mdx @@ -8,7 +8,7 @@ tag: NEW import { VersionBadge } from "/snippets/version-badge.mdx" - + This guide shows you how to secure your FastMCP server using **Azure OAuth** (Microsoft Entra ID). Since Azure doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/servers/auth/oauth-proxy) pattern to bridge Azure's traditional OAuth with MCP's authentication requirements. FastMCP validates Azure JWTs against your application's client_id. diff --git a/examples/auth/github_oauth/client.py b/examples/auth/github_oauth/client.py index 5f1f39bb2..99cc4a42e 100644 --- a/examples/auth/github_oauth/client.py +++ b/examples/auth/github_oauth/client.py @@ -10,7 +10,7 @@ import asyncio from fastmcp.client import Client -SERVER_URL = "http://127.0.0.1:8000/mcp" +SERVER_URL = "http://127.0.0.1:8000/api/mcp" async def main(): diff --git a/examples/auth/github_oauth/server.py b/examples/auth/github_oauth/server.py index 1f88c6977..aa0162dbf 100644 --- a/examples/auth/github_oauth/server.py +++ b/examples/auth/github_oauth/server.py @@ -1,6 +1,7 @@ -"""GitHub OAuth server example for FastMCP. +"""GitHub OAuth server example for FastMCP with mounting. -This example demonstrates how to protect a FastMCP server with GitHub OAuth. +This example demonstrates how to protect a FastMCP server with GitHub OAuth +and mount it under a path prefix in a parent ASGI application. Required environment variables: - FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID: Your GitHub OAuth app client ID @@ -12,13 +13,18 @@ To run: import os +import uvicorn +from starlette.applications import Starlette +from starlette.routing import Mount + from fastmcp import FastMCP from fastmcp.server.auth.providers.github import GitHubProvider auth = GitHubProvider( client_id=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID") or "", client_secret=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET") or "", - base_url="http://localhost:8000", + base_url="http://localhost:8000/api", # Where OAuth endpoints will be accessible + issuer_url="http://localhost:8000", # Where auth server metadata is located (root level) # redirect_path="/auth/callback", # Default path - change if using a different callback URL ) @@ -32,4 +38,25 @@ def echo(message: str) -> str: if __name__ == "__main__": - mcp.run(transport="http", port=8000) + # Create the MCP app with internal path only + mcp_app = mcp.http_app(path="/mcp") + + # Get well-known routes (mounted at root level for RFC compliance) + # Pass the internal MCP path - it combines with base_url internally + well_known_routes = auth.get_routes(mcp_path="/mcp") + + # Create parent app and mount everything + app = Starlette( + routes=[ + *well_known_routes, # Well-known discovery routes at root level + Mount("/api", app=mcp_app), # MCP app mounted under /api prefix + ], + lifespan=mcp_app.lifespan, + ) + + # URLs after mounting: + # - MCP endpoint: http://localhost:8000/api/mcp + # - OAuth callback: http://localhost:8000/api/auth/callback + # - Auth server metadata: http://localhost:8000/.well-known/oauth-authorization-server + # - Protected resource metadata: http://localhost:8000/.well-known/oauth-protected-resource/api/mcp + uvicorn.run(app, host="127.0.0.1", port=8000) diff --git a/pyproject.toml b/pyproject.toml index d81a55dfe..9b1d6abb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ dependencies = [ "python-dotenv>=1.1.0", "exceptiongroup>=1.2.2", "httpx>=0.28.1", - "mcp>=1.12.4,<2.0.0", + "mcp>=1.17.0,<2.0.0", "openapi-pydantic>=0.5.1", "rich>=13.9.4", "cyclopts>=3.0.0", diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index 25264ce05..571535391 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -6,6 +6,7 @@ from contextvars import ContextVar from typing import TYPE_CHECKING from mcp.server.auth.middleware.bearer_auth import RequireAuthMiddleware +from mcp.server.auth.routes import build_resource_metadata_url from mcp.server.lowlevel.server import LifespanResultT from mcp.server.sse import SseServerTransport from mcp.server.streamable_http import EventStore @@ -173,13 +174,17 @@ def create_sse_app( server_middleware.extend(auth_middleware) # Create protected SSE endpoint route with GET method only + # Use RFC 9728 path-scoped well-known URL for WWW-Authenticate header + resource_url = auth._get_resource_url(sse_path) + metadata_url = build_resource_metadata_url(resource_url) if resource_url else None + server_routes.append( Route( sse_path, endpoint=RequireAuthMiddleware( handle_sse, auth.required_scopes, - auth._get_resource_url("/.well-known/oauth-protected-resource"), + metadata_url, ), methods=["GET"], ) @@ -192,7 +197,7 @@ def create_sse_app( app=RequireAuthMiddleware( sse.handle_post_message, auth.required_scopes, - auth._get_resource_url("/.well-known/oauth-protected-resource"), + metadata_url, ), ) ) @@ -295,13 +300,17 @@ def create_streamable_http_app( server_middleware.extend(auth_middleware) # Create protected HTTP endpoint route + # Use RFC 9728 path-scoped well-known URL for WWW-Authenticate header + resource_url = auth._get_resource_url(streamable_http_path) + metadata_url = build_resource_metadata_url(resource_url) if resource_url else None + server_routes.append( Route( streamable_http_path, endpoint=RequireAuthMiddleware( streamable_http_app, auth.required_scopes, - auth._get_resource_url("/.well-known/oauth-protected-resource"), + metadata_url, ), ) ) diff --git a/tests/server/auth/test_auth_provider.py b/tests/server/auth/test_auth_provider.py index d108d5e43..361415e8b 100644 --- a/tests/server/auth/test_auth_provider.py +++ b/tests/server/auth/test_auth_provider.py @@ -57,10 +57,11 @@ class TestAuthProviderBase: assert match is not None metadata_url = match.group(1) - # Should point to base URL, not include /api/v1/mcp + # RFC 9728: Should be path-scoped well-known URL + # Resource is at /api/v1/mcp, so well-known is at /.well-known/oauth-protected-resource/api/v1/mcp assert ( metadata_url - == "https://my-server.com/.well-known/oauth-protected-resource" + == "https://my-server.com/.well-known/oauth-protected-resource/api/v1/mcp" ) async def test_automatic_resource_url_capture(self, basic_remote_provider): @@ -78,7 +79,7 @@ class TestAuthProviderBase: base_url="https://my-server.com", ) as client: # Get the .well-known metadata - response = await client.get("/.well-known/oauth-protected-resource") + response = await client.get("/.well-known/oauth-protected-resource/mcp") assert response.status_code == 200 data = response.json() @@ -94,7 +95,8 @@ class TestAuthProviderBase: transport=httpx.ASGITransport(app=mcp_http_app), base_url="https://my-server.com", ) as client: - response = await client.get("/.well-known/oauth-protected-resource") + # RFC 9728: path-scoped well-known URL + response = await client.get("/.well-known/oauth-protected-resource/api/v2/services/mcp") assert response.status_code == 200 data = response.json() diff --git a/tests/server/auth/test_oauth_mounting.py b/tests/server/auth/test_oauth_mounting.py new file mode 100644 index 000000000..6c698501c --- /dev/null +++ b/tests/server/auth/test_oauth_mounting.py @@ -0,0 +1,194 @@ +"""Tests for OAuth .well-known routes when FastMCP apps are mounted in parent ASGI apps. + +This test file validates the fix for issue #2077 where .well-known/oauth-protected-resource +returns 404 at root level when a FastMCP app is mounted under a path prefix. + +The fix uses MCP SDK 1.17+ which implements RFC 9728 path-scoped well-known URLs. +""" + +import httpx +import pytest +from pydantic import AnyHttpUrl +from starlette.applications import Starlette +from starlette.routing import Mount + +from fastmcp import FastMCP +from fastmcp.server.auth import RemoteAuthProvider +from fastmcp.server.auth.providers.jwt import StaticTokenVerifier + + +@pytest.fixture +def test_tokens(): + """Standard test tokens fixture.""" + return { + "test_token": { + "client_id": "test-client", + "scopes": ["read", "write"], + } + } + + +class TestOAuthMounting: + """Test OAuth .well-known routes with mounted FastMCP apps.""" + + async def test_well_known_with_direct_deployment(self, test_tokens): + """Test that .well-known routes work when app is deployed directly (not mounted). + + This is the baseline - it should work as expected. + Per RFC 9728, if the resource is at /mcp, the well-known endpoint is at + /.well-known/oauth-protected-resource/mcp (path-scoped). + """ + token_verifier = StaticTokenVerifier(tokens=test_tokens) + auth_provider = RemoteAuthProvider( + token_verifier=token_verifier, + authorization_servers=[AnyHttpUrl("https://auth.example.com")], + base_url="https://api.example.com", + ) + + mcp = FastMCP("test-server", auth=auth_provider) + mcp_app = mcp.http_app() + + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=mcp_app), + base_url="https://api.example.com", + ) as client: + # RFC 9728: path-scoped well-known URL + # Resource is at /mcp, so well-known should be at /.well-known/oauth-protected-resource/mcp + response = await client.get("/.well-known/oauth-protected-resource/mcp") + assert response.status_code == 200 + + data = response.json() + assert data["resource"] == "https://api.example.com/mcp" + assert data["authorization_servers"] == ["https://auth.example.com/"] + + async def test_well_known_with_mounted_app(self, test_tokens): + """Test that .well-known routes work when explicitly mounted at root. + + When mounting a FastMCP app under a prefix, users should: + 1. Get the well-known routes from auth provider with the FULL mount path + 2. Mount those routes directly on the parent app (at root level) + 3. Mount the MCP app under the desired prefix + + This ensures RFC 9728 compliance - the well-known endpoint is at root. + """ + token_verifier = StaticTokenVerifier(tokens=test_tokens) + auth_provider = RemoteAuthProvider( + token_verifier=token_verifier, + authorization_servers=[AnyHttpUrl("https://auth.example.com")], + base_url="https://api.example.com", + ) + + mcp = FastMCP("test-server", auth=auth_provider) + mcp_app = mcp.http_app(path="/mcp") + + # Get well-known routes for the FULL mount path (/api/mcp) + # and mount them at root level on the parent app + well_known_routes = auth_provider.get_routes(mcp_path="/api/mcp") + + parent_app = Starlette( + routes=[ + *well_known_routes, # Well-known routes at root level + Mount("/api", app=mcp_app), # MCP app under /api + ], + lifespan=mcp_app.lifespan, + ) + + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=parent_app), + base_url="https://api.example.com", + ) as client: + # The CORRECT RFC 9728 path-scoped well-known URL at root + # Resource is at /api/mcp, so well-known is at /.well-known/oauth-protected-resource/api/mcp + response = await client.get("/.well-known/oauth-protected-resource/api/mcp") + assert response.status_code == 200 + + data = response.json() + assert data["resource"] == "https://api.example.com/api/mcp" + assert data["authorization_servers"] == ["https://auth.example.com/"] + + # There will also be an extra route at /api/.well-known/oauth-protected-resource/mcp + # (from the mounted MCP app), but we don't care about that as long as the correct one exists + + async def test_mcp_endpoint_with_mounted_app(self, test_tokens): + """Test that MCP endpoint works correctly when mounted. + + This confirms the MCP functionality itself works with mounting. + """ + token_verifier = StaticTokenVerifier(tokens=test_tokens) + auth_provider = RemoteAuthProvider( + token_verifier=token_verifier, + authorization_servers=[AnyHttpUrl("https://auth.example.com")], + base_url="https://api.example.com", + ) + + mcp = FastMCP("test-server", auth=auth_provider) + + @mcp.tool + def test_tool(message: str) -> str: + return f"Echo: {message}" + + mcp_app = mcp.http_app(path="/mcp") + + # Mount the MCP app under /api prefix + parent_app = Starlette( + routes=[ + Mount("/api", app=mcp_app), + ], + lifespan=mcp_app.lifespan, + ) + + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=parent_app), + base_url="https://api.example.com", + ) as client: + # The MCP endpoint should work at /api/mcp (mounted correctly) + # This is a basic connectivity test + response = await client.get("/api/mcp") + + # We expect either 200 (if no auth required for GET) or 401 (if auth required) + # The key is that it's NOT 404 + assert response.status_code in [200, 401, 405] + + async def test_nested_mounting(self, test_tokens): + """Test .well-known routes with deeply nested mounts. + + Same pattern as single mount - explicitly mount well-known routes at root. + """ + token_verifier = StaticTokenVerifier(tokens=test_tokens) + auth_provider = RemoteAuthProvider( + token_verifier=token_verifier, + authorization_servers=[AnyHttpUrl("https://auth.example.com")], + base_url="https://api.example.com", + ) + + mcp = FastMCP("test-server", auth=auth_provider) + mcp_app = mcp.http_app(path="/mcp") + + # Get well-known routes for the FULL nested mount path + well_known_routes = auth_provider.get_routes(mcp_path="/outer/inner/mcp") + + # Create nested mounts + inner_app = Starlette( + routes=[Mount("/inner", app=mcp_app)], + ) + outer_app = Starlette( + routes=[ + *well_known_routes, # Well-known routes at root level + Mount("/outer", app=inner_app), + ], + lifespan=mcp_app.lifespan, + ) + + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=outer_app), + base_url="https://api.example.com", + ) as client: + # RFC 9728: path-scoped well-known URL for nested mounting + # Resource is at /outer/inner/mcp, so well-known is at /.well-known/oauth-protected-resource/outer/inner/mcp + response = await client.get( + "/.well-known/oauth-protected-resource/outer/inner/mcp" + ) + assert response.status_code == 200 + + data = response.json() + assert data["resource"] == "https://api.example.com/outer/inner/mcp" diff --git a/tests/server/auth/test_remote_auth_provider.py b/tests/server/auth/test_remote_auth_provider.py index eedb871b3..6a296a336 100644 --- a/tests/server/auth/test_remote_auth_provider.py +++ b/tests/server/auth/test_remote_auth_provider.py @@ -78,6 +78,7 @@ class TestRemoteAuthProvider: assert len(routes) == 1 # Check that the route is the OAuth protected resource metadata endpoint + # When called without mcp_path, it creates route at /.well-known/oauth-protected-resource route = routes[0] assert route.path == "/.well-known/oauth-protected-resource" assert route.methods is not None @@ -99,10 +100,10 @@ class TestRemoteAuthProvider: ) metadata_url = provider._get_resource_url( - "/.well-known/oauth-protected-resource" + "/.well-known/oauth-protected-resource/mcp" ) assert metadata_url == AnyHttpUrl( - "https://api.example.com/.well-known/oauth-protected-resource" + "https://api.example.com/.well-known/oauth-protected-resource/mcp" ) def test_get_resource_url_with_nested_base_url(self): @@ -121,10 +122,10 @@ class TestRemoteAuthProvider: ) metadata_url = provider._get_resource_url( - "/.well-known/oauth-protected-resource" + "/.well-known/oauth-protected-resource/mcp" ) assert metadata_url == AnyHttpUrl( - "https://api.example.com/v1/.well-known/oauth-protected-resource" + "https://api.example.com/v1/.well-known/oauth-protected-resource/mcp" ) def test_get_resource_url_handles_trailing_slash(self): @@ -143,10 +144,10 @@ class TestRemoteAuthProvider: ) metadata_url = provider._get_resource_url( - "/.well-known/oauth-protected-resource" + "/.well-known/oauth-protected-resource/mcp" ) assert metadata_url == AnyHttpUrl( - "https://api.example.com/.well-known/oauth-protected-resource" + "https://api.example.com/.well-known/oauth-protected-resource/mcp" ) @@ -195,7 +196,7 @@ class TestRemoteAuthProviderIntegration: transport=httpx.ASGITransport(app=mcp_http_app), base_url="https://api.example.com", ) as client: - response = await client.get("/.well-known/oauth-protected-resource") + response = await client.get("/.well-known/oauth-protected-resource/mcp") assert response.status_code == 200 async def test_protected_resource_metadata_endpoint_resource_field(self): @@ -209,7 +210,7 @@ class TestRemoteAuthProviderIntegration: transport=httpx.ASGITransport(app=mcp_http_app), base_url="https://api.example.com", ) as client: - response = await client.get("/.well-known/oauth-protected-resource") + response = await client.get("/.well-known/oauth-protected-resource/mcp") data = response.json() # This is the key test - ensure resource field contains the full MCP URL @@ -228,20 +229,20 @@ class TestRemoteAuthProviderIntegration: transport=httpx.ASGITransport(app=mcp_http_app), base_url="https://api.example.com", ) as client: - response = await client.get("/.well-known/oauth-protected-resource") + response = await client.get("/.well-known/oauth-protected-resource/mcp") data = response.json() assert data["authorization_servers"] == ["https://auth.example.com/"] @pytest.mark.parametrize( - "base_url,expected_resource", + "base_url,expected_resource,well_known_path", [ - ("https://api.example.com", "https://api.example.com/mcp"), - ("https://api.example.com/", "https://api.example.com/mcp"), - ("https://api.example.com/v1/", "https://api.example.com/v1/mcp"), + ("https://api.example.com", "https://api.example.com/mcp", "/.well-known/oauth-protected-resource/mcp"), + ("https://api.example.com/", "https://api.example.com/mcp", "/.well-known/oauth-protected-resource/mcp"), + ("https://api.example.com/v1/", "https://api.example.com/v1/mcp", "/.well-known/oauth-protected-resource/v1/mcp"), ], ) - async def test_base_url_configurations(self, base_url: str, expected_resource: str): + async def test_base_url_configurations(self, base_url: str, expected_resource: str, well_known_path: str): """Test different base_url configurations.""" auth_provider = self._create_test_auth_provider(base_url=base_url) mcp = FastMCP("test-server", auth=auth_provider) @@ -251,7 +252,8 @@ class TestRemoteAuthProviderIntegration: transport=httpx.ASGITransport(app=mcp_http_app), base_url="https://test.example.com", ) as client: - response = await client.get("/.well-known/oauth-protected-resource") + # RFC 9728: path-scoped well-known URL based on resource location + response = await client.get(well_known_path) assert response.status_code == 200 data = response.json() @@ -275,7 +277,7 @@ class TestRemoteAuthProviderIntegration: transport=httpx.ASGITransport(app=mcp_http_app), base_url="https://api.example.com", ) as client: - response = await client.get("/.well-known/oauth-protected-resource") + response = await client.get("/.well-known/oauth-protected-resource/mcp") data = response.json() assert data["resource"] == "https://api.example.com/mcp" @@ -298,7 +300,7 @@ class TestRemoteAuthProviderIntegration: transport=httpx.ASGITransport(app=mcp_http_app), base_url="https://api.example.com", ) as client: - response = await client.get("/.well-known/oauth-protected-resource") + response = await client.get("/.well-known/oauth-protected-resource/mcp") data = response.json() assert set(data["authorization_servers"]) == { @@ -382,7 +384,7 @@ class TestRemoteAuthProviderIntegration: transport=httpx.ASGITransport(app=mcp_http_app), base_url="https://my-server.com", ) as client: - response = await client.get("/.well-known/oauth-protected-resource") + response = await client.get("/.well-known/oauth-protected-resource/mcp") assert response.status_code == 200 data = response.json() @@ -418,7 +420,7 @@ class TestRemoteAuthProviderIntegration: transport=httpx.ASGITransport(app=mcp_http_app), base_url="https://my-server.com", ) as client: - response = await client.get("/.well-known/oauth-protected-resource") + response = await client.get("/.well-known/oauth-protected-resource/mcp") assert response.status_code == 200 data = response.json() @@ -455,7 +457,7 @@ class TestRemoteAuthProviderIntegration: transport=httpx.ASGITransport(app=mcp_http_app), base_url="https://my-server.com", ) as client: - response = await client.get("/.well-known/oauth-protected-resource") + response = await client.get("/.well-known/oauth-protected-resource/mcp") assert response.status_code == 200 data = response.json() diff --git a/uv.lock b/uv.lock index 37903520c..a590816a2 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.11'", @@ -599,7 +599,7 @@ requires-dist = [ { name = "cyclopts", specifier = ">=3.0.0" }, { name = "exceptiongroup", specifier = ">=1.2.2" }, { name = "httpx", specifier = ">=0.28.1" }, - { name = "mcp", specifier = ">=1.12.4,<2.0.0" }, + { name = "mcp", specifier = ">=1.17.0,<2.0.0" }, { name = "openai", marker = "extra == 'openai'", specifier = ">=1.102.0" }, { name = "openapi-core", specifier = ">=0.19.5" }, { name = "openapi-pydantic", specifier = ">=0.5.1" }, @@ -1041,7 +1041,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.12.4" +version = "1.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1056,9 +1056,9 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/88/f6cb7e7c260cd4b4ce375f2b1614b33ce401f63af0f49f7141a2e9bf0a45/mcp-1.12.4.tar.gz", hash = "sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5", size = 431148, upload-time = "2025-08-07T20:31:18.082Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/e0/fe34ce16ea2bacce489ab859abd1b47ae28b438c3ef60b9c5eee6c02592f/mcp-1.18.0.tar.gz", hash = "sha256:aa278c44b1efc0a297f53b68df865b988e52dd08182d702019edcf33a8e109f6", size = 482926, upload-time = "2025-10-16T19:19:55.125Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/68/316cbc54b7163fa22571dcf42c9cc46562aae0a021b974e0a8141e897200/mcp-1.12.4-py3-none-any.whl", hash = "sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789", size = 160145, upload-time = "2025-08-07T20:31:15.69Z" }, + { url = "https://files.pythonhosted.org/packages/1b/44/f5970e3e899803823826283a70b6003afd46f28e082544407e24575eccd3/mcp-1.18.0-py3-none-any.whl", hash = "sha256:42f10c270de18e7892fdf9da259029120b1ea23964ff688248c69db9d72b1d0a", size = 168762, upload-time = "2025-10-16T19:19:53.2Z" }, ] [[package]]