Quería que ChatGPT pudiera consultar las cuentas de Google Ads a las que ya tengo acceso sin tener que copiar informes manualmente en una conversación ni colocar un único usuario compartido de Google detrás de toda la integración.

Google publica un servidor MCP oficial para Google Ads y una guía de integración. El proyecto permite a un cliente MCP consultar datos de Google Ads en modo lectura y también puede funcionar de forma remota por HTTP mediante el proxy OAuth de Google de FastMCP.

Yo quería algo que pudiera dejar funcionando como infraestructura: alojado en Google Cloud Run, con el estado de OAuth persistiendo entre reinicios, los secretos fuera del contenedor y cada persona autenticándose con su propia cuenta de Google.

Este artículo documenta el despliegue que hice realmente en septiembre de 2026 y las correcciones que apliqué después al utilizarlo con más personas. He conservado las revisiones exactas del código, los comandos relevantes y los errores reales porque quiero que siga siendo útil dentro de seis meses, cuando seguramente ya no recuerde por qué existe un permiso IAM concreto o una determinada variable de entorno.

Qué vamos a construir

El recorrido final de una petición es, aproximadamente, este:

Cliente MCP
   |
   | OAuth / Streamable HTTP
   v
Google Cloud Run
   |
   +-- Google Ads MCP
   |      |
   |      +-- Proxy OAuth de FastMCP
   |      +-- Google OAuth
   |
   +-- Firestore
   |      |
   |      +-- estado OAuth persistente
   |
   v
Google Ads API

La decisión arquitectónica importante es que la autenticación de Google se realiza por usuario. El servicio de Cloud Run es compartido, pero cuando alguien conecta el servidor MCP desde un cliente compatible, esa persona completa Google OAuth con su propia identidad de Google.

Por tanto, los permisos de Google Ads siguen siendo exactamente los que ya tiene concedidos ese usuario en Google.

Por qué no me limité a ejecutarlo en local

El proyecto oficial permite una configuración local mucho más sencilla. Para un único desarrollador puede ser suficiente: ejecutas el MCP en tu equipo, configuras las credenciales y haces que tu cliente MCP arranque ese proceso.

Yo buscaba otra arquitectura:

  • una única URL MCP remota en lugar de un proceso local por ordenador;
  • autenticación OAuth independiente para cada persona;
  • un despliegue que pudiera usar desde ChatGPT y desde otros clientes MCP remotos;
  • estado OAuth que sobreviviera a nuevas revisiones e instancias de Cloud Run;
  • secretos gestionados fuera de la imagen Docker;
  • una identidad de ejecución dedicada con permisos IAM explícitos;
  • una revisión concreta del servidor fijada para poder reproducir el despliegue más adelante.

La contrapartida es evidente: hay más infraestructura que configurar.

Local, Cloud Run mínimo o Cloud Run persistente

La documentación actual de Google incluye tanto configuraciones locales como un despliegue en Cloud Run utilizando el proxy OAuth. Mi configuración no pretende sustituir la guía de Google; es una versión más explícita y persistente de ese despliegue remoto.

Local / stdio Cloud Run mínimo Este despliegue
Endpoint remoto compartido No Sí Sí
Google OAuth por usuario No como servicio compartido Sí Sí
Persistencia OAuth en Firestore No Opcional Sí
Clave JWT persistente No Recomendada Sí
Cifrado de tokens almacenados No Opcional Sí
Secret Manager No No obligatorio en el ejemplo mínimo Sí
Service account dedicada No Opcional Sí
IAM explícito Mínimo Depende Sí
Revisión del código fijada Depende Los ejemplos suelen usar latest Sí
Verificación del protocolo Manual Manual Incluida aquí
Complejidad operativa Baja Media Mayor

Requisitos de ChatGPT

En el momento de publicar este artículo, ChatGPT Free no permite este flujo de MCP personalizado mediante modo desarrollador.

La documentación actual de OpenAI indica que los usuarios Pro pueden conectar servidores MCP personalizados con permisos de lectura/obtención en modo desarrollador. El soporte MCP completo, incluidas acciones de modificación y escritura, está disponible en beta para Business, Enterprise y Edu. El MCP oficial de Google Ads es actualmente de solo lectura, así que el caso relevante para este tutorial es precisamente el acceso de lectura.

La interfaz concreta también puede cambiar según el plan y según estés o no dentro de un workspace administrado. Business y Enterprise/Edu pueden añadir pasos de administración, publicación y control de acceso que un usuario Pro no verá.

Como estas capacidades cambian con bastante rapidez, conviene consultar la documentación actual sobre Developer Mode y aplicaciones MCP en ChatGPT antes de empezar.

Requisitos previos

