@weppa-cloud/mcp-google-ads
Version:
Google Ads MCP server for growth marketing - campaign optimization, keyword research, and ROI tracking
255 lines • 12 kB
JavaScript
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