Cómo implementar un servidor de MCP seguro en Cloud Run

1. Introducción

Descripción general

En este lab, compilarás e implementarás un servidor de Model Context Protocol (MCP). Los servidores de MCP son útiles para proporcionar a los LLM acceso a herramientas y servicios externos. Lo configurarás como un servicio seguro y listo para producción en Cloud Run al que se puede acceder desde varios clientes. Luego, te conectarás al servidor de MCP remoto desde la CLI de Gemini.

Actividades

Usaremos FastMCP para crear un servidor de MCP de zoológico que tenga dos herramientas: get_animals_by_species y get_animal_details. FastMCP proporciona una forma rápida y pythonica de compilar servidores y clientes de MCP.

Gráfico del servidor de MCP del zoológico

Qué aprenderás

  • Implementa el servidor de MCP en Cloud Run.
  • Protege el extremo de tu servidor exigiendo la autenticación para todas las solicitudes, lo que garantiza que solo los clientes y agentes autorizados puedan comunicarse con él.
  • Conéctate al extremo de tu servidor de MCP seguro desde Gemini CLI

2. Configuración del proyecto

  1. Si aún no tienes una Cuenta de Google, debes crear una.
    • Usa una cuenta personal en lugar de una cuenta de trabajo o institución educativa. Es posible que las cuentas de trabajo y de instituciones educativas tengan restricciones que te impidan habilitar las APIs necesarias para este lab.
  2. Accede a la consola de Google Cloud.
  3. Habilita la facturación en la consola de Cloud.
    • Completar este lab debería costar menos de USD 1 en recursos de Cloud.
    • Puedes seguir los pasos al final de este lab para borrar recursos y evitar cargos adicionales.
    • Los usuarios nuevos pueden acceder a la prueba gratuita de USD 300.
  4. Crea un proyecto nuevo o elige reutilizar uno existente.

3. Abre el editor de Cloud Shell

  1. Haz clic en este vínculo para navegar directamente al editor de Cloud Shell.
  2. Si se te solicita autorización en algún momento, haz clic en Autorizar para continuar. Haz clic para autorizar Cloud Shell
  3. Si la terminal no aparece en la parte inferior de la pantalla, ábrela:
    • Haz clic en Ver.
    • Haz clic en Terminal.Abre una terminal nueva en el editor de Cloud Shell
  4. En la terminal, configura tu proyecto con este comando:
    • Formato:
      gcloud config set project [PROJECT_ID]
      
    • Ejemplo:
      gcloud config set project lab-project-id-example
      
    • Si no recuerdas el ID de tu proyecto, haz lo siguiente:
      • Puedes enumerar todos los IDs de tus proyectos con el siguiente comando:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
        
      Establece el ID del proyecto en la terminal del editor de Cloud Shell
  5. Deberías ver el siguiente mensaje:
    Updated property [core/project].
    
    Si ves un WARNING y se te pregunta Do you want to continue (Y/n)?, es probable que hayas ingresado el ID del proyecto de forma incorrecta. Presiona n, presiona Enter y vuelve a intentar ejecutar el comando gcloud config set project.

4. Habilita las APIs

En la terminal, habilita las APIs:

gcloud services enable \
  run.googleapis.com \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com

Si se te solicita autorización, haz clic en Autorizar para continuar. Haz clic para autorizar Cloud Shell

Este comando puede tardar unos minutos en completarse, pero, finalmente, debería producir un mensaje de éxito similar a este:

Operation "operations/acf.p2-73d90d00-47ee-447a-b600" finished successfully.