Para este despliegue utilicé:

  • un proyecto de Google Cloud con facturación activada;
  • acceso a la API de Google Ads para ese proyecto;
  • un cliente OAuth 2.0 de tipo Web application;
  • una cuenta manager de Google Ads, porque mi acceso abarca múltiples cuentas;
  • Google Cloud Shell;
  • un plan o workspace de ChatGPT compatible con el MCP personalizado requerido, o cualquier otro cliente MCP compatible;
  • el repositorio oficial googleads/google-ads-mcp.

Mi región de Cloud Run y Firestore es:

europe-southwest1

No es obligatorio usar Madrid. La elegí porque era la región que quería para este proyecto.

1. Seleccionar el proyecto correcto y abrir Cloud Shell

Antes de ejecutar ningún comando, asegúrate de estar trabajando en el proyecto correcto de Google Cloud. Las APIs habilitadas, las credenciales OAuth, la facturación y los recursos que crearemos pertenecen a un proyecto concreto.

Identificar el Project ID

Abre Google Cloud Console y mira la barra superior, junto al logo de Google Cloud. Ahí aparece el selector de proyecto.

Al abrirlo verás algo parecido a:

Nombre del proyecto       ID del proyecto
-----------------------------------------------
Google Ads API            my-project-123456789
Otro proyecto             another-project

Para los comandos de este tutorial necesitas el Project ID, no el nombre visible del proyecto ni el número numérico de proyecto.

Si ya tienes un proyecto que utilizas para Google Ads API, como era mi caso, reutilízalo en lugar de crear uno nuevo sin necesidad. Lo importante es que sea el proyecto al que quieres asociar el acceso a Google Ads, las credenciales OAuth y el despliegue del MCP.

Abrir Cloud Shell

Todos los comandos de esta guía los ejecuté desde Google Cloud Shell. No hace falta instalar gcloud en Windows ni utilizar un servidor propio.

En la esquina superior derecha de Google Cloud Console pulsa el icono de terminal, parecido a:

>_

Se llama Activate Cloud Shell / Activar Cloud Shell. Al abrirlo aparece una terminal en la parte inferior del navegador. Después de unos segundos verás algo similar a:

your-user@cloudshell:~$

Cloud Shell es una máquina proporcionada por Google para trabajar con tu proyecto. Ya trae herramientas como gcloud, git y docker preparadas para usar.

Comprueba primero qué proyecto tiene activo gcloud:

gcloud config get-value project

El valor debe coincidir con el Project ID que acabas de identificar en la consola.

Después guardé ese proyecto en una variable y definí Madrid como región:

export PROJECT_ID="$(gcloud config get-value project)"
export REGION="europe-southwest1"

gcloud config set run/region "$REGION"

Dos comprobaciones útiles antes de continuar:

gcloud auth list
gcloud billing projects describe "$PROJECT_ID"

El segundo comando debe confirmar que el proyecto tiene la facturación activada.

2. Comprobar y habilitar las APIs de Google Cloud

Antes de habilitar todo por terminal, conviene comprobar visualmente que estás mirando el proyecto correcto y que la Google Ads API está disponible.

Comprobar Google Ads API desde la interfaz

En Google Cloud Console:

  1. Utiliza la búsqueda superior y escribe Google Ads API.
  2. Abre el resultado Google Ads API dentro de APIs y servicios.
  3. Si ves Manage / Administrar o que la API está habilitada, ya está activa en ese proyecto.
  4. Si ves Enable / Habilitar, pulsa ese botón.

El propio repositorio oficial de Google Ads MCP incluye la activación de Google Ads API como paso previo de configuración.

Después habilité desde Cloud Shell todos los servicios que necesita este despliegue:

gcloud services enable \
  googleads.googleapis.com \
  run.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com \
  secretmanager.googleapis.com \
  firestore.googleapis.com

Cada uno tiene una función concreta:

  • googleads.googleapis.com: acceso a Google Ads API;
  • run.googleapis.com: ejecución del servicio MCP en Cloud Run;
  • cloudbuild.googleapis.com: construcción de la imagen;
  • artifactregistry.googleapis.com: almacenamiento de la imagen Docker;
  • secretmanager.googleapis.com: almacenamiento de secretos;
  • firestore.googleapis.com: persistencia del estado OAuth.

Si alguna ya estaba habilitada, el comando simplemente mantiene ese estado.

Google Ads API con acceso básico
El proyecto utilizado tenía acceso básico a Google Ads API.

3. Crear Firestore para persistir OAuth

FastMCP puede utilizar diferentes backends para almacenar el estado de OAuth. La memoria es cómoda para pruebas locales, pero no es una buena opción para un servicio de Cloud Run que puede reiniciarse o escalar a más de una instancia.

En este despliegue utilicé Firestore. Hay tres opciones de la pantalla de creación que conviene entender antes de pulsar botones:

  • (default) es el ID de la base de datos predeterminada. El Google Ads MCP utiliza ese nombre por defecto si no configuras otro explícitamente.
  • Native mode significa utilizar la API nativa de Firestore como base de datos documental. La otra opción de Standard es Datastore mode, pensada para compatibilidad con aplicaciones que utilizan la API de Datastore.
  • Standard edition ofrece las capacidades normales de Firestore. Enterprise añade un motor de consultas y capacidades adicionales que este almacenamiento de estado OAuth no necesita.

