// ============================================ // MCP Tools — Typed wrappers for Meta MCP tools // ============================================ import { callMetaTool, type Client } from './client'; // ---- Helper: extract text content from MCP result ---- function extractContent(result: Awaited>): unknown { if (!result.content || !Array.isArray(result.content)) return null; const textPart = result.content.find((c: { type: string }) => c.type === 'text'); if (!textPart || !('text' in textPart)) return null; try { return JSON.parse(textPart.text as string); } catch { return textPart.text; } } function requireId(value: unknown, label: string) { if (!value || typeof value !== 'string') { throw new Error(`${label} obrigatório não informado`); } } function getDefaultOptimizationGoal(objective: string, destination?: string) { if (destination === 'WHATSAPP') { return 'CONVERSATIONS'; } switch (objective) { case 'OUTCOME_TRAFFIC': return 'LINK_CLICKS'; case 'OUTCOME_AWARENESS': return 'REACH'; case 'OUTCOME_ENGAGEMENT': return 'POST_ENGAGEMENT'; case 'OUTCOME_LEADS': return 'LEAD_GENERATION'; case 'OUTCOME_SALES': // OFFSITE_CONVERSIONS requires a pixel_id. Since we don't have it in MVP, fallback to LINK_CLICKS return 'LINK_CLICKS'; default: return 'LINK_CLICKS'; } } function getDefaultBillingEvent(objective: string) { switch (objective) { case 'OUTCOME_TRAFFIC': case 'OUTCOME_AWARENESS': case 'OUTCOME_ENGAGEMENT': case 'OUTCOME_LEADS': case 'OUTCOME_SALES': default: return 'IMPRESSIONS'; } } function normalizeBidCapParams(params: { bid_strategy?: string; bid_amount?: number; }) { if (!params.bid_strategy && !params.bid_amount) return {}; const bidStrategy = params.bid_strategy || 'LOWEST_COST_WITH_BID_CAP'; if (bidStrategy === 'LOWEST_COST_WITH_BID_CAP' && !params.bid_amount) { throw new Error('Para campanha Bid Cap, informe bid_amount em centavos. Exemplo: R$10,00 = 1000.'); } return { bid_strategy: bidStrategy, bid_amount: params.bid_amount, }; } // ============================== // Campaign Management // ============================== export async function listCampaigns(client: Client, accountId?: string, datePreset: string = 'last_30d') { const args: Record = { date_preset: datePreset }; if (accountId) args.ad_account_id = accountId; const result = await callMetaTool(client, 'list_campaigns', args); return extractContent(result); } export async function getCampaignDetails( client: Client, campaignId: string, datePreset: string = 'last_30d' ) { requireId(campaignId, 'campaignId'); const result = await callMetaTool(client, 'get_campaign_details', { campaign_id: campaignId, date_preset: datePreset, }); return extractContent(result); } export async function pauseCampaign(client: Client, campaignId: string) { requireId(campaignId, 'campaignId'); const result = await callMetaTool(client, 'update_campaign', { campaign_id: campaignId, status: 'PAUSED', }); return extractContent(result); } export async function resumeCampaign(client: Client, campaignId: string) { requireId(campaignId, 'campaignId'); const result = await callMetaTool(client, 'update_campaign', { campaign_id: campaignId, status: 'ACTIVE', }); return extractContent(result); } /** * Mantido por compatibilidade. * ATENÇÃO: orçamento normalmente é no AdSet, não na campanha. * Para atualizar orçamento real, prefira updateAdSetBudget(). */ export async function updateCampaignBudget( client: Client, campaignId: string, dailyBudget: number ) { requireId(campaignId, 'campaignId'); throw new Error( `updateCampaignBudget não deve atualizar daily_budget direto na campanha. Use listAdSets(client, campaignId), pegue o adset_id e chame updateAdSetBudget(client, adsetId, dailyBudget).` ); } // ============================== // Insights & Analytics // ============================== export async function getCampaignInsights( client: Client, campaignId: string, datePreset: string = 'last_7d' ) { requireId(campaignId, 'campaignId'); const result = await callMetaTool(client, 'get_insights_for_campaign', { campaign_id: campaignId, date_preset: datePreset, }); return extractContent(result); } export async function getAccountInsights( client: Client, datePreset: string = 'last_7d', accountId?: string ) { const args: Record = { date_preset: datePreset, }; if (accountId) args.ad_account_id = accountId; const result = await callMetaTool(client, 'get_insights_for_account', args); return extractContent(result); } export async function getAdAccounts(client: Client) { const result = await callMetaTool(client, 'list_ad_accounts'); return extractContent(result); } export async function getPages(client: Client) { const result = await callMetaTool(client, 'list_pages'); return extractContent(result); } // ============================== // Ad Set Management // ============================== export async function listAdSets(client: Client, campaignId: string) { requireId(campaignId, 'campaignId'); const result = await callMetaTool(client, 'list_adsets', { campaign_id: campaignId, }); return extractContent(result); } export async function updateAdSetBudget( client: Client, adSetId: string, dailyBudget: number ) { requireId(adSetId, 'adSetId'); const result = await callMetaTool(client, 'update_adset', { adset_id: adSetId, daily_budget: dailyBudget, }); return extractContent(result); } export async function pauseAdSet(client: Client, adSetId: string) { requireId(adSetId, 'adSetId'); const result = await callMetaTool(client, 'update_adset', { adset_id: adSetId, status: 'PAUSED', }); return extractContent(result); } export async function resumeAdSet(client: Client, adSetId: string) { requireId(adSetId, 'adSetId'); const result = await callMetaTool(client, 'update_adset', { adset_id: adSetId, status: 'ACTIVE', }); return extractContent(result); } // ============================== // Campaign Creation // ============================== export async function createCampaign( client: Client, params: { name: string; objective: string; status?: string; ad_account_id?: string; } ) { const result = await callMetaTool(client, 'create_campaign', { name: params.name, objective: params.objective, status: params.status || 'PAUSED', ad_account_id: params.ad_account_id, }); return extractContent(result); } export async function createAdSet( client: Client, params: { name: string; campaign_id: string; daily_budget: number; targeting: Record; objective: string; optimization_goal?: string; billing_event?: string; status?: string; promoted_object?: Record; destination_type?: string; bid_strategy?: string; bid_amount?: number; start_time?: string; end_time?: string; ad_account_id?: string; } ) { const bidCapParams: any = {}; if (params.bid_strategy) bidCapParams['bid_strategy'] = params.bid_strategy; if (params.bid_amount) bidCapParams['bid_amount'] = params.bid_amount; const result = await callMetaTool(client, 'create_adset', { name: params.name, campaign_id: params.campaign_id, daily_budget: params.daily_budget, targeting: params.targeting, objective: params.objective, optimization_goal: params.optimization_goal || getDefaultOptimizationGoal(params.objective, params.destination_type), billing_event: params.billing_event || getDefaultBillingEvent(params.objective), status: params.status || 'PAUSED', promoted_object: params.promoted_object, destination_type: params.destination_type, start_time: params.start_time, end_time: params.end_time, ad_account_id: params.ad_account_id, ...bidCapParams, }); return extractContent(result); } export async function createAd( client: Client, params: { name: string; adset_id: string; creative_id: string; status?: string; ad_account_id?: string; } ) { const result = await callMetaTool(client, 'create_ad', { name: params.name, adset_id: params.adset_id, creative_id: params.creative_id, status: params.status || 'PAUSED', ad_account_id: params.ad_account_id, }); return extractContent(result); } export async function createAdCreative( client: Client, params: { name: string; page_id: string; instagram_actor_id?: string; image_hash?: string; image_hashes?: string[]; link: string; message: string; headline: string; description?: string; call_to_action_type?: string; ad_account_id?: string; } ) { const result = await callMetaTool(client, 'create_ad_creative', { name: params.name, page_id: params.page_id, instagram_actor_id: params.instagram_actor_id, image_hash: params.image_hash, image_hashes: params.image_hashes, link: params.link, message: params.message, headline: params.headline, description: params.description || '', call_to_action_type: params.call_to_action_type || 'LEARN_MORE', ad_account_id: params.ad_account_id, }); return extractContent(result); } export async function createWhatsappAdCreative( client: Client, params: { name: string; page_id: string; instagram_actor_id?: string; image_hash?: string; image_hashes?: string[]; message: string; headline: string; description?: string; link?: string; whatsapp_link?: string; whatsapp_phone_number?: string; ad_account_id?: string; } ) { const result = await callMetaTool(client, 'create_whatsapp_ad_creative', { name: params.name, page_id: params.page_id, instagram_actor_id: params.instagram_actor_id, image_hash: params.image_hash, image_hashes: params.image_hashes, message: params.message, headline: params.headline, description: params.description || '', link: params.link, whatsapp_link: params.whatsapp_link, whatsapp_phone_number: params.whatsapp_phone_number, ad_account_id: params.ad_account_id, }); return extractContent(result); } export async function uploadAdImage( client: Client, imageUrl?: string, imageBytes?: string, adAccountId?: string ) { const params: Record = {}; if (imageUrl) params.image_url = imageUrl; if (imageBytes) params.image_bytes = imageBytes; if (adAccountId) params.ad_account_id = adAccountId; const result = await callMetaTool(client, 'upload_ad_image', params); return extractContent(result); } // ============================== // Full Campaign Creation Pipeline // ============================== export interface FullCampaignParams { campaign_name: string; objective: string; daily_budget: number; targeting: Record; page_id: string; instagram_actor_id?: string; image_urls?: string[]; image_bytes?: string[]; link: string; headline: string; primary_text: string; description: string; cta: string; ad_account_id?: string; bid_strategy?: string; bid_amount?: number; optimization_goal?: string; billing_event?: string; destination_type?: string; promoted_object?: Record; is_whatsapp?: boolean; whatsapp_link?: string; whatsapp_phone_number?: string; } export async function createFullCampaign(client: Client, params: FullCampaignParams) { // Step 1: Campaign const campaign = await createCampaign(client, { name: params.campaign_name, objective: params.objective, ad_account_id: params.ad_account_id, }); const campaignId = (campaign as any)?.id; if (!campaignId) { throw new Error(`[Step 1 - Campaign] Falha: ${JSON.stringify(campaign)}`); } // Step 2: Ad Set const destinationType = params.destination_type || (params.is_whatsapp ? 'WHATSAPP' : undefined); let promotedObject = params.promoted_object; if (!promotedObject && params.is_whatsapp) { promotedObject = { page_id: params.page_id, }; } const optimizationGoal = params.optimization_goal || getDefaultOptimizationGoal(params.objective, destinationType); const billingEvent = params.billing_event || getDefaultBillingEvent(params.objective); const adSet = await createAdSet(client, { name: `${params.campaign_name} — Ad Set`, campaign_id: campaignId, daily_budget: params.daily_budget, targeting: params.targeting, objective: params.objective, optimization_goal: optimizationGoal, billing_event: billingEvent, promoted_object: promotedObject, destination_type: destinationType, bid_strategy: params.bid_strategy, bid_amount: params.bid_amount, ad_account_id: params.ad_account_id, }); const adSetId = (adSet as any)?.id; if (!adSetId) { throw new Error(`[Step 2 - Ad Set] Falha: ${JSON.stringify(adSet)}`); } // Step 3: Image Uploads const imageHashes: string[] = []; const DATA_URI_RE = /^data:image\/[a-zA-Z0-9.+-]+;base64,(.+)$/; const processImageUploads = async (urls?: string[], bytes?: string[]) => { const allBytes = [...(bytes || [])]; const realUrls: string[] = []; // gpt-image-2 returns base64 data URIs (data:image/png;base64,...) — Meta's // upload_ad_image expects those as raw base64 via image_bytes, not image_url // (sending a data URI as image_url causes "tipo de arquivo não suportado"). for (const url of urls || []) { const match = DATA_URI_RE.exec(url); if (match) { allBytes.push(match[1]); } else { realUrls.push(url); } } for (const url of realUrls) { const res = await uploadAdImage(client, url, undefined, params.ad_account_id); if ((res as any)?.hash) imageHashes.push((res as any).hash); } for (const b of allBytes) { const res = await uploadAdImage(client, undefined, b, params.ad_account_id); if ((res as any)?.hash) imageHashes.push((res as any).hash); } }; await processImageUploads(params.image_urls, params.image_bytes); if (imageHashes.length === 0) { throw new Error('[Step 3 - Upload] Nenhuma imagem foi enviada ou processada.'); } // Step 4 & 5: Create Ad Creative and Ad const creativeName = `${params.campaign_name} — Creative`; // Use a single primary image via the standard object_story_spec path — // asset_feed_spec (Asset Customization / Dynamic Creative) requires the ad // set to be created with is_dynamic_creative=true, which we don't set here, // and combining it with degrees_of_freedom_spec triggers Meta error subcode // 2490433 ("unexpected error"). The first generated image (1080x1080) is // the most broadly compatible across placements. const primaryImageHash = imageHashes[0]; let creative; if (params.is_whatsapp) { creative = await createWhatsappAdCreative(client, { name: creativeName, page_id: params.page_id, instagram_actor_id: params.instagram_actor_id, image_hash: primaryImageHash, message: params.primary_text, headline: params.headline, description: params.description, link: params.link, whatsapp_link: params.whatsapp_link, whatsapp_phone_number: params.whatsapp_phone_number, ad_account_id: params.ad_account_id, }); } else { creative = await createAdCreative(client, { name: creativeName, page_id: params.page_id, instagram_actor_id: params.instagram_actor_id, image_hash: primaryImageHash, link: params.link, message: params.primary_text, headline: params.headline, description: params.description, call_to_action_type: params.cta, ad_account_id: params.ad_account_id, }); } const creativeId = (creative as any)?.id; if (!creativeId) { throw new Error(`[Step 4 - Creative] Falha no criativo: ${JSON.stringify(creative)}`); } const ad = await createAd(client, { name: `${params.campaign_name} — Ad`, adset_id: adSetId, creative_id: creativeId, ad_account_id: params.ad_account_id, }); const adId = (ad as any)?.id; if (!adId) { throw new Error(`[Step 5 - Ad] Falha no anúncio: ${JSON.stringify(ad)}`); } return { campaign_id: campaignId, adset_id: adSetId, creative_ids: [creativeId], ad_ids: [adId], objective: params.objective, optimization_goal: optimizationGoal, billing_event: billingEvent, bid_strategy: params.bid_strategy, bid_amount: params.bid_amount, is_whatsapp: params.is_whatsapp || false, }; }