Gabriel Herencia

~/insights / Herramientas de Desarrollo

El MCP oficial de Meta Ads: instalarlo sin crear una app, y cómo funciona el login desde local

Meta publicó su servidor MCP oficial de Ads. Su documentación te manda a crear una app de desarrollador y registrar una redirect URI a mano. Su metadata OAuth dice que nada de eso hace falta. Cómo lo verifiqué, el flujo de login completo desde una máquina local y las trampas de las primeras consultas.

·9 min de lectura
Herramientas de DesarrolloIAClaude CodeAutomatizaciónMCPoauthmeta-ads
Portada: El MCP oficial de Meta Ads: instalarlo sin crear una app, y cómo funciona el login desde local

Meta liberó su servidor MCP oficial para la Marketing API: un endpoint hospedado por ellos, con ~98 herramientas de reporting, creación y gestión de campañas, catálogos y diagnóstico de señales. No es un wrapper de la comunidad — lo sirve Meta, en https://mcp.facebook.com/ads.

Lo instalé. El proceso tomó menos de lo que dice la documentación, y esa diferencia es el motivo de este post.

La documentación pide un App ID

La guía oficial de Get started plantea un camino con fricción real:

  1. Crear o reutilizar una app en el portal de desarrolladores.
  2. Añadirle el caso de uso «Create & manage ads with ads MCP server».
  3. Configurar la redirect URI a mano en los ajustes de Facebook Login for Business, haciéndola coincidir con el cliente MCP que se vaya a usar.
  4. Solo entonces, conectar pasando el App ID como client_id.

Para Claude Code, la doc da textualmente este comando:

claude mcp add --transport http --client-id <META_APP_ID> meta-ads https://mcp.facebook.com/ads

Ese <META_APP_ID> es el peaje. Implica crear una app, esperar que el caso de uso se propague y adivinar qué puerto va a usar el cliente para el callback antes de registrarlo.

No hizo falta nada de eso.

Antes de la doc, la metadata

Hace poco conté oauth-2-1-para-un-mcp-remoto-tres-bugs-y-un-bloqueo-que-no-salia-en-los-logs: implementar OAuth 2.1 completo del lado servidor para mi propio MCP. Una de las conclusiones de ese trabajo fue que RFC 7591 —registro dinámico de cliente— no es un lujo: sin registration_endpoint, un cliente nativo se queda sin nada que pre-registrar, porque elige su puerto de callback al vuelo.

Esta vez me tocaba el lado contrario: ser el cliente. Y ese conocimiento se pagó solo, porque en lugar de abrir el portal de desarrolladores hice tres peticiones.

Primero, tocar el endpoint sin credencial. Un servidor MCP que cumple la especificación tiene que responder 401 con una cabecera que explique adónde ir:

curl -s -i -X POST "https://mcp.facebook.com/ads" \
  -H "Content-Type: application/json" \
  --data-raw '{"jsonrpc":"2.0","method":"tools/list","id":1}'
HTTP/2 401
www-authenticate: Bearer
  resource_metadata="https://mcp.facebook.com/.well-known/oauth-protected-resource/ads",
  scope="ads_management ads_read catalog_management business_management
         pages_show_list instagram_basic ads_mcp_management"

Ahí ya tenemos dos regalos: la ruta de descubrimiento y la lista exacta de scopes que el servidor va a pedir. Nótese el path del recurso después del segmento well-known — esa forma contraintuitiva de RFC 9728 que todo el mundo se equivoca la primera vez.

Segundo, seguir el rastro hasta la metadata del servidor de autorización:

curl -s "https://mcp.facebook.com/.well-known/oauth-authorization-server/ads"
{
  "issuer": "https://www.facebook.com",
  "authorization_endpoint": "https://www.facebook.com/v26.0/dialog/oauth",
  "token_endpoint": "https://graph.facebook.com/v26.0/oauth/access_token",
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"],
  "registration_endpoint": "https://mcp.facebook.com/.well-known/register/ads"
}

Ahí está, en la última línea. registration_endpoint. Meta soporta registro dinámico y su documentación de Get started no lo menciona en ninguna parte.

