From b16e6111d39e307e6484f96dfa24f94cd5cb8d2f Mon Sep 17 00:00:00 2001 From: Irosh <15094153+irosh-colombage-ZocDoc2@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:00:43 -0400 Subject: [PATCH 1/3] fix(mcp): scope authorization server issuer Generated with AI Co-Authored-By: Claude Code --- .../proxy/_experimental/mcp_server/discoverable_endpoints.py | 3 ++- .../_experimental/mcp_server/test_discoverable_endpoints.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py index 693e3f8e47d..db4797551a0 100644 --- a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py @@ -2412,7 +2412,8 @@ 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 + # Match the per-server identifier advertised in protected-resource metadata. + "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"], diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py index 9bc84b43fc5..6939fe02bdc 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py @@ -8163,8 +8163,9 @@ 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() From 9288d964019fad52c26a5713508e8e54b95c097f Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 17 Aug 2026 13:08:14 -0700 Subject: [PATCH 2/3] refactor(mcp): drop redundant issuer comment --- litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py | 1 - 1 file changed, 1 deletion(-) diff --git a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py index 1b511e144ed..da6167f1744 100644 --- a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py @@ -2416,7 +2416,6 @@ def _build_oauth_authorization_server_response( _raise_unless_oauth2_discovery_server(mcp_server, mcp_server_name, "not an OAuth authorization server") return { - # Match the per-server identifier advertised in protected-resource metadata. "issuer": f"{request_base_url}/{mcp_server_name}" if mcp_server_name else request_base_url, "authorization_endpoint": authorization_endpoint, "token_endpoint": token_endpoint, From 5ddff616dcba8382a4b5d1692c7552f71586950b Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 17 Aug 2026 13:41:36 -0700 Subject: [PATCH 3/3] fix(mcp): keep origin issuer on the openid-configuration alias --- .../mcp_server/discoverable_endpoints.py | 23 ++++++++------ .../mcp_server/test_discoverable_endpoints.py | 31 +++++++++++++++++++ 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py index da6167f1744..3139b679e5c 100644 --- a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py @@ -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") diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py index 6939fe02bdc..6bfd83b82b8 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py @@ -8170,6 +8170,37 @@ async def test_bare_origin_discovery_resolves_single_server_not_aggregate(): 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() + + def test_gateway_dcr_flow_routing_engages_only_for_llm_dcrc_clients(monkeypatch): """The aggregate DCR arms engage for llm_dcrc_ client_ids (register always mints one, authorize/token route into the aggregate flow); a non-gateway client_id keeps the