fastmcp/tests/server/auth/test_auth_provider.py
Jeremiah Lowin 4a9f02c87c
Upgrade to MCP 1.17+ with RFC 9728 compliance (#2122)
* 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
2025-10-17 09:29:23 -04:00

105 lines
4.2 KiB
Python

import re
import httpx
import pytest
from pydantic import AnyHttpUrl
from fastmcp import FastMCP
from fastmcp.server.auth import RemoteAuthProvider
from fastmcp.server.auth.providers.jwt import StaticTokenVerifier
class TestAuthProviderBase:
"""Test suite for base AuthProvider behaviors that apply to all auth providers."""
@pytest.fixture
def basic_remote_provider(self):
"""Basic RemoteAuthProvider fixture for testing base AuthProvider behaviors."""
# Create a static token verifier with a test token
tokens = {
"test_token": {
"client_id": "test-client",
"scopes": ["read", "write"],
}
}
token_verifier = StaticTokenVerifier(tokens=tokens)
return RemoteAuthProvider(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl("https://auth.example.com")],
base_url="https://my-server.com",
)
async def test_www_authenticate_header_points_to_base_url(
self, basic_remote_provider
):
"""Test that WWW-Authenticate header points to RFC 9728-compliant metadata URL.
The WWW-Authenticate header includes the resource path per RFC 9728,
so clients can discover where the metadata is actually registered.
"""
mcp = FastMCP("test-server", auth=basic_remote_provider)
# Mount MCP at a non-root path
mcp_http_app = mcp.http_app(path="/api/v1/mcp")
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=mcp_http_app),
base_url="https://my-server.com",
) as client:
# Make unauthorized request to MCP endpoint
response = await client.get("/api/v1/mcp")
assert response.status_code == 401
www_auth = response.headers.get("www-authenticate", "")
assert "resource_metadata=" in www_auth
# Extract the metadata URL from the header
match = re.search(r'resource_metadata="([^"]+)"', www_auth)
assert match is not None
metadata_url = match.group(1)
# The metadata URL includes the resource path per RFC 9728
assert (
metadata_url
== "https://my-server.com/.well-known/oauth-protected-resource/api/v1/mcp"
)
async def test_automatic_resource_url_capture(self, basic_remote_provider):
"""Test that resource URL is automatically captured from MCP path.
This test verifies PR #1682 functionality where the resource URL
should be automatically set based on the MCP endpoint path.
"""
mcp = FastMCP("test-server", auth=basic_remote_provider)
# Mount MCP at a specific path
mcp_http_app = mcp.http_app(path="/mcp")
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=mcp_http_app),
base_url="https://my-server.com",
) as client:
# The .well-known metadata is at a path-aware location per RFC 9728
response = await client.get("/.well-known/oauth-protected-resource/mcp")
assert response.status_code == 200
data = response.json()
# The resource URL should be automatically set to the MCP path
assert data.get("resource") == "https://my-server.com/mcp"
async def test_automatic_resource_url_with_nested_path(self, basic_remote_provider):
"""Test automatic resource URL capture with deeply nested MCP path."""
mcp = FastMCP("test-server", auth=basic_remote_provider)
mcp_http_app = mcp.http_app(path="/api/v2/services/mcp")
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=mcp_http_app),
base_url="https://my-server.com",
) as client:
# The .well-known metadata includes the resource path per RFC 9728
response = await client.get(
"/.well-known/oauth-protected-resource/api/v2/services/mcp"
)
assert response.status_code == 200
data = response.json()
# Should automatically capture the nested path
assert data.get("resource") == "https://my-server.com/api/v2/services/mcp"