5. Prepara tu proyecto de Python

  1. Crea una carpeta llamada mcp-on-cloudrun para almacenar el código fuente de la implementación:
      mkdir mcp-on-cloudrun && cd mcp-on-cloudrun
    
  2. Crea un proyecto de Python con la herramienta uv para generar un archivo pyproject.toml:
      uv init --description "Example of deploying an MCP server on Cloud Run" --bare --python 3.13
    
    El comando uv init crea un archivo pyproject.toml para tu proyecto.Para ver el contenido del archivo, ejecuta el siguiente comando:
    cat pyproject.toml
    
    El resultado debería ser similar al siguiente:
    [project]
    name = "mcp-on-cloudrun"
    version = "0.1.0"
    description = "Example of deploying an MCP server on Cloud Run"
    requires-python = ">=3.13"
    dependencies = []
    

6. Crea el servidor de MCP del zoo

Para proporcionar un contexto valioso que permita mejorar el uso de LLM con MCP, configura un servidor de MCP de zoo con FastMCP, un framework estándar para trabajar con el Protocolo de contexto del modelo. FastMCP proporciona una forma rápida de compilar servidores y clientes de MCP con Python. Este servidor de MCP proporciona datos sobre los animales de un zoológico ficticio. Para simplificar, almacenamos los datos en la memoria. Para un servidor de MCP de producción, probablemente quieras proporcionar datos de fuentes como bases de datos o APIs.

  1. Ejecuta el siguiente comando para agregar FastMCP como dependencia en el archivo pyproject.toml:
    uv add fastmcp==2.11.1 --no-sync
    
    Esto agregará un archivo uv.lock a tu proyecto.
  2. Crea y abre un nuevo archivo server.py para el código fuente del servidor de MCP:
    cloudshell edit server.py
    
    El comando cloudshell edit abrirá el archivo server.py en el editor que se encuentra sobre la terminal.
  3. Agrega el siguiente código fuente del servidor de MCP del zoo al archivo server.py:
    import asyncio
    import logging
    import os
    from typing import List, Dict, Any
    
    from fastmcp import FastMCP
    
    logger = logging.getLogger(__name__)
    logging.basicConfig(format="[%(levelname)s]: %(message)s", level=logging.INFO)
    
    mcp = FastMCP("Zoo Animal MCP Server 🦁🐧🐻")
    
    # Dictionary of animals at the zoo
    ZOO_ANIMALS = [
        {
            "species": "lion",
            "name": "Leo",
            "age": 7,
            "enclosure": "The Big Cat Plains",
            "trail": "Savannah Heights"
        },
        {
            "species": "lion",
            "name": "Nala",
            "age": 6,
            "enclosure": "The Big Cat Plains",
            "trail": "Savannah Heights"
        },
        {
            "species": "lion",
            "name": "Simba",
            "age": 3,
            "enclosure": "The Big Cat Plains",
            "trail": "Savannah Heights"
        },
        {
            "species": "lion",
            "name": "King",
            "age": 8,
            "enclosure": "The Big Cat Plains",
            "trail": "Savannah Heights"
        },
        {
            "species": "penguin",
            "name": "Waddles",
            "age": 2,
            "enclosure": "The Arctic Exhibit",
            "trail": "Polar Path"
        },
        {
            "species": "penguin",
            "name": "Pip",
            "age": 4,
            "enclosure": "The Arctic Exhibit",
            "trail": "Polar Path"
        },
        {
            "species": "penguin",
            "name": "Skipper",
            "age": 5,
            "enclosure": "The Arctic Exhibit",
            "trail": "Polar Path"
        },
        {
            "species": "penguin",
            "name": "Chilly",
            "age": 3,
            "enclosure": "The Arctic Exhibit",
            "trail": "Polar Path"
        },
        {
            "species": "penguin",
            "name": "Pingu",
            "age": 6,
            "enclosure": "The Arctic Exhibit",
            "trail": "Polar Path"
        },
        {
            "species": "penguin",
            "name": "Noot",
            "age": 1,
            "enclosure": "The Arctic Exhibit",
            "trail": "Polar Path"
        },
        {
            "species": "elephant",
            "name": "Ellie",
            "age": 15,
            "enclosure": "The Pachyderm Sanctuary",
            "trail": "Savannah Heights"
        },
        {
            "species": "elephant",
            "name": "Peanut",
            "age": 12,
            "enclosure": "The Pachyderm Sanctuary",
            "trail": "Savannah Heights"
        },
        {
            "species": "elephant",
            "name": "Dumbo",
            "age": 5,
            "enclosure": "The Pachyderm Sanctuary",
            "trail": "Savannah Heights"
        },
        {
            "species": "elephant",
            "name": "Trunkers",
            "age": 10,
            "enclosure": "The Pachyderm Sanctuary",
            "trail": "Savannah Heights"
        },
        {
            "species": "bear",
            "name": "Smokey",
            "age": 10,
            "enclosure": "The Grizzly Gulch",
            "trail": "Polar Path"
        },
        {
            "species": "bear",
            "name": "Grizzly",
            "age": 8,
            "enclosure": "The Grizzly Gulch",
            "trail": "Polar Path"
        },
        {
            "species": "bear",
            "name": "Barnaby",
            "age": 6,
            "enclosure": "The Grizzly Gulch",
            "trail": "Polar Path"
        },
        {
            "species": "bear",
            "name": "Bruin",
            "age": 12,
            "enclosure": "The Grizzly Gulch",
            "trail": "Polar Path"
        },
        {
            "species": "giraffe",
            "name": "Gerald",
            "age": 4,
            "enclosure": "The Tall Grass Plains",
            "trail": "Savannah Heights"
        },
        {
            "species": "giraffe",
            "name": "Longneck",
            "age": 5,
            "enclosure": "The Tall Grass Plains",
            "trail": "Savannah Heights"
        },
        {
            "species": "giraffe",
            "name": "Patches",
            "age": 3,
            "enclosure": "The Tall Grass Plains",
            "trail": "Savannah Heights"
        },
        {
            "species": "giraffe",
            "name": "Stretch",
            "age": 6,
            "enclosure": "The Tall Grass Plains",
            "trail": "Savannah Heights"
        },
        {
            "species": "antelope",
            "name": "Speedy",
            "age": 2,
            "enclosure": "The Tall Grass Plains",
            "trail": "Savannah Heights"
        },
        {
            "species": "antelope",
            "name": "Dash",
            "age": 3,
            "enclosure": "The Tall Grass Plains",
            "trail": "Savannah Heights"
        },
        {
            "species": "antelope",
            "name": "Gazelle",
            "age": 4,
            "enclosure": "The Tall Grass Plains",
            "trail": "Savannah Heights"
        },
        {
            "species": "antelope",
            "name": "Swift",
            "age": 5,
            "enclosure": "The Tall Grass Plains",
            "trail": "Savannah Heights"
        },
        {
            "species": "polar bear",
            "name": "Snowflake",
            "age": 7,
            "enclosure": "The Arctic Exhibit",
            "trail": "Polar Path"
        },
        {
            "species": "polar bear",
            "name": "Blizzard",
            "age": 5,
            "enclosure": "The Arctic Exhibit",
            "trail": "Polar Path"
        },
        {
            "species": "polar bear",
            "name": "Iceberg",
            "age": 9,
            "enclosure": "The Arctic Exhibit",
            "trail": "Polar Path"
        },
        {
            "species": "walrus",
            "name": "Wally",
            "age": 10,
            "enclosure": "The Walrus Cove",
            "trail": "Polar Path"
        },
        {
            "species": "walrus",
            "name": "Tusker",
            "age": 12,
            "enclosure": "The Walrus Cove",
            "trail": "Polar Path"
        },
        {
            "species": "walrus",
            "name": "Moby",
            "age": 8,
            "enclosure": "The Walrus Cove",
            "trail": "Polar Path"
        },
        {
            "species": "walrus",
            "name": "Flippers",
            "age": 9,
            "enclosure": "The Walrus Cove",
            "trail": "Polar Path"
        }
    ]
    
    @mcp.tool()
    def get_animals_by_species(species: str) -> List[Dict[str, Any]]:
        """
        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.
    
        Args:
            species: The species of the animal (e.g., 'lion', 'penguin').
    
        Returns:
            A list of dictionaries, where each dictionary represents an animal
            and contains details like name, age, enclosure, and trail.
        """
        logger.info(f">>> 🛠️ Tool: 'get_animals_by_species' called for '{species}'")
        return [animal for animal in ZOO_ANIMALS if animal["species"].lower() == species.lower()]
    
    @mcp.tool()
    def get_animal_details(name: str) -> Dict[str, Any]:
        """
        Retrieves the details of a specific animal by its name.
    
        Args:
            name: The name of the animal.
    
        Returns:
            A dictionary with the animal's details (species, name, age, enclosure, trail)
            or an empty dictionary if the animal is not found.
        """
        logger.info(f">>> 🛠️ Tool: 'get_animal_details' called for '{name}'")
        for animal in ZOO_ANIMALS:
            if animal["name"].lower() == name.lower():
                return animal
        return {}
    
    if __name__ == "__main__":
        logger.info(f"🚀 MCP server started on port {os.getenv('PORT', 8080)}")
        asyncio.run(
            mcp.run_async(
                transport="http",
                host="0.0.0.0",
                port=os.getenv("PORT", 8080),
            )
        )
    

