metaads-pro/mcp-server/index.ts
Marcio Bevervanso 234314d1c6 Publish MetaAds Pro: AI-driven Meta Ads campaign management dashboard
Full app with AI campaign creation/copy/creative generation, dashboard,
campaign automation engine, Meta OAuth login, and a redesigned landing page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 19:30:33 -03:00

243 lines
9.2 KiB
JavaScript

#!/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<string, unknown>) => ({
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);