Skip to content

Commit 0955ee2

Browse files
committed
fix(api_registry): only attach ADC credentials to Google API hosts
ApiRegistry.get_toolset attaches the runtime's Application Default Credentials to the MCP server URL from the API Registry listing without checking the host, so the runtime's Google credentials can be sent to a non-Google host registered in the catalog. AgentRegistry already gates the same credentials behind a Google-host check (_is_google_api); this brings ApiRegistry in line. Non-Google servers can authenticate via header_provider.
1 parent 065f4ae commit 0955ee2

2 files changed

Lines changed: 61 additions & 7 deletions

File tree

src/google/adk/integrations/api_registry/api_registry.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from typing import Any
1818
from typing import Callable
19+
from urllib.parse import urlparse
1920

2021
from google.adk.agents.readonly_context import ReadonlyContext
2122
from google.adk.tools.base_toolset import ToolPredicate
@@ -28,6 +29,14 @@
2829
API_REGISTRY_URL = "https://cloudapiregistry.googleapis.com"
2930

3031

32+
def _is_google_api(url: str) -> bool:
33+
"""Returns True if the URL points to a Google API host."""
34+
hostname = urlparse(url).hostname
35+
return hostname is not None and (
36+
hostname == "googleapis.com" or hostname.endswith(".googleapis.com")
37+
)
38+
39+
3140
class ApiRegistry:
3241
"""Registry that provides McpToolsets for MCP servers registered in API Registry."""
3342

@@ -110,12 +119,19 @@ def get_toolset(
110119
raise ValueError(f"MCP server {mcp_server_name} has no URLs.")
111120

112121
mcp_server_url = server["urls"][0]
113-
headers = self._get_auth_headers()
114122

115123
# Only prepend "https://" if the URL doesn't already have a scheme
116124
if not mcp_server_url.startswith(("http://", "https://")):
117125
mcp_server_url = "https://" + mcp_server_url
118126

127+
# Only attach the runtime's Application Default Credentials to Google API
128+
# hosts. The server URL comes from the API Registry listing and may point at
129+
# a non-Google host, which must not receive the runtime's Google
130+
# credentials. This mirrors AgentRegistry, which gates the same credentials
131+
# with _is_google_api. Non-Google servers can authenticate via
132+
# header_provider.
133+
headers = self._get_auth_headers() if _is_google_api(mcp_server_url) else {}
134+
119135
return McpToolset(
120136
connection_params=StreamableHTTPConnectionParams(
121137
url=mcp_server_url,

tests/unittests/integrations/api_registry/test_api_registry.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
"name": "test-mcp-server-2",
3434
"urls": ["mcp.server2.com"],
3535
},
36+
{
37+
"name": "test-mcp-server-google",
38+
"urls": ["mcp.googleapis.com"],
39+
},
3640
{
3741
"name": "test-mcp-server-no-url",
3842
},
@@ -79,7 +83,7 @@ def test_init_success(self, MockHttpClient):
7983
api_registry_project_id=self.project_id, location=self.location
8084
)
8185

82-
self.assertEqual(len(api_registry._mcp_servers), 5)
86+
self.assertEqual(len(api_registry._mcp_servers), 6)
8387
self.assertIn("test-mcp-server-1", api_registry._mcp_servers)
8488
self.assertIn("test-mcp-server-2", api_registry._mcp_servers)
8589
self.assertIn("test-mcp-server-no-url", api_registry._mcp_servers)
@@ -107,7 +111,7 @@ def test_init_with_quota_project_id_success(self, MockHttpClient):
107111
api_registry_project_id=self.project_id, location=self.location
108112
)
109113

110-
self.assertEqual(len(api_registry._mcp_servers), 5)
114+
self.assertEqual(len(api_registry._mcp_servers), 6)
111115
self.assertIn("test-mcp-server-1", api_registry._mcp_servers)
112116
self.assertIn("test-mcp-server-2", api_registry._mcp_servers)
113117
self.assertIn("test-mcp-server-no-url", api_registry._mcp_servers)
@@ -237,9 +241,43 @@ async def test_get_toolset_success(self, MockHttpClient, MockMcpToolset):
237241

238242
toolset = api_registry.get_toolset("test-mcp-server-1")
239243

244+
# A non-Google host must not receive the runtime's ADC credentials.
240245
MockMcpToolset.assert_called_once_with(
241246
connection_params=StreamableHTTPConnectionParams(
242247
url="https://mcp.server1.com",
248+
headers={},
249+
),
250+
tool_filter=None,
251+
tool_name_prefix=None,
252+
header_provider=None,
253+
)
254+
self.assertEqual(toolset, MockMcpToolset.return_value)
255+
256+
@patch(
257+
"google.adk.integrations.api_registry.api_registry.McpToolset",
258+
autospec=True,
259+
)
260+
@patch("httpx.Client", autospec=True)
261+
async def test_get_toolset_google_host_includes_credentials(
262+
self, MockHttpClient, MockMcpToolset
263+
):
264+
mock_response = MagicMock()
265+
mock_response.raise_for_status = MagicMock()
266+
mock_response.json = MagicMock(return_value=MOCK_MCP_SERVERS_LIST)
267+
mock_client_instance = MockHttpClient.return_value
268+
mock_client_instance.__enter__.return_value = mock_client_instance
269+
mock_client_instance.get.return_value = mock_response
270+
271+
api_registry = ApiRegistry(
272+
api_registry_project_id=self.project_id, location=self.location
273+
)
274+
275+
toolset = api_registry.get_toolset("test-mcp-server-google")
276+
277+
# A Google API host receives the runtime's ADC credentials.
278+
MockMcpToolset.assert_called_once_with(
279+
connection_params=StreamableHTTPConnectionParams(
280+
url="https://mcp.googleapis.com",
243281
headers={"Authorization": "Bearer mock_token"},
244282
),
245283
tool_filter=None,
@@ -267,11 +305,11 @@ async def test_get_toolset_with_quota_project_id_success(
267305
api_registry_project_id=self.project_id, location=self.location
268306
)
269307

270-
toolset = api_registry.get_toolset("test-mcp-server-1")
308+
toolset = api_registry.get_toolset("test-mcp-server-google")
271309

272310
MockMcpToolset.assert_called_once_with(
273311
connection_params=StreamableHTTPConnectionParams(
274-
url="https://mcp.server1.com",
312+
url="https://mcp.googleapis.com",
275313
headers={
276314
"Authorization": "Bearer mock_token",
277315
"x-goog-user-project": "quota-project",
@@ -312,7 +350,7 @@ async def test_get_toolset_with_filter_and_prefix(
312350
MockMcpToolset.assert_called_once_with(
313351
connection_params=StreamableHTTPConnectionParams(
314352
url="https://mcp.server1.com",
315-
headers={"Authorization": "Bearer mock_token"},
353+
headers={},
316354
),
317355
tool_filter=tool_filter,
318356
tool_name_prefix=tool_name_prefix,
@@ -348,7 +386,7 @@ def test_get_toolset_url_scheme(self):
348386
MockMcpToolset.assert_called_once_with(
349387
connection_params=StreamableHTTPConnectionParams(
350388
url=mock_url,
351-
headers={"Authorization": "Bearer mock_token"},
389+
headers={},
352390
),
353391
tool_filter=None,
354392
tool_name_prefix=None,

0 commit comments

Comments
 (0)