Khám phá máy chủ MCP động bằng Sổ đăng ký tác nhân và ADK

1. Giới thiệu

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách sử dụng Google Cloud Agent Registry và Agent Development Kit (ADK) để tự động khám phá và gọi các máy chủ Giao thức ngữ cảnh mô hình (MCP) đã đăng ký trong thời gian chạy, loại bỏ các điểm cuối và cấu hình công cụ được mã hoá cứng.

Phòng thí nghiệm này sử dụng ADK và ngôn ngữ lập trình Python để xây dựng một ứng dụng. Bạn có thể áp dụng các nguyên tắc tương tự trong các ngôn ngữ lập trình khác và sử dụng các khung phát triển tác nhân khác.

Bạn sẽ thực hiện

  • Bật và định cấu hình Google Cloud Agent Registry trong dự án của bạn
  • Triển khai máy chủ MCP lên Cloud Run và đăng ký máy chủ đó trong Agent Registry
  • Tạo một tác nhân ADK gọi máy chủ MCP bằng URL được cố định giá trị trong mã
  • Thay đổi tác nhân để khám phá máy chủ MCP trong Sổ đăng ký tác nhân theo danh tính của máy chủ đó
  • Thay đổi tác nhân để khám phá máy chủ MCP trong thời gian chạy bằng tính năng tìm kiếm ngữ nghĩa

Bạn cần có

  • Một trình duyệt web hiện đại (chẳng hạn như Chrome)
  • Một dự án trên Google Cloud đã bật tính năng thanh toán
  • Một tài khoản có quyền tạo và quản lý tài nguyên, cũng như định cấu hình chính sách IAM trên dự án trên đám mây của Google

2. Thiết lập môi trường

Hướng dẫn trong lớp học lập trình này giả định rằng bạn chạy các lệnh trong Cloud Shell trong Google Cloud Console. Cloud Shell là một môi trường dòng lệnh chạy trong Google Cloud, được tải sẵn các công cụ cần thiết. Nếu đã cài đặt gcloud CLI, git và Python 3 trong môi trường cục bộ, bạn cũng có thể chạy các lệnh tại đó.

Tạo một dự án trên Google Cloud

  1. Trong Google Cloud Console, trên trang bộ chọn dự án, hãy tạo một dự án trên đám mây của Google Cloud.
  2. Đảm bảo rằng bạn đã bật tính năng thanh toán cho dự án trên đám mây của bạn. Tìm hiểu cách kiểm tra xem tính năng thanh toán có được bật trong dự án hay không.

Bắt đầu một cửa sổ dòng lệnh Cloud Shell

  1. Sử dụng bộ chọn tài nguyên để chọn dự án Google Cloud mà bạn đã tạo.
  2. Nhấp vào Kích hoạt Cloud Shell Biểu tượng Kích hoạt Cloud Shell ở đầu Google Cloud Console.
  3. Sau khi kết nối với Cloud Shell, hãy xác minh thông tin xác thực của bạn:
    gcloud auth list
    
  4. Xác nhận rằng dự án của bạn đã được định cấu hình:
    gcloud config get project
    
  5. Nếu dự án của bạn không được thiết lập như mong đợi hoặc nếu bạn sử dụng một môi trường trên máy cục bộ thay vì Cloud Shell, hãy thiết lập mã dự án:
    export GOOGLE_CLOUD_PROJECT=<YOUR_PROJECT_ID>
    gcloud config set project "${GOOGLE_CLOUD_PROJECT}"
    

Chọn một dự án trên đám mây của Google và đặt các biến môi trường

  1. Bật các API Google Cloud cần thiết:
    gcloud services enable \
      agentregistry.googleapis.com \
      aiplatform.googleapis.com \
      cloudbuild.googleapis.com \
      run.googleapis.com \
      --project="${GOOGLE_CLOUD_PROJECT}"
    

Thiết lập môi trường Python

  1. Tạo và kích hoạt môi trường ảo, đồng thời cài đặt các phần phụ thuộc bắt buộc:
    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. Tạo và đăng ký máy chủ MCP Zoo

Ở bước này, bạn sẽ tạo và đăng ký một máy chủ MCP Zoo. Máy chủ này cung cấp 3 công cụ: list_species – liệt kê tất cả các loài sống trong sở thú, get_animals_by_species – liệt kê các loài động vật thuộc một loài cụ thể và get_animal_details – lấy thêm thông tin về một loài động vật cụ thể.