Crear la base de datos desde la interfaz

En Google Cloud Console:

  1. Busca Firestore o entra en la sección de bases de datos.
  2. Pulsa Create database / Crear base de datos.
  3. Usa (default) como ID de la base de datos.
  4. Selecciona Standard edition.
  5. Selecciona Firestore in Native mode.
  6. Elige la misma región que para Cloud Run; en mi caso, europe-southwest1 (Madrid).
  7. Crea la base de datos.

Yo dejé documentado también el comando equivalente, que crea exactamente esa configuración:

gcloud firestore databases create \
  --database="(default)" \
  --location="$REGION" \
  --edition=standard \
  --type=firestore-native

Después la verifiqué:

gcloud firestore databases list
Base de datos Firestore predeterminada, edición Estándar y modo nativo
La base de datos (default) creada en europe-southwest1, Madrid.

4. Crear el cliente OAuth Web de Google

El servidor remoto necesita un cliente OAuth de Google de tipo Web application.

En Google Auth Platform creé un cliente Web para el servicio MCP y guardé su Client ID y Client Secret.

En este punto todavía no existe la URL callback definitiva porque Cloud Run aún no ha asignado una URL al servicio. Volveremos al cliente OAuth después del primer despliegue para añadir:

https://YOUR_CLOUD_RUN_HOST/auth/callback

FastMCP utiliza ese callback fijo para completar el flujo OAuth contra Google. El callback del propio cliente MCP pertenece a otra parte del flujo del proxy OAuth y se gestiona mediante registro dinámico.

Público de Google OAuth: usuarios externos y estado en producción
Así estaba configurado el público de mi aplicación OAuth durante el despliegue.
Lista de clientes OAuth con los identificadores ocultos
El cliente del MCP debe ser de tipo Aplicación web. Identificadores anonimizados.

5. Definir las variables del despliegue

De vuelta en Cloud Shell:

export SERVICE_NAME="google-ads-mcp"
export OAUTH_CLIENT_ID="YOUR_GOOGLE_OAUTH_CLIENT_ID"

En un despliegue compartido por varios usuarios no definiría aquí un login customer ID global. El motivo es que cada persona puede llegar a una cuenta de Google Ads por una ruta distinta: acceso directo, un MCC concreto u otro manager.

Desde Google Ads MCP 0.0.4, las herramientas que lo necesitan aceptan un parámetro opcional login_customer_id por llamada. Si el usuario tiene acceso directo a la cuenta cliente, se omite. Si la consulta debe realizarse a través de un manager, se envía el customer ID de ese manager solo en esa llamada.

En la primera revisión de Cloud Run no voy a inventar el hostname del servicio. Cloud Run nos dará su URL canónica al crearlo y entonces configuraremos GOOGLE_ADS_MCP_BASE_URL con ese valor exacto.

6. Guardar los secretos en Secret Manager

No quería que los secretos de OAuth o las claves de firma estuvieran dentro de la imagen Docker ni escritos directamente en el comando de despliegue.

Creamos los tres secretos que usa el despliegue final:

gcloud secrets create google-ads-oauth-client-secret \
  --replication-policy="automatic"

gcloud secrets create google-ads-mcp-jwt-signing-key \
  --replication-policy="automatic"

gcloud secrets create google-ads-mcp-storage-encryption-key \
  --replication-policy="automatic"

Añadimos el Client Secret de Google OAuth sin mostrarlo en la terminal:

read -s -p "OAuth Client Secret: " OAUTH_SECRET
echo

printf '%s' "$OAUTH_SECRET" | \
  gcloud secrets versions add google-ads-oauth-client-secret --data-file=-

unset OAUTH_SECRET

Generamos una clave JWT persistente para FastMCP y otra clave para cifrar los datos OAuth almacenados:

openssl rand -base64 48 | \
  gcloud secrets versions add google-ads-mcp-jwt-signing-key --data-file=-

openssl rand -base64 32 | \
  gcloud secrets versions add google-ads-mcp-storage-encryption-key --data-file=-

Un cambio importante de 2026: ya no necesitas developer token

Google retiró los developer tokens de Google Ads el 9 de septiembre de 2026. Los clientes antiguos pueden seguir enviando el header por compatibilidad, pero Google indica que ahora es opcional y que sus servidores lo ignoran.

Los niveles de acceso a la API están asociados actualmente al proyecto de Google Cloud propietario de las credenciales OAuth.

La revisión del MCP utilizada en este artículo todavía contiene referencias de compatibilidad a GOOGLE_ADS_DEVELOPER_TOKEN. Para un despliegue nuevo hoy, no crearía ni publicaría ningún developer token.

