איתור דינמי של שרתי MCP באמצעות Agent Registry ו-ADK

1. מבוא

בשיעור ה-Codelab הזה תלמדו איך להשתמש ב-Google Cloud Agent Registry ובערכה לפיתוח סוכנים (ADK) כדי לגלות ולהפעיל באופן דינמי שרתי Model Context Protocol‏ (MCP) רשומים בזמן הריצה, בלי צורך בנקודות קצה ובהגדרות כלים בתוך הקוד.

בשיעור ה-Lab הזה משתמשים ב-ADK ובשפת התכנות Python כדי ליצור אפליקציה. אפשר ליישם את אותם עקרונות בשפות תכנות אחרות ובמסגרות אחרות לפיתוח נציגים.

הפעולות שתבצעו:

  • הפעלה והגדרה של Google Cloud Agent Registry בפרויקט
  • פריסת שרת MCP ב-Cloud Run ורישום שלו ב-Agent Registry
  • פיתוח סוכן ADK שקורא לשרת ה-MCP באמצעות כתובת URL בתוך הקוד
  • משנים את הסוכן כדי למצוא את שרת ה-MCP ב-Agent Registry לפי הזהות שלו
  • שינוי הסוכן כדי לגלות שרתי MCP בזמן הריצה באמצעות חיפוש סמנטי

הדרישות

  • דפדפן אינטרנט מודרני (כמו Chrome)
  • פרויקט ב-Google Cloud שהחיוב מופעל בו
  • חשבון עם הרשאות ליצור ולנהל משאבים ולהגדיר כללי מדיניות של IAM בפרויקט בענן של Google

2. הגדרת הסביבה

ההוראות ב-Codelab הזה מבוססות על ההנחה שאתם מריצים פקודות ב-Cloud Shell במסוף Google Cloud. סביבת Cloud Shell היא סביבה של שורת פקודה שפועלת ב-Google Cloud, וכוללת כלים נחוצים שנטענים מראש. אם התקנתם בסביבה המקומית את ה-CLI של gcloud, את git ואת Python 3, אתם יכולים להריץ את הפקודות גם שם.

יצירת פרויקט בענן של Google

  1. במסוף Google Cloud, בדף לבחירת הפרויקט, יוצרים פרויקט ב-Google Cloud.
  2. הקפידו לוודא שהחיוב מופעל בפרויקט שלכם ב-Cloud. כך בודקים אם החיוב מופעל בפרויקט

הפעלת מסוף Cloud Shell

  1. בבורר המשאבים, בוחרים את הפרויקט בענן של Google שיצרתם.
  2. לוחצים על Activate Cloud Shell הסמל של Activate 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. יצירה ורישום של שרת Zoo MCP

בשלב הזה יוצרים ורושמים שרת Zoo MCP. השרת חושף שלושה כלים: 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 ורישום ב-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 חדש. ההודעה הזו תופיע אם אין לכם מאגר 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 ב-Agent Registry במיקום "גלובלי":
    fastmcp inspect server.py --format mcp | jq 'del(..|nulls) | del(.tools[]?._meta?)' > toolspec.json
    MCP_URL=$(gcloud run services describe zoo-mcp-server --region=us-west1 --format='value(status.url)')
    gcloud agent-registry services create zoo-mcp-server \
        --display-name="zoo-mcp-server" \
        --project="${GOOGLE_CLOUD_PROJECT}" \
        --location=global \
        --mcp-server-spec-type="tool-spec" \
        --mcp-server-spec-content="toolspec.json" \
        --interfaces="url=${MCP_URL}/mcp,protocolBinding=jsonrpc"
    
    הפקודות האלה יוצרות את toolspec.json עם מניפסט הכלי וקוראות ל-Agent Registry API כדי לרשום את השרת.
  5. מוודאים שהסוכן מופיע במרשם:
    gcloud agent-registry mcp-servers list \
        --location=global \
        --filter="display_name=zoo-mcp-server"
    
    יופיע פלט YAML עם פרטי שרת ה-MCP, כמו בדוגמה הבאה:
    ---
    attributes:
      agentregistry.googleapis.com/system/RuntimeReference:
        uri: //agentregistry.googleapis.com/projects/[PROJECT_NUMBER]/locations/global/services/zoo-mcp-server
    createTime: '<create time datetime in ISO 8601>'
    displayName: zoo-mcp-server
    interfaces:
    - protocolBinding: JSONRPC
      url: https://zoo-mcp-server-[PROJECT_NUMBER].us-west1.run.app/mcp
    mcpServerId: urn:mcp:projects-[PROJECT_NUMBER]:projects:[PROJECT_NUMBER]:locations:global:agentregistry:services:zoo-mcp-server
    name: projects/[PROJECT_ID]/locations/global/mcpServers/agentregistry-00000000-0000-0000-1234-56789abcdef
    tools:
    - annotations:
        idempotentHint: true
        readOnlyHint: true
      description: |-
        Returns every species in the zoo. Use it first for zoo-wide questions
        such as counting all animals or listing everything the zoo has.
      name: list_species
    - annotations:
        idempotentHint: true
        readOnlyHint: true
      description: |-
        Retrieves all animals of a specific species from the zoo.
        Can also be used to collect the base data for aggregate queries
        of animals of a specific species - like counting the number of penguins
        or finding the oldest lion.
      name: get_animals_by_species
    - annotations:
        idempotentHint: true
        readOnlyHint: true
      description: Retrieves the details of a specific animal by its name.
      name: get_animal_details
    updateTime: '<last updated datetime in ISO 8601>'
    

