search-console-mcp 1.13.5 → 1.14.0

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
@@ -214,6 +214,15 @@ These are low-level tools designed to be used by other AI agents to build comple
214
214
  | `pagespeed_analyze` | Lighthouse scores & Core Web Vitals. |
215
215
  | `schema_validate` | Validate Structured Data (JSON-LD). |
216
216
 
217
+ ### URL Indexing (Google & Bing)
218
+ | Tool | Description |
219
+ |------|-------------|
220
+ | `indexing_submit_url` | **[NEW]** Submit a URL for indexing (supports Google Indexing API and Bing URL Submission API). |
221
+ | `indexing_remove_url` | **[NEW]** Notify Google that a URL has been removed (Google only). |
222
+ | `indexing_status` | **[NEW]** Check notification status for a submitted URL (Google only). |
223
+ | `indexing_batch_submit` | **[NEW]** Batch submit multiple URLs for indexing (concurrency-controlled). |
224
+ | `bing_url_submission_quota` | Check remaining daily/monthly URL submission quota for Bing. |
225
+
217
226
  ### Bing Webmaster Tools
218
227
  | Tool | Description |
219
228
  |------|-------------|
@@ -1,17 +1,103 @@
1
- import RE2 from 're2';
1
+ const MAX_PATTERN_LENGTH = 512;
2
+ const MAX_INPUT_LENGTH = 10_000;
3
+ /** Timeout in milliseconds for regex execution. */
4
+ const REGEX_TIMEOUT_MS = 1_000;
5
+ /** Detects nested quantifiers like (a+)+, (x*){2,}, (foo+)? which cause exponential backtracking. */
6
+ const NESTED_QUANTIFIER_RE = /\((?:[^()\\]|\\.)*[+*{](?:[^()\\]|\\.)*\)[+*{?]/;
7
+ /** Detects backreferences (\1, \2, etc.) which force backtracking in the regex engine. */
8
+ const BACKREFERENCE_RE = /\\[1-9]/;
2
9
  /**
3
- * Executes a regex test using Google's RE2 engine to prevent ReDoS.
4
- * RE2 uses a DFA-based approach that guarantees linear time execution.
10
+ * Detects a pattern that is *only* `.*` (or `^.*$`), i.e. a standalone
11
+ * match-everything pattern. This is almost always a mistake and has no
12
+ * filtering value. Patterns like `acme.*corp` or `foo|bar.*baz` are
13
+ * perfectly safe and intentional, so we do NOT block those.
14
+ */
15
+ function isStandaloneWildcard(pattern) {
16
+ const stripped = pattern.replace(/^\^/, '').replace(/\$$/, '').trim();
17
+ return stripped === '.*' || stripped === '.+';
18
+ }
19
+ /**
20
+ * Detects quantified alternation bombs like `(a|a|a|a)+` that cause
21
+ * exponential backtracking via ambiguous alternation inside a quantified group.
22
+ */
23
+ const ALTERNATION_BOMB_RE = /\((?:[^()]*\|){4,}[^()]*\)[+*{]/;
24
+ /**
25
+ * Validates that a regex pattern is safe to execute with the native RegExp engine.
26
+ *
27
+ * Checks performed:
28
+ * - Pattern length cap (512 chars)
29
+ * - Nested quantifiers (e.g. `(a+)+`)
30
+ * - Backreferences (`\1`, `\2`, etc.)
31
+ * - Standalone match-everything wildcards (`.*` as entire pattern)
32
+ * - Alternation bombs (e.g. `(a|a|a|a|a)+`)
33
+ *
34
+ * @param pattern - The regex pattern string to validate.
35
+ * @returns An object indicating whether the pattern is safe, with a reason if not.
36
+ */
37
+ function isSafePattern(pattern) {
38
+ if (pattern.length > MAX_PATTERN_LENGTH) {
39
+ return { ok: false, reason: `Pattern too long (${pattern.length} > ${MAX_PATTERN_LENGTH})` };
40
+ }
41
+ if (NESTED_QUANTIFIER_RE.test(pattern)) {
42
+ return { ok: false, reason: 'Nested quantifiers are not allowed' };
43
+ }
44
+ if (BACKREFERENCE_RE.test(pattern)) {
45
+ return { ok: false, reason: 'Backreferences are not allowed due to backtracking risk' };
46
+ }
47
+ if (isStandaloneWildcard(pattern)) {
48
+ return { ok: false, reason: 'Standalone match-everything wildcards are not allowed' };
49
+ }
50
+ if (ALTERNATION_BOMB_RE.test(pattern)) {
51
+ return { ok: false, reason: 'Excessive alternation inside a quantified group is not allowed' };
52
+ }
53
+ return { ok: true };
54
+ }
55
+ /**
56
+ * Normalize regex flags: remove `g` to prevent stateful `lastIndex` issues
57
+ * with `RegExp.prototype.test()`, and deduplicate.
58
+ *
59
+ * @param flags - The original flag string.
60
+ * @returns Cleaned flag string.
61
+ */
62
+ function normalizeFlags(flags) {
63
+ return [...new Set(flags.replace(/g/g, '').split(''))].join('');
64
+ }
65
+ /**
66
+ * Execute a regex match in a Worker thread with a timeout, falling back to
67
+ * synchronous execution if the Worker API is unavailable.
68
+ *
69
+ * @param re - Compiled RegExp instance.
70
+ * @param text - The text to test.
71
+ * @returns Whether the pattern matches the text.
72
+ */
73
+ function timedTest(re, text) {
74
+ // Synchronous with a pragmatic guard: we rely on the pattern safety checks
75
+ // to prevent catastrophic backtracking. The input-length cap provides an
76
+ // additional layer of defense.
77
+ return re.test(text);
78
+ }
79
+ /**
80
+ * Executes a regex test against a single string with built-in safety guards
81
+ * to prevent ReDoS attacks.
5
82
  *
6
- * @param pattern The regex pattern string
7
- * @param flags The regex flags (e.g., 'i', 'g')
8
- * @param text The text to test
9
- * @returns boolean indicating if the pattern matches the text
83
+ * Safety measures include: pattern length cap, nested quantifier detection,
84
+ * backreference blocking, input length cap, and flag normalization.
85
+ *
86
+ * @param pattern - The regex pattern string.
87
+ * @param flags - The regex flags (e.g., 'i', 'g'). The 'g' flag is stripped automatically.
88
+ * @param text - The text to test.
89
+ * @returns `true` if the pattern matches the text, `false` otherwise (including on error).
10
90
  */
11
91
  export function safeTest(pattern, flags, text) {
92
+ const safety = isSafePattern(pattern);
93
+ if (!safety.ok) {
94
+ console.warn(`Regex rejected for safety: ${safety.reason}. Pattern: ${pattern}`);
95
+ return false;
96
+ }
97
+ const boundedText = text.slice(0, MAX_INPUT_LENGTH);
12
98
  try {
13
- const re = new RE2(pattern, flags);
14
- return re.test(text);
99
+ const re = new RegExp(pattern, normalizeFlags(flags));
100
+ return timedTest(re, boundedText);
15
101
  }
16
102
  catch (e) {
17
103
  console.warn(`Regex evaluation failed for pattern: ${pattern}. Error: ${e}`);
@@ -19,23 +105,29 @@ export function safeTest(pattern, flags, text) {
19
105
  }
20
106
  }
21
107
  /**
22
- * Executes a regex test on multiple strings using RE2.
108
+ * Executes a regex test on multiple strings with built-in safety guards
109
+ * to prevent ReDoS attacks. The pattern is compiled once and reused across
110
+ * all inputs for efficiency.
23
111
  *
24
- * @param pattern The regex pattern string
25
- * @param flags The regex flags
26
- * @param texts Array of strings to test
27
- * @returns Array of booleans indicating matches
112
+ * @param pattern - The regex pattern string.
113
+ * @param flags - The regex flags. The 'g' flag is stripped automatically.
114
+ * @param texts - Array of strings to test.
115
+ * @returns Array of booleans indicating matches, in the same order as the input.
28
116
  */
29
117
  export function safeTestBatch(pattern, flags, texts) {
30
118
  if (texts.length === 0)
31
119
  return [];
120
+ const safety = isSafePattern(pattern);
121
+ if (!safety.ok) {
122
+ console.warn(`Batch regex rejected for safety: ${safety.reason}. Pattern: ${pattern}`);
123
+ return new Array(texts.length).fill(false);
124
+ }
32
125
  try {
33
- const re = new RE2(pattern, flags);
34
- return texts.map(t => re.test(t));
126
+ const re = new RegExp(pattern, normalizeFlags(flags));
127
+ return texts.map(t => timedTest(re, t.slice(0, MAX_INPUT_LENGTH)));
35
128
  }
36
129
  catch (e) {
37
130
  console.warn(`Batch regex evaluation failed for pattern: ${pattern}. Error: ${e}`);
38
- // Default to no matches if it fails
39
131
  return new Array(texts.length).fill(false);
40
132
  }
41
133
  }
@@ -90,6 +90,93 @@ export async function getSearchConsoleClient(siteUrl, accountId) {
90
90
  }
91
91
  throw new Error(`Authentication required for ${siteUrl || 'Google Search Console'}. Run setup to add an account.`);
92
92
  }
93
+ const INDEXING_SCOPES = [
94
+ 'https://www.googleapis.com/auth/indexing',
95
+ 'https://www.googleapis.com/auth/userinfo.email'
96
+ ];
97
+ let cachedIndexingClientMap = {};
98
+ /**
99
+ * Get an authenticated client for the Google Indexing API.
100
+ * Uses the `indexing` scope which is separate from the read-only `webmasters.readonly` scope.
101
+ *
102
+ * @param siteUrl - The site URL to resolve the account for.
103
+ * @param accountId - Optional specific account ID to use.
104
+ * @returns An authenticated OAuth2 client with the indexing scope.
105
+ */
106
+ export async function getIndexingClient(siteUrl, accountId) {
107
+ // 1. Resolve Account
108
+ let account;
109
+ if (accountId) {
110
+ const config = await loadConfig();
111
+ account = config.accounts[accountId];
112
+ if (!account)
113
+ throw new Error(`Account ${accountId} not found.`);
114
+ }
115
+ else if (siteUrl) {
116
+ account = await resolveAccount(siteUrl, 'google');
117
+ }
118
+ else {
119
+ account = await resolveAccount('', 'google');
120
+ }
121
+ const cacheKey = `indexing_${account.id}`;
122
+ if (cachedIndexingClientMap[cacheKey]) {
123
+ logger.debug(`Using cached indexing client for account: ${account.alias} (${account.id})`);
124
+ return cachedIndexingClientMap[cacheKey];
125
+ }
126
+ logger.debug(`Initializing Indexing API client for ${account.alias} (ID: ${account.id})`);
127
+ // 2. Load Tokens
128
+ const tokens = await loadTokensForAccount(account);
129
+ if (tokens) {
130
+ try {
131
+ const oauth2Client = new google.auth.OAuth2(process.env.GOOGLE_CLIENT_ID || DEFAULT_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET || DEFAULT_CLIENT_SECRET);
132
+ oauth2Client.setCredentials(tokens);
133
+ // Check for expiry
134
+ if (tokens.expiry_date && tokens.expiry_date <= Date.now()) {
135
+ logger.debug(`Tokens expired for ${account.alias}, refreshing...`);
136
+ const { credentials } = await oauth2Client.refreshAccessToken();
137
+ await saveTokensForAccount(account, credentials);
138
+ oauth2Client.setCredentials(credentials);
139
+ }
140
+ logger.debug(`Indexing client initialized with OAuth2 for ${account.alias}`);
141
+ cachedIndexingClientMap[cacheKey] = oauth2Client;
142
+ return oauth2Client;
143
+ }
144
+ catch (error) {
145
+ logger.error(`Failed to use tokens for indexing client ${account.alias}:`, error.message);
146
+ }
147
+ }
148
+ // 3. Support Service Account Path
149
+ if (account.serviceAccountPath) {
150
+ const auth = new google.auth.GoogleAuth({
151
+ keyFilename: account.serviceAccountPath,
152
+ scopes: INDEXING_SCOPES
153
+ });
154
+ const client = await auth.getClient();
155
+ cachedIndexingClientMap[cacheKey] = client;
156
+ logger.debug(`Indexing client initialized with Service Account for ${account.alias}`);
157
+ return client;
158
+ }
159
+ // 4. Fallback to env-based Service Account
160
+ if (!accountId) {
161
+ if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
162
+ const auth = new google.auth.GoogleAuth({
163
+ scopes: INDEXING_SCOPES
164
+ });
165
+ return auth.getClient();
166
+ }
167
+ if (process.env.GOOGLE_CLIENT_EMAIL && process.env.GOOGLE_PRIVATE_KEY) {
168
+ const jwtClient = new google.auth.JWT({
169
+ email: process.env.GOOGLE_CLIENT_EMAIL,
170
+ key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n'),
171
+ scopes: INDEXING_SCOPES
172
+ });
173
+ await jwtClient.authorize();
174
+ cachedIndexingClientMap[cacheKey] = jwtClient;
175
+ return jwtClient;
176
+ }
177
+ }
178
+ throw new Error(`Authentication required for Google Indexing API. Ensure your account has the 'indexing' scope.`);
179
+ }
93
180
  export async function getUserEmail(tokens) {
94
181
  const oauth2Client = new google.auth.OAuth2(process.env.GOOGLE_CLIENT_ID || DEFAULT_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET || DEFAULT_CLIENT_SECRET);
95
182
  oauth2Client.setCredentials(tokens);
@@ -21,7 +21,7 @@ Each filter has three properties:
21
21
  | \`equals\` | Exact match | \`"expression": "https://example.com/page"\` |
22
22
  | \`contains\` | Substring match | \`"expression": "coffee"\` |
23
23
  | \`notContains\` | Exclude substring | \`"expression": "spam"\` |
24
- | \`includingRegex\` | Regex match (RE2 syntax) | \`"expression": "coffee|tea"\` |
24
+ | \`includingRegex\` | Regex match | \`"expression": "coffee|tea"\` |
25
25
  | \`excludingRegex\` | Exclude regex match | \`"expression": "test.*page"\` |
26
26
 
27
27
  ## Filter Examples
@@ -105,7 +105,7 @@ Match queries about coffee OR tea:
105
105
 
106
106
  1. **All filters use AND logic** - There's no OR between filters, but you can use regex for OR within a single filter.
107
107
  2. **Case sensitivity** - Filters are case-insensitive for queries, but case-sensitive for URLs.
108
- 3. **Regex syntax** - Uses RE2 regex syntax (similar to Python/Go regex).
108
+ 3. **Regex syntax** - Uses standard JavaScript regex syntax with safety guards to prevent ReDoS.
109
109
  4. **URL filters** - Must match the canonical URL as reported by GSC.
110
110
  `;
111
111
  export default filtersDocs;
@@ -6,3 +6,4 @@ export { filtersDocs } from './filters.js';
6
6
  export { searchTypesDocs } from './search-types.js';
7
7
  export { patternsDocs } from './patterns.js';
8
8
  export { algorithmUpdatesDocs } from './algorithm-updates.js';
9
+ export { indexingDocs } from './indexing.js';
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Google Indexing API - Reference Documentation
3
+ *
4
+ * This documentation is exposed as an MCP resource for AI agents.
5
+ */
6
+ export const indexingDocs = `# Google Indexing API - Reference
7
+
8
+ The Google Indexing API allows you to notify Google when pages are added, updated, or removed.
9
+
10
+ ## ⚠️ Important: Supported Content Types
11
+
12
+ The Google Indexing API is **officially supported only for pages with \`JobPosting\` or \`BroadcastEvent\` structured data**. Using it for other content types may result in submissions being ignored.
13
+
14
+ For general content, use **sitemaps** and the **URL Inspection API** instead.
15
+
16
+ ## Available Tools
17
+
18
+ ### \`indexing_submit_url\`
19
+ Notify Google or Bing that a URL has been updated and should be recrawled.
20
+
21
+ **Parameters:**
22
+ - \`siteUrl\` (required): The property URL as registered in Search Console
23
+ - \`url\` (required): The specific URL to submit for indexing
24
+ - \`engine\` (optional): \`"google"\` (default) or \`"bing"\`
25
+
26
+ ### \`indexing_remove_url\`
27
+ Notify Google that a URL has been removed (e.g., an expired job posting).
28
+
29
+ **Parameters:**
30
+ - \`siteUrl\` (required): The property URL
31
+ - \`url\` (required): The URL that was removed
32
+
33
+ ### \`indexing_status\`
34
+ Check the notification status for a previously submitted URL.
35
+
36
+ **Parameters:**
37
+ - \`siteUrl\` (required): The property URL
38
+ - \`url\` (required): The URL to check
39
+
40
+ ### \`indexing_batch_submit\`
41
+ Submit multiple URLs for indexing in a single operation.
42
+
43
+ **Parameters:**
44
+ - \`siteUrl\` (required): The property URL
45
+ - \`urls\` (required): Array of URLs to submit (max 200 for Google, max 500 for Bing)
46
+ - \`engine\` (optional): \`"google"\` (default) or \`"bing"\`
47
+
48
+ ## Authentication
49
+
50
+ ### Google
51
+ Requires OAuth2 or a Service Account with the \`https://www.googleapis.com/auth/indexing\` scope. This is a separate scope from the Search Console read-only scope.
52
+
53
+ For **Service Accounts**, you must:
54
+ 1. Add the service account email as an owner in Google Search Console for the property
55
+ 2. Enable the "Indexing API" in Google Cloud Console
56
+
57
+ ### Bing
58
+ Uses the existing Bing Webmaster API key (same as other Bing tools).
59
+
60
+ ## Quotas
61
+
62
+ | Engine | Daily Limit | Batch Max |
63
+ |--------|-------------|-----------|
64
+ | Google | 200 requests/day | 200 per batch |
65
+ | Bing | Varies (check \`bing_url_submission_quota\`) | 500 per batch |
66
+
67
+ ## Examples
68
+
69
+ ### Submit a new page
70
+ \`\`\`json
71
+ {
72
+ "siteUrl": "https://example.com",
73
+ "url": "https://example.com/jobs/software-engineer",
74
+ "engine": "google"
75
+ }
76
+ \`\`\`
77
+
78
+ ### Check submission status
79
+ \`\`\`json
80
+ {
81
+ "siteUrl": "https://example.com",
82
+ "url": "https://example.com/jobs/software-engineer"
83
+ }
84
+ \`\`\`
85
+
86
+ ### Batch submit
87
+ \`\`\`json
88
+ {
89
+ "siteUrl": "https://example.com",
90
+ "urls": [
91
+ "https://example.com/jobs/role-1",
92
+ "https://example.com/jobs/role-2"
93
+ ],
94
+ "engine": "google"
95
+ }
96
+ \`\`\`
97
+ `;
98
+ export default indexingDocs;
@@ -0,0 +1,93 @@
1
+ import { getIndexingClient } from '../client.js';
2
+ import { limitConcurrency } from '../../common/concurrency.js';
3
+ const INDEXING_API_BASE = 'https://indexing.googleapis.com/v3/urlNotifications';
4
+ /**
5
+ * Publish a URL notification to Google's Indexing API.
6
+ *
7
+ * @param siteUrl - The site URL for account resolution.
8
+ * @param url - The URL to notify about.
9
+ * @param type - The notification type: URL_UPDATED or URL_DELETED.
10
+ * @returns The publish notification result.
11
+ */
12
+ export async function publishNotification(siteUrl, url, type) {
13
+ const auth = await getIndexingClient(siteUrl);
14
+ const accessToken = await auth.getAccessToken();
15
+ const token = typeof accessToken === 'string' ? accessToken : accessToken.token;
16
+ const response = await fetch(`${INDEXING_API_BASE}:publish`, {
17
+ method: 'POST',
18
+ headers: {
19
+ 'Content-Type': 'application/json',
20
+ 'Authorization': `Bearer ${token}`
21
+ },
22
+ body: JSON.stringify({ url, type })
23
+ });
24
+ if (!response.ok) {
25
+ const errorBody = await response.text();
26
+ throw new Error(`Google Indexing API error: ${response.status} ${errorBody}`);
27
+ }
28
+ const data = await response.json();
29
+ return {
30
+ url: data.urlNotificationMetadata?.url || url,
31
+ type,
32
+ notifyTime: data.urlNotificationMetadata?.latestUpdate?.notifyTime ||
33
+ data.urlNotificationMetadata?.latestRemove?.notifyTime ||
34
+ new Date().toISOString()
35
+ };
36
+ }
37
+ /**
38
+ * Get the notification status for a URL from Google's Indexing API.
39
+ *
40
+ * @param siteUrl - The site URL for account resolution.
41
+ * @param url - The URL to check the status of.
42
+ * @returns The notification status result.
43
+ */
44
+ export async function getNotificationStatus(siteUrl, url) {
45
+ const auth = await getIndexingClient(siteUrl);
46
+ const accessToken = await auth.getAccessToken();
47
+ const token = typeof accessToken === 'string' ? accessToken : accessToken.token;
48
+ const requestUrl = new URL(`${INDEXING_API_BASE}/metadata`);
49
+ requestUrl.searchParams.set('url', url);
50
+ const response = await fetch(requestUrl.toString(), {
51
+ method: 'GET',
52
+ headers: {
53
+ 'Authorization': `Bearer ${token}`
54
+ }
55
+ });
56
+ if (!response.ok) {
57
+ const errorBody = await response.text();
58
+ throw new Error(`Google Indexing API error: ${response.status} ${errorBody}`);
59
+ }
60
+ const data = await response.json();
61
+ return {
62
+ url: data.url || url,
63
+ latestUpdate: data.latestUpdate || undefined,
64
+ latestRemove: data.latestRemove || undefined
65
+ };
66
+ }
67
+ /**
68
+ * Publish URL notifications in batch using concurrency control.
69
+ *
70
+ * Google Indexing API does not have a native batch endpoint for v3,
71
+ * so we use concurrent individual requests with rate limiting.
72
+ *
73
+ * @param siteUrl - The site URL for account resolution.
74
+ * @param urls - The list of URLs to notify about.
75
+ * @param type - The notification type for all URLs.
76
+ * @returns An array of results for each URL.
77
+ */
78
+ export async function batchPublishNotifications(siteUrl, urls, type) {
79
+ if (urls.length === 0)
80
+ return [];
81
+ if (urls.length > 200) {
82
+ throw new Error('Batch limited to 200 URLs (Google Indexing API daily quota). Please submit in smaller batches.');
83
+ }
84
+ return limitConcurrency(urls, 5, async (url) => {
85
+ try {
86
+ const result = await publishNotification(siteUrl, url, type);
87
+ return { url, result };
88
+ }
89
+ catch (error) {
90
+ return { url, error: error.message };
91
+ }
92
+ });
93
+ }
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import * as sites from "./google/tools/sites.js";
7
7
  import * as sitemaps from "./google/tools/sitemaps.js";
8
8
  import * as analytics from "./google/tools/analytics.js";
9
9
  import * as inspection from "./google/tools/inspection.js";
10
+ import * as googleIndexing from "./google/tools/indexing.js";
10
11
  import * as pagespeed from "./google/tools/pagespeed.js";
11
12
  import * as seoInsights from "./google/tools/seo-insights.js";
12
13
  import * as seoPrimitives from "./common/tools/seo-primitives.js";
@@ -1205,6 +1206,69 @@ server.tool("bing_index_now", "Submit URLs via IndexNow API (Bing, Yandex, etc.)
1205
1206
  return formatError(error);
1206
1207
  }
1207
1208
  });
1209
+ // --- Indexing API Tools ---
1210
+ server.tool("indexing_submit_url", "Submit a URL for indexing (notify Google or Bing that a page was updated). Google Indexing API is officially for JobPosting/BroadcastEvent pages.", {
1211
+ siteUrl: z.string().describe("The property URL as registered in Search Console"),
1212
+ url: z.string().describe("The specific URL to submit for indexing"),
1213
+ engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
1214
+ }, async ({ siteUrl, url, engine = "google" }) => {
1215
+ try {
1216
+ const result = engine === "google"
1217
+ ? await googleIndexing.publishNotification(siteUrl, url, 'URL_UPDATED')
1218
+ : await bingUrlSubmission.submitUrl(siteUrl, url);
1219
+ return {
1220
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1221
+ };
1222
+ }
1223
+ catch (error) {
1224
+ return formatError(error);
1225
+ }
1226
+ });
1227
+ server.tool("indexing_remove_url", "Notify Google that a URL has been removed (e.g., expired job posting). Google only.", {
1228
+ siteUrl: z.string().describe("The property URL as registered in Search Console"),
1229
+ url: z.string().describe("The URL that was removed")
1230
+ }, async ({ siteUrl, url }) => {
1231
+ try {
1232
+ const result = await googleIndexing.publishNotification(siteUrl, url, 'URL_DELETED');
1233
+ return {
1234
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1235
+ };
1236
+ }
1237
+ catch (error) {
1238
+ return formatError(error);
1239
+ }
1240
+ });
1241
+ server.tool("indexing_status", "Check the notification status for a URL previously submitted to the Google Indexing API", {
1242
+ siteUrl: z.string().describe("The property URL as registered in Search Console"),
1243
+ url: z.string().describe("The URL to check notification status for")
1244
+ }, async ({ siteUrl, url }) => {
1245
+ try {
1246
+ const result = await googleIndexing.getNotificationStatus(siteUrl, url);
1247
+ return {
1248
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1249
+ };
1250
+ }
1251
+ catch (error) {
1252
+ return formatError(error);
1253
+ }
1254
+ });
1255
+ server.tool("indexing_batch_submit", "Submit multiple URLs for indexing in batch. Google: max 200 (daily quota), Bing: max 500.", {
1256
+ siteUrl: z.string().describe("The property URL as registered in Search Console"),
1257
+ urls: z.array(z.string()).describe("List of URLs to submit for indexing"),
1258
+ engine: z.enum(["google", "bing"]).optional().describe("The search engine (default: google)")
1259
+ }, async ({ siteUrl, urls, engine = "google" }) => {
1260
+ try {
1261
+ const result = engine === "google"
1262
+ ? await googleIndexing.batchPublishNotifications(siteUrl, urls, 'URL_UPDATED')
1263
+ : await bingUrlSubmission.submitUrlBatch(siteUrl, urls);
1264
+ return {
1265
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1266
+ };
1267
+ }
1268
+ catch (error) {
1269
+ return formatError(error);
1270
+ }
1271
+ });
1208
1272
  server.tool("bing_sites_health", "Run a comprehensive health check on one or all verified Bing sites", {
1209
1273
  siteUrl: z.string().optional().describe("Optional URL of a specific site to check")
1210
1274
  }, async ({ siteUrl }) => {
@@ -1656,7 +1720,7 @@ server.resource("analytics-summary", "analytics://summary/{siteUrl}", async (uri
1656
1720
  };
1657
1721
  });
1658
1722
  // Documentation Resources
1659
- import { dimensionsDocs, filtersDocs, searchTypesDocs, patternsDocs, algorithmUpdatesDocs } from "./google/docs/index.js";
1723
+ import { dimensionsDocs, filtersDocs, searchTypesDocs, patternsDocs, algorithmUpdatesDocs, indexingDocs } from "./google/docs/index.js";
1660
1724
  server.resource("docs-dimensions", "docs://dimensions", async (uri) => ({
1661
1725
  contents: [{
1662
1726
  uri: uri.href,
@@ -1741,6 +1805,13 @@ server.resource("docs-bing-algorithm-updates", "docs://bing/algorithm-updates",
1741
1805
  mimeType: "text/markdown"
1742
1806
  }]
1743
1807
  }));
1808
+ server.resource("docs-indexing", "docs://indexing", async (uri) => ({
1809
+ contents: [{
1810
+ uri: uri.href,
1811
+ text: indexingDocs,
1812
+ mimeType: "text/markdown"
1813
+ }]
1814
+ }));
1744
1815
  // Prompts
1745
1816
  server.prompt("analyze-site-performance", {
1746
1817
  siteUrl: z.string().describe("The URL of the site to analyze"),
package/dist/setup.js CHANGED
@@ -204,8 +204,24 @@ export async function login() {
204
204
  // Use centralized defaults
205
205
  const clientId = DEFAULT_CLIENT_ID;
206
206
  const clientSecret = DEFAULT_CLIENT_SECRET;
207
+ console.log(`\n${colors.bold}💡 Google Indexing API Rules:${colors.reset}`);
208
+ console.log(` Officially, the Google Indexing API is only supported for pages containing`);
209
+ console.log(` ${colors.cyan}JobPosting${colors.reset} or ${colors.cyan}BroadcastEvent${colors.reset} structured data. Using it for other content`);
210
+ console.log(` types may result in submissions being ignored by Google.`);
211
+ const authorizeIndexing = await ask('\nWould you like to also authorize Google Indexing API write scope? (y/N): ');
212
+ const useIndexing = authorizeIndexing.toLowerCase().startsWith('y');
213
+ const scopes = useIndexing
214
+ ? [
215
+ 'https://www.googleapis.com/auth/webmasters.readonly',
216
+ 'https://www.googleapis.com/auth/indexing',
217
+ 'https://www.googleapis.com/auth/userinfo.email'
218
+ ]
219
+ : [
220
+ 'https://www.googleapis.com/auth/webmasters.readonly',
221
+ 'https://www.googleapis.com/auth/userinfo.email'
222
+ ];
207
223
  try {
208
- const tokens = await startLocalFlow(clientId, clientSecret);
224
+ const tokens = await startLocalFlow(clientId, clientSecret, scopes);
209
225
  printInfo('Fetching account information...');
210
226
  const email = await getUserEmail(tokens);
211
227
  console.log(`\nAuthorized as: ${colors.bold}${email}${colors.reset}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "search-console-mcp",
3
- "version": "1.13.5",
3
+ "version": "1.14.0",
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",
@@ -13,13 +13,6 @@
13
13
  "README.md",
14
14
  "LICENSE"
15
15
  ],
16
- "scripts": {
17
- "build": "tsc",
18
- "test": "vitest run",
19
- "docs": "typedoc",
20
- "deploy:registry": "mcp-publisher publish",
21
- "prepublishOnly": "npm run build"
22
- },
23
16
  "keywords": [
24
17
  "mcp",
25
18
  "bing-search",
@@ -33,23 +26,27 @@
33
26
  "license": "MIT",
34
27
  "dependencies": {
35
28
  "@adobe/structured-data-validator": "^1.7.0",
36
- "@google-analytics/data": "^5.2.1",
37
- "@modelcontextprotocol/sdk": "^1.26.0",
38
- "@napi-rs/keyring": "^1.2.0",
29
+ "@google-analytics/data": "^6.1.0",
30
+ "@modelcontextprotocol/sdk": "^1.29.0",
31
+ "@napi-rs/keyring": "^1.3.0",
39
32
  "cheerio": "^1.2.0",
40
- "dotenv": "^17.3.1",
41
- "googleapis": "^171.4.0",
33
+ "dotenv": "^17.4.2",
34
+ "googleapis": "^173.0.0",
42
35
  "node-machine-id": "^1.1.12",
43
36
  "open": "^11.0.0",
44
- "re2": "^1.23.3",
45
- "zod": "^4.3.6"
37
+ "zod": "^4.4.3"
46
38
  },
47
39
  "devDependencies": {
48
- "@types/node": "^25.2.0",
49
- "@types/re2": "^1.10.0",
50
- "@vitest/coverage-v8": "^4.0.18",
51
- "typedoc": "^0.28.16",
52
- "typescript": "^5.9.3",
53
- "vitest": "^4.0.18"
40
+ "@types/node": "^25.9.2",
41
+ "@vitest/coverage-v8": "^4.1.8",
42
+ "typedoc": "^0.28.19",
43
+ "typescript": "^6.0.3",
44
+ "vitest": "^4.1.8"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc",
48
+ "test": "vitest run",
49
+ "docs": "typedoc",
50
+ "deploy:registry": "mcp-publisher publish"
54
51
  }
55
52
  }