#!/usr/bin/env node /** * MetaAds Pro — MCP Server * * Exposes Meta Marketing API operations as MCP tools. * Can be connected to Claude Desktop, Cursor, or any MCP client. * * Tools: * - list_campaigns: Lists all active/paused campaigns * - get_campaign_insights: Gets metrics for a specific campaign * - get_account_overview: Gets account-level metrics summary * - pause_campaign: Pauses a campaign * - resume_campaign: Resumes a paused campaign * - update_budget: Updates a campaign's daily budget */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; // @ts-expect-error - CJS module const bizSdk = await import('facebook-nodejs-business-sdk'); const accessToken = process.env.META_ACCESS_TOKEN; const adAccountId = process.env.META_AD_ACCOUNT_ID; if (!accessToken || !adAccountId) { console.error('Error: META_ACCESS_TOKEN and META_AD_ACCOUNT_ID must be set'); process.exit(1); } // Init Meta API Object.defineProperty(bizSdk.default.FacebookAdsApi, 'VERSION', { get: () => 'v20.0' }); bizSdk.default.FacebookAdsApi.init(accessToken); const AdAccount = bizSdk.default.AdAccount; const Campaign = bizSdk.default.Campaign; // ---- MCP Server ---- const server = new Server( { name: 'metaads-pro', version: '1.0.0' }, { capabilities: { tools: {} } } ); // ---- Tool Definitions ---- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'list_campaigns', description: 'Lista todas as campanhas ativas e pausadas da conta de anúncios do Meta/Facebook', inputSchema: { type: 'object' as const, properties: {}, required: [], }, }, { name: 'get_campaign_insights', description: 'Busca métricas detalhadas (spend, CTR, CPC, CPM, leads, ROAS) de uma campanha específica', inputSchema: { type: 'object' as const, properties: { campaign_id: { type: 'string', description: 'ID da campanha' }, date_preset: { type: 'string', enum: ['today', 'yesterday', 'last_7d', 'last_14d', 'last_30d', 'this_month', 'lifetime'], description: 'Período de tempo (padrão: last_30d)', }, }, required: ['campaign_id'], }, }, { name: 'get_account_overview', description: 'Retorna um resumo geral da conta: gasto total, leads, ROAS, CTR, CPC, CPM', inputSchema: { type: 'object' as const, properties: { date_preset: { type: 'string', enum: ['today', 'yesterday', 'last_7d', 'last_14d', 'last_30d', 'this_month', 'lifetime'], description: 'Período de tempo (padrão: last_30d)', }, }, required: [], }, }, { name: 'pause_campaign', description: 'Pausa uma campanha ativa', inputSchema: { type: 'object' as const, properties: { campaign_id: { type: 'string', description: 'ID da campanha para pausar' }, }, required: ['campaign_id'], }, }, { name: 'resume_campaign', description: 'Reativa uma campanha pausada', inputSchema: { type: 'object' as const, properties: { campaign_id: { type: 'string', description: 'ID da campanha para reativar' }, }, required: ['campaign_id'], }, }, { name: 'update_budget', description: 'Altera o orçamento diário de uma campanha (valor em reais)', inputSchema: { type: 'object' as const, properties: { campaign_id: { type: 'string', description: 'ID da campanha' }, daily_budget_brl: { type: 'number', description: 'Novo orçamento diário em R$ (ex: 50.00)' }, }, required: ['campaign_id', 'daily_budget_brl'], }, }, ], })); // ---- Tool Handlers ---- server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'list_campaigns': { const account = new AdAccount(adAccountId); const campaigns = await account.getCampaigns( ['id', 'name', 'status', 'objective', 'daily_budget', 'created_time'], { limit: 100, filtering: [{ field: 'status', operator: 'IN', value: ['ACTIVE', 'PAUSED'] }] } ); const data = campaigns.map((c: Record) => ({ id: c.id, name: c.name, status: c.status, objective: c.objective, daily_budget: `R$ ${(parseInt(String(c.daily_budget || '0'), 10) / 100).toFixed(2)}`, })); return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }; } case 'get_campaign_insights': { const { campaign_id, date_preset = 'last_30d' } = args as { campaign_id: string; date_preset?: string }; const campaign = new Campaign(campaign_id); const insights = await campaign.getInsights( ['campaign_name', 'spend', 'ctr', 'cpc', 'cpm', 'impressions', 'clicks', 'actions', 'purchase_roas'], { date_preset, limit: 1 } ); if (!insights || insights.length === 0) { return { content: [{ type: 'text', text: 'Nenhum dado de insights encontrado para este período.' }] }; } const i = insights[0]; const leads = i.actions?.find((a: { action_type: string }) => a.action_type === 'lead')?.value || '0'; const roas = i.purchase_roas?.[0]?.value || '0'; const summary = { campaign: i.campaign_name, spend: `R$ ${parseFloat(i.spend || '0').toFixed(2)}`, impressions: parseInt(i.impressions || '0', 10), clicks: parseInt(i.clicks || '0', 10), ctr: `${parseFloat(i.ctr || '0').toFixed(2)}%`, cpc: `R$ ${parseFloat(i.cpc || '0').toFixed(2)}`, cpm: `R$ ${parseFloat(i.cpm || '0').toFixed(2)}`, leads: parseInt(leads, 10), roas: parseFloat(roas).toFixed(2) + 'x', }; return { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] }; } case 'get_account_overview': { const { date_preset = 'last_30d' } = args as { date_preset?: string }; const account = new AdAccount(adAccountId); const insights = await account.getInsights( ['spend', 'ctr', 'cpc', 'cpm', 'impressions', 'clicks', 'actions', 'purchase_roas'], { date_preset, level: 'account', limit: 1 } ); if (!insights || insights.length === 0) { return { content: [{ type: 'text', text: 'Nenhum dado disponível para este período.' }] }; } const i = insights[0]; const leads = i.actions?.find((a: { action_type: string }) => a.action_type === 'lead')?.value || '0'; const roas = i.purchase_roas?.[0]?.value || '0'; const summary = { total_spend: `R$ ${parseFloat(i.spend || '0').toFixed(2)}`, impressions: parseInt(i.impressions || '0', 10), clicks: parseInt(i.clicks || '0', 10), ctr: `${parseFloat(i.ctr || '0').toFixed(2)}%`, cpc: `R$ ${parseFloat(i.cpc || '0').toFixed(2)}`, cpm: `R$ ${parseFloat(i.cpm || '0').toFixed(2)}`, leads: parseInt(leads, 10), roas: parseFloat(roas).toFixed(2) + 'x', }; return { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] }; } case 'pause_campaign': { const { campaign_id } = args as { campaign_id: string }; const campaign = new Campaign(campaign_id); await campaign.update([], { [Campaign.Fields.status]: 'PAUSED' }); return { content: [{ type: 'text', text: `✅ Campanha ${campaign_id} pausada com sucesso.` }] }; } case 'resume_campaign': { const { campaign_id } = args as { campaign_id: string }; const campaign = new Campaign(campaign_id); await campaign.update([], { [Campaign.Fields.status]: 'ACTIVE' }); return { content: [{ type: 'text', text: `✅ Campanha ${campaign_id} reativada com sucesso.` }] }; } case 'update_budget': { const { campaign_id, daily_budget_brl } = args as { campaign_id: string; daily_budget_brl: number }; const budgetCents = Math.round(daily_budget_brl * 100); const campaign = new Campaign(campaign_id); await campaign.update([], { [Campaign.Fields.daily_budget]: budgetCents }); return { content: [{ type: 'text', text: `✅ Orçamento da campanha ${campaign_id} atualizado para R$ ${daily_budget_brl.toFixed(2)}.` }], }; } default: return { content: [{ type: 'text', text: `Tool "${name}" não encontrada.` }], isError: true }; } } catch (error) { const msg = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: 'text', text: `❌ Erro: ${msg}` }], isError: true }; } }); // ---- Start Server ---- async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('MetaAds Pro MCP Server running on stdio'); } main().catch(console.error);