¡Tu código está completo! Es hora de implementar el servidor de MCP en Cloud Run.

7. Implementa en Cloud Run

Ahora implementa un servidor de MCP en Cloud Run directamente desde el código fuente.

  1. Crea y abre un nuevo Dockerfile para la implementación en Cloud Run:
    cloudshell edit Dockerfile
    
  2. Incluye el siguiente código en el Dockerfile para usar la herramienta uv y ejecutar el archivo server.py:
    # 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
    
    EXPOSE $PORT
    
    # Run the FastMCP server
    CMD ["uv", "run", "server.py"]
    
  3. Ejecuta el comando gcloud para implementar la aplicación en Cloud Run
    gcloud run deploy zoo-mcp-server \
        --no-allow-unauthenticated \
        --region=europe-west1 \
        --source=. \
        --labels=dev-tutorial=codelab-mcp
    
    Usa la marca --no-allow-unauthenticated para exigir autenticación. Esto es importante por motivos de seguridad. Si no requieres autenticación, cualquier persona puede llamar a tu servidor de MCP y, potencialmente, dañar tu sistema.
  4. Confirma la creación de un nuevo repositorio de Artifact Registry. Como es la primera vez que implementas en Cloud Run desde el código fuente, verás lo siguiente:
    Deploying from source requires an Artifact Registry Docker repository to store built containers. A repository named 
    [cloud-run-source-deploy] in region [europe-west1] will be created.
    
    Do you want to continue (Y/n)?
    
    Escribe Y y presiona Enter. Esto creará un repositorio de Artifact Registry para tu implementación. Esto es necesario para almacenar el contenedor de Docker del servidor de MCP para el servicio de Cloud Run.
  5. Después de unos minutos, verás un mensaje como el siguiente:
    Service [zoo-mcp-server] revision [zoo-mcp-server-12345-abc] has been deployed and is serving 100 percent of traffic.
    

