Dynamic MCP Server Discovery with Agent Registry and ADK

1. Introduction

In this codelab, you will learn how to use Google Cloud Agent Registry and the Agent Development Kit (ADK) to dynamically discover and invoke registered Model Context Protocol (MCP) servers at runtime, eliminating hardcoded endpoints and tool configurations.

This lab uses ADK and Python programming language to build an application. The same principles can be applied in other programming languages and using other agent development frameworks.

What you'll do

  • Enable and configure Google Cloud Agent Registry in your project
  • Deploy an MCP server to Cloud Run and register it in Agent Registry
  • Build an ADK agent that calls the MCP server using a hardcoded URL
  • Change the agent to discover the MCP server in Agent Registry by its identity
  • Change the agent to discover MCP servers at runtime using semantic search

What you'll need

  • A modern web browser (such as Chrome)
  • A Google Cloud project with billing enabled
  • An account with permissions to create and manage resources and configure IAM policies on the Google Cloud project

2. Set up your environment

The instructions in this codelab assume that you run commands in Cloud Shell in the Google Cloud console. Cloud Shell is a command-line environment running in Google Cloud that comes preloaded with necessary tools. If you have the gcloud CLI, git and Python 3 installed in your local environment, you can also run the commands there.

Create a Google Cloud Project

  1. In the Google Cloud Console, on the project selector page, create a Google Cloud project.
  2. Make sure that billing is enabled for your Cloud project. Learn how to check if billing is enabled on a project.

Start a Cloud Shell terminal

  1. Using resource picker select the Google Cloud project that you created.
  2. Click Activate Cloud Shell Activate Cloud Shell icon at the top of the Google Cloud console.
  3. Once connected to Cloud Shell, verify your authentication:
    gcloud auth list
    
  4. Confirm your project is configured:
    gcloud config get project
    
  5. If your project is not set as expected or if you use an environment on your local machine instead of Cloud Shell, set up the project ID:
    export GOOGLE_CLOUD_PROJECT=<YOUR_PROJECT_ID>
    gcloud config set project "${GOOGLE_CLOUD_PROJECT}"
    

Select a Google Cloud project and set environment variables

  1. Enable the required Google Cloud APIs:
    gcloud services enable \
      agentregistry.googleapis.com \
      aiplatform.googleapis.com \
      cloudbuild.googleapis.com \
      run.googleapis.com \
      --project="${GOOGLE_CLOUD_PROJECT}"
    

Set up the Python environment

  1. Create and activate a virtual environment and install the required dependencies:
    mkdir -p adk-dynamic-discovery && cd adk-dynamic-discovery
    python3 -m venv .venv
    source .venv/bin/activate
    pip install "fastmcp<5.0.0" "google-adk[mcp,a2a,agent-identity]>=2.5.0,<3.0.0"
    

3. Create and register a Zoo MCP server

In this step, you create and register a Zoo MCP server. The server exposes three tools: list_species ‒ list all species that live in the zoo, get_animals_by_species ‒ list the animals of a specific species, and get_animal_details ‒ get additional information about a specific animal.

You can find all files for this step on GitHub.

Create the MCP server

  1. Set up a folder and Python project for the MCP server
    mkdir mcp-on-cloudrun && cd mcp-on-cloudrun
    uv init --description "Example of deploying an MCP server on Cloud Run" --bare --python 3.13
    uv add "fastmcp<5.0.0" --no-sync
    
  2. Download the MCP server code:
    curl -fsSL https://raw.githubusercontent.com/GoogleCloudPlatform/devrel-demos/main/codelabs/agent-registry-dynamic-discovery-with-adk/steps/step3/server.py -o server.py
    

Deploy the MCP server to Cloud Run and register at Agent Registry