Puedes consultar la documentación de Google sobre la retirada del developer token.

Nombres de los secretos almacenados en Secret Manager
Solo se muestran nombres, nunca valores. El developer token aparece en esta captura histórica; no forma parte de los tres secretos que configura esta guía.

7. Crear una service account dedicada para Cloud Run

En lugar de ejecutar el contenedor con una identidad por defecto demasiado amplia, creé una service account específica:

gcloud iam service-accounts create google-ads-mcp-runner \
  --display-name="Google Ads MCP Cloud Run"

export RUN_SA="google-ads-mcp-runner@$PROJECT_ID.iam.gserviceaccount.com"

El servicio necesita leer y escribir el estado OAuth almacenado en Firestore:

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:$RUN_SA" \
  --role="roles/datastore.user"

8. Dar acceso de la service account a todos los secretos

Este paso debe hacerse antes del primer despliegue en Cloud Run.

gcloud secrets add-iam-policy-binding google-ads-oauth-client-secret \
  --member="serviceAccount:$RUN_SA" \
  --role="roles/secretmanager.secretAccessor"

gcloud secrets add-iam-policy-binding google-ads-mcp-jwt-signing-key \
  --member="serviceAccount:$RUN_SA" \
  --role="roles/secretmanager.secretAccessor"

gcloud secrets add-iam-policy-binding google-ads-mcp-storage-encryption-key \
  --member="serviceAccount:$RUN_SA" \
  --role="roles/secretmanager.secretAccessor"

La primera vez que hice el despliegue me faltaba precisamente este permiso para el secreto de cifrado del almacenamiento. No era un problema del contenedor ni de FastMCP, sino un permiso IAM que había omitido. Por eso en esta guía lo coloco directamente en el orden correcto.

9. Crear el repositorio de Artifact Registry

La imagen Docker necesita un registro donde almacenarse:

gcloud artifacts repositories create mcp-servers \
  --repository-format=docker \
  --location="$REGION" \
  --description="Docker images for MCP servers"
Repositorio Docker mcp-servers en Artifact Registry
El repositorio mcp-servers almacena la imagen que se desplegará en Cloud Run.

10. Clonar el servidor oficial y fijar la revisión

No quería desplegar simplemente lo que hubiera en main en ese momento.

Mi despliegue inicial de septiembre usó la revisión 7a40eae... (Google Ads MCP 0.0.3). El 7 de octubre lo actualicé después de comprobar que 0.0.4 añadía un parámetro login_customer_id opcional por llamada, necesario para un uso multiusuario correcto.

La revisión exacta que tengo desplegada actualmente es:

8efbd2e2b56da755cd0b3e642149ed8ad96b44b5

Además de la funcionalidad de 0.0.4, esa revisión actualiza FastMCP por una corrección upstream relacionada con el registro de access tokens OAuth.

Clonamos el repositorio y hacemos checkout de ese commit:

cd ~

git clone https://github.com/googleads/google-ads-mcp.git
cd google-ads-mcp

git checkout 8efbd2e2b56da755cd0b3e642149ed8ad96b44b5
git rev-parse HEAD

Fijar la revisión hace que el resto del artículo sea reproducible. El proyecto oficial seguirá evolucionando; esta guía describe la revisión que realmente tengo desplegada.

11. Adaptar el Dockerfile para Firestore y eliminar un workaround incompatible

El backend de Firestore necesita instalar el extra google-ads-mcp[firestore]. En la revisión desplegada, el Dockerfile sigue instalando únicamente el paquete base:

uv pip install --system .

Lo cambié a:

sed -i 's|uv pip install --system \.|uv pip install --system .[firestore]|' Dockerfile

Al actualizar a 8efbd2e... encontré además un error reproducible durante Cloud Build. El Dockerfile contenía un workaround para Codex que exigía exactamente FastMCP 4.0.3:

assert version('fastmcp') == '4.0.3'

pero el pyproject.toml de esa revisión ya permitía FastMCP 4.0.6 o superior. Mi build resolvió FastMCP 4.0.11 y terminó con AssertionError.

Como ese parche era una compatibilidad específica para una versión antigua de Codex y no forma parte de mi flujo con ChatGPT, eliminé ese bloque antes de construir:

sed -i '/^# Codex CLI 0\.146/,/^LABEL io\.google-mcp\.workaround=/d' Dockerfile

Comprobé ambas cosas:

grep -n "4.0.3\|workaround" Dockerfile
grep -n "uv pip install" Dockerfile

La primera orden no debe devolver nada y la segunda debe mostrar:

RUN uv pip install --system .[firestore]

12. Construir la imagen con Cloud Build

En lugar de utilizar latest, etiqueté la imagen con el commit corto del código que estaba desplegando:

export IMAGE="$REGION-docker.pkg.dev/$PROJECT_ID/mcp-servers/google-ads-mcp:8efbd2e"

Construimos y subimos la imagen:

