@weppa-cloud/mcp-search-console
Version:
MCP server for Google Search Console with growth-focused metrics
309 lines (308 loc) • 11.9 kB
JavaScript
// 🚀 Growth-focused Search Console Tools
export async function getSEOPulse(client, siteUrl, days = 7) {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const previousEndDate = new Date(startDate);
const previousStartDate = new Date();
previousStartDate.setDate(previousStartDate.getDate() - days * 2);
// Current period
const currentData = await client.searchanalytics.query({
siteUrl,
requestBody: {
startDate: startDate.toISOString().split('T')[0],
endDate: endDate.toISOString().split('T')[0],
dimensions: ['date'],
type: 'web',
},
});
// Previous period for comparison
const previousData = await client.searchanalytics.query({
siteUrl,
requestBody: {
startDate: previousStartDate.toISOString().split('T')[0],
endDate: previousEndDate.toISOString().split('T')[0],
dimensions: ['date'],
type: 'web',
},
});
const current = aggregateMetrics(currentData.data.rows || []);
const previous = aggregateMetrics(previousData.data.rows || []);
return {
visibility: {
impressions: current.impressions,
change: calculateChange(current.impressions, previous.impressions),
alert: getAlert(current.impressions, previous.impressions),
},
traffic: {
clicks: current.clicks,
change: calculateChange(current.clicks, previous.clicks),
alert: getAlert(current.clicks, previous.clicks),
},
performance: {
ctr: `${(current.ctr * 100).toFixed(1)}%`,
avgPosition: current.position.toFixed(1),
ctrChange: calculateChange(current.ctr, previous.ctr),
positionChange: `${(previous.position - current.position).toFixed(1)}`,
},
topIssue: detectSEOIssue(current, previous),
topOpportunity: detectSEOOpportunity(currentData.data.rows || []),
};
}
export async function getKeywordOpportunities(client, siteUrl, minImpressions = 100) {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 30);
const response = await client.searchanalytics.query({
siteUrl,
requestBody: {
startDate: startDate.toISOString().split('T')[0],
endDate: endDate.toISOString().split('T')[0],
dimensions: ['query', 'page'],
// Remove position filter from API request - will filter programmatically
rowLimit: 1000,
type: 'web',
},
});
const opportunities = (response.data.rows || [])
.filter(row => {
const position = row.position || 0;
// Filter for positions 4-20 programmatically
return position >= 4 && position <= 20 && (row.impressions || 0) >= minImpressions;
})
.map(row => {
const potentialClicks = calculatePotentialClicks(row);
const ctr = row.ctr || 0;
const position = row.position || 0;
const clicks = row.clicks || 0;
const impressions = row.impressions || 0;
const difficulty = estimateDifficulty(position);
return {
keyword: row.keys?.[0] || '',
page: row.keys?.[1] || '',
currentPosition: position,
impressions: impressions,
clicks: clicks,
ctr: `${(ctr * 100).toFixed(1)}%`,
potentialClicks,
potentialTraffic: `+${potentialClicks - clicks} clicks/month`,
difficulty,
action: getOptimizationAction(position, difficulty),
};
})
.sort((a, b) => b.potentialClicks - a.potentialClicks)
.slice(0, 20);
return {
opportunities,
summary: {
totalOpportunities: opportunities.length,
potentialTraffic: opportunities.reduce((sum, o) => sum + (o.potentialClicks - o.clicks), 0),
quickWins: opportunities.filter(o => o.difficulty === 'easy').length,
},
};
}
export async function getContentPerformance(client, siteUrl, limit = 20) {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 30);
const response = await client.searchanalytics.query({
siteUrl,
requestBody: {
startDate: startDate.toISOString().split('T')[0],
endDate: endDate.toISOString().split('T')[0],
dimensions: ['page'],
rowLimit: limit,
type: 'web',
},
});
const pages = (response.data.rows || []).map(row => {
const url = row.keys?.[0] || '';
const performance = getPerformanceRating(row);
return {
url,
metrics: {
impressions: row.impressions || 0,
clicks: row.clicks || 0,
ctr: `${((row.ctr || 0) * 100).toFixed(1)}%`,
position: (row.position || 0).toFixed(1),
},
performance,
recommendations: getContentRecommendations(row),
estimatedValue: calculateContentValue(row),
};
});
return {
topPages: pages,
insights: generateContentInsights(pages),
};
}
export async function getCompetitorKeywords(client, siteUrl) {
// Get keywords where we rank poorly but have impressions
const response = await client.searchanalytics.query({
siteUrl,
requestBody: {
startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
endDate: new Date().toISOString().split('T')[0],
dimensions: ['query'],
// Remove position filter from API request - will filter programmatically
rowLimit: 500, // Increase limit to get more data before filtering
type: 'web',
},
});
const competitorKeywords = (response.data.rows || [])
.filter(row => {
const position = row.position || 0;
// Filter for positions 10-50 programmatically
return position >= 10 && position <= 50 && (row.impressions || 0) > 50;
})
.map(row => ({
keyword: row.keys?.[0] || '',
position: row.position || 0,
impressions: row.impressions || 0,
marketShare: `${(((row.clicks || 0) / (row.impressions || 1)) * 100).toFixed(1)}%`,
competitorStrength: getCompetitorStrength(row.position || 0),
strategy: getCompetitiveStrategy(row),
}));
return {
keywords: competitorKeywords,
summary: {
totalKeywords: competitorKeywords.length,
highValueTargets: competitorKeywords.filter(k => k.impressions > 1000).length,
quickWins: competitorKeywords.filter(k => (k.position || 0) < 20).length,
},
};
}
// Helper functions
function aggregateMetrics(rows) {
const totals = rows.reduce((acc, row) => ({
impressions: acc.impressions + (row.impressions || 0),
clicks: acc.clicks + (row.clicks || 0),
ctr: 0,
position: 0,
}), { impressions: 0, clicks: 0, ctr: 0, position: 0 });
totals.ctr = totals.impressions > 0 ? totals.clicks / totals.impressions : 0;
// Calculate weighted average position
const weightedPosition = rows.reduce((sum, row) => sum + ((row.position || 0) * (row.impressions || 0)), 0);
totals.position = totals.impressions > 0 ? weightedPosition / totals.impressions : 0;
return totals;
}
function calculateChange(current, previous) {
if (previous === 0)
return '0%';
const change = ((current - previous) / previous * 100).toFixed(1);
return `${Number(change) > 0 ? '+' : ''}${change}%`;
}
function getAlert(current, previous) {
const change = previous === 0 ? 0 : ((current - previous) / previous * 100);
if (change >= 0)
return '🟢';
if (change >= -10)
return '🟡';
return '🔴';
}
function detectSEOIssue(current, previous) {
if (current.impressions < previous.impressions * 0.8) {
return 'Visibility dropped 20%+ - check for algorithm updates or technical issues';
}
if (current.ctr < previous.ctr * 0.85) {
return 'CTR dropped significantly - review meta descriptions and titles';
}
if (current.position > previous.position + 2) {
return 'Average position worsened - competitors may be gaining ground';
}
return undefined;
}
function detectSEOOpportunity(rows) {
const highImpressionLowCTR = rows.find(r => (r.impressions || 0) > 1000 && (r.ctr || 0) < 0.02);
if (highImpressionLowCTR) {
return 'High impression keywords with low CTR - optimize meta tags for better clicks';
}
return undefined;
}
function calculatePotentialClicks(row) {
const position = row.position || 0;
const impressions = row.impressions || 0;
// CTR by position benchmarks
const ctrByPosition = {
1: 0.285, 2: 0.157, 3: 0.111, 4: 0.085, 5: 0.067,
6: 0.052, 7: 0.041, 8: 0.033, 9: 0.027, 10: 0.023,
};
const targetPosition = Math.max(1, Math.floor(position) - 3);
const targetCTR = ctrByPosition[targetPosition] || 0.02;
return Math.floor(impressions * targetCTR);
}
function estimateDifficulty(position) {
if (position <= 7)
return 'easy';
if (position <= 15)
return 'medium';
return 'hard';
}
function getOptimizationAction(position, difficulty) {
if (difficulty === 'easy') {
return 'Quick content update + internal links';
}
if (difficulty === 'medium') {
return 'Expand content + build backlinks';
}
return 'Create comprehensive guide + aggressive link building';
}
function getPerformanceRating(row) {
const ctr = row.ctr || 0;
const position = row.position || 0;
const score = (ctr * 100) + (50 - position);
if (score > 45)
return 'excellent';
if (score > 30)
return 'good';
if (score > 15)
return 'needs improvement';
return 'poor';
}
function getContentRecommendations(row) {
const recommendations = [];
if ((row.ctr || 0) < 0.02) {
recommendations.push('Improve meta title and description');
}
if ((row.position || 0) > 10) {
recommendations.push('Add more comprehensive content');
recommendations.push('Build quality backlinks');
}
if ((row.impressions || 0) > 1000 && (row.clicks || 0) < 20) {
recommendations.push('Test different titles for better CTR');
}
return recommendations;
}
function calculateContentValue(row) {
// Estimated value based on typical conversion rates
const estimatedValue = (row.clicks || 0) * 0.02 * 50; // 2% conversion, $50 AOV
return `$${estimatedValue.toFixed(0)}/month`;
}
function generateContentInsights(pages) {
const insights = [];
const highTrafficLowCTR = pages.filter(p => p.metrics.impressions > 1000 && parseFloat(p.metrics.ctr) < 2);
if (highTrafficLowCTR.length > 0) {
insights.push(`${highTrafficLowCTR.length} pages with high impressions but low CTR - major opportunity`);
}
const excellentPages = pages.filter(p => p.performance === 'excellent');
if (excellentPages.length > 0) {
insights.push(`${excellentPages.length} top-performing pages - use as templates for new content`);
}
return insights;
}
function getCompetitorStrength(position) {
if (position < 20)
return 'weak';
if (position < 35)
return 'moderate';
return 'strong';
}
function getCompetitiveStrategy(row) {
if (row.position < 15) {
return 'Target with focused content campaign - winnable';
}
if ((row.impressions || 0) > 1000) {
return 'High-value keyword - invest in comprehensive guide';
}
return 'Monitor and build domain authority first';
}