Next, you will create Dockerfile for the server and deploy the server to Cloud Run.

  1. Create Dockerfile:
    cat << EOF > Dockerfile
    # Use the official Python image
    FROM python:3.13-slim
    
    # Install uv
    COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
    
    # Install the project into /app
    COPY . /app
    WORKDIR /app
    
    # Allow statements and log messages to immediately appear in the logs
    ENV PYTHONUNBUFFERED=1
    
    # Install dependencies
    RUN uv sync
    
    # Run the FastMCP server
    CMD ["uv", "run", "server.py"]
    EOF
    
  2. Create designated service account for the server:
    gcloud iam service-accounts create zoo-mcp-server-sa \
        --display-name="Zoo MCP Server Service Account"
    
  3. Deploy the server to the us-west1 region.
    gcloud run deploy zoo-mcp-server \
        --service-account=zoo-mcp-server-sa@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com \
        --region=us-west1 \
        --source=. \
        --labels=dev-tutorial=codelab-dynamic-mcp-discovery \
        --invoker-iam-check \
        --no-allow-unauthenticated
    
    If you see the following prompt, confirm the creation of a new Artifact Registry repository. This prompt will be shown if you don't have Artifact Registry repository in your project for the us-west1 region:
    Deploying from source requires an Artifact Registry Docker repository to store built containers. A repository named
    [cloud-run-source-deploy] in region [us-west1] will be created.
    
    Do you want to continue (Y/n)?
    
  4. Register the MCP server in Agent Registry at "global" location:
    fastmcp inspect server.py --format mcp | jq 'del(..|nulls) | del(.tools[]?._meta?)' > toolspec.json
    MCP_URL=$(gcloud run services describe zoo-mcp-server --region=us-west1 --format='value(status.url)')
    gcloud agent-registry services create zoo-mcp-server \
        --display-name="zoo-mcp-server" \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --location=global \
        --mcp-server-spec-type="tool-spec" \
        --mcp-server-spec-content="toolspec.json" \
        --interfaces="url=${MCP_URL}/mcp,protocolBinding=jsonrpc"
    
    These commands generate toolspec.json with the tool manifest and call Agent Registry API to register the server.
  5. Verify that the agent appears in the registry:
    gcloud agent-registry mcp-servers list \
        --location=global \
        --filter="display_name=zoo-mcp-server"
    
    You will see YAML output with the MCP server information like this:
    ---
    attributes:
      agentregistry.googleapis.com/system/RuntimeReference:
        uri: //agentregistry.googleapis.com/projects/[PROJECT_NUMBER]/locations/global/services/zoo-mcp-server
    createTime: '<create time datetime in ISO 8601>'
    displayName: zoo-mcp-server
    interfaces:
    - protocolBinding: JSONRPC
      url: https://zoo-mcp-server-[PROJECT_NUMBER].us-west1.run.app/mcp
    mcpServerId: urn:mcp:projects-[PROJECT_NUMBER]:projects:[PROJECT_NUMBER]:locations:global:agentregistry:services:zoo-mcp-server
    name: projects/[PROJECT_ID]/locations/global/mcpServers/agentregistry-00000000-0000-0000-1234-56789abcdef
    tools:
    - annotations:
        idempotentHint: true
        readOnlyHint: true
      description: |-
        Returns every species in the zoo. Use it first for zoo-wide questions
        such as counting all animals or listing everything the zoo has.
      name: list_species
    - annotations:
        idempotentHint: true
        readOnlyHint: true
      description: |-
        Retrieves all animals of a specific species from the zoo.
        Can also be used to collect the base data for aggregate queries
        of animals of a specific species - like counting the number of penguins
        or finding the oldest lion.
      name: get_animals_by_species
    - annotations:
        idempotentHint: true
        readOnlyHint: true
      description: Retrieves the details of a specific animal by its name.
      name: get_animal_details
    updateTime: '<last updated datetime in ISO 8601>'
    

4. Create a basic ADK agent to use the Zoo MCP server