Y la línea de arriba lo confirma desde otro ángulo: token_endpoint_auth_methods_supported: ["none"] significa cliente público sin secreto. Es exactamente el perfil de una CLI que corre en la máquina del usuario. Si el diseño esperado fuera «usa tu propia app confidencial», ese campo diría otra cosa.

La documentación describe el camino que el proveedor quiere que recorras. La metadata describe el que realmente soporta. Cuando no coinciden, la metadata es la que el servidor va a obedecer.

La instalación, en una línea

claude mcp add --transport http --callback-port 8123 -s user \
  meta-ads https://mcp.facebook.com/ads

Sin --client-id. Desglosando las decisiones:

  • --transport http — es un servidor remoto hospedado, no un proceso local por stdio.
  • -s user — alcance de usuario, no de proyecto. Una cuenta publicitaria no pertenece a un repositorio; la quiero disponible desde cualquier directorio.
  • --callback-port 8123 — el puerto fijo para el callback de OAuth.

Ese último flag merece una explicación, porque con registro dinámico técnicamente no hace falta. Por defecto el cliente toma un puerto efímero cualquiera, y con DCR eso funciona: el cliente se registra en ese momento declarando el puerto que consiguió.

Lo fijo de todos modos, como seguro. Si algún día el registro dinámico deja de estar disponible —o si hay que pasar a una app propia por política de la organización—, el único cambio es registrar http://localhost:8123/callback en los ajustes de la app y añadir --client-id. El resto de la configuración no se toca. Cuesta un flag y ahorra una migración.

Cómo funciona el login desde local

Esta es la parte que más confunde, porque el token nunca llegas a tocarlo. El flujo completo:

renderizando diagrama…

En la práctica son dos pasos humanos: correr el comando de instalación, y después /mcpAuthenticate. Se abre el navegador, inicias sesión con tu cuenta de Facebook o tu Meta Managed Account, apruebas los permisos y listo.

La pieza que lo hace posible es el servidor local efímero. El cliente MCP levanta un HTTP en localhost:8123 durante los segundos que dura la autorización, recibe el code en el redirect, lo canjea contra el token endpoint y se apaga. El código de autorización viaja por la barra de direcciones del navegador hacia la propia máquina — nunca sale de ella.

Y PKCE es lo que hace ese diseño seguro sin secreto. El cliente genera un verificador aleatorio, manda su hash SHA-256 al pedir la autorización, y presenta el verificador original al canjear el código. Un code interceptado no sirve de nada sin él. Por eso token_endpoint_auth_methods_supported: ["none"] no es una debilidad: la prueba de posesión la aporta PKCE, no un secreto compartido que de todas formas habría que guardar en algún lugar.

El detalle que rompe en WSL y por SSH

Yo corro Claude Code dentro de WSL2. Ahí no siempre hay navegador ni servidor gráfico, y el flujo tiene una salida prevista para eso: el cliente detecta que no puede abrir un navegador local e imprime la URL de autorización en lugar de intentarlo.

Se abre esa URL en el navegador del host, se completa el login, y cuando el redirect falle con un error de conexión —porque el localhost del navegador de Windows y el de WSL pueden no ser el mismo— se copia la URL completa de la barra de direcciones y se pega de vuelta en el prompt. Incluye el code, que es todo lo que el cliente necesita.

Requiere una terminal interactiva para el pegado. Por SSH, conectarse con ssh -t.

Dónde queda la credencial

En el almacén de credenciales local del cliente, no en el archivo de configuración del MCP. Al abrir la configuración del servidor solo aparece esto:

meta-ads:
  Scope: User config (available in all your projects)
  Status: ! Needs authentication   →  ✔ Connected
  Type: http
  URL: https://mcp.facebook.com/ads
  OAuth: callback_port 8123

Ni un secreto a la vista. Esa es exactamente la diferencia que perseguía cuando migré mi propio MCP de un token estático a OAuth: un token pegado a mano termina en un fichero de configuración, en el historial del shell y en los logs de cualquier proxy intermedio. Aquí no hay nada que pegar, y revocar el acceso se hace desde los ajustes de la cuenta de Meta sin tocar la máquina.

Las trampas de las primeras consultas

Conectado no es lo mismo que funcionando. Tres cosas que descubrí probando.

No todas las cuentas están habilitadas