Bạn có thể tìm thấy tất cả tệp cho bước này trên GitHub.

Tạo máy chủ MCP

  1. Thiết lập thư mục và dự án Python cho máy chủ 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. Tải mã máy chủ MCP xuống:
    curl -fsSL https://raw.githubusercontent.com/GoogleCloudPlatform/devrel-demos/main/codelabs/agent-registry-dynamic-discovery-with-adk/steps/step3/server.py -o server.py
    

Triển khai máy chủ MCP lên Cloud Run và đăng ký tại Agent Registry

Tiếp theo, bạn sẽ tạo Dockerfile cho máy chủ và triển khai máy chủ lên Cloud Run.

  1. Tạo 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. Tạo tài khoản dịch vụ được chỉ định cho máy chủ:
    gcloud iam service-accounts create zoo-mcp-server-sa \
        --display-name="Zoo MCP Server Service Account"
    
  3. Triển khai máy chủ đến khu vực 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
    
    Nếu bạn thấy lời nhắc sau, hãy xác nhận việc tạo kho lưu trữ Artifact Registry mới. Lời nhắc này sẽ xuất hiện nếu bạn không có kho lưu trữ Artifact Registry trong dự án cho khu vực 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. Đăng ký máy chủ MCP trong Sổ đăng ký tác nhân tại vị trí "global":
    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"
    
    Các lệnh này tạo toolspec.json bằng tệp kê khai công cụ và gọi API Sổ đăng ký tác nhân để đăng ký máy chủ.
  5. Xác minh rằng tác nhân xuất hiện trong sổ đăng ký:
    gcloud agent-registry mcp-servers list \
        --location=global \
        --filter="display_name=zoo-mcp-server"
    
    Bạn sẽ thấy kết quả YAML có thông tin về máy chủ MCP như sau:
    ---
    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. Tạo một tác nhân ADK cơ bản để sử dụng máy chủ Zoo MCP

Trong bước này, bạn sẽ tạo một tác nhân ADK để trả lời các yêu cầu của người dùng về sở thú bằng URL máy chủ MCP của Sở thú được cố định giá trị trong mã. Giá trị cố định trong mã buộc bạn phải biết URL của máy chủ MCP TRƯỚC KHI triển khai tác nhân.

Bạn có thể tìm thấy các tệp nguồn cho bước này trên GitHub.

Tạo ứng dụng tác nhân

Tạo một bản dựng khung của dự án ADK và tác nhân Zoo sử dụng máy chủ Zoo MCP.

  1. Thiết lập thư mục và dự án cho tác nhân 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. Tạo thư mục gói tác nhân và tải tệp __init__.py xuống:
    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. Tạo tệp .env và điền các biến môi trường được dùng trong bước này và các bước sau:
    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
    
    Lệnh này ghi tất cả biến môi trường cần thiết trong bước này và các bước khám phá tiếp theo vào zoo_agent/.env, vì vậy, bạn không cần tạo lại tệp sau này. Trong bước này, các biến sau đây sẽ được sử dụng:
    • GOOGLE_GENAI_USE_ENTERPRISE, GOOGLE_CLOUD_PROJECT và GOOGLE_CLOUD_LOCATION: Được ADK và SDK Google GenAI sử dụng ngầm để định tuyến các lệnh gọi mô hình Gemini thông qua Vertex AI trong dự án trên đám mây và vị trí Google Cloud của bạn (global). Biến này cũng lưu trữ vị trí cho Sổ đăng ký tác nhân nơi máy chủ MCP được đăng ký.
    • GOOGLE_CLOUD_PROJECT_NUMBER: Không được dùng trong bước này; được điền sẵn ở đây để dùng trong bước tiếp theo.
    • MCP_SERVER_URL: Đọc trực tiếp trong zoo_agent/agent.py để định cấu hình McpToolset bằng URL Cloud Run được cố định giá trị trong mã của máy chủ Zoo MCP.
  4. Tải agent.py xuống cùng với mã cho tác nhân Zoo kết nối với máy chủ 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
    

Cách hoạt động của quy trình uỷ quyền máy chủ MCP

