Enable Protected Resource Metadata to provide resource_name and resou… (#1371)

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
This commit is contained in:
Yann Jouanin 2025-08-10 15:01:12 +02:00 committed by GitHub
commit 456b22ecf4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 74 additions and 5 deletions

View file

@ -7,7 +7,7 @@ dependencies = [
"python-dotenv>=1.1.0",
"exceptiongroup>=1.2.2",
"httpx>=0.28.1",
"mcp>=1.10.0",
"mcp>=1.12.4",
"openapi-pydantic>=0.5.1",
"rich>=13.9.4",
"cyclopts>=3.0.0",

View file

@ -140,6 +140,8 @@ class RemoteAuthProvider(AuthProvider):
token_verifier: TokenVerifier,
authorization_servers: list[AnyHttpUrl],
resource_server_url: AnyHttpUrl | str,
resource_name: str | None = None,
resource_documentation: AnyHttpUrl | None = None,
):
"""Initialize the remote auth provider.
@ -153,6 +155,8 @@ class RemoteAuthProvider(AuthProvider):
super().__init__(resource_server_url=resource_server_url)
self.token_verifier = token_verifier
self.authorization_servers = authorization_servers
self.resource_name = resource_name
self.resource_documentation = resource_documentation
async def verify_token(self, token: str) -> AccessToken | None:
"""Verify token using the configured token verifier."""
@ -171,6 +175,8 @@ class RemoteAuthProvider(AuthProvider):
resource_url=self.resource_server_url,
authorization_servers=self.authorization_servers,
scopes_supported=self.token_verifier.required_scopes,
resource_name=self.resource_name,
resource_documentation=self.resource_documentation,
)

View file

@ -327,3 +327,66 @@ class TestRemoteAuthProviderIntegration:
# The RemoteAuthProvider correctly returns the full MCP endpoint URL
assert data["resource"] == "https://my-server.com/mcp/"
assert data["authorization_servers"] == ["https://accounts.google.com/"]
async def test_resource_name_field(self):
"""Test that RemoteAuthProvider correctly returns the resource_name.
This test confirms that RemoteAuthProvider works correctly and returns
the exact resource_name specified.
"""
token_verifier = SimpleTokenVerifier()
auth_provider = RemoteAuthProvider(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl("https://accounts.google.com")],
resource_server_url="https://my-server.com/mcp/",
resource_name="My Test Resource",
)
mcp = FastMCP("test-server", auth=auth_provider)
mcp_http_app = mcp.http_app()
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=mcp_http_app),
base_url="https://my-server.com",
) as client:
response = await client.get("/.well-known/oauth-protected-resource")
assert response.status_code == 200
data = response.json()
# The RemoteAuthProvider correctly returns the resource_name
assert data["resource_name"] == "My Test Resource"
async def test_resource_documentation_field(self):
"""Test that RemoteAuthProvider correctly returns the resource_documentation.
This test confirms that RemoteAuthProvider works correctly and returns
the exact resource_documentation specified.
"""
token_verifier = SimpleTokenVerifier()
auth_provider = RemoteAuthProvider(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl("https://accounts.google.com")],
resource_server_url="https://my-server.com/mcp/",
resource_documentation=AnyHttpUrl(
"https://doc.my-server.com/resource-docs"
),
)
mcp = FastMCP("test-server", auth=auth_provider)
mcp_http_app = mcp.http_app()
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=mcp_http_app),
base_url="https://my-server.com",
) as client:
response = await client.get("/.well-known/oauth-protected-resource")
assert response.status_code == 200
data = response.json()
# The RemoteAuthProvider correctly returns the resource_documentation
assert (
data["resource_documentation"]
== "https://doc.my-server.com/resource-docs"
)

8
uv.lock generated
View file

@ -574,7 +574,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.10.0" },
{ name = "mcp", specifier = ">=1.12.4" },
{ name = "msgspec", specifier = ">=0.19.0" },
{ name = "openapi-core", specifier = ">=0.19.5" },
{ name = "openapi-pydantic", specifier = ">=0.5.1" },
@ -943,7 +943,7 @@ wheels = [
[[package]]
name = "mcp"
version = "1.12.3"
version = "1.12.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@ -958,9 +958,9 @@ dependencies = [
{ name = "starlette" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4d/19/9955e2df5384ff5dd25d38f8e88aaf89d2d3d9d39f27e7383eaf0b293836/mcp-1.12.3.tar.gz", hash = "sha256:ab2e05f5e5c13e1dc90a4a9ef23ac500a6121362a564447855ef0ab643a99fed", size = 427203, upload-time = "2025-07-31T18:36:36.795Z" }
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" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/8b/0be74e3308a486f1d127f3f6767de5f9f76454c9b4183210c61cc50999b6/mcp-1.12.3-py3-none-any.whl", hash = "sha256:5483345bf39033b858920a5b6348a303acacf45b23936972160ff152107b850e", size = 158810, upload-time = "2025-07-31T18:36:34.915Z" },
{ 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" },
]
[[package]]