build MCP OAuth discovery metadata from mcp.shared.auth models

Replace hand-built dicts in the well-known oauth-authorization-server and
oauth-protected-resource endpoints (gateway and BYOK flavors) with the SDK's
RFC 8414 OAuthMetadata and RFC 9728 ProtectedResourceMetadata pydantic
models, so field names and URL shapes are validated against the spec instead
of being free-form. Bare-origin URLs now serialize in RFC 3986 normalized
form (trailing slash), matching what SDK-based MCP servers emit.
This commit is contained in:
mateo-berri 2026-06-10 16:27:50 +00:00
parent eb215e37a6
commit 67a8c0f27b
No known key found for this signature in database
3 changed files with 40 additions and 30 deletions

View file

@ -600,28 +600,32 @@ def _build_authorize_html(
@router.get("/.well-known/oauth-authorization-server", include_in_schema=False)
async def oauth_authorization_server_metadata(request: Request) -> JSONResponse:
"""RFC 8414 Authorization Server Metadata for the BYOK OAuth flow."""
from mcp.shared.auth import OAuthMetadata
base_url = get_request_base_url(request)
return JSONResponse(
{
"issuer": base_url,
"authorization_endpoint": f"{base_url}/v1/mcp/oauth/authorize",
"token_endpoint": f"{base_url}/v1/mcp/oauth/token",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"code_challenge_methods_supported": ["S256"],
}
OAuthMetadata(
issuer=base_url, # type: ignore[arg-type]
authorization_endpoint=f"{base_url}/v1/mcp/oauth/authorize", # type: ignore[arg-type]
token_endpoint=f"{base_url}/v1/mcp/oauth/token", # type: ignore[arg-type]
response_types_supported=["code"],
grant_types_supported=["authorization_code"],
code_challenge_methods_supported=["S256"],
).model_dump(mode="json", exclude_none=True)
)
@router.get("/.well-known/oauth-protected-resource", include_in_schema=False)
async def oauth_protected_resource_metadata(request: Request) -> JSONResponse:
"""RFC 9728 Protected Resource Metadata pointing back at this server."""
from mcp.shared.auth import ProtectedResourceMetadata
base_url = get_request_base_url(request)
return JSONResponse(
{
"resource": base_url,
"authorization_servers": [base_url],
}
ProtectedResourceMetadata(
resource=base_url, # type: ignore[arg-type]
authorization_servers=[base_url], # type: ignore[list-item]
).model_dump(mode="json", exclude_none=True)
)

View file

@ -1043,19 +1043,21 @@ async def _build_oauth_protected_resource_response(
),
)
return {
"authorization_servers": [
from mcp.shared.auth import ProtectedResourceMetadata
return ProtectedResourceMetadata(
resource=resource_url, # type: ignore[arg-type]
authorization_servers=[
(
f"{request_base_url}/{mcp_server_name}"
if mcp_server_name
else f"{request_base_url}"
)
],
"resource": resource_url,
"scopes_supported": (
], # type: ignore[list-item]
scopes_supported=(
mcp_server.scopes if mcp_server and mcp_server.scopes else []
),
}
).model_dump(mode="json", exclude_none=True)
# Standard MCP pattern: /.well-known/oauth-protected-resource/mcp/{server_name}
@ -1145,24 +1147,26 @@ def _build_oauth_authorization_server_response(
mcp_server_name, client_ip=client_ip
)
return {
"issuer": request_base_url, # point to your proxy
"authorization_endpoint": authorization_endpoint,
"token_endpoint": token_endpoint,
"response_types_supported": ["code"],
"scopes_supported": (
from mcp.shared.auth import OAuthMetadata
return OAuthMetadata(
issuer=request_base_url, # type: ignore[arg-type]
authorization_endpoint=authorization_endpoint, # type: ignore[arg-type]
token_endpoint=token_endpoint, # type: ignore[arg-type]
response_types_supported=["code"],
scopes_supported=(
mcp_server.scopes if mcp_server and mcp_server.scopes else []
),
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["client_secret_post"],
grant_types_supported=["authorization_code", "refresh_token"],
code_challenge_methods_supported=["S256"],
token_endpoint_auth_methods_supported=["client_secret_post"],
# Claude expects a registration endpoint, even if we just fake it
"registration_endpoint": (
registration_endpoint=( # type: ignore[arg-type]
f"{request_base_url}/{mcp_server_name}/register"
if mcp_server_name
else f"{request_base_url}/register"
),
}
).model_dump(mode="json", exclude_none=True)
# Standard MCP pattern: /.well-known/oauth-authorization-server/mcp/{server_name}

View file

@ -2015,7 +2015,9 @@ async def test_discovery_root_does_not_expose_private_server_for_external_client
assert "/test_oauth/" not in authorization_response["authorization_endpoint"]
assert "/test_oauth/" not in authorization_response["token_endpoint"]
assert authorization_response["scopes_supported"] == []
assert resource_response["authorization_servers"] == ["https://llm.example.com"]
assert resource_response["authorization_servers"] == [
"https://llm.example.com/"
]
assert resource_response["scopes_supported"] == []
finally:
global_mcp_server_manager.registry.clear()