mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-09-01 20:13:19 +02:00
* Upgrade to MCP 1.17+ with RFC 9728 compliance Updates FastMCP to require MCP 1.17+ and implements RFC 9728-compliant OAuth protected resource metadata URL handling. The key change is that .well-known/oauth-protected-resource endpoints are now registered at path-aware locations. For example, if an MCP server is mounted at /mcp, the metadata endpoint is now at /.well-known/oauth-protected-resource/mcp instead of /.well-known/oauth-protected-resource. This ensures proper OAuth discovery for path-based resource servers and aligns with the MCP SDK's implementation of RFC 9728 §3.1. Changes include: - Update minimum MCP version from 1.12.4 to 1.17.0 - Use build_resource_metadata_url() for RFC 9728 compliance - Configure CI to test with latest package versions (--upgrade) - Update tests for path-aware metadata URLs - Add icons field to Tool model (introduced in MCP 1.17) * Fix RemoteAuthProvider integration tests for RFC 9728 * Fix parameterized test for nested base URL paths
80 lines
2.9 KiB
Python
80 lines
2.9 KiB
Python
import pytest
|
|
from mcp.server.auth.middleware.bearer_auth import RequireAuthMiddleware
|
|
from starlette.routing import Route
|
|
|
|
from fastmcp.server import FastMCP
|
|
from fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair
|
|
from fastmcp.server.http import create_streamable_http_app
|
|
|
|
|
|
class TestStreamableHTTPAppResourceMetadataURL:
|
|
"""Test resource_metadata_url logic in create_streamable_http_app."""
|
|
|
|
@pytest.fixture
|
|
def rsa_key_pair(self) -> RSAKeyPair:
|
|
"""Generate RSA key pair for testing."""
|
|
return RSAKeyPair.generate()
|
|
|
|
@pytest.fixture
|
|
def bearer_auth_provider(self, rsa_key_pair):
|
|
provider = JWTVerifier(
|
|
public_key=rsa_key_pair.public_key,
|
|
issuer="https://issuer",
|
|
audience="https://audience",
|
|
base_url="https://resource.example.com",
|
|
)
|
|
return provider
|
|
|
|
def test_require_auth_middleware_receives_resource_metadata_url(
|
|
self, bearer_auth_provider
|
|
):
|
|
server = FastMCP(name="TestServer")
|
|
|
|
app = create_streamable_http_app(
|
|
server=server,
|
|
streamable_http_path="/mcp",
|
|
auth=bearer_auth_provider,
|
|
)
|
|
|
|
route = next(r for r in app.routes if isinstance(r, Route) and r.path == "/mcp")
|
|
|
|
assert isinstance(route.endpoint, RequireAuthMiddleware)
|
|
# The metadata URL includes the resource path per RFC 9728
|
|
assert (
|
|
str(route.endpoint.resource_metadata_url)
|
|
== "https://resource.example.com/.well-known/oauth-protected-resource/mcp"
|
|
)
|
|
|
|
def test_trailing_slash_handling_in_resource_server_url(self, rsa_key_pair):
|
|
provider = JWTVerifier(
|
|
public_key=rsa_key_pair.public_key,
|
|
issuer="https://issuer",
|
|
audience="https://audience",
|
|
base_url="https://resource.example.com/",
|
|
)
|
|
server = FastMCP(name="TestServer")
|
|
app = create_streamable_http_app(
|
|
server=server,
|
|
streamable_http_path="/mcp",
|
|
auth=provider,
|
|
)
|
|
route = next(r for r in app.routes if isinstance(r, Route) and r.path == "/mcp")
|
|
assert isinstance(route.endpoint, RequireAuthMiddleware)
|
|
# The metadata URL includes the resource path per RFC 9728
|
|
# Trailing slash in base_url is normalized
|
|
assert (
|
|
str(route.endpoint.resource_metadata_url)
|
|
== "https://resource.example.com/.well-known/oauth-protected-resource/mcp"
|
|
)
|
|
|
|
def test_no_auth_provider_mounts_without_require_auth_middleware(
|
|
self, rsa_key_pair
|
|
):
|
|
server = FastMCP(name="TestServer")
|
|
app = create_streamable_http_app(
|
|
server=server,
|
|
streamable_http_path="/mcp",
|
|
auth=None,
|
|
)
|
|
route = next(r for r in app.routes if isinstance(r, Route) and r.path == "/mcp")
|
|
assert not isinstance(route.endpoint, RequireAuthMiddleware)
|