gcloud builds submit \
  --tag "$IMAGE" \
  .

Y comprobamos que aparece en Artifact Registry:

gcloud artifacts docker images list \
  "$REGION-docker.pkg.dev/$PROJECT_ID/mcp-servers"

En mi actualización de octubre, la compilación instaló google-ads-mcp==0.0.4, FastMCP 4.0.11 y las dependencias de Firestore. La imagen se construyó y publicó correctamente después de retirar el workaround anterior.

13. Desplegar la primera revisión en Cloud Run

En esta primera revisión todavía no configuro GOOGLE_ADS_MCP_BASE_URL. Cloud Run aún no nos ha dado la URL del servicio, por lo que no tiene sentido inventarla.

Los metadatos OAuth serán provisionales durante unos segundos. Los corregiremos inmediatamente después obteniendo la URL real de Cloud Run.

gcloud run deploy "$SERVICE_NAME" \
  --image="$IMAGE" \
  --region="$REGION" \
  --platform=managed \
  --service-account="$RUN_SA" \
  --allow-unauthenticated \
  --set-env-vars="GOOGLE_PROJECT_ID=$PROJECT_ID,GOOGLE_ADS_MCP_OAUTH_CLIENT_ID=$OAUTH_CLIENT_ID,GOOGLE_ADS_MCP_STORAGE_TYPE=firestore,FASTMCP_HOST=0.0.0.0" \
  --set-secrets="GOOGLE_ADS_MCP_OAUTH_CLIENT_SECRET=google-ads-oauth-client-secret:latest,GOOGLE_ADS_MCP_JWT_SIGNING_KEY=google-ads-mcp-jwt-signing-key:latest,GOOGLE_ADS_MCP_STORAGE_ENCRYPTION_KEY=google-ads-mcp-storage-encryption-key:latest"

¿Por qué Cloud Run permite acceso público?

La opción –allow-unauthenticated puede parecer incorrecta a primera vista.

En esta arquitectura es intencionada. Cloud Run tiene que aceptar las peticiones HTTP que inician la negociación MCP y OAuth. La autenticación se aplica después, en la capa de aplicación de FastMCP.

Lo comprobaremos explícitamente más adelante: una petición anónima llega al servicio, pero /mcp responde con 401 Unauthorized e indica al cliente dónde encontrar los metadatos OAuth.

14. Configurar como base URL la URL real de Cloud Run

Una vez creado el servicio, pedimos a Cloud Run su URL canónica:

export ACTUAL_URL="$(gcloud run services describe "$SERVICE_NAME" \
  --region="$REGION" \
  --format='value(status.url)')"

echo "$ACTUAL_URL"

Actualizamos el servidor para que sus metadatos OAuth y redirects utilicen exactamente ese host:

gcloud run services update "$SERVICE_NAME" \
  --region="$REGION" \
  --update-env-vars="GOOGLE_ADS_MCP_BASE_URL=$ACTUAL_URL"

En un MCP compartido por varios usuarios no establezcas GOOGLE_ADS_LOGIN_CUSTOMER_ID globalmente salvo que todos accedan a las cuentas cliente a través del mismo manager.

Ese valor global se aplica a todas las llamadas y puede romper a usuarios que tengan acceso directo a una cuenta cliente pero no tengan permisos sobre ese MCC. Con 0.0.4 prefiero dejar el valor global sin definir y utilizar el parámetro opcional login_customer_id solamente cuando una consulta concreta necesite el contexto de una cuenta manager.

Por último, comprobamos la revisión activa:

gcloud run services describe "$SERVICE_NAME" \
  --region="$REGION" \
  --format="table(status.url,status.latestReadyRevisionName,status.conditions[0].status)"

Antes de tocar ChatGPT yo quería ver el estado en True.

Servicio google-ads-mcp activo en Cloud Run en europe-southwest1
Cloud Run muestra el servicio en verde. Public access corresponde a la capa HTTP; el recurso MCP sigue protegido por OAuth.

15. Añadir el redirect URI definitivo en Google OAuth

Volvemos ahora al cliente OAuth Web de Google Auth Platform.

Añadimos la URL exacta de Cloud Run seguida del callback de FastMCP:

https://YOUR_CLOUD_RUN_HOST/auth/callback

Guardamos el cliente.

Configuración del cliente OAuth con el identificador y la URL ocultos
Aquí se añade el redirect URI terminado en /auth/callback. Se han ocultado el client ID y la URL concreta de mi despliegue.

16. Comprobar que el servidor ha arrancado correctamente

Los logs de Cloud Run son la primera comprobación útil:

gcloud run services logs read "$SERVICE_NAME" \
  --region="$REGION" \
  --limit=50

Si quieres seguir los logs en tiempo real y tu versión de gcloud no acepta gcloud run services logs tail, el comando que utilicé fue:

gcloud beta run services logs tail "$SERVICE_NAME" \
  --region="$REGION"

