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 +111 -196
- package/dist/common/utils/regex.js +109 -17
- package/dist/google/client.js +87 -0
- package/dist/google/docs/algorithm-updates.js +4 -0
- package/dist/google/docs/filters.js +2 -2
- package/dist/google/docs/index.js +1 -0
- package/dist/google/docs/indexing.js +98 -0
- package/dist/google/tools/advanced-analytics.js +4 -0
- package/dist/google/tools/indexing.js +93 -0
- package/dist/google/tools/pagespeed.js +26 -5
- package/dist/index.js +72 -1
- package/dist/setup.js +84 -6
- package/package.json +19 -22
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
|
-
|
|
5
|
+
**Google Search Console + Bing Webmaster Tools + GA4 โ in one context window.**
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
---
|
|
7
|
+
Stop exporting CSVs. Start asking your AI agent questions.
|
|
9
8
|
|
|
10
9
|
[](https://opensource.org/licenses/MIT)
|
|
11
10
|
[](https://github.com/saurabhsharma2u/search-console-mcp/actions/workflows/ci.yml)
|
|
11
|
+
[](https://github.com/saurabhsharma2u/search-console-mcp/stargazers)
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
55
|
+
---
|
|
100
56
|
|
|
101
|
-
|
|
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
|
-
|
|
61
|
+
> **"My traffic dropped this week vs. last. Find exactly when it started and which pages are responsible."**
|
|
113
62
|
|
|
114
|
-
|
|
63
|
+
> **"Find keywords for example.com ranking positions 8โ15 with 1,000+ impressions โ my best quick wins."**
|
|
115
64
|
|
|
116
|
-
|
|
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
|
-
|
|
69
|
+
<details>
|
|
70
|
+
<summary>More example prompts</summary>
|
|
127
71
|
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
```
|
|
100
|
+
<details>
|
|
101
|
+
<summary id="service-account-advanced">Service Account setup (for servers/automation)</summary>
|
|
152
102
|
|
|
153
|
-
|
|
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
|
-
|
|
114
|
+
40+ tools across five categories. A few flagship ones:
|
|
159
115
|
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* @
|
|
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
|
|
14
|
-
return re
|
|
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
|
|
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
|
|
34
|
-
return texts.map(t => re.
|
|
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
|
}
|
package/dist/google/client.js
CHANGED
|
@@ -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
|
|
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
|
|
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;
|
|
@@ -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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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.
|
|
527
|
-
|
|
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.
|
|
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": "^
|
|
37
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
38
|
-
"@napi-rs/keyring": "^1.
|
|
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.
|
|
41
|
-
"googleapis": "^
|
|
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
|
-
"
|
|
45
|
-
"zod": "^4.3.6"
|
|
37
|
+
"zod": "^4.4.3"
|
|
46
38
|
},
|
|
47
39
|
"devDependencies": {
|
|
48
|
-
"@types/node": "^25.2
|
|
49
|
-
"@
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
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
|
}
|