4. יצירת סוכן ADK בסיסי לשימוש בשרת Zoo MCP

בשלב הזה, יוצרים סוכן ADK שיענה על בקשות של משתמשים לגבי גן החיות בעזרת כתובת ה-URL של שרת ה-MCP של גן החיות שהוגדרה בתוך הקוד. אם אתם משתמשים בכתובת URL בתוך הקוד, אתם צריכים לדעת את כתובת ה-URL של שרת ה-MCP לפני פריסת הסוכן.

קובצי המקור לשלב הזה זמינים ב-GitHub.

יצירת אפליקציית הסוכן

יוצרים פיגום של פרויקט ה-ADK ושל סוכן גן החיות שמשתמש בשרת ה-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 בפרויקט ובמיקום (global) ב-Google Cloud. הם גם מאחסנים את המיקום של Agent Registry שבו רשום שרת ה-MCP.
    • ‫GOOGLE_CLOUD_PROJECT_NUMBER: לא משתמשים בו בשלב הזה. הוא מאוכלס מראש לשימוש בשלב הבא.
    • ‫MCP_SERVER_URL: קורא ישירות ב-zoo_agent/agent.py כדי להגדיר את McpToolset עם כתובת ה-URL ב-Cloud Run בתוך הקוד של שרת Zoo MCP.
  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 שניות משאיר מקום להפעלות במצב התחלתי (cold start) של 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סמל של תצוגה מקדימה באינטרנט בסרגל הכלים של Cloud Shell (בצד ימין). לוחצים עליו ובוחרים באפשרות Preview on port 8080. ב-Cloud Shell תיפתח כרטיסייה חדשה בדפדפן שבה יוצג ממשק האינטרנט של ADK.
  4. מתחילים להתכתב בצ'אט עם הנציג. לדוגמה, אפשר לשאול על בעלי חיים בגן החיות:
    How many animals are in the zoo?
    
    הסוכן קורא לפונקציה list_species ואז לפונקציה get_animals_by_species לכל מין כדי לחשב את התשובה.

ממשק האינטרנט של ADK מאפשר לכם לבדוק איך הסוכן פועל, כולל הקריאות שלו לשרת MCP של Zoo. במאמרי העזרה של ממשק המשתמש באינטרנט מוסבר איך עושים את זה.

5. לגלות באופן דינמי את נקודת הקצה של שרת ה-MCP

בשלב הזה, אתם משנים את הסוכן כדי לגלות את נקודת הקצה של שרת ה-MCP של גן החיות שרשומה ב-Agent Registry, במקום להגדיר את כתובת ה-URL של נקודת הקצה באופן קבוע בקוד.

כל הקבצים לשלב הזה נמצאים ב-GitHub.

אתם משתמשים בשילוב של ADK עם Agent Registry כדי למצוא ולאחזר את כתובת ה-URL של השרת. כדי להטמיע את התכונה, אתם צריכים את הפרטים הבאים:

  • מספר הפרויקט והמיקום של Agent Registry שבו שרת ה-MCP רשום (global).
  • מזהה השירות שבו השתמשתם כדי לרשום את שרת ה-MCP‏ (zoo-mcp-server).

