Agent Registry と ADK を使用した動的な MCP サーバー検出

1. はじめに

この Codelab では、Google Cloud Agent Registry と Agent Development Kit(ADK)を使用して、登録済みの Model Context Protocol(MCP)サーバーを実行時に動的に検出して呼び出す方法を学びます。これにより、エンドポイントとツール構成をハードコードする必要がなくなります。

このラボでは、ADK と Python プログラミング言語を使用してアプリケーションを構築します。同じ原則を他のプログラミング言語や他のエージェント開発フレームワークにも適用できます。

演習内容

  • Google Cloud Agent Registry を有効にしてプロジェクトで構成する
  • MCP サーバーを Cloud Run にデプロイして Agent Registry に登録する
  • ハードコードされた URL を使用して MCP サーバーを呼び出す ADK エージェントを構築する
  • エージェントを変更して、Agent Registry で MCP サーバーを ID で検出する
  • セマンティック検索を使用して実行時に MCP サーバーを検出するようにエージェントを変更する

必要なもの

  • 最新のウェブブラウザ(Chrome など)
  • 課金を有効にした Google Cloud プロジェクト
  • Google Cloud プロジェクトでリソースを作成および管理し、IAM ポリシーを構成する権限を持つアカウント

2. 環境を設定する

この Codelab の手順では、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 APIs を有効にします。
    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 サーバーを作成して登録します。このサーバーは、3 つのツールを公開しています。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. 「global」ロケーションの Agent Registry に 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"
    
    次のような MCP サーバー情報を含む YAML 出力が表示されます。
    ---
    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. Zoo MCP サーバーを使用する基本的な ADK エージェントを作成する

このステップでは、ハードコードされた動物園の MCP サーバー URL を使用して、動物園に関するユーザーのリクエストに回答する ADK エージェントを作成します。ハードコードすると、エージェントがデプロイされる前に MCP サーバーの URL を把握する必要があります。

このステップのソースファイルは GitHub で入手できます。

エージェント アプリケーションを作成する

ADK プロジェクトと、Zoo MCP サーバーを使用する Zoo エージェントのスキャフォールドを作成します。

  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 Cloud プロジェクトとロケーション(global)の Vertex AI を介して Gemini モデル呼び出しをルーティングするために暗黙的に使用します。また、MCP サーバーが登録されている Agent Registry のロケーションも保存します。
    • GOOGLE_CLOUD_PROJECT_NUMBER: このステップでは使用されません。次のステップで使用するために、ここで事前入力されています。
    • MCP_SERVER_URL: zoo_agent/agent.py で直接読み取り、Zoo MCP サーバーのハードコードされた Cloud Run URL を使用して McpToolset を構成します。
  4. Zoo MCP サーバーに接続する Zoo エージェントのコードを含む agent.py をダウンロードします。
    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 クラスは 2 つの方法のいずれかで ID トークン認証情報を取得します。まず、fetch_id_token_credentials() を呼び出して、メタデータ サーバーまたはサービス アカウント キーからサービス URL の 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),
)

リクエストされたオーディエンスはサービス URL(MCP_SERVER_URL)ですが、接続 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
    
    コマンドによって出力された URL をブラウザで開きます。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 ウェブ インターフェースを使用すると、動物園 MCP サーバーへの呼び出しなど、エージェントの動作を確認できます。方法については、ウェブ UI のドキュメントをご覧ください。

5. MCP サーバー エンドポイントを動的に検出する

このステップでは、エンドポイント URL をハードコードする代わりに、Agent Registry に登録されている Zoo MCP サーバー エンドポイントを検出するようにエージェントを変更します。

このステップのすべてのファイルは GitHub で確認できます。

ADK と Agent Registry の統合を使用して、サーバーの URL を検索して取得します。実装するには、次の情報が必要です。

  • MCP サーバーが登録されている Agent Registry のプロジェクト番号とロケーション(global)。
  • MCP サーバーの登録に使用したサービス ID(zoo-mcp-server)。