Implementaste tu servidor de MCP. Ahora puedes usarlo.

8. Agrega el servidor de MCP remoto a Gemini CLI

Ahora que implementaste correctamente un servidor MCP remoto, puedes conectarte a él con varias aplicaciones, como Google Code Assist o Gemini CLI. En esta sección, estableceremos una conexión con tu nuevo servidor de MCP remoto usando la CLI de Gemini.

  1. Otorga permiso a tu cuenta de usuario para llamar al servidor de MCP remoto
    gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \
        --member=user:$(gcloud config get-value account) \
        --role='roles/run.invoker'
    
  2. Guarda tus credenciales de Google Cloud y el número de proyecto en variables de entorno para usarlos en el archivo de configuración de Gemini:
    export PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT --format="value(projectNumber)")
    export ID_TOKEN=$(gcloud auth print-identity-token)
    
  3. Abre el archivo de configuración de Gemini CLI
    cloudshell edit ~/.gemini/settings.json
    
  4. Reemplaza tu archivo de configuración de la CLI de Gemini para agregar el servidor de MCP de Cloud Run
    {
      "mcpServers": {
        "zoo-remote": {
          "httpUrl": "https://zoo-mcp-server-$PROJECT_NUMBER.europe-west1.run.app/mcp/",
          "headers": {
            "Authorization": "Bearer $ID_TOKEN"
          }
        }
      },
      "selectedAuthType": "cloud-shell",
      "hasSeenIdeIntegrationNudge": true
    }
    

  1. Inicia Gemini CLI en Cloud Shell
    gemini
    
    Es posible que debas presionar Enter para aceptar algunos parámetros de configuración predeterminados.Vista inicial de la CLI de Gemini
  2. Haz que Gemini enumere las herramientas de MCP disponibles en su contexto.
    /mcp
    
  3. Pídele a Gemini que encuentre algo en el zoológico
    Where can I find penguins?
    
    La CLI de Gemini debe saber que debe usar el servidor de MCP de zoo-remote y te preguntará si quieres permitir la ejecución de MCP.
  4. Usa la flecha hacia abajo y, luego, presiona Enter para seleccionar.
    Yes, always allow all tools from server "zoo-remote"
    
    Permitir herramientas remotas de la CLI de Gemini