Vì dịch vụ Cloud Run zoo-mcp-server được triển khai bằng cờ --invoker-iam-check và --no-allow-unauthenticated, nên mọi yêu cầu HTTP đến máy chủ MCP đều phải bao gồm một mã thông báo giá trị nhận dạng do Google ký trong tiêu đề Authorization. Mã thông báo phải xác định một phương thức gọi có quyền run.routes.invoke đối với dịch vụ.

Trong zoo_agent/agent.py, lớp _GoogleAuthHeaders lấy thông tin xác thực mã thông báo giá trị nhận dạng theo một trong hai cách. Trước tiên, ứng dụng gọi fetch_id_token_credentials() để lấy mã thông báo nhận dạng cho URL dịch vụ từ máy chủ siêu dữ liệu hoặc khoá tài khoản dịch vụ. Nếu không thành công, hệ thống sẽ sử dụng mã thông báo giá trị nhận dạng của Thông tin xác thực mặc định của ứng dụng (ADC) của người dùng. Mỗi khi được gọi, thực thể này sẽ làm mới mã thông báo nếu cần và trả về tiêu đề 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}"}

Tác nhân truyền một thực thể của lớp đến McpToolset dưới dạng đối số header_provider. ADK gọi nhà cung cấp tiêu đề và đính kèm các tiêu đề mà nhà cung cấp đó trả về cho mọi yêu cầu đến máy chủ MCP. Vì máy khách HTTP do ADK tạo, nên cùng một mã sẽ hoạt động bất kể phiên bản MCP Python SDK và thư viện HTTP của SDK đó được cài đặt:

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

Đối tượng được yêu cầu là URL dịch vụ (MCP_SERVER_URL), còn URL kết nối bao gồm đường dẫn /mcp nơi máy chủ FastMCP phân phát giao thức MCP. Thời gian chờ 30 giây giúp Cloud Run có thời gian khởi động nguội.

Kiểm thử tác nhân bằng sân chơi ADK trong môi trường cục bộ

Trước khi kiểm thử tác nhân, hãy đảm bảo rằng tài khoản người dùng của bạn có thể gọi máy chủ MCP.

  1. Chỉ dành cho thiết bị đầu cuối cục bộ (bỏ qua bước này trong Cloud Shell): khởi chạy ADC bằng thông tin xác thực người dùng của bạn:
    gcloud auth application-default login
    
    Làm theo hướng dẫn để hoàn tất quá trình khởi chạy ADC.
  2. (Không bắt buộc) Cấp quyền cho tài khoản người dùng của bạn để gọi máy chủ MCP từ xa:
    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. Chạy tác nhân bằng lệnh adk run:
    adk run zoo_agent
    
    hoặc chạy giao diện web ADK bằng lệnh adk web:
    adk web --port 8080
    
    rồi mở URL mà lệnh in trong trình duyệt.Nếu bạn sử dụng Cloud Shell, hãy thêm đối số --allow_origins để cho phép mở giao diện Web từ Cloud Shell:
    adk web --port 8080 --allow_origins="regex:https://.*\.cloudshell\.dev"
    
    Sau khi chạy lệnh, hãy tìm biểu tượng Web Preview (Xem trước trên web) Biểu tượng xem trước trên web trong thanh công cụ Cloud Shell (bên phải). Nhấp vào biểu tượng đó rồi chọn Preview on port 8080 (Xem trước trên cổng 8080). Cloud Shell sẽ mở một thẻ trình duyệt mới hiển thị giao diện web của ADK.
  4. Bắt đầu trò chuyện với tác nhân. Ví dụ: hỏi về các loài động vật trong sở thú:
    How many animals are in the zoo?
    
    Tác nhân gọi list_species rồi gọi get_animals_by_species cho từng loài để tính toán câu trả lời.

Giao diện web của ADK cho phép bạn khám phá cách hoạt động của tác nhân, bao gồm cả các lệnh gọi đến máy chủ MCP của Zoo. Hãy xem tài liệu về giao diện người dùng web để tìm hiểu cách thực hiện.

5. Tự động khám phá điểm cuối của máy chủ MCP

Trong bước này, bạn sẽ thay đổi tác nhân để khám phá điểm cuối của máy chủ MCP Zoo đã đăng ký trong Sổ đăng ký tác nhân thay vì cố định giá trị trong mã URL điểm cuối của máy chủ đó.

