UNPKG

@weppa-cloud/mcp-google-ads

Version:

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

629 lines 29.6 kB
import { z } from 'zod'; const budgetOptimizerSchema = z.object({ targetRoas: z.number().min(1).default(3).describe('ROAS objetivo para redistribución'), maxBudgetChange: z.number().min(0).max(100).default(30).describe('Cambio máximo de presupuesto en %'), includeSeasonality: z.boolean().default(true).describe('Considerar estacionalidad'), }); const budgetForecastSchema = z.object({ forecastDays: z.number().min(7).max(90).default(30).describe('Días a proyectar'), scenarioType: z.enum(['conservative', 'realistic', 'aggressive']).default('realistic'), plannedBudgetIncrease: z.number().optional().describe('Incremento de presupuesto planeado en %'), }); const smartBiddingRecommendationSchema = z.object({ minConversionsRequired: z.number().default(30).describe('Conversiones mínimas para recomendar'), includeSimulation: z.boolean().default(true).describe('Incluir simulación de resultados'), }); export const budgetTools = [ { name: 'budget_optimizer', description: '💰 Optimiza la distribución de presupuesto entre campañas para maximizar ROI', inputSchema: budgetOptimizerSchema, handler: async (args, auth) => { const validated = budgetOptimizerSchema.parse(args); const { startDate, endDate } = getDateRange('30days'); // Obtener datos de campañas con presupuesto const query = ` SELECT campaign.id, campaign.name, campaign.status, campaign_budget.id, campaign_budget.amount_micros, campaign_budget.total_amount_micros, campaign_budget.status, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.clicks, metrics.impressions, metrics.search_impression_share, metrics.search_lost_impression_share_budget FROM campaign WHERE segments.date BETWEEN '${startDate}' AND '${endDate}' AND campaign.status = 'ENABLED' AND campaign_budget.status = 'ENABLED' ORDER BY metrics.cost_micros DESC `; const campaigns = await auth.query(query); // Calcular métricas para cada campaña const campaignAnalysis = campaigns.map((row) => { const currentBudget = row.campaign_budget.amount_micros / 1_000_000; const dailyBudget = row.campaign_budget.total_amount_micros ? row.campaign_budget.total_amount_micros / 1_000_000 / 30 : currentBudget; const spend = row.metrics.cost_micros / 1_000_000; const revenue = row.metrics.conversions_value || 0; const roas = spend > 0 ? revenue / spend : 0; const budgetUtilization = dailyBudget > 0 ? (spend / 30) / dailyBudget : 0; return { id: row.campaign.id, name: row.campaign.name, currentDailyBudget: dailyBudget, actualSpend: spend, revenue: revenue, roas: roas, conversions: row.metrics.conversions || 0, budgetUtilization: budgetUtilization, lostImpressionShareBudget: row.metrics.search_lost_impression_share_budget || 0, impressionShare: row.metrics.search_impression_share || 0, }; }); // Calcular presupuesto total disponible const totalCurrentBudget = campaignAnalysis.reduce((sum, c) => sum + c.currentDailyBudget, 0); // Optimizar distribución const optimization = optimizeBudgetDistribution(campaignAnalysis, totalCurrentBudget, validated.targetRoas, validated.maxBudgetChange); // Considerar estacionalidad si está habilitada let seasonalityAdjustments = null; if (validated.includeSeasonality) { seasonalityAdjustments = calculateSeasonalityAdjustments(campaigns); } // Calcular impacto proyectado const projectedImpact = calculateProjectedImpact(campaignAnalysis, optimization); return { summary: { totalDailyBudget: totalCurrentBudget, currentOverallRoas: calculateOverallRoas(campaignAnalysis), projectedOverallRoas: projectedImpact.projectedRoas, estimatedRevenueIncrease: projectedImpact.revenueIncrease, }, recommendations: optimization.recommendations, budgetChanges: optimization.changes.filter(c => Math.abs(c.changePercent) > 5), seasonalityInsights: seasonalityAdjustments, implementation: { priority: generatePriorityActions(optimization.changes), warnings: generateBudgetWarnings(optimization.changes), timeline: 'Implementa cambios gradualmente en 3-5 días para minimizar volatilidad', }, }; }, }, { name: 'budget_forecast', description: '📈 Proyecta resultados futuros basados en diferentes escenarios de presupuesto', inputSchema: budgetForecastSchema, handler: async (args, auth) => { const validated = budgetForecastSchema.parse(args); // Obtener datos históricos (90 días para mejor proyección) const historicalDays = 90; const endDate = new Date(); const startDate = new Date(); startDate.setDate(startDate.getDate() - historicalDays); const query = ` SELECT segments.date, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.clicks, metrics.impressions FROM customer WHERE segments.date BETWEEN '${startDate.toISOString().split('T')[0]}' AND '${endDate.toISOString().split('T')[0]}' ORDER BY segments.date `; const historicalData = await auth.query(query); // Calcular tendencias y estacionalidad const trends = analyzeTrends(historicalData); const seasonality = detectSeasonalityPatterns(historicalData); // Generar proyecciones según escenario const projections = generateProjections(historicalData, trends, seasonality, validated.forecastDays, validated.scenarioType, validated.plannedBudgetIncrease); // Calcular métricas clave proyectadas const forecastMetrics = calculateForecastMetrics(projections); // Identificar riesgos y oportunidades const riskAnalysis = analyzeforecastRisks(projections, trends); return { forecast: { scenario: validated.scenarioType, period: `Next ${validated.forecastDays} days`, budgetIncrease: validated.plannedBudgetIncrease || 0, }, projections: { daily: projections.daily.slice(0, 7), // Primeros 7 días detallados weekly: projections.weekly, summary: forecastMetrics, }, insights: { trends: trends, seasonality: seasonality, confidenceLevel: calculateConfidenceLevel(historicalData, trends), }, risks: riskAnalysis, recommendations: generateForecastRecommendations(forecastMetrics, validated.scenarioType, validated.plannedBudgetIncrease), }; }, }, { name: 'smart_bidding_recommendations', description: '🤖 Recomienda estrategias de Smart Bidding personalizadas por campaña', inputSchema: smartBiddingRecommendationSchema, handler: async (args, auth) => { const validated = smartBiddingRecommendationSchema.parse(args); const { startDate, endDate } = getDateRange('30days'); const query = ` SELECT campaign.id, campaign.name, campaign.bidding_strategy_type, campaign.target_cpa.target_cpa_micros, campaign.target_roas.target_roas, campaign.maximize_conversions.target_cpa_micros, metrics.conversions, metrics.conversions_value, metrics.cost_micros, metrics.average_cpc, metrics.cost_per_conversion FROM campaign WHERE segments.date BETWEEN '${startDate}' AND '${endDate}' AND campaign.status = 'ENABLED' `; const campaigns = await auth.query(query); const recommendations = []; for (const campaign of campaigns) { const conversions = campaign.metrics?.conversions || 0; const cost = (campaign.metrics?.cost_micros || 0) / 1_000_000; const revenue = campaign.metrics?.conversions_value || 0; const currentStrategy = campaign.campaign?.bidding_strategy_type || 'UNKNOWN'; const roas = cost > 0 ? revenue / cost : 0; const cpa = conversions > 0 ? cost / conversions : 0; const recommendation = { campaignId: campaign.campaign?.id || '', campaignName: campaign.campaign?.name || 'Unknown', currentStrategy: currentStrategy, performance: { conversions: conversions, roas: roas, cpa: cpa, spend: cost, }, }; // Analizar y recomendar estrategia if (conversions >= validated.minConversionsRequired) { if (currentStrategy === 'MANUAL_CPC' || currentStrategy === 'MANUAL_CPM') { // Recomendar cambio a Smart Bidding if (revenue > 0 && roas > 2) { recommendation.suggestedStrategy = 'TARGET_ROAS'; recommendation.suggestedTarget = Math.floor(roas * 0.8 * 100) / 100; // 80% del ROAS actual recommendation.reason = 'Suficientes conversiones con valor para optimizar ROAS'; recommendation.expectedImpact = 'Aumento del 15-25% en revenue manteniendo eficiencia'; } else if (conversions > validated.minConversionsRequired) { recommendation.suggestedStrategy = 'TARGET_CPA'; recommendation.suggestedTarget = Math.ceil(cpa * 1.1); // 110% del CPA actual recommendation.reason = 'Volumen de conversiones adecuado para Target CPA'; recommendation.expectedImpact = 'Aumento del 20-30% en conversiones'; } else { recommendation.suggestedStrategy = 'MAXIMIZE_CONVERSIONS'; recommendation.reason = 'Buena base de conversiones para maximizar volumen'; recommendation.expectedImpact = 'Aumento del 25-35% en conversiones'; } recommendation.priority = 'HIGH'; recommendation.confidence = 'HIGH'; } else if (currentStrategy === 'TARGET_CPA' && roas > 3) { // Sugerir cambio a Target ROAS si hay buen valor recommendation.suggestedStrategy = 'TARGET_ROAS'; recommendation.suggestedTarget = Math.floor(roas * 0.85 * 100) / 100; recommendation.reason = 'Excelente ROAS actual, optimizar para valor'; recommendation.priority = 'MEDIUM'; recommendation.confidence = 'MEDIUM'; } } else { // No suficientes conversiones if (currentStrategy !== 'MAXIMIZE_CONVERSIONS' && conversions > 10) { recommendation.suggestedStrategy = 'MAXIMIZE_CONVERSIONS'; recommendation.reason = 'Aumentar volumen de conversiones antes de optimizar CPA/ROAS'; recommendation.priority = 'MEDIUM'; recommendation.confidence = 'MEDIUM'; recommendation.expectedImpact = 'Recopilar más datos para futuras optimizaciones'; } else { recommendation.suggestedStrategy = 'KEEP_CURRENT'; recommendation.reason = conversions < 10 ? 'Insuficientes conversiones para Smart Bidding efectivo' : 'Estrategia actual apropiada por ahora'; recommendation.priority = 'LOW'; } } // Simular impacto si está habilitado if (validated.includeSimulation && recommendation.suggestedStrategy !== 'KEEP_CURRENT') { recommendation.simulation = simulateBiddingChange(campaign, recommendation.suggestedStrategy, recommendation.suggestedTarget); } recommendations.push(recommendation); } // Ordenar por prioridad e impacto potencial recommendations.sort((a, b) => { const priorityOrder = { HIGH: 3, MEDIUM: 2, LOW: 1 }; return priorityOrder[b.priority] - priorityOrder[a.priority]; }); return { summary: { totalCampaigns: campaigns.length, eligibleForSmartBidding: recommendations.filter(r => r.suggestedStrategy !== 'KEEP_CURRENT').length, highPriorityChanges: recommendations.filter(r => r.priority === 'HIGH').length, }, recommendations: recommendations.slice(0, 20), implementationGuide: { testing: 'Implementa cambios en campañas de prueba primero', monitoring: 'Monitorea durante 2-3 semanas antes de evaluar', rollback: 'Prepara plan de reversión si performance cae >20%', }, bestPractices: generateSmartBiddingBestPractices(recommendations), }; }, }, ]; 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 optimizeBudgetDistribution(campaigns, totalBudget, targetRoas, maxChangePercent) { // Calcular scores para cada campaña const scoredCampaigns = campaigns.map(c => { let score = 0; // Factor ROAS (peso: 40%) if (c.roas >= targetRoas) { score += 40 * (c.roas / targetRoas); } else { score += 40 * (c.roas / targetRoas) * 0.5; } // Factor de oportunidad perdida (peso: 30%) score += 30 * c.lostImpressionShareBudget; // Factor de utilización de presupuesto (peso: 20%) score += 20 * Math.min(c.budgetUtilization, 1); // Factor de volumen de conversiones (peso: 10%) const maxConversions = Math.max(...campaigns.map(c => c.conversions)); score += 10 * (c.conversions / maxConversions); return { ...c, score }; }); // Normalizar scores const totalScore = scoredCampaigns.reduce((sum, c) => sum + c.score, 0); // Calcular nueva distribución const changes = scoredCampaigns.map(c => { const idealBudget = (c.score / totalScore) * totalBudget; const currentBudget = c.currentDailyBudget; // Aplicar límite de cambio máximo let newBudget = idealBudget; const maxIncrease = currentBudget * (1 + maxChangePercent / 100); const maxDecrease = currentBudget * (1 - maxChangePercent / 100); newBudget = Math.min(newBudget, maxIncrease); newBudget = Math.max(newBudget, maxDecrease); const change = newBudget - currentBudget; const changePercent = currentBudget > 0 ? (change / currentBudget) * 100 : 0; return { campaignId: c.id, campaignName: c.name, currentBudget: currentBudget, recommendedBudget: newBudget, change: change, changePercent: changePercent, reason: generateBudgetChangeReason(c, changePercent), }; }); // Generar recomendaciones const recommendations = []; const increasingBudget = changes.filter(c => c.changePercent > 10); if (increasingBudget.length > 0) { recommendations.push(`📈 Aumentar presupuesto en ${increasingBudget.length} campañas de alto rendimiento`); } const decreasingBudget = changes.filter(c => c.changePercent < -10); if (decreasingBudget.length > 0) { recommendations.push(`📉 Reducir presupuesto en ${decreasingBudget.length} campañas de bajo rendimiento`); } return { changes, recommendations }; } function calculateSeasonalityAdjustments(campaigns) { // Análisis simplificado de estacionalidad // En producción, esto requeriría datos históricos más extensos const currentMonth = new Date().getMonth(); const seasonalFactors = { 0: 'Enero - Post-navidad, menor demanda', 11: 'Diciembre - Pico navideño, aumentar presupuesto', 10: 'Noviembre - Black Friday/Cyber Monday', 6: 'Julio - Inicio temporada verano', 7: 'Agosto - Vacaciones, posible menor conversión', }; return { currentPeriod: seasonalFactors[currentMonth] || 'Período normal', recommendation: currentMonth === 10 || currentMonth === 11 ? 'Considera aumentar presupuestos 20-30% por temporada alta' : 'Mantener presupuestos actuales', }; } function calculateProjectedImpact(current, optimization) { let projectedRevenue = 0; let projectedCost = 0; current.forEach(campaign => { const change = optimization.changes.find((c) => c.campaignId === campaign.id); if (change) { const budgetMultiplier = change.recommendedBudget / campaign.currentDailyBudget; // Asumir elasticidad del 0.7 (70% del incremento se traduce en más gasto) const spendMultiplier = 1 + (budgetMultiplier - 1) * 0.7; projectedCost += campaign.actualSpend * spendMultiplier; // Revenue aumenta menos que el gasto (ley de rendimientos decrecientes) const revenueMultiplier = 1 + (spendMultiplier - 1) * 0.6; projectedRevenue += campaign.revenue * revenueMultiplier; } }); const currentRevenue = current.reduce((sum, c) => sum + c.revenue, 0); const currentCost = current.reduce((sum, c) => sum + c.actualSpend, 0); return { projectedRoas: projectedCost > 0 ? projectedRevenue / projectedCost : 0, revenueIncrease: projectedRevenue - currentRevenue, costIncrease: projectedCost - currentCost, }; } function generateBudgetChangeReason(campaign, changePercent) { if (changePercent > 10) { if (campaign.roas > 3) return 'Alto ROAS - escalar para más revenue'; if (campaign.lostImpressionShareBudget > 0.2) return 'Perdiendo impresiones por presupuesto'; return 'Buen rendimiento con potencial de crecimiento'; } else if (changePercent < -10) { if (campaign.roas < 1) return 'ROAS no rentable - reducir pérdidas'; if (campaign.conversions === 0) return 'Sin conversiones - revisar estrategia'; return 'Bajo rendimiento comparado con otras campañas'; } return 'Mantener presupuesto actual'; } function calculateOverallRoas(campaigns) { const totalRevenue = campaigns.reduce((sum, c) => sum + c.revenue, 0); const totalCost = campaigns.reduce((sum, c) => sum + c.actualSpend, 0); return totalCost > 0 ? totalRevenue / totalCost : 0; } function generatePriorityActions(changes) { return changes .filter(c => Math.abs(c.changePercent) > 20) .sort((a, b) => Math.abs(b.changePercent) - Math.abs(a.changePercent)) .slice(0, 5) .map(c => ({ campaign: c.campaignName, action: c.changePercent > 0 ? 'INCREASE' : 'DECREASE', amount: `$${Math.abs(c.change).toFixed(2)}`, urgency: Math.abs(c.changePercent) > 30 ? 'HIGH' : 'MEDIUM', })); } function generateBudgetWarnings(changes) { const warnings = []; const drasticCuts = changes.filter(c => c.changePercent < -30); if (drasticCuts.length > 0) { warnings.push(`⚠️ ${drasticCuts.length} campañas con reducción >30% - monitorear impacto`); } const bigIncreases = changes.filter(c => c.changePercent > 50); if (bigIncreases.length > 0) { warnings.push(`📈 ${bigIncreases.length} campañas con aumento >50% - escalar gradualmente`); } return warnings; } function analyzeTrends(data) { // Calcular tendencia de conversiones y costos const recentDays = data.slice(-30); const previousDays = data.slice(-60, -30); const recentAvgConversions = recentDays.reduce((sum, d) => sum + (d.metrics.conversions || 0), 0) / recentDays.length; const previousAvgConversions = previousDays.reduce((sum, d) => sum + (d.metrics.conversions || 0), 0) / previousDays.length; const conversionTrend = previousAvgConversions > 0 ? (recentAvgConversions - previousAvgConversions) / previousAvgConversions : 0; return { conversions: { trend: conversionTrend > 0 ? 'INCREASING' : 'DECREASING', changeRate: conversionTrend, }, interpretation: interpretTrend(conversionTrend), }; } function detectSeasonalityPatterns(data) { // Análisis simplificado por día de la semana const dayPatterns = {}; data.forEach(row => { const date = new Date(row.segments.date); const dayOfWeek = date.getDay(); if (!dayPatterns[dayOfWeek]) { dayPatterns[dayOfWeek] = []; } dayPatterns[dayOfWeek].push(row.metrics.conversions || 0); }); // Calcular promedio por día const dayAverages = Object.entries(dayPatterns).map(([day, conversions]) => ({ day: parseInt(day), avgConversions: conversions.reduce((a, b) => a + b, 0) / conversions.length, })); // Identificar mejores y peores días const sorted = [...dayAverages].sort((a, b) => b.avgConversions - a.avgConversions); return { bestDays: sorted.slice(0, 2), worstDays: sorted.slice(-2), pattern: 'Weekly seasonality detected', }; } function generateProjections(historical, trends, seasonality, days, scenario, budgetIncrease) { const dailyProjections = []; const baselineAvg = historical.slice(-30).reduce((sum, d) => sum + (d.metrics.conversions || 0), 0) / 30; // Factores según escenario const scenarioFactors = { conservative: 0.9, realistic: 1.0, aggressive: 1.15, }; const factor = scenarioFactors[scenario]; const budgetFactor = budgetIncrease ? 1 + (budgetIncrease / 100) * 0.7 : 1; // Generar proyecciones diarias for (let i = 0; i < days; i++) { const date = new Date(); date.setDate(date.getDate() + i + 1); // Aplicar tendencia y factores let projectedConversions = baselineAvg * factor * budgetFactor; // Aplicar ligera tendencia if (trends.conversions.trend === 'INCREASING') { projectedConversions *= 1 + (trends.conversions.changeRate * 0.5); } dailyProjections.push({ date: date.toISOString().split('T')[0], conversions: Math.round(projectedConversions), confidence: scenario === 'realistic' ? 'MEDIUM' : 'LOW', }); } // Agregar a semanas const weeklyProjections = []; for (let i = 0; i < days; i += 7) { const weekData = dailyProjections.slice(i, i + 7); const weekTotal = weekData.reduce((sum, d) => sum + d.conversions, 0); weeklyProjections.push({ weekNumber: Math.floor(i / 7) + 1, conversions: weekTotal, }); } return { daily: dailyProjections, weekly: weeklyProjections, }; } function calculateForecastMetrics(projections) { const totalProjectedConversions = projections.daily.reduce((sum, d) => sum + d.conversions, 0); const avgDailyConversions = totalProjectedConversions / projections.daily.length; return { totalProjectedConversions, avgDailyConversions, weeklyAverage: projections.weekly.length > 0 ? projections.weekly.reduce((sum, w) => sum + w.conversions, 0) / projections.weekly.length : 0, }; } function analyzeforecastRisks(projections, trends) { const risks = []; if (trends.conversions.trend === 'DECREASING') { risks.push({ type: 'TREND_RISK', severity: 'MEDIUM', description: 'Tendencia decreciente puede impactar proyecciones', mitigation: 'Revisar y optimizar campañas para revertir tendencia', }); } risks.push({ type: 'MARKET_RISK', severity: 'LOW', description: 'Cambios en competencia o demanda no considerados', mitigation: 'Monitorear competidores y ajustar pujas si es necesario', }); return risks; } function calculateConfidenceLevel(historical, trends) { // Basado en la cantidad de datos y estabilidad if (historical.length < 30) return 'LOW'; const variance = calculateVariance(historical.map(d => d.metrics.conversions || 0)); if (variance < 0.2) return 'HIGH'; if (variance < 0.5) return 'MEDIUM'; return 'LOW'; } function calculateVariance(numbers) { const mean = numbers.reduce((a, b) => a + b, 0) / numbers.length; const variance = numbers.reduce((sum, num) => sum + Math.pow(num - mean, 2), 0) / numbers.length; return Math.sqrt(variance) / mean; // Coeficiente de variación } function generateForecastRecommendations(metrics, scenario, budgetIncrease) { const recommendations = []; if (scenario === 'conservative' && budgetIncrease && budgetIncrease > 20) { recommendations.push('⚠️ Escenario conservador con alto incremento - considera enfoque gradual'); } if (metrics.avgDailyConversions > 100) { recommendations.push('📊 Alto volumen proyectado - asegura capacidad de fulfillment'); } recommendations.push('🔄 Revisa proyecciones semanalmente y ajusta según resultados reales'); recommendations.push('💡 Implementa alertas automáticas si performance varía >20% de proyección'); return recommendations; } function interpretTrend(trend) { if (trend > 0.2) return '📈 Fuerte crecimiento - capitalizar momentum'; if (trend > 0) return '📊 Crecimiento estable'; if (trend > -0.1) return '➡️ Estable con ligera caída'; return '📉 Tendencia negativa - requiere intervención'; } function simulateBiddingChange(campaign, newStrategy, target) { // Simulación básica basada en datos históricos y mejores prácticas const currentConversions = campaign.metrics.conversions || 0; const currentCost = campaign.metrics.cost_micros / 1_000_000; let projectedChange = { conversions: 0, cost: 0, confidence: 'MEDIUM', }; switch (newStrategy) { case 'TARGET_ROAS': projectedChange.conversions = currentConversions * 1.15; projectedChange.cost = currentCost * 1.05; break; case 'TARGET_CPA': projectedChange.conversions = currentConversions * 1.25; projectedChange.cost = currentCost * 1.1; break; case 'MAXIMIZE_CONVERSIONS': projectedChange.conversions = currentConversions * 1.3; projectedChange.cost = currentCost * 1.2; break; } return { projected: projectedChange, timeToResults: '2-3 semanas para estabilización', confidence: currentConversions > 50 ? 'HIGH' : 'MEDIUM', }; } function generateSmartBiddingBestPractices(recommendations) { const practices = []; const targetRoasCount = recommendations.filter(r => r.suggestedStrategy === 'TARGET_ROAS').length; if (targetRoasCount > 0) { practices.push('🎯 Para Target ROAS: Comienza con objetivo 10-20% menor al actual'); } practices.push('⏰ Permite 2-3 semanas de aprendizaje antes de hacer ajustes'); practices.push('📊 No cambies objetivos más de 20% a la vez'); practices.push('🔍 Monitorea Search Terms Report semanalmente'); return practices; } //# sourceMappingURL=budget.js.map