El resultado debe mostrar la respuesta correcta y un cuadro de visualización que indique que se usó el servidor de MCP.

La CLI de Gemini muestra el resultado del servidor de MCP del zoo

¡Lo lograste! Implementaste correctamente un servidor de MCP remoto en Cloud Run y lo probaste con la CLI de Gemini.

Cuando quieras finalizar la sesión, escribe /quit y, luego, presiona Enter para salir de la CLI de Gemini.

Depuración

Si ves un error como este:

🔍 Attempting OAuth discovery for 'zoo-remote'...
❌ 'zoo-remote' requires authentication but no OAuth configuration found
Error connecting to MCP server 'zoo-remote': MCP server 'zoo-remote' requires authentication. Please configure OAuth or check server settings.

Es probable que se haya agotado el tiempo de espera del token de ID y que se deba configurar ID_TOKEN nuevamente.

  1. Escribe /quit y, luego, presiona Enter para salir de Gemini CLI.
  2. Configura tu proyecto en la terminal
    gcloud config set project [PROJECT_ID]
    
  3. Reinicia el paso 2 anterior

9. Verifica las llamadas a herramientas en los registros del servidor (opcional)

Para verificar que se llamó a tu servidor de MCP de Cloud Run, consulta los registros del servicio.

gcloud run services logs read zoo-mcp-server --region europe-west1 --limit=5

Deberías ver un registro de salida que confirme que se realizó una llamada a la herramienta. 🛠️

2025-08-05 19:50:31 INFO:     169.254.169.126:39444 - "POST /mcp/ HTTP/1.1" 200 OK
2025-08-05 19:50:31 [INFO]: Processing request of type CallToolRequest
2025-08-05 19:50:31 [INFO]: >>> 🛠️ Tool: 'get_animals_by_species' called for 'penguin'

10. (Opcional) Agrega la instrucción de MCP al servidor

Una instrucción de MCP puede acelerar tu flujo de trabajo para las instrucciones que ejecutas con frecuencia, ya que crea una abreviatura para una instrucción más larga.

La CLI de Gemini convierte automáticamente las instrucciones de MCP en comandos de barra personalizados para que puedas invocar una instrucción de MCP escribiendo /prompt_name, donde prompt_name es el nombre de tu instrucción de MCP.

