{"slug": "dynamic-mcp-server-discovery-on-google-cloud", "title": "Dynamic MCP Server Discovery on Google Cloud", "summary": "Google Cloud made Agent Registry generally available in June 2026, providing a central registry that lets AI agents discover MCP server URLs at runtime instead of hardcoding them, according to a Google Cloud post. The post demonstrates using the Agent Development Kit's AgentRegistry client and McpToolset class to look up registered MCP servers, addressing the maintenance burden of updating every agent when a server moves region, is redeployed under a new name, or gets a separate staging copy. Agent Registry handles registration and discovery for MCP servers, agents, inference services (LLM engines) and skills.", "body_md": "# Dynamic MCP Server Discovery on Google Cloud\n\n## Why you need an MCP Server Registry\n\nEvery agent that uses an MCP server needs the server’s URL. That seems harmless until the server moves to another region, gets redeployed under a new name or gets a separate staging copy. Then you have to update and redeploy every agent that has the URL hardcoded, and often nobody finds out until something breaks. With a few agents and a dozen servers, that one pasted URL becomes a real maintenance burden.\n\nHere’s a typical way to connect an agent to an MCP server. It uses the Agent Development Kit (ADK) and its general-purpose `McpToolset` class, which works with any MCP server that uses the Streamable HTTP transport:\n\n```\nfrom google.adk.tools.mcp_tool.mcp_session_manager import (\n    StreamableHTTPConnectionParams\n)\nfrom google.adk.tools.mcp_tool.mcp_toolset import (\n    McpToolset\n)\n\nmcp_toolset = McpToolset(\n    connection_params=StreamableHTTPConnectionParams(\n        url=MCP_SERVER_URL,\n        timeout=30.0,  # custom timeout\n    ),\n#    custom header provider to add authorization headers\n#    header_provider=_auth_header_provider(),\n)\n```\n\n`McpToolset` handles the MCP protocol for the agent: the connection, listing the server’s tools and calling them. If the server requires authentication, a header provider can add headers such as [Authorization](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Authorization) to each request. ADK also has higher-level, ready-made integrations for some MCP servers, including several Google Cloud ones. I use the general-purpose toolset here because it works the same way with any server, including custom ones that you or your team build. Once the toolset is created, you can add it to an ADK agent as a tool.\n\n``` python\nfrom google.adk.agents.llm_agent import Agent\n\n_ROLE = \"\"\"You are the friendly guide.\nYour goal is to help users with their requests.\nRules:\n- Never answer questions from your own knowledge.\n  Every fact MUST come from the tools. If the tools return nothing, say so.\n- Use the tools on the very first question as well. Do not reply with a\n  generic greeting when the user has already asked something.\n- If the user only greets you, briefly introduce yourself and offer to\n  help them\n\"\"\"\n\nagent = Agent(\n    model=\"gemini-3.8-flash\",\n    name=\"my_agent\",\n    description=\"The friendly assistant for handling a user's requests.\",\n    instruction=_ROLE,\n    tools=[\n        mcp_toolset,\n    ],\n)\n```\n\nOther agent frameworks have their own ways to connect to MCP servers, and they all assume the same thing: you know the server’s URL before the agent starts. In practice, you either put the URL into the deployment or fetch it from “somewhere” before you configure the agent.\n\nThat “somewhere” is what this post is about. Instead of every agent keeping its own copy of the URL, you store it once in a registry, and agents look it up when they need it. The registry becomes the one place that records which servers exist and where they run. Agents only need to know what they’re looking for. Many services already offer this kind of registry for MCP servers.\n\nThe rest of this post shows how it works with Google Cloud’s version. In June 2026, Google Cloud made [Agent Registry](https://docs.cloud.google.com/agent-registry/overview) generally available. Despite its name, Agent Registry is also an MCP server registry. It handles registration and discovery for several types of AI workloads: MCP servers, agents, inference services (LLM engines) and skills. I’ll focus on discovering MCP servers, but I plan to explore Agent Registry more in the future.\n\n## Direct approach\n\nWhen I built my first implementation of the agent that uses Agent Registry, I took a straightforward approach: walk through all registered MCP servers and search for mine. [ADK provides](https://adk.dev/integrations/agent-registry/) the `AgentRegistry` class, a client for the Agent Registry API. Conveniently, the class can build an `McpToolset` instance from the registration record. My code looked like this:\n\n```\nfrom google.adk.integrations.agent_registry import (\n    AgentRegistry\n)\n\nregistry = AgentRegistry(\n    project_id=PROJECT_ID,\n    location=LOCATION,\n#    custom header provider to add authorization headers\n#    header_provider=header_provider,\n)\n\nmcp_toolset = None\nmcp_servers = registry.list_mcp_servers().get(\"mcpServers\", [])\nfor s in mcp_servers:\n    if s.get(\"mcpServerId\") == MCP_SERVER_URN:\n        mcp_toolset = registry.get_mcp_toolset(mcp_server_name=s[\"name\"])\n        break\n```\n\nYou can add `mcp_toolset` to the agent’s tools as shown in the previous section. The code no longer needs the server’s URL. However, to find the server, the code uses its URN ‒ a [uniform resource name](https://en.wikipedia.org/wiki/Uniform_Resource_Name) that Agent Registry uses as the server’s unique ID. For MCP servers, the URN has the following format:\n\n```\nurn:mcp:projects-PROJECT1_NUMBER:projects:PROJECT2_NUMBER:locations:LOC:SERVICE\n```\n\nThe variable parts of the URN are:\n\n- `PROJECT1_NUMBER` : the project number of the Google Cloud project where the MCP server is registered.\n- `PROJECT2_NUMBER` : the project number of the Google Cloud runtime where the MCP server is deployed. When the MCP server is registered through the Agent Registry API,`PROJECT2_NUMBER` is the same as`PROJECT1_NUMBER.`\n- `LOC` : the Agent Registry[location](https://docs.cloud.google.com/agent-registry/locations) that was used to register the MCP server.\n- `SERVICE` : the path to the service.\n\nAgent Registry integrates with [Cloud Run](https://docs.cloud.google.com/agent-registry/register-mcp-servers#cloud-run-mcp) and [GKE](https://docs.cloud.google.com/agent-registry/register-mcp-servers#gke-mcp) to register an MCP server automatically when it’s deployed to one of these runtimes. Automatic registration has a few caveats, which I cover in the “Navigating the pitfalls” section. The structure of `SERVICE` depends on how the server was registered:\n\n| Registration | `SERVICE` structure | \n|---|---|\n| Agent Registry API | `agentregistry:services:NAME` | \n| Integration with Cloud Run | `run:services:NAME` | \n| Integration with GKE | `container:clusters:CLUSTER_NAME:k8s:namespaces:NAMESPACE:apps:deployments:NAME` | \n\n- `NAME` stands for the service or deployment name.\n- `CLUSTER_NAME` stands for the name of the GKE cluster.\n- `NAMESPACE` stands for the Kubernetes namespace of the MCP server deployment.\n\nAt first glance, this solution just replaces one piece of information (the URL) with another (the URN). The difference is that the agent no longer stores the endpoint; it reads the endpoint from the registry. This decouples the MCP server’s deployment details from the development lifecycle of the agents that use it.\n\n## Fully dynamic discovery\n\nKnowing the URN is still a burden, especially in situations where the agent might not be aware of any registration details. Additionally, these details may change during the lifetime of the agent, making discovery at launch inflexible.\n\nAgent Registry also supports semantic search for MCP servers. By combining it with a custom tool, a custom toolset and dynamic instructions, you can find the best-suited MCP server at runtime. My implementation does everything in a single agent. Depending on the number of MCP servers and the size of the context, you may want to split this work across several agents.\n\n### Semantic search\n\nThe `AgentRegistry` ADK class has a `search_mcp_servers` method that supports both keyword and [semantic search](https://en.wikipedia.org/wiki/Semantic_search). The following code wraps the call to the method in a tool, so the agent can compose a semantic query and run it against Agent Registry.\n\n``` python\nimport time\nfrom typing import Any\nfrom google.adk.tools import ToolContext\n\ndef discover_mcp_servers(\n    query: str,\n    ctx: ToolContext,\n) -> dict[str, Any]:\n    cached = _fresh_discovery(ctx.state)\n    if cached:\n        return {\n            \"status\": \"already_discovered\",\n            \"message\": \"MCP servers are already discovered.\",\n            \"servers\": cached[\"servers\"],\n        }\n    try:\n        response = registry.search_mcp_servers(\n            search_string=query,\n            search_type=\"SEMANTIC\",\n            page_size=5,\n        )\n    except Exception as e:\n        return {\n            \"status\": \"error\",\n            \"message\": f\"Agent Registry search failed: {e}\",\n        }\n    found_servers = response.get(\"mcpServers\", [])\n    if not found_servers:\n        return {\n            \"status\": \"not_found\",\n            \"message\": f\"No servers found matching '{query}'.\",\n        }\n    servers = [s[\"name\"] for s in found_servers if \"name\" in s]\n    ctx.state[DISCOVERY_STATE_KEY] = {\n        \"servers\": servers,\n        \"discovered_at\": time.time(),\n    }\n    return {\n        \"status\": \"discovered\",\n        \"message\": f\"Successfully discovered {len(servers)} MCP server(s).\",\n        \"servers\": servers,\n    }\n```\n\nThe code uses a `registry` variable that is initialized the same way as in the direct approach. But instead of going through the list and matching against known values, the code searches for the MCP server with a query that the agent composes. The code stores the results in the state under the `DISCOVERY_STATE_KEY` key. See [ADK state scopes](https://adk.dev/sessions/state/) for ways to control the scope of the discovery results. The code also sets a retention timeout, in case the registration changes while the agent is still running. The `_fresh_discovery` function returns the record from the state if it exists and hasn’t expired:\n\n``` php\nimport time\nfrom typing import Any\n\ndef _fresh_discovery(state: Any) -> dict[str, Any] | None:\n    if not state:\n        return None\n    record = state.get(DISCOVERY_STATE_KEY)\n    if not record or not record.get(\"servers\"):\n        return None\n    if time.time() - record.get(\"discovered_at\", 0) > DISCOVERY_TTL_SECONDS:\n        return None\n    return record\n```\n\n### Discovering and using the tools\n\nThe code above discovers servers, not their tools. Although the tool manifest is included in the Agent Registry response, it isn’t enough to call the tools. The obvious way would be to create an instance of `McpToolset` and add it to the agent’s tools. However, that would require re-creating the agent at runtime. It’s possible, but in my opinion the biggest drawback is the added complexity of implementing and maintaining the agent’s code, especially if the application doesn’t otherwise need dynamically created agents. A simpler way is a custom toolset class that creates toolsets for the discovered servers and returns their tools at runtime. ADK provides the `BaseToolset` class. A custom toolset inherits from it and implements the `get_tools` method, which returns the tools of the discovered MCP servers:\n\n``` python\nimport asyncio\nfrom google.adk.agents.readonly_context import (\n    ReadonlyContext\n)\nfrom google.adk.integrations.agent_registry import (\n    AgentRegistry\n)\nfrom google.adk.tools import BaseTool\nfrom google.adk.tools.base_toolset import BaseToolset\nfrom google.adk.tools.mcp_tool.mcp_toolset import (\n    McpToolset\n)\n\nclass RegistryDiscoveredToolset(BaseToolset):\n    def __init__(self, agent_registry: AgentRegistry):\n        super().__init__()\n        self._registry = agent_registry\n        self._toolsets: dict[str, McpToolset] = {}\n        # Override BaseToolset._use_invocation_cache\n        # to react immediately to (re)discovery\n        self._use_invocation_cache = False\n\n    async def _tools_of(\n             self,\n             server_name: str,\n             ctx: ReadonlyContext,\n    ) -> list[BaseTool]:\n        try:\n            if server_name not in self._toolsets:\n                self._toolsets[server_name] = await asyncio.to_thread(\n                    self._registry.get_mcp_toolset, server_name\n                )\n                self._toolsets[server_name].connection_params.timeout = 30.0\n            return await self._toolsets[server_name].get_tools_with_prefix(ctx)\n        except Exception as e:\n            print(f\"Skipping MCP server {server_name}: {e}\")\n            return []\n\n    async def get_tools(\n        self,\n        ctx: ReadonlyContext | None = None,\n    ) -> list[BaseTool]:\n        if not ctx:\n            return []\n        record = _fresh_discovery(ctx.state)\n        if not record or not record.get(\"servers\"):\n            if self._toolsets:\n                await self.close()\n            return []\n        per_server = await asyncio.gather(\n            *(self._tools_of(name, ctx) for name in record[\"servers\"])\n        )\n        return [tool for tools in per_server for tool in tools]\n\n    async def close(self) -> None:\n        if self._toolsets:\n            await asyncio.gather(\n                *(toolset.close() for toolset in self._toolsets.values()),\n                return_exceptions=True,\n            )\n            self._toolsets.clear()\n```\n\n### Dynamic instructions\n\nThe agent’s instructions have to reflect whether the MCP servers are already discovered. In ADK, an agent’s instructions can be static text or the output of a function. The following function tells the agent to discover tools first and to use them once they’re discovered:\n\n```\nfrom google.adk.agents.readonly_context import (\n    ReadonlyContext\n)\n\n_ROLE = \"\"\"You are a friendly, knowledgeable guide. You help users to find answers to their inquires. Base every fact about on tool results; never invent them.\n\"\"\"\ndef agent_instructions(ctx: ReadonlyContext) -> str:\n    record = _fresh_discovery(ctx.state)\n    if record is None:\n        return _ROLE + \"\"\"TOOL DISCOVERY - do this once before answering the user:\n1. Call `discover_mcp_servers` with a semantic query.\n2. As soon as the discovery succeeds, the tools become available to you.\n3. Immediately use those tools to answer the user's request.\n\"\"\"\n    return _ROLE + (\n        f\"\\nYour active MCP servers: {', '.join(record['servers'])}. \"\n        \"Do not run discovery again; use the active tools directly to fulfill the user's request.\"\n    )\n```\n\nWhat’s left is to update the code that creates the agent so that it uses all these constructs:\n\n``` python\nfrom google.adk.agents.llm_agent import Agent\n\nagent = Agent(\n    model=\"gemini-3.8-flash\",\n    name=\"my_agent\",\n    description=\"The friendly assistant for handling a user's requests.\",\n    instruction=agent_instructions,\n    tools=[\n        discover_mcp_servers,\n        RegistryDiscoveredToolset(registry),\n    ],\n)\n```\n\n## Navigating the pitfalls\n\nThe path to this result wasn’t without friction. Here are a few pitfalls I encountered and how you can work around them:\n\n- \n**Location dependency:** You may have noticed that the`AgentRegistry` constructor accepts a location argument. You see only the servers registered in the location that you pass in this argument. For example, if you registered your server in`us-central1` but passed`global` as the location argument, you won’t find the server. The good news is that the registration location doesn’t have to match the deployment location.Tip Unless data residency requirements apply to you, register your MCP servers in the `global` location.\n- \n**Automatic registration with Cloud Run and GKE:** As mentioned, GKE and Cloud Run can automatically register workloads labeled as MCP servers. These registrations are performed in the same project and region as the workload. For example, deploying an MCP server on GKE in the`us-west1` region registers it in the`us-west1` Agent Registry location.Warning When I wrote this post, automatic registration in Cloud Run recorded only the server’s name and type, which made the record unusable for discovery. Verify the registered metadata, such as the tool manifest, before you use it in production. Tip Register your MCP servers explicitly to control the registration location.\n- \n**Two project numbers in URN:** The URN format requires two separate project numbers in the URN prefix:\n\n```\nurn:mcp:projects-PROJECT1_NUMBER:projects:PROJECT2_NUMBER:...\n```\n\n `PROJECT1_NUMBER` represents the publisher, in this case Agent Registry.`PROJECT2_NUMBER` represents the runtime resource. When an MCP server is manually registered using the Agent Registry API, the two values are the same. The two numbers can differ only when Agent Registry ingests or indexes native Google Cloud runtimes across multiple projects (such as Cloud Run, Vertex AI / Reasoning Engine, or GKE).Tip If you use the URN to find the MCP server, make sure you know both project numbers: the Agent Registry project and the server’s runtime project. Don’t assume they’re always the same.\n- \n**Tokens for authorized invocations:** The code snippets include commented-out lines that show where to add authorization headers. It’s important to distinguish between the tokens that you use to call Agent Registry and the tokens that you use to call MCP servers. Reusing one token for both types of calls, or for calls to different MCP servers, can create a security vulnerability or simply not work.Tip Use the agent’s Application Default Credentials to call Agent Registry, and use a dedicated mechanism to obtain tokens for calls to MCP servers. If you use `RegistryDiscoveredToolset` with servers that need different credentials, give each server its own`AgentRegistry` instance and header provider. See the[Dynamic MCP Server Discovery](https://codelabs.developers.google.com/agent-registry-dynamic-discovery-with-adk) codelab for an example.\n\n## Alternatives and final thoughts\n\nThe implementation I went with is only one way to use Agent Registry for discovery. Another way is to connect your agent to Agent Registry’s own MCP server (in Preview at the time of writing) instead of writing a custom discovery tool. That server has a fixed, Google-managed endpoint, so hardcoding its URL doesn’t bring back the problem this post started with. If you build an agent whose job is tool discovery, the Agent Registry MCP server gives you discovery without writing and maintaining your own tool. Keep in mind that its search tool performs keyword or prefix search, not the semantic search that I used in this post.\n\nWhichever option you choose, your agents no longer carry the server’s URL. In my implementation, the agent is equipped with two tools: `discover_mcp_servers` to discover MCP server(s) and `RegistryDiscoveredToolset` to mount the tools of the discovered servers, and use them to answer the user’s requests. Later requests reuse the discovery until it expires. When a server moves to another region or gets redeployed under a new name, you update its registration once. The agents find it at the new location after their cached discovery expires, with no code change and no redeployment.\n\nYou can find the complete code, including the authorization and error handling that I left out of the snippets, in the [Dynamic MCP Server Discovery with Agent Registry and ADK](https://codelabs.developers.google.com/agent-registry-dynamic-discovery-with-adk) codelab. The code for all steps of the codelab is on [GitHub](https://github.com/GoogleCloudPlatform/devrel-demos/tree/main/codelabs/agent-registry-dynamic-discovery-with-adk).", "url": "https://wpnews.pro/news/dynamic-mcp-server-discovery-on-google-cloud", "canonical_source": "https://leoy.blog/posts/dynamic-mcp-server-discovery/", "published_at": "2026-10-19 16:24:05+00:00", "updated_at": "2026-10-19 17:18:35.770884+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-tools", "developer-tools"], "entities": ["Google Cloud", "Agent Registry", "Agent Development Kit", "McpToolset", "AgentRegistry", "Gemini"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/dynamic-mcp-server-discovery-on-google-cloud", "markdown": "https://wpnews.pro/news/dynamic-mcp-server-discovery-on-google-cloud.md", "text": "https://wpnews.pro/news/dynamic-mcp-server-discovery-on-google-cloud.txt", "jsonld": "https://wpnews.pro/news/dynamic-mcp-server-discovery-on-google-cloud.jsonld"}}