Spaces:
Running
Running
Yann Jouanin Jeremiah Lowin commited on
Enable Protected Resource Metadata to provide resource_name and resou… (#1371)
Browse files- pyproject.toml +1 -1
- src/fastmcp/server/auth/auth.py +6 -0
- tests/server/auth/test_remote_auth_provider.py +63 -0
- uv.lock +4 -4
pyproject.toml
CHANGED
|
@@ -7,7 +7,7 @@ dependencies = [
|
|
| 7 |
"python-dotenv>=1.1.0",
|
| 8 |
"exceptiongroup>=1.2.2",
|
| 9 |
"httpx>=0.28.1",
|
| 10 |
-
"mcp>=1.
|
| 11 |
"openapi-pydantic>=0.5.1",
|
| 12 |
"rich>=13.9.4",
|
| 13 |
"cyclopts>=3.0.0",
|
|
|
|
| 7 |
"python-dotenv>=1.1.0",
|
| 8 |
"exceptiongroup>=1.2.2",
|
| 9 |
"httpx>=0.28.1",
|
| 10 |
+
"mcp>=1.12.4",
|
| 11 |
"openapi-pydantic>=0.5.1",
|
| 12 |
"rich>=13.9.4",
|
| 13 |
"cyclopts>=3.0.0",
|
src/fastmcp/server/auth/auth.py
CHANGED
|
@@ -140,6 +140,8 @@ class RemoteAuthProvider(AuthProvider):
|
|
| 140 |
token_verifier: TokenVerifier,
|
| 141 |
authorization_servers: list[AnyHttpUrl],
|
| 142 |
resource_server_url: AnyHttpUrl | str,
|
|
|
|
|
|
|
| 143 |
):
|
| 144 |
"""Initialize the remote auth provider.
|
| 145 |
|
|
@@ -153,6 +155,8 @@ class RemoteAuthProvider(AuthProvider):
|
|
| 153 |
super().__init__(resource_server_url=resource_server_url)
|
| 154 |
self.token_verifier = token_verifier
|
| 155 |
self.authorization_servers = authorization_servers
|
|
|
|
|
|
|
| 156 |
|
| 157 |
async def verify_token(self, token: str) -> AccessToken | None:
|
| 158 |
"""Verify token using the configured token verifier."""
|
|
@@ -171,6 +175,8 @@ class RemoteAuthProvider(AuthProvider):
|
|
| 171 |
resource_url=self.resource_server_url,
|
| 172 |
authorization_servers=self.authorization_servers,
|
| 173 |
scopes_supported=self.token_verifier.required_scopes,
|
|
|
|
|
|
|
| 174 |
)
|
| 175 |
|
| 176 |
|
|
|
|
| 140 |
token_verifier: TokenVerifier,
|
| 141 |
authorization_servers: list[AnyHttpUrl],
|
| 142 |
resource_server_url: AnyHttpUrl | str,
|
| 143 |
+
resource_name: str | None = None,
|
| 144 |
+
resource_documentation: AnyHttpUrl | None = None,
|
| 145 |
):
|
| 146 |
"""Initialize the remote auth provider.
|
| 147 |
|
|
|
|
| 155 |
super().__init__(resource_server_url=resource_server_url)
|
| 156 |
self.token_verifier = token_verifier
|
| 157 |
self.authorization_servers = authorization_servers
|
| 158 |
+
self.resource_name = resource_name
|
| 159 |
+
self.resource_documentation = resource_documentation
|
| 160 |
|
| 161 |
async def verify_token(self, token: str) -> AccessToken | None:
|
| 162 |
"""Verify token using the configured token verifier."""
|
|
|
|
| 175 |
resource_url=self.resource_server_url,
|
| 176 |
authorization_servers=self.authorization_servers,
|
| 177 |
scopes_supported=self.token_verifier.required_scopes,
|
| 178 |
+
resource_name=self.resource_name,
|
| 179 |
+
resource_documentation=self.resource_documentation,
|
| 180 |
)
|
| 181 |
|
| 182 |
|
tests/server/auth/test_remote_auth_provider.py
CHANGED
|
@@ -327,3 +327,66 @@ class TestRemoteAuthProviderIntegration:
|
|
| 327 |
# The RemoteAuthProvider correctly returns the full MCP endpoint URL
|
| 328 |
assert data["resource"] == "https://my-server.com/mcp/"
|
| 329 |
assert data["authorization_servers"] == ["https://accounts.google.com/"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
# The RemoteAuthProvider correctly returns the full MCP endpoint URL
|
| 328 |
assert data["resource"] == "https://my-server.com/mcp/"
|
| 329 |
assert data["authorization_servers"] == ["https://accounts.google.com/"]
|
| 330 |
+
|
| 331 |
+
async def test_resource_name_field(self):
|
| 332 |
+
"""Test that RemoteAuthProvider correctly returns the resource_name.
|
| 333 |
+
|
| 334 |
+
This test confirms that RemoteAuthProvider works correctly and returns
|
| 335 |
+
the exact resource_name specified.
|
| 336 |
+
"""
|
| 337 |
+
token_verifier = SimpleTokenVerifier()
|
| 338 |
+
auth_provider = RemoteAuthProvider(
|
| 339 |
+
token_verifier=token_verifier,
|
| 340 |
+
authorization_servers=[AnyHttpUrl("https://accounts.google.com")],
|
| 341 |
+
resource_server_url="https://my-server.com/mcp/",
|
| 342 |
+
resource_name="My Test Resource",
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
mcp = FastMCP("test-server", auth=auth_provider)
|
| 346 |
+
mcp_http_app = mcp.http_app()
|
| 347 |
+
|
| 348 |
+
async with httpx.AsyncClient(
|
| 349 |
+
transport=httpx.ASGITransport(app=mcp_http_app),
|
| 350 |
+
base_url="https://my-server.com",
|
| 351 |
+
) as client:
|
| 352 |
+
response = await client.get("/.well-known/oauth-protected-resource")
|
| 353 |
+
|
| 354 |
+
assert response.status_code == 200
|
| 355 |
+
data = response.json()
|
| 356 |
+
|
| 357 |
+
# The RemoteAuthProvider correctly returns the resource_name
|
| 358 |
+
assert data["resource_name"] == "My Test Resource"
|
| 359 |
+
|
| 360 |
+
async def test_resource_documentation_field(self):
|
| 361 |
+
"""Test that RemoteAuthProvider correctly returns the resource_documentation.
|
| 362 |
+
|
| 363 |
+
This test confirms that RemoteAuthProvider works correctly and returns
|
| 364 |
+
the exact resource_documentation specified.
|
| 365 |
+
"""
|
| 366 |
+
token_verifier = SimpleTokenVerifier()
|
| 367 |
+
auth_provider = RemoteAuthProvider(
|
| 368 |
+
token_verifier=token_verifier,
|
| 369 |
+
authorization_servers=[AnyHttpUrl("https://accounts.google.com")],
|
| 370 |
+
resource_server_url="https://my-server.com/mcp/",
|
| 371 |
+
resource_documentation=AnyHttpUrl(
|
| 372 |
+
"https://doc.my-server.com/resource-docs"
|
| 373 |
+
),
|
| 374 |
+
)
|
| 375 |
+
|
| 376 |
+
mcp = FastMCP("test-server", auth=auth_provider)
|
| 377 |
+
mcp_http_app = mcp.http_app()
|
| 378 |
+
|
| 379 |
+
async with httpx.AsyncClient(
|
| 380 |
+
transport=httpx.ASGITransport(app=mcp_http_app),
|
| 381 |
+
base_url="https://my-server.com",
|
| 382 |
+
) as client:
|
| 383 |
+
response = await client.get("/.well-known/oauth-protected-resource")
|
| 384 |
+
|
| 385 |
+
assert response.status_code == 200
|
| 386 |
+
data = response.json()
|
| 387 |
+
|
| 388 |
+
# The RemoteAuthProvider correctly returns the resource_documentation
|
| 389 |
+
assert (
|
| 390 |
+
data["resource_documentation"]
|
| 391 |
+
== "https://doc.my-server.com/resource-docs"
|
| 392 |
+
)
|
uv.lock
CHANGED
|
@@ -574,7 +574,7 @@ requires-dist = [
|
|
| 574 |
{ name = "cyclopts", specifier = ">=3.0.0" },
|
| 575 |
{ name = "exceptiongroup", specifier = ">=1.2.2" },
|
| 576 |
{ name = "httpx", specifier = ">=0.28.1" },
|
| 577 |
-
{ name = "mcp", specifier = ">=1.
|
| 578 |
{ name = "msgspec", specifier = ">=0.19.0" },
|
| 579 |
{ name = "openapi-core", specifier = ">=0.19.5" },
|
| 580 |
{ name = "openapi-pydantic", specifier = ">=0.5.1" },
|
|
@@ -943,7 +943,7 @@ wheels = [
|
|
| 943 |
|
| 944 |
[[package]]
|
| 945 |
name = "mcp"
|
| 946 |
-
version = "1.12.
|
| 947 |
source = { registry = "https://pypi.org/simple" }
|
| 948 |
dependencies = [
|
| 949 |
{ name = "anyio" },
|
|
@@ -958,9 +958,9 @@ dependencies = [
|
|
| 958 |
{ name = "starlette" },
|
| 959 |
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
| 960 |
]
|
| 961 |
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
| 962 |
wheels = [
|
| 963 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 964 |
]
|
| 965 |
|
| 966 |
[[package]]
|
|
|
|
| 574 |
{ name = "cyclopts", specifier = ">=3.0.0" },
|
| 575 |
{ name = "exceptiongroup", specifier = ">=1.2.2" },
|
| 576 |
{ name = "httpx", specifier = ">=0.28.1" },
|
| 577 |
+
{ name = "mcp", specifier = ">=1.12.4" },
|
| 578 |
{ name = "msgspec", specifier = ">=0.19.0" },
|
| 579 |
{ name = "openapi-core", specifier = ">=0.19.5" },
|
| 580 |
{ name = "openapi-pydantic", specifier = ">=0.5.1" },
|
|
|
|
| 943 |
|
| 944 |
[[package]]
|
| 945 |
name = "mcp"
|
| 946 |
+
version = "1.12.4"
|
| 947 |
source = { registry = "https://pypi.org/simple" }
|
| 948 |
dependencies = [
|
| 949 |
{ name = "anyio" },
|
|
|
|
| 958 |
{ name = "starlette" },
|
| 959 |
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
| 960 |
]
|
| 961 |
+
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" }
|
| 962 |
wheels = [
|
| 963 |
+
{ 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" },
|
| 964 |
]
|
| 965 |
|
| 966 |
[[package]]
|