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.
@@ -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 }, true);
45
+ return this.request('GetQueryStats', { siteUrl });
40
46
  }
41
47
  async getPageStats(siteUrl) {
42
- return this.request('GetPageStats', { siteUrl }, true);
48
+ return this.request('GetPageStats', { siteUrl });
49
+ }
50
+ async getPageQueryStats(siteUrl, page) {
51
+ return this.request('GetPageQueryStats', { siteUrl, page });
43
52
  }
44
- async getPageQueryStats(siteUrl, pageUrl) {
45
- return this.request('GetPageQueryStats', { siteUrl, pageUrl }, true);
53
+ async submitSitemap(siteUrl, feedUrl) {
54
+ await this.request('SubmitFeed', { siteUrl, feedUrl }, true);
46
55
  }
47
- async submitSitemap(siteUrl, sitemapUrl) {
48
- await this.request('SubmitSitemap', { siteUrl, sitemapUrl }, true);
56
+ async deleteSitemap(siteUrl, feedUrl) {
57
+ await this.request('RemoveFeed', { siteUrl, feedUrl }, true);
49
58
  }
50
- async getSitemaps(siteUrl) {
51
- return this.request('GetSitemaps', { siteUrl }, true);
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 }, true);
63
+ return this.request('GetKeywordStats', { q, country, language });
55
64
  }
56
65
  async getCrawlIssues(siteUrl) {
57
- return this.request('GetCrawlIssues', { siteUrl }, true);
66
+ return this.request('GetCrawlIssues', { siteUrl });
58
67
  }
59
68
  async getUrlSubmissionQuota(siteUrl) {
60
- return this.request('GetUrlSubmissionQuota', { siteUrl }, true);
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 }, true);
78
+ return this.request('GetQueryPageStats', { siteUrl });
70
79
  }
71
80
  async getRankAndTrafficStats(siteUrl) {
72
- return this.request('GetRankAndTrafficStats', { siteUrl }, true);
81
+ return this.request('GetRankAndTrafficStats', { siteUrl });
73
82
  }
74
83
  async getCrawlStats(siteUrl) {
75
- return this.request('GetCrawlStats', { siteUrl }, true);
84
+ return this.request('GetCrawlStats', { siteUrl });
76
85
  }
77
86
  async getUrlInfo(siteUrl, url) {
78
- return this.request('GetUrlInfo', { siteUrl, url }, true);
87
+ return this.request('GetUrlInfo', { siteUrl, url });
79
88
  }
80
89
  async getLinkCounts(siteUrl) {
81
- return this.request('GetLinkCounts', { siteUrl }, true);
90
+ return this.request('GetLinkCounts', { siteUrl });
82
91
  }
83
92
  async getRelatedKeywords(q, country, language) {
84
- return this.request('GetRelatedKeywords', { q, country, language }, true);
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.getSitemaps(siteUrl);
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
+ }
@@ -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 in Search Console", {}, async () => {
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 result = await sites.listSites();
40
+ const results = engine === "google" ? await sites.listSites() : await bingSites.listSites();
41
41
  return {
42
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
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 website to Search Console", { siteUrl: z.string().describe("The URL of the site to add (e.g., 'https://example.com' or 'sc-domain:example.com')") }, async ({ siteUrl }) => {
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 website from Search Console", { siteUrl: z.string().describe("The URL of the site to delete") }, async ({ siteUrl }) => {
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 week-over-week performance, sitemap status, and traffic anomalies. Returns a structured report with an overall status (healthy/warning/critical) per site.", {
83
- siteUrl: z.string().optional().describe("Optional. The URL of a specific site to check. If omitted, checks all verified sites.")
84
- }, async ({ siteUrl }) => {
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 = await sitesHealth.healthCheck(siteUrl);
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", { siteUrl: z.string().describe("The URL of the site") }, async ({ siteUrl }) => {
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 result = await sitemaps.listSitemaps(siteUrl);
110
+ const results = engine === "google" ? await sitemaps.listSitemaps(siteUrl) : await bingSitemaps.listSitemaps(siteUrl);
99
111
  return {
100
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
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
- }, async ({ siteUrl, feedpath }) => {
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
- }, async ({ siteUrl, feedpath }) => {
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. Defaults to 28 days. Note: Data is typically delayed by 2-3 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
- }, async ({ siteUrl, days }) => {
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 = await analytics.getPerformanceSummary(siteUrl, days);
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 mobile usability", {
357
- siteUrl: z.string().describe("The URL of the property (must match a verified property in Search Console)"),
358
- inspectionUrl: z.string().describe("The fully-qualified URL to inspect (must be under the siteUrl property)"),
359
- languageCode: z.string().optional().describe("Language code for localized results (default: en-US)")
360
- }, async ({ siteUrl, inspectionUrl, languageCode }) => {
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 = await inspection.inspectUrl(siteUrl, inspectionUrl, languageCode);
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 (positions 5-20) that have potential for quick wins", {
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 to analyze (default: 28)"),
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
- }, async ({ siteUrl, days, minImpressions, limit }) => {
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 = await seoInsights.findLowHangingFruit(siteUrl, { days, minImpressions, limit });
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 to analyze (default: 28)"),
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
- }, async ({ siteUrl, days, minImpressions, limit }) => {
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 = await seoInsights.detectCannibalization(siteUrl, { days, minImpressions, limit });
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 ranking in positions 1-10 with low CTR (< 60% of benchmark). Great for title tag optimization.", {
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 to analyze (default: 28)"),
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 results to return (default: 50)")
451
- }, async ({ siteUrl, days, minImpressions, limit }) => {
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 = await seoInsights.findLowCTROpportunities(siteUrl, { days, minImpressions, limit });
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 to analyze (default: 28)"),
465
- limit: z.number().optional().describe("Max results to return (default: 50)")
466
- }, async ({ siteUrl, days, limit }) => {
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 = await seoInsights.findStrikingDistance(siteUrl, { days, limit });
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 to analyze (default: 28)"),
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
- }, async ({ siteUrl, days, minImpressions, limit }) => {
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 = await seoInsights.findQuickWins(siteUrl, { days, minImpressions, limit });
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).", { position: z.number().describe("The ranking position") }, async ({ position }) => {
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
- }, async ({ current, previous }) => {
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
- }, async ({ query, brandRegex }) => {
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
- }, async ({ query, pageA_position, pageA_impressions, pageA_clicks, pageB_position, pageB_impressions, pageB_clicks }) => {
554
- const pageA = { position: pageA_position, impressions: pageA_impressions, clicks: pageA_clicks };
555
- const pageB = { position: pageB_position, impressions: pageB_impressions, clicks: pageB_clicks };
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", { siteUrl: z.string().describe("The URL of the site to analyze") }, ({ siteUrl }) => ({
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
- Use the 'analytics_performance_summary' tool to get high-level metrics, and 'analytics_query' to dig deeper into top queries and pages if needed.
1115
- Provide a summary of the site's health and any opportunities for improvement.`
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", { siteUrl: z.string().describe("The URL of the site to analyze") }, ({ siteUrl }) => ({
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
- Use the 'analytics_compare_periods' tool to compare the two periods:
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", { siteUrl: z.string().describe("The URL of the site to analyze") }, ({ siteUrl }) => ({
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
- Steps:
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", { siteUrl: z.string().describe("The URL of the site to analyze") }, ({ siteUrl }) => ({
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
- Use 'analytics_top_queries' to get top queries, then analyze for:
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
- - These need better titles/meta descriptions
1166
- - Look for CTR < 2% with impressions > 100
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
- }, ({ siteUrl, pageUrl }) => ({
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 if the page is indexed
1190
- 2. Use 'analytics_query' with a page filter for this URL to get its performance data
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", { siteUrl: z.string().describe("The URL of the site to analyze") }, ({ siteUrl }) => ({
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
- Use 'analytics_query' with dimension 'device' to get device-level metrics.
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 between mobile and desktop
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 (e.g., mobile CTR much lower), investigate:
1216
- - Use 'inspection_inspect' on key pages to check mobile usability
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. If omitted, checks all verified sites.")
1225
- }, ({ siteUrl }) => ({
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 ? ` for ${siteUrl}` : ' across all my verified sites'}.
1336
+ text: `Run a comprehensive health check for ${siteUrl ? siteUrl : 'all verified sites'} on ${engine === 'google' ? 'Google' : 'Bing'}.
1231
1337
 
1232
- Use the 'sites_health_check' tool${siteUrl ? ` with siteUrl '${siteUrl}'` : ' without a siteUrl to check all sites'}.
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
- 1. **Summarize the status** (healthy / warning / critical) with a clear visual indicator.
1237
- 2. **Performance:** Report the week-over-week changes in clicks, impressions, CTR, and position.
1238
- 3. **Sitemaps:** Note any missing sitemaps, errors, or warnings.
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
- - List the specific issues found.
1243
- - For critical traffic drops, use 'analytics_drop_attribution' to check if a Google algorithm update is the likely cause.
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('║ 🚀 Google Search Console MCP Server ║');
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("Google Search Console MCP Server running on stdio");
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "search-console-mcp",
3
- "version": "1.11.0",
3
+ "version": "1.11.1",
4
4
  "mcpName": "io.github.saurabhsharma2u/search-console-mcp",
5
5
  "description": "MCP server for Google Search and Bing Search APIs",
6
6
  "type": "module",