In this step, you create an ADK agent to answer user's requests about the zoo with the help of the hardcoded Zoo MCP server URL. Hardcoded enforces you to know the URL of the MCP server BEFORE the agent is deployed.

You can find source files for this step on GitHub.

Create the agent application

Create a scaffold of the ADK project and the Zoo agent that uses the Zoo MCP server.

  1. Set up a folder and project for the Zoo agent:
    mkdir ../sample-agent && cd ../sample-agent
    uv init --description "Example of ADK agent to showcase dynamic MCP discovery" --bare --python 3.13
    uv add "google-adk[mcp,a2a,agent-identity]>=2.5.0,<3.0.0" --no-sync
    
  2. Create the agent package folder and download the __init__.py file:
    mkdir zoo_agent
    curl -fsSL https://raw.githubusercontent.com/GoogleCloudPlatform/devrel-demos/main/codelabs/agent-registry-dynamic-discovery-with-adk/steps/step4/zoo_agent/__init__.py -o zoo_agent/__init__.py
    
  3. Create the .env file and populate it with environment variables that are used in this step and the following steps:
    MCP_URL=$(gcloud run services describe zoo-mcp-server --region=us-west1 --format='value(status.url)')
    cat << EOF > zoo_agent/.env
    GOOGLE_GENAI_USE_ENTERPRISE=TRUE
    GOOGLE_CLOUD_LOCATION=global
    GOOGLE_CLOUD_PROJECT="${GOOGLE_CLOUD_PROJECT}"
    GOOGLE_CLOUD_PROJECT_NUMBER=$(gcloud projects describe ${GOOGLE_CLOUD_PROJECT} \
       --format='value(projectNumber)')
    MCP_SERVER_URL="${MCP_URL}"
    EOF
    
    This command writes all environment variables required across this step and the subsequent discovery steps into zoo_agent/.env, so you do not need to recreate the file later. In this step, the following variables are used:
    • GOOGLE_GENAI_USE_ENTERPRISE, GOOGLE_CLOUD_PROJECT, and GOOGLE_CLOUD_LOCATION: Used implicitly by ADK and the Google GenAI SDK to route Gemini model calls through Vertex AI in your Google Cloud project and location (global). It also stores the location for Agent Registry where the MCP server is registered.
    • GOOGLE_CLOUD_PROJECT_NUMBER: Not used in this step; it is pre-populated here for use in the next step.
    • MCP_SERVER_URL: Read directly in zoo_agent/agent.py to configure the McpToolset with the hardcoded Cloud Run URL of the Zoo MCP server.
  4. Download agent.py with the code for the Zoo agent that connects to the Zoo MCP server:
    curl -fsSL https://raw.githubusercontent.com/GoogleCloudPlatform/devrel-demos/main/codelabs/agent-registry-dynamic-discovery-with-adk/steps/step4/zoo_agent/agent.py -o zoo_agent/agent.py
    

How MCP server authorization works

Because the zoo-mcp-server Cloud Run service was deployed with the --invoker-iam-check and --no-allow-unauthenticated flags, every HTTP request to the MCP server must include a Google-signed ID token in the Authorization header. The token must identify a caller that has the run.routes.invoke permission on the service.

In zoo_agent/agent.py, the _GoogleAuthHeaders class gets ID token credentials in one of two ways. First it calls fetch_id_token_credentials() to get an ID token for the service URL from the metadata server or a service account key. If this fails, it uses the ID token of your user Application Default Credentials (ADC). Each time the instance is called, it refreshes the token if necessary and returns the Authorization header:

class _GoogleAuthHeaders:
    def __init__(self, audience: str):
        self._request = Request()
        self._id_token = False
        try:
            self._credentials = id_token.fetch_id_token_credentials(audience, request=self._request)
        except exceptions.DefaultCredentialsError:
            self._credentials, _ = google.auth.default()
            self._id_token = True
            if not isinstance(self._credentials, credentials.Credentials):
                raise

    def __call__(self, unused_context: ReadonlyContext | None = None) -> dict[str, str]:
        if not self._credentials.valid or (self._id_token and not self._credentials.id_token):
            self._credentials.refresh(self._request)
        token = self._credentials.id_token if self._id_token else self._credentials.token
        return {"Authorization": f"Bearer {token}"}

