mcp-meta-ads
MCP server for the Meta Marketing API enabling ad performance reports and operational adjustments like pause/resume and budget changes on client ad accounts, with strict allowlist and read-only safeguards.
README
mcp-1nort-meta
Servidor MCP que expõe a Meta Marketing API ao Claude: relatórios de desempenho de anúncios e ajustes operacionais (pausar/reativar, alterar orçamento), para as contas dos clientes da 1nort.
Roda local, por stdio, em TypeScript sobre Node 20+.
O token configurado é de produção. Ele é de um usuário de sistema do Business Manager e alcança as contas de todos os clientes da agência. A allowlist de contas é o que restringe este servidor a um subconjunto — leia docs/seguranca-mutacoes.md antes de liberar escrita.
Começando
npm ci
cp .env.example .env # e preencha
npm run check # typecheck + testes
npm run build
O .env precisa ter META_AD_ACCOUNT_ALLOWLIST preenchida. Sem ela o servidor não inicia — de
propósito: vazio nunca significa "liberar todas as contas".
Rodar e inspecionar
npm run inspector # abre o MCP Inspector apontando para o servidor
npm run dev # roda direto do TypeScript, sem build
npm start # roda o build de dist/
Registrar no Claude Code
O .mcp.json na raiz já registra dois servidores (só caminhos, nenhum segredo):
meta-ads— respeita o.env.meta-ads-somente-leitura— forçaMETA_READ_ONLY=true. Como o ambiente do processo tem precedência sobre o--env-file, este é somente-leitura por construção, independente do que o.envdiga. Use este como padrão e deixe o outro para sessões deliberadas de alteração.
Ou pela linha de comando:
claude mcp add --scope project meta-ads -- node --env-file=.env dist/index.js
No Claude Desktop, o claude_desktop_config.json precisa de caminhos absolutos (Desktop não tem
raiz de projeto).
Ferramentas
Leitura (sempre disponíveis)
| Ferramenta | O que faz |
|---|---|
meta_server_status |
Guard-rails em vigor: contas permitidas, modo, limites. Zero chamadas à Meta. Consulte antes de propor qualquer alteração. |
meta_list_ad_accounts |
As contas da allowlist, com moeda, fuso e orçamento mínimo. |
meta_get_insights |
Métricas por conta/campanha/conjunto/anúncio, uma linha por dia por padrão. |
meta_list_campaigns |
Campanhas com status, objetivo e orçamento. |
meta_list_ad_sets |
Conjuntos, opcionalmente de uma campanha específica. |
meta_list_ads |
Anúncios com miniatura em 512px; include_media resolve a URL do vídeo. |
Banco (só com DB_FERRAMENTAS_MCP=true)
Leem o banco da 1nort com um usuário SELECT-only. Histórico já coletado, sem consumir cota da Meta.
| Ferramenta | O que faz |
|---|---|
db_scope_summary |
Quantas contas o escopo alcança e até que dia o banco tem dado. |
db_list_clients |
Nome e segmento do cliente de cada conta — traduz "cliente X" em id de conta. |
db_account_daily |
Série diária de uma conta a partir do histórico coletado. |
db_collector_health |
Se a coleta de uma conta está bloqueada e até onde chegou. |
db_top_ads |
Ranking de anúncios do período. |
Nenhuma delas aceita SQL, e nenhuma aceita e-mail. SQL livre não tem como ser seguro aqui: o
usuário somente-leitura enxerga tb_credenciais (com token_kommo, token_ghl, pixel_token) e a
própria tb_meta_ads tem pixel_token; um agente que lê nome de campanha escrito por cliente é superfície
de injeção. Filtrar SQL na aplicação não resolve (CTE, subquery, pg_catalog, ::regclass), e a
restrição correta — REVOKE no Postgres — depende de um direito que não temos. E como nenhuma
ferramenta aceita e-mail, o modelo não consegue trocar o próprio escopo: isso é ação do humano
na tela.
Escrita (só com META_READ_ONLY=false)
| Ferramenta | O que faz |
|---|---|
meta_update_status |
Pausa ou reativa campanha, conjunto ou anúncio. |
meta_update_budget |
Altera orçamento diário ou total de campanha ou conjunto. |
meta_create_campaign |
Cria campanha. Nasce pausada. |
meta_create_ad_set |
Cria conjunto numa campanha existente. Nasce pausado. |
meta_create_ad |
Cria anúncio a partir de publicação, criativo salvo ou URL de imagem. |
meta_create_audience |
Cria público personalizado (Instagram, página, site ou formulário). |
E três de leitura que existem para o modelo não inventar id: meta_list_creatives,
meta_list_publications e meta_list_audiences.
Tudo nasce pausado, e isso não é parâmetro. Nenhuma ferramenta de criação aceita status: um
objeto pausado não gasta, então errar custa zero, e ativar continua sendo meta_update_status —
auditado e com confirmação do host.
special_ad_categories é constante [], fora do schema. A 1nort declara que não anuncia
crédito, emprego, moradia nem tema social. Se isso mudar — financiamento solar seria CREDIT —, o
valor está em SEM_CATEGORIA_ESPECIAL, num lugar só, e alterá-lo é decisão de um humano. Declarar
categoria especial errada é violação de política da Meta, não erro de campanha.
Não existe exclusão nem arquivamento. DELETED e ARCHIVED não estão no schema, então não há
caminho de código que chegue a eles — objeto criado por engano se apaga à mão no Gerenciador.
Duas regras de negócio que não podem ser afrouxadas
1. Leads de formulário e de mensagem nunca se somam.
leads_form ← onsite_conversion.lead_grouped
leads_message ← onsite_conversion.messaging_conversation_started_7d
O action type genérico lead é ignorado — conta formulário por outro critério e diverge de
lead_grouped por ordem de grandeza. Ausência devolve null, nunca 0: "não medido" e "medido e
deu zero" são fatos diferentes. Regra portada de ExtratorAcoesMeta.java, do projeto Citrino, para
que os relatórios dos dois sistemas não divirjam para o mesmo cliente no mesmo dia.
2. Orçamento é informado na unidade MAIOR da moeda.
amount: 250.50 significa R$ 250,50. A Meta armazena em unidade menor (25050) e a conversão é do
servidor. Mandar centavos aqui definiria um orçamento cem vezes maior — por isso a resposta sempre
mostra as duas unidades lado a lado, para o erro saltar aos olhos antes da confirmação.
Segurança
Resumo; o contrato completo está em docs/seguranca-mutacoes.md.
- Allowlist de contas, com comparação exata e tipo branded que o compilador cobra.
- Vínculo objeto → conta antes de toda escrita:
POST /{id}não menciona conta nenhuma, então sem essa releitura a allowlist não valeria nada na escrita. - Duas chaves para aplicar:
META_WRITE_DRY_RUN=falseeconfirm: true. Prévia e aplicação percorrem o mesmo código; muda só ovalidate_only, que é validado pela própria Meta. - Limites de orçamento com falha fechada; moeda fora da tabela é recusada, nunca chutada.
- Auditoria de toda tentativa, inclusive recusas, em
stderr. - Token só no header
Authorization;appsecret_proofem toda chamada.
Variáveis de ambiente
Ver .env.example. As que mais importam:
| Variável | Padrão | Efeito |
|---|---|---|
META_AD_ACCOUNT_ALLOWLIST |
(nenhum) | Obrigatória. Sem ela o servidor não inicia. |
META_READ_ONLY |
true |
Ferramentas de escrita nem são registradas. |
META_WRITE_DRY_RUN |
true |
Toda escrita vira validate_only. |
META_MAX_BUDGET_CHANGE_PCT |
50 |
Variação máxima num ajuste. |
META_MAX_DAILY_BUDGET |
1000 |
Teto diário, unidade maior. |
META_ALLOWLIST_FONTE |
env |
banco faz a allowlist vir do que META_ESCOPO_EMAILo usuário somente-leitura enxerga. |
DB_URL / DB_USER / DB_PASSWORD |
(nenhum) | Ausentes = recurso de banco desligado. |
DB_FERRAMENTAS_MCP |
false |
Registra as cinco ferramentas db_*. |
Interface local
npm run ui # constrói tudo e sobe em http://127.0.0.1:8787
Ou, do Claude Code: /painel — ou /painel email@do-usuario para abrir já escopado.
O painel lê do banco: uma consulta traz o histórico de todas as contas do escopo. Um gestor com 67 contas carrega em menos de 2 s com zero chamadas à Meta. O que só a Graph sabe — teto de gastos, status da conta, moeda, fuso, campanhas ativas — chega ao expandir uma linha, uma conta por vez. Antes eram quatro chamadas por conta na carga da tela, o que a 67 contas seriam 268 chamadas contra um limite de ~15/min.
O chat responde interpretando os números, com as duas famílias de ferramenta. A conversa é
gravada em .conversas/ (fora do git): sobrevive a trocar de aba, a F5, a fechar o navegador e a
reiniciar o servidor. Uma resposta cortada no meio volta marcada como interrompida — e nunca é
retomada sozinha, o que recobraria tokens e poderia reexecutar consultas.
Escopo por usuário
Ativar um e-mail resolve users → tb_usuarios_clientes → tb_clientes → tb_meta_ads e constrói uma
Allowlist nova com essas contas. ROLE_ADMINo usuário somente-leitura enxerga todos os clientes ativos, porque a
plataforma não enumera vínculo de administrador. Contas sem cliente vinculado não entram no escopo
de ninguém — conta sem dono não é de todo mundo —, mas a contagem aparece no aviso.
Ativar é ação de operador, pelo controle da tela ou por ?ativar=. Não é um padrão que o sistema
reconheça dentro de uma mensagem: o agente lê texto escrito por cliente, e uma campanha chamada
[ativar:alguem@1nort...] viraria escalonamento de escopo por um campo que o cliente controla. O
atalho de digitação existe, mas é tratado só no navegador e removido antes do envio.
De onde vem cada número
| Dado | Fonte |
|---|---|
| gasto, impressões, cliques, leads, série diária | banco (tb_meta_anuncios_metricas) |
| cliente, segmento, saúde da coleta | banco |
| teto de gastos, status da conta, moeda, fuso | Meta, sob demanda |
| campanhas ativas, dados de hoje | Meta, sob demanda |
A escolha da tabela foi medida, não suposta (npm run diag:comparar). Em julho/2026, contra a
Graph: tb_dados_trafego perdia 67 leads de mensagem numa conta, 54 de formulário noutra, e não
tinha linha nenhuma para uma terceira que gastou R$ 4.850 no mês. tb_meta_anuncios_metricas bateu
nas três, dentro de 0,2%, e ainda preserva NULL para "não medido" — coisa que o agregado legado
não faz (13.264 linhas, 8.875 zeros onde deveria haver nulo).
Período pedido além da cobertura é recortado e avisado, nunca completado em silêncio: somar um dia do banco com um dia da Graph produz um total que não reconcilia com fonte nenhuma.
Desenvolvimento
npm test # 325 testes
npm run typecheck
npm run fmt
npm run diag:banco # sonda o banco: cobertura, semântica de nulo, escopo de um e-mail
npm run diag:comparar -- act_123 2026-07-01 2026-07-31 # banco × Graph, mesma conta e período
Testes injetam fetch em vez de mockar o cliente Graph, então exercitam o que de fato quebra:
montagem de URL, a string fields, encoding do time_range, cursor de paginação e parsing de
número-string. test/invariantes.test.ts varre todas as URLs emitidas atrás do token, confere que
ele está no Authorization de toda chamada, e falha se alguma coisa escrever em stdout.
Pendências conhecidas
- Conferir os números contra o Gerenciador de Anúncios antes de confiar neles. A Meta interpreta
datas no fuso da conta, e a janela de atribuição padrão molda a contagem de leads. Toda
resposta de
meta_get_insightstrazaccount_timezonee o período resolvido justamente para permitir essa conferência. Pode ser necessário fixaraction_attribution_windowsexplicitamente. - Criar uma campanha sacrificial antes do primeiro teste de escrita real — campanha nova, pausada, orçamento mínimo, sem anúncios. Senão a primeira escrita cai num cliente pagante.
- Ligar Require App Secret nas configurações do app Meta. O
appsecret_proofjá é enviado em toda chamada, mas o benefício (token roubado deixa de servir sozinho) só vale depois de ativar a exigência do lado da Meta. - Token com escopo menor. A allowlist é controle de aplicação sobre uma credencial sem limite técnico. A correção estrutural é um segundo usuário de sistema do BM com acesso só às contas permitidas.
v21.0expira em 2027-01-21. Agendar o bump, de preferência junto com o Citrino, para o conhecimento compartilhado de erros e action types continuar valendo.- Rate limit (~15 req/min por conta). Há teto de páginas e de linhas, e o resultado sempre
informa
truncated. Falta decidir se vale acrescentar pausa entre páginas. node_modules/edist/dentro do OneDrive sofrem varredura de sincronização contínua. Recomendo excluir os dois do sync, ou mover o projeto para fora do OneDrive.- O agregado
tb_dados_trafegoestá errado ondetb_meta_anuncios_metricasestá certo. Vale investigar o coletor ou aposentar a tabela — hoje ela só alimentavw_desempenho_conta_diario, que também expõe umtotal_leadssomado e deveria sair de cena junto. Ver docs/proposta-tabelas.sql. .conversas/dentro do OneDrive significa transcript com dado de cliente (gasto, nome de campanha, id de conta) sincronizando para uma nuvem pessoal. Mesmo problema do item 7, com conteúdo mais sensível.- Postgres de produção sem TLS na internet pública. A senha e todas as linhas viajam em texto
claro. Não é bug deste projeto, mas vale levar a quem cuida do
db02.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。