Динамическое обнаружение MCP-серверов с помощью реестра агентов и ADK

1. Введение

В этой практической работе вы узнаете, как использовать Google Cloud Agent Registry и Agent Development Kit (ADK) для динамического обнаружения и вызова зарегистрированных серверов Model Context Protocol (MCP) во время выполнения, что устраняет необходимость в жестко закодированных конечных точках и конфигурациях инструментов.

В этой лабораторной работе для создания приложения используется ADK и язык программирования Python. Те же принципы можно применять в других языках программирования и фреймворках для разработки агентов.

Что вам предстоит сделать

  • Как включить и настроить Google Cloud Agent Registry в проекте
  • Разверните сервер MCP в Cloud Run и зарегистрируйте его в реестре агентов
  • Создайте агент ADK, который вызывает сервер MCP, используя жестко закодированный URL
  • Изменить агента, чтобы он находил MCP-сервер в реестре агентов по идентификатору
  • Измените агент, чтобы он обнаруживал серверы MCP во время выполнения с помощью семантического поиска

Что вам понадобится

  • современный браузер, например Chrome;
  • Проект Google Cloud с включенными платежными функциями.
  • Аккаунт с разрешениями на создание ресурсов и управление ими, а также на настройку политик IAM в облачном проекте Google Cloud.

2. Как настроить среду

Инструкции в этой практической работе предполагают, что вы выполняете команды в Cloud Shell в консоли Google Cloud. Cloud Shell – это среда командной строки, работающая в Google Cloud, в которой предварительно загружены необходимые инструменты. Если в вашей локальной среде установлены gcloud CLI, git и Python 3, вы также можете выполнять команды там.

Как создать проект Google Cloud

  1. В Google Cloud Console на странице выбора проекта создайте проект Google Cloud.
  2. Убедитесь, что для вашего облачного проекта включены платежные функции. Подробнее о том, как проверить, включены ли платежные функции для проекта…

Запустите терминал Cloud Shell

  1. С помощью инструмента выбора ресурсов выберите созданный проект Google Cloud.
  2. Нажмите Активировать Cloud Shell Значок "Активировать Cloud Shell" в верхней части консоли Google Cloud.
  3. После подключения к Cloud Shell проверьте аутентификацию:
    gcloud auth list
    
  4. Убедитесь, что проект настроен:
    gcloud config get project
    
  5. Если проект не задан или вы используете среду на локальном компьютере, а не Cloud Shell, настройте идентификатор проекта:
    export GOOGLE_CLOUD_PROJECT=<YOUR_PROJECT_ID>
    gcloud config set project "${GOOGLE_CLOUD_PROJECT}"
    

Выберите проект Google Cloud и задайте переменные среды

  1. Включите необходимые API Google Cloud:
    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. Создайте и зарегистрируйте MCP-сервер Zoo

На этом шаге вы создадите и зарегистрируете MCP-сервер Zoo. Сервер предоставляет три инструмента: list_species – список всех видов, живущих в зоопарке; get_animals_by_species – список животных определенного вида; get_animal_details – дополнительная информация об определенном животном.

Все файлы для этого шага можно найти на GitHub.

Создайте MCP-сервер

  1. Настройте папку и проект Python для MCP-сервера
    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 и зарегистрируйте его в реестре агентов

Затем вы создадите 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. Если в вашем проекте нет хранилища Artifact Registry для региона us-west1, вы увидите следующее сообщение:
    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. Зарегистрируйте 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 для использования сервера MCP Zoo

На этом шаге вы создадите агента ADK, который будет отвечать на запросы пользователей о зоопарке с помощью заданного в коде URL сервера MCP. Если вы зададите URL в коде, вам нужно будет знать URL сервера 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 для маршрутизации вызовов модели Gemini через Vertex AI в вашем облачном проекте и местоположении Google Cloud (global). Также хранит местоположение для реестра агентов, в котором зарегистрирован сервер MCP.
    • GOOGLE_CLOUD_PROJECT_NUMBER – не используется на этом шаге, но автоматически заполняется для использования на следующем.
    • MCP_SERVER_URL: прочитайте напрямую в zoo_agent/agent.py, чтобы настроить McpToolset с помощью заданного в коде URL Cloud Run MCP-сервера Zoo.
  4. Скачайте файл agent.py с кодом для агента Zoo, который подключается к MCP-серверу Zoo:
    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-сервере

