使用 Agent Registry 和 ADK 動態探索 MCP 伺服器

1. 簡介

在本程式碼實驗室中,您將瞭解如何使用 Google Cloud Agent Registry 和 Agent Development Kit (ADK),在執行階段動態探索及叫用已註冊的 Model Context Protocol (MCP) 伺服器,不必硬式編碼端點和工具設定。

本實驗室會使用 ADK 和 Python 程式設計語言建構應用程式。同樣的原則也適用於其他程式設計語言和代理開發架構。

學習內容

  • 在專案中啟用及設定 Google Cloud Agent Registry
  • 將 MCP 伺服器部署至 Cloud Run,並在 Agent Registry 註冊
  • 建構 ADK 代理,使用硬式編碼網址呼叫 MCP 伺服器
  • 變更代理,以便在 Agent Registry 中依身分尋找 MCP 伺服器
  • 變更代理,在執行階段使用語意搜尋探索 MCP 伺服器

軟硬體需求

  • 新式網路瀏覽器 (例如 Chrome)
  • 已啟用計費功能的 Google Cloud 雲端專案
  • 具備建立及管理資源,以及設定 Google Cloud 專案 IAM 政策權限的帳戶

2. 設定環境

本程式碼實驗室的操作說明假設您在 Google Cloud 控制台的 Cloud Shell 中執行指令。Cloud Shell 是在 Google Cloud 中運作的指令列環境,已預先載入必要工具。如果本機環境已安裝 gcloud CLI、git 和 Python 3,也可以在本機環境執行指令。

建立 Google Cloud 專案

  1. 在 Google Cloud 控制台的專案選取器頁面中,建立 Google Cloud 專案。
  2. 確認 Cloud 專案已啟用計費功能。瞭解如何確認專案是否已啟用計費功能。

啟動 Cloud Shell 終端機

  1. 使用資源挑選器選取您建立的 Google Cloud 專案。
  2. 點選 Google Cloud 控制台頂端的「啟用 Cloud Shell」 圖示 「啟用 Cloud Shell」圖示。
  3. 連至 Cloud Shell 後,請驗證您的驗證狀態:
    gcloud auth list
    
  4. 確認專案已設定:
    gcloud config get project
    
  5. 如果專案未如預期設定,或您使用的是本機電腦上的環境,而非 Cloud Shell,請設定專案 ID:
    export GOOGLE_CLOUD_PROJECT=<YOUR_PROJECT_ID>
    gcloud config set project "${GOOGLE_CLOUD_PROJECT}"
    

選取 Google Cloud 雲端專案並設定環境變數

  1. 啟用必要的 Google Cloud API:
    gcloud services enable \
      agentregistry.googleapis.com \
      aiplatform.googleapis.com \
      cloudbuild.googleapis.com \
      run.googleapis.com \
      --project="${GOOGLE_CLOUD_PROJECT}"
    

設定 Python 環境

  1. 建立並啟用虛擬環境,然後安裝必要的依附元件:
    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. 建立及註冊 Zoo MCP 伺服器

在這個步驟中,您將建立並註冊 Zoo MCP 伺服器。伺服器會提供三項工具:list_species (列出動物園內的所有物種)、get_animals_by_species (列出特定物種的動物),以及 get_animal_details (取得特定動物的其他資訊)。

您可以在 GitHub 找到這個步驟的所有檔案。

建立 MCP 伺服器

  1. 為 MCP 伺服器設定資料夾和 Python 專案
    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. 下載 MCP 伺服器程式碼:
    curl -fsSL https://raw.githubusercontent.com/GoogleCloudPlatform/devrel-demos/main/codelabs/agent-registry-dynamic-discovery-with-adk/steps/step3/server.py -o server.py
    

將 MCP 伺服器部署至 Cloud Run,並在 Agent Registry 註冊

接著,您將為伺服器建立 Dockerfile,並將伺服器部署至 Cloud Run。

  1. 建立 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. 為伺服器建立指定服務帳戶:
    gcloud iam service-accounts create zoo-mcp-server-sa \
        --display-name="Zoo MCP Server Service Account"
    
  3. 將伺服器部署至 us-west1 區域。
    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
    
    如果看到下列提示,請確認建立新的 Artifact Registry 存放區。如果專案中沒有 us-west1 區域的 Artifact Registry 存放區,系統會顯示以下提示:
    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. 在 Agent Registry 的「global」位置註冊 MCP 伺服器:
    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"
    
    這些指令會產生含有工具資訊清單的 toolspec.json,並呼叫 Agent Registry API 來註冊伺服器。
  5. 確認代理程式顯示在登錄檔中:
    gcloud agent-registry mcp-servers list \
        --location=global \
        --filter="display_name=zoo-mcp-server"
    
    您會看到 YAML 輸出內容,其中包含 MCP 伺服器資訊,如下所示:
    ---
    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. 建立基本的 ADK 代理,以便使用 Zoo MCP 伺服器

