search-console-mcp 1.13.5 โ†’ 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,261 +1,176 @@
1
+ <div align="center">
1
2
 
2
- # Search Console MCP
3
+ # ๐Ÿ” Search Console MCP
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
+ **Google Search Console + Bing Webmaster Tools + GA4 โ€” in one context window.**
5
6
 
6
- [๐Ÿ“š View Documentation](https://searchconsolemcp.saurabh.app/)
7
-
8
- ---
7
+ Stop exporting CSVs. Start asking your AI agent questions.
9
8
 
10
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
10
  [![Tests](https://github.com/saurabhsharma2u/search-console-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/saurabhsharma2u/search-console-mcp/actions/workflows/ci.yml)
11
+ [![Stars](https://img.shields.io/github/stars/saurabhsharma2u/search-console-mcp?style=social)](https://github.com/saurabhsharma2u/search-console-mcp/stargazers)
12
12
 
13
- ## Why use this?
14
-
15
- ### โŒ The Old, Broken Way
16
- * **Data Silos**: Manually checking Google, then Bing, then GA4.
17
- * **Manual Correlation**: Exporting 3 different CSVs and using VLOOKUPs to see if your #1 ranking page actually converts.
18
- * **Switching Fatigue**: Logging in and out to manage multiple clients or properties.
19
- * **AI Context Limits**: Uploading giant spreadsheets that hit context limits and cause model hallucinations.
13
+ [๐Ÿ“š Docs](https://searchconsolemcp.saurabh.app/) ยท [Quick Start](#-quick-start) ยท [Tools](#-tools) ยท [Security](#-security)
20
14
 
21
- ### โœ… The Search Console MCP Way
22
- * **Platform Synergy**: **GSC + Bing + GA4** in a single context window. Stop exporting, start analyzing.
23
- * **Deterministic Intelligence**: The server performs the complex SEO math (cannibalization, Z-score anomalies, striking distance) so your AI agent gets curated insights, not raw data piles.
24
- * **Cross-Platform ROI**: Use the **Opportunity Matrix** to prioritize keywords that have high search visibility (GSC) but poor on-site engagement (GA4).
25
- * **Zero-Config Multi-Account**: Connect 20+ accounts. The server automatically resolves the correct credentials for every site URL.
26
-
27
- **One Server. Three Platforms. Infinite Accounts. Actionable Intelligence.**
15
+ </div>
28
16
 
29
17
  ---
30
- ## ๐ŸŽฏ Magic Prompts
31
-
32
- Copy and paste these into your MCP client (Claude Desktop, etc.) to see the intelligence engine in action:
33
-
34
- #### ๐Ÿ” The Traffic Detective
35
- > "My traffic dropped this week compared to last. Use the anomaly detection and time-series tools to find exactly when the drop started and which pages are responsible."
36
-
37
- #### ๐ŸŽฏ The "Striking Distance" Hunter
38
- > "Find keywords for https://example.com where I'm ranking in positions 8-15 but have at least 1,000 impressions. These are my best opportunities for a quick traffic boost."
39
-
40
- #### โš”๏ธ The Cannibalization Cleaner
41
- > "Check for keyword cannibalization. Are there any queries where two or more of my pages are competing and splitting the traffic? Suggest which one should be the primary authority."
42
-
43
- #### ๐Ÿ“ˆ The SEO Opportunity Scoreboard
44
- > "Analyze my top 50 keywords for the last 90 days. Rank them by a custom 'Opportunity Score' (Impressions / Position). Give me the top 5 specific pages to focus on."
45
18
 
46
- #### ๐Ÿ“Š The Executive Health Check
47
- > "Run a full SEO health check for my site. Segment the results by Brand vs. Non-Brand and give me 3 high-impact actions for the upcoming week."
19
+ ## Why this exists
48
20
 
49
- #### โšก The Speed vs. Ranking Correlator
50
- > "Fetch the top 5 pages by impressions. For these pages, run a PageSpeed audit. Is there any correlation between low performance scores and recently declining positions?"
21
+ SEO data lives in three different silos. Answering one question โ€” *"did my traffic drop because of a ranking loss or a UX issue?"* โ€” usually means logging into three dashboards, exporting three CSVs, and doing VLOOKUPs by hand.
51
22
 
52
- #### ๐Ÿ”€ Multi-Engine Comparison
53
- > "Compare my performance between Google and Bing for the last 30 days. Which keywords are ranking better on Bing but have lower traffic on Google?"
23
+ Search Console MCP puts **GSC, Bing, and GA4** behind one set of tools your AI agent can call directly, and does the SEO math (cannibalization, anomaly detection, opportunity scoring) *before* the data ever reaches your context window โ€” so your agent gets insights, not spreadsheets.
54
24
 
55
- #### ๐ŸŽฏ Bing Opportunity Finder
56
- > "Show me keywords where I'm in the top 5 on Google but not ranking on Bing. These are my easy Bing wins."
57
-
58
- #### โš ๏ธ Google Dependency Check
59
- > "Am I too dependent on Google? Check my click share across both engines and flag any keywords where over 85% of traffic comes from Google."
60
-
61
- #### ๐Ÿ’ฐ The ROI Prioritizer (GSC + GA4)
62
- > "Run an `opportunity_matrix` for my top 20 pages. Which high-visibility pages have the lowest engagement or conversion rates? These are my conversion optimization priorities."
25
+ | | Before | After |
26
+ |---|---|---|
27
+ | **Data** | 3 dashboards, manual exports | 1 unified context |
28
+ | **Analysis** | Manual VLOOKUPs & pivot tables | Deterministic SEO math, done server-side |
29
+ | **Accounts** | Constant re-login | 20+ accounts, auto-resolved per site |
30
+ | **Insight** | Raw rows, agent guesses | Curated signals (opportunity scores, anomalies) |
63
31
 
64
32
  ---
65
33
 
66
- ## ๐Ÿ” Authentication (Desktop Flow)
67
-
68
- Search Console MCP uses a **Secure Desktop Flow**. This provides high-security, professional grade authentication for your Google account:
69
- - **Multi-Account Support**: Connect multiple Google and Bing accounts. The server automatically picks the right one for each site.
70
- - **System Keychain Primary**: Tokens are stored in your OS's native credential manager (macOS Keychain, Windows Credential Manager, or Linux Secret Service).
71
- - **AES-256-GCM Hardware-Bound Encryption**: Fallback storage is encrypted with AES-256-GCM using a key derived from your unique hardware machine ID. Tokens stolen from your machine cannot be decrypted on another computer.
72
- - **Silent Background Refresh**: Tokens auto-refresh silently when they expire.
34
+ ## โšก Quick Start
73
35
 
74
- ### ๐Ÿš€ Step 1 โ€” Initiate Login
75
- Run the following command to start the authorization process:
76
36
  ```bash
77
37
  npx search-console-mcp setup
78
38
  ```
79
39
 
80
- The CLI will:
81
- 1. Briefly start a secure local server to handle the redirect.
82
- 2. Open your default web browser to the Google Authorization page.
83
- 3. Automatically fetch your email after authorization to label your credentials securely.
84
-
85
- ### ๐Ÿ”‘ Step 2 โ€” Logout & Management
86
- To wipe your credentials from both the keychain and the disk:
87
- ```bash
88
- # Logout of the default account
89
- npx search-console-mcp logout
90
-
91
- # Logout of a specific account
92
- npx search-console-mcp logout user@gmail.com
40
+ This opens your browser, authorizes your Google account, and stores your credentials securely (see [Security](#-security)). Then add it to your MCP client config (Claude Desktop, Cursor, etc.):
41
+
42
+ ```json
43
+ {
44
+ "mcpServers": {
45
+ "search-console": {
46
+ "command": "npx",
47
+ "args": ["search-console-mcp"]
48
+ }
49
+ }
50
+ }
93
51
  ```
94
52
 
95
- ---
96
-
97
- ## ๐Ÿ”‘ Alternative: Service Account (Advanced)
53
+ Restart your client โ€” and try one of the prompts below.
98
54
 
99
- For server-side environments or automated tasks where interactive login isn't possible, you can use a Google Cloud Service Account.
55
+ ---
100
56
 
101
- ### Setup:
102
- 1. **Create Service Account**: Go to the [Google Cloud Console](https://console.cloud.google.com/iam-admin/serviceaccounts) and create a service account.
103
- 2. **Generate Key**: Click "Keys" > "Add Key" > "Create new key" (JSON). Download this file.
104
- 3. **Share Access**: In Google Search Console, add the service account's email address (e.g., `account@project.iam.gserviceaccount.com`) as a user with at least "Full" or "Restricted" permissions.
105
- 4. **Configure**: Point the server to your key file:
106
- ```bash
107
- export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/key.json"
108
- ```
57
+ ## ๐Ÿ’ฌ Try it
109
58
 
110
- ---
59
+ Paste these straight into your agent:
111
60
 
112
- ## ๐Ÿ”‘ Bing Webmaster Tools (API Key)
61
+ > **"My traffic dropped this week vs. last. Find exactly when it started and which pages are responsible."**
113
62
 
114
- To access Bing data, you simply need an API Key.
63
+ > **"Find keywords for example.com ranking positions 8โ€“15 with 1,000+ impressions โ€” my best quick wins."**
115
64
 
116
- ### Setup:
117
- 1. **Get Your API Key**: Go to [Bing Webmaster Tools Settings](https://www.bing.com/webmasters/settings/api).
118
- 2. **Configure**: Set the API key in your environment:
119
- ```bash
120
- export BING_API_KEY="your-api-key-here"
121
- ```
122
- 3. **IndexNow**: Bing tools also support **IndexNow** for instant URL submission.
65
+ > **"Check for keyword cannibalization โ€” are two of my pages competing for the same query?"**
123
66
 
124
- ---
67
+ > **"Run `opportunity_matrix` on my top 20 pages: which have high search visibility but poor on-site engagement?"**
125
68
 
126
- ## ๐Ÿ“Š Google Analytics 4 (GA4)
69
+ <details>
70
+ <summary>More example prompts</summary>
127
71
 
128
- Connect your GA4 properties to correlate ranking data with user behavior.
72
+ - *"Run a full SEO health check, segmented by Brand vs Non-Brand, with 3 high-impact actions."*
73
+ - *"Fetch my top 5 pages by impressions and run a PageSpeed audit โ€” any correlation with declining rankings?"*
74
+ - *"Compare Google vs Bing performance for the last 30 days โ€” where is Bing winning?"*
75
+ - *"Am I too dependent on Google? Flag keywords where 85%+ of clicks come from one engine."*
129
76
 
130
- ### Setup:
131
- 1. **Run Setup**: `npx search-console-mcp setup --engine=ga4`
132
- 2. **Auth Method**: Choose **Service Account** (JSON Key). This is the recommended method for server-side integrations.
133
- * **Service Account**: You must add the service account email as a user in **GA4 Admin > Property Settings > Property Access Management**.
134
- 3. **Property Selection**: The tool will automatically fetch all available GA4 properties for your account and let you select one from a list. You can also enter a Property ID manually if needed.
77
+ </details>
135
78
 
136
79
  ---
137
80
 
138
- ## ๐Ÿ‘ฅ Multi-Account Management
81
+ ## ๐Ÿ”Œ Connect your accounts
82
+
83
+ | Platform | Method | Setup |
84
+ |---|---|---|
85
+ | **Google Search Console** | OAuth (recommended) | `npx search-console-mcp setup` |
86
+ | **Google Search Console** | Service Account | Set `GOOGLE_APPLICATION_CREDENTIALS` โ€” [details](#service-account-advanced) |
87
+ | **Bing Webmaster Tools** | API Key | `export BING_API_KEY="..."` โ€” [get a key](https://www.bing.com/webmasters/settings/api) |
88
+ | **Google Analytics 4** | Service Account | `npx search-console-mcp setup --engine=ga4` |
139
89
 
140
- Manage multiple Google and Bing accounts from the CLI:
90
+ Manage everything from the CLI:
141
91
 
142
92
  ```bash
143
- # List all connected accounts
144
93
  npx search-console-mcp accounts list
94
+ npx search-console-mcp accounts add-site --account=you@company.com --site=example.com
95
+ npx search-console-mcp accounts remove --account=you@company.com
96
+ ```
145
97
 
146
- # Remove an account
147
- npx search-console-mcp accounts remove --account=marketing@company.com
98
+ When your agent queries a site, the server auto-resolves which account owns it โ€” no manual switching. [Multi-account docs โ†’](https://searchconsolemcp.mintlify.app/getting-started/multi-account)
148
99
 
149
- # Add a site boundary to an account
150
- npx search-console-mcp accounts add-site --account=marketing@company.com --site=example.com
151
- ```
100
+ <details>
101
+ <summary id="service-account-advanced">Service Account setup (for servers/automation)</summary>
152
102
 
153
- When your AI agent queries a site, the server automatically resolves which account to use. [Learn more โ†’](https://searchconsolemcp.mintlify.app/getting-started/multi-account)
103
+ 1. Create a service account in the [Google Cloud Console](https://console.cloud.google.com/iam-admin/serviceaccounts)
104
+ 2. Generate a JSON key
105
+ 3. Add the service account email as a user in Search Console with "Full" or "Restricted" access
106
+ 4. `export GOOGLE_APPLICATION_CREDENTIALS="/path/to/key.json"`
107
+
108
+ </details>
154
109
 
155
110
  ---
156
111
 
112
+ ## ๐Ÿ›  Tools
157
113
 
158
- ## ๐Ÿ›ก๏ธ Fort Knox Security
114
+ 40+ tools across five categories. A few flagship ones:
159
115
 
160
- This MCP server implements a multi-layered security architecture:
116
+ | Tool | What it does |
117
+ |---|---|
118
+ | `opportunity_matrix` | Ranks pages by combining GSC visibility with GA4 engagement โ€” where's the ROI? |
119
+ | `seo_striking_distance` | Keywords ranking 8โ€“15 โ€” your fastest wins |
120
+ | `seo_cannibalization` | Pages competing for the same query, with a recommended primary |
121
+ | `analytics_anomalies` | Statistical traffic drop/spike detection |
122
+ | `sites_health_check` | One-shot WoW performance + sitemap + anomaly check |
123
+ | `compare_engines` | Google vs Bing performance, side by side |
161
124
 
162
- * **Keychain Integration**: Primarily uses the **macOS Keychain**, **Windows Credential Manager**, or **libsecret (Linux)** to store tokens.
163
- * **Encrypted Config**: Account configuration is stored in `~/.search-console-mcp-config.enc` using **AES-256-GCM** encryption.
164
- * **Machine Fingerprinting**: The encryption key is derived from your unique hardware UUID and OS user. The encrypted file is useless if moved to another machine.
165
- * **Minimalist Storage**: Only the `refresh_token` and `expiry_date` are stored.
166
- * **Legacy Support**: Automatically detects credentials from older versions (tokens files, environment variables).
167
- * **Strict Unix Permissions**: Config files are created with `mode 600` (read/write only by your user).
125
+ <details>
126
+ <summary><strong>Full tool reference (40+ tools)</strong></summary>
168
127
 
169
- ---
128
+ ### Analytics & Trends
129
+ `analytics_query` ยท `analytics_trends` ยท `analytics_anomalies` ยท `analytics_drop_attribution` ยท `analytics_time_series` ยท `analytics_compare_periods` ยท `seo_brand_vs_nonbrand`
170
130
 
171
- ## Tools Reference
172
-
173
- ### Google Analytics
174
- | Tool | Description |
175
- |------|-------------|
176
- | `analytics_query` | Master tool for raw data. Supports `dimensions`, `filters`, `aggregationType` (byPage/byProperty), `dataState` (final/all), and `type` (web/image/news/discover). |
177
- | `analytics_trends` | Detect trends (rising/falling) for specific queries or pages. |
178
- | `analytics_anomalies` | Detect statistical anomalies in daily traffic. |
179
- | `analytics_drop_attribution` | **[NEW]** Attribute traffic drops to mobile/desktop or correlate with known Google Algorithm Updates. |
180
- | `analytics_time_series` | **[NEW]** Advanced time series with rolling averages, seasonality detection, and forecasting. |
181
- | `analytics_compare_periods` | Compare two date ranges (e.g., WoW, MoM). |
182
- | `seo_brand_vs_nonbrand` | **[NEW]** Analyze performance split between Brand vs Non-Brand traffic. (Supports Google & Bing). |
183
-
184
- ### SEO Opportunities (Opinionated)
185
- | Tool | Description |
186
- |------|-------------|
187
- | `seo_low_hanging_fruit` | Find keywords ranking in pos 5-20 with high impressions. |
188
- | `seo_striking_distance` | **[NEW]** Find keywords ranking 8-15 (Quickest ROI wins). |
189
- | `seo_low_ctr_opportunities` | **[NEW]** Find top ranking queries (pos 1-10) with poor CTR. |
190
- | `seo_cannibalization` | **[Enhanced]** Detect pages competing for the same query with traffic conflict. |
191
- | `seo_lost_queries` | **[NEW]** Identify queries that lost all traffic in the last 28 days. (Supports Google & Bing). |
192
-
193
- ### SEO Primitives (Atoms for Agents)
194
- These are low-level tools designed to be used by other AI agents to build complex logic.
195
- | Tool | Description |
196
- |------|-------------|
197
- | `seo_primitive_ranking_bucket` | Categorize a position (e.g. "Top 3", "Page 1", "Unranked"). |
198
- | `seo_primitive_traffic_delta` | Calculate absolute and % change between two numbers. |
199
- | `seo_primitive_is_brand` | Check if a query matches a brand regex. |
200
- | `seo_primitive_is_cannibalized` | Check if two pages are competing for the same query. |
131
+ ### SEO Opportunities
132
+ `seo_low_hanging_fruit` ยท `seo_striking_distance` ยท `seo_low_ctr_opportunities` ยท `seo_cannibalization` ยท `seo_lost_queries`
133
+
134
+ ### SEO Primitives (building blocks for agent logic)
135
+ `seo_primitive_ranking_bucket` ยท `seo_primitive_traffic_delta` ยท `seo_primitive_is_brand` ยท `seo_primitive_is_cannibalized`
201
136
 
202
137
  ### Sites & Sitemaps
203
- | Tool | Description |
204
- |------|-------------|
205
- | `sites_list` | List all verified sites. |
206
- | `sites_add` / `sites_delete` | Manage properties. |
207
- | `sites_health_check` | **[NEW]** Run a health check on one or all sites. Checks WoW performance, sitemaps, and anomalies. |
208
- | `sitemaps_list` / `sitemaps_submit` | Manage sitemaps. |
138
+ `sites_list` ยท `sites_add` ยท `sites_delete` ยท `sites_health_check` ยท `sitemaps_list` ยท `sitemaps_submit`
209
139
 
210
140
  ### Inspection & Validation
211
- | Tool | Description |
212
- |------|-------------|
213
- | `inspection_inspect` | Google URL Inspection API (Index status, mobile usability). |
214
- | `pagespeed_analyze` | Lighthouse scores & Core Web Vitals. |
215
- | `schema_validate` | Validate Structured Data (JSON-LD). |
141
+ `inspection_inspect` ยท `pagespeed_analyze` ยท `schema_validate`
142
+
143
+ ### URL Indexing
144
+ `indexing_submit_url` ยท `indexing_remove_url` ยท `indexing_status` ยท `indexing_batch_submit` ยท `bing_url_submission_quota`
216
145
 
217
146
  ### Bing Webmaster Tools
218
- | Tool | Description |
219
- |------|-------------|
220
- | `bing_sites_list` | List all verified sites in Bing. |
221
- | `bing_analytics_query` | Query search performance from Bing. |
222
- | `bing_opportunity_finder` | Find low-hanging fruit keywords on Bing. |
223
- | `bing_seo_recommendations` | Get prioritized SEO insights for Bing. |
224
- | `bing_url_info` | Detailed indexing and crawl info for a URL (Bing). |
225
- | `bing_index_now` | **[NEW]** Instantly notify search engines of changes. |
226
- | `bing_crawl_issues` | List crawl issues detected by Bing. |
227
- | `bing_analytics_detect_anomalies` | Detect daily spikes or drops in Bing traffic. |
228
- | `bing_analytics_time_series` | Advanced time series analysis for Bing. |
229
- | `bing_seo_lost_queries` | Identify queries that lost significant traffic on Bing. |
230
- | `bing_brand_analysis` | Analyze performance split between Brand vs Non-Brand traffic on Bing. |
231
- | `bing_sitemaps_list` / `bing_sitemaps_submit` | Manage sitemaps in Bing. |
232
-
233
- ### Google Analytics 4 (GA4)
234
- | Tool | Description |
235
- |------|-------------|
236
- | `analytics_page_performance` | Detailed page metrics (sessions, engagement, views). |
237
- | `analytics_traffic_sources` | Analyze sessions by Channel, Source, and Medium. |
238
- | `analytics_organic_landing_pages` | Focused metrics for organic traffic landing pages. |
239
- | `analytics_content_performance` | Analyze content performance by Content Group in GA4. |
240
- | `analytics_conversion_funnel` | Top converting pages and events. |
241
- | `analytics_user_behavior` | Device, Country, and Engagement breakdown. |
242
- | `analytics_audience_segments` | New vs Returning, Age, and OS analysis. |
243
- | `analytics_realtime` | Live active user data by page and location. |
244
- | `analytics_ecommerce` | Product and revenue performance. |
245
- | `analytics_pagespeed_correlation` | Correlate GA4 metrics with PageSpeed scores. |
147
+ `bing_sites_list` ยท `bing_analytics_query` ยท `bing_opportunity_finder` ยท `bing_seo_recommendations` ยท `bing_url_info` ยท `bing_index_now` ยท `bing_crawl_issues` ยท `bing_analytics_detect_anomalies` ยท `bing_analytics_time_series` ยท `bing_seo_lost_queries` ยท `bing_brand_analysis` ยท `bing_sitemaps_list` ยท `bing_sitemaps_submit`
148
+
149
+ ### Google Analytics 4
150
+ `analytics_page_performance` ยท `analytics_traffic_sources` ยท `analytics_organic_landing_pages` ยท `analytics_content_performance` ยท `analytics_conversion_funnel` ยท `analytics_user_behavior` ยท `analytics_audience_segments` ยท `analytics_realtime` ยท `analytics_ecommerce` ยท `analytics_pagespeed_correlation`
246
151
 
247
152
  ### Cross-Platform Intelligence
248
- | Tool | Description |
249
- |------|-------------|
250
- | `opportunity_matrix` | **[Flagship]** Prioritize SEO tasks by combining signals from GSC, GA4, and Bing. |
251
- | `page_analysis` | Joint analysis of ranking (GSC) vs behavior (GA4) for pages. |
252
- | `traffic_health_check` | Diagnose tracking gaps by comparing GSC clicks to GA4 organic sessions. |
253
- | `brand_analysis` | Brand vs Non-Brand split across GSC, Bing, and GA4. |
254
- | `compare_engines` | Compare keyword performance between Google and Bing. |
153
+ `opportunity_matrix` ยท `page_analysis` ยท `traffic_health_check` ยท `brand_analysis` ยท `compare_engines`
154
+
155
+ </details>
255
156
 
157
+ ---
256
158
 
159
+ ## ๐Ÿ”’ Security
160
+
161
+ - **OS keychain first** โ€” tokens stored in macOS Keychain, Windows Credential Manager, or Linux Secret Service
162
+ - **AES-256-GCM fallback** โ€” encrypted with a key derived from your machine's hardware ID; a stolen file is useless on another device
163
+ - **Minimal storage** โ€” only `refresh_token` and `expiry_date` are persisted, at `mode 600`
164
+ - **Silent refresh** โ€” tokens renew automatically in the background
165
+
166
+ ---
257
167
 
258
168
  ## License
259
169
 
260
- [MIT](LICENSE)
261
- [Contributing](CONTRIBUTING.md)
170
+ [MIT](./LICENSE) ยท [Contributing guide](./CONTRIBUTING.md)
171
+
172
+ <div align="center">
173
+
174
+ If this saves you a spreadsheet, consider โญ starring the repo.
175
+
176
+ </div>
@@ -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);
@@ -8,6 +8,10 @@ The \`analytics_drop_attribution\` tool correlates traffic drops with major know
8
8
  | Date | Update Name | Impact Area |
9
9
  |------|-------------|-------------|
10
10
  | 2026-02-05 | February 2026 Discover Core Update | Discover Feed |
11
+ | 2026-03-24 | March 2026 Spam Update | Spam / Content Quality |
12
+ | 2026-03-27 | March 2026 Core Update | General Ranking |
13
+ | 2026-05-21 | May 2026 Core Update | General Ranking |
14
+ | 2026-06-24 | June 2026 Spam Update | Spam / Content Quality |
11
15
 
12
16
  ## 2025
13
17
 
@@ -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;
@@ -35,6 +35,10 @@ const ALGORITHM_UPDATES = [
35
35
  { date: '2025-12-11', name: 'December 2025 Core Update' },
36
36
  // 2026
37
37
  { date: '2026-02-05', name: 'February 2026 Discover Core Update' },
38
+ { date: '2026-03-24', name: 'March 2026 Spam Update' },
39
+ { date: '2026-03-27', name: 'March 2026 Core Update' },
40
+ { date: '2026-05-21', name: 'May 2026 Core Update' },
41
+ { date: '2026-06-24', name: 'June 2026 Spam Update' },
38
42
  ];
39
43
  /**
40
44
  * Identify the cause of a significant traffic drop by analyzing device distribution and algorithm updates.
@@ -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
+ }
@@ -1,5 +1,12 @@
1
1
  import { google } from 'googleapis';
2
2
  const pagespeed = google.pagespeedonline('v5');
3
+ /**
4
+ * Returns the PageSpeed API key from environment, if configured.
5
+ * When set, this increases the daily quota from ~100 to 25,000 queries/day.
6
+ */
7
+ function getApiKey() {
8
+ return process.env.PAGESPEED_API_KEY || undefined;
9
+ }
3
10
  /**
4
11
  * Run a full PageSpeed Insights analysis on a URL.
5
12
  *
@@ -8,11 +15,25 @@ const pagespeed = google.pagespeedonline('v5');
8
15
  * @returns Performance, accessibility, and SEO scores along with field/lab metrics.
9
16
  */
10
17
  export async function analyzePageSpeed(url, strategy = 'mobile') {
11
- const res = await pagespeed.pagespeedapi.runpagespeed({
12
- url,
13
- strategy,
14
- category: ['performance', 'accessibility', 'best-practices', 'seo']
15
- });
18
+ const key = getApiKey();
19
+ let res;
20
+ try {
21
+ res = await pagespeed.pagespeedapi.runpagespeed({
22
+ url,
23
+ strategy,
24
+ category: ['performance', 'accessibility', 'best-practices', 'seo'],
25
+ ...(key && { key })
26
+ });
27
+ }
28
+ catch (error) {
29
+ const status = error?.code || error?.response?.status;
30
+ if ((status === 429 || status === 500) && !key) {
31
+ throw new Error(`PageSpeed Insights rate limit exceeded. You are using the free tier which is limited to ~100 queries/day. ` +
32
+ `To increase your quota to 25,000/day, set the PAGESPEED_API_KEY environment variable. ` +
33
+ `Get a free key at: https://console.cloud.google.com/apis/credentials`);
34
+ }
35
+ throw error;
36
+ }
16
37
  const data = res.data;
17
38
  const lighthouse = data.lighthouseResult;
18
39
  const categories = lighthouse?.categories;
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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import 'dotenv/config';
3
- import { readFileSync, existsSync, statSync } from 'fs';
3
+ import { readFileSync, existsSync, statSync, writeFileSync } from 'fs';
4
4
  import { resolve, dirname, extname } from 'path';
5
5
  import { createInterface } from 'readline';
6
6
  import { homedir } from 'os';
@@ -93,19 +93,22 @@ async function detectConfig() {
93
93
  googleAccounts: accounts.filter(a => a.engine === 'google'),
94
94
  bingAccounts: accounts.filter(a => a.engine === 'bing'),
95
95
  ga4Accounts: accounts.filter(a => a.engine === 'ga4'),
96
- legacyBing: !!process.env.BING_API_KEY
96
+ legacyBing: !!process.env.BING_API_KEY,
97
+ pagespeedApiKey: !!process.env.PAGESPEED_API_KEY
97
98
  };
98
99
  }
99
100
  function printDetectionSummary(results) {
100
101
  const gCount = results.googleAccounts ? results.googleAccounts.length : 0;
101
102
  const bCount = (results.bingAccounts ? results.bingAccounts.length : 0) + (results.legacyBing ? 1 : 0);
102
103
  const ga4Count = results.ga4Accounts ? results.ga4Accounts.length : 0;
103
- if (gCount === 0 && bCount === 0 && ga4Count === 0)
104
+ const hasPageSpeed = !!results.pagespeedApiKey;
105
+ if (gCount === 0 && bCount === 0 && ga4Count === 0 && !hasPageSpeed)
104
106
  return;
105
107
  console.log(`${colors.bold}${colors.dim}๐Ÿ” Connection Status${colors.reset}\n`);
106
108
  printStatusLine('Google Search Console', gCount > 0);
107
109
  printStatusLine('Google Analytics 4', ga4Count > 0);
108
110
  printStatusLine('Bing Webmaster Tools', bCount > 0);
111
+ printStatusLine('PageSpeed Insights (API Key)', hasPageSpeed);
109
112
  console.log('');
110
113
  }
111
114
  export function validateKeyFile(path) {
@@ -204,8 +207,24 @@ export async function login() {
204
207
  // Use centralized defaults
205
208
  const clientId = DEFAULT_CLIENT_ID;
206
209
  const clientSecret = DEFAULT_CLIENT_SECRET;
210
+ console.log(`\n${colors.bold}๐Ÿ’ก Google Indexing API Rules:${colors.reset}`);
211
+ console.log(` Officially, the Google Indexing API is only supported for pages containing`);
212
+ console.log(` ${colors.cyan}JobPosting${colors.reset} or ${colors.cyan}BroadcastEvent${colors.reset} structured data. Using it for other content`);
213
+ console.log(` types may result in submissions being ignored by Google.`);
214
+ const authorizeIndexing = await ask('\nWould you like to also authorize Google Indexing API write scope? (y/N): ');
215
+ const useIndexing = authorizeIndexing.toLowerCase().startsWith('y');
216
+ const scopes = useIndexing
217
+ ? [
218
+ 'https://www.googleapis.com/auth/webmasters.readonly',
219
+ 'https://www.googleapis.com/auth/indexing',
220
+ 'https://www.googleapis.com/auth/userinfo.email'
221
+ ]
222
+ : [
223
+ 'https://www.googleapis.com/auth/webmasters.readonly',
224
+ 'https://www.googleapis.com/auth/userinfo.email'
225
+ ];
207
226
  try {
208
- const tokens = await startLocalFlow(clientId, clientSecret);
227
+ const tokens = await startLocalFlow(clientId, clientSecret, scopes);
209
228
  printInfo('Fetching account information...');
210
229
  const email = await getUserEmail(tokens);
211
230
  console.log(`\nAuthorized as: ${colors.bold}${email}${colors.reset}`);
@@ -516,6 +535,10 @@ export async function main() {
516
535
  await handleGA4Flow(configStatus);
517
536
  return;
518
537
  }
538
+ else if (engineFlag === 'pagespeed') {
539
+ await setupPageSpeed();
540
+ return;
541
+ }
519
542
  while (true) {
520
543
  printHeader();
521
544
  printDetectionSummary(configStatus);
@@ -523,8 +546,9 @@ export async function main() {
523
546
  console.log(`\n1. Google Search Console`);
524
547
  console.log('2. Google Analytics 4');
525
548
  console.log('3. Bing Webmaster Tools');
526
- console.log('4. Exit');
527
- const choice = await ask(`\n${colors.bold}${colors.cyan}Enter your choice (1-4): ${colors.reset}`);
549
+ console.log('4. PageSpeed Insights (Optional API Key)');
550
+ console.log('5. Exit');
551
+ const choice = await ask(`\n${colors.bold}${colors.cyan}Enter your choice (1-5): ${colors.reset}`);
528
552
  switch (choice) {
529
553
  case '1':
530
554
  await handleGoogleFlow(configStatus, true);
@@ -536,6 +560,11 @@ export async function main() {
536
560
  await handleBingFlow(configStatus);
537
561
  break;
538
562
  case '4':
563
+ await setupPageSpeed();
564
+ // Refresh config status after setting key
565
+ Object.assign(configStatus, await detectConfig());
566
+ break;
567
+ case '5':
539
568
  default:
540
569
  console.log(`\n${colors.dim}See you on the flip side!${colors.reset}`);
541
570
  rl.close();
@@ -710,6 +739,55 @@ async function setupGA4OAuth() {
710
739
  printError(`Failed: ${e.message}`);
711
740
  }
712
741
  }
742
+ async function setupPageSpeed() {
743
+ printBoxHeader('PageSpeed Insights Setup');
744
+ console.log('This key is optional, but raises daily quota from ~100 to 25,000 queries/day.');
745
+ console.log('1. Go to https://console.cloud.google.com/apis/credentials');
746
+ console.log('2. Create an API key and enable the "PageSpeed Insights API" in your project.\n');
747
+ const apiKey = await ask('Enter your PageSpeed API Key (leave empty to skip): ');
748
+ if (!apiKey) {
749
+ printInfo('PageSpeed setup skipped or cleared.');
750
+ return;
751
+ }
752
+ const envPath = resolve('.env');
753
+ let envContent = '';
754
+ if (existsSync(envPath)) {
755
+ envContent = readFileSync(envPath, 'utf8');
756
+ if (envContent.includes('PAGESPEED_API_KEY=')) {
757
+ // Replace existing line
758
+ envContent = envContent.replace(/PAGESPEED_API_KEY=.*/, `PAGESPEED_API_KEY=${apiKey}`);
759
+ }
760
+ else {
761
+ // Append
762
+ envContent += `\nPAGESPEED_API_KEY=${apiKey}\n`;
763
+ }
764
+ }
765
+ else {
766
+ const examplePath = resolve('.env.example');
767
+ if (existsSync(examplePath)) {
768
+ envContent = readFileSync(examplePath, 'utf8');
769
+ if (envContent.includes('PAGESPEED_API_KEY=')) {
770
+ envContent = envContent.replace(/PAGESPEED_API_KEY=.*/, `PAGESPEED_API_KEY=${apiKey}`);
771
+ }
772
+ else {
773
+ envContent += `\nPAGESPEED_API_KEY=${apiKey}\n`;
774
+ }
775
+ }
776
+ else {
777
+ envContent = `PAGESPEED_API_KEY=${apiKey}\n`;
778
+ }
779
+ }
780
+ writeFileSync(envPath, envContent, 'utf8');
781
+ printSuccess('Successfully wrote PAGESPEED_API_KEY to .env file!');
782
+ console.log(`\n${colors.bold}Note for MCP Client integration:${colors.reset}`);
783
+ console.log('If you run this server via an MCP Client (like Cursor, Claude Desktop, VS Code),');
784
+ console.log('you must configure the environment variables in your client\'s config file.');
785
+ console.log('See the documentation for more details:');
786
+ console.log(`${colors.cyan}https://github.com/saurabhsharma2u/search-console-mcp#pagespeed-insights-optional-api-key${colors.reset}\n`);
787
+ // Force reloading of env vars for current process
788
+ process.env.PAGESPEED_API_KEY = apiKey;
789
+ await ask('Press Enter to continue...');
790
+ }
713
791
  const isMain = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
714
792
  if (isMain) {
715
793
  main().catch((error) => {
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.1",
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",
@@ -29,27 +22,31 @@
29
22
  "google analytics",
30
23
  "ga4"
31
24
  ],
32
- "author": "",
25
+ "author": "Saurabh Sharma",
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
  }