The agent passes an instance of the class to McpToolset as the header_provider argument. ADK calls the header provider and attaches the headers it returns to every request to the MCP server. Because the HTTP client is created by ADK, the same code works regardless of which version of the MCP Python SDK and its HTTP library is installed:

zoo_mcp_toolset = McpToolset(
    connection_params=StreamableHTTPConnectionParams(
        url=f"{MCP_SERVER_URL}/mcp",
        timeout=30.0,
    ),
    header_provider=_GoogleAuthHeaders(MCP_SERVER_URL),
)

The requested audience is the service URL (MCP_SERVER_URL), while the connection URL includes the /mcp path where the FastMCP server serves the MCP protocol. The 30-second timeout leaves room for Cloud Run cold starts.

Test agent using the ADK playground in your local environment

Before you test the agent, make sure that your user account can call the MCP server.

  1. Local terminal only (skip this step in Cloud Shell): initialize ADC with your user credentials:
    gcloud auth application-default login
    
    Follow the instructions to complete ADC initialization.
  2. (Optional) Grant your user account permission to call the remote MCP server:
    gcloud run services add-iam-policy-binding zoo-mcp-server \
       --region=us-west1 \
       --project=${GOOGLE_CLOUD_PROJECT} \
       --member=user:$(gcloud config get-value account) \
       --role='roles/run.invoker'
    
  3. Run your agent with the adk run command:
    adk run zoo_agent
    
    or run ADK Web interface with the adk web command:
    adk web --port 8080
    
    and then open the URL that the command prints in the browser.If you use Cloud Shell, add the --allow_origins argument to allow opening the Web interface from Cloud Shell:
    adk web --port 8080 --allow_origins="regex:https://.*\.cloudshell\.dev"
    
    After running the command find the Web PreviewWeb Preview icon icon in the Cloud Shell toolbar (right side). Click it and select Preview on port 8080. Cloud Shell will open a new browser tab displaying the ADK Web interface.
  4. Start chatting with the agent. For example, ask it about animals in the zoo:
    How many animals are in the zoo?
    
    The agent calls list_species and then get_animals_by_species for each species to calculate the answer.

The ADK Web interface lets you explore how the agent works, including its calls to the Zoo MCP server. See the Web UI documentation to learn how.

5. Dynamically discover the MCP server endpoint

In this step, you change the agent to discover the Zoo MCP server endpoint registered in Agent Registry instead of hardcoding its endpoint URL.

You can find all files for this step on GitHub.

You use ADK integration with Agent Registry to find and retrieve the server's URL. To implement it you need the following information:

  • Project number and location of the Agent Registry where your MCP server is registered (global).
  • The service ID that you used to register the MCP server (zoo-mcp-server).

