UNPKG

@weppa-cloud/mcp-google-ads

Version:

Google Ads MCP server for growth marketing - campaign optimization, keyword research, and ROI tracking

255 lines 12 kB
import { z } from 'zod'; const campaignPerformanceSchema = z.object({ timeframe: z.enum(['7days', '30days', '90days']).default('30days').describe('Período de análisis'), orderBy: z.enum(['cost', 'conversions', 'clicks', 'impressions']).default('cost').describe('Métrica para ordenar'), limit: z.number().min(1).max(50).default(10).describe('Número de campañas a mostrar'), }); const campaignOptimizationSchema = z.object({ targetRoas: z.number().optional().describe('ROAS objetivo para identificar campañas bajo rendimiento'), minImpressions: z.number().default(1000).describe('Mínimo de impresiones para considerar'), }); export const campaignTools = [ { name: 'campaign_performance', description: '📊 Analiza el rendimiento de tus campañas con métricas clave y ROI', inputSchema: campaignPerformanceSchema, handler: async (args, auth) => { const validated = campaignPerformanceSchema.parse(args); const { startDate, endDate } = getDateRange(validated.timeframe); const query = ` SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, campaign_budget.amount_micros, metrics.cost_micros, metrics.clicks, metrics.impressions, metrics.conversions, metrics.conversions_value, metrics.average_cpc, metrics.cost_per_conversion FROM campaign WHERE segments.date BETWEEN '${startDate}' AND '${endDate}' AND campaign.status != 'REMOVED' ORDER BY metrics.${validated.orderBy === 'cost' ? 'cost_micros' : validated.orderBy} DESC LIMIT ${validated.limit} `; const response = await auth.query(query); const campaigns = response.map((row) => { const cost = row.metrics.cost_micros / 1_000_000; const conversionsValue = row.metrics.conversions_value || 0; const roas = cost > 0 ? conversionsValue / cost : 0; return { id: row.campaign.id, name: row.campaign.name, status: row.campaign.status, channel: row.campaign.advertising_channel_type, budget: row.campaign_budget?.amount_micros ? row.campaign_budget.amount_micros / 1_000_000 : 0, metrics: { cost: cost, clicks: row.metrics.clicks || 0, impressions: row.metrics.impressions || 0, conversions: row.metrics.conversions || 0, conversionsValue: conversionsValue, avgCpc: row.metrics.average_cpc ? row.metrics.average_cpc / 1_000_000 : 0, costPerConversion: row.metrics.cost_per_conversion ? row.metrics.cost_per_conversion / 1_000_000 : 0, ctr: row.metrics.impressions > 0 ? (row.metrics.clicks / row.metrics.impressions) * 100 : 0, conversionRate: row.metrics.clicks > 0 ? (row.metrics.conversions / row.metrics.clicks) * 100 : 0, roas: roas, }, insights: generateCampaignInsights(row, roas), }; }); const totalCost = campaigns.reduce((sum, c) => sum + c.metrics.cost, 0); const totalConversionsValue = campaigns.reduce((sum, c) => sum + c.metrics.conversionsValue, 0); const overallRoas = totalCost > 0 ? totalConversionsValue / totalCost : 0; return { summary: { totalCampaigns: campaigns.length, totalCost: totalCost, totalConversions: campaigns.reduce((sum, c) => sum + c.metrics.conversions, 0), totalRevenue: totalConversionsValue, overallRoas: overallRoas, period: `${startDate} to ${endDate}`, }, campaigns, recommendations: generateRecommendations(campaigns, overallRoas), }; }, }, { name: 'campaign_optimization_opportunities', description: '💡 Encuentra oportunidades para optimizar tus campañas y mejorar el ROI', inputSchema: campaignOptimizationSchema, handler: async (args, auth) => { const validated = campaignOptimizationSchema.parse(args); const { startDate, endDate } = getDateRange('30days'); const targetRoas = validated.targetRoas || 2.0; const query = ` SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, campaign.bidding_strategy_type, campaign_budget.amount_micros, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.impressions, metrics.clicks, metrics.search_impression_share, metrics.search_lost_impression_share_budget, metrics.search_lost_impression_share_rank FROM campaign WHERE segments.date BETWEEN '${startDate}' AND '${endDate}' AND campaign.status = 'ENABLED' AND metrics.impressions >= ${validated.minImpressions} ORDER BY metrics.cost_micros DESC LIMIT 50 `; const response = await auth.query(query); const opportunities = []; for (const row of response) { const cost = (row.metrics?.cost_micros || 0) / 1_000_000; const conversionsValue = row.metrics?.conversions_value || 0; const roas = cost > 0 ? conversionsValue / cost : 0; const opportunity = { campaign: { id: row.campaign?.id || '', name: row.campaign?.name || 'Unknown', channel: row.campaign?.advertising_channel_type || 'UNKNOWN', biddingStrategy: row.campaign?.bidding_strategy_type || 'UNKNOWN', }, currentPerformance: { cost: cost, roas: roas, conversions: row.metrics?.conversions || 0, impressionShare: row.metrics?.search_impression_share || 0, }, opportunities: [], }; // Identificar oportunidades específicas if (roas < targetRoas && roas > 0) { opportunity.opportunities.push({ type: 'LOW_ROAS', issue: `ROAS (${roas.toFixed(2)}) está por debajo del objetivo (${targetRoas})`, action: 'Revisar keywords negativos, ajustar pujas o pausar', impact: 'HIGH', potentialSavings: cost * (1 - roas / targetRoas), }); } const budgetLostShare = row.metrics?.search_budget_lost_impression_share || 0; if (budgetLostShare > 0.1) { opportunity.opportunities.push({ type: 'BUDGET_LIMITED', issue: `Perdiendo ${(budgetLostShare * 100).toFixed(1)}% de impresiones por presupuesto`, action: 'Aumentar presupuesto o redistribuir entre campañas', impact: 'MEDIUM', potentialGain: cost * budgetLostShare, }); } const rankLostShare = row.metrics?.search_rank_lost_impression_share || 0; if (rankLostShare > 0.2) { opportunity.opportunities.push({ type: 'LOW_AD_RANK', issue: `Perdiendo ${(rankLostShare * 100).toFixed(1)}% de impresiones por ranking`, action: 'Mejorar Quality Score o aumentar pujas', impact: 'MEDIUM', }); } const conversions = row.metrics?.conversions || 0; if (row.campaign?.bidding_strategy_type === 'MANUAL_CPC' && conversions > 10) { opportunity.opportunities.push({ type: 'BIDDING_STRATEGY', issue: 'Usando pujas manuales con suficientes conversiones', action: 'Cambiar a estrategia automatizada (Target CPA o Target ROAS)', impact: 'HIGH', }); } if (opportunity.opportunities.length > 0) { opportunities.push(opportunity); } } // Ordenar por impacto potencial opportunities.sort((a, b) => { const impactA = a.opportunities.filter((o) => o.impact === 'HIGH').length; const impactB = b.opportunities.filter((o) => o.impact === 'HIGH').length; return impactB - impactA; }); return { summary: { campaignsAnalyzed: response.length, opportunitiesFound: opportunities.length, totalPotentialSavings: opportunities.reduce((sum, o) => { const savings = o.opportunities .filter((op) => op.potentialSavings) .reduce((s, op) => s + op.potentialSavings, 0); return sum + savings; }, 0), }, opportunities: opportunities.slice(0, 10), // Top 10 oportunidades quickWins: opportunities .flatMap(o => o.opportunities) .filter((op) => op.impact === 'HIGH') .slice(0, 5), }; }, }, ]; function getDateRange(timeframe) { const endDate = new Date(); const startDate = new Date(); switch (timeframe) { case '7days': startDate.setDate(startDate.getDate() - 7); break; case '30days': startDate.setDate(startDate.getDate() - 30); break; case '90days': startDate.setDate(startDate.getDate() - 90); break; } return { startDate: startDate.toISOString().split('T')[0], endDate: endDate.toISOString().split('T')[0], }; } function generateCampaignInsights(campaign, roas) { const insights = []; if (roas < 1) { insights.push('⚠️ Campaña no rentable - ROAS < 1'); } else if (roas > 4) { insights.push('🌟 Excelente rendimiento - considera aumentar presupuesto'); } const ctr = campaign.metrics.impressions > 0 ? (campaign.metrics.clicks / campaign.metrics.impressions) * 100 : 0; if (ctr < 1) { insights.push('📉 CTR bajo - revisa creativos y copy'); } if (campaign.metrics.conversions === 0 && campaign.metrics.clicks > 50) { insights.push('🚫 Sin conversiones - verifica tracking o landing page'); } return insights; } function generateRecommendations(campaigns, overallRoas) { const recommendations = []; const lowPerformers = campaigns.filter(c => c.metrics.roas < 1).length; if (lowPerformers > 0) { recommendations.push(`🔴 ${lowPerformers} campañas no rentables - considera pausar o optimizar`); } const highPerformers = campaigns.filter(c => c.metrics.roas > 3); if (highPerformers.length > 0) { recommendations.push(`🟢 ${highPerformers.length} campañas con alto ROAS - aumenta presupuesto`); } if (overallRoas < 2) { recommendations.push('📊 ROAS general bajo - revisa estrategia de targeting y pujas'); } return recommendations; } //# sourceMappingURL=campaigns.js.map