@digestseo/mcp-gsc 0.4.11 → 0.4.12
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 +7 -0
- package/README.md +5 -3
- package/SECURITY.md +1 -1
- package/package.json +1 -1
- package/src/google.ts +50 -20
- package/src/index.ts +21 -8
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.12] - 2026-09-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `urls.inspect_many` now runs URL Inspection requests with bounded concurrency of three instead of strictly sequentially, reducing batch latency while preserving per-URL safety-budget accounting, input-order results, per-URL failures, and batch-fatal Google-access revocation.
|
|
14
|
+
- Search Analytics now rejects `searchAppearance` combined with another grouping dimension and documents Google's required two-step appearance discovery/filter workflow instead of sending invalid grouped requests to the provider.
|
|
15
|
+
- Brand/non-brand Search Analytics guidance now distinguishes caller-supplied regex segmentation from Search Console's AI-assisted native Branded/Non-branded filter, which the Search Analytics API does not expose.
|
|
16
|
+
|
|
10
17
|
## [0.4.11] - 2026-09-22
|
|
11
18
|
|
|
12
19
|
### Fixed
|
package/README.md
CHANGED
|
@@ -75,10 +75,10 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 21 tools. Read-onl
|
|
|
75
75
|
| **`server.capabilities`** | Read | List every tool this server exposes and report whether your Google connection is currently authenticated (`connected` / `not_connected`). Takes no arguments — a good first call for discovery. |
|
|
76
76
|
| **`sites.list`** | Read | List the Search Console properties the connected Google account can access (`siteUrl`, `permissionLevel`). |
|
|
77
77
|
| **`sites.get`** | Read | Retrieve one exact Search Console property and the connected account's permission level for it. |
|
|
78
|
-
| **`analytics.query`** | Read | Impressions, clicks, CTR, and average position over a date range, with dimensions, filters, safe pagination, and selectable search type. Pass `dimensions: []` for true site totals; Google's aggregate row may omit `keys`. Search Analytics does not guarantee every data row and can return only top rows, so exhausting local pagination is not proof that the provider dataset is exhaustive. For `search_type: "discover"` or `"googleNews"`, `position_supported` is `false`; those reports expose clicks, impressions, and CTR but not average position, and query grouping/filtering is unavailable. |
|
|
78
|
+
| **`analytics.query`** | Read | Impressions, clicks, CTR, and average position over a date range, with dimensions, filters, safe pagination, and selectable search type. Pass `dimensions: []` for true site totals; Google's aggregate row may omit `keys`. Search Analytics does not guarantee every data row and can return only top rows, so exhausting local pagination is not proof that the provider dataset is exhaustive. Search appearance discovery is a two-step workflow: group by `searchAppearance` alone to discover the exact values Google returns for the property, then filter to one returned value in a separate query when grouping by page/query/country/device/date/hour. For `search_type: "discover"` or `"googleNews"`, `position_supported` is `false`; those reports expose clicks, impressions, and CTR but not average position, and query grouping/filtering is unavailable. |
|
|
79
79
|
| **`insights.page_queries`** / **`insights.query_pages`** | Read | Drill from one exact page to its Search Console queries, or from one exact query to the pages Google surfaced for it, with `row_limit` / `start_row` pagination. These query-centric helpers support web, image, video, and the Search results News tab; Discover and the Google News report are excluded because they do not expose query data. |
|
|
80
80
|
| **`urls.inspect`** | Read | Google's URL Inspection report for the version of one page currently known in Google's index. It is not a live URL test; mobile-usability output is deprecated. |
|
|
81
|
-
| **`urls.inspect_many`** | Read | Inspect Google's indexed versions of up to 10 URLs
|
|
81
|
+
| **`urls.inspect_many`** | Read | Inspect Google's indexed versions of up to 10 URLs with bounded concurrency of 3 in one call while preserving input-order results. Each URL still consumes one Google URL Inspection request and one unit of the server's shared inspection safety budget; this does not run live URL tests. |
|
|
82
82
|
| **`sitemaps.list`** / **`sitemaps.get`** | Read | List submitted sitemaps or retrieve one sitemap's details. `lastSubmitted` is when the sitemap was submitted to Search Console; `lastDownloaded` is when Google last downloaded the sitemap. Neither value is the sitemap file's modification/deploy time or a page crawl/indexing timestamp. |
|
|
83
83
|
| **`insights.quick_wins`** / **`insights.cannibalization`** / **`insights.content_decay`** | Read | Surface average-position opportunity rows, query/page overlap, and evidence-ranked click declines. Quick-win candidates are observed query/page rows selected by aggregate Search Console average position, not proof of a stable current rank, and CTR is context rather than an eligibility filter. Cannibalization candidate totals/shares are scoped to observed query/page rows rather than true query-level property totals; content-decay results compare only pages returned in both periods and do not turn one-sided row absence into zero traffic. |
|
|
84
84
|
| **`indexing.list_pages`** / **`analytics.compare`** | Read | Analyze pages receiving Search Console impressions and compare two periods, optionally using the same search type and query/page/country/device/search-appearance filters for both periods. `analytics.compare` compares only dimension keys returned in both period responses; a key absent from one non-exhaustive Search Analytics response is not assumed to have zero metrics. Because its comparison contract includes average position, it excludes Discover and Google News. `indexing.list_pages` is performance data, not index coverage: a missing URL may still be indexed; use `urls.inspect` / `urls.inspect_many` for URL-level index status. |
|
|
@@ -105,9 +105,11 @@ Once connected, ask your assistant things like:
|
|
|
105
105
|
- *"Which high-impression query/page rows have an average position between 5 and 15? Show me those optimization opportunities."*
|
|
106
106
|
- *"Give me a weekly digest for `sc-domain:example.com` ending today."*
|
|
107
107
|
- *"Do any of my sitemaps have errors or warnings?"*
|
|
108
|
-
- *"
|
|
108
|
+
- *"Using this brand regex I provide, approximate my clicks as brand vs non-brand."*
|
|
109
109
|
- *"How is my site doing in Google Discover vs regular web search?"*
|
|
110
110
|
|
|
111
|
+
Query regexes are a manual approximation only. Search Console's native Branded/Non-branded filter uses Google's own AI-assisted classification (including language variants, typos, and associated products/services), and that native classifier is not exposed as a Search Analytics API filter.
|
|
112
|
+
|
|
111
113
|
## Connect in your AI client
|
|
112
114
|
|
|
113
115
|
Once you've deployed the server (see **[SETUP.md](SETUP.md)**), connect it by pasting your Worker's `/mcp` URL into your client:
|
package/SECURITY.md
CHANGED
|
@@ -58,7 +58,7 @@ delay and does not disclose another user's usage.
|
|
|
58
58
|
| Category | Tools | Limit |
|
|
59
59
|
|---|---|---|
|
|
60
60
|
| Search Analytics | `analytics.query`, `insights.quick_wins`, `insights.cannibalization`, `insights.content_decay`, `indexing.list_pages`, `analytics.compare` | 30 per user / 10 minutes |
|
|
61
|
-
| URL inspection | `urls.inspect`, `urls.inspect_many` | 20 inspected URLs per user / 24 hours; batch requests reserve one unit per URL |
|
|
61
|
+
| URL inspection | `urls.inspect`, `urls.inspect_many` | 20 inspected URLs per user / 24 hours; batch requests reserve one unit per URL and run at most 3 inspections concurrently |
|
|
62
62
|
| Search Console writes | `sites.add`, `sites.delete`, `sitemaps.submit`, `sitemaps.delete` | 10 per user / hour |
|
|
63
63
|
| Indexing publish | `indexing.request` | 2 per user / 24 hours; 200 per deployment / 24 hours |
|
|
64
64
|
| Weekly digest | `reports.weekly_digest` | 6 per user / hour |
|
package/package.json
CHANGED
package/src/google.ts
CHANGED
|
@@ -309,6 +309,12 @@ export function assertSearchAnalyticsQueryCompatible(body: SearchAnalyticsQuery)
|
|
|
309
309
|
throw new Error('Search Analytics dimensions must not contain duplicates.');
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
if (body.dimensions.includes('searchAppearance') && body.dimensions.length > 1) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
'Search Analytics searchAppearance must be the only grouping dimension. Discover an appearance value with dimensions ["searchAppearance"], then filter by that value in a separate query when grouping by page, query, country, device, date, or hour.',
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
312
318
|
const hasHourDimension = body.dimensions.includes('hour');
|
|
313
319
|
const usesHourlyData = body.dataState === 'hourly_all';
|
|
314
320
|
if (hasHourDimension && !usesHourlyData) {
|
|
@@ -467,7 +473,9 @@ export interface UrlInspectionBatchResult {
|
|
|
467
473
|
error?: string;
|
|
468
474
|
}
|
|
469
475
|
|
|
470
|
-
export
|
|
476
|
+
export const URL_INSPECTION_BATCH_CONCURRENCY = 3;
|
|
477
|
+
|
|
478
|
+
export async function inspectUrlsBoundedConcurrently(
|
|
471
479
|
accessToken: string,
|
|
472
480
|
siteUrl: string,
|
|
473
481
|
inspectionUrls: readonly string[],
|
|
@@ -475,31 +483,53 @@ export async function inspectUrlsSequentially(
|
|
|
475
483
|
): Promise<UrlInspectionBatchResult[]> {
|
|
476
484
|
const results: UrlInspectionBatchResult[] = [];
|
|
477
485
|
|
|
478
|
-
for (
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
486
|
+
for (
|
|
487
|
+
let offset = 0;
|
|
488
|
+
offset < inspectionUrls.length;
|
|
489
|
+
offset += URL_INSPECTION_BATCH_CONCURRENCY
|
|
490
|
+
) {
|
|
491
|
+
const chunk = inspectionUrls.slice(offset, offset + URL_INSPECTION_BATCH_CONCURRENCY);
|
|
492
|
+
const chunkResults = await Promise.all(
|
|
493
|
+
chunk.map(async (inspectionUrl): Promise<UrlInspectionBatchResult | Error> => {
|
|
494
|
+
try {
|
|
495
|
+
return {
|
|
496
|
+
inspectionUrl,
|
|
497
|
+
inspectionResult: await inspectUrl(
|
|
498
|
+
accessToken,
|
|
499
|
+
siteUrl,
|
|
500
|
+
inspectionUrl,
|
|
501
|
+
languageCode,
|
|
502
|
+
),
|
|
503
|
+
};
|
|
504
|
+
} catch (error) {
|
|
505
|
+
if (error instanceof Error && error.message === GSC_ACCESS_REVOKED_MESSAGE) {
|
|
506
|
+
return error;
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
inspectionUrl,
|
|
510
|
+
error: error instanceof Error ? error.message : 'URL inspection failed.',
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
}),
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const accessRevoked = chunkResults.find((result): result is Error => result instanceof Error);
|
|
517
|
+
if (accessRevoked) {
|
|
518
|
+
throw accessRevoked;
|
|
497
519
|
}
|
|
520
|
+
results.push(
|
|
521
|
+
...chunkResults.filter(
|
|
522
|
+
(result): result is UrlInspectionBatchResult => !(result instanceof Error),
|
|
523
|
+
),
|
|
524
|
+
);
|
|
498
525
|
}
|
|
499
526
|
|
|
500
527
|
return results;
|
|
501
528
|
}
|
|
502
529
|
|
|
530
|
+
// Kept for compatibility with callers that imported the original helper directly.
|
|
531
|
+
export const inspectUrlsSequentially = inspectUrlsBoundedConcurrently;
|
|
532
|
+
|
|
503
533
|
export interface SitemapEntry {
|
|
504
534
|
path: string;
|
|
505
535
|
lastSubmitted?: string | null;
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
GSC_ACCESS_REVOKED_MESSAGE,
|
|
13
13
|
MCP_RECONNECT_INSTRUCTION,
|
|
14
14
|
inspectUrl,
|
|
15
|
-
|
|
15
|
+
inspectUrlsBoundedConcurrently,
|
|
16
|
+
URL_INSPECTION_BATCH_CONCURRENCY,
|
|
16
17
|
listSitemaps,
|
|
17
18
|
listSites,
|
|
18
19
|
getSite,
|
|
@@ -589,7 +590,7 @@ const TOOL_CATALOG = [
|
|
|
589
590
|
{
|
|
590
591
|
name: 'urls.inspect_many',
|
|
591
592
|
description:
|
|
592
|
-
|
|
593
|
+
`Inspect Google's indexed versions of up to 10 URLs with bounded concurrency of ${URL_INSPECTION_BATCH_CONCURRENCY} in one MCP call while charging the same URL Inspection safety budget per URL. This does not run live URL tests.`,
|
|
593
594
|
},
|
|
594
595
|
{
|
|
595
596
|
name: 'sitemaps.list',
|
|
@@ -836,7 +837,7 @@ class GscMcpRuntime {
|
|
|
836
837
|
'urls.inspect_many',
|
|
837
838
|
{
|
|
838
839
|
title: 'Inspect multiple URLs',
|
|
839
|
-
description: `Inspect the versions of up to 10 URLs currently known in Google's index from one Search Console property. This API does not run live URL tests. Google still processes one URL Inspection request per URL, so every requested URL consumes one quota unit and one unit of this server's shared URL-inspection safety budget. Requests
|
|
840
|
+
description: `Inspect the versions of up to 10 URLs currently known in Google's index from one Search Console property. This API does not run live URL tests. Google still processes one URL Inspection request per URL, so every requested URL consumes one quota unit and one unit of this server's shared URL-inspection safety budget. Requests use bounded concurrency of ${URL_INSPECTION_BATCH_CONCURRENCY}; results preserve input order, and a Google-access revocation stops later chunks from starting. Use this for a small group of important or debugging-target URLs; do not use it to crawl an entire site.`,
|
|
840
841
|
inputSchema: {
|
|
841
842
|
site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
|
|
842
843
|
inspection_urls: z
|
|
@@ -873,7 +874,7 @@ class GscMcpRuntime {
|
|
|
873
874
|
if (rateLimitError) return rateLimitError;
|
|
874
875
|
|
|
875
876
|
const accessToken = await this.getAccessToken(googleId);
|
|
876
|
-
const batchResults = await
|
|
877
|
+
const batchResults = await inspectUrlsBoundedConcurrently(
|
|
877
878
|
accessToken,
|
|
878
879
|
site_url,
|
|
879
880
|
inspection_urls,
|
|
@@ -985,10 +986,22 @@ class GscMcpRuntime {
|
|
|
985
986
|
' data_state to "hourly_all". The hourly date range can span at',
|
|
986
987
|
' most 10 inclusive days, and hourly data',
|
|
987
988
|
' is preliminary.',
|
|
989
|
+
'- SEARCH APPEARANCE uses a two-step workflow. To discover the',
|
|
990
|
+
' property-specific appearance values Google currently exposes,',
|
|
991
|
+
' call with dimensions: ["searchAppearance"] and no other grouping',
|
|
992
|
+
' dimension. Then filter to one exact returned searchAppearance',
|
|
993
|
+
' value in a separate query and group by page/query/country/device/',
|
|
994
|
+
' date/hour as needed. Do not guess new appearance identifiers;',
|
|
995
|
+
' discover the values Google returns for the property first.',
|
|
988
996
|
'- Use dimension_filter_groups to filter by country, device, query',
|
|
989
997
|
' content, page URL, or search feature. includingRegex and',
|
|
990
|
-
' excludingRegex use RE2 syntax.
|
|
991
|
-
|
|
998
|
+
' excludingRegex use RE2 syntax. A query regex can provide a manual',
|
|
999
|
+
' brand/non-brand approximation when the caller supplies the brand',
|
|
1000
|
+
" pattern. It is not equivalent to Search Console's native Branded/",
|
|
1001
|
+
' Non-branded filter: Google describes that classification as',
|
|
1002
|
+
' AI-assisted (including language variants, typos, and associated',
|
|
1003
|
+
' products/services), and the Search Analytics API does not expose',
|
|
1004
|
+
' that native classifier as a filter.',
|
|
992
1005
|
"- data_state defaults to 'all' which matches the GSC dashboard.",
|
|
993
1006
|
" Pass 'final' only when the user explicitly asks for stable,",
|
|
994
1007
|
' non-preliminary data.',
|
|
@@ -1013,7 +1026,7 @@ class GscMcpRuntime {
|
|
|
1013
1026
|
)
|
|
1014
1027
|
.default(['query'])
|
|
1015
1028
|
.describe(
|
|
1016
|
-
"Dimensions to group rows by. Pass [] (empty array) to get a single row of true site-level totals. The 'hour' dimension requires data_state 'hourly_all' and an inclusive date range of at most 10 days. Google Discover and Google News do not support the query dimension.",
|
|
1029
|
+
"Dimensions to group rows by. Pass [] (empty array) to get a single row of true site-level totals. searchAppearance must be the only grouping dimension; discover an appearance value with ['searchAppearance'], then filter to that value in a separate query when grouping by another dimension. The 'hour' dimension requires data_state 'hourly_all' and an inclusive date range of at most 10 days. Google Discover and Google News do not support the query dimension.",
|
|
1017
1030
|
),
|
|
1018
1031
|
row_limit: z
|
|
1019
1032
|
.number()
|
|
@@ -1732,7 +1745,7 @@ class GscMcpRuntime {
|
|
|
1732
1745
|
)
|
|
1733
1746
|
.optional()
|
|
1734
1747
|
.describe(
|
|
1735
|
-
"Optional Search Console filters applied identically to both periods.
|
|
1748
|
+
"Optional Search Console filters applied identically to both periods. A caller-supplied query regex can approximate a manual brand/non-brand split, but it is not equivalent to Search Console's AI-assisted native Branded/Non-branded filter, which the Search Analytics API does not expose. Countries use ISO 3166-1 alpha-3 codes.",
|
|
1736
1749
|
),
|
|
1737
1750
|
limit: ANALYSIS_RESULT_LIMIT_SCHEMA,
|
|
1738
1751
|
start_row: RESULT_START_ROW_SCHEMA,
|