עדכון הסוכן לשימוש ב-Agent Registry

  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 מ-Agent Registry במקום לקרוא אותה מהסביבה.

    איך פועלים הזיהוי והגילוי של שרת MCP ב-AgentRegistry

    ב-Agent Registry, לכל שרת MCP רשום יש מזהה ייחודי שמאוחסן בשדה mcpServerId שלו, בפורמט של שם משאב אחיד (URN). אפשר לראות אותו בפלט של הפקודה gcloud agent-registry mcp-servers list בשלב 3. ה-agent משחזר את המזהה הזה ממשתני הסביבה:
    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 באופן פנימי, אתם מעבירים את ספק הכותרת ל-constructor של AgentRegistry.בשלב הקודם, הקהל היה הערך של MCP_SERVER_URL שידעתם מראש. עכשיו כתובת ה-URL ידועה רק אחרי הגילוי. ‫ADK קורא לספק הכותרות כשהסוכן שולח בקשה לשרת ה-MCP, ולא כשנוצר AgentRegistry. כך הסוכן יכול ליצור קודם את הספק ולקשר אותו לנקודת הקצה שזוהתה אחרי ש-get_mcp_toolset() מחזיר ערך. ה-method‏ bind() מקבלת את פרטי הכניסה באותו אופן כמו ה-constructor בשלב הקודם, כשהקהל נלקח מכתובת ה-URL של נקודת הקצה (endpoint) שזוהתה, בלי הנתיב /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).

    איך גילוי דינמי משולב בתהליך העבודה של Agentic Workflows

    כדי לטעון כלים משרתים שמתגלים בזמן הריצה ולהנחות את הנציג בתהליך הגילוי ואז ההפעלה, agent.py משלב הטמעה מותאמת אישית של BaseToolset (RegistryDiscoveredToolset) עם ספק הוראות דינמי (zoo_guide_instruction):
    1. ערכת כלים דינמית (RegistryDiscoveredToolset): מחלקת משנה BaseToolset ומגדירה את self._use_invocation_cache = False כך ש-ADK יעריך מחדש את הכלים הזמינים מיד אחרי ש-discover_zoo_mcp_servers יעדכן את מצב הסשן, במקום לשמור במטמון רשימת כלים ריקה בהתחלה. כשמפעילים את get_tools(), היא קוראת את שמות השרתים שזוהו מהמצב, יוצרת McpToolset מאומת לכל שרת ב-Thread עובד ומחזירה את כל הכלים הפעילים. אם לא מצליחים להגיע לשרת אחד, זה לא משפיע על השרתים האחרים. ‫_build_toolset() מבצע פעולות קלט/פלט חוסמות, ולכן _tools_of() מריץ אותו ב-thread עובד.
      class RegistryDiscoveredToolset(BaseToolset):
          def __init__(self, agent_registry: AgentRegistry):
              super().__init__()
              self._registry = agent_registry
              self._toolsets: dict[str, McpToolset] = {}
              self._use_invocation_cache = False
      
          def _build_toolset(self, server_name: str) -> McpToolset:
              header_provider = _GoogleAuthHeaders()
              server_registry = AgentRegistry(
                  project_id=self._registry.project_id,
                  location=self._registry.location,
                  header_provider=header_provider,
              )
              toolset = server_registry.get_mcp_toolset(server_name)
              header_provider.bind(toolset.connection_params.url)
              toolset.connection_params.timeout = 30.0
              return toolset
      
          async def _tools_of(self, server_name: str, ctx: ReadonlyContext) -> list[BaseTool]:
              try:
                  if server_name not in self._toolsets:
                      self._toolsets[server_name] = await asyncio.to_thread(
                          self._build_toolset, server_name
                      )
                  return await self._toolsets[server_name].get_tools_with_prefix(ctx)
              except Exception as e:
                  print(f"Skipping MCP server {server_name}: {e}")
                  return []
      
          async def get_tools(self, readonly_context: ReadonlyContext | None = None) -> list[BaseTool]:
              if not readonly_context:
                  return []
              record = _fresh_discovery(readonly_context.state)
              if not record or not record.get("servers"):
                  if self._toolsets:
                      await self.close()
                  return []
              per_server = await asyncio.gather(
                  *(self._tools_of(name, readonly_context) for name in record["servers"])
              )
              return [tool for tools in per_server for tool in tools]
      
    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 Registry בכל פעם, ועדיין לרענן את נקודות הקצה שזוהו מדי פעם, 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". אחרי שפג תוקף ה-TTL של 24 השעות, _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: מחיקת הפרויקט

אם יצרתם פרויקט ייעודי ל-Codelab הזה, הדרך הכי קלה לבטל את החיוב היא למחוק את הפרויקט:

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

אפשרות 2: מחיקת משאבים ספציפיים

אם השתמשתם בפרויקט קיים ואתם רוצים לשמור אותו, מחקו את המשאבים הספציפיים שיצרתם במהלך ה-Codelab הזה:

  1. מוחקים את הרישום של שרת ה-MCP מ-Agent Registry. מכיוון שרשמתם את השרת באופן ידני, מחיקת השירות של 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. אם המאגר cloud-run-source-deploy ב-Artifact Registry נוצר במיוחד במהלך ה-Codelab הזה, צריך למחוק אותו כדי להימנע מחיובים על אחסון קובצי אימג' של קונטיינרים:
    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 הזה למדתם איך לגלות שרתי MCP באופן דינמי ולהשתמש בהם באמצעות Google Cloud Agent Registry והערכה לפיתוח סוכנים (ADK):

  1. רשמתם שרת MCP: פרסתם שרת MCP מאובטח ב-Cloud Run ורשמתם אותו ב-Agent Registry.
  2. זיהוי שרת MCP לפי זהות: פתרון נקודת הקצה של שרת ה-MCP מ-Agent Registry במקום להגדיר את כתובת ה-URL שלו באופן קשיח.
  3. חיפוש סמנטי של שרתי MCP: הסוכן יכול למצוא ולהתקין כלי MCP בזמן הריצה באמצעות חיפוש בשפה טבעית, עם שמירה ורענון.

השלבים הבאים