En mi despliegue FastMCP arrancó con transporte Streamable HTTP escuchando en:

http://0.0.0.0:8080/mcp

Esto confirma que el contenedor funciona, pero todavía no demuestra que el discovery OAuth esté bien configurado.

17. Comprobar los metadatos del recurso MCP protegido

Consultamos primero el endpoint de discovery del recurso protegido:

curl -sS \
  "$ACTUAL_URL/.well-known/oauth-protected-resource/mcp" \
  | python3 -m json.tool

Las partes importantes son equivalentes a estas:

{
  "resource": "https://YOUR_CLOUD_RUN_HOST/mcp",
  "authorization_servers": [
    "https://YOUR_CLOUD_RUN_HOST/"
  ],
  "scopes_supported": [
    "openid",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/userinfo.profile",
    "https://www.googleapis.com/auth/adwords"
  ],
  "bearer_methods_supported": [
    "header"
  ]
}

Esta fue una de las comprobaciones que más me ayudó durante la instalación. Si resource o authorization_servers apuntan a un hostname incorrecto, hay que arreglar GOOGLE_ADS_MCP_BASE_URL antes de seguir depurando el cliente MCP.

18. Comprobar los metadatos del servidor OAuth

A continuación:

curl -sS \
  "$ACTUAL_URL/.well-known/oauth-authorization-server" \
  | python3 -m json.tool

Mi despliegue exponía los endpoints OAuth esperados:

{
  "issuer": "https://YOUR_CLOUD_RUN_HOST/",
  "authorization_endpoint": "https://YOUR_CLOUD_RUN_HOST/authorize",
  "token_endpoint": "https://YOUR_CLOUD_RUN_HOST/token",
  "registration_endpoint": "https://YOUR_CLOUD_RUN_HOST/register",
  "grant_types_supported": [
    "authorization_code",
    "refresh_token"
  ],
  "code_challenge_methods_supported": [
    "S256"
  ]
}

En este punto Cloud Run, FastMCP y la capa de discovery OAuth ya estaban funcionando como debían.

19. Un 401 en /mcp es una buena señal

La siguiente prueba parece un error, pero en realidad era exactamente lo que quería ver:

curl -i "$ACTUAL_URL/mcp"

Resultado esperado:

HTTP/2 401
WWW-Authenticate: Bearer ...

El header WWW-Authenticate debe apuntar de nuevo a:

/.well-known/oauth-protected-resource/mcp

Eso le indica a un cliente MCP que el servidor existe, que el recurso está protegido y dónde encontrar la información necesaria para iniciar la autenticación.

También probé manualmente una petición initialize:

curl -i -X POST "$ACTUAL_URL/mcp" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  --data '{
    "jsonrpc":"2.0",
    "id":1,
    "method":"initialize",
    "params":{
      "protocolVersion":"2026-07-28",
      "capabilities":{},
      "clientInfo":{
        "name":"manual-test",
        "version":"1.0"
      }
    }
  }'

Sin un bearer token, esa petición también devolvía correctamente 401.

20. Conectar el servidor MCP con ChatGPT

La URL MCP remota es simplemente:

https://YOUR_CLOUD_RUN_HOST/mcp

En ChatGPT hay que activar Developer Mode o el soporte de MCP personalizado correspondiente al plan o workspace y crear la aplicación utilizando esa URL.

No quiero que este artículo dependa demasiado de nombres concretos de botones, porque la interfaz cambia. La distinción importante, y la que me hizo perder tiempo, es esta:

crear o publicar la aplicación no significa que tu propio usuario ya esté conectado a ella.

Formulario de ChatGPT para crear un complemento MCP con OAuth
Introduce la URL de tu servicio terminada en /mcp y selecciona OAuth. El /sse del campo vacío es un ejemplo de la interfaz.

21. “0 acciones habilitadas”: faltaba conectar mi usuario

En mi caso, este mensaje no indicaba ningún problema en Cloud Run ni en el servidor MCP.

Yo había creado la aplicación dentro del workspace de ChatGPT Business, pero mi propio usuario todavía no se había conectado a ella. Por eso ChatGPT mostraba:

0 acciones habilitadas

y no me dejaba actualizar sus acciones.

La solución fue abrir la aplicación desde mi usuario de ChatGPT, pulsar Conectar y completar el flujo de Google OAuth. En cuanto hice esa conexión individual, las herramientas MCP aparecieron disponibles.

Aplicación de ChatGPT con 0 acciones habilitadas antes de conectar al usuario
El estado que me hizo perder tiempo: la aplicación existía, pero faltaba conectar mi usuario.

22. Por qué el proxy OAuth es especialmente útil con varios usuarios

Este es el motivo principal por el que elegí esta arquitectura.

El servicio de Cloud Run tiene una única configuración de aplicación OAuth de Google, pero eso no obliga a que todo el mundo utilice un único usuario de Google Ads.

