Merge pull request #36482 from irosh-colombage-ZocDoc2/fix/mcp-oauth-scoped-issuer

fix(mcp): scope authorization server issuer
This commit is contained in:
Mateo Wang 2026-08-17 14:27:38 -07:00 • committed by GitHub
commit c1fc5983ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 49 additions and 12 deletions

View file

@ -2416,7 +2416,7 @@ def _build_oauth_authorization_server_response(
_raise_unless_oauth2_discovery_server(mcp_server, mcp_server_name, "not an OAuth authorization server")
return {
"issuer": request_base_url, # point to your proxy
"issuer": f"{request_base_url}/{mcp_server_name}" if mcp_server_name else request_base_url,
"authorization_endpoint": authorization_endpoint,
"token_endpoint": token_endpoint,
"response_types_supported": ["code"],
@ -2464,7 +2464,14 @@ async def oauth_authorization_server_mcp(request: Request, mcp_server_name: str
# Alias for standard OpenID discovery
@router.get("/.well-known/openid-configuration")
async def openid_configuration(request: Request):
response = await oauth_authorization_server_mcp(request)
response: Final = await oauth_authorization_server_mcp(request)
if not isinstance(response, dict):
return response
request_base_url: Final = get_request_base_url(request)
# OIDC verifiers derive this URL from their configured issuer (the proxy origin),
# so keep the origin issuer here even when root resolution scoped the metadata.
unscoped_response: Final = {**response, "issuer": request_base_url}
# If MCPJWTSigner is active, augment the discovery doc with JWKS fields so
# MCP servers and gateways (e.g. AWS Bedrock AgentCore Gateway) can resolve
@ -2476,17 +2483,15 @@ async def openid_configuration(request: Request):
signer: Final = get_mcp_jwt_signer()
if signer is not None:
request_base_url: Final = get_request_base_url(request)
if isinstance(response, dict):
response = {
**response,
"jwks_uri": f"{request_base_url}/.well-known/jwks.json",
"id_token_signing_alg_values_supported": ["RS256"],
}
return {
**unscoped_response,
"jwks_uri": f"{request_base_url}/.well-known/jwks.json",
"id_token_signing_alg_values_supported": ["RS256"],
}
except ImportError:
pass
return response
return unscoped_response
@router.get("/.well-known/jwks.json")

View file

@ -8163,8 +8163,40 @@ async def test_bare_origin_discovery_resolves_single_server_not_aggregate():
)
# per-server, not aggregate: the single server's name is in the endpoints
assert "/test_oauth/authorize" in authorization_response["authorization_endpoint"]
assert authorization_response["issuer"] == "https://llm.example.com"
assert resource_response["authorization_servers"] == ["https://llm.example.com/test_oauth"]
expected_issuer = "https://llm.example.com/test_oauth"
assert authorization_response["issuer"] == expected_issuer
assert resource_response["authorization_servers"] == [expected_issuer]
finally:
global_mcp_server_manager.registry.clear()
@pytest.mark.asyncio
async def test_openid_configuration_alias_keeps_origin_issuer_on_root_resolution():
"""OIDC verifiers derive /.well-known/openid-configuration from their configured issuer
(the proxy origin), so the alias must keep the origin issuer even when single-server
root resolution scopes the underlying authorization-server metadata."""
from fastapi import Request
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
openid_configuration,
)
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
global_mcp_server_manager.registry.clear()
oauth2_server = _create_oauth2_server()
global_mcp_server_manager.registry[oauth2_server.server_id] = oauth2_server
mock_request = MagicMock(spec=Request)
mock_request.base_url = "https://llm.example.com/"
mock_request.headers = {}
try:
response = await openid_configuration(mock_request)
assert isinstance(response, dict)
assert response["issuer"] == "https://llm.example.com"
assert "/test_oauth/authorize" in response["authorization_endpoint"]
finally:
global_mcp_server_manager.registry.clear()