Crea una instrucción de MCP para que puedas encontrar rápidamente un animal en el zoológico escribiendo /find animal en la CLI de Gemini.

  1. Agrega este código a tu archivo server.py sobre el guardián principal (if __name__ == "__main__":).
    @mcp.prompt()
    def find(animal: str) -> str:
        """
        Find which exhibit and trail a specific animal might be located.
        """
    
        return (
            f"Please find the exhibit and trail information for {animal} in the zoo. "
            f"Respond with '[animal] can be found in the [exhibit] on the [trail].'"
            f"Example: Penguins can be found in The Arctic Exhibit on the Polar Path."
        )
    
  2. Vuelve a implementar tu aplicación en Cloud Run
    gcloud run deploy zoo-mcp-server \
        --no-allow-unauthenticated \
        --region=europe-west1 \
        --source=. \
        --labels=dev-tutorial=codelab-mcp
    
  3. Actualiza tu ID_TOKEN para tu servidor de MCP remoto
    export ID_TOKEN=$(gcloud auth print-identity-token)
    
  4. Después de implementar la nueva versión de tu aplicación, inicia Gemini CLI.
    gemini
    
  5. En la instrucción, usa el nuevo comando personalizado que creaste:
    /find --animal="lions"
    

Deberías ver que Gemini CLI llama a la herramienta get_animals_by_species y da formato a la respuesta según las instrucciones de la instrucción de MCP.

╭───────────────────────────╮
│  > /find --animal="lion"  │
╰───────────────────────────╯

 ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮
 │ ✔  get_animals_by_species (zoo-remote MCP Server) get_animals_by_species (zoo-remote MCP Server)  │
 │                                                                                                   │
 │    [{"species":"lion","name":"Leo","age":7,"enclosure":"The Big Cat                               │
 │    Plains","trail":"Savannah                                                                      │
 │    Heights"},{"species":"lion","name":"Nala","age":6,"enclosure":"The Big Cat                     │
 │    Plains","trail":"Savannah                                                                      │
 │    Heights"},{"species":"lion","name":"Simba","age":3,"enclosure":"The Big Cat                    │
 │    Plains","trail":"Savannah                                                                      │
 │    Heights"},{"species":"lion","name":"King","age":8,"enclosure":"The Big Cat                     │
 │    Plains","trail":"Savannah Heights"}]                                                           │
 ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ Lions can be found in The Big Cat Plains on the Savannah Heights.

Objetivos flexibles para ponerte a prueba

Para un desafío adicional, intenta seguir los mismos pasos para crear una instrucción que devuelva datos curiosos sobre especies animales específicas en el zoológico.

O, para poner a prueba lo que aprendiste, piensa en una herramienta que usarías con frecuencia y, luego, implementa un segundo servidor de MCP remoto. Luego, agrégalo a la configuración de la CLI de Gemini para ver si funciona.

11. Conclusión

¡Felicitaciones! Implementaste correctamente un servidor de MCP remoto seguro y te conectaste a él.

Continúa con el siguiente lab

Este lab es el primero de una serie de tres partes. En el segundo lab, usarás el servidor de MCP que creaste con un agente de ADK.

Usa un servidor de MCP en Cloud Run con un agente de ADK

(Opcional) Limpieza

Si no continuarás con el siguiente lab y deseas limpiar lo que creaste, puedes borrar tu proyecto de Cloud para evitar que se generen cargos adicionales.

Si bien Cloud Run no cobra cuando el servicio no se usa, es posible que se te cobre por el almacenamiento de la imagen del contenedor en Artifact Registry. Si borras tu proyecto de Cloud, se dejan de facturar todos los recursos que usaste en ese proyecto.

Si quieres, borra el proyecto:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

También puedes borrar los recursos innecesarios de tu disco de Cloud Shell. Puedes hacer lo siguiente:

  1. Borra el directorio del proyecto del codelab:
    rm -rf ~/mcp-on-cloudrun
    
  2. Advertencia. Esta próxima acción no se puede deshacer. Si quieres borrar todo el contenido de Cloud Shell para liberar espacio, puedes borrar todo el directorio principal. Ten cuidado de guardar en otro lugar todo lo que quieras conservar.
    sudo rm -rf $HOME