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. He conservado la revisión exacta del código y los comandos relevantes 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. Definir el proyecto de Google Cloud y la región
Hice todo el despliegue desde Cloud Shell.
Primero guardé el proyecto activo y definí la 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. Habilitar las APIs de Google Cloud
Estas son las APIs que habilité para el despliegue:
gcloud services enable \
googleads.googleapis.com \
run.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com \
secretmanager.googleapis.com \
firestore.googleapis.com
Google Ads MCP necesita la API de Google Ads. El resto de servicios se utilizan para construir la imagen, ejecutarla, almacenar secretos y persistir el estado OAuth.
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.
Creé la base de datos por defecto de Firestore en modo Native, edición Standard y en la misma región:
gcloud firestore databases create \
--database="(default)" \
--location="$REGION" \
--edition=standard \
--type=firestore-native
Después la verifiqué:
gcloud firestore databases list
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.
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"
Si accedes a las cuentas de anunciantes a través de una cuenta manager de Google Ads, guarda también su customer ID. Lo añadiremos después, cuando el servicio básico ya esté funcionando.
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.
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"
10. Clonar el servidor oficial y fijar la revisión
No quería desplegar simplemente lo que hubiera en main en ese momento.
La revisión exacta que utilicé fue:
7a40eae9655194a84d5291c63af817bb20d02ea8
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 7a40eae9655194a84d5291c63af817bb20d02ea8
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 versión que realmente desplegué.
11. Añadir la dependencia de Firestore a la imagen Docker
En esa revisión, el Dockerfile instalaba el paquete base. El backend de Firestore necesita el extra correspondiente.
Modifiqué la línea de instalación con:
sed -i 's|uv pip install --system \.|uv pip install --system .[firestore]|' Dockerfile
Lo comprobé con:
grep -n "uv pip install" Dockerfile
La línea relevante debe quedar así:
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:7a40eae"
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 sesión, esta compilación instaló google-ads-mcp 0.0.3, FastMCP 4.0.5 y las dependencias de Firestore presentes en esa revisión fijada.
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"
Si accedes a las cuentas de anunciantes a través de una cuenta manager, añade también su customer ID:
gcloud run services update "$SERVICE_NAME" \
--region="$REGION" \
--update-env-vars="GOOGLE_ADS_LOGIN_CUSTOMER_ID=YOUR_MANAGER_CUSTOMER_ID"
Utiliza el customer ID numérico sin guiones.
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.
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.
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.
21. El problema de “0 acciones habilitadas”
Esta fue la parte final y más confusa de toda la instalación.
ChatGPT mostraba:
0 acciones habilitadas
y no me permitía actualizar las acciones porque indicaba que la aplicación tenía que estar conectada primero.
En ese momento yo ya había comprobado que:
- la revisión de Cloud Run estaba sana;
- /.well-known/oauth-protected-resource/mcp devolvía metadatos válidos;
- /.well-known/oauth-authorization-server devolvía metadatos válidos;
- /mcp respondía con el challenge OAuth esperado.
Por tanto, reconstruir el contenedor o seguir tocando Cloud Run era ir en la dirección equivocada.
La solución fue abrir la aplicación en ChatGPT, pulsar Conectar y completar el flujo Google OAuth con mi propio usuario.
En cuanto lo hice, las herramientas MCP aparecieron disponibles.
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.
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.
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.
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;
- 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:
- Que la API de Google Ads y las APIs necesarias de Google Cloud estén habilitadas.
- Que Firestore exista antes de configurarlo como backend de OAuth.
- Que la imagen Docker incluya realmente la dependencia de Firestore.
- Que la service account de Cloud Run pueda leer todos los secretos de Secret Manager que necesita.
- Que los endpoints de discovery OAuth funcionen antes de empezar a depurar el cliente MCP.
- Que un 401 sin autenticar en /mcp puede ser exactamente la respuesta correcta.
- Y, en ChatGPT, que hay que conectar la aplicación con tu propio usuario después de crearla.
Ese último punto me hizo perder bastante más tiempo del que debería.
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.
Comentarios