From b1ba9897a40d185f0325767ca11c4c76697d73e2 Mon Sep 17 00:00:00 2001 From: ShresthSamyak Date: Fri, 24 Jul 2026 00:37:15 +0530 Subject: [PATCH 1/3] fix: route MCP calls to the .mtls.googleapis.com endpoint for Agent Identity MCPSessionManager negotiates an mTLS transport for Google MCP endpoints, but get_mcp_toolset builds the connection with the endpoint as registered (a standard *.googleapis.com host). A channel-bound (Agent Identity) access token is only accepted on the *.mtls.googleapis.com endpoint, so the call still 401s even though mTLS is configured. Resolve the MCP endpoint through _mtls_utils.effective_googleapis_endpoint when a client certificate is configured, mirroring the A2A fix (#6370) and the Application Integration endpoint resolution in 37ca6fb. No-op for non-Google hosts, already-mTLS hosts, and when GOOGLE_API_USE_MTLS_ENDPOINT=never. Related to #6365. --- .../agent_registry/agent_registry.py | 10 ++++ .../agent_registry/test_agent_registry.py | 50 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/google/adk/integrations/agent_registry/agent_registry.py b/src/google/adk/integrations/agent_registry/agent_registry.py index 0f67b111171..1816d656f44 100644 --- a/src/google/adk/integrations/agent_registry/agent_registry.py +++ b/src/google/adk/integrations/agent_registry/agent_registry.py @@ -39,6 +39,7 @@ from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams from google.adk.tools.mcp_tool.mcp_toolset import McpToolset +from google.adk.utils._mtls_utils import effective_googleapis_endpoint import google.auth from google.auth.transport import mtls from google.auth.transport import requests as requests_auth @@ -422,6 +423,15 @@ def get_mcp_toolset( f"MCP Server endpoint URI not found for: {mcp_server_name}" ) + # Route Google-hosted MCP servers to the mutual-TLS endpoint when a client + # certificate is configured. A channel-bound (Agent Identity) access token + # is only accepted on *.mtls.googleapis.com; presenting it to the standard + # endpoint returns 401 UNAUTHENTICATED even though the mTLS transport is + # negotiated. effective_googleapis_endpoint is a no-op for non-Google hosts, + # already-mTLS hosts, and when GOOGLE_API_USE_MTLS_ENDPOINT=never. + if _use_client_cert_effective(): + endpoint_uri = effective_googleapis_endpoint(endpoint_uri) + if mcp_server_id and not auth_scheme: try: bindings_data = self._make_request("bindings") diff --git a/tests/unittests/integrations/agent_registry/test_agent_registry.py b/tests/unittests/integrations/agent_registry/test_agent_registry.py index 420a2f959c0..a9393eb85ed 100644 --- a/tests/unittests/integrations/agent_registry/test_agent_registry.py +++ b/tests/unittests/integrations/agent_registry/test_agent_registry.py @@ -196,6 +196,56 @@ async def test_get_mcp_toolset_handles_missing_destination_id( # The custom_metadata shouldn't have been added assert tool.custom_metadata is None + def _stub_mcp_server(self, registry, url): + """Configures the registry to return an MCP server whose interface is `url`.""" + mock_response = MagicMock() + mock_response.json.return_value = { + "displayName": "TestPrefix", + "interfaces": [{"url": url, "protocolBinding": "JSONRPC"}], + } + registry._session.get.return_value = mock_response + registry._credentials.token = "token" + registry._credentials.refresh = MagicMock() + + def test_get_mcp_toolset_uses_mtls_endpoint_for_google(self, registry): + self._stub_mcp_server( + registry, "https://us-central1-aiplatform.googleapis.com/mcp" + ) + with patch( + "google.adk.integrations.agent_registry.agent_registry._use_client_cert_effective", + return_value=True, + ): + toolset = registry.get_mcp_toolset("test-mcp-server") + assert ( + toolset.connection_params.url + == "https://us-central1-aiplatform.mtls.googleapis.com/mcp" + ) + + def test_get_mcp_toolset_keeps_plain_endpoint_without_client_cert( + self, registry + ): + self._stub_mcp_server( + registry, "https://us-central1-aiplatform.googleapis.com/mcp" + ) + with patch( + "google.adk.integrations.agent_registry.agent_registry._use_client_cert_effective", + return_value=False, + ): + toolset = registry.get_mcp_toolset("test-mcp-server") + assert ( + toolset.connection_params.url + == "https://us-central1-aiplatform.googleapis.com/mcp" + ) + + def test_get_mcp_toolset_keeps_non_google_endpoint(self, registry): + self._stub_mcp_server(registry, "https://mcp.example.com/mcp") + with patch( + "google.adk.integrations.agent_registry.agent_registry._use_client_cert_effective", + return_value=True, + ): + toolset = registry.get_mcp_toolset("test-mcp-server") + assert toolset.connection_params.url == "https://mcp.example.com/mcp" + def test_init_raises_value_error_if_params_missing(self): with pytest.raises( ValueError, match="project_id and location must be provided" From 582d8a9f666eb1fdedf1e520690ab2bece6a7478 Mon Sep 17 00:00:00 2001 From: ShresthSamyak Date: Fri, 24 Jul 2026 12:21:10 +0530 Subject: [PATCH 2/3] fix: gate MCP mTLS endpoint on cert availability and full policy matrix Address review feedback: get_mcp_toolset selected the mTLS endpoint from the client-cert *use* policy (_use_client_cert_effective) rather than cert *availability* plus GOOGLE_API_USE_MTLS_ENDPOINT, mishandling two cases: - auto + cert-use-enabled but no cert present: rewrote to the mTLS host while MCPSessionManager fell back to a plain client, pointing a plain client at the mTLS-only host; - always + cert-use-disabled: never rewrote, leaving the standard host. Extract the base-URL decision into _should_use_mtls_endpoint(client_cert_source) (reused by _get_agent_registry_base_url so they cannot drift), retain the resolved client_cert_source on the registry, and gate the MCP endpoint rewrite on it: always -> mTLS unconditionally; auto -> mTLS only when a cert is available; never -> original URL. Adds regression tests for the full matrix. --- .../agent_registry/agent_registry.py | 39 ++++++++--- .../agent_registry/test_agent_registry.py | 69 +++++++++++-------- 2 files changed, 69 insertions(+), 39 deletions(-) diff --git a/src/google/adk/integrations/agent_registry/agent_registry.py b/src/google/adk/integrations/agent_registry/agent_registry.py index 1816d656f44..a7dd013f61c 100644 --- a/src/google/adk/integrations/agent_registry/agent_registry.py +++ b/src/google/adk/integrations/agent_registry/agent_registry.py @@ -223,6 +223,9 @@ def __init__( else None ) self._session.configure_mtls_channel(client_cert_source) + # Retained so endpoint selection for MCP/A2A targets can mirror the base-URL + # policy: `auto` uses the mTLS endpoint only when a cert is actually present. + self._client_cert_source = client_cert_source self._base_url = _get_agent_registry_base_url(client_cert_source) def _get_auth_headers(self) -> Dict[str, str]: @@ -423,13 +426,15 @@ def get_mcp_toolset( f"MCP Server endpoint URI not found for: {mcp_server_name}" ) - # Route Google-hosted MCP servers to the mutual-TLS endpoint when a client - # certificate is configured. A channel-bound (Agent Identity) access token - # is only accepted on *.mtls.googleapis.com; presenting it to the standard - # endpoint returns 401 UNAUTHENTICATED even though the mTLS transport is - # negotiated. effective_googleapis_endpoint is a no-op for non-Google hosts, - # already-mTLS hosts, and when GOOGLE_API_USE_MTLS_ENDPOINT=never. - if _use_client_cert_effective(): + # Route Google-hosted MCP servers to the mutual-TLS endpoint following the + # same policy as the registry base URL. A channel-bound (Agent Identity) + # access token is only accepted on *.mtls.googleapis.com; presenting it to + # the standard endpoint returns 401 even though mTLS was negotiated. Gating + # on the resolved client cert (not just the cert-use policy) avoids pointing + # a plain client at the mTLS host when `auto` is set but no cert is present. + # effective_googleapis_endpoint is itself a no-op for non-Google hosts and + # already-mTLS hosts. + if _should_use_mtls_endpoint(self._client_cert_source): endpoint_uri = effective_googleapis_endpoint(endpoint_uri) if mcp_server_id and not auth_scheme: @@ -647,8 +652,15 @@ def _use_client_cert_effective() -> bool: return use_client_cert_str == "true" -def _get_agent_registry_base_url(client_cert_source: Any | None = None) -> str: - """Returns the base URL based on mTLS configuration and cert availability.""" +def _should_use_mtls_endpoint(client_cert_source: Any | None) -> bool: + """Whether Google endpoints should resolve to their .mtls.googleapis.com host. + + Follows the GOOGLE_API_USE_MTLS_ENDPOINT policy (AIP-4114): ``always`` selects + the mTLS endpoint unconditionally, ``auto`` only when a client certificate is + actually available (``client_cert_source is not None``), and ``never`` never. + Using cert *availability* rather than cert-use *policy* avoids pointing a + plain client (mTLS fell back) at an mTLS-only host. + """ use_mtls_endpoint_str = os.getenv( "GOOGLE_API_USE_MTLS_ENDPOINT", _MtlsEndpoint.AUTO.value ).lower() @@ -656,8 +668,13 @@ def _get_agent_registry_base_url(client_cert_source: Any | None = None) -> str: use_mtls_endpoint = _MtlsEndpoint(use_mtls_endpoint_str) except ValueError: use_mtls_endpoint = _MtlsEndpoint.AUTO - if (use_mtls_endpoint is _MtlsEndpoint.ALWAYS) or ( + return (use_mtls_endpoint is _MtlsEndpoint.ALWAYS) or ( use_mtls_endpoint is _MtlsEndpoint.AUTO and client_cert_source is not None - ): + ) + + +def _get_agent_registry_base_url(client_cert_source: Any | None = None) -> str: + """Returns the base URL based on mTLS configuration and cert availability.""" + if _should_use_mtls_endpoint(client_cert_source): return AGENT_REGISTRY_MTLS_BASE_URL return AGENT_REGISTRY_BASE_URL diff --git a/tests/unittests/integrations/agent_registry/test_agent_registry.py b/tests/unittests/integrations/agent_registry/test_agent_registry.py index a9393eb85ed..734db97fa26 100644 --- a/tests/unittests/integrations/agent_registry/test_agent_registry.py +++ b/tests/unittests/integrations/agent_registry/test_agent_registry.py @@ -196,6 +196,11 @@ async def test_get_mcp_toolset_handles_missing_destination_id( # The custom_metadata shouldn't have been added assert tool.custom_metadata is None + _GOOGLE_MCP_URL = "https://us-central1-aiplatform.googleapis.com/mcp" + _GOOGLE_MCP_MTLS_URL = ( + "https://us-central1-aiplatform.mtls.googleapis.com/mcp" + ) + def _stub_mcp_server(self, registry, url): """Configures the registry to return an MCP server whose interface is `url`.""" mock_response = MagicMock() @@ -207,42 +212,50 @@ def _stub_mcp_server(self, registry, url): registry._credentials.token = "token" registry._credentials.refresh = MagicMock() - def test_get_mcp_toolset_uses_mtls_endpoint_for_google(self, registry): - self._stub_mcp_server( - registry, "https://us-central1-aiplatform.googleapis.com/mcp" - ) - with patch( - "google.adk.integrations.agent_registry.agent_registry._use_client_cert_effective", - return_value=True, - ): + def test_get_mcp_toolset_auto_with_cert_uses_mtls_endpoint(self, registry): + # auto + client certificate available -> mTLS endpoint. + self._stub_mcp_server(registry, self._GOOGLE_MCP_URL) + registry._client_cert_source = object() + with patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}): toolset = registry.get_mcp_toolset("test-mcp-server") - assert ( - toolset.connection_params.url - == "https://us-central1-aiplatform.mtls.googleapis.com/mcp" - ) + assert toolset.connection_params.url == self._GOOGLE_MCP_MTLS_URL - def test_get_mcp_toolset_keeps_plain_endpoint_without_client_cert( + def test_get_mcp_toolset_auto_without_cert_keeps_plain_endpoint( self, registry ): - self._stub_mcp_server( - registry, "https://us-central1-aiplatform.googleapis.com/mcp" - ) - with patch( - "google.adk.integrations.agent_registry.agent_registry._use_client_cert_effective", - return_value=False, - ): + # auto + no certificate available -> plain endpoint (avoids pointing a + # plain client, after mTLS fallback, at the mTLS host). + self._stub_mcp_server(registry, self._GOOGLE_MCP_URL) + registry._client_cert_source = None + with patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "auto"}): toolset = registry.get_mcp_toolset("test-mcp-server") - assert ( - toolset.connection_params.url - == "https://us-central1-aiplatform.googleapis.com/mcp" - ) + assert toolset.connection_params.url == self._GOOGLE_MCP_URL + + def test_get_mcp_toolset_always_without_cert_uses_mtls_endpoint( + self, registry + ): + # always -> mTLS endpoint unconditionally, even without a cert. + self._stub_mcp_server(registry, self._GOOGLE_MCP_URL) + registry._client_cert_source = None + with patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): + toolset = registry.get_mcp_toolset("test-mcp-server") + assert toolset.connection_params.url == self._GOOGLE_MCP_MTLS_URL + + def test_get_mcp_toolset_never_keeps_plain_endpoint_with_cert( + self, registry + ): + # never -> plain endpoint even when a cert is available. + self._stub_mcp_server(registry, self._GOOGLE_MCP_URL) + registry._client_cert_source = object() + with patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): + toolset = registry.get_mcp_toolset("test-mcp-server") + assert toolset.connection_params.url == self._GOOGLE_MCP_URL def test_get_mcp_toolset_keeps_non_google_endpoint(self, registry): + # Non-Google hosts are never rewritten, even under always + cert. self._stub_mcp_server(registry, "https://mcp.example.com/mcp") - with patch( - "google.adk.integrations.agent_registry.agent_registry._use_client_cert_effective", - return_value=True, - ): + registry._client_cert_source = object() + with patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): toolset = registry.get_mcp_toolset("test-mcp-server") assert toolset.connection_params.url == "https://mcp.example.com/mcp" From 4b11548554a2942d7fa18763a5da77a86f473974 Mon Sep 17 00:00:00 2001 From: ShresthSamyak Date: Fri, 24 Jul 2026 12:45:16 +0530 Subject: [PATCH 3/3] chore: apply pyink formatting to agent registry mTLS tests Collapse a test signature that fits on one line, per pyink==25.12. --- .../integrations/agent_registry/test_agent_registry.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unittests/integrations/agent_registry/test_agent_registry.py b/tests/unittests/integrations/agent_registry/test_agent_registry.py index 734db97fa26..fd8e261fb9d 100644 --- a/tests/unittests/integrations/agent_registry/test_agent_registry.py +++ b/tests/unittests/integrations/agent_registry/test_agent_registry.py @@ -241,9 +241,7 @@ def test_get_mcp_toolset_always_without_cert_uses_mtls_endpoint( toolset = registry.get_mcp_toolset("test-mcp-server") assert toolset.connection_params.url == self._GOOGLE_MCP_MTLS_URL - def test_get_mcp_toolset_never_keeps_plain_endpoint_with_cert( - self, registry - ): + def test_get_mcp_toolset_never_keeps_plain_endpoint_with_cert(self, registry): # never -> plain endpoint even when a cert is available. self._stub_mcp_server(registry, self._GOOGLE_MCP_URL) registry._client_cert_source = object()