Bạn có thể tìm thấy tất cả tệp cho bước này trên GitHub.

Bạn sử dụng tính năng tích hợp ADK với Sổ đăng ký tác nhân để tìm và truy xuất URL của máy chủ. Để triển khai, bạn cần có những thông tin sau:

  • Số dự án và vị trí của Sổ đăng ký tác nhân nơi máy chủ MCP của bạn được đăng ký (global).
  • Mã dịch vụ mà bạn đã dùng để đăng ký máy chủ MCP (zoo-mcp-server).

Cập nhật tác nhân để sử dụng Sổ đăng ký tác nhân

  1. Tải tệp agent.py đã cập nhật xuống từ GitHub để ghi đè phiên bản đầu tiên của tác nhân:
    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
    

    Biến môi trường trong bước này

    Với bản cập nhật này, tác nhân sẽ dựa vào các biến môi trường sau đây của zoo_agent/.env:
    • GOOGLE_CLOUD_PROJECT_NUMBER: Hiện bắt buộc phải tạo URN nhận dạng của máy chủ MCP (MCP_SERVER_URN).
    • GOOGLE_CLOUD_PROJECT, GOOGLE_GENAI_USE_ENTERPRISE và GOOGLE_CLOUD_LOCATION: Được ADK dùng để định cấu hình quyền truy cập vào Vertex AI. Vị trí này cũng được dùng để tìm máy chủ MCP.
    • Không còn cần thiết: MCP_SERVER_URL không còn được sử dụng vì tác nhân truy xuất URL điểm cuối của máy chủ MCP từ Sổ đăng ký tác nhân thay vì đọc URL đó từ môi trường.

    Cách hoạt động của tính năng nhận dạng và khám phá máy chủ MCP trong AgentRegistry

    Trong Sổ đăng ký tác nhân, mỗi máy chủ MCP đã đăng ký đều có một giá trị nhận dạng riêng biệt được lưu trữ trong trường mcpServerId, được định dạng dưới dạng Tên tài nguyên thống nhất (URN). Bạn có thể xem mã này trong kết quả của lệnh gcloud agent-registry mcp-servers list ở Bước 3. Tác nhân sẽ tạo lại giá trị nhận dạng này từ các biến môi trường:
    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 mã hoá số dự án của sổ đăng ký (projects-{PROJECT_NUMBER}), số dự án và vị trí nơi máy chủ được đăng ký (projects:{PROJECT_NUMBER}:locations:{LOCATION}) và mã dịch vụ mà bạn đã sử dụng trong lệnh gcloud agent-registry services create (agentregistry:services:zoo-mcp-server).Tác nhân liệt kê các máy chủ MCP đã đăng ký bằng registry.list_mcp_servers() và so khớp mcpServerId của mỗi máy chủ với MCP_SERVER_URN. Sau đó, lớp này sẽ truyền tài nguyên của máy chủ phù hợp name (projects/.../locations/.../mcpServers/...) đến registry.get_mcp_toolset(). Lớp này sẽ đọc URL điểm cuối từ giao diện đã đăng ký của máy chủ (https://...run.app/mcp) và trả về một McpToolset được kết nối với 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() thêm tên hiển thị của máy chủ vào trước tên công cụ, vì vậy, tác nhân sẽ thấy các công cụ là zoo_mcp_server_list_species, zoo_mcp_server_get_animals_by_species và zoo_mcp_server_get_animal_details. Tiền tố này giúp ngăn chặn xung đột tên khi một tác nhân sử dụng các công cụ từ nhiều máy chủ MCP. Dòng cuối cùng tăng thời gian chờ kết nối từ giá trị mặc định là 5 giây để có thời gian cho quá trình khởi động nguội của Cloud Run.

    Cách thay đổi quy trình uỷ quyền bằng AgentRegistry

    AgentRegistry xác thực các lệnh gọi API của riêng mình, chẳng hạn như list_mcp_servers(), bằng ADC, vì vậy, nhà cung cấp tiêu đề chỉ cần thiết cho các lệnh gọi đến máy chủ MCP được phát hiện. Vì AgentRegistry tạo McpToolset trong nội bộ, nên bạn truyền trình cung cấp tiêu đề đến hàm khởi tạo AgentRegistry.Ở bước trước, đối tượng là giá trị MCP_SERVER_URL mà bạn đã biết trước. Giờ đây, URL chỉ được biết sau khi khám phá. ADK gọi nhà cung cấp tiêu đề khi tác nhân gửi yêu cầu đến máy chủ MCP, chứ không phải khi AgentRegistry được tạo. Điều này cho phép tác nhân tạo nhà cung cấp trước rồi liên kết nhà cung cấp đó với điểm cuối được phát hiện sau khi get_mcp_toolset() trả về. Phương thức bind() lấy thông tin xác thực theo cách tương tự như hàm khởi tạo trong bước trước, với đối tượng được lấy từ URL điểm cuối đã khám phá mà không có đường dẫn /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
    
    Phương thức __call__ giống như trong bước trước, ngoại trừ việc phương thức này sẽ báo lỗi nếu được gọi trước bind().
  2. Chạy tác nhân để kiểm tra thay đổi. Sử dụng lệnh adk run:
    adk run zoo_agent
    
    hoặc chạy giao diện web ADK bằng lệnh adk web:
    adk web --port 8080
    
    rồi mở URL mà lệnh in trong trình duyệt.Nếu bạn sử dụng Cloud Shell, hãy thêm đối số --allow_origins để cho phép mở giao diện Web từ Cloud Shell:
    adk web --port 8080 --allow_origins="regex:https://.*\.cloudshell\.dev"
    
    Hỏi tác nhân cùng một câu hỏi như ở bước trước. Câu trả lời vẫn như vậy, nhưng trong giao diện web của ADK, các lệnh gọi công cụ hiện xuất hiện dưới dạng zoo_mcp_server_list_species và zoo_mcp_server_get_animals_by_species.

Thay đổi này giúp bạn không cần phải có URL máy chủ MCP rõ ràng. Tuy nhiên, bạn vẫn cần biết số dự án, vị trí của sổ đăng ký và mã dịch vụ dùng để đăng ký máy chủ MCP. Ngoài ra, quá trình khám phá được thực hiện tĩnh trong quá trình khởi chạy tác nhân.

6. Sử dụng tính năng tìm kiếm ngữ nghĩa cho máy chủ MCP trong chế độ khám phá động

Trong bước này, bạn sẽ cải thiện khả năng khám phá động của máy chủ MCP bằng cách thay thế tính năng so khớp URN chính xác khi khởi động bằng tính năng tìm kiếm ngữ nghĩa trong thời gian chạy. Phương pháp này giúp bạn không cần biết số dự án nơi máy chủ được triển khai hoặc danh tính chính xác của máy chủ (MCP_SERVER_URN), đồng thời tích hợp tính năng khám phá công cụ trực tiếp vào quy trình thời gian chạy chuẩn của tác nhân thay vì xử lý trong quá trình khởi chạy.

Bạn có thể tìm thấy tất cả tệp cho bước này trên GitHub.

Cập nhật tác nhân để sử dụng tính năng khám phá ngữ nghĩa trong thời gian chạy

  1. Tải tệp agent.py đã cập nhật xuống từ GitHub để ghi đè cách triển khai hiện có:
    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
    

    Biến môi trường trong bước này

    Vì tác nhân không còn tạo URN của máy chủ MCP, nên cấu hình bắt buộc sẽ được đơn giản hoá hơn nữa:
    • GOOGLE_CLOUD_PROJECT, GOOGLE_GENAI_USE_ENTERPRISE và GOOGLE_CLOUD_LOCATION: Được ADK dùng để định cấu hình quyền truy cập vào Vertex AI. Vị trí cũng được dùng để khám phá máy chủ MCP.
    • Không còn cần thiết: GOOGLE_CLOUD_PROJECT_NUMBER không còn cần thiết nữa (ngoài MCP_SERVER_URL đã bị xoá ở bước trước).
    Thay vì gọi registry.list_mcp_servers() trong quá trình khởi chạy mô-đun và so sánh từng mục nhập với MCP_SERVER_URN đã tạo, agent.py hiện xác định một hàm công cụ khám phá discover_zoo_mcp_servers gọi registry.search_mcp_servers() bằng 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,
        }
    
    Việc này cho phép tác nhân tìm kiếm trong Sổ đăng ký tác nhân trong thời gian chạy bằng cách sử dụng nội dung mô tả bằng ngôn ngữ tự nhiên về các chức năng mà tác nhân cần (ví dụ: "zoo animals, exhibits, habitat, care, feeding schedule") và lưu tên tài nguyên máy chủ được phát hiện vào trạng thái phiên ADK (tool_context.state).

    Cách tích hợp tính năng khám phá linh hoạt vào quy trình làm việc của nhân viên hỗ trợ

    Để gắn các công cụ từ máy chủ được phát hiện trong thời gian chạy và hướng dẫn tác nhân thực hiện quy trình phát hiện rồi gọi, agent.py kết hợp một phương thức triển khai BaseToolset tuỳ chỉnh (RegistryDiscoveredToolset) với một trình cung cấp lệnh động (zoo_guide_instruction):
    1. Bộ công cụ động (RegistryDiscoveredToolset): Tạo lớp con BaseToolset và đặt self._use_invocation_cache = False để ADK đánh giá lại các công cụ có sẵn ngay sau khi discover_zoo_mcp_servers cập nhật trạng thái phiên thay vì lưu danh sách công cụ trống ban đầu vào bộ nhớ đệm. Khi được gọi, get_tools() sẽ đọc tên máy chủ được phát hiện từ trạng thái, tạo một McpToolset đã xác thực cho mỗi máy chủ trong một luồng worker và trả về tất cả công cụ đang hoạt động. Việc không kết nối được với một máy chủ sẽ không ảnh hưởng đến các máy chủ khác. _build_toolset() thực hiện chặn I/O, vì vậy, _tools_of() chạy nó trong một luồng worker.
      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. Chỉ dẫn linh động cho nhân viên hỗ trợ (zoo_guide_instruction): Đánh giá trạng thái phiên trước mỗi lượt của mô hình. Khi chưa phát hiện được máy chủ nào, hệ thống sẽ thêm _DISCOVERY_PROMPT để hướng dẫn tác nhân gọi discover_zoo_mcp_servers trước. Sau khi khám phá thành công, câu lệnh sẽ được cập nhật để liệt kê các máy chủ MCP đang hoạt động và hướng dẫn tác nhân sử dụng trực tiếp các công cụ sở thú đã gắn mà không cần chạy lại quy trình khám phá:
      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."
          )
      

    Uỷ quyền cho các máy chủ được phát hiện linh động

    Tính năng tìm kiếm ngữ nghĩa có thể trả về một số máy chủ và ADK gọi một nhà cung cấp tiêu đề mà không cho biết máy chủ nào nhận được yêu cầu. Do đó, _build_toolset() sẽ tạo một _GoogleAuthHeaders và AgentRegistry riêng cho từng máy chủ được phát hiện và liên kết nhà cung cấp với điểm cuối của máy chủ đó. Nhà cung cấp này giống như trong bước trước, có cờ _bound khiến nhà cung cấp này không hoạt động nếu được sử dụng trước bind().

    Logic giữ lại và làm mới

    Để tránh truy vấn Sổ đăng ký tác nhân ở mỗi lượt trong khi vẫn làm mới các điểm cuối được phát hiện theo định kỳ, agent.py triển khai việc giữ lại trạng thái bằng thời gian tồn tại (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
    
    • Giữ lại trong phạm vi ứng dụng: Tên máy chủ được phát hiện và dấu thời gian discovered_at được lưu trữ ở trạng thái phiên trong khoá app:zoo_mcp_servers. Việc sử dụng tiền tố app: của ADK sẽ đặt phạm vi cho bản ghi khám phá được lưu vào bộ nhớ đệm trong suốt quá trình chạy ứng dụng.
    • Xác thực TTL và tự động làm mới: _fresh_discovery() xác minh rằng bản ghi đã lưu tồn tại và tuổi của bản ghi không vượt quá DISCOVERY_TTL_SECONDS (24 giờ). Khi bản ghi còn mới, RegistryDiscoveredToolset sẽ sử dụng lại các kết nối McpToolset đã lưu vào bộ nhớ đệm (self._toolsets), zoo_guide_instruction bỏ qua lời nhắc khám phá và mọi lệnh gọi lặp lại đến discover_zoo_mcp_servers sẽ ngay lập tức trả về "already_discovered". Sau khi TTL 24 giờ hết hạn, _fresh_discovery() sẽ trả về None, khiến RegistryDiscoveredToolset.get_tools() đóng các phiên máy khách MCP hiện có (await self.close()) và zoo_guide_instruction nhắc tác nhân thực hiện một quy trình khám phá mới.
  2. Chạy tác nhân để kiểm thử quy trình khám phá ngữ nghĩa. Sử dụng lệnh adk run:
    adk run zoo_agent
    
    hoặc chạy giao diện web ADK bằng lệnh adk web:
    adk web --port 8080
    
    rồi mở URL mà lệnh in trong trình duyệt.Nếu bạn sử dụng Cloud Shell, hãy thêm đối số --allow_origins để cho phép mở giao diện Web từ Cloud Shell:
    adk web --port 8080 --allow_origins="regex:https://.*\.cloudshell\.dev"
    
    Khi bạn đặt câu hỏi cho tác nhân về sở thú (ví dụ: How many animals are in the zoo?), hãy quan sát thấy tác nhân này trước tiên gọi discover_zoo_mcp_servers, gắn động các công cụ từ máy chủ MCP được phát hiện, sau đó gọi các công cụ của sở thú, chẳng hạn như zoo_mcp_server_list_species và zoo_mcp_server_get_animals_by_species, để trả lời câu hỏi của bạn.Đặt một câu hỏi tiếp theo, ví dụ: Which is the oldest lion?. Tác nhân trả lời bằng các công cụ về sở thú đã được gắn và không gọi lại discover_zoo_mcp_servers, vì bản ghi khám phá trong trạng thái phiên vẫn còn mới.

7. Dọn dẹp

Cách 1: Xoá dự án

Nếu bạn đã tạo một dự án riêng cho lớp học lập trình này, thì cách dễ nhất để loại bỏ việc thanh toán là xoá dự án:

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

Cách 2: Xoá từng tài nguyên

Nếu bạn đã sử dụng một dự án hiện có và muốn giữ lại dự án đó, hãy xoá từng tài nguyên được tạo trong lớp học lập trình này:

  1. Xoá thông tin đăng ký máy chủ MCP khỏi Sổ đăng ký tác nhân. Vì bạn đã đăng ký máy chủ theo cách thủ công, nên việc xoá dịch vụ Cloud Run sẽ không xoá thông tin đăng ký:
    gcloud agent-registry services delete zoo-mcp-server \
        --location=global \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --quiet
    
  2. Xoá dịch vụ máy chủ MCP Cloud Run:
    gcloud run services delete zoo-mcp-server \
        --region=us-west1 \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --quiet
    
  3. Xoá tài khoản dịch vụ được tạo cho máy chủ MCP:
    gcloud iam service-accounts delete \
        "zoo-mcp-server-sa@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --quiet
    
  4. Nếu kho lưu trữ Artifact Registry cloud-run-source-deploy được tạo riêng trong lớp học lập trình này, hãy xoá kho lưu trữ đó để tránh bị tính phí lưu trữ hình ảnh vùng chứa:
    gcloud artifacts repositories delete cloud-run-source-deploy \
        --location=us-west1 \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --quiet
    
  5. Nếu muốn, bạn có thể xoá thư mục dự án cục bộ đã tạo trong Cloud Shell:
    cd ~ && rm -rf adk-dynamic-discovery
    

8. Xin chúc mừng

Trong lớp học lập trình này, bạn đã tìm hiểu cách khám phá và sử dụng máy chủ MCP một cách linh động bằng Sổ đăng ký tác nhân của Google Cloud và Bộ công cụ phát triển tác nhân (ADK):

  1. Đã đăng ký một máy chủ MCP: Triển khai một máy chủ MCP bảo mật cho Cloud Run và đăng ký máy chủ đó trong Agent Registry.
  2. Phát hiện máy chủ MCP theo danh tính: Phân giải điểm cuối của máy chủ MCP từ Sổ đăng ký tác nhân thay vì cố định giá trị trong mã URL của máy chủ đó.
  3. Khám phá máy chủ MCP theo ngữ nghĩa: Cho phép tác nhân tìm và gắn các công cụ MCP trong thời gian chạy bằng tính năng tìm kiếm bằng ngôn ngữ tự nhiên, có khả năng giữ lại và làm mới.

Các bước tiếp theo