La primera llamada devolvió 40 cuentas publicitarias. Pero cada una viene con dos banderas que hay que leer antes de usarla:

  • is_ads_mcp_enabled — el despliegue de la funcionalidad es gradual. Varias cuentas activas y sanas devuelven false con el mensaje de que hay que esperar al rollout.
  • is_queryable + not_queryable_reason — cuentas en estado UNSETTLED o CLOSED no admiten consultas de entidades.

Es decir: tener acceso a la cuenta no implica poder consultarla por esta vía. Conviene filtrar por ambas banderas antes de intentar cualquier cosa, en lugar de descubrirlo con un error.

Los errores de campo son la mejor documentación

Pedí spend y campaign_name. El servidor rechazó la llamada — y en el mensaje de error devolvió la lista completa de campos válidos para ese nivel:

Unsupported field(s) at level 'ad': campaign_name, adset_name.
Supported fields are: actions:comment, actions:like, ..., amount_spent,
campaign_id, clicks, cost_per_result, cpc, cpm, ctr, delivery,
effective_status, frequency, id, impressions, lead, name, reach,
results, ...

Dos aprendizajes de un solo error: la métrica de gasto se llama amount_spent, no spend; y los nombres de entidades padre no existen a nivel de anuncio, solo sus IDs.

Un error de validación que devuelve el catálogo completo de campos válidos vale más que media página de documentación. Es la API enseñando a usarla en el momento exacto en que hace falta.

Los campos además cambian según el nivel (ad_account, campaign, adset, ad), así que el mismo error a distinto nivel devuelve una lista distinta.

«Activa» no es lo mismo que «entregando»

Una campaña con effective_status: ACTIVE puede tener todos sus conjuntos de anuncios pausados y no estar mostrando nada. Para confirmar entrega real hay que bajar de nivel y mirar el objeto delivery:

{ "status": "active", "substatuses": ["active"] }

Lo comprobé en una cuenta propia: la campaña figuraba activa, y efectivamente había un anuncio entregando. Pero la verificación es el punto — el estado del padre es una condición necesaria, no suficiente.

Y ahí apareció el hallazgo que ningún panel me habría señalado. La campaña reportaba 0 resultados con un costo por resultado de 0, mientras el campo lead devolvía 3. La explicación es que el evento configurado como «resultado» de la campaña era uno de sitio web que no se estaba disparando, mientras los leads llegaban por otra vía. El Administrador de Anuncios mostraba cero; el costo real por lead rondaba los siete soles.

Es la misma idea de siempre — verdad-de-fondo-depurar-con-evidencia — pero aplicada a métricas: el panel no miente, mide otra cosa. Cruzar dos campos que deberían coincidir es lo que revela la brecha.

Lo que me llevo

Lee la metadata antes que la doc. Cinco minutos de curl sobre los .well-known me ahorraron crear una app, esperar la propagación de un caso de uso y registrar una redirect URI. La documentación de un proveedor grande describe el camino soportado oficialmente, que casi nunca es el conjunto completo de lo que el servidor acepta.

Implementar un protocolo del lado servidor te vuelve mejor cliente. Yo supe qué buscar —registration_endpoint, token_endpoint_auth_methods_supported, code_challenge_methods_supported— porque hace poco me tocó implementarlos. El conocimiento no era de Meta ni de un producto: era del estándar. Es el argumento más honesto que conozco a favor de meterse en las tripas de las herramientas que uno usa.

Fija el puerto de callback aunque no haga falta. Es un flag hoy y es una migración menos mañana.

Que la credencial no exista como archivo. Ningún token pegado en una config, ningún secreto en el historial del shell, revocación desde el proveedor sin tocar la máquina. La misma mentalidad de como-construi-mi-propio-mcp-de-postgresql y de automatizar-las-barreras-mcp-hooks-skills: que la regla viva en la infraestructura y no en mi disciplina.

Verifica el primer dato que devuelve, no solo que conecte. Banderas por cuenta, campos que cambian según el nivel, estados que parecen decir una cosa y dicen otra. Un MCP conectado con 98 herramientas es una superficie grande, y la única forma de saber qué hace de verdad es pedirle algo cuya respuesta se pueda contrastar de forma independiente.