Update the agent to use Agent Registry

  1. Download the updated agent.py file from GitHub to override the first version of the agent:
    curl -fsSL https://raw.githubusercontent.com/GoogleCloudPlatform/devrel-demos/main/codelabs/agent-registry-dynamic-discovery-with-adk/steps/step5/zoo_agent/agent.py -o zoo_agent/agent.py
    

    Environment variables in this step

    With this update, the agent relies on the following environment variables from zoo_agent/.env:
    • GOOGLE_CLOUD_PROJECT_NUMBER: Now required to construct the MCP server's identity URN (MCP_SERVER_URN).
    • GOOGLE_CLOUD_PROJECT, GOOGLE_GENAI_USE_ENTERPRISE and GOOGLE_CLOUD_LOCATION: Used by ADK to configure Vertex AI access. The location is also used to discover the MCP server.
    • No longer required: MCP_SERVER_URL is no longer used because the agent retrieves the MCP server endpoint URL from Agent Registry rather than reading it from the environment.

    How MCP server identity and discovery work in AgentRegistry

    In Agent Registry, each registered MCP server has a unique identifier stored in its mcpServerId field, formatted as a Uniform Resource Name (URN). You can see it in the output of the gcloud agent-registry mcp-servers list command in Step 3. The agent reconstructs this identifier from the environment variables:
    PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
    PROJECT_NUMBER = os.environ["GOOGLE_CLOUD_PROJECT_NUMBER"]
    LOCATION = os.environ.get("GOOGLE_CLOUD_LOCATION", "global")
    MCP_SERVER_URN = f"urn:mcp:projects-{PROJECT_NUMBER}:projects:{PROJECT_NUMBER}:locations:{LOCATION}:agentregistry:services:zoo-mcp-server"
    
    The URN encodes the project number of the registry (projects-{PROJECT_NUMBER}), the project number and location where the server is registered (projects:{PROJECT_NUMBER}:locations:{LOCATION}), and the service ID that you used in the gcloud agent-registry services create command (agentregistry:services:zoo-mcp-server).The agent lists the registered MCP servers with registry.list_mcp_servers() and matches each server's mcpServerId against MCP_SERVER_URN. It then passes the matching server's resource name (projects/.../locations/.../mcpServers/...) to registry.get_mcp_toolset(), which reads the endpoint URL from the server's registered interface (https://...run.app/mcp) and returns an McpToolset connected to that URL:
    header_provider = _GoogleAuthHeaders()
    registry = AgentRegistry(
        project_id=PROJECT_ID,
        location=LOCATION,
        header_provider=header_provider,
    )
    
    mcp_server_name = next(
        (
            s["name"]
            for s in registry.list_mcp_servers().get("mcpServers", [])
            if s.get("mcpServerId") == MCP_SERVER_URN
        ),
        None,
    )
    if not mcp_server_name:
        raise RuntimeError(f"MCP server '{MCP_SERVER_URN}' not found in Agent Registry.")
    zoo_mcp_toolset = registry.get_mcp_toolset(mcp_server_name=mcp_server_name)
    header_provider.bind(zoo_mcp_toolset.connection_params.url)
    zoo_mcp_toolset.connection_params.timeout = 30.0
    
    get_mcp_toolset() prefixes the tool names with the server's display name, so the agent sees the tools as zoo_mcp_server_list_species, zoo_mcp_server_get_animals_by_species, and zoo_mcp_server_get_animal_details. The prefix prevents name collisions when an agent uses tools from multiple MCP servers. The last line raises the connection timeout from its default of 5 seconds to leave room for Cloud Run cold starts.

    How authorization changes with AgentRegistry

    AgentRegistry authenticates its own API calls, such as list_mcp_servers(), with ADC, so the header provider is only needed for the calls to the discovered MCP server. Because AgentRegistry creates the McpToolset internally, you pass the header provider to the AgentRegistry constructor.In the previous step, the audience was the MCP_SERVER_URL value that you knew in advance. Now the URL is known only after discovery. ADK calls the header provider when the agent sends a request to the MCP server, not when AgentRegistry is created. This lets the agent create the provider first and bind it to the discovered endpoint after get_mcp_toolset() returns. The bind() method gets the credentials the same way as the constructor in the previous step, with the audience taken from the discovered endpoint URL without the /mcp path:
        def bind(self, endpoint_url: str) -> None:
            parsed = urlparse(endpoint_url)
            audience = f"{parsed.scheme}://{parsed.netloc}"
            try:
                self._credentials = id_token.fetch_id_token_credentials(audience, request=self._request)
            except exceptions.DefaultCredentialsError:
                self._credentials, _ = google.auth.default()
                self._id_token = True
                if not isinstance(self._credentials, credentials.Credentials):
                    raise
    
    The __call__ method is the same as in the previous step, except that it raises an error if it is called before bind().
  2. Run your agent to test the change. Use the adk run command:
    adk run zoo_agent
    
    or run ADK Web interface with the adk web command:
    adk web --port 8080
    
    and then open the URL that the command prints in the browser.If you use Cloud Shell, add the --allow_origins argument to allow opening the Web interface from Cloud Shell:
    adk web --port 8080 --allow_origins="regex:https://.*\.cloudshell\.dev"
    
    Ask the agent the same question as in the previous step. The answer is the same, but in the ADK Web interface the tool calls now appear as zoo_mcp_server_list_species and zoo_mcp_server_get_animals_by_species.