Cada persona completa Google OAuth de forma independiente. El access token utilizado contra Google Ads representa a ese usuario, y list_accessible_customers devuelve los customer IDs a los que ese usuario autenticado tiene acceso directo.

Para un equipo, el modelo queda mucho más limpio:

Manuel -> Google OAuth -> acceso de Google Ads de Manuel
Usuario B -> Google OAuth -> acceso de Google Ads del usuario B
Usuario C -> Google OAuth -> acceso de Google Ads del usuario C

Solo hay un servicio MCP remoto que mantener, pero Google sigue siendo la fuente de verdad para decidir quién puede acceder a cada cuenta.

El matiz de login-customer-id con varios usuarios

El token OAuth identifica al usuario, pero Google Ads necesita además saber por qué ruta de manager se está consultando una cuenta cuando el acceso no es directo.

La regla que acabó funcionando bien es:

Acceso directo a la cuenta cliente
-> omitir login_customer_id

Acceso a través de un MCC
-> login_customer_id = customer ID del MCC utilizado para esa llamada

No confundas esto con el nivel de acceso del usuario. Un usuario con permiso Solo lectura puede consultar datos perfectamente. El problema aparece cuando el header login-customer-id apunta a un manager al que ese usuario no tiene acceso.

23. Verificar el acceso a Google Ads de extremo a extremo

La prueba final no consiste simplemente en comprobar que ChatGPT muestra la aplicación.

Hay que validar que una petición pueda completar todo el recorrido:

ChatGPT
-> endpoint MCP remoto
-> proxy OAuth
-> autorización de Google del usuario
-> Google Ads MCP
-> Google Ads API
-> cuentas accesibles

Una primera consulta muy útil es preguntar qué cuentas de Google Ads puede ver el usuario autenticado.

Eso ejecuta list_accessible_customers y confirma que realmente se está utilizando la autorización específica de ese usuario.

A partir de ahí, el servidor MCP puede lanzar consultas de solo lectura para analizar campañas, rendimiento, productos y otros datos disponibles mediante Google Ads API.

Consulta de cuentas accesibles desde ChatGPT con los identificadores anonimizados
La consulta devuelve las cuentas accesibles. Esta captura también muestra una cuenta que requería configurar el contexto de la cuenta manager; listar una cuenta no garantiza poder consultarla.

24. Usarlo desde otros clientes distintos de ChatGPT

No hay nada en este despliegue de Cloud Run que sea intrínsecamente específico de ChatGPT.

El proyecto oficial de Google Ads MCP documenta configuraciones para otros clientes MCP, y el servidor remoto expone endpoints estándar de discovery MCP/OAuth.

El requisito práctico es que el cliente soporte el transporte remoto y el flujo OAuth utilizados por el servidor. Las implementaciones de los clientes pueden diferir, así que prefiero probar cada una en lugar de asumir que todas se comportarán exactamente igual.

Solución de problemas

Cloud Run no puede acceder a un secreto

Comprueba la service account que ejecuta Cloud Run, no tu propio usuario de Cloud Shell:

gcloud secrets get-iam-policy YOUR_SECRET_NAME

Todos los secretos referenciados por el servicio deben ser legibles por la service account de ejecución.

Fallan los imports de Firestore al construir la imagen

Comprueba que el Dockerfile instala:

.[firestore]

y no únicamente el paquete base.

Los metadatos OAuth anuncian un hostname incorrecto

Obtén la URL real de Cloud Run:

gcloud run services describe "$SERVICE_NAME" \
  --region="$REGION" \
  --format='value(status.url)'

y actualiza GOOGLE_ADS_MCP_BASE_URL.

/mcp devuelve 401

Antes de autenticarte, es lo esperado. Revisa el header WWW-Authenticate y los metadatos del recurso protegido en lugar de interpretar el código 401 por sí solo como un fallo.

ChatGPT muestra cero acciones

Comprueba que la aplicación está conectada para el usuario actual de ChatGPT y que se ha completado el flujo Google OAuth.

A mí me funciona, pero a otro usuario le aparece un error de permisos del MCC

Este fue el segundo problema real que encontré al incorporar a otro usuario del equipo.

El MCP funcionaba con mi cuenta, pero otro usuario con acceso directo y de solo lectura a una cuenta cliente recibía:

User doesn't have permission to access customer.
If you're accessing a client customer, the manager's customer id
must be set in the login-customer-id header.

El problema no era su permiso de solo lectura. Mi Cloud Run tenía configurado globalmente un GOOGLE_ADS_LOGIN_CUSTOMER_ID, por lo que todas las peticiones se enviaban como si cualquier usuario estuviera entrando a través de ese mismo MCC.

La solución fue:

  1. actualizar Google Ads MCP a una revisión con soporte para login_customer_id opcional por llamada;
  2. eliminar GOOGLE_ADS_LOGIN_CUSTOMER_ID de las variables globales de Cloud Run;
  3. omitir login_customer_id cuando el usuario tiene acceso directo;
  4. enviarlo únicamente cuando la cuenta debe consultarse a través de un manager.
