mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
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:
parent
eb215e37a6
commit
67a8c0f27b
3 changed files with 40 additions and 30 deletions
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue