search-console-mcp 1.11.0 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bing/client.js +26 -17
- package/dist/bing/tools/analytics.js +26 -0
- package/dist/bing/tools/seo-insights.js +60 -3
- package/dist/bing/tools/sitemaps.js +12 -1
- package/dist/bing/tools/sites.js +22 -0
- package/dist/common/tools/seo-primitives.js +9 -7
- package/dist/index.js +221 -120
- package/package.json +1 -1
package/dist/bing/client.js
CHANGED
|
@@ -35,29 +35,38 @@ export class BingClient {
|
|
|
35
35
|
async getSiteList() {
|
|
36
36
|
return this.request('GetUserSites');
|
|
37
37
|
}
|
|
38
|
+
async addSite(siteUrl) {
|
|
39
|
+
return this.request('AddSite', { siteUrl }, true);
|
|
40
|
+
}
|
|
41
|
+
async removeSite(siteUrl) {
|
|
42
|
+
return this.request('RemoveSite', { siteUrl }, true);
|
|
43
|
+
}
|
|
38
44
|
async getQueryStats(siteUrl) {
|
|
39
|
-
return this.request('GetQueryStats', { siteUrl }
|
|
45
|
+
return this.request('GetQueryStats', { siteUrl });
|
|
40
46
|
}
|
|
41
47
|
async getPageStats(siteUrl) {
|
|
42
|
-
return this.request('GetPageStats', { siteUrl }
|
|
48
|
+
return this.request('GetPageStats', { siteUrl });
|
|
49
|
+
}
|
|
50
|
+
async getPageQueryStats(siteUrl, page) {
|
|
51
|
+
return this.request('GetPageQueryStats', { siteUrl, page });
|
|
43
52
|
}
|
|
44
|
-
async
|
|
45
|
-
|
|
53
|
+
async submitSitemap(siteUrl, feedUrl) {
|
|
54
|
+
await this.request('SubmitFeed', { siteUrl, feedUrl }, true);
|
|
46
55
|
}
|
|
47
|
-
async
|
|
48
|
-
await this.request('
|
|
56
|
+
async deleteSitemap(siteUrl, feedUrl) {
|
|
57
|
+
await this.request('RemoveFeed', { siteUrl, feedUrl }, true);
|
|
49
58
|
}
|
|
50
|
-
async
|
|
51
|
-
return this.request('
|
|
59
|
+
async getFeeds(siteUrl) {
|
|
60
|
+
return this.request('GetFeeds', { siteUrl });
|
|
52
61
|
}
|
|
53
62
|
async getKeywordStats(q, country, language) {
|
|
54
|
-
return this.request('GetKeywordStats', { q, country, language }
|
|
63
|
+
return this.request('GetKeywordStats', { q, country, language });
|
|
55
64
|
}
|
|
56
65
|
async getCrawlIssues(siteUrl) {
|
|
57
|
-
return this.request('GetCrawlIssues', { siteUrl }
|
|
66
|
+
return this.request('GetCrawlIssues', { siteUrl });
|
|
58
67
|
}
|
|
59
68
|
async getUrlSubmissionQuota(siteUrl) {
|
|
60
|
-
return this.request('GetUrlSubmissionQuota', { siteUrl }
|
|
69
|
+
return this.request('GetUrlSubmissionQuota', { siteUrl });
|
|
61
70
|
}
|
|
62
71
|
async submitUrl(siteUrl, url) {
|
|
63
72
|
return this.request('SubmitUrl', { siteUrl, url }, true);
|
|
@@ -66,22 +75,22 @@ export class BingClient {
|
|
|
66
75
|
return this.request('SubmitUrlBatch', { siteUrl, urlList }, true);
|
|
67
76
|
}
|
|
68
77
|
async getQueryPageStats(siteUrl) {
|
|
69
|
-
return this.request('GetQueryPageStats', { siteUrl }
|
|
78
|
+
return this.request('GetQueryPageStats', { siteUrl });
|
|
70
79
|
}
|
|
71
80
|
async getRankAndTrafficStats(siteUrl) {
|
|
72
|
-
return this.request('GetRankAndTrafficStats', { siteUrl }
|
|
81
|
+
return this.request('GetRankAndTrafficStats', { siteUrl });
|
|
73
82
|
}
|
|
74
83
|
async getCrawlStats(siteUrl) {
|
|
75
|
-
return this.request('GetCrawlStats', { siteUrl }
|
|
84
|
+
return this.request('GetCrawlStats', { siteUrl });
|
|
76
85
|
}
|
|
77
86
|
async getUrlInfo(siteUrl, url) {
|
|
78
|
-
return this.request('GetUrlInfo', { siteUrl, url }
|
|
87
|
+
return this.request('GetUrlInfo', { siteUrl, url });
|
|
79
88
|
}
|
|
80
89
|
async getLinkCounts(siteUrl) {
|
|
81
|
-
return this.request('GetLinkCounts', { siteUrl }
|
|
90
|
+
return this.request('GetLinkCounts', { siteUrl });
|
|
82
91
|
}
|
|
83
92
|
async getRelatedKeywords(q, country, language) {
|
|
84
|
-
return this.request('GetRelatedKeywords', { q, country, language }
|
|
93
|
+
return this.request('GetRelatedKeywords', { q, country, language });
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
96
|
let cachedBingClient = null;
|
|
@@ -119,3 +119,29 @@ export async function detectAnomalies(siteUrl, options = {}) {
|
|
|
119
119
|
}
|
|
120
120
|
return anomalies;
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Get aggregate performance summary for a Bing site for the last N days.
|
|
124
|
+
*/
|
|
125
|
+
export async function getPerformanceSummary(siteUrl, days = 28) {
|
|
126
|
+
const stats = await getRankAndTrafficStats(siteUrl);
|
|
127
|
+
// Bing typically has historical data; filter for last N days
|
|
128
|
+
const endDate = new Date();
|
|
129
|
+
const startDate = new Date();
|
|
130
|
+
startDate.setDate(endDate.getDate() - days);
|
|
131
|
+
const filtered = stats.filter((row) => {
|
|
132
|
+
const d = new Date(row.Date);
|
|
133
|
+
return d >= startDate && d <= endDate;
|
|
134
|
+
});
|
|
135
|
+
const clicks = filtered.reduce((acc, row) => acc + row.Clicks, 0);
|
|
136
|
+
const impressions = filtered.reduce((acc, row) => acc + row.Impressions, 0);
|
|
137
|
+
const weightedPosSum = filtered.reduce((acc, row) => acc + (row.AvgPosition * row.Impressions), 0);
|
|
138
|
+
const avgPos = impressions > 0 ? weightedPosSum / impressions : 0;
|
|
139
|
+
return {
|
|
140
|
+
clicks,
|
|
141
|
+
impressions,
|
|
142
|
+
ctr: impressions > 0 ? clicks / impressions : 0,
|
|
143
|
+
position: parseFloat(avgPos.toFixed(2)),
|
|
144
|
+
startDate: startDate.toISOString().split('T')[0],
|
|
145
|
+
endDate: endDate.toISOString().split('T')[0]
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getQueryStats } from './analytics.js';
|
|
1
|
+
import { getQueryStats, getQueryPageStats } from './analytics.js';
|
|
2
2
|
/**
|
|
3
3
|
* Find "low-hanging fruit" keywords in Bing.
|
|
4
4
|
*/
|
|
@@ -68,15 +68,62 @@ export async function findLowCTROpportunities(siteUrl, options = {}) {
|
|
|
68
68
|
.sort((a, b) => b.Impressions - a.Impressions)
|
|
69
69
|
.slice(0, limit);
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Detect keyword cannibalization in Bing where multiple pages compete for the same query.
|
|
73
|
+
*/
|
|
74
|
+
export async function detectCannibalization(siteUrl, options = {}) {
|
|
75
|
+
const { minImpressions = 50, limit = 30 } = options;
|
|
76
|
+
const rows = await getQueryPageStats(siteUrl);
|
|
77
|
+
// Group by query
|
|
78
|
+
const queryMap = new Map();
|
|
79
|
+
for (const row of rows) {
|
|
80
|
+
if (row.Impressions < minImpressions)
|
|
81
|
+
continue;
|
|
82
|
+
if (!queryMap.has(row.Query)) {
|
|
83
|
+
queryMap.set(row.Query, []);
|
|
84
|
+
}
|
|
85
|
+
queryMap.get(row.Query).push({
|
|
86
|
+
page: row.Page,
|
|
87
|
+
clicks: row.Clicks,
|
|
88
|
+
impressions: row.Impressions,
|
|
89
|
+
date: row.Date
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const cannibalization = [];
|
|
93
|
+
for (const [query, pages] of queryMap) {
|
|
94
|
+
if (pages.length >= 2) {
|
|
95
|
+
pages.sort((a, b) => b.clicks - a.clicks);
|
|
96
|
+
const totalClicks = pages.reduce((sum, p) => sum + p.clicks, 0);
|
|
97
|
+
const totalImpressions = pages.reduce((sum, p) => sum + p.impressions, 0);
|
|
98
|
+
// Calculate conflict score
|
|
99
|
+
const shares = pages.map(p => totalClicks > 0 ? p.clicks / totalClicks : 0);
|
|
100
|
+
const hhi = shares.reduce((sum, s) => sum + s * s, 0);
|
|
101
|
+
const conflictScore = 1 - hhi;
|
|
102
|
+
if (conflictScore > 0.1 || pages[1].impressions > (pages[0].impressions * 0.2)) {
|
|
103
|
+
cannibalization.push({
|
|
104
|
+
query,
|
|
105
|
+
pages,
|
|
106
|
+
totalClicks,
|
|
107
|
+
totalImpressions,
|
|
108
|
+
clickShareConflict: parseFloat(conflictScore.toFixed(2))
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return cannibalization
|
|
114
|
+
.sort((a, b) => (b.totalClicks * b.clickShareConflict) - (a.totalClicks * a.clickShareConflict))
|
|
115
|
+
.slice(0, limit);
|
|
116
|
+
}
|
|
71
117
|
/**
|
|
72
118
|
* Generate prioritized Bing SEO recommendations.
|
|
73
119
|
*/
|
|
74
120
|
export async function generateRecommendations(siteUrl) {
|
|
75
121
|
const insights = [];
|
|
76
|
-
const [lowHangingFruit, strikingDistance, lowCTR] = await Promise.all([
|
|
122
|
+
const [lowHangingFruit, strikingDistance, lowCTR, cannibalization] = await Promise.all([
|
|
77
123
|
findLowHangingFruit(siteUrl, { limit: 10 }),
|
|
78
124
|
findStrikingDistance(siteUrl, { limit: 10 }),
|
|
79
|
-
findLowCTROpportunities(siteUrl, { limit: 10 })
|
|
125
|
+
findLowCTROpportunities(siteUrl, { limit: 10 }),
|
|
126
|
+
detectCannibalization(siteUrl, { limit: 10 })
|
|
80
127
|
]);
|
|
81
128
|
if (lowHangingFruit.length > 0) {
|
|
82
129
|
const totalPotential = lowHangingFruit.reduce((sum, l) => sum + l.potentialClicks, 0);
|
|
@@ -99,6 +146,16 @@ export async function generateRecommendations(siteUrl) {
|
|
|
99
146
|
data: { topQueries: lowCTR.slice(0, 5).map(q => q.Query) }
|
|
100
147
|
});
|
|
101
148
|
}
|
|
149
|
+
if (cannibalization.length > 0) {
|
|
150
|
+
insights.push({
|
|
151
|
+
type: 'warning',
|
|
152
|
+
category: 'Content',
|
|
153
|
+
title: `${cannibalization.length} Bing Cannibalization Issues`,
|
|
154
|
+
description: `Multiple pages are competing for the same keywords in Bing. Consider consolidating content.`,
|
|
155
|
+
priority: 'medium',
|
|
156
|
+
data: { topIssues: cannibalization.slice(0, 3).map(c => c.query) }
|
|
157
|
+
});
|
|
158
|
+
}
|
|
102
159
|
if (strikingDistance.length > 0) {
|
|
103
160
|
insights.push({
|
|
104
161
|
type: 'opportunity',
|
|
@@ -7,7 +7,7 @@ import { getBingClient } from '../client.js';
|
|
|
7
7
|
*/
|
|
8
8
|
export async function listSitemaps(siteUrl) {
|
|
9
9
|
const client = await getBingClient();
|
|
10
|
-
return client.
|
|
10
|
+
return client.getFeeds(siteUrl);
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* Submit a sitemap to Bing Webmaster Tools.
|
|
@@ -20,3 +20,14 @@ export async function submitSitemap(siteUrl, sitemapUrl) {
|
|
|
20
20
|
await client.submitSitemap(siteUrl, sitemapUrl);
|
|
21
21
|
return `Successfully submitted sitemap: ${sitemapUrl} for site ${siteUrl}`;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Remove a sitemap from Bing Webmaster Tools.
|
|
25
|
+
*
|
|
26
|
+
* @param siteUrl - The URL of the site.
|
|
27
|
+
* @param sitemapUrl - The URL of the sitemap to remove.
|
|
28
|
+
*/
|
|
29
|
+
export async function deleteSitemap(siteUrl, sitemapUrl) {
|
|
30
|
+
const client = await getBingClient();
|
|
31
|
+
await client.deleteSitemap(siteUrl, sitemapUrl);
|
|
32
|
+
return `Successfully removed sitemap: ${sitemapUrl} for site ${siteUrl}`;
|
|
33
|
+
}
|
package/dist/bing/tools/sites.js
CHANGED
|
@@ -8,3 +8,25 @@ export async function listSites() {
|
|
|
8
8
|
const client = await getBingClient();
|
|
9
9
|
return client.getSiteList();
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Add a site to Bing Webmaster Tools.
|
|
13
|
+
*
|
|
14
|
+
* @param siteUrl - The URL of the site to add.
|
|
15
|
+
* @returns A success message.
|
|
16
|
+
*/
|
|
17
|
+
export async function addSite(siteUrl) {
|
|
18
|
+
const client = await getBingClient();
|
|
19
|
+
await client.addSite(siteUrl);
|
|
20
|
+
return `Successfully added site: ${siteUrl}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Remove a site from Bing Webmaster Tools.
|
|
24
|
+
*
|
|
25
|
+
* @param siteUrl - The URL of the site to remove.
|
|
26
|
+
* @returns A success message.
|
|
27
|
+
*/
|
|
28
|
+
export async function removeSite(siteUrl) {
|
|
29
|
+
const client = await getBingClient();
|
|
30
|
+
await client.removeSite(siteUrl);
|
|
31
|
+
return `Successfully removed site: ${siteUrl}`;
|
|
32
|
+
}
|
|
@@ -5,7 +5,7 @@ import { safeTest } from '../../common/utils/regex.js';
|
|
|
5
5
|
* @param position - The average ranking position.
|
|
6
6
|
* @returns The categorized bucket result.
|
|
7
7
|
*/
|
|
8
|
-
export function getRankingBucket(position) {
|
|
8
|
+
export function getRankingBucket(position, engine) {
|
|
9
9
|
let bucket;
|
|
10
10
|
if (position <= 0)
|
|
11
11
|
bucket = 'Unranked';
|
|
@@ -17,7 +17,7 @@ export function getRankingBucket(position) {
|
|
|
17
17
|
bucket = 'Page 2 (11-20)';
|
|
18
18
|
else
|
|
19
19
|
bucket = 'Page 3+';
|
|
20
|
-
return { position, bucket };
|
|
20
|
+
return { position, bucket, engine };
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* Calculate the delta and percentage change between two traffic metrics.
|
|
@@ -26,7 +26,7 @@ export function getRankingBucket(position) {
|
|
|
26
26
|
* @param previous - The baseline metric value.
|
|
27
27
|
* @returns A detailed delta result including status.
|
|
28
28
|
*/
|
|
29
|
-
export function calculateTrafficDelta(current, previous) {
|
|
29
|
+
export function calculateTrafficDelta(current, previous, engine) {
|
|
30
30
|
const absoluteChange = current - previous;
|
|
31
31
|
let percentChange = 0;
|
|
32
32
|
let status;
|
|
@@ -51,7 +51,7 @@ export function calculateTrafficDelta(current, previous) {
|
|
|
51
51
|
else
|
|
52
52
|
status = 'unchanged';
|
|
53
53
|
}
|
|
54
|
-
return { current, previous, absoluteChange, percentChange, status };
|
|
54
|
+
return { current, previous, absoluteChange, percentChange, status, engine };
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
57
|
* Determine if a search query matches a brand pattern using regex.
|
|
@@ -60,12 +60,13 @@ export function calculateTrafficDelta(current, previous) {
|
|
|
60
60
|
* @param brandRegexString - The regex string to use for matching.
|
|
61
61
|
* @returns A brand detection result.
|
|
62
62
|
*/
|
|
63
|
-
export function isBrandQuery(query, brandRegexString) {
|
|
63
|
+
export function isBrandQuery(query, brandRegexString, engine) {
|
|
64
64
|
const isBrand = safeTest(brandRegexString, 'i', query);
|
|
65
65
|
return {
|
|
66
66
|
query,
|
|
67
67
|
isBrand,
|
|
68
|
-
matchedPattern: isBrand ? brandRegexString : undefined
|
|
68
|
+
matchedPattern: isBrand ? brandRegexString : undefined,
|
|
69
|
+
engine
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
/**
|
|
@@ -106,6 +107,7 @@ export function isCannibalized(query, pageA, pageB) {
|
|
|
106
107
|
pageB: "Page B",
|
|
107
108
|
isCannibalized,
|
|
108
109
|
overlapScore,
|
|
109
|
-
recommendation
|
|
110
|
+
recommendation,
|
|
111
|
+
engine: pageA.engine // Use engine from pageA if available
|
|
110
112
|
};
|
|
111
113
|
}
|
package/dist/index.js
CHANGED
|
@@ -35,20 +35,23 @@ const server = new McpServer({
|
|
|
35
35
|
version: "1.0.0",
|
|
36
36
|
});
|
|
37
37
|
// Sites Tools
|
|
38
|
-
server.tool("sites_list", "List all sites
|
|
38
|
+
server.tool("sites_list", "List all verified sites", { engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)") }, async ({ engine = "google" }) => {
|
|
39
39
|
try {
|
|
40
|
-
const
|
|
40
|
+
const results = engine === "google" ? await sites.listSites() : await bingSites.listSites();
|
|
41
41
|
return {
|
|
42
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
42
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
catch (error) {
|
|
46
46
|
return formatError(error);
|
|
47
47
|
}
|
|
48
48
|
});
|
|
49
|
-
server.tool("sites_add", "Add a
|
|
49
|
+
server.tool("sites_add", "Add a new site to Search Console or Bing Webmaster Tools", {
|
|
50
|
+
siteUrl: z.string().describe("The URL of the site to add"),
|
|
51
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
|
|
52
|
+
}, async ({ siteUrl, engine = "google" }) => {
|
|
50
53
|
try {
|
|
51
|
-
const result = await sites.addSite(siteUrl);
|
|
54
|
+
const result = engine === "google" ? await sites.addSite(siteUrl) : await bingSites.addSite(siteUrl);
|
|
52
55
|
return {
|
|
53
56
|
content: [{ type: "text", text: result }]
|
|
54
57
|
};
|
|
@@ -57,9 +60,12 @@ server.tool("sites_add", "Add a website to Search Console", { siteUrl: z.string(
|
|
|
57
60
|
return formatError(error);
|
|
58
61
|
}
|
|
59
62
|
});
|
|
60
|
-
server.tool("sites_delete", "Remove a
|
|
63
|
+
server.tool("sites_delete", "Remove a site from Search Console or Bing Webmaster Tools", {
|
|
64
|
+
siteUrl: z.string().describe("The URL of the site to delete"),
|
|
65
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
|
|
66
|
+
}, async ({ siteUrl, engine = "google" }) => {
|
|
61
67
|
try {
|
|
62
|
-
const result = await sites.deleteSite(siteUrl);
|
|
68
|
+
const result = engine === "google" ? await sites.deleteSite(siteUrl) : await bingSites.removeSite(siteUrl);
|
|
63
69
|
return {
|
|
64
70
|
content: [{ type: "text", text: result }]
|
|
65
71
|
};
|
|
@@ -79,11 +85,14 @@ server.tool("sites_get", "Get information about a specific site", { siteUrl: z.s
|
|
|
79
85
|
return formatError(error);
|
|
80
86
|
}
|
|
81
87
|
});
|
|
82
|
-
server.tool("sites_health_check", "Run a health check on one or all verified sites. Checks
|
|
83
|
-
siteUrl: z.string().optional().describe("Optional. The URL of a specific site to check.
|
|
84
|
-
|
|
88
|
+
server.tool("sites_health_check", "Run a health check on one or all verified sites. Checks performance trends and status.", {
|
|
89
|
+
siteUrl: z.string().optional().describe("Optional. The URL of a specific site to check."),
|
|
90
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
|
|
91
|
+
}, async ({ siteUrl, engine = "google" }) => {
|
|
85
92
|
try {
|
|
86
|
-
const result =
|
|
93
|
+
const result = engine === "google"
|
|
94
|
+
? await sitesHealth.healthCheck(siteUrl)
|
|
95
|
+
: await bingHealth.healthCheck(siteUrl);
|
|
87
96
|
return {
|
|
88
97
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
89
98
|
};
|
|
@@ -93,11 +102,14 @@ server.tool("sites_health_check", "Run a health check on one or all verified sit
|
|
|
93
102
|
}
|
|
94
103
|
});
|
|
95
104
|
// Sitemaps Tools
|
|
96
|
-
server.tool("sitemaps_list", "List sitemaps for a site", {
|
|
105
|
+
server.tool("sitemaps_list", "List sitemaps for a site", {
|
|
106
|
+
siteUrl: z.string().describe("The URL of the site"),
|
|
107
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
|
|
108
|
+
}, async ({ siteUrl, engine = "google" }) => {
|
|
97
109
|
try {
|
|
98
|
-
const
|
|
110
|
+
const results = engine === "google" ? await sitemaps.listSitemaps(siteUrl) : await bingSitemaps.listSitemaps(siteUrl);
|
|
99
111
|
return {
|
|
100
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
112
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
101
113
|
};
|
|
102
114
|
}
|
|
103
115
|
catch (error) {
|
|
@@ -118,12 +130,13 @@ server.tool("sitemaps_get", "Get details about a specific sitemap", {
|
|
|
118
130
|
return formatError(error);
|
|
119
131
|
}
|
|
120
132
|
});
|
|
121
|
-
server.tool("sitemaps_submit", "Submit a sitemap", {
|
|
133
|
+
server.tool("sitemaps_submit", "Submit a sitemap to Search Console or Bing Webmaster Tools", {
|
|
122
134
|
siteUrl: z.string().describe("The URL of the site"),
|
|
123
|
-
feedpath: z.string().describe("The URL of the sitemap")
|
|
124
|
-
|
|
135
|
+
feedpath: z.string().describe("The URL of the sitemap"),
|
|
136
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
|
|
137
|
+
}, async ({ siteUrl, feedpath, engine = "google" }) => {
|
|
125
138
|
try {
|
|
126
|
-
const result = await sitemaps.submitSitemap(siteUrl, feedpath);
|
|
139
|
+
const result = engine === "google" ? await sitemaps.submitSitemap(siteUrl, feedpath) : await bingSitemaps.submitSitemap(siteUrl, feedpath);
|
|
127
140
|
return {
|
|
128
141
|
content: [{ type: "text", text: result }]
|
|
129
142
|
};
|
|
@@ -132,12 +145,13 @@ server.tool("sitemaps_submit", "Submit a sitemap", {
|
|
|
132
145
|
return formatError(error);
|
|
133
146
|
}
|
|
134
147
|
});
|
|
135
|
-
server.tool("sitemaps_delete", "Delete a sitemap", {
|
|
148
|
+
server.tool("sitemaps_delete", "Delete a sitemap from Search Console or Bing Webmaster Tools", {
|
|
136
149
|
siteUrl: z.string().describe("The URL of the site"),
|
|
137
|
-
feedpath: z.string().describe("The URL of the sitemap")
|
|
138
|
-
|
|
150
|
+
feedpath: z.string().describe("The URL of the sitemap"),
|
|
151
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
|
|
152
|
+
}, async ({ siteUrl, feedpath, engine = "google" }) => {
|
|
139
153
|
try {
|
|
140
|
-
const result = await sitemaps.deleteSitemap(siteUrl, feedpath);
|
|
154
|
+
const result = engine === "google" ? await sitemaps.deleteSitemap(siteUrl, feedpath) : await bingSitemaps.deleteSitemap(siteUrl, feedpath);
|
|
141
155
|
return {
|
|
142
156
|
content: [{ type: "text", text: result }]
|
|
143
157
|
};
|
|
@@ -173,12 +187,15 @@ server.tool("analytics_query", "Query search analytics data with optional pagina
|
|
|
173
187
|
return formatError(error);
|
|
174
188
|
}
|
|
175
189
|
});
|
|
176
|
-
server.tool("analytics_performance_summary", "Get the aggregate performance metrics (clicks, impressions, CTR, position) for the last N days.
|
|
190
|
+
server.tool("analytics_performance_summary", "Get the aggregate performance metrics (clicks, impressions, CTR, position) for the last N days.", {
|
|
177
191
|
siteUrl: z.string().describe("The URL of the site"),
|
|
178
|
-
days: z.number().optional().describe("Number of days to look back (default: 28)")
|
|
179
|
-
|
|
192
|
+
days: z.number().optional().describe("Number of days to look back (default: 28)"),
|
|
193
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
|
|
194
|
+
}, async ({ siteUrl, days, engine = "google" }) => {
|
|
180
195
|
try {
|
|
181
|
-
const result =
|
|
196
|
+
const result = engine === "google"
|
|
197
|
+
? await analytics.getPerformanceSummary(siteUrl, days)
|
|
198
|
+
: await bingAnalytics.getPerformanceSummary(siteUrl, days);
|
|
182
199
|
return {
|
|
183
200
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
184
201
|
};
|
|
@@ -353,13 +370,16 @@ server.tool("analytics_time_series", "Get advanced time series data including ro
|
|
|
353
370
|
}
|
|
354
371
|
});
|
|
355
372
|
// Inspection Tools
|
|
356
|
-
server.tool("inspection_inspect", "Inspect a URL to check its indexing status, crawl info, and
|
|
357
|
-
siteUrl: z.string().describe("The URL of the property
|
|
358
|
-
inspectionUrl: z.string().describe("The fully-qualified URL to inspect
|
|
359
|
-
languageCode: z.string().optional().describe("Language code for localized results (
|
|
360
|
-
|
|
373
|
+
server.tool("inspection_inspect", "Inspect a URL to check its indexing status, crawl info, and health", {
|
|
374
|
+
siteUrl: z.string().describe("The URL of the property"),
|
|
375
|
+
inspectionUrl: z.string().describe("The fully-qualified URL to inspect"),
|
|
376
|
+
languageCode: z.string().optional().describe("Language code for localized results (Google only)"),
|
|
377
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
|
|
378
|
+
}, async ({ siteUrl, inspectionUrl, languageCode, engine = "google" }) => {
|
|
361
379
|
try {
|
|
362
|
-
const result =
|
|
380
|
+
const result = engine === "google"
|
|
381
|
+
? await inspection.inspectUrl(siteUrl, inspectionUrl, languageCode)
|
|
382
|
+
: await bingInspection.getUrlInfo(siteUrl, inspectionUrl);
|
|
363
383
|
return {
|
|
364
384
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
365
385
|
};
|
|
@@ -411,14 +431,17 @@ server.tool("seo_recommendations", "Generate SEO recommendations based on site p
|
|
|
411
431
|
return formatError(error);
|
|
412
432
|
}
|
|
413
433
|
});
|
|
414
|
-
server.tool("seo_low_hanging_fruit", "Find keywords with high impressions but low rankings
|
|
434
|
+
server.tool("seo_low_hanging_fruit", "Find keywords with high impressions but low rankings that have potential for growth", {
|
|
415
435
|
siteUrl: z.string().describe("The site URL"),
|
|
416
|
-
days: z.number().optional().describe("Number of days
|
|
436
|
+
days: z.number().optional().describe("Number of days (Google only, default: 28)"),
|
|
417
437
|
minImpressions: z.number().optional().describe("Minimum impressions threshold (default: 100)"),
|
|
418
|
-
limit: z.number().optional().describe("Max results to return (default: 50)")
|
|
419
|
-
|
|
438
|
+
limit: z.number().optional().describe("Max results to return (default: 50)"),
|
|
439
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
|
|
440
|
+
}, async ({ siteUrl, days, minImpressions, limit, engine = "google" }) => {
|
|
420
441
|
try {
|
|
421
|
-
const result =
|
|
442
|
+
const result = engine === "google"
|
|
443
|
+
? await seoInsights.findLowHangingFruit(siteUrl, { days, minImpressions, limit })
|
|
444
|
+
: await bingSeoInsights.findLowHangingFruit(siteUrl, { minImpressions, limit });
|
|
422
445
|
return {
|
|
423
446
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
424
447
|
};
|
|
@@ -429,12 +452,15 @@ server.tool("seo_low_hanging_fruit", "Find keywords with high impressions but lo
|
|
|
429
452
|
});
|
|
430
453
|
server.tool("seo_cannibalization", "Detect keyword cannibalization - multiple pages competing for the same query", {
|
|
431
454
|
siteUrl: z.string().describe("The site URL"),
|
|
432
|
-
days: z.number().optional().describe("Number of days
|
|
455
|
+
days: z.number().optional().describe("Number of days (Google only, default: 28)"),
|
|
433
456
|
minImpressions: z.number().optional().describe("Minimum impressions threshold (default: 50)"),
|
|
434
|
-
limit: z.number().optional().describe("Max issues to return (default: 30)")
|
|
435
|
-
|
|
457
|
+
limit: z.number().optional().describe("Max issues to return (default: 30)"),
|
|
458
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
|
|
459
|
+
}, async ({ siteUrl, days, minImpressions, limit, engine = "google" }) => {
|
|
436
460
|
try {
|
|
437
|
-
const result =
|
|
461
|
+
const result = engine === "google"
|
|
462
|
+
? await seoInsights.detectCannibalization(siteUrl, { days, minImpressions, limit })
|
|
463
|
+
: await bingSeoInsights.detectCannibalization(siteUrl, { minImpressions, limit });
|
|
438
464
|
return {
|
|
439
465
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
440
466
|
};
|
|
@@ -443,14 +469,17 @@ server.tool("seo_cannibalization", "Detect keyword cannibalization - multiple pa
|
|
|
443
469
|
return formatError(error);
|
|
444
470
|
}
|
|
445
471
|
});
|
|
446
|
-
server.tool("seo_low_ctr_opportunities", "Find queries
|
|
472
|
+
server.tool("seo_low_ctr_opportunities", "Find queries with low CTR relative to their ranking position. Great for title tag optimization.", {
|
|
447
473
|
siteUrl: z.string().describe("The site URL"),
|
|
448
|
-
days: z.number().optional().describe("Number of days
|
|
474
|
+
days: z.number().optional().describe("Number of days (Google only, default: 28)"),
|
|
449
475
|
minImpressions: z.number().optional().describe("Minimum impressions threshold (default: 500)"),
|
|
450
|
-
limit: z.number().optional().describe("Max
|
|
451
|
-
|
|
476
|
+
limit: z.number().optional().describe("Max issues to return (default: 50)"),
|
|
477
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
|
|
478
|
+
}, async ({ siteUrl, days, minImpressions, limit, engine = "google" }) => {
|
|
452
479
|
try {
|
|
453
|
-
const result =
|
|
480
|
+
const result = engine === "google"
|
|
481
|
+
? await seoInsights.findLowCTROpportunities(siteUrl, { days, minImpressions, limit })
|
|
482
|
+
: await bingSeoInsights.findLowCTROpportunities(siteUrl, { minImpressions, limit });
|
|
454
483
|
return {
|
|
455
484
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
456
485
|
};
|
|
@@ -461,11 +490,14 @@ server.tool("seo_low_ctr_opportunities", "Find queries ranking in positions 1-10
|
|
|
461
490
|
});
|
|
462
491
|
server.tool("seo_striking_distance", "Find keywords ranking in positions 8-15. These are high-priority targets to push to Page 1.", {
|
|
463
492
|
siteUrl: z.string().describe("The site URL"),
|
|
464
|
-
days: z.number().optional().describe("Number of days
|
|
465
|
-
limit: z.number().optional().describe("Max results to return (default: 50)")
|
|
466
|
-
|
|
493
|
+
days: z.number().optional().describe("Number of days (Google only, default: 28)"),
|
|
494
|
+
limit: z.number().optional().describe("Max results to return (default: 50)"),
|
|
495
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
|
|
496
|
+
}, async ({ siteUrl, days, limit, engine = "google" }) => {
|
|
467
497
|
try {
|
|
468
|
-
const result =
|
|
498
|
+
const result = engine === "google"
|
|
499
|
+
? await seoInsights.findStrikingDistance(siteUrl, { days, limit })
|
|
500
|
+
: await bingSeoInsights.findStrikingDistance(siteUrl, { limit });
|
|
469
501
|
return {
|
|
470
502
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
471
503
|
};
|
|
@@ -506,12 +538,15 @@ server.tool("seo_brand_vs_nonbrand", "Analyze performance split between Brand an
|
|
|
506
538
|
});
|
|
507
539
|
server.tool("seo_quick_wins", "Find pages with queries ranking on page 2 (positions 11-20) that could be pushed to page 1", {
|
|
508
540
|
siteUrl: z.string().describe("The site URL"),
|
|
509
|
-
days: z.number().optional().describe("Number of days
|
|
541
|
+
days: z.number().optional().describe("Number of days (Google only, default: 28)"),
|
|
510
542
|
minImpressions: z.number().optional().describe("Minimum impressions threshold (default: 100)"),
|
|
511
|
-
limit: z.number().optional().describe("Max results to return (default: 20)")
|
|
512
|
-
|
|
543
|
+
limit: z.number().optional().describe("Max results to return (default: 20)"),
|
|
544
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
|
|
545
|
+
}, async ({ siteUrl, days, minImpressions, limit, engine = "google" }) => {
|
|
513
546
|
try {
|
|
514
|
-
const result =
|
|
547
|
+
const result = engine === "google"
|
|
548
|
+
? await seoInsights.findQuickWins(siteUrl, { days, minImpressions, limit })
|
|
549
|
+
: await bingSeoInsights.findLowHangingFruit(siteUrl, { minImpressions, limit });
|
|
515
550
|
return {
|
|
516
551
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
517
552
|
};
|
|
@@ -521,25 +556,30 @@ server.tool("seo_quick_wins", "Find pages with queries ranking on page 2 (positi
|
|
|
521
556
|
}
|
|
522
557
|
});
|
|
523
558
|
// SEO Primitives (Atoms)
|
|
524
|
-
server.tool("seo_primitive_ranking_bucket", "primitive: Get the ranking bucket for a specific position (e.g. Top 3, Page 1).", {
|
|
559
|
+
server.tool("seo_primitive_ranking_bucket", "primitive: Get the ranking bucket for a specific position (e.g. Top 3, Page 1).", {
|
|
560
|
+
position: z.number().describe("The ranking position"),
|
|
561
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (optional)")
|
|
562
|
+
}, async ({ position, engine }) => {
|
|
525
563
|
return {
|
|
526
|
-
content: [{ type: "text", text: JSON.stringify(seoPrimitives.getRankingBucket(position), null, 2) }]
|
|
564
|
+
content: [{ type: "text", text: JSON.stringify(seoPrimitives.getRankingBucket(position, engine), null, 2) }]
|
|
527
565
|
};
|
|
528
566
|
});
|
|
529
567
|
server.tool("seo_primitive_traffic_delta", "primitive: Calculate the delta between two traffic metrics (absolute and percentage).", {
|
|
530
568
|
current: z.number().describe("Current value"),
|
|
531
|
-
previous: z.number().describe("Previous value")
|
|
532
|
-
|
|
569
|
+
previous: z.number().describe("Previous value"),
|
|
570
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (optional)")
|
|
571
|
+
}, async ({ current, previous, engine }) => {
|
|
533
572
|
return {
|
|
534
|
-
content: [{ type: "text", text: JSON.stringify(seoPrimitives.calculateTrafficDelta(current, previous), null, 2) }]
|
|
573
|
+
content: [{ type: "text", text: JSON.stringify(seoPrimitives.calculateTrafficDelta(current, previous, engine), null, 2) }]
|
|
535
574
|
};
|
|
536
575
|
});
|
|
537
576
|
server.tool("seo_primitive_is_brand", "primitive: Check if a query is a brand query based on a regex pattern.", {
|
|
538
577
|
query: z.string().describe("The search query"),
|
|
539
|
-
brandRegex: z.string().describe("Regex pattern to identify brand terms")
|
|
540
|
-
|
|
578
|
+
brandRegex: z.string().describe("Regex pattern to identify brand terms"),
|
|
579
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (optional)")
|
|
580
|
+
}, async ({ query, brandRegex, engine }) => {
|
|
541
581
|
return {
|
|
542
|
-
content: [{ type: "text", text: JSON.stringify(seoPrimitives.isBrandQuery(query, brandRegex), null, 2) }]
|
|
582
|
+
content: [{ type: "text", text: JSON.stringify(seoPrimitives.isBrandQuery(query, brandRegex, engine), null, 2) }]
|
|
543
583
|
};
|
|
544
584
|
});
|
|
545
585
|
server.tool("seo_primitive_is_cannibalized", "primitive: Check if two pages are competing for the same query based on their metrics.", {
|
|
@@ -549,10 +589,11 @@ server.tool("seo_primitive_is_cannibalized", "primitive: Check if two pages are
|
|
|
549
589
|
pageA_clicks: z.number(),
|
|
550
590
|
pageB_position: z.number(),
|
|
551
591
|
pageB_impressions: z.number(),
|
|
552
|
-
pageB_clicks: z.number()
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const
|
|
592
|
+
pageB_clicks: z.number(),
|
|
593
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine (optional)")
|
|
594
|
+
}, async ({ query, pageA_position, pageA_impressions, pageA_clicks, pageB_position, pageB_impressions, pageB_clicks, engine }) => {
|
|
595
|
+
const pageA = { position: pageA_position, impressions: pageA_impressions, clicks: pageA_clicks, engine };
|
|
596
|
+
const pageB = { position: pageB_position, impressions: pageB_impressions, clicks: pageB_clicks, engine };
|
|
556
597
|
return {
|
|
557
598
|
content: [{ type: "text", text: JSON.stringify(seoPrimitives.isCannibalized(query, pageA, pageB), null, 2) }]
|
|
558
599
|
};
|
|
@@ -597,6 +638,28 @@ server.tool("bing_sites_list", "List all sites verified in Bing Webmaster Tools"
|
|
|
597
638
|
return formatError(error);
|
|
598
639
|
}
|
|
599
640
|
});
|
|
641
|
+
server.tool("bing_sites_add", "Add a new site to Bing Webmaster Tools", { siteUrl: z.string().describe("The URL of the site to add") }, async ({ siteUrl }) => {
|
|
642
|
+
try {
|
|
643
|
+
const result = await bingSites.addSite(siteUrl);
|
|
644
|
+
return {
|
|
645
|
+
content: [{ type: "text", text: result }]
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
return formatError(error);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
server.tool("bing_sites_delete", "Remove a site from Bing Webmaster Tools", { siteUrl: z.string().describe("The URL of the site to remove") }, async ({ siteUrl }) => {
|
|
653
|
+
try {
|
|
654
|
+
const result = await bingSites.removeSite(siteUrl);
|
|
655
|
+
return {
|
|
656
|
+
content: [{ type: "text", text: result }]
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
return formatError(error);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
600
663
|
server.tool("bing_sitemaps_list", "List sitemaps for a Bing site", {
|
|
601
664
|
siteUrl: z.string().describe("The URL of the site")
|
|
602
665
|
}, async ({ siteUrl }) => {
|
|
@@ -624,6 +687,20 @@ server.tool("bing_sitemaps_submit", "Submit a sitemap to Bing Webmaster Tools",
|
|
|
624
687
|
return formatError(error);
|
|
625
688
|
}
|
|
626
689
|
});
|
|
690
|
+
server.tool("bing_sitemaps_delete", "Remove a sitemap from Bing Webmaster Tools", {
|
|
691
|
+
siteUrl: z.string().describe("The URL of the site"),
|
|
692
|
+
sitemapUrl: z.string().describe("The URL of the sitemap to remove")
|
|
693
|
+
}, async ({ siteUrl, sitemapUrl }) => {
|
|
694
|
+
try {
|
|
695
|
+
const result = await bingSitemaps.deleteSitemap(siteUrl, sitemapUrl);
|
|
696
|
+
return {
|
|
697
|
+
content: [{ type: "text", text: result }]
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
return formatError(error);
|
|
702
|
+
}
|
|
703
|
+
});
|
|
627
704
|
server.tool("bing_analytics_query", "Get query performance stats from Bing Webmaster Tools (Top Queries)", {
|
|
628
705
|
siteUrl: z.string().describe("The URL of the site")
|
|
629
706
|
}, async ({ siteUrl }) => {
|
|
@@ -1105,45 +1182,68 @@ server.resource("docs-bing-algorithm-updates", "docs://bing/algorithm-updates",
|
|
|
1105
1182
|
}]
|
|
1106
1183
|
}));
|
|
1107
1184
|
// Prompts
|
|
1108
|
-
server.prompt("analyze-site-performance", {
|
|
1185
|
+
server.prompt("analyze-site-performance", {
|
|
1186
|
+
siteUrl: z.string().describe("The URL of the site to analyze"),
|
|
1187
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine to use (default: google)")
|
|
1188
|
+
}, ({ siteUrl, engine = "google" }) => ({
|
|
1109
1189
|
messages: [{
|
|
1110
1190
|
role: "user",
|
|
1111
1191
|
content: {
|
|
1112
1192
|
type: "text",
|
|
1113
|
-
text: `Please analyze the performance of the site ${siteUrl} for the last 28 days.
|
|
1114
|
-
|
|
1115
|
-
|
|
1193
|
+
text: `Please analyze the performance of the site ${siteUrl} on ${engine === 'google' ? 'Google' : 'Bing'} for the last 28 days.
|
|
1194
|
+
${engine === 'google'
|
|
1195
|
+
? "Use the 'analytics_performance_summary' tool to get high-level metrics, and 'analytics_query' to dig deeper into top queries and pages if needed."
|
|
1196
|
+
: "Use the 'bing_analytics_query' tool to get query stats and 'bing_analytics_page' for page-level performance."}
|
|
1197
|
+
Provide a summary of the site's health and any opportunities for improvement on ${engine === 'google' ? 'Google' : 'Bing'}.`
|
|
1116
1198
|
}
|
|
1117
1199
|
}]
|
|
1118
1200
|
}));
|
|
1119
|
-
server.prompt("compare-performance", {
|
|
1201
|
+
server.prompt("compare-performance", {
|
|
1202
|
+
siteUrl: z.string().describe("The URL of the site to analyze"),
|
|
1203
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine to use (default: google)")
|
|
1204
|
+
}, ({ siteUrl, engine = "google" }) => ({
|
|
1120
1205
|
messages: [{
|
|
1121
1206
|
role: "user",
|
|
1122
1207
|
content: {
|
|
1123
1208
|
type: "text",
|
|
1124
|
-
text: `Compare the performance of ${siteUrl} for this week vs last week.
|
|
1209
|
+
text: `Compare the performance of ${siteUrl} on ${engine === 'google' ? 'Google' : 'Bing'} for this week vs last week.
|
|
1125
1210
|
|
|
1126
|
-
|
|
1211
|
+
${engine === 'google'
|
|
1212
|
+
? `Use the 'analytics_compare_periods' tool to compare the two periods:
|
|
1127
1213
|
- Period 1 (this week): last 7 days ending 3 days ago (to account for data delay)
|
|
1128
1214
|
- Period 2 (last week): the 7 days before that
|
|
1129
1215
|
|
|
1130
1216
|
Analyze the changes in clicks, impressions, CTR, and position.
|
|
1131
1217
|
Highlight any significant improvements or declines.
|
|
1132
1218
|
If there are notable changes, use 'analytics_top_queries' to identify which queries are driving the change.`
|
|
1219
|
+
: `Use the 'bing_analytics_compare_periods' tool to compare the two periods.
|
|
1220
|
+
Define two 7-day ranges (e.g., current period and previous period).
|
|
1221
|
+
|
|
1222
|
+
Analyze the changes in clicks, impressions, CTR, and position.
|
|
1223
|
+
Highlight any significant improvements or declines.
|
|
1224
|
+
Use 'bing_analytics_query' to identify which queries are driving changes in traffic.`}`
|
|
1133
1225
|
}
|
|
1134
1226
|
}]
|
|
1135
1227
|
}));
|
|
1136
|
-
server.prompt("find-declining-pages", {
|
|
1228
|
+
server.prompt("find-declining-pages", {
|
|
1229
|
+
siteUrl: z.string().describe("The URL of the site to analyze"),
|
|
1230
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine to use (default: google)")
|
|
1231
|
+
}, ({ siteUrl, engine = "google" }) => ({
|
|
1137
1232
|
messages: [{
|
|
1138
1233
|
role: "user",
|
|
1139
1234
|
content: {
|
|
1140
1235
|
type: "text",
|
|
1141
|
-
text: `Find pages on ${siteUrl} that are losing traffic.
|
|
1236
|
+
text: `Find pages on ${siteUrl} that are losing traffic on ${engine === 'google' ? 'Google' : 'Bing'}.
|
|
1142
1237
|
|
|
1143
|
-
|
|
1238
|
+
${engine === 'google'
|
|
1239
|
+
? `Steps:
|
|
1144
1240
|
1. Use 'analytics_compare_periods' to compare this week vs last week overall
|
|
1145
1241
|
2. Use 'analytics_query' with dimension 'page' to get page-level data for both periods
|
|
1146
|
-
3. Identify pages with significant click/impression drops
|
|
1242
|
+
3. Identify pages with significant click/impression drops`
|
|
1243
|
+
: `Steps:
|
|
1244
|
+
1. Use 'bing_analytics_compare_periods' to identify overall traffic direction.
|
|
1245
|
+
2. Use 'bing_analytics_page' to get top pages.
|
|
1246
|
+
3. Use 'bing_analytics_page_query' for specific pages to see which queries dropped.`}
|
|
1147
1247
|
|
|
1148
1248
|
For each declining page, provide:
|
|
1149
1249
|
- The URL
|
|
@@ -1152,25 +1252,24 @@ For each declining page, provide:
|
|
|
1152
1252
|
}
|
|
1153
1253
|
}]
|
|
1154
1254
|
}));
|
|
1155
|
-
server.prompt("keyword-opportunities", {
|
|
1255
|
+
server.prompt("keyword-opportunities", {
|
|
1256
|
+
siteUrl: z.string().describe("The URL of the site to analyze"),
|
|
1257
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine to use (default: google)")
|
|
1258
|
+
}, ({ siteUrl, engine = "google" }) => ({
|
|
1156
1259
|
messages: [{
|
|
1157
1260
|
role: "user",
|
|
1158
1261
|
content: {
|
|
1159
1262
|
type: "text",
|
|
1160
|
-
text: `Find keyword opportunities for ${siteUrl}.
|
|
1263
|
+
text: `Find keyword opportunities for ${siteUrl} on ${engine === 'google' ? 'Google' : 'Bing'}.
|
|
1161
1264
|
|
|
1162
|
-
|
|
1265
|
+
${engine === 'google'
|
|
1266
|
+
? "Use 'analytics_top_queries' or 'seo_low_hanging_fruit' to find high-potential targets."
|
|
1267
|
+
: "Use 'bing_opportunity_finder' or 'bing_striking_distance' to find high-potential keywords."}
|
|
1163
1268
|
|
|
1269
|
+
Analyze for:
|
|
1164
1270
|
1. **Low CTR, High Impressions**: Queries where you rank but don't get clicks
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
2. **High Position (>10), Good Impressions**: Queries not on page 1
|
|
1169
|
-
- These are close to ranking well
|
|
1170
|
-
- Small optimization could move them up
|
|
1171
|
-
|
|
1172
|
-
3. **New Ranking Queries**: Queries that appeared recently
|
|
1173
|
-
- Opportunities to create more content
|
|
1271
|
+
2. **High Position (>10), Good Impressions**: Queries not on page 1 (Striking Distance)
|
|
1272
|
+
3. **New Ranking Queries**: Queries that appeared recently (use comparison tools)
|
|
1174
1273
|
|
|
1175
1274
|
Provide specific recommendations for the top 5 opportunities.`
|
|
1176
1275
|
}
|
|
@@ -1178,17 +1277,18 @@ Provide specific recommendations for the top 5 opportunities.`
|
|
|
1178
1277
|
}));
|
|
1179
1278
|
server.prompt("new-content-impact", {
|
|
1180
1279
|
siteUrl: z.string().describe("The URL of the site"),
|
|
1181
|
-
pageUrl: z.string().describe("The URL of the new content to analyze")
|
|
1182
|
-
|
|
1280
|
+
pageUrl: z.string().describe("The URL of the new content to analyze"),
|
|
1281
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine to use (default: google)")
|
|
1282
|
+
}, ({ siteUrl, pageUrl, engine = "google" }) => ({
|
|
1183
1283
|
messages: [{
|
|
1184
1284
|
role: "user",
|
|
1185
1285
|
content: {
|
|
1186
1286
|
type: "text",
|
|
1187
|
-
text: `Analyze the impact of new content at ${pageUrl} on site ${siteUrl}.
|
|
1287
|
+
text: `Analyze the impact of new content at ${pageUrl} on site ${siteUrl} in ${engine === 'google' ? 'Google' : 'Bing'}.
|
|
1188
1288
|
|
|
1189
|
-
1. Use 'inspection_inspect' to check
|
|
1190
|
-
2. Use '
|
|
1191
|
-
3. Identify which queries are driving traffic to this page
|
|
1289
|
+
1. Use '${engine === 'google' ? 'inspection_inspect' : 'bing_url_info'}' to check indexing status.
|
|
1290
|
+
2. Use '${engine === 'google' ? 'analytics_query' : 'bing_analytics_page_query'}' to get performance for this specific URL.
|
|
1291
|
+
3. Identify which queries are driving traffic to this page.
|
|
1192
1292
|
|
|
1193
1293
|
Provide:
|
|
1194
1294
|
- Indexing status
|
|
@@ -1198,53 +1298,54 @@ Provide:
|
|
|
1198
1298
|
}
|
|
1199
1299
|
}]
|
|
1200
1300
|
}));
|
|
1201
|
-
server.prompt("mobile-vs-desktop", {
|
|
1301
|
+
server.prompt("mobile-vs-desktop", {
|
|
1302
|
+
siteUrl: z.string().describe("The URL of the site to analyze"),
|
|
1303
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine to use (default: google)")
|
|
1304
|
+
}, ({ siteUrl, engine = "google" }) => ({
|
|
1202
1305
|
messages: [{
|
|
1203
1306
|
role: "user",
|
|
1204
1307
|
content: {
|
|
1205
1308
|
type: "text",
|
|
1206
|
-
text: `Compare mobile vs desktop performance for ${siteUrl}.
|
|
1309
|
+
text: `Compare mobile vs desktop performance for ${siteUrl} on ${engine === 'google' ? 'Google' : 'Bing'}.
|
|
1207
1310
|
|
|
1208
|
-
|
|
1311
|
+
${engine === 'google'
|
|
1312
|
+
? "Use 'analytics_query' with dimension 'device' to get device-level metrics."
|
|
1313
|
+
: "Note: Bing Webmaster API provides limited native device breakdown via the public API, but check if 'bing_analytics_query' or 'bing_analytics_page' results show device distinctions if available."}
|
|
1209
1314
|
|
|
1210
1315
|
Analyze:
|
|
1211
|
-
1. Click and impression distribution across devices
|
|
1212
|
-
2. CTR differences
|
|
1316
|
+
1. Click and impression distribution across devices (if data available)
|
|
1317
|
+
2. CTR differences
|
|
1213
1318
|
3. Position ranking differences
|
|
1214
1319
|
|
|
1215
|
-
If there's a significant gap
|
|
1216
|
-
- Use 'inspection_inspect' on key pages to check
|
|
1217
|
-
- Recommend specific improvements
|
|
1320
|
+
If there's a significant gap, investigate:
|
|
1321
|
+
- Use '${engine === 'google' ? 'inspection_inspect' : 'bing_url_info'}' on key pages to check health/usability.
|
|
1322
|
+
- Recommend specific improvements.
|
|
1218
1323
|
|
|
1219
1324
|
Provide a summary with actionable recommendations.`
|
|
1220
1325
|
}
|
|
1221
1326
|
}]
|
|
1222
1327
|
}));
|
|
1223
1328
|
server.prompt("site-health-check", {
|
|
1224
|
-
siteUrl: z.string().optional().describe("Optional. The URL of a specific site to check.
|
|
1225
|
-
|
|
1329
|
+
siteUrl: z.string().optional().describe("Optional. The URL of a specific site to check."),
|
|
1330
|
+
engine: z.enum(["google", "bing"]).optional().describe("The search engine to use (default: google)")
|
|
1331
|
+
}, ({ siteUrl, engine = "google" }) => ({
|
|
1226
1332
|
messages: [{
|
|
1227
1333
|
role: "user",
|
|
1228
1334
|
content: {
|
|
1229
1335
|
type: "text",
|
|
1230
|
-
text: `Run a comprehensive health check${siteUrl ?
|
|
1336
|
+
text: `Run a comprehensive health check for ${siteUrl ? siteUrl : 'all verified sites'} on ${engine === 'google' ? 'Google' : 'Bing'}.
|
|
1231
1337
|
|
|
1232
|
-
Use the '
|
|
1338
|
+
Use the '${engine === 'google' ? 'sites_health_check' : 'bing_sites_health'}' tool.
|
|
1233
1339
|
|
|
1234
1340
|
Then for each site in the results:
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
4. **Anomalies:** Highlight any traffic anomaly drops detected.
|
|
1341
|
+
1. **Summarize the status** (healthy / warning / critical).
|
|
1342
|
+
2. **Performance:** Report changes in clicks, impressions, CTR, and position.
|
|
1343
|
+
3. **Sitemaps:** Note any errors or warnings (use '${engine === 'google' ? 'sitemaps_list' : 'bing_crawl_issues'}').
|
|
1344
|
+
4. **Anomalies:** Highlight any traffic drops (use '${engine === 'google' ? 'analytics_anomalies' : 'bing_analytics_detect_anomalies'}').
|
|
1240
1345
|
|
|
1241
1346
|
If any site has a 'critical' or 'warning' status:
|
|
1242
|
-
-
|
|
1243
|
-
-
|
|
1244
|
-
- For sitemap errors, use 'sitemaps_list' to get detailed error information.
|
|
1245
|
-
- Provide 3 prioritized action items for each affected site.
|
|
1246
|
-
|
|
1247
|
-
End with an overall portfolio summary and the single most important action to take right now.`
|
|
1347
|
+
- For critical drops, use '${engine === 'google' ? 'analytics_drop_attribution' : 'bing_analytics_drop_attribution'}'.
|
|
1348
|
+
- Provide 3 prioritized action items.`
|
|
1248
1349
|
}
|
|
1249
1350
|
}]
|
|
1250
1351
|
}));
|
|
@@ -1272,7 +1373,7 @@ async function main() {
|
|
|
1272
1373
|
const hasOAuthTokens = existsSync(tokenPath);
|
|
1273
1374
|
if (!hasServiceAccount && !hasOAuthTokens) {
|
|
1274
1375
|
console.error('\n╔══════════════════════════════════════════════════════════════╗');
|
|
1275
|
-
console.error('║
|
|
1376
|
+
console.error('║ 🚀 Search Console MCP ║');
|
|
1276
1377
|
console.error('╚══════════════════════════════════════════════════════════════╝\n');
|
|
1277
1378
|
console.error('❌ No authentication found.\n');
|
|
1278
1379
|
console.error('💡 Please authorize with your Google Account:');
|
|
@@ -1284,7 +1385,7 @@ async function main() {
|
|
|
1284
1385
|
}
|
|
1285
1386
|
const transport = new StdioServerTransport();
|
|
1286
1387
|
await server.connect(transport);
|
|
1287
|
-
console.error("
|
|
1388
|
+
console.error("Search Console MCP running on stdio");
|
|
1288
1389
|
}
|
|
1289
1390
|
main().catch((error) => {
|
|
1290
1391
|
console.error("Fatal error in main():", error);
|