gcloud run services update google-ads-mcp \
  --region=europe-southwest1 \
  --remove-env-vars=GOOGLE_ADS_LOGIN_CUSTOMER_ID

He actualizado el MCP, pero ChatGPT sigue mostrando el esquema antiguo

Las herramientas publicadas en ChatGPT no se refrescan continuamente. Después de cambiar el servidor, entra en la administración de la aplicación, abre Herramientas y pulsa Actualizar.

En mi caso, después de refrescar aparecieron las tres herramientas y search_search ya mostraba login_customer_id como STRING | INTEGER | NULL.

Notas de seguridad y mantenimiento

Hay varias decisiones que mantendría exactamente igual si tuviera que reconstruir este despliegue:

  • utilizar una service account dedicada para Cloud Run;
  • guardar los secretos en Secret Manager;
  • no incluir nunca secretos OAuth dentro de la imagen;
  • utilizar una clave de firma persistente entre revisiones de Cloud Run;
  • cifrar los datos OAuth persistidos;
  • fijar la revisión del código de Google Ads MCP utilizada para la imagen;
  • evitar un GOOGLE_ADS_LOGIN_CUSTOMER_ID global en despliegues multiusuario salvo que sea realmente común a todos;
  • refrescar las herramientas de ChatGPT cuando cambie el esquema del servidor MCP;
  • mantener privado el historial bruto de la terminal;
  • ocultar client IDs, customer IDs y credenciales en las capturas cuando no aporten nada al tutorial.

Para un despliegue que vaya a permanecer funcionando mucho tiempo, añadiría además una estrategia periódica de limpieza de registros OAuth expirados en Firestore.

Qué he conseguido y por qué lo he documentado

Al terminar esta configuración tengo un servidor MCP de Google Ads ejecutándose en Cloud Run que puedo conectar a ChatGPT —o a otro cliente MCP compatible— sin ligar todo el servicio a un único usuario de Google.

Cada persona se autentica con su propia cuenta de Google a través de OAuth, por lo que los permisos de Google Ads permanecen donde deben estar: en Google.

Esa capa adicional hace que el despliegue sea bastante más complejo que ejecutar el MCP en local, pero para mi caso compensa. Tengo un endpoint remoto, autenticación por usuario, estado OAuth persistente, secretos gestionados y un servicio que puedo reutilizar desde distintos clientes y con diferentes usuarios autorizados.

Qué comprobaría primero si tuviera que reconstruirlo mañana

Las partes con más posibilidades de dar problemas no son las consultas de Google Ads, sino todo lo que rodea al servidor:

  1. Que la API de Google Ads y las APIs necesarias de Google Cloud estén habilitadas.
  2. Que Firestore exista antes de configurarlo como backend de OAuth.
  3. Que la imagen Docker incluya realmente la dependencia de Firestore.
  4. Que la service account de Cloud Run pueda leer todos los secretos de Secret Manager que necesita.
  5. Que los endpoints de discovery OAuth funcionen antes de empezar a depurar el cliente MCP.
  6. Que un 401 sin autenticar en /mcp puede ser exactamente la respuesta correcta.
  7. En ChatGPT, que hay que conectar la aplicación con tu propio usuario después de crearla.
  8. Si hay varios usuarios, no forzar un login-customer-id global cuando sus rutas de acceso a Google Ads son diferentes.
  9. Después de actualizar el servidor MCP, refrescar las herramientas en ChatGPT para que lea el nuevo esquema.

Los puntos 7, 8 y 9 fueron precisamente los que más tiempo me hicieron perder al pasar del despliegue inicial a un uso real con más personas.

El servidor estaba funcionando, los metadatos OAuth eran correctos, Cloud Run estaba sano y el endpoint MCP se comportaba como debía. Aun así, ChatGPT seguía mostrando cero acciones habilitadas. Lo único que faltaba era conectar la aplicación desde mi propia cuenta de ChatGPT y completar el flujo OAuth.

Es uno de esos detalles que parecen evidentes cuando ya conoces la respuesta y casi invisibles mientras estás depurando el problema.

Y esa es también una de las razones por las que he escrito este artículo.

Muchas veces construyo algo, consigo que funcione, paso al siguiente problema y doy por hecho que dentro de seis meses seguiré recordando cómo encajaban todas las piezas. Normalmente no es así.

Por eso esto es en parte un tutorial para cualquiera que quiera conectar Google Ads con un LLM compatible con MCP y, en parte, documentación para mi yo del futuro: la arquitectura que elegí, los comandos que ejecuté, los errores que encontré, el motivo por el que existe cada pieza y esos pequeños detalles que acabaron siendo importantes.

Si dentro de un año necesito reconstruir todo esto, quiero que esta página sea suficiente.