Поскольку сервис Cloud Run zoo-mcp-server был развернут с флагами --invoker-iam-check и --no-allow-unauthenticated, каждый HTTP-запрос к MCP-серверу должен включать токен идентификации, подписанный Google, в заголовке Authorization. Токен должен идентифицировать вызывающего абонента, у которого есть разрешение run.routes.invoke в сервисе.

В zoo_agent/agent.py класс _GoogleAuthHeaders получает учетные данные токена идентификации одним из двух способов. Сначала он вызывает fetch_id_token_credentials(), чтобы получить токен идентификатора для URL сервиса от сервера метаданных или ключа сервисного аккаунта. Если это не удается, используется токен идентификатора Application Default Credentials (ADC) пользователя. При каждом вызове экземпляр обновляет токен, если это необходимо, и возвращает заголовок 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}"}

Агент передает экземпляр класса в McpToolset в качестве аргумента header_provider. 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 подключения включает путь /mcp, по которому сервер FastMCP обслуживает протокол 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 с помощью команды adk web:
    adk web --port 8080
    
    Затем откройте в браузере URL, который выведет команда.Если вы используете Cloud Shell, добавьте аргумент --allow_origins, чтобы разрешить открытие веб-интерфейса из Cloud Shell:
    adk web --port 8080 --allow_origins="regex:https://.*\.cloudshell\.dev"
    
    После выполнения команды найдите значок Web Preview (Предварительный просмотр)Значок &quot;Предварительный просмотр&quot; на панели инструментов Cloud Shell (справа). Нажмите на него и выберите Предварительный просмотр на порте 8080. Cloud Shell откроет новую вкладку браузера с веб-интерфейсом ADK.
  4. Начните чат с агентом. Например, спросите о животных в зоопарке:
    How many animals are in the zoo?
    
    Агент вызывает list_species, а затем get_animals_by_species для каждого вида, чтобы рассчитать ответ.

Веб-интерфейс ADK позволяет изучить, как работает агент, в том числе как он обращается к серверу MCP зоопарка. Подробные инструкции приведены в документации по веб-интерфейсу.

5. Динамически обнаруживать конечную точку MCP-сервера

На этом шаге вы измените агента, чтобы он обнаруживал конечную точку MCP-сервера зоопарка, зарегистрированную в реестре агентов, а не задавал ее URL в коде.

Все файлы для этого шага можно найти на GitHub.

Вы используете интеграцию ADK с реестром агентов, чтобы найти и получить URL сервера. Для реализации вам потребуется следующая информация:

  • Номер проекта и местоположение реестра агентов, в котором зарегистрирован ваш MCP-сервер (global).
  • Идентификатор сервиса, который вы использовали для регистрации MCP-сервера (zoo-mcp-server).