在這個步驟中,您將建立 ADK 代理,並使用硬式編碼的動物園 MCP 伺服器網址,回答使用者對動物園的提問。硬式編碼會強制您在部署代理之前,先知道 MCP 伺服器的網址。

您可以在 GitHub 找到這個步驟的來源檔案。

建立代理應用程式

建立 ADK 專案和 Zoo 代理的鷹架,並使用 Zoo MCP 伺服器。

  1. 為 Zoo 代理設定資料夾和專案:
    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. 建立代理程式套件資料夾,並下載 __init__.py 檔案:
    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. 建立 .env 檔案,並填入這個步驟和後續步驟會用到的環境變數:
    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
    
    這個指令會將這個步驟和後續探索步驟所需的所有環境變數寫入 zoo_agent/.env,因此您之後不必重新建立檔案。這個步驟會使用下列變數:
    • GOOGLE_GENAI_USE_ENTERPRISE、GOOGLE_CLOUD_PROJECT 和 GOOGLE_CLOUD_LOCATION:ADK 和 Google GenAI SDK 會隱含使用這些變數,透過 Google 雲端專案和位置 (global) 中的 Vertex AI 轉送 Gemini 模型呼叫。此外,這些變數也會儲存 MCP 伺服器註冊的 Agent Registry 位置。
    • GOOGLE_CLOUD_PROJECT_NUMBER:這個步驟不會用到,此處預先填入是為了在下一個步驟使用。
    • MCP_SERVER_URL:直接在 zoo_agent/agent.py 中閱讀,使用 Zoo MCP 伺服器的硬式編碼 Cloud Run 網址設定 McpToolset。
  4. 下載 agent.py,其中包含連至動物園 MCP 伺服器的動物園代理程式碼:
    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
    

MCP 伺服器授權的運作方式

由於 zoo-mcp-server Cloud Run 服務部署時使用了 --invoker-iam-check 和 --no-allow-unauthenticated 旗標,因此傳送至 MCP 伺服器的每個 HTTP 要求,都必須在 Authorization 標頭中包含 Google 簽署的 ID 權杖。權杖必須識別在服務上具備 run.routes.invoke 權限的呼叫端。

在 zoo_agent/agent.py 中,_GoogleAuthHeaders 類別會透過下列兩種方式之一取得 ID 權杖憑證。首先,它會呼叫 fetch_id_token_credentials(),從中繼資料伺服器或服務帳戶金鑰取得服務網址的 ID 權杖。如果失敗,則會使用使用者應用程式預設憑證 (ADC) 的 ID 權杖。每次呼叫執行個體時,系統都會視需要重新整理權杖,並傳回 Authorization 標頭:

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}"}

代理會將類別的執行個體做為 header_provider 引數傳遞至 McpToolset。ADK 會呼叫標頭提供者,並將傳回的標頭附加至傳送給 MCP 伺服器的每個要求。由於 HTTP 用戶端是由 ADK 建立,因此無論安裝哪個版本的 MCP Python SDK 和其 HTTP 程式庫,相同的程式碼都能運作:

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

要求的目標對象是服務網址 (MCP_SERVER_URL),而連線網址則包含 FastMCP 伺服器提供 MCP 通訊協定的 /mcp 路徑。30 秒的逾時時間可讓 Cloud Run 有時間冷啟動。

在本機環境中使用 ADK 測試區測試代理

測試代理前,請確認您的使用者帳戶可以呼叫 MCP 伺服器。

  1. 僅限本機終端機 (在 Cloud Shell 中請略過這個步驟):使用使用者憑證初始化 ADC:
    gcloud auth application-default login
    
    按照操作說明完成 ADC 初始化。
  2. (選用) 授予使用者帳戶呼叫遠端 MCP 伺服器的權限:
    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. 使用 adk run 指令執行代理:
    adk run zoo_agent
    
    或使用 adk web 指令執行 ADK 網頁介面:
    adk web --port 8080
    
    然後在瀏覽器中開啟指令輸出的網址。如果您使用 Cloud Shell,請新增 --allow_origins 引數,允許從 Cloud Shell 開啟網頁介面:
    adk web --port 8080 --allow_origins="regex:https://.*\.cloudshell\.dev"
    
    執行指令後,在 Cloud Shell 工具列 (右側) 找到「網頁預覽」圖示 「網頁預覽」圖示。點選該圖示,然後選取「透過以下通訊埠預覽:8080」。Cloud Shell 會開啟新的瀏覽器分頁,顯示 ADK 網頁介面。
  4. 開始與代理對話。舉例來說,你可以詢問動物園裡的動物:
    How many animals are in the zoo?
    
    代理會呼叫 list_species,然後針對每個物種呼叫 get_animals_by_species,以計算答案。

