search-console-mcp 1.13.2 → 1.13.3

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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  A Model Context Protocol (MCP) server that transforms how you interact with **Google Search Console**, **Bing Webmaster Tools**, and **Google Analytics 4**. Stop exporting CSVs and start asking questions.
5
5
 
6
- [📚 View Documentation](https://searchconsolemcp.mintlify.app/)
6
+ [📚 View Documentation](https://searchconsolemcp.saurabh.app/)
7
7
 
8
8
  ---
9
9
 
@@ -3,7 +3,7 @@ import { getBingClient } from '../client.js';
3
3
  * Get crawl issues for a site.
4
4
  */
5
5
  export async function getCrawlIssues(siteUrl) {
6
- const client = await getBingClient();
6
+ const client = await getBingClient(siteUrl);
7
7
  return client.getCrawlIssues(siteUrl);
8
8
  }
9
9
  /**
@@ -3,13 +3,13 @@ import { getBingClient } from '../client.js';
3
3
  * Get historical stats for a keyword.
4
4
  */
5
5
  export async function getKeywordStats(q, country, language) {
6
- const client = await getBingClient();
6
+ const client = await getBingClient(); // No siteUrl required for keyword research
7
7
  return client.getKeywordStats(q, country, language);
8
8
  }
9
9
  /**
10
10
  * Get related keywords and search volume.
11
11
  */
12
12
  export async function getRelatedKeywords(q, country, language) {
13
- const client = await getBingClient();
13
+ const client = await getBingClient(); // No siteUrl required for keyword research
14
14
  return client.getRelatedKeywords(q, country, language);
15
15
  }
@@ -6,12 +6,18 @@ export function jsonToCsv(data) {
6
6
  if (!data || data.length === 0) {
7
7
  return "";
8
8
  }
9
- // Get headers from the first object
10
- const headers = Object.keys(data[0]);
9
+ // Get all unique headers from all objects
10
+ const headerSet = new Set();
11
+ for (const row of data) {
12
+ if (row && typeof row === 'object') {
13
+ Object.keys(row).forEach(key => headerSet.add(key));
14
+ }
15
+ }
16
+ const headers = Array.from(headerSet);
11
17
  const csvRows = [headers.join(',')];
12
18
  for (const row of data) {
13
19
  const values = headers.map(header => {
14
- const val = row[header];
20
+ const val = row && typeof row === 'object' ? row[header] : undefined;
15
21
  const stringVal = val === undefined || val === null ? '' : String(val);
16
22
  // Escape double quotes by doubling them
17
23
  const escaped = stringVal.replace(/"/g, '""');
@@ -1,5 +1,6 @@
1
1
  import { getOrganicLandingPages } from './analytics.js';
2
2
  import { analyzePageSpeed } from '../../google/tools/pagespeed.js';
3
+ import { limitConcurrency } from '../../common/concurrency.js';
3
4
  export async function getPageSpeedCorrelation(propertyId, domain, startDate, endDate, limit = 5, strategy = 'mobile', accountId) {
4
5
  // Normalize domain
5
6
  let baseUrl = domain.trim();
@@ -11,28 +12,25 @@ export async function getPageSpeedCorrelation(propertyId, domain, startDate, end
11
12
  }
12
13
  // 1. Get top organic landing pages
13
14
  const pages = await getOrganicLandingPages(propertyId, startDate, endDate, limit, accountId);
14
- // 2. Run PageSpeed on each
15
- const results = await Promise.allSettled(pages.map(async (page) => {
16
- const path = page.landingPagePlusQueryString || page.pagePath;
17
- if (!path)
18
- throw new Error("No page path found");
19
- const fullUrl = `${baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;
20
- const psiResult = await analyzePageSpeed(fullUrl, strategy);
21
- return {
22
- ...page,
23
- pageSpeed: psiResult
24
- };
25
- }));
26
- // 3. Merge
27
- return results.map((res, index) => {
28
- if (res.status === 'fulfilled') {
29
- return res.value;
15
+ // 2. Run PageSpeed on each with concurrency limit
16
+ const results = await limitConcurrency(pages, 5, async (page) => {
17
+ try {
18
+ const path = page.landingPagePlusQueryString || page.pagePath;
19
+ if (!path)
20
+ throw new Error("No page path found");
21
+ const fullUrl = `${baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;
22
+ const psiResult = await analyzePageSpeed(fullUrl, strategy);
23
+ return {
24
+ ...page,
25
+ pageSpeed: psiResult
26
+ };
30
27
  }
31
- else {
28
+ catch (error) {
32
29
  return {
33
- ...pages[index],
34
- pageSpeedError: res.reason?.message || "Failed to analyze"
30
+ ...page,
31
+ pageSpeedError: error.message || "Failed to analyze"
35
32
  };
36
33
  }
37
34
  });
35
+ return results;
38
36
  }
@@ -0,0 +1,28 @@
1
+ import { loadConfig } from '../../common/auth/config.js';
2
+ /**
3
+ * List configured GA4 properties.
4
+ * Since GA4 properties are configured explicitly during setup,
5
+ * we return the configured properties.
6
+ */
7
+ export async function listProperties(accountId) {
8
+ const config = await loadConfig();
9
+ const accounts = Object.values(config.accounts).filter(a => a.engine === 'ga4');
10
+ if (accountId) {
11
+ const account = accounts.find(a => a.id === accountId);
12
+ if (!account) {
13
+ throw new Error(`GA4 account ${accountId} not found.`);
14
+ }
15
+ return [{
16
+ id: account.id,
17
+ alias: account.alias,
18
+ propertyId: account.ga4PropertyId,
19
+ siteUrl: account.ga4PropertyId // Alias for sites_list consistency
20
+ }];
21
+ }
22
+ return accounts.map(a => ({
23
+ id: a.id,
24
+ alias: a.alias,
25
+ propertyId: a.ga4PropertyId,
26
+ siteUrl: a.ga4PropertyId // Alias for sites_list consistency
27
+ }));
28
+ }
@@ -9,7 +9,7 @@ import { limitConcurrency } from '../../common/concurrency.js';
9
9
  * @returns Comprehensive indexing and status information for the URL.
10
10
  */
11
11
  export async function inspectUrl(siteUrl, inspectionUrl, languageCode = 'en-US') {
12
- const client = await getSearchConsoleClient();
12
+ const client = await getSearchConsoleClient(siteUrl);
13
13
  const res = await client.urlInspection.index.inspect({
14
14
  requestBody: {
15
15
  inspectionUrl,
@@ -6,7 +6,7 @@ import { getSearchConsoleClient } from '../client.js';
6
6
  * @returns A list of sitemap metadata objects.
7
7
  */
8
8
  export async function listSitemaps(siteUrl) {
9
- const client = await getSearchConsoleClient();
9
+ const client = await getSearchConsoleClient(siteUrl);
10
10
  const res = await client.sitemaps.list({ siteUrl });
11
11
  return res.data.sitemap || [];
12
12
  }
@@ -18,7 +18,7 @@ export async function listSitemaps(siteUrl) {
18
18
  * @returns A success message.
19
19
  */
20
20
  export async function submitSitemap(siteUrl, feedpath) {
21
- const client = await getSearchConsoleClient();
21
+ const client = await getSearchConsoleClient(siteUrl);
22
22
  await client.sitemaps.submit({ siteUrl, feedpath });
23
23
  return `Successfully submitted sitemap: ${feedpath} for ${siteUrl}`;
24
24
  }
@@ -30,7 +30,7 @@ export async function submitSitemap(siteUrl, feedpath) {
30
30
  * @returns A success message.
31
31
  */
32
32
  export async function deleteSitemap(siteUrl, feedpath) {
33
- const client = await getSearchConsoleClient();
33
+ const client = await getSearchConsoleClient(siteUrl);
34
34
  await client.sitemaps.delete({ siteUrl, feedpath });
35
35
  return `Successfully deleted sitemap: ${feedpath} from ${siteUrl}`;
36
36
  }
@@ -42,7 +42,7 @@ export async function deleteSitemap(siteUrl, feedpath) {
42
42
  * @returns Sitemap details including status and item counts.
43
43
  */
44
44
  export async function getSitemap(siteUrl, feedpath) {
45
- const client = await getSearchConsoleClient();
45
+ const client = await getSearchConsoleClient(siteUrl);
46
46
  const res = await client.sitemaps.get({ siteUrl, feedpath });
47
47
  return res.data;
48
48
  }
package/dist/index.js CHANGED
@@ -32,7 +32,10 @@ import * as ga4Behavior from "./ga4/tools/behavior.js";
32
32
  import * as ga4PageSpeed from "./ga4/tools/pagespeed.js";
33
33
  import * as ga4GscComparator from "./common/tools/compare-engines/ga4-gsc-comparator.js";
34
34
  import * as ga4GscBingComparator from "./common/tools/compare-engines/ga4-gsc-bing-comparator.js";
35
+ import * as ga4Properties from "./ga4/tools/properties.js";
35
36
  import { loadConfig, removeAccount, updateAccount } from './common/auth/config.js';
37
+ import { normalizeWebsite } from './common/auth/resolver.js';
38
+ import { limitConcurrency } from './common/concurrency.js';
36
39
  import { bingApiDocs, indexNowDocs, dimensionsDocs as bingDimensionsDocs, filtersDocs as bingFiltersDocs, searchTypesDocs as bingSearchTypesDocs, patternsDocs as bingPatternsDocs, algorithmUpdatesDocs as bingAlgorithmUpdatesDocs } from "./bing/docs/index.js";
37
40
  import { formatError } from "./common/errors.js";
38
41
  import { existsSync } from "fs";
@@ -64,7 +67,7 @@ const server = new McpServer({
64
67
  // Get Started Tool
65
68
  server.tool(getStartedToolName, getStartedToolDescription, getStartedToolSchema, getStartedHandler);
66
69
  // Sites Tools
67
- server.tool("sites_list", "List all verified sites across all authorized accounts", { engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)") }, async ({ engine = "google" }) => {
70
+ server.tool("sites_list", "List all verified sites or properties across all authorized accounts", { engine: z.enum(["google", "bing", "ga4"]).optional().describe("The search engine (default: google)") }, async ({ engine = "google" }) => {
68
71
  try {
69
72
  const config = await loadConfig();
70
73
  const accounts = Object.values(config.accounts).filter(a => a.engine === engine);
@@ -73,26 +76,54 @@ server.tool("sites_list", "List all verified sites across all authorized account
73
76
  const results = await bingSites.listSites();
74
77
  return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
75
78
  }
76
- const allResults = [];
77
- for (const account of accounts) {
79
+ const allResults = await limitConcurrency(accounts, 5, async (account) => {
78
80
  try {
79
- const results = engine === "google"
80
- ? await sites.listSites(account.id)
81
- : await bingSites.listSites(account.id);
82
- allResults.push({
81
+ let results;
82
+ if (engine === "google") {
83
+ results = await sites.listSites(account.id);
84
+ }
85
+ else if (engine === "bing") {
86
+ results = await bingSites.listSites(account.id);
87
+ }
88
+ else if (engine === "ga4") {
89
+ results = await ga4Properties.listProperties(account.id);
90
+ }
91
+ else {
92
+ results = [];
93
+ }
94
+ // Boundary Filtering: Honor the website in the config
95
+ if (account.websites && account.websites.length > 0) {
96
+ results = results.filter(site => {
97
+ const url = engine === "ga4" ? (site.propertyId) : (site.siteUrl || site.Url);
98
+ if (!url)
99
+ return false;
100
+ // If it's a numeric property ID (GA4), check direct inclusion
101
+ if (engine === "ga4" && /^\d+$/.test(url)) {
102
+ return account.websites.includes(url);
103
+ }
104
+ try {
105
+ const normalizedSite = normalizeWebsite(url).value;
106
+ return account.websites.some(w => normalizeWebsite(w).value === normalizedSite);
107
+ }
108
+ catch {
109
+ return account.websites.includes(url);
110
+ }
111
+ });
112
+ }
113
+ return {
83
114
  account: account.alias,
84
115
  accountId: account.id,
85
116
  sites: results
86
- });
117
+ };
87
118
  }
88
119
  catch (e) {
89
- allResults.push({
120
+ return {
90
121
  account: account.alias,
91
122
  accountId: account.id,
92
123
  error: e.message
93
- });
124
+ };
94
125
  }
95
- }
126
+ });
96
127
  return {
97
128
  content: [{ type: "text", text: JSON.stringify(allResults, null, 2) }]
98
129
  };
@@ -101,6 +132,49 @@ server.tool("sites_list", "List all verified sites across all authorized account
101
132
  return formatError(error);
102
133
  }
103
134
  });
135
+ server.tool("bing_seo_cannibalization", "Detect pages competing for the same query in Bing.", {
136
+ siteUrl: z.string().describe("The URL of the site"),
137
+ minImpressions: z.number().optional().describe("Minimum impressions threshold (default 50)")
138
+ }, async ({ siteUrl, minImpressions }) => {
139
+ try {
140
+ const results = await bingSeoInsights.detectCannibalization(siteUrl, { minImpressions });
141
+ return {
142
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
143
+ };
144
+ }
145
+ catch (error) {
146
+ return formatError(error);
147
+ }
148
+ });
149
+ server.tool("bing_seo_lost_queries", "Identify queries that lost significant traffic on Bing compared to the previous period.", {
150
+ siteUrl: z.string().describe("The URL of the site"),
151
+ days: z.number().optional().describe("Lookback period in days (default 28)")
152
+ }, async ({ siteUrl, days }) => {
153
+ try {
154
+ const results = await bingSeoInsights.findLostQueries(siteUrl, { days });
155
+ return {
156
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
157
+ };
158
+ }
159
+ catch (error) {
160
+ return formatError(error);
161
+ }
162
+ });
163
+ server.tool("bing_brand_analysis", "Analyze Brand vs Non-Brand performance on Bing.", {
164
+ siteUrl: z.string().describe("The URL of the site"),
165
+ brandRegex: z.string().describe("Regex matching brand terms (e.g. 'acme|acmecorp')"),
166
+ days: z.number().optional().describe("Number of days to analyze (default 28)")
167
+ }, async ({ siteUrl, brandRegex, days }) => {
168
+ try {
169
+ const results = await bingSeoInsights.analyzeBrandVsNonBrand(siteUrl, brandRegex, { days });
170
+ return {
171
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
172
+ };
173
+ }
174
+ catch (error) {
175
+ return formatError(error);
176
+ }
177
+ });
104
178
  server.tool("bing_analytics_trends", "Detect rising or declining trends in Bing query performance", {
105
179
  siteUrl: z.string().describe("The URL of the site"),
106
180
  days: z.number().optional().describe("Number of days to analyze (default 28)"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "search-console-mcp",
3
- "version": "1.13.2",
3
+ "version": "1.13.3",
4
4
  "mcpName": "io.github.saurabhsharma2u/search-console-mcp",
5
5
  "description": "MCP server for Google Search Console, Bing Webmaster Tools, and Google Analytics 4",
6
6
  "type": "module",
@@ -32,13 +32,13 @@
32
32
  "author": "",
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
- "@adobe/structured-data-validator": "^1.6.0",
35
+ "@adobe/structured-data-validator": "^1.7.0",
36
36
  "@google-analytics/data": "^5.2.1",
37
37
  "@modelcontextprotocol/sdk": "^1.26.0",
38
38
  "@napi-rs/keyring": "^1.2.0",
39
39
  "cheerio": "^1.2.0",
40
40
  "dotenv": "^17.3.1",
41
- "googleapis": "^171.0.0",
41
+ "googleapis": "^171.4.0",
42
42
  "node-machine-id": "^1.1.12",
43
43
  "open": "^11.0.0",
44
44
  "re2": "^1.23.3",