search-console-mcp 1.9.2 → 1.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/docs/algorithm-updates.js +44 -3
- package/dist/index.js +49 -1
- package/dist/setup.js +10 -6
- package/dist/tools/advanced-analytics.js +25 -1
- package/dist/tools/seo-insights.js +6 -5
- package/dist/tools/seo-primitives.js +7 -16
- package/dist/tools/sites-health.js +135 -0
- package/dist/utils/regex.js +41 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ We take your data security seriously. This tool is designed to be **local-first*
|
|
|
87
87
|
First, use the interactive wizard to validate your credentials and get your configuration:
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
|
-
npx search-console-mcp
|
|
90
|
+
npx -y search-console-mcp setup
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
The wizard will guide you through:
|
|
@@ -155,6 +155,7 @@ These are low-level tools designed to be used by other AI agents to build comple
|
|
|
155
155
|
|------|-------------|
|
|
156
156
|
| `sites_list` | List all verified sites. |
|
|
157
157
|
| `sites_add` / `sites_delete` | Manage properties. |
|
|
158
|
+
| `sites_health_check` | **[NEW]** Run a health check on one or all sites. Checks WoW performance, sitemaps, and anomalies. |
|
|
158
159
|
| `sitemaps_list` / `sitemaps_submit` | Manage sitemaps. |
|
|
159
160
|
|
|
160
161
|
### Inspection & Validation
|
|
@@ -1,18 +1,59 @@
|
|
|
1
1
|
export const algorithmUpdatesDocs = `
|
|
2
2
|
# Google Algorithm Updates Reference
|
|
3
3
|
|
|
4
|
-
The \`analytics_drop_attribution\` tool correlates traffic drops with major known Google Algorithm Updates. The following updates are currently
|
|
4
|
+
The \`analytics_drop_attribution\` tool correlates traffic drops with major known Google Algorithm Updates. The following updates are currently tracked (source: [Search Engine Journal](https://www.searchenginejournal.com/google-algorithm-history/)):
|
|
5
|
+
|
|
6
|
+
## 2026
|
|
7
|
+
|
|
8
|
+
| Date | Update Name | Impact Area |
|
|
9
|
+
|------|-------------|-------------|
|
|
10
|
+
| 2026-02-05 | February 2026 Discover Core Update | Discover Feed |
|
|
11
|
+
|
|
12
|
+
## 2025
|
|
13
|
+
|
|
14
|
+
| Date | Update Name | Impact Area |
|
|
15
|
+
|------|-------------|-------------|
|
|
16
|
+
| 2025-12-11 | December 2025 Core Update | General Ranking |
|
|
17
|
+
| 2025-08-26 | August 2025 Spam Update | Spam / Content Quality |
|
|
18
|
+
| 2025-06-30 | June 2025 Core Update | General Ranking |
|
|
19
|
+
| 2025-03-13 | March 2025 Core Update | General Ranking |
|
|
20
|
+
|
|
21
|
+
## 2024
|
|
5
22
|
|
|
6
23
|
| Date | Update Name | Impact Area |
|
|
7
24
|
|------|-------------|-------------|
|
|
8
|
-
|
|
|
25
|
+
| 2024-12-19 | December 2024 Spam Update | Spam / Content Quality |
|
|
26
|
+
| 2024-12-12 | December 2024 Core Update | General Ranking |
|
|
9
27
|
| 2024-11-11 | November 2024 Core Update | General Ranking |
|
|
10
|
-
| 2024-08-15 | August 2024 Core Update | General Ranking |
|
|
28
|
+
| 2024-08-15 | August 2024 Core Update | General Ranking / Quality |
|
|
11
29
|
| 2024-06-20 | June 2024 Spam Update | Content Quality / Spam |
|
|
30
|
+
| 2024-05-14 | AI Overviews Rollout | Search Features / SERP |
|
|
31
|
+
| 2024-05-06 | Site Reputation Abuse (Manual Actions) | Parasite SEO / Spam |
|
|
12
32
|
| 2024-03-05 | March 2024 Core Update | General Ranking / Quality |
|
|
33
|
+
|
|
34
|
+
## 2023
|
|
35
|
+
|
|
36
|
+
| Date | Update Name | Impact Area |
|
|
37
|
+
|------|-------------|-------------|
|
|
38
|
+
| 2023-11-08 | November 2023 Reviews Update | Review Content |
|
|
13
39
|
| 2023-11-02 | November 2023 Core Update | General Ranking |
|
|
14
40
|
| 2023-10-05 | October 2023 Core Update | General Ranking |
|
|
41
|
+
| 2023-10-04 | October 2023 Spam Update | Spam / Multi-language |
|
|
15
42
|
| 2023-09-14 | September 2023 Helpful Content Update | Quality / Value |
|
|
43
|
+
| 2023-08-22 | August 2023 Core Update | General Ranking |
|
|
44
|
+
| 2023-04-12 | April 2023 Reviews Update | Review Content |
|
|
45
|
+
| 2023-03-15 | March 2023 Core Update | General Ranking |
|
|
46
|
+
| 2023-02-21 | February 2023 Product Reviews Update | Product Reviews |
|
|
47
|
+
|
|
48
|
+
## 2022
|
|
49
|
+
|
|
50
|
+
| Date | Update Name | Impact Area |
|
|
51
|
+
|------|-------------|-------------|
|
|
52
|
+
| 2022-12-14 | December 2022 Link Spam Update | Link Spam / SpamBrain |
|
|
53
|
+
| 2022-12-05 | December 2022 Helpful Content Update | Quality / Value |
|
|
54
|
+
| 2022-10-19 | October 2022 Spam Update | Spam |
|
|
55
|
+
| 2022-09-20 | September 2022 Product Review Update | Product Reviews |
|
|
56
|
+
| 2022-09-12 | September 2022 Core Update | General Ranking |
|
|
16
57
|
|
|
17
58
|
## How Attribution Works
|
|
18
59
|
When a traffic drop is detected, the system checks if it occurred within **+/- 2 days** of any of these dates. If it matches, the update is flagged as a potential primary cause of the drop.
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as seoInsights from "./tools/seo-insights.js";
|
|
|
11
11
|
import * as seoPrimitives from "./tools/seo-primitives.js";
|
|
12
12
|
import * as schemaValidator from "./tools/schema-validator.js";
|
|
13
13
|
import * as advancedAnalytics from "./tools/advanced-analytics.js";
|
|
14
|
+
import * as sitesHealth from "./tools/sites-health.js";
|
|
14
15
|
import { formatError } from "./errors.js";
|
|
15
16
|
const server = new McpServer({
|
|
16
17
|
name: "search-console-mcp",
|
|
@@ -61,6 +62,19 @@ server.tool("sites_get", "Get information about a specific site", { siteUrl: z.s
|
|
|
61
62
|
return formatError(error);
|
|
62
63
|
}
|
|
63
64
|
});
|
|
65
|
+
server.tool("sites_health_check", "Run a health check on one or all verified sites. Checks week-over-week performance, sitemap status, and traffic anomalies. Returns a structured report with an overall status (healthy/warning/critical) per site.", {
|
|
66
|
+
siteUrl: z.string().optional().describe("Optional. The URL of a specific site to check. If omitted, checks all verified sites.")
|
|
67
|
+
}, async ({ siteUrl }) => {
|
|
68
|
+
try {
|
|
69
|
+
const result = await sitesHealth.healthCheck(siteUrl);
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return formatError(error);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
64
78
|
// Sitemaps Tools
|
|
65
79
|
server.tool("sitemaps_list", "List sitemaps for a site", { siteUrl: z.string().describe("The URL of the site") }, async ({ siteUrl }) => {
|
|
66
80
|
try {
|
|
@@ -740,14 +754,48 @@ Provide a summary with actionable recommendations.`
|
|
|
740
754
|
}
|
|
741
755
|
}]
|
|
742
756
|
}));
|
|
757
|
+
server.prompt("site-health-check", {
|
|
758
|
+
siteUrl: z.string().optional().describe("Optional. The URL of a specific site to check. If omitted, checks all verified sites.")
|
|
759
|
+
}, ({ siteUrl }) => ({
|
|
760
|
+
messages: [{
|
|
761
|
+
role: "user",
|
|
762
|
+
content: {
|
|
763
|
+
type: "text",
|
|
764
|
+
text: `Run a comprehensive health check${siteUrl ? ` for ${siteUrl}` : ' across all my verified sites'}.
|
|
765
|
+
|
|
766
|
+
Use the 'sites_health_check' tool${siteUrl ? ` with siteUrl '${siteUrl}'` : ' without a siteUrl to check all sites'}.
|
|
767
|
+
|
|
768
|
+
Then for each site in the results:
|
|
769
|
+
|
|
770
|
+
1. **Summarize the status** (healthy / warning / critical) with a clear visual indicator.
|
|
771
|
+
2. **Performance:** Report the week-over-week changes in clicks, impressions, CTR, and position.
|
|
772
|
+
3. **Sitemaps:** Note any missing sitemaps, errors, or warnings.
|
|
773
|
+
4. **Anomalies:** Highlight any traffic anomaly drops detected.
|
|
774
|
+
|
|
775
|
+
If any site has a 'critical' or 'warning' status:
|
|
776
|
+
- List the specific issues found.
|
|
777
|
+
- For critical traffic drops, use 'analytics_drop_attribution' to check if a Google algorithm update is the likely cause.
|
|
778
|
+
- For sitemap errors, use 'sitemaps_list' to get detailed error information.
|
|
779
|
+
- Provide 3 prioritized action items for each affected site.
|
|
780
|
+
|
|
781
|
+
End with an overall portfolio summary and the single most important action to take right now.`
|
|
782
|
+
}
|
|
783
|
+
}]
|
|
784
|
+
}));
|
|
743
785
|
async function main() {
|
|
786
|
+
// Allow `npx search-console-mcp setup` to launch the setup wizard
|
|
787
|
+
if (process.argv[2] === 'setup') {
|
|
788
|
+
const { main: setupMain } = await import('./setup.js');
|
|
789
|
+
await setupMain();
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
744
792
|
if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
|
|
745
793
|
console.error('\n╔══════════════════════════════════════════════════════════════╗');
|
|
746
794
|
console.error('║ 🚀 Google Search Console MCP Server ║');
|
|
747
795
|
console.error('╚══════════════════════════════════════════════════════════════╝\n');
|
|
748
796
|
console.error('❌ GOOGLE_APPLICATION_CREDENTIALS environment variable is not set.\n');
|
|
749
797
|
console.error('💡 To set up the server, run the setup wizard:');
|
|
750
|
-
console.error(' npx search-console-mcp
|
|
798
|
+
console.error(' npx -y search-console-mcp setup\n');
|
|
751
799
|
console.error('Alternatively, set the variable manually:');
|
|
752
800
|
console.error(' export GOOGLE_APPLICATION_CREDENTIALS=/path/to/your/key.json\n');
|
|
753
801
|
console.error('─'.repeat(64) + '\n');
|
package/dist/setup.js
CHANGED
|
@@ -38,7 +38,10 @@ function printInfo(text) {
|
|
|
38
38
|
function validateKeyFile(path) {
|
|
39
39
|
try {
|
|
40
40
|
const sanitizedPath = path.trim().replace(/\0/g, '');
|
|
41
|
-
const
|
|
41
|
+
const expandedPath = sanitizedPath.startsWith('~')
|
|
42
|
+
? sanitizedPath.replace('~', homedir())
|
|
43
|
+
: sanitizedPath;
|
|
44
|
+
const fullPath = resolve(expandedPath);
|
|
42
45
|
if (!existsSync(fullPath)) {
|
|
43
46
|
printError(`File not found: ${fullPath}`);
|
|
44
47
|
return null;
|
|
@@ -145,7 +148,7 @@ export function resolveRepo(dirname) {
|
|
|
145
148
|
}
|
|
146
149
|
return repo;
|
|
147
150
|
}
|
|
148
|
-
async function main() {
|
|
151
|
+
export async function main() {
|
|
149
152
|
printHeader();
|
|
150
153
|
console.log('This wizard will help you set up Search Console MCP.');
|
|
151
154
|
console.log('You will need a Google Cloud service account with Search Console access.\n');
|
|
@@ -215,16 +218,17 @@ async function main() {
|
|
|
215
218
|
}
|
|
216
219
|
// Step 5: Support the project
|
|
217
220
|
printStep(5, 'Support this project');
|
|
218
|
-
const answer = await ask('Would you like to star the repo on GitHub? (
|
|
219
|
-
if (answer.toLowerCase().startsWith('y')) {
|
|
221
|
+
const answer = await ask('Would you like to star the repo on GitHub? (Y/n): ');
|
|
222
|
+
if (answer === '' || answer.toLowerCase().startsWith('y')) {
|
|
220
223
|
try {
|
|
221
224
|
const repo = resolveRepo(__dirname);
|
|
222
|
-
|
|
225
|
+
// Harden against command injection: ensure repo is in 'owner/repo' format
|
|
226
|
+
if (repo && /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(repo)) {
|
|
223
227
|
execSync(`gh api -X PUT /user/starred/${repo}`, { stdio: 'ignore' });
|
|
224
228
|
printSuccess('Thanks for your support! ⭐');
|
|
225
229
|
}
|
|
226
230
|
else {
|
|
227
|
-
throw new Error('
|
|
231
|
+
throw new Error('Invalid repository format');
|
|
228
232
|
}
|
|
229
233
|
}
|
|
230
234
|
catch (error) {
|
|
@@ -3,14 +3,38 @@ import { queryAnalytics, detectAnomalies } from './analytics.js';
|
|
|
3
3
|
* Historical Google Algorithm Update dates (recent notable ones)
|
|
4
4
|
*/
|
|
5
5
|
const ALGORITHM_UPDATES = [
|
|
6
|
+
// 2022
|
|
7
|
+
{ date: '2022-09-12', name: 'September 2022 Core Update' },
|
|
8
|
+
{ date: '2022-09-20', name: 'September 2022 Product Review Update' },
|
|
9
|
+
{ date: '2022-10-19', name: 'October 2022 Spam Update' },
|
|
10
|
+
{ date: '2022-12-05', name: 'December 2022 Helpful Content Update' },
|
|
11
|
+
{ date: '2022-12-14', name: 'December 2022 Link Spam Update' },
|
|
12
|
+
// 2023
|
|
13
|
+
{ date: '2023-02-21', name: 'February 2023 Product Reviews Update' },
|
|
14
|
+
{ date: '2023-03-15', name: 'March 2023 Core Update' },
|
|
15
|
+
{ date: '2023-04-12', name: 'April 2023 Reviews Update' },
|
|
16
|
+
{ date: '2023-08-22', name: 'August 2023 Core Update' },
|
|
6
17
|
{ date: '2023-09-14', name: 'September 2023 Helpful Content Update' },
|
|
18
|
+
{ date: '2023-10-04', name: 'October 2023 Spam Update' },
|
|
7
19
|
{ date: '2023-10-05', name: 'October 2023 Core Update' },
|
|
8
20
|
{ date: '2023-11-02', name: 'November 2023 Core Update' },
|
|
21
|
+
{ date: '2023-11-08', name: 'November 2023 Reviews Update' },
|
|
22
|
+
// 2024
|
|
9
23
|
{ date: '2024-03-05', name: 'March 2024 Core Update' },
|
|
24
|
+
{ date: '2024-05-06', name: 'Site Reputation Abuse (Manual Actions)' },
|
|
25
|
+
{ date: '2024-05-14', name: 'AI Overviews Rollout' },
|
|
10
26
|
{ date: '2024-06-20', name: 'June 2024 Spam Update' },
|
|
11
27
|
{ date: '2024-08-15', name: 'August 2024 Core Update' },
|
|
12
28
|
{ date: '2024-11-11', name: 'November 2024 Core Update' },
|
|
13
|
-
{ date: '
|
|
29
|
+
{ date: '2024-12-12', name: 'December 2024 Core Update' },
|
|
30
|
+
{ date: '2024-12-19', name: 'December 2024 Spam Update' },
|
|
31
|
+
// 2025
|
|
32
|
+
{ date: '2025-03-13', name: 'March 2025 Core Update' },
|
|
33
|
+
{ date: '2025-06-30', name: 'June 2025 Core Update' },
|
|
34
|
+
{ date: '2025-08-26', name: 'August 2025 Spam Update' },
|
|
35
|
+
{ date: '2025-12-11', name: 'December 2025 Core Update' },
|
|
36
|
+
// 2026
|
|
37
|
+
{ date: '2026-02-05', name: 'February 2026 Discover Core Update' },
|
|
14
38
|
];
|
|
15
39
|
/**
|
|
16
40
|
* Identify the cause of a significant traffic drop by analyzing device distribution and algorithm updates.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { queryAnalytics } from './analytics.js';
|
|
2
|
+
import { safeTestBatch } from '../utils/regex.js';
|
|
2
3
|
/**
|
|
3
4
|
* Find "low-hanging fruit" keywords: high impressions, low CTR, and positions in striking distance.
|
|
4
5
|
*
|
|
@@ -284,18 +285,18 @@ export async function analyzeBrandVsNonBrand(siteUrl, brandRegexString, options
|
|
|
284
285
|
dimensions: ['query'],
|
|
285
286
|
limit: 10000
|
|
286
287
|
});
|
|
287
|
-
const
|
|
288
|
+
const queries = rows.map(row => row.keys?.[0] ?? '');
|
|
289
|
+
const isBrandResults = safeTestBatch(brandRegexString, 'i', queries);
|
|
288
290
|
const brandStats = { clicks: 0, impressions: 0, ctr: 0, position: 0, queryCount: 0, weightedPos: 0 };
|
|
289
291
|
const nonBrandStats = { clicks: 0, impressions: 0, ctr: 0, position: 0, queryCount: 0, weightedPos: 0 };
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
const isBrand = brandRegex.test(query);
|
|
292
|
+
rows.forEach((row, index) => {
|
|
293
|
+
const isBrand = isBrandResults[index];
|
|
293
294
|
const stats = isBrand ? brandStats : nonBrandStats;
|
|
294
295
|
stats.clicks += row.clicks ?? 0;
|
|
295
296
|
stats.impressions += row.impressions ?? 0;
|
|
296
297
|
stats.weightedPos += (row.position ?? 0) * (row.impressions ?? 0);
|
|
297
298
|
stats.queryCount++;
|
|
298
|
-
}
|
|
299
|
+
});
|
|
299
300
|
const calc = (stats, segment) => ({
|
|
300
301
|
segment,
|
|
301
302
|
clicks: stats.clicks,
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* SEO Primitives: Atomic functions for building higher-level SEO agents.
|
|
3
|
-
*/
|
|
1
|
+
import { safeTest } from '../utils/regex.js';
|
|
4
2
|
/**
|
|
5
3
|
* Categorize a ranking position into a discrete bucket (Top 3, Page 1, etc.).
|
|
6
4
|
*
|
|
@@ -63,19 +61,12 @@ export function calculateTrafficDelta(current, previous) {
|
|
|
63
61
|
* @returns A brand detection result.
|
|
64
62
|
*/
|
|
65
63
|
export function isBrandQuery(query, brandRegexString) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
matchedPattern: isBrand ? brandRegexString : undefined
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
catch (e) {
|
|
76
|
-
// Invalid regex fallback
|
|
77
|
-
return { query, isBrand: false };
|
|
78
|
-
}
|
|
64
|
+
const isBrand = safeTest(brandRegexString, 'i', query);
|
|
65
|
+
return {
|
|
66
|
+
query,
|
|
67
|
+
isBrand,
|
|
68
|
+
matchedPattern: isBrand ? brandRegexString : undefined
|
|
69
|
+
};
|
|
79
70
|
}
|
|
80
71
|
/**
|
|
81
72
|
* Check if two pages are potentially cannibalizing each other for the same query.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as sites from './sites.js';
|
|
2
|
+
import * as sitemaps from './sitemaps.js';
|
|
3
|
+
import * as analytics from './analytics.js';
|
|
4
|
+
/**
|
|
5
|
+
* Run a health check on a single site, returning a structured report.
|
|
6
|
+
*
|
|
7
|
+
* @param siteUrl - The site URL to check.
|
|
8
|
+
* @returns A health report for the site.
|
|
9
|
+
*/
|
|
10
|
+
async function checkSite(siteUrl) {
|
|
11
|
+
const issues = [];
|
|
12
|
+
// Run all checks in parallel
|
|
13
|
+
const [siteInfo, sitemapList, comparison, anomalies] = await Promise.all([
|
|
14
|
+
sites.getSite(siteUrl).catch(() => null),
|
|
15
|
+
sitemaps.listSitemaps(siteUrl).catch(() => []),
|
|
16
|
+
getWeekOverWeekComparison(siteUrl),
|
|
17
|
+
analytics.detectAnomalies(siteUrl, { days: 14, threshold: 2.5 }).catch(() => []),
|
|
18
|
+
]);
|
|
19
|
+
const permissionLevel = siteInfo?.permissionLevel ?? 'unknown';
|
|
20
|
+
// --- Analyze performance ---
|
|
21
|
+
const { period1, period2, changes } = comparison;
|
|
22
|
+
if (changes.clicksPercent <= -30) {
|
|
23
|
+
issues.push(`Critical traffic drop: clicks down ${Math.abs(changes.clicksPercent).toFixed(1)}% week-over-week`);
|
|
24
|
+
}
|
|
25
|
+
else if (changes.clicksPercent <= -15) {
|
|
26
|
+
issues.push(`Traffic declining: clicks down ${Math.abs(changes.clicksPercent).toFixed(1)}% week-over-week`);
|
|
27
|
+
}
|
|
28
|
+
if (changes.impressionsPercent <= -30) {
|
|
29
|
+
issues.push(`Critical visibility drop: impressions down ${Math.abs(changes.impressionsPercent).toFixed(1)}% week-over-week`);
|
|
30
|
+
}
|
|
31
|
+
else if (changes.impressionsPercent <= -15) {
|
|
32
|
+
issues.push(`Visibility declining: impressions down ${Math.abs(changes.impressionsPercent).toFixed(1)}% week-over-week`);
|
|
33
|
+
}
|
|
34
|
+
if (changes.position > 3) {
|
|
35
|
+
issues.push(`Average position worsened by ${changes.position.toFixed(1)} positions`);
|
|
36
|
+
}
|
|
37
|
+
if (period1.clicks === 0 && period1.impressions === 0) {
|
|
38
|
+
issues.push('No traffic data for the current period — site may not be receiving any search traffic');
|
|
39
|
+
}
|
|
40
|
+
// --- Analyze sitemaps ---
|
|
41
|
+
let sitemapsWithErrors = 0;
|
|
42
|
+
let sitemapsWithWarnings = 0;
|
|
43
|
+
const sitemapDetails = sitemapList.map(sm => {
|
|
44
|
+
const hasErrors = (Number(sm.errors) || 0) > 0;
|
|
45
|
+
const isPending = sm.isPending ?? false;
|
|
46
|
+
if (hasErrors)
|
|
47
|
+
sitemapsWithErrors++;
|
|
48
|
+
if ((Number(sm.warnings) || 0) > 0)
|
|
49
|
+
sitemapsWithWarnings++;
|
|
50
|
+
return {
|
|
51
|
+
path: sm.path ?? 'unknown',
|
|
52
|
+
type: sm.type ?? undefined,
|
|
53
|
+
isPending,
|
|
54
|
+
hasErrors,
|
|
55
|
+
lastDownloaded: sm.lastDownloaded ?? undefined,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
if (sitemapList.length === 0) {
|
|
59
|
+
issues.push('No sitemaps submitted — consider submitting a sitemap for better crawling');
|
|
60
|
+
}
|
|
61
|
+
if (sitemapsWithErrors > 0) {
|
|
62
|
+
issues.push(`${sitemapsWithErrors} sitemap(s) have errors`);
|
|
63
|
+
}
|
|
64
|
+
if (sitemapsWithWarnings > 0) {
|
|
65
|
+
issues.push(`${sitemapsWithWarnings} sitemap(s) have warnings`);
|
|
66
|
+
}
|
|
67
|
+
// --- Analyze anomalies ---
|
|
68
|
+
const drops = anomalies.filter(a => a.type === 'drop');
|
|
69
|
+
if (drops.length > 0) {
|
|
70
|
+
issues.push(`${drops.length} traffic anomaly drop(s) detected in the last 14 days`);
|
|
71
|
+
}
|
|
72
|
+
// --- Determine overall status ---
|
|
73
|
+
let status = 'healthy';
|
|
74
|
+
if (issues.some(i => i.startsWith('Critical') || i.includes('No traffic data'))) {
|
|
75
|
+
status = 'critical';
|
|
76
|
+
}
|
|
77
|
+
else if (issues.length > 0) {
|
|
78
|
+
status = 'warning';
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
siteUrl,
|
|
82
|
+
status,
|
|
83
|
+
permissionLevel,
|
|
84
|
+
performance: {
|
|
85
|
+
current: period1,
|
|
86
|
+
previous: period2,
|
|
87
|
+
changes,
|
|
88
|
+
},
|
|
89
|
+
sitemaps: {
|
|
90
|
+
total: sitemapList.length,
|
|
91
|
+
withErrors: sitemapsWithErrors,
|
|
92
|
+
withWarnings: sitemapsWithWarnings,
|
|
93
|
+
details: sitemapDetails,
|
|
94
|
+
},
|
|
95
|
+
anomalies,
|
|
96
|
+
issues,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get a week-over-week performance comparison for a site.
|
|
101
|
+
*/
|
|
102
|
+
async function getWeekOverWeekComparison(siteUrl) {
|
|
103
|
+
const DATA_DELAY_DAYS = 3;
|
|
104
|
+
const now = new Date();
|
|
105
|
+
const period1End = new Date(now);
|
|
106
|
+
period1End.setDate(period1End.getDate() - DATA_DELAY_DAYS);
|
|
107
|
+
const period1Start = new Date(period1End);
|
|
108
|
+
period1Start.setDate(period1Start.getDate() - 7);
|
|
109
|
+
const period2End = new Date(period1Start);
|
|
110
|
+
period2End.setDate(period2End.getDate() - 1);
|
|
111
|
+
const period2Start = new Date(period2End);
|
|
112
|
+
period2Start.setDate(period2Start.getDate() - 7);
|
|
113
|
+
const fmt = (d) => d.toISOString().split('T')[0];
|
|
114
|
+
return analytics.comparePeriods(siteUrl, fmt(period1Start), fmt(period1End), fmt(period2Start), fmt(period2End));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Run a health check on one or all verified sites.
|
|
118
|
+
*
|
|
119
|
+
* @param siteUrl - Optional. If provided, check only this site. If omitted, check all verified sites.
|
|
120
|
+
* @returns An array of health reports.
|
|
121
|
+
*/
|
|
122
|
+
export async function healthCheck(siteUrl) {
|
|
123
|
+
if (siteUrl) {
|
|
124
|
+
const report = await checkSite(siteUrl);
|
|
125
|
+
return [report];
|
|
126
|
+
}
|
|
127
|
+
const allSites = await sites.listSites();
|
|
128
|
+
if (allSites.length === 0) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
const reports = await Promise.all(allSites.map(site => checkSite(site.siteUrl)));
|
|
132
|
+
// Sort: critical first, then warning, then healthy
|
|
133
|
+
const order = { critical: 0, warning: 1, healthy: 2 };
|
|
134
|
+
return reports.sort((a, b) => order[a.status] - order[b.status]);
|
|
135
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import RE2 from 're2';
|
|
2
|
+
/**
|
|
3
|
+
* Executes a regex test using Google's RE2 engine to prevent ReDoS.
|
|
4
|
+
* RE2 uses a DFA-based approach that guarantees linear time execution.
|
|
5
|
+
*
|
|
6
|
+
* @param pattern The regex pattern string
|
|
7
|
+
* @param flags The regex flags (e.g., 'i', 'g')
|
|
8
|
+
* @param text The text to test
|
|
9
|
+
* @returns boolean indicating if the pattern matches the text
|
|
10
|
+
*/
|
|
11
|
+
export function safeTest(pattern, flags, text) {
|
|
12
|
+
try {
|
|
13
|
+
const re = new RE2(pattern, flags);
|
|
14
|
+
return re.test(text);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
console.warn(`Regex evaluation failed for pattern: ${pattern}. Error: ${e}`);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Executes a regex test on multiple strings using RE2.
|
|
23
|
+
*
|
|
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
|
|
28
|
+
*/
|
|
29
|
+
export function safeTestBatch(pattern, flags, texts) {
|
|
30
|
+
if (texts.length === 0)
|
|
31
|
+
return [];
|
|
32
|
+
try {
|
|
33
|
+
const re = new RE2(pattern, flags);
|
|
34
|
+
return texts.map(t => re.test(t));
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.warn(`Batch regex evaluation failed for pattern: ${pattern}. Error: ${e}`);
|
|
38
|
+
// Default to no matches if it fails
|
|
39
|
+
return new Array(texts.length).fill(false);
|
|
40
|
+
}
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "search-console-mcp",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.3",
|
|
4
4
|
"mcpName": "io.github.saurabhsharma2u/search-console-mcp",
|
|
5
5
|
"description": "MCP server for Google Search Console API",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"bin": {
|
|
9
|
-
"search-console-mcp": "./dist/index.js"
|
|
10
|
-
"search-console-mcp-setup": "./dist/setup.js"
|
|
9
|
+
"search-console-mcp": "./dist/index.js"
|
|
11
10
|
},
|
|
12
11
|
"files": [
|
|
13
12
|
"dist",
|
|
@@ -35,13 +34,15 @@
|
|
|
35
34
|
"cheerio": "^1.2.0",
|
|
36
35
|
"dotenv": "^17.2.3",
|
|
37
36
|
"googleapis": "^171.0.0",
|
|
37
|
+
"re2": "^1.23.3",
|
|
38
38
|
"zod": "^4.3.6"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "^25.2.0",
|
|
42
|
+
"@types/re2": "^1.10.0",
|
|
42
43
|
"@vitest/coverage-v8": "^4.0.18",
|
|
43
44
|
"typedoc": "^0.28.16",
|
|
44
45
|
"typescript": "^5.9.3",
|
|
45
46
|
"vitest": "^4.0.18"
|
|
46
47
|
}
|
|
47
|
-
}
|
|
48
|
+
}
|