Skip to content

Commit ed579c1

Browse files
wukathcopybara-github
authored andcommitted
feat: Add search agents and search MCP severs to agent registry
Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 945244717
1 parent f41bc79 commit ed579c1

4 files changed

Lines changed: 172 additions & 7 deletions

File tree

contributing/samples/integrations/agent_registry_agent/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ The sample uses `AgentRegistry` to:
4444

4545
- List registered agents using `list_agents()`.
4646
- List registered MCP servers using `list_mcp_servers()`.
47+
- Search registered agents using `search_agents(search_string)`.
48+
- Search registered MCP servers using `search_mcp_servers(search_string)`.
4749

4850
It also shows (in comments) how to:
4951

contributing/samples/integrations/agent_registry_agent/agent.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@
4545
for endpoint in endpoints.get("endpoints", []):
4646
print(f"- Endpoint: {endpoint.get('displayName')} ({endpoint.get('name')})")
4747

48+
# Search agents and MCP servers matching a query
49+
print(f"\nSearching agents matching 'Workspace' in {project_id}/{location}...")
50+
matching_agents = registry.search_agents(search_string="Workspace")
51+
for agent in matching_agents.get("agents", []):
52+
print(f"- Found Agent: {agent.get('displayName')} ({agent.get('name')})")
53+
54+
print(
55+
"\nSearching MCP servers matching 'agentregistry' in"
56+
f" {project_id}/{location}..."
57+
)
58+
matching_servers = registry.search_mcp_servers(search_string="agentregistry")
59+
for server in matching_servers.get("mcpServers", []):
60+
print(
61+
f"- Found MCP Server: {server.get('displayName')} ({server.get('name')})"
62+
)
63+
4864
# Example of using a specific agent or MCP server from the registry:
4965
# (Note: These names should be full resource names as returned by list methods)
5066

src/google/adk/integrations/agent_registry/agent_registry.py

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from typing import Callable
2525
from typing import Dict
2626
from typing import List
27+
from typing import Literal
2728
from typing import Mapping
2829
from typing import TypedDict
2930
from urllib.parse import urlparse
@@ -239,9 +240,13 @@ def _get_auth_headers(self) -> Dict[str, str]:
239240
) from e
240241