Agent Registry を使用するようにエージェントを更新する

  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 サーバーの ID URN(MCP_SERVER_URN)を構築するために必要になりました。
    • GOOGLE_CLOUD_PROJECT、GOOGLE_GENAI_USE_ENTERPRISE、GOOGLE_CLOUD_LOCATION: ADK が Vertex AI アクセスを構成するために使用します。この場所は、MCP サーバーの検出にも使用されます。
    • 不要: エージェントは環境から読み取るのではなく、Agent Registry から MCP サーバー エンドポイント URL を取得するため、MCP_SERVER_URL は使用されなくなりました。

    AgentRegistry での MCP サーバーの ID と検出の仕組み

    Agent Registry では、登録された各 MCP サーバーに、mcpServerId フィールドに保存された固有識別子があり、Uniform Resource Name(URN)としてフォーマットされています。これは、ステップ 3 の gcloud agent-registry mcp-servers list コマンドの出力で確認できます。エージェントは、環境変数からこの識別子を再構築します。
    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)をエンコードします。エージェントは、registry.list_mcp_servers() を使用して登録済みの MCP サーバーを一覧表示し、各サーバーの mcpServerId を MCP_SERVER_URN と照合します。次に、一致するサーバーのリソース name(projects/.../locations/.../mcpServers/...)を registry.get_mcp_toolset() に渡します。registry.get_mcp_toolset() は、サーバーの登録済みインターフェース(https://...run.app/mcp)からエンドポイント URL を読み取り、その URL に接続された 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 サーバーのツールを使用する際の名前の競合を防ぐことができます。最後の行では、Cloud Run のコールド スタートに備えて、接続タイムアウトをデフォルトの 5 秒から引き上げています。

    AgentRegistry による認可の変更

    AgentRegistry は、list_mcp_servers() などの独自の API 呼び出しを ADC で認証するため、ヘッダー プロバイダは検出された MCP サーバーへの呼び出しにのみ必要です。AgentRegistry は内部で McpToolset を作成するため、ヘッダー プロバイダを AgentRegistry コンストラクタに渡します。前のステップでは、オーディエンスは事前にわかっていた MCP_SERVER_URL 値でした。URL は検出後にのみ認識されるようになりました。ADK は、AgentRegistry の作成時ではなく、エージェントが MCP サーバーにリクエストを送信するときにヘッダー プロバイダを呼び出します。これにより、エージェントは最初にプロバイダを作成し、get_mcp_toolset() が戻った後に検出されたエンドポイントにバインドできます。bind() メソッドは、前のステップのコンストラクタと同じ方法で認証情報を取得します。オーディエンスは、検出されたエンドポイント URL から /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
    
    コマンドによって出力された URL をブラウザで開きます。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 サーバー URL が不要になりました。ただし、MCP サーバーの登録に使用されるプロジェクト番号、レジストリのロケーション、サービス ID を把握しておく必要があります。検出は、エージェントの初期化中に静的に行われます。

6. 動的検出で MCP サーバーのセマンティック検索を使用する

このステップでは、起動時の完全な URN マッチングをランタイム セマンティック検索に置き換えることで、MCP サーバーの動的検出を改善します。このアプローチでは、サーバーがデプロイされているプロジェクト番号やサーバーの正確な ID(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 は前のステップですでに削除されています)。
    モジュールの初期化中に registry.list_mcp_servers() を呼び出して、各エントリを構築された MCP_SERVER_URN と比較する代わりに、agent.py は search_type="SEMANTIC" を指定して registry.search_mcp_servers() を呼び出す検出ツール関数 discover_zoo_mcp_servers を定義するようになりました。
    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 を構築し、すべてのアクティブなツールを返します。1 つのサーバーに到達できなくても、他のサーバーには影響しません。_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 を作成し、プロバイダをそのサーバーのエンドポイントにバインドします。プロバイダは前のステップと同じですが、bind() より前に使用されると失敗する _bound フラグが追加されています。

    保持と更新のロジック

    agent.py は、検出されたエンドポイントを定期的に更新しながら、すべてのターンで Agent Registry にクエリを実行することを回避するために、有効期間(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
    
    コマンドによって出力された URL をブラウザで開きます。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: プロジェクトを削除する

この Codelab 専用のプロジェクトを作成した場合、課金を停止する最も簡単な方法は、プロジェクトを削除することです。

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

オプション 2: 個別のリソースを削除する

既存のプロジェクトを使用して、それを保持する場合は、この Codelab で作成した個々のリソースを削除します。

  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. この Codelab の実施中に 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. 完了

この Codelab では、Google Cloud Agent Registry と Agent Development Kit(ADK)を使用して MCP サーバーを動的に検出して使用する方法を学びました。

  1. MCP サーバーを登録した: 安全な MCP サーバーを Cloud Run にデプロイし、Agent Registry に登録しました。
  2. ID で MCP サーバーを検出: URL をハードコードする代わりに、Agent Registry から MCP サーバー エンドポイントを解決しました。
  3. MCP サーバーをセマンティックに検出: エージェントが自然言語検索を使用して、実行時に MCP ツールを検出してマウントできるようにします。保持と更新も可能です。

次のステップ