您可以透過 ADK 網頁介面,瞭解代理的運作方式,包括對 Zoo MCP 伺服器的呼叫。如要瞭解操作方式,請參閱 Web UI 說明文件。

5. 動態探索 MCP 伺服器端點

在這個步驟中,您將變更代理,改為探索 Agent Registry 中註冊的動物園 MCP 伺服器端點,而非硬式編碼端點網址。

您可以在 GitHub 找到這個步驟的所有檔案。

您可以使用 ADK 與 Agent Registry 的整合功能,尋找及擷取伺服器的網址。如要導入,您需要下列資訊:

  • MCP 伺服器註冊的 Agent Registry 專案編號和位置 (global)。
  • 您用來註冊 MCP 伺服器的服務 ID (zoo-mcp-server)。

更新代理以使用代理登錄

  1. 從 GitHub 下載更新後的 agent.py 檔案,覆寫第一個版本的代理:
    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
    

    這個步驟中的環境變數

    本次更新後,代理程式會依賴 zoo_agent/.env 的下列環境變數:
    • GOOGLE_CLOUD_PROJECT_NUMBER:現在必須建構 MCP 伺服器的身分 URN (MCP_SERVER_URN)。
    • GOOGLE_CLOUD_PROJECT、GOOGLE_GENAI_USE_ENTERPRISE 和 GOOGLE_CLOUD_LOCATION:ADK 會使用這些變數設定 Vertex AI 存取權。位置也會用於尋找 MCP 伺服器。
    • 不再需要:代理會從 Agent Registry 擷取 MCP 伺服器端點網址,而非從環境讀取,因此不再使用 MCP_SERVER_URL。

    AgentRegistry 如何識別及探索 MCP 伺服器

    在代理程式登錄檔中,每個已註冊的 MCP 伺服器都有專屬 ID,儲存在 mcpServerId 欄位中,格式為統一資源名稱 (URN)。您可以在步驟 3 的 gcloud agent-registry mcp-servers list 指令輸出內容中查看。代理程式會從環境變數重新建構這個 ID:
    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"
    
    URN 會編碼註冊資料庫的專案編號 (projects-{PROJECT_NUMBER})、伺服器註冊的專案編號和位置 (projects:{PROJECT_NUMBER}:locations:{LOCATION}),以及您在 gcloud agent-registry services create 指令中使用的服務 ID (agentregistry:services:zoo-mcp-server)。代理會列出已註冊的 MCP 伺服器,並使用 registry.list_mcp_servers() 將每部伺服器的 mcpServerId 與 MCP_SERVER_URN 進行比對。接著,系統會將相符伺服器的資源 name (projects/.../locations/.../mcpServers/...) 傳遞至 registry.get_mcp_toolset(),後者會從伺服器的註冊介面 (https://...run.app/mcp) 讀取端點網址,並傳回連至該網址的 McpToolset:
    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() 會在工具名稱前加上伺服器的顯示名稱,因此代理會將工具視為 zoo_mcp_server_list_species、zoo_mcp_server_get_animals_by_species 和 zoo_mcp_server_get_animal_details。當代理使用多個 MCP 伺服器的工具時,前置字串可避免名稱衝突。最後一行將連線逾時時間從預設的 5 秒提高,為 Cloud Run 冷啟動預留空間。

    授權如何隨著AgentRegistry而改變

    AgentRegistry 會使用 ADC 驗證自己的 API 呼叫 (例如 list_mcp_servers()),因此只有在呼叫探索到的 MCP 伺服器時,才需要標頭提供者。由於 AgentRegistry 會在內部建立 McpToolset,因此您需要將標頭供應器傳遞至 AgentRegistry 建構函式。在上一個步驟中,目標對象是您預先知道的 MCP_SERVER_URL 值。現在,網址只有在探索後才會顯示。ADK 會在代理向 MCP 伺服器傳送要求時呼叫標頭提供者,而非在建立 AgentRegistry 時。這樣一來,代理程式就能先建立提供者,並在 get_mcp_toolset() 傳回後,將提供者繫結至探索到的端點。bind() 方法取得憑證的方式與上一個步驟中的建構函式相同,目標對象則取自探索到的端點網址,不含 /mcp 路徑:
        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
    
    __call__ 方法與上一個步驟相同,但如果在 bind() 之前呼叫,則會引發錯誤。
  2. 執行代理來測試變更。使用 adk run 指令:
    adk run zoo_agent
    
    或使用 adk web 指令執行 ADK 網頁介面:
    adk web --port 8080
    
    然後在瀏覽器中開啟指令輸出的網址。如果您使用 Cloud Shell,請新增 --allow_origins 引數,允許從 Cloud Shell 開啟網頁介面:
    adk web --port 8080 --allow_origins="regex:https://.*\.cloudshell\.dev"
    
    向代理提出與上一步相同的問題。答案相同,但在 ADK 網頁介面中,工具呼叫現在會顯示為 zoo_mcp_server_list_species 和 zoo_mcp_server_get_animals_by_species。

這項異動可讓您不必提供明確的 MCP 伺服器網址。不過,您仍須知道專案編號、儲存庫位置,以及用於註冊 MCP 伺服器的服務 ID,且探索作業會在代理初始化期間以靜態方式執行。

6. 在動態探索中,使用語意搜尋功能尋找 MCP 伺服器

在這個步驟中,您會將啟動時的 URN 精確比對,替換為執行階段語意搜尋,藉此改善 MCP 伺服器動態探索功能。這樣一來,您就不必知道伺服器部署的專案編號或伺服器的確切身分 (MCP_SERVER_URN),而且工具探索功能會直接整合至代理的標準執行階段工作流程,而非在初始化時處理。

您可以在 GitHub 找到這個步驟的所有檔案。

更新代理,在執行階段使用語意探索

  1. 從 GitHub 下載更新後的 agent.py 檔案,覆寫現有實作:
    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
    

    這個步驟中的環境變數

    由於代理不再建構 MCP 伺服器 URN,因此所需設定進一步簡化:
    • GOOGLE_CLOUD_PROJECT、GOOGLE_GENAI_USE_ENTERPRISE 和 GOOGLE_CLOUD_LOCATION:ADK 會使用這些變數設定 Vertex AI 存取權。位置也會用於尋找 MCP 伺服器。
    • 不再需要:不再需要 GOOGLE_CLOUD_PROJECT_NUMBER (除了上一步已移除的 MCP_SERVER_URL 之外)。
    agent.py 現在會定義探索工具函式 discover_zoo_mcp_servers,並使用 search_type="SEMANTIC" 呼叫 registry.search_mcp_servers(),而不是在模組初始化期間呼叫 registry.list_mcp_servers(),並將每個項目與建構的 MCP_SERVER_URN 進行比較:
    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"),搜尋 Agent Registry,並將找到的伺服器資源名稱儲存在 ADK 工作階段狀態 (tool_context.state)。

    如何將動態探索功能整合至代理工作流程

    為了在執行階段掛接從伺服器探索到的工具,並引導代理完成「探索後叫用」流程,agent.py 結合了自訂 BaseToolset 實作 (RegistryDiscoveredToolset) 和動態指令提供者 (zoo_guide_instruction):
    1. 動態工具集 (RegistryDiscoveredToolset):子類別 BaseToolset 並設定 self._use_invocation_cache = False,讓 ADK 在 discover_zoo_mcp_servers 更新工作階段狀態後立即重新評估可用工具,而不是快取一開始空白的工具清單。呼叫 get_tools() 時,系統會從狀態讀取探索到的伺服器名稱,在背景工作執行緒中為每個伺服器建構已驗證的 McpToolset,並傳回所有有效的工具。如果無法連線至某個伺服器,其他伺服器不會受到影響。_build_toolset() 會執行阻斷式 I/O,因此 _tools_of() 會在背景工作執行緒中執行。
      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. 動態代理指令 (zoo_guide_instruction):在每次模型對話前評估工作階段狀態。如果尚未探索到任何伺服器,系統會附加 _DISCOVERY_PROMPT,指示代理先呼叫 discover_zoo_mcp_servers。探索成功後,系統會更新提示,列出有效的 MCP 伺服器,並指示代理直接使用掛接的動物園工具,不必再次執行探索:
      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."
          )
      

    動態探索伺服器的授權

    語意搜尋可能會傳回多個伺服器,而 ADK 呼叫標頭提供者時,不會告知要求傳送至哪個伺服器。因此,_build_toolset() 會為每個探索到的伺服器建立專屬的 _GoogleAuthHeaders 和 AgentRegistry,並將供應商繫結至該伺服器的端點。供應商與上一個步驟相同,並含有 _bound 旗標,如果在 bind() 之前使用,就會失敗。

    保留和重新整理邏輯

    為避免每次都查詢代理程式註冊資料庫,同時定期重新整理探索到的端點,agent.py 會實作狀態保留機制,並設定存留時間 (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
    
    • 應用程式範圍保留:探索到的伺服器名稱和 discovered_at 時間戳記會儲存在 app:zoo_mcp_servers 鍵下的工作階段狀態中。使用 ADK 的 app: 前置字元,可在應用程式執行期間限定快取探索記錄的範圍。
    • TTL 驗證和自動重新整理:_fresh_discovery() 會驗證儲存的記錄是否存在,且存在時間不超過 DISCOVERY_TTL_SECONDS (24 小時)。當記錄為最新狀態時,RegistryDiscoveredToolset 會重複使用快取的 McpToolset 連線 (self._toolsets),zoo_guide_instruction 會省略探索提示,且任何對 discover_zoo_mcp_servers 的重複呼叫都會立即傳回 "already_discovered"。24 小時的 TTL 到期後,_fresh_discovery() 會傳回 None,導致 RegistryDiscoveredToolset.get_tools() 關閉現有的 MCP 用戶端工作階段 (await self.close()),並由 zoo_guide_instruction 提示代理執行新的探索。
  2. 執行代理,測試語意探索工作流程。使用 adk run 指令:
    adk run zoo_agent
    
    或使用 adk web 指令執行 ADK 網頁介面:
    adk web --port 8080
    
    然後在瀏覽器中開啟指令輸出的網址。如果您使用 Cloud Shell,請新增 --allow_origins 引數,允許從 Cloud Shell 開啟網頁介面:
    adk web --port 8080 --allow_origins="regex:https://.*\.cloudshell\.dev"
    
    向代理詢問動物園相關問題 (例如 How many animals are in the zoo?) 時,請觀察代理是否先叫用 discover_zoo_mcp_servers,動態掛接探索到的 MCP 伺服器工具,然後呼叫動物園工具 (例如 zoo_mcp_server_list_species 和 zoo_mcp_server_get_animals_by_species) 來回答問題。接著,提出後續問題,例如 Which is the oldest lion?。代理會使用已掛接的動物園工具回答問題,不會再次呼叫 discover_zoo_mcp_servers,因為工作階段狀態中的探索記錄仍是最新狀態。

7. 清除

方法 1:刪除專案

如果您為這個程式碼實驗室建立了專屬專案,最簡單的避免計費方式就是刪除專案:

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

選項 2:刪除個別資源

如果您使用的是現有專案,且想保留該專案,請刪除在本程式碼實驗室中建立的個別資源:

  1. 從 Agent Registry 刪除 MCP 伺服器註冊。由於伺服器是手動註冊,刪除 Cloud Run 服務不會移除註冊資訊:
    gcloud agent-registry services delete zoo-mcp-server \
        --location=global \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --quiet
    
  2. 刪除 Cloud Run MCP 伺服器服務:
    gcloud run services delete zoo-mcp-server \
        --region=us-west1 \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --quiet
    
  3. 刪除為 MCP 伺服器建立的服務帳戶:
    gcloud iam service-accounts delete \
        "zoo-mcp-server-sa@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --quiet
    
  4. 如果 cloud-run-source-deploy Artifact Registry 存放區是專為這個程式碼實驗室建立,請刪除該存放區,以免產生容器映像檔儲存費用:
    gcloud artifacts repositories delete cloud-run-source-deploy \
        --location=us-west1 \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --quiet
    
  5. (選用) 移除在 Cloud Shell 中建立的本機專案目錄:
    cd ~ && rm -rf adk-dynamic-discovery
    

8. 恭喜

在本程式碼實驗室,您學會如何使用 Google Cloud Agent Registry 和 Agent Development Kit (ADK),動態探索及使用 MCP 伺服器:

  1. 註冊 MCP 伺服器:將安全的 MCP 伺服器部署至 Cloud Run,並在 Agent Registry 中註冊。
  2. 依身分探索 MCP 伺服器:從 Agent Registry 解析 MCP 伺服器端點,而非對網址進行硬式編碼。
  3. 透過語意搜尋探索 MCP 伺服器:讓代理在執行階段使用自然語言搜尋,尋找並掛接 MCP 工具,並保留及重新整理。

後續步驟