Обновите агент, чтобы использовать реестр агентов

  1. Скачайте обновленный файл agent.py с GitHub, чтобы перезаписать первую версию агента:
    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 – теперь требуется для создания URN идентификатора сервера MCP (MCP_SERVER_URN).
    • GOOGLE_CLOUD_PROJECT, GOOGLE_GENAI_USE_ENTERPRISE и GOOGLE_CLOUD_LOCATION используются ADK для настройки доступа к Vertex AI. Местоположение также используется для обнаружения MCP-сервера.
    • Больше не требуется. Переменная MCP_SERVER_URL больше не используется, потому что агент получает URL конечной точки MCP-сервера из реестра агентов, а не из среды.

    Как работает идентификация и обнаружение MCP-серверов в AgentRegistry

    В реестре агентов у каждого зарегистрированного MCP-сервера есть уникальный идентификатор, хранящийся в поле mcpServerId и отформатированный как унифицированное имя ресурса (URN). Его можно найти в выходных данных команды gcloud agent-registry mcp-servers list на шаге 3. Агент восстанавливает этот идентификатор из переменных среды:
    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 (agentregistry:services:zoo-mcp-server).Агент перечисляет зарегистрированные MCP-серверы с помощью registry.list_mcp_servers() и сопоставляет mcpServerId каждого сервера с MCP_SERVER_URN. Затем он передает ресурс соответствующего сервера name (projects/.../locations/.../mcpServers/...) в registry.get_mcp_toolset(), который считывает URL конечной точки из зарегистрированного интерфейса сервера (https://...run.app/mcp) и возвращает McpToolset, подключенный к этому 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() добавляет к названиям инструментов отображаемое название сервера, поэтому агент видит инструменты как zoo_mcp_server_list_species, zoo_mcp_server_get_animals_by_species и zoo_mcp_server_get_animal_details. Префикс предотвращает конфликты имен, когда агент использует инструменты с нескольких серверов MCP. Последняя строка увеличивает время ожидания подключения со значения по умолчанию (5 секунд), чтобы оставить место для холодных запусков Cloud Run.

    Как меняется авторизация с AgentRegistry

    AgentRegistry аутентифицирует собственные вызовы API, например list_mcp_servers(), с помощью ADC, поэтому поставщик заголовков нужен только для вызовов обнаруженного MCP-сервера. Поскольку AgentRegistry создает McpToolset внутри, вы передаете поставщика заголовков конструктору AgentRegistry.На предыдущем шаге аудиторией было значение MCP_SERVER_URL, которое вы знали заранее. Теперь URL становится известен только после обнаружения. ADK вызывает поставщика заголовков, когда агент отправляет запрос на сервер MCP, а не когда создается AgentRegistry. Это позволяет агенту сначала создать поставщика, а затем связать его с обнаруженной конечной точкой после возврата 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 с помощью команды adk web:
    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.

Это изменение позволило обойти необходимость явного указания URL MCP-сервера. Однако вам по-прежнему нужно знать номер проекта, местоположение реестра и идентификатор сервиса, используемый для регистрации MCP-сервера. Обнаружение выполняется статически во время инициализации агента.

6. Использовать семантический поиск для MCP-сервера в динамическом обнаружении

На этом шаге вы улучшите динамическое обнаружение сервера MCP, заменив точное сопоставление URN при запуске семантическим поиском во время выполнения. Этот подход устраняет необходимость знать номер проекта, в котором развернут сервер, или точный идентификатор сервера (MCP_SERVER_URN). Кроме того, он интегрирует обнаружение инструментов непосредственно в стандартный рабочий процесс агента, а не обрабатывает его при инициализации.

Все файлы для этого шага можно найти на GitHub.

Обновите агент, чтобы использовать семантический поиск во время выполнения

  1. Скачайте обновленный файл agent.py с GitHub, чтобы перезаписать существующую реализацию:
    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
    

    Переменные среды на этом шаге

    Поскольку агент больше не создает URN сервера MCP, необходимая конфигурация упрощается:
    • 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 теперь определяет функцию инструмента обнаружения discover_zoo_mcp_servers, которая вызывает registry.search_mcp_servers() с 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,
        }
    
    Это позволяет агенту выполнять поиск в Agent Registry во время выполнения, используя описание необходимых ему возможностей на естественном языке (например, "zoo animals, exhibits, habitat, care, feeding schedule"), и сохранять обнаруженные имена ресурсов сервера в состоянии сеанса 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() выполняет блокирующий ввод-вывод, поэтому _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. Использование префикса app: ADK позволяет ограничить область действия кешированной записи обнаружения временем работы приложения.
    • Проверка 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 с помощью команды adk web:
    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. Удалите проект

Если вы создали отдельный проект для этой практической работы, проще всего удалить его.

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

Вариант 2. Удалите отдельные ресурсы

Если вы использовали существующий проект и хотите его сохранить, удалите отдельные ресурсы, созданные в ходе этой практической работы:

  1. Удалите регистрацию MCP-сервера из реестра агентов. Поскольку вы зарегистрировали сервер вручную, удаление сервиса Cloud Run не отменяет регистрацию.
    gcloud agent-registry services delete zoo-mcp-server \
        --location=global \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --quiet
    
  2. Удалите сервис MCP-сервера Cloud Run:
    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. Если хранилище Artifact Registry cloud-run-source-deploy было создано специально для этой практической работы, удалите его, чтобы избежать платы за хранение образов контейнеров.
    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. Поздравляю!

В этой практической работе вы узнали, как динамически обнаруживать и использовать серверы MCP с помощью Google Cloud Agent Registry и Agent Development Kit (ADK):

  1. Зарегистрировали MCP-сервер. Развернули защищенный MCP-сервер в Cloud Run и зарегистрировали его в реестре агентов.
  2. Обнаружение MCP-сервера по идентификатору. Конечная точка MCP-сервера определяется по реестру агентов, а не по жестко заданному URL.
  3. Семантический поиск MCP-серверов. Агент может находить и подключать инструменты MCP во время выполнения, используя поиск на естественном языке, с сохранением и обновлением.

Дальнейшие действия