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]]