The change allowed you to bypass the need for the explicit MCP server URL. However, you still need to know the project number, the registry location, and the service ID used to register the MCP server, and the discovery is performed statically during agent initialization.

6. Use semantic search for MCP server in dynamic discovery

In this step, you improve the MCP server dynamic discovery by replacing exact URN matching at startup with runtime semantic search. This approach eliminates the need to know the project number where the server is deployed or the server's exact identity (MCP_SERVER_URN), and integrates tool discovery directly into the agent's standard runtime workflow rather than handling it at initialization.

You can find all files for this step on GitHub.

Update the agent to use semantic discovery at runtime

  1. Download the updated agent.py file from GitHub to overwrite the existing implementation:
    curl -fsSL https://raw.githubusercontent.com/GoogleCloudPlatform/devrel-demos/main/codelabs/agent-registry-dynamic-discovery-with-adk/steps/step6/zoo_agent/agent.py -o zoo_agent/agent.py
    

    Environment variables in this step

    Because the agent no longer constructs an MCP server URN, the required configuration is further simplified:
    • GOOGLE_CLOUD_PROJECT, GOOGLE_GENAI_USE_ENTERPRISE and GOOGLE_CLOUD_LOCATION: Used by ADK to configure Vertex AI access. The location is also used to discover the MCP server.
    • No longer required: GOOGLE_CLOUD_PROJECT_NUMBER is no longer needed (in addition to MCP_SERVER_URL, which was already removed in the previous step).
    Instead of calling registry.list_mcp_servers() during module initialization and comparing each entry against a constructed MCP_SERVER_URN, agent.py now defines a discovery tool function discover_zoo_mcp_servers that calls registry.search_mcp_servers() with search_type="SEMANTIC":
    def discover_zoo_mcp_servers(query: str, tool_context: ToolContext) -> dict[str, Any]:
        """Discovers Zoo MCP server(s) matching the query using semantic search.
    
        Args:
            query: Semantic description of zoo capabilities needed (e.g. 'zoo animal info')
        """
        cached = _fresh_discovery(tool_context.state)
        if cached:
            return {
                "status": "already_discovered",
                "message": "Zoo MCP servers are already discovered.",
                "servers": cached["servers"],
            }
    
        try:
            response = registry.search_mcp_servers(
                search_string=query,
                search_type="SEMANTIC",
                page_size=5,
            )
        except Exception as e:
            return {
                "status": "error",
                "message": f"Agent Registry search failed: {e}",
            }
    
        found_servers = response.get("mcpServers", [])
        if not found_servers:
            return {
                "status": "not_found",
                "message": f"No MCP servers found in Agent Registry at {LOCATION} matching '{query}'.",
            }
    
        valid_servers = [s["name"] for s in found_servers if "name" in s]
        tool_context.state[DISCOVERY_STATE_KEY] = {
            "servers": valid_servers,
            "discovered_at": time.time(),
        }
        return {
            "status": "discovered",
            "message": f"Successfully discovered {len(valid_servers)} Zoo MCP server(s).",
            "servers": valid_servers,
        }
    
    This lets the agent search Agent Registry at runtime using a natural-language description of the capabilities it needs (for example, "zoo animals, exhibits, habitat, care, feeding schedule") and saves the discovered server resource names in ADK session state (tool_context.state).

    How dynamic discovery is integrated into the agent workflow

    To mount tools from servers discovered at runtime and guide the agent through the discovery-then-invocation flow, agent.py combines a custom BaseToolset implementation (RegistryDiscoveredToolset) with a dynamic instruction provider (zoo_guide_instruction):
    1. Dynamic toolset (RegistryDiscoveredToolset): Subclasses BaseToolset and sets self._use_invocation_cache = False so ADK re-evaluates available tools immediately after discover_zoo_mcp_servers updates the session state rather than caching an initially empty tool list. When get_tools() is called, it reads the discovered server names from state, builds an authenticated McpToolset for each server in a worker thread, and returns all active tools. A failure to reach one server doesn't affect the others. _build_toolset() performs blocking I/O, so _tools_of() runs it in a worker thread.
      class RegistryDiscoveredToolset(BaseToolset):
          def __init__(self, agent_registry: AgentRegistry):
              super().__init__()
              self._registry = agent_registry
              self._toolsets: dict[str, McpToolset] = {}
              self._use_invocation_cache = False
      
          def _build_toolset(self, server_name: str) -> McpToolset:
              header_provider = _GoogleAuthHeaders()
              server_registry = AgentRegistry(
                  project_id=self._registry.project_id,
                  location=self._registry.location,
                  header_provider=header_provider,
              )
              toolset = server_registry.get_mcp_toolset(server_name)
              header_provider.bind(toolset.connection_params.url)
              toolset.connection_params.timeout = 30.0
              return toolset
      
          async def _tools_of(self, server_name: str, ctx: ReadonlyContext) -> list[BaseTool]:
              try:
                  if server_name not in self._toolsets:
                      self._toolsets[server_name] = await asyncio.to_thread(
                          self._build_toolset, server_name
                      )
                  return await self._toolsets[server_name].get_tools_with_prefix(ctx)
              except Exception as e:
                  print(f"Skipping MCP server {server_name}: {e}")
                  return []
      
          async def get_tools(self, readonly_context: ReadonlyContext | None = None) -> list[BaseTool]:
              if not readonly_context:
                  return []
              record = _fresh_discovery(readonly_context.state)
              if not record or not record.get("servers"):
                  if self._toolsets:
                      await self.close()
                  return []
              per_server = await asyncio.gather(
                  *(self._tools_of(name, readonly_context) for name in record["servers"])
              )
              return [tool for tools in per_server for tool in tools]
      
    2. Dynamic agent instruction (zoo_guide_instruction): Evaluates session state before each model turn. When no servers have been discovered yet, it appends _DISCOVERY_PROMPT instructing the agent to call discover_zoo_mcp_servers first. Once discovery succeeds, it updates the prompt to list the active MCP servers and instructs the agent to use the mounted zoo tools directly without running discovery again:
      def zoo_guide_instruction(ctx: ReadonlyContext) -> str:
          record = _fresh_discovery(ctx.state)
          if record is None:
              return _ROLE + _DISCOVERY_PROMPT
          return _ROLE + (
              f"\nYour active Zoo MCP servers: {', '.join(record['servers'])}. "
              "Do not run discovery again; use the active zoo tools directly to fulfill the visitor's request."
          )
      

    Authorization for dynamically discovered servers

    Semantic search can return several servers, and ADK calls a header provider without telling it which server a request goes to. Therefore, _build_toolset() creates a dedicated _GoogleAuthHeaders and AgentRegistry for each discovered server and binds the provider to that server's endpoint. The provider is the same as in the previous step, with a _bound flag that makes it fail if it is used before bind().

    Retention and refresh logic

    To avoid querying Agent Registry on every turn while still refreshing discovered endpoints periodically, agent.py implements state retention with a time-to-live (TTL):
    DISCOVERY_STATE_KEY = "app:zoo_mcp_servers"
    DISCOVERY_TTL_SECONDS = 24 * 60 * 60
    
    def _fresh_discovery(state: Any) -> dict[str, Any] | None:
        if not state:
            return None
        record = state.get(DISCOVERY_STATE_KEY)
        if not record or not record.get("servers"):
            return None
        if time.time() - record.get("discovered_at", 0) > DISCOVERY_TTL_SECONDS:
            return None
        return record
    
    • Application-scoped retention: Discovered server names and a discovered_at timestamp are stored in session state under the app:zoo_mcp_servers key. Using ADK's app: prefix scopes the cached discovery record across the application run.
    • TTL validation and automatic refresh: _fresh_discovery() verifies that the saved record exists and its age does not exceed DISCOVERY_TTL_SECONDS (24 hours). While the record is fresh, RegistryDiscoveredToolset reuses the cached McpToolset connections (self._toolsets), zoo_guide_instruction omits the discovery prompt, and any repeat call to discover_zoo_mcp_servers immediately returns "already_discovered". Once the 24-hour TTL expires, _fresh_discovery() returns None, causing RegistryDiscoveredToolset.get_tools() to close existing MCP client sessions (await self.close()) and zoo_guide_instruction to prompt the agent to perform a fresh discovery.
  2. Run your agent to test the semantic discovery workflow. Use the adk run command:
    adk run zoo_agent
    
    or run ADK Web interface with the adk web command:
    adk web --port 8080
    
    and then open the URL that the command prints in the browser.If you use Cloud Shell, add the --allow_origins argument to allow opening the Web interface from Cloud Shell:
    adk web --port 8080 --allow_origins="regex:https://.*\.cloudshell\.dev"
    
    When you ask the agent a question about the zoo (for example, How many animals are in the zoo?), observe that it first invokes discover_zoo_mcp_servers, dynamically mounts the tools from the discovered MCP server, and then calls the zoo tools, such as zoo_mcp_server_list_species and zoo_mcp_server_get_animals_by_species, to answer your question.Ask a follow-up question, for example Which is the oldest lion?. The agent answers using the already mounted zoo tools and doesn't call discover_zoo_mcp_servers again, because the discovery record in the session state is still fresh.

