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
- In the Google Cloud Console, on the project selector page, create a Google Cloud project.
- 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
- Using resource picker select the Google Cloud project that you created.
- Click Activate Cloud Shell
at the top of the Google Cloud console.
- Once connected to Cloud Shell, verify your authentication:
gcloud auth list - Confirm your project is configured:
gcloud config get project - 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
- 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
- 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
- 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 - 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.
- 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 - Create designated service account for the server:
gcloud iam service-accounts create zoo-mcp-server-sa \ --display-name="Zoo MCP Server Service Account" - Deploy the server to the
us-west1region. 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 thegcloud 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-unauthenticatedus-west1region: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)? - Register the MCP server in Agent Registry at "global" location:
These commands generatefastmcp 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"toolspec.jsonwith the tool manifest and call Agent Registry API to register the server. - Verify that the agent appears in the registry:
You will see YAML output with the MCP server information like this:gcloud agent-registry mcp-servers list \ --location=global \ --filter="display_name=zoo-mcp-server"--- 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.
- 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 - Create the agent package folder and download the
__init__.pyfile: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 - Create the
.envfile and populate it with environment variables that are used in this step and the following steps: This command writes all environment variables required across this step and the subsequent discovery steps intoMCP_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}" EOFzoo_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, andGOOGLE_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 inzoo_agent/agent.pyto configure theMcpToolsetwith the hardcoded Cloud Run URL of the Zoo MCP server.
- Download
agent.pywith 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.
- Local terminal only (skip this step in Cloud Shell): initialize ADC with your user credentials:
Follow the instructions to complete ADC initialization.gcloud auth application-default login - (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' - Run your agent with the
adk runcommand: or run ADK Web interface with theadk run zoo_agentadk webcommand: and then open the URL that the command prints in the browser.If you use Cloud Shell, add theadk web --port 8080--allow_originsargument to allow opening the Web interface from Cloud Shell: After running the command find the Web Previewadk web --port 8080 --allow_origins="regex:https://.*\.cloudshell\.dev"
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. - Start chatting with the agent. For example, ask it about animals in the zoo:
The agent callsHow many animals are in the zoo?list_speciesand thenget_animals_by_speciesfor 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
- Download the updated
agent.pyfile 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.pyEnvironment variables in this step
With this update, the agent relies on the following environment variables fromzoo_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_ENTERPRISEandGOOGLE_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_URLis 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
In Agent Registry, each registered MCP server has a unique identifier stored in itsAgentRegistrymcpServerIdfield, formatted as a Uniform Resource Name (URN). You can see it in the output of thegcloud agent-registry mcp-servers listcommand in Step 3. The agent reconstructs this identifier from the environment variables: The URN encodes the project number of the registry (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"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 thegcloud agent-registry services createcommand (agentregistry:services:zoo-mcp-server).The agent lists the registered MCP servers withregistry.list_mcp_servers()and matches each server'smcpServerIdagainstMCP_SERVER_URN. It then passes the matching server's resourcename(projects/.../locations/.../mcpServers/...) toregistry.get_mcp_toolset(), which reads the endpoint URL from the server's registered interface (https://...run.app/mcp) and returns anMcpToolsetconnected 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.0get_mcp_toolset()prefixes the tool names with the server's display name, so the agent sees the tools aszoo_mcp_server_list_species,zoo_mcp_server_get_animals_by_species, andzoo_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
AgentRegistryAgentRegistryauthenticates its own API calls, such aslist_mcp_servers(), with ADC, so the header provider is only needed for the calls to the discovered MCP server. BecauseAgentRegistrycreates theMcpToolsetinternally, you pass the header provider to theAgentRegistryconstructor.In the previous step, the audience was theMCP_SERVER_URLvalue 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 whenAgentRegistryis created. This lets the agent create the provider first and bind it to the discovered endpoint afterget_mcp_toolset()returns. Thebind()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/mcppath: Thedef 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__call__method is the same as in the previous step, except that it raises an error if it is called beforebind(). - Run your agent to test the change. Use the
adk runcommand: or run ADK Web interface with theadk run zoo_agentadk webcommand: and then open the URL that the command prints in the browser.If you use Cloud Shell, add theadk web --port 8080--allow_originsargument to allow opening the Web interface from Cloud Shell: 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 asadk web --port 8080 --allow_origins="regex:https://.*\.cloudshell\.dev"zoo_mcp_server_list_speciesandzoo_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
- Download the updated
agent.pyfile 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.pyEnvironment 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_ENTERPRISEandGOOGLE_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_NUMBERis no longer needed (in addition toMCP_SERVER_URL, which was already removed in the previous step).
Shift from exact matching to semantic search
Instead of callingregistry.list_mcp_servers()during module initialization and comparing each entry against a constructedMCP_SERVER_URN,agent.pynow defines a discovery tool functiondiscover_zoo_mcp_serversthat callsregistry.search_mcp_servers()withsearch_type="SEMANTIC": This lets the agent search Agent Registry at runtime using a natural-language description of the capabilities it needs (for example,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, }"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.pycombines a customBaseToolsetimplementation (RegistryDiscoveredToolset) with a dynamic instruction provider (zoo_guide_instruction):- Dynamic toolset (
RegistryDiscoveredToolset): SubclassesBaseToolsetand setsself._use_invocation_cache = Falseso ADK re-evaluates available tools immediately afterdiscover_zoo_mcp_serversupdates the session state rather than caching an initially empty tool list. Whenget_tools()is called, it reads the discovered server names from state, builds an authenticatedMcpToolsetfor 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] - Dynamic agent instruction (
zoo_guide_instruction): Evaluates session state before each model turn. When no servers have been discovered yet, it appends_DISCOVERY_PROMPTinstructing the agent to calldiscover_zoo_mcp_serversfirst. 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_GoogleAuthHeadersandAgentRegistryfor each discovered server and binds the provider to that server's endpoint. The provider is the same as in the previous step, with a_boundflag that makes it fail if it is used beforebind().Retention and refresh logic
To avoid querying Agent Registry on every turn while still refreshing discovered endpoints periodically,agent.pyimplements 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_attimestamp are stored in session state under theapp:zoo_mcp_serverskey. Using ADK'sapp: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 exceedDISCOVERY_TTL_SECONDS(24 hours). While the record is fresh,RegistryDiscoveredToolsetreuses the cachedMcpToolsetconnections (self._toolsets),zoo_guide_instructionomits the discovery prompt, and any repeat call todiscover_zoo_mcp_serversimmediately returns"already_discovered". Once the 24-hour TTL expires,_fresh_discovery()returnsNone, causingRegistryDiscoveredToolset.get_tools()to close existing MCP client sessions (await self.close()) andzoo_guide_instructionto prompt the agent to perform a fresh discovery.
- Run your agent to test the semantic discovery workflow. Use the
adk runcommand: or run ADK Web interface with theadk run zoo_agentadk webcommand: and then open the URL that the command prints in the browser.If you use Cloud Shell, add theadk web --port 8080--allow_originsargument to allow opening the Web interface from Cloud Shell: When you ask the agent a question about the zoo (for example,adk web --port 8080 --allow_origins="regex:https://.*\.cloudshell\.dev"How many animals are in the zoo?), observe that it first invokesdiscover_zoo_mcp_servers, dynamically mounts the tools from the discovered MCP server, and then calls the zoo tools, such aszoo_mcp_server_list_speciesandzoo_mcp_server_get_animals_by_species, to answer your question.Ask a follow-up question, for exampleWhich is the oldest lion?. The agent answers using the already mounted zoo tools and doesn't calldiscover_zoo_mcp_serversagain, 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:
- 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 - Delete the Cloud Run MCP server service:
gcloud run services delete zoo-mcp-server \ --region=us-west1 \ --project="${GOOGLE_CLOUD_PROJECT}" \ --quiet - 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 - If the
cloud-run-source-deployArtifact 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 - 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):
- Registered an MCP server: Deployed a secured MCP server to Cloud Run and registered it in Agent Registry.
- Discovered an MCP server by identity: Resolved the MCP server endpoint from Agent Registry instead of hardcoding its URL.
- Discovered MCP servers semantically: Let the agent find and mount MCP tools at runtime using natural-language search, with retention and refresh.