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>
243 lines
9.2 KiB
JavaScript
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);
|