241242
def _make_request(
242-
self, path: str, params: Dict[str, Any] | None = None
243+
self,
244+
path: str,
245+
method: str = "GET",
246+
params: Dict[str, Any] | None = None,
247+
json_data: Dict[str, Any] | None = None,
243248
) -> Dict[str, Any]:
244-
"""Helper function to make GET requests to the Agent Registry API."""
249+
"""Helper function to make requests to the Agent Registry API."""
245250
if path.startswith("projects/"):
246251
url = f"{self._base_url}/{path}"
247252
else:
@@ -254,7 +259,10 @@ def _make_request(
254259
)
255260
try:
256261
# Using AuthorizedSession for internal API calls to handle mTLS/Auth.
257-
response = self._session.get(url, headers=headers, params=params)
262+
if method == "POST":
263+
response = self._session.post(url, headers=headers, json=json_data)
264+
else:
265+
response = self._session.get(url, headers=headers, params=params)
258266
response.raise_for_status()
259267
return response.json()
260268
except requests.exceptions.HTTPError as e:
@@ -267,12 +275,41 @@ def _make_request(
267275
except Exception as e:
268276
raise RuntimeError(f"API request failed: {e}") from e
269277

278+
def _search(
279+
self,
280+
resource_type: str,
281+
*,
282+
search_string: str | None = None,
283+
search_type: Literal["KEYWORD", "SEMANTIC"] | None = None,
284+
filter_str: str | None = None,
285+
order_by: str | None = None,
286+
page_size: int | None = None,
287+
page_token: str | None = None,
288+
) -> Dict[str, Any]:
289+
"""Helper function to execute search requests."""
290+
json_data: dict[str, Any] = {}
291+
if search_string is not None:
292+
json_data["searchString"] = search_string
293+
if search_type is not None:
294+
json_data["searchType"] = search_type
295+
if filter_str is not None:
296+
json_data["filter"] = filter_str
297+
if order_by is not None:
298+
json_data["orderBy"] = order_by
299+
if page_size is not None:
300+
json_data["pageSize"] = page_size
301+
if page_token is not None:
302+
json_data["pageToken"] = page_token
303+
return self._make_request(
304+
f"{resource_type}:search", method="POST", json_data=json_data
305+
)
306+
270307
def _get_connection_uri(
271308
self,
272309
resource_details: Mapping[str, Any],
273310
protocol_type: _ProtocolType | None = None,
274311
protocol_binding: _compat.TransportProtocol | None = None,
275-
) -> tuple[Any, Any, Any]:
312+
) -> tuple[str | None, str | None, _compat.TransportProtocol | None]:
276313
"""Extracts the first matching URI based on type and binding filters."""
277314
protocols = list(resource_details.get("protocols", []))
278315
if "interfaces" in resource_details:
@@ -318,6 +355,27 @@ def list_mcp_servers(
318355
params["pageToken"] = page_token
319356
return self._make_request("mcpServers", params=params)
320357

358+
def search_mcp_servers(
359+
self,
360+
*,
361+
search_string: str | None = None,
362+
search_type: Literal["KEYWORD", "SEMANTIC"] | None = None,
363+
filter_str: str | None = None,
364+
order_by: str | None = None,
365+
page_size: int | None = None,
366+
page_token: str | None = None,
367+
) -> Dict[str, Any]:
368+
"""Searches registered MCP Servers."""
369+
return self._search(
370+
"mcpServers",
371+
search_string=search_string,
372+
search_type=search_type,
373+
filter_str=filter_str,
374+
order_by=order_by,
375+
page_size=page_size,
376+
page_token=page_token,
377+
)
378+
321379
def get_mcp_server(self, name: str) -> Dict[str, Any]:
322380
"""Retrieves details of a specific MCP Server."""
323381
return self._make_request(name)
@@ -473,6 +531,27 @@ def list_agents(
473531
params["pageToken"] = page_token
474532
return self._make_request("agents", params=params)
475533

534+
def search_agents(
535+
self,
536+
*,
537+
search_string: str | None = None,
538+
search_type: Literal["KEYWORD", "SEMANTIC"] | None = None,
539+
filter_str: str | None = None,
540+
order_by: str | None = None,
541+
page_size: int | None = None,
542+
page_token: str | None = None,
543+
) -> Dict[str, Any]:
544+
"""Searches registered A2A Agents."""
545+
return self._search(
546+
"agents",
547+
search_string=search_string,
548+
search_type=search_type,
549+
filter_str=filter_str,
550+
order_by=order_by,
551+
page_size=page_size,
552+
page_token=page_token,
553+
)
554+
476555
def get_agent_info(self, name: str) -> Dict[str, Any]:
477556
"""Retrieves detailed metadata of a specific A2A Agent."""
478557
return self._make_request(name)

tests/unittests/integrations/agent_registry/test_agent_registry.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,74 @@ def test_list_agents(self, registry):
302302
agents = registry.list_agents()
303303
assert agents == {"agents": []}
304304

305+
def test_search_agents(self, registry):
306+
"""Tests search_agents API call."""
307+
# pylint: disable=protected-access
308+
mock_response = MagicMock()
309+
mock_response.json.return_value = {"agents": [{"name": "agent-1"}]}
310+
mock_response.raise_for_status = MagicMock()
311+
registry._session.post.return_value = mock_response
312+
313+
registry._credentials.token = "token"
314+
registry._credentials.refresh = MagicMock()
315+
316+
agents = registry.search_agents(
317+
search_string="test-agent",
318+
search_type="KEYWORD",
319+
filter_str="display_name:test",
320+
order_by="name",
321+
page_size=10,
322+
page_token="next-token",
323+
)
324+
assert agents == {"agents": [{"name": "agent-1"}]}
325+
registry._session.post.assert_called_once_with(
326+
f"{registry._base_url}/projects/test-project/locations/global/agents:search",
327+
headers={"x-goog-user-project": "test-project"},
328+
json={
329+
"searchString": "test-agent",
330+
"searchType": "KEYWORD",
331+
"filter": "display_name:test",
332+
"orderBy": "name",
333+
"pageSize": 10,
334+
"pageToken": "next-token",
335+
},
336+
)
337+
# pylint: enable=protected-access
338+
339+
def test_search_mcp_servers(self, registry):
340+
"""Tests search_mcp_servers API call."""
341+
# pylint: disable=protected-access
342+
mock_response = MagicMock()
343+
mock_response.json.return_value = {"mcpServers": [{"name": "mcp-1"}]}
344+
mock_response.raise_for_status = MagicMock()
345+
registry._session.post.return_value = mock_response
346+
347+
registry._credentials.token = "token"
348+
registry._credentials.refresh = MagicMock()
349+
350+
mcp_servers = registry.search_mcp_servers(
351+
search_string="test-mcp",
352+
search_type="KEYWORD",
353+
filter_str="display_name:test",
354+
order_by="name",
355+
page_size=10,
356+
page_token="next-token",
357+
)
358+
assert mcp_servers == {"mcpServers": [{"name": "mcp-1"}]}
359+
registry._session.post.assert_called_once_with(
360+
f"{registry._base_url}/projects/test-project/locations/global/mcpServers:search",
361+
headers={"x-goog-user-project": "test-project"},
362+
json={
363+
"searchString": "test-mcp",
364+
"searchType": "KEYWORD",
365+
"filter": "display_name:test",
366+
"orderBy": "name",
367+
"pageSize": 10,
368+
"pageToken": "next-token",
369+
},
370+
)
371+
# pylint: enable=protected-access
372+
305373
def test_get_mcp_server(self, registry):
306374
mock_response = MagicMock()
307375
mock_response.json.return_value = {"name": "test-mcp"}
@@ -606,8 +674,8 @@ def test_get_auth_headers(self, registry):
606674

607675
def test_make_request_raises_http_status_error(self, registry):
608676
mock_response = MagicMock()
609-
mock_response.status_code = 404
610-
mock_response.text = "Not Found"
677+
mock_response.status_code = 500
678+
mock_response.text = "Internal Server Error"
611679
error = requests.exceptions.HTTPError(
612680
"Error", request=MagicMock(), response=mock_response
613681
)
@@ -617,7 +685,7 @@ def test_make_request_raises_http_status_error(self, registry):
617685
registry._credentials.refresh = MagicMock()
618686

619687
with pytest.raises(
620-
RuntimeError, match="API request failed with status 404"
688+
RuntimeError, match="API request failed with status 500"
621689
):
622690
registry._make_request("test-path")
623691

0 commit comments

Comments
 (0)