7. Clean up

Option 1: Delete the project

If you created a dedicated project for this codelab, the easiest way to eliminate billing is to delete the project:

gcloud projects delete "${GOOGLE_CLOUD_PROJECT}" --quiet

Option 2: Delete individual resources

If you used an existing project and want to keep it, delete the individual resources created during this codelab:

  1. Delete the MCP server registration from Agent Registry. Because you registered the server manually, deleting the Cloud Run service doesn't remove the registration:
    gcloud agent-registry services delete zoo-mcp-server \
        --location=global \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --quiet
    
  2. Delete the Cloud Run MCP server service:
    gcloud run services delete zoo-mcp-server \
        --region=us-west1 \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --quiet
    
  3. Delete the service account created for the MCP server:
    gcloud iam service-accounts delete \
        "zoo-mcp-server-sa@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --quiet
    
  4. If the cloud-run-source-deploy Artifact Registry repository was created specifically during this codelab, delete it to avoid container image storage charges:
    gcloud artifacts repositories delete cloud-run-source-deploy \
        --location=us-west1 \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --quiet
    
  5. Optionally, remove the local project directory created in Cloud Shell:
    cd ~ && rm -rf adk-dynamic-discovery
    

8. Congratulations

In this codelab, you learned how to dynamically discover and use MCP servers with Google Cloud Agent Registry and the Agent Development Kit (ADK):

  1. Registered an MCP server: Deployed a secured MCP server to Cloud Run and registered it in Agent Registry.
  2. Discovered an MCP server by identity: Resolved the MCP server endpoint from Agent Registry instead of hardcoding its URL.
  3. Discovered MCP servers semantically: Let the agent find and mount MCP tools at runtime using natural-language search, with retention and refresh.

Next steps