search-console-mcp 1.11.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -15
- package/dist/accounts.js +215 -0
- package/dist/bing/client.js +37 -8
- package/dist/bing/tools/analytics.js +49 -17
- package/dist/bing/tools/crawl.js +1 -1
- package/dist/bing/tools/inspection.js +20 -1
- package/dist/bing/tools/links.js +1 -1
- package/dist/bing/tools/seo-insights.js +10 -9
- package/dist/bing/tools/sitemaps.js +3 -3
- package/dist/bing/tools/sites-health.js +2 -2
- package/dist/bing/tools/sites.js +6 -5
- package/dist/bing/tools/url-submission.js +3 -3
- package/dist/common/auth/config.js +149 -0
- package/dist/common/auth/resolver.js +65 -0
- package/dist/common/concurrency.js +22 -0
- package/dist/common/errors.js +5 -1
- package/dist/common/platforms.js +19 -0
- package/dist/common/tools/compare-engines/adapters.js +95 -0
- package/dist/common/tools/compare-engines/comparator.js +61 -0
- package/dist/common/tools/compare-engines/ga4-adapters.js +12 -0
- package/dist/common/tools/compare-engines/ga4-gsc-bing-comparator.js +207 -0
- package/dist/common/tools/compare-engines/ga4-gsc-comparator.js +110 -0
- package/dist/common/tools/compare-engines/index.js +37 -0
- package/dist/common/tools/compare-engines/normalizer.js +20 -0
- package/dist/common/tools/compare-engines/signals.js +34 -0
- package/dist/common/tools/compare-engines/types.js +1 -0
- package/dist/common/tools/compare-engines/utils.js +33 -0
- package/dist/common/tools/get-started.js +348 -0
- package/dist/common/tools/schema-validator.js +10 -1
- package/dist/ga4/client.js +151 -0
- package/dist/ga4/tools/analytics.js +245 -0
- package/dist/ga4/tools/behavior.js +113 -0
- package/dist/ga4/tools/pagespeed.js +38 -0
- package/dist/ga4/tools/realtime.js +17 -0
- package/dist/ga4/utils.js +19 -0
- package/dist/google/client.js +84 -136
- package/dist/google/tools/analytics.js +1 -1
- package/dist/google/tools/inspection.js +20 -0
- package/dist/google/tools/seo-insights.js +99 -45
- package/dist/google/tools/sites-health.js +5 -25
- package/dist/google/tools/sites.js +7 -6
- package/dist/index.js +653 -139
- package/dist/prompts/index.js +315 -0
- package/dist/setup.js +472 -60
- package/dist/utils/ui.js +35 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
# Search Console MCP
|
|
3
3
|
|
|
4
|
-
A Model Context Protocol (MCP) server that transforms how you interact with **Google Search Console** and **
|
|
4
|
+
A Model Context Protocol (MCP) server that transforms how you interact with **Google Search Console**, **Bing Webmaster Tools**, and **Google Analytics 4**. Stop exporting CSVs and start asking questions.
|
|
5
5
|
|
|
6
6
|
[📚 View Documentation](https://searchconsolemcp.mintlify.app/)
|
|
7
7
|
|
|
@@ -12,17 +12,19 @@ A Model Context Protocol (MCP) server that transforms how you interact with **Go
|
|
|
12
12
|
|
|
13
13
|
## Why use this?
|
|
14
14
|
|
|
15
|
-
### ❌ The Old Way
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
5. Create a filter for "Position > 10" AND "Impressions > 1000"
|
|
21
|
-
6. Analyze manually to find opportunities
|
|
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.
|
|
22
20
|
|
|
23
|
-
### ✅ The
|
|
24
|
-
**
|
|
25
|
-
|
|
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.**
|
|
26
28
|
|
|
27
29
|
---
|
|
28
30
|
## 🎯 Magic Prompts
|
|
@@ -47,15 +49,24 @@ Copy and paste these into your MCP client (Claude Desktop, etc.) to see the inte
|
|
|
47
49
|
#### ⚡ The Speed vs. Ranking Correlator
|
|
48
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?"
|
|
49
51
|
|
|
50
|
-
####
|
|
52
|
+
#### 🔀 Multi-Engine Comparison
|
|
51
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?"
|
|
52
54
|
|
|
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."
|
|
63
|
+
|
|
53
64
|
---
|
|
54
65
|
|
|
55
66
|
## 🔐 Authentication (Desktop Flow)
|
|
56
67
|
|
|
57
68
|
Search Console MCP uses a **Secure Desktop Flow**. This provides high-security, professional grade authentication for your Google account:
|
|
58
|
-
- **Multi-Account Support**:
|
|
69
|
+
- **Multi-Account Support**: Connect multiple Google and Bing accounts. The server automatically picks the right one for each site.
|
|
59
70
|
- **System Keychain Primary**: Tokens are stored in your OS's native credential manager (macOS Keychain, Windows Credential Manager, or Linux Secret Service).
|
|
60
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.
|
|
61
72
|
- **Silent Background Refresh**: Tokens auto-refresh silently when they expire.
|
|
@@ -112,16 +123,48 @@ To access Bing data, you simply need an API Key.
|
|
|
112
123
|
|
|
113
124
|
---
|
|
114
125
|
|
|
126
|
+
## 📊 Google Analytics 4 (GA4)
|
|
127
|
+
|
|
128
|
+
Connect your GA4 properties to correlate ranking data with user behavior.
|
|
129
|
+
|
|
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.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 👥 Multi-Account Management
|
|
139
|
+
|
|
140
|
+
Manage multiple Google and Bing accounts from the CLI:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# List all connected accounts
|
|
144
|
+
npx search-console-mcp accounts list
|
|
145
|
+
|
|
146
|
+
# Remove an account
|
|
147
|
+
npx search-console-mcp accounts remove --account=marketing@company.com
|
|
148
|
+
|
|
149
|
+
# Add a site boundary to an account
|
|
150
|
+
npx search-console-mcp accounts add-site --account=marketing@company.com --site=example.com
|
|
151
|
+
```
|
|
152
|
+
|
|
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)
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
115
157
|
|
|
116
158
|
## 🛡️ Fort Knox Security
|
|
117
159
|
|
|
118
160
|
This MCP server implements a multi-layered security architecture:
|
|
119
161
|
|
|
120
162
|
* **Keychain Integration**: Primarily uses the **macOS Keychain**, **Windows Credential Manager**, or **libsecret (Linux)** to store tokens.
|
|
121
|
-
* **
|
|
163
|
+
* **Encrypted Config**: Account configuration is stored in `~/.search-console-mcp-config.enc` using **AES-256-GCM** encryption.
|
|
122
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.
|
|
123
165
|
* **Minimalist Storage**: Only the `refresh_token` and `expiry_date` are stored.
|
|
124
|
-
* **
|
|
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
168
|
|
|
126
169
|
---
|
|
127
170
|
|
|
@@ -185,6 +228,29 @@ These are low-level tools designed to be used by other AI agents to build comple
|
|
|
185
228
|
| `bing_analytics_time_series` | Advanced time series analysis for Bing. |
|
|
186
229
|
| `bing_sitemaps_list` / `bing_sitemaps_submit` | Manage sitemaps in Bing. |
|
|
187
230
|
|
|
231
|
+
### Google Analytics 4 (GA4)
|
|
232
|
+
| Tool | Description |
|
|
233
|
+
|------|-------------|
|
|
234
|
+
| `analytics_page_performance` | Detailed page metrics (sessions, engagement, views). |
|
|
235
|
+
| `analytics_traffic_sources` | Analyze sessions by Channel, Source, and Medium. |
|
|
236
|
+
| `analytics_organic_landing_pages` | Focused metrics for organic traffic landing pages. |
|
|
237
|
+
| `analytics_content_performance` | Analyze content performance by Content Group in GA4. |
|
|
238
|
+
| `analytics_conversion_funnel` | Top converting pages and events. |
|
|
239
|
+
| `analytics_user_behavior` | Device, Country, and Engagement breakdown. |
|
|
240
|
+
| `analytics_audience_segments` | New vs Returning, Age, and OS analysis. |
|
|
241
|
+
| `analytics_realtime` | Live active user data by page and location. |
|
|
242
|
+
| `analytics_ecommerce` | Product and revenue performance. |
|
|
243
|
+
| `analytics_pagespeed_correlation` | Correlate GA4 metrics with PageSpeed scores. |
|
|
244
|
+
|
|
245
|
+
### Cross-Platform Intelligence
|
|
246
|
+
| Tool | Description |
|
|
247
|
+
|------|-------------|
|
|
248
|
+
| `opportunity_matrix` | **[Flagship]** Prioritize SEO tasks by combining signals from GSC, GA4, and Bing. |
|
|
249
|
+
| `page_analysis` | Joint analysis of ranking (GSC) vs behavior (GA4) for pages. |
|
|
250
|
+
| `traffic_health_check` | Diagnose tracking gaps by comparing GSC clicks to GA4 organic sessions. |
|
|
251
|
+
| `brand_analysis` | Brand vs Non-Brand split across GSC, Bing, and GA4. |
|
|
252
|
+
| `compare_engines` | Compare keyword performance between Google and Bing. |
|
|
253
|
+
|
|
188
254
|
|
|
189
255
|
|
|
190
256
|
## License
|
package/dist/accounts.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { loadConfig, removeAccount, updateAccount } from './common/auth/config.js';
|
|
2
|
+
function parseFlags(args) {
|
|
3
|
+
const flags = {};
|
|
4
|
+
for (const arg of args) {
|
|
5
|
+
const match = arg.match(/^--([^=]+)=(.+)$/);
|
|
6
|
+
if (match) {
|
|
7
|
+
flags[match[1]] = match[2];
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return flags;
|
|
11
|
+
}
|
|
12
|
+
function findAccountByAliasOrId(accounts, identifier) {
|
|
13
|
+
// Try exact ID match first
|
|
14
|
+
if (accounts[identifier])
|
|
15
|
+
return accounts[identifier];
|
|
16
|
+
// Then try alias match (case-insensitive)
|
|
17
|
+
return Object.values(accounts).find((a) => a.alias?.toLowerCase() === identifier.toLowerCase() || a.id === identifier);
|
|
18
|
+
}
|
|
19
|
+
export async function main(args) {
|
|
20
|
+
const subcommand = args[0] || 'list';
|
|
21
|
+
const flags = parseFlags(args.slice(1));
|
|
22
|
+
// Positional args (non-flag args after the subcommand)
|
|
23
|
+
const positional = args.slice(1).filter(a => !a.startsWith('--'));
|
|
24
|
+
if (subcommand === 'list') {
|
|
25
|
+
try {
|
|
26
|
+
const config = await loadConfig();
|
|
27
|
+
const accounts = Object.values(config.accounts);
|
|
28
|
+
if (accounts.length === 0) {
|
|
29
|
+
console.log(JSON.stringify({
|
|
30
|
+
accounts: [],
|
|
31
|
+
message: "No accounts connected.",
|
|
32
|
+
resolution: {
|
|
33
|
+
command: "search-console-mcp setup",
|
|
34
|
+
google: "search-console-mcp setup --engine=google",
|
|
35
|
+
bing: "search-console-mcp setup --engine=bing",
|
|
36
|
+
ga4: "search-console-mcp setup --engine=ga4"
|
|
37
|
+
},
|
|
38
|
+
setup_instructions: {
|
|
39
|
+
google: [
|
|
40
|
+
"Run: search-console-mcp setup --engine=google",
|
|
41
|
+
"Choose OAuth 2.0 login or Service Account",
|
|
42
|
+
"Authorize in browser and select sites to restrict (or allow all)"
|
|
43
|
+
],
|
|
44
|
+
bing: [
|
|
45
|
+
"Run: search-console-mcp setup --engine=bing",
|
|
46
|
+
"Get your API key from https://www.bing.com/webmasters/settings/api",
|
|
47
|
+
"Enter the API key when prompted"
|
|
48
|
+
],
|
|
49
|
+
ga4: [
|
|
50
|
+
"Run: search-console-mcp setup --engine=ga4",
|
|
51
|
+
"Choose Service Account (OAuth coming soon)",
|
|
52
|
+
"Add the service account email to your GA4 property"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}, null, 2));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const siteRows = [];
|
|
59
|
+
for (const a of accounts) {
|
|
60
|
+
const alias = a.alias || '[Unnamed]';
|
|
61
|
+
const engine = a.engine === 'google' ? 'Google' : a.engine === 'bing' ? 'Bing' : 'GA4';
|
|
62
|
+
if (!a.websites || a.websites.length === 0) {
|
|
63
|
+
siteRows.push({
|
|
64
|
+
site: '[All Sites Authorized]',
|
|
65
|
+
account: alias,
|
|
66
|
+
engine: engine,
|
|
67
|
+
id: a.id
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
for (const site of a.websites) {
|
|
72
|
+
siteRows.push({
|
|
73
|
+
site: site,
|
|
74
|
+
account: alias,
|
|
75
|
+
engine: engine,
|
|
76
|
+
id: a.id
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
console.log(JSON.stringify({ accounts: siteRows }, null, 2));
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.log(JSON.stringify({
|
|
85
|
+
error: "Failed to list accounts",
|
|
86
|
+
message: error.message
|
|
87
|
+
}, null, 2));
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (subcommand === 'remove') {
|
|
92
|
+
// Flags (agent): --account=alias --site=url | Positional (human): remove <alias_or_site>
|
|
93
|
+
const identifier = flags['account'] || flags['id'] || positional[0];
|
|
94
|
+
const siteToRemove = flags['site'];
|
|
95
|
+
if (!identifier && !siteToRemove) {
|
|
96
|
+
console.log(JSON.stringify({
|
|
97
|
+
error: "Missing arguments",
|
|
98
|
+
message: "Provide --account=<alias|id> to remove an account, or --site=<url> to remove a specific site boundary.",
|
|
99
|
+
resolution: {
|
|
100
|
+
by_account: "search-console-mcp accounts remove --account=<alias_or_id>",
|
|
101
|
+
by_site: "search-console-mcp accounts remove --site=<siteUrl>"
|
|
102
|
+
}
|
|
103
|
+
}, null, 2));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const config = await loadConfig();
|
|
108
|
+
if (identifier) {
|
|
109
|
+
// Remove entire account by alias or ID
|
|
110
|
+
const account = findAccountByAliasOrId(config.accounts, identifier);
|
|
111
|
+
if (!account) {
|
|
112
|
+
console.log(JSON.stringify({
|
|
113
|
+
error: "Account not found",
|
|
114
|
+
message: `No account matching '${identifier}' was found.`,
|
|
115
|
+
resolution: "Run: search-console-mcp accounts list"
|
|
116
|
+
}, null, 2));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await removeAccount(account.id);
|
|
120
|
+
console.log(JSON.stringify({
|
|
121
|
+
success: true,
|
|
122
|
+
message: `Account '${account.alias}' (${account.id}) removed successfully.`
|
|
123
|
+
}, null, 2));
|
|
124
|
+
}
|
|
125
|
+
else if (siteToRemove) {
|
|
126
|
+
// Remove a specific site boundary from whichever account owns it
|
|
127
|
+
let found = false;
|
|
128
|
+
for (const account of Object.values(config.accounts)) {
|
|
129
|
+
if (account.websites?.includes(siteToRemove)) {
|
|
130
|
+
account.websites = account.websites.filter((w) => w !== siteToRemove);
|
|
131
|
+
await updateAccount(account);
|
|
132
|
+
found = true;
|
|
133
|
+
console.log(JSON.stringify({
|
|
134
|
+
success: true,
|
|
135
|
+
message: `Site '${siteToRemove}' removed from account '${account.alias}'.`
|
|
136
|
+
}, null, 2));
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!found) {
|
|
141
|
+
console.log(JSON.stringify({
|
|
142
|
+
error: "Site not found",
|
|
143
|
+
message: `No account has '${siteToRemove}' in its site boundaries.`,
|
|
144
|
+
resolution: "Run: search-console-mcp accounts list"
|
|
145
|
+
}, null, 2));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
console.log(JSON.stringify({
|
|
151
|
+
error: "Failed to remove",
|
|
152
|
+
message: error.message
|
|
153
|
+
}, null, 2));
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (subcommand === 'add-site') {
|
|
158
|
+
// Flags (agent): --account=alias --site=url | Positional (human): add-site <alias> <site>
|
|
159
|
+
const identifier = flags['account'] || flags['id'] || positional[0];
|
|
160
|
+
const site = flags['site'] || positional[1];
|
|
161
|
+
if (!identifier || !site) {
|
|
162
|
+
console.log(JSON.stringify({
|
|
163
|
+
error: "Missing arguments",
|
|
164
|
+
message: "Both --account and --site are required.",
|
|
165
|
+
resolution: "search-console-mcp accounts add-site --account=<alias_or_id> --site=<siteUrl>"
|
|
166
|
+
}, null, 2));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const config = await loadConfig();
|
|
171
|
+
const account = findAccountByAliasOrId(config.accounts, identifier);
|
|
172
|
+
if (!account) {
|
|
173
|
+
console.log(JSON.stringify({
|
|
174
|
+
error: "Account not found",
|
|
175
|
+
message: `No account matching '${identifier}' was found.`,
|
|
176
|
+
resolution: "Run: search-console-mcp accounts list"
|
|
177
|
+
}, null, 2));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!account.websites)
|
|
181
|
+
account.websites = [];
|
|
182
|
+
if (!account.websites.includes(site)) {
|
|
183
|
+
account.websites.push(site);
|
|
184
|
+
await updateAccount(account);
|
|
185
|
+
console.log(JSON.stringify({
|
|
186
|
+
success: true,
|
|
187
|
+
message: `Site '${site}' added to account '${account.alias}'.`
|
|
188
|
+
}, null, 2));
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
console.log(JSON.stringify({
|
|
192
|
+
success: true,
|
|
193
|
+
message: `Account '${account.alias}' already has '${site}' in its boundaries.`
|
|
194
|
+
}, null, 2));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
console.log(JSON.stringify({
|
|
199
|
+
error: "Failed to add site",
|
|
200
|
+
message: error.message
|
|
201
|
+
}, null, 2));
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
console.log(JSON.stringify({
|
|
206
|
+
error: "Unknown command",
|
|
207
|
+
message: `'${subcommand}' is not a valid subcommand.`,
|
|
208
|
+
resolution: {
|
|
209
|
+
list: "search-console-mcp accounts list",
|
|
210
|
+
remove_account: "search-console-mcp accounts remove --account=<alias_or_id>",
|
|
211
|
+
remove_site: "search-console-mcp accounts remove --site=<siteUrl>",
|
|
212
|
+
add_site: "search-console-mcp accounts add-site --account=<alias_or_id> --site=<siteUrl>"
|
|
213
|
+
}
|
|
214
|
+
}, null, 2));
|
|
215
|
+
}
|
package/dist/bing/client.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveAccount } from '../common/auth/resolver.js';
|
|
2
|
+
import { loadConfig } from '../common/auth/config.js';
|
|
1
3
|
export class BingClient {
|
|
2
4
|
apiKey;
|
|
3
5
|
baseUrl = 'https://ssl.bing.com/webmaster/api.svc/json';
|
|
@@ -93,14 +95,41 @@ export class BingClient {
|
|
|
93
95
|
return this.request('GetRelatedKeywords', { q, country, language });
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
|
-
let
|
|
97
|
-
export async function getBingClient() {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
let cachedBingClients = {};
|
|
99
|
+
export async function getBingClient(siteUrl, accountId) {
|
|
100
|
+
// 1. Resolve Account
|
|
101
|
+
let apiKey;
|
|
102
|
+
let cacheKey;
|
|
103
|
+
if (accountId) {
|
|
104
|
+
const config = await loadConfig();
|
|
105
|
+
const account = config.accounts[accountId];
|
|
106
|
+
if (!account || account.engine !== 'bing') {
|
|
107
|
+
throw new Error(`Bing account ${accountId} not found.`);
|
|
108
|
+
}
|
|
109
|
+
apiKey = account.apiKey;
|
|
110
|
+
cacheKey = account.id;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
try {
|
|
114
|
+
const account = await resolveAccount(siteUrl || '', 'bing');
|
|
115
|
+
apiKey = account.apiKey;
|
|
116
|
+
cacheKey = account.id;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// Fallback to environment variable for legacy support if resolution fails or no site specified
|
|
120
|
+
apiKey = process.env.BING_API_KEY;
|
|
121
|
+
cacheKey = 'env_fallback';
|
|
122
|
+
if (!apiKey) {
|
|
123
|
+
throw error; // Re-throw the resolution error if no ENV fallback either
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (cachedBingClients[cacheKey])
|
|
128
|
+
return cachedBingClients[cacheKey];
|
|
101
129
|
if (!apiKey) {
|
|
102
|
-
throw new Error('Bing API Key not found. Please
|
|
130
|
+
throw new Error('Bing API Key not found. Please run setup to add an account.');
|
|
103
131
|
}
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
const client = new BingClient(apiKey);
|
|
133
|
+
cachedBingClients[cacheKey] = client;
|
|
134
|
+
return client;
|
|
106
135
|
}
|
|
@@ -1,37 +1,69 @@
|
|
|
1
1
|
import { getBingClient } from '../client.js';
|
|
2
2
|
/**
|
|
3
|
-
* Get query performance stats for a Bing site.
|
|
3
|
+
* Get query performance stats for a Bing site with optional date filtering.
|
|
4
4
|
*/
|
|
5
|
-
export async function getQueryStats(siteUrl) {
|
|
6
|
-
const client = await getBingClient();
|
|
7
|
-
|
|
5
|
+
export async function getQueryStats(siteUrl, startDate, endDate) {
|
|
6
|
+
const client = await getBingClient(siteUrl);
|
|
7
|
+
const stats = await client.getQueryStats(siteUrl);
|
|
8
|
+
if (!startDate && !endDate)
|
|
9
|
+
return stats;
|
|
10
|
+
const start = startDate ? new Date(startDate) : new Date(0);
|
|
11
|
+
const end = endDate ? new Date(endDate) : new Date();
|
|
12
|
+
return stats.filter(row => {
|
|
13
|
+
const d = new Date(row.Date);
|
|
14
|
+
return d >= start && d <= end;
|
|
15
|
+
});
|
|
8
16
|
}
|
|
9
17
|
/**
|
|
10
|
-
* Get page performance stats (top pages) for a Bing site.
|
|
18
|
+
* Get page performance stats (top pages) for a Bing site with optional date filtering.
|
|
11
19
|
*/
|
|
12
|
-
export async function getPageStats(siteUrl) {
|
|
13
|
-
const client = await getBingClient();
|
|
14
|
-
|
|
20
|
+
export async function getPageStats(siteUrl, startDate, endDate) {
|
|
21
|
+
const client = await getBingClient(siteUrl);
|
|
22
|
+
const stats = await client.getPageStats(siteUrl);
|
|
23
|
+
if (!startDate && !endDate)
|
|
24
|
+
return stats;
|
|
25
|
+
const start = startDate ? new Date(startDate) : new Date(0);
|
|
26
|
+
const end = endDate ? new Date(endDate) : new Date();
|
|
27
|
+
return stats.filter(row => {
|
|
28
|
+
const d = new Date(row.Date);
|
|
29
|
+
return d >= start && d <= end;
|
|
30
|
+
});
|
|
15
31
|
}
|
|
16
32
|
/**
|
|
17
|
-
* Get query stats for a specific page on a Bing site.
|
|
33
|
+
* Get query stats for a specific page on a Bing site with optional date filtering.
|
|
18
34
|
*/
|
|
19
|
-
export async function getPageQueryStats(siteUrl, pageUrl) {
|
|
20
|
-
const client = await getBingClient();
|
|
21
|
-
|
|
35
|
+
export async function getPageQueryStats(siteUrl, pageUrl, startDate, endDate) {
|
|
36
|
+
const client = await getBingClient(siteUrl);
|
|
37
|
+
const stats = await client.getPageQueryStats(siteUrl, pageUrl);
|
|
38
|
+
if (!startDate && !endDate)
|
|
39
|
+
return stats;
|
|
40
|
+
const start = startDate ? new Date(startDate) : new Date(0);
|
|
41
|
+
const end = endDate ? new Date(endDate) : new Date();
|
|
42
|
+
return stats.filter(row => {
|
|
43
|
+
const d = new Date(row.Date);
|
|
44
|
+
return d >= start && d <= end;
|
|
45
|
+
});
|
|
22
46
|
}
|
|
23
47
|
/**
|
|
24
|
-
* Get combined query and page performance stats.
|
|
48
|
+
* Get combined query and page performance stats with optional date filtering.
|
|
25
49
|
*/
|
|
26
|
-
export async function getQueryPageStats(siteUrl) {
|
|
27
|
-
const client = await getBingClient();
|
|
28
|
-
|
|
50
|
+
export async function getQueryPageStats(siteUrl, startDate, endDate) {
|
|
51
|
+
const client = await getBingClient(siteUrl);
|
|
52
|
+
const stats = await client.getQueryPageStats(siteUrl);
|
|
53
|
+
if (!startDate && !endDate)
|
|
54
|
+
return stats;
|
|
55
|
+
const start = startDate ? new Date(startDate) : new Date(0);
|
|
56
|
+
const end = endDate ? new Date(endDate) : new Date();
|
|
57
|
+
return stats.filter(row => {
|
|
58
|
+
const d = new Date(row.Date);
|
|
59
|
+
return d >= start && d <= end;
|
|
60
|
+
});
|
|
29
61
|
}
|
|
30
62
|
/**
|
|
31
63
|
* Get historical rank and traffic statistics for a site.
|
|
32
64
|
*/
|
|
33
65
|
export async function getRankAndTrafficStats(siteUrl) {
|
|
34
|
-
const client = await getBingClient();
|
|
66
|
+
const client = await getBingClient(siteUrl);
|
|
35
67
|
return client.getRankAndTrafficStats(siteUrl);
|
|
36
68
|
}
|
|
37
69
|
/**
|
package/dist/bing/tools/crawl.js
CHANGED
|
@@ -10,6 +10,6 @@ export async function getCrawlIssues(siteUrl) {
|
|
|
10
10
|
* Get crawl statistics for a site.
|
|
11
11
|
*/
|
|
12
12
|
export async function getCrawlStats(siteUrl) {
|
|
13
|
-
const client = await getBingClient();
|
|
13
|
+
const client = await getBingClient(siteUrl);
|
|
14
14
|
return client.getCrawlStats(siteUrl);
|
|
15
15
|
}
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import { getBingClient } from '../client.js';
|
|
2
|
+
import { limitConcurrency } from '../../common/concurrency.js';
|
|
2
3
|
/**
|
|
3
4
|
* Get detailed indexing and crawl information for a URL.
|
|
4
5
|
*/
|
|
5
6
|
export async function getUrlInfo(siteUrl, url) {
|
|
6
|
-
const client = await getBingClient();
|
|
7
|
+
const client = await getBingClient(siteUrl);
|
|
7
8
|
return client.getUrlInfo(siteUrl, url);
|
|
8
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Inspect multiple URLs for a site in batch (Bing).
|
|
12
|
+
*
|
|
13
|
+
* @param siteUrl - The URL of the site as defined in Bing Webmaster Tools.
|
|
14
|
+
* @param inspectionUrls - The list of URLs to inspect.
|
|
15
|
+
* @returns An array of results, each containing the URL and its inspection result or error.
|
|
16
|
+
*/
|
|
17
|
+
export async function inspectBatch(siteUrl, inspectionUrls) {
|
|
18
|
+
return limitConcurrency(inspectionUrls, 5, async (url) => {
|
|
19
|
+
try {
|
|
20
|
+
const result = await getUrlInfo(siteUrl, url);
|
|
21
|
+
return { url, result };
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return { url, error: error.message };
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
package/dist/bing/tools/links.js
CHANGED
|
@@ -3,8 +3,8 @@ import { getQueryStats, getQueryPageStats } from './analytics.js';
|
|
|
3
3
|
* Find "low-hanging fruit" keywords in Bing.
|
|
4
4
|
*/
|
|
5
5
|
export async function findLowHangingFruit(siteUrl, options = {}) {
|
|
6
|
-
const { minImpressions = 100, limit = 50 } = options;
|
|
7
|
-
const rows = await getQueryStats(siteUrl);
|
|
6
|
+
const { minImpressions = 100, limit = 50, queryStats } = options;
|
|
7
|
+
const rows = queryStats || await getQueryStats(siteUrl);
|
|
8
8
|
// Filter for low-hanging fruit: position 5-20, high impressions
|
|
9
9
|
const candidates = rows
|
|
10
10
|
.filter(row => {
|
|
@@ -36,8 +36,8 @@ export async function findLowHangingFruit(siteUrl, options = {}) {
|
|
|
36
36
|
* Find keywords ranking just off page 1 in Bing (positions 8-15).
|
|
37
37
|
*/
|
|
38
38
|
export async function findStrikingDistance(siteUrl, options = {}) {
|
|
39
|
-
const { limit = 50 } = options;
|
|
40
|
-
const rows = await getQueryStats(siteUrl);
|
|
39
|
+
const { limit = 50, queryStats } = options;
|
|
40
|
+
const rows = queryStats || await getQueryStats(siteUrl);
|
|
41
41
|
return rows
|
|
42
42
|
.filter(r => r.AvgPosition >= 8 && r.AvgPosition <= 15)
|
|
43
43
|
.sort((a, b) => b.Impressions - a.Impressions)
|
|
@@ -47,8 +47,8 @@ export async function findStrikingDistance(siteUrl, options = {}) {
|
|
|
47
47
|
* Identify queries where the site ranks well but has low CTR in Bing.
|
|
48
48
|
*/
|
|
49
49
|
export async function findLowCTROpportunities(siteUrl, options = {}) {
|
|
50
|
-
const { minImpressions = 500, limit = 50 } = options;
|
|
51
|
-
const rows = await getQueryStats(siteUrl);
|
|
50
|
+
const { minImpressions = 500, limit = 50, queryStats } = options;
|
|
51
|
+
const rows = queryStats || await getQueryStats(siteUrl);
|
|
52
52
|
// Approximate benchmarks for CTR by position (simplified)
|
|
53
53
|
const benchmarks = {
|
|
54
54
|
1: 0.30, 2: 0.15, 3: 0.10, 4: 0.06, 5: 0.04,
|
|
@@ -119,10 +119,11 @@ export async function detectCannibalization(siteUrl, options = {}) {
|
|
|
119
119
|
*/
|
|
120
120
|
export async function generateRecommendations(siteUrl) {
|
|
121
121
|
const insights = [];
|
|
122
|
+
const queryStatsPromise = getQueryStats(siteUrl);
|
|
122
123
|
const [lowHangingFruit, strikingDistance, lowCTR, cannibalization] = await Promise.all([
|
|
123
|
-
findLowHangingFruit(siteUrl, { limit: 10 }),
|
|
124
|
-
findStrikingDistance(siteUrl, { limit: 10 }),
|
|
125
|
-
findLowCTROpportunities(siteUrl, { limit: 10 }),
|
|
124
|
+
queryStatsPromise.then(stats => findLowHangingFruit(siteUrl, { limit: 10, queryStats: stats })),
|
|
125
|
+
queryStatsPromise.then(stats => findStrikingDistance(siteUrl, { limit: 10, queryStats: stats })),
|
|
126
|
+
queryStatsPromise.then(stats => findLowCTROpportunities(siteUrl, { limit: 10, queryStats: stats })),
|
|
126
127
|
detectCannibalization(siteUrl, { limit: 10 })
|
|
127
128
|
]);
|
|
128
129
|
if (lowHangingFruit.length > 0) {
|
|
@@ -6,7 +6,7 @@ import { getBingClient } from '../client.js';
|
|
|
6
6
|
* @returns A list of sitemaps.
|
|
7
7
|
*/
|
|
8
8
|
export async function listSitemaps(siteUrl) {
|
|
9
|
-
const client = await getBingClient();
|
|
9
|
+
const client = await getBingClient(siteUrl);
|
|
10
10
|
return client.getFeeds(siteUrl);
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
@@ -16,7 +16,7 @@ export async function listSitemaps(siteUrl) {
|
|
|
16
16
|
* @param sitemapUrl - The URL of the sitemap file.
|
|
17
17
|
*/
|
|
18
18
|
export async function submitSitemap(siteUrl, sitemapUrl) {
|
|
19
|
-
const client = await getBingClient();
|
|
19
|
+
const client = await getBingClient(siteUrl);
|
|
20
20
|
await client.submitSitemap(siteUrl, sitemapUrl);
|
|
21
21
|
return `Successfully submitted sitemap: ${sitemapUrl} for site ${siteUrl}`;
|
|
22
22
|
}
|
|
@@ -27,7 +27,7 @@ export async function submitSitemap(siteUrl, sitemapUrl) {
|
|
|
27
27
|
* @param sitemapUrl - The URL of the sitemap to remove.
|
|
28
28
|
*/
|
|
29
29
|
export async function deleteSitemap(siteUrl, sitemapUrl) {
|
|
30
|
-
const client = await getBingClient();
|
|
30
|
+
const client = await getBingClient(siteUrl);
|
|
31
31
|
await client.deleteSitemap(siteUrl, sitemapUrl);
|
|
32
32
|
return `Successfully removed sitemap: ${sitemapUrl} for site ${siteUrl}`;
|
|
33
33
|
}
|
|
@@ -103,7 +103,7 @@ export async function healthCheck(siteUrl) {
|
|
|
103
103
|
if (allSites.length === 0) {
|
|
104
104
|
return [];
|
|
105
105
|
}
|
|
106
|
-
const reports = await Promise.all(allSites.map(site => checkSite(site.Url)));
|
|
106
|
+
const reports = await Promise.all(allSites.map((site) => checkSite(site.Url)));
|
|
107
107
|
const order = { critical: 0, warning: 1, healthy: 2 };
|
|
108
|
-
return reports.sort((a, b) => order[a.status] - order[b.status]);
|
|
108
|
+
return reports.sort((a, b) => (order[a.status] || 0) - (order[b.status] || 0));
|
|
109
109
|
}
|