@digestseo/mcp-gsc 0.4.26 → 0.4.27
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/CHANGELOG.md +9 -0
- package/README.md +9 -1
- package/package.json +1 -1
- package/src/google.ts +26 -4
- package/src/index.ts +26 -13
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.27] - 2026-09-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- GitHub Copilot CLI can now install this repository directly as a plugin, bundling the existing `gsc-weekly-review`, `gsc-indexing-triage`, and `gsc-search-opportunities` Agent Skills with the verified local loopback MCP configuration.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Direct Search Analytics-backed pagination in `analytics.query` and `indexing.list_pages` now treats short non-empty provider pages as potentially continuable and advances `next_start_row` by Google's requested page size, stopping only after an explicit empty provider page when no local response truncation remains.
|
|
17
|
+
- Idempotent Google read calls now apply the existing bounded retry policy when Search Console returns documented transient 403 rate-limit reasons, while permission/configuration 403 responses and write calls remain non-retried.
|
|
18
|
+
|
|
10
19
|
## [0.4.26] - 2026-09-23
|
|
11
20
|
|
|
12
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -58,11 +58,19 @@ claude mcp add --transport http gsc http://127.0.0.1:8080/mcp
|
|
|
58
58
|
|
|
59
59
|
When this repository is installed as a Cursor plugin, Cursor also auto-discovers three bundled Agent Skills that compose the MCP tools into evidence-safe workflows: `gsc-weekly-review`, `gsc-indexing-triage`, and `gsc-search-opportunities`. The **Add to Cursor** button above installs the MCP connection only; the bundled skills are part of the repository's Cursor plugin package.
|
|
60
60
|
|
|
61
|
+
**GitHub Copilot CLI**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
copilot plugin install AKzar1el/mcp-gsc
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The Copilot plugin bundles the same three Agent Skills plus the local loopback MCP configuration. Start the npm launcher before using the plugin's MCP server; the plugin does not start or deploy mcp-gsc for you.
|
|
68
|
+
|
|
61
69
|
**Kiro**
|
|
62
70
|
|
|
63
71
|
[](https://kiro.dev/launch/mcp/add?name=mcp-gsc&config=%7B%22url%22%3A%22http%3A%2F%2F127.0.0.1%3A8080%2Fmcp%22%2C%22disabled%22%3Afalse%2C%22autoApprove%22%3A%5B%5D%7D)
|
|
64
72
|
|
|
65
|
-
Start the npm launcher before using either install button. Complete its Google OAuth flow on first use.
|
|
73
|
+
Start the npm launcher before using either install button or the Copilot plugin. Complete its Google OAuth flow on first use.
|
|
66
74
|
|
|
67
75
|
**ChatGPT** — ChatGPT cannot connect directly to this loopback `127.0.0.1` endpoint. Use OpenAI's **Secure MCP Tunnel** for the local launcher, or deploy your own remote Cloudflare Worker and add that `/mcp` URL as a custom MCP app in Developer mode; see [Connect in your AI client](#connect-in-your-ai-client) below.
|
|
68
76
|
|
package/package.json
CHANGED
package/src/google.ts
CHANGED
|
@@ -52,10 +52,34 @@ export class GoogleRefreshTokenRevokedError extends Error {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
const GOOGLE_READ_RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
|
|
55
|
+
const GOOGLE_READ_RETRYABLE_403_REASONS = new Set([
|
|
56
|
+
'concurrentLimitExceeded',
|
|
57
|
+
'rateLimitExceeded',
|
|
58
|
+
'servingLimitExceeded',
|
|
59
|
+
'userRateLimitExceeded',
|
|
60
|
+
]);
|
|
55
61
|
const GOOGLE_READ_MAX_ATTEMPTS = 3;
|
|
56
62
|
const GOOGLE_READ_BASE_DELAY_MS = 1_000;
|
|
57
63
|
const GOOGLE_READ_MAX_DELAY_MS = 5_000;
|
|
58
64
|
|
|
65
|
+
async function isGoogleReadRetryableResponse(response: Response): Promise<boolean> {
|
|
66
|
+
if (GOOGLE_READ_RETRYABLE_STATUSES.has(response.status)) return true;
|
|
67
|
+
if (response.status !== 403) return false;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const data = (await response.clone().json()) as {
|
|
71
|
+
error?: { errors?: Array<{ reason?: unknown }> };
|
|
72
|
+
};
|
|
73
|
+
return data.error?.errors?.some(
|
|
74
|
+
(entry) =>
|
|
75
|
+
typeof entry.reason === 'string' &&
|
|
76
|
+
GOOGLE_READ_RETRYABLE_403_REASONS.has(entry.reason),
|
|
77
|
+
) ?? false;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
function googleReadRetryDelayMs(response: Response, retryNumber: number): number | null {
|
|
60
84
|
const retryAfter = response.headers.get('retry-after')?.trim();
|
|
61
85
|
if (retryAfter) {
|
|
@@ -79,10 +103,8 @@ async function fetchGoogleRead(url: string, init?: RequestInit): Promise<Respons
|
|
|
79
103
|
for (let attempt = 1; attempt <= GOOGLE_READ_MAX_ATTEMPTS; attempt += 1) {
|
|
80
104
|
try {
|
|
81
105
|
const response = await fetch(url, init);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
attempt === GOOGLE_READ_MAX_ATTEMPTS
|
|
85
|
-
) {
|
|
106
|
+
const retryable = await isGoogleReadRetryableResponse(response);
|
|
107
|
+
if (!retryable || attempt === GOOGLE_READ_MAX_ATTEMPTS) {
|
|
86
108
|
return response;
|
|
87
109
|
}
|
|
88
110
|
|
package/src/index.ts
CHANGED
|
@@ -611,22 +611,30 @@ function windowSourceRows<T>(
|
|
|
611
611
|
sourceStartRow: number,
|
|
612
612
|
requestedLimit: number,
|
|
613
613
|
sourceMayHaveMore: boolean,
|
|
614
|
+
sourceNextStartRow?: number,
|
|
614
615
|
) {
|
|
615
616
|
const bounded = takeBoundedItems(items, requestedLimit);
|
|
616
617
|
const returnedCount = bounded.items.length;
|
|
617
|
-
const
|
|
618
|
-
bounded.byteLimitReached ||
|
|
619
|
-
|
|
620
|
-
|
|
618
|
+
const localContinuation =
|
|
619
|
+
bounded.byteLimitReached || returnedCount < items.length;
|
|
620
|
+
const hasMore = localContinuation || sourceMayHaveMore;
|
|
621
|
+
const resultPage = resultPageMetadata({
|
|
622
|
+
startRow: sourceStartRow,
|
|
623
|
+
limit: requestedLimit,
|
|
624
|
+
returnedCount,
|
|
625
|
+
hasMore,
|
|
626
|
+
byteLimitReached: bounded.byteLimitReached,
|
|
627
|
+
});
|
|
628
|
+
if (
|
|
629
|
+
!localContinuation &&
|
|
630
|
+
sourceMayHaveMore &&
|
|
631
|
+
sourceNextStartRow !== undefined
|
|
632
|
+
) {
|
|
633
|
+
resultPage.next_start_row = sourceNextStartRow;
|
|
634
|
+
}
|
|
621
635
|
return {
|
|
622
636
|
items: bounded.items,
|
|
623
|
-
resultPage
|
|
624
|
-
startRow: sourceStartRow,
|
|
625
|
-
limit: requestedLimit,
|
|
626
|
-
returnedCount,
|
|
627
|
-
hasMore,
|
|
628
|
-
byteLimitReached: bounded.byteLimitReached,
|
|
629
|
-
}),
|
|
637
|
+
resultPage,
|
|
630
638
|
};
|
|
631
639
|
}
|
|
632
640
|
|
|
@@ -1268,13 +1276,17 @@ class GscMcpRuntime {
|
|
|
1268
1276
|
? { dimensionFilterGroups: dimension_filter_groups }
|
|
1269
1277
|
: {}),
|
|
1270
1278
|
});
|
|
1279
|
+
// Search Analytics pagination is complete only after Google returns an
|
|
1280
|
+
// explicit empty page. A short non-empty page can still be followed by
|
|
1281
|
+
// later rows, so preserve a continuation for dimensioned queries.
|
|
1271
1282
|
const sourceMayHaveMore =
|
|
1272
|
-
dimensions.length > 0 && response.rows.length
|
|
1283
|
+
dimensions.length > 0 && response.rows.length > 0;
|
|
1273
1284
|
const bounded = windowSourceRows(
|
|
1274
1285
|
response.rows,
|
|
1275
1286
|
start_row,
|
|
1276
1287
|
row_limit,
|
|
1277
1288
|
sourceMayHaveMore,
|
|
1289
|
+
start_row + sourceRowLimit,
|
|
1278
1290
|
);
|
|
1279
1291
|
const payload: Record<string, unknown> = {
|
|
1280
1292
|
row_count: bounded.items.length,
|
|
@@ -1961,7 +1973,8 @@ class GscMcpRuntime {
|
|
|
1961
1973
|
pages,
|
|
1962
1974
|
start_row,
|
|
1963
1975
|
row_limit,
|
|
1964
|
-
response.rows.length
|
|
1976
|
+
response.rows.length > 0,
|
|
1977
|
+
start_row + sourceRowLimit,
|
|
1965
1978
|
);
|
|
1966
1979
|
|
|
1967
1980
|
const payload = {
|