@digestseo/mcp-gsc 0.4.2 → 0.4.4
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 +13 -1
- package/README.md +4 -4
- package/SETUP.md +1 -1
- package/package.json +2 -2
- package/src/google.ts +17 -0
- package/src/index.ts +39 -10
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,19 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.4.4] - 2026-09-21
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Search Analytics requests grouped by `hour` now require the documented `hourly_all` data state and reject hourly windows longer than Google's documented 10-day limit before calling the provider.
|
|
12
|
+
- Search Analytics outputs now distinguish local pagination completeness from Google's provider-level limitation that only top rows are guaranteed, preventing agents from treating a fully fetched local window as an exhaustive dataset.
|
|
13
|
+
|
|
14
|
+
## [0.4.3] - 2026-09-21
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- The documented and npm-declared Node.js minimum is now 22+, matching the bundled Wrangler 4 runtime requirement instead of advertising unsupported Node.js 20 installs.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- URL Inspection guidance now states that Google reports the version currently in its index rather than running a live URL test, and no longer presents the deprecated mobile-usability field as a current primary signal.
|
|
9
21
|
|
|
10
22
|
## [0.4.2] - 2026-09-21
|
|
11
23
|
|
package/README.md
CHANGED
|
@@ -73,10 +73,10 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 21 tools. Read-onl
|
|
|
73
73
|
| **`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. |
|
|
74
74
|
| **`sites.list`** | Read | List the Search Console properties the connected Google account can access (`siteUrl`, `permissionLevel`). |
|
|
75
75
|
| **`sites.get`** | Read | Retrieve one exact Search Console property and the connected account's permission level for it. |
|
|
76
|
-
| **`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`. |
|
|
76
|
+
| **`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. |
|
|
77
77
|
| **`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. |
|
|
78
|
-
| **`urls.inspect`** | Read | Google's URL Inspection report for
|
|
79
|
-
| **`urls.inspect_many`** | Read | Inspect up to 10 URLs sequentially in one call. Each URL still consumes one Google URL Inspection request and one unit of the server's shared inspection safety budget. |
|
|
78
|
+
| **`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. |
|
|
79
|
+
| **`urls.inspect_many`** | Read | Inspect Google's indexed versions of up to 10 URLs sequentially in one call. 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. |
|
|
80
80
|
| **`sitemaps.list`** / **`sitemaps.get`** | Read | List submitted sitemaps or retrieve one sitemap's details. |
|
|
81
81
|
| **`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 distinguish likely decay from weak evidence and improving visibility with click volatility. |
|
|
82
82
|
| **`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. `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. |
|
|
@@ -97,7 +97,7 @@ Once connected, ask your assistant things like:
|
|
|
97
97
|
|
|
98
98
|
- *"What are my top 20 queries by clicks in the last 28 days?"*
|
|
99
99
|
- *"Compare impressions for example.com this month vs last month — which pages dropped?"*
|
|
100
|
-
- *"
|
|
100
|
+
- *"What does Google's indexed version of `https://example.com/pricing` show for index status and last crawl?"*
|
|
101
101
|
- *"Which high-impression query/page rows have an average position between 5 and 15? Show me those optimization opportunities."*
|
|
102
102
|
- *"Give me a weekly digest for `sc-domain:example.com` ending today."*
|
|
103
103
|
- *"Do any of my sitemaps have errors or warnings?"*
|
package/SETUP.md
CHANGED
|
@@ -10,7 +10,7 @@ By the end you'll have a Worker at `https://<your-worker>.workers.dev/mcp` that
|
|
|
10
10
|
|
|
11
11
|
- A [Cloudflare account](https://dash.cloudflare.com/sign-up) (the free Workers plan is enough to start).
|
|
12
12
|
- A [Google account](https://accounts.google.com/) with access to the Search Console properties you want to query.
|
|
13
|
-
- Node.js
|
|
13
|
+
- Node.js 22+ and npm. The bundled Wrangler 4 runtime requires Node 22 or newer.
|
|
14
14
|
- The Wrangler CLI — installed for you by `npm install`; invoke it with `npx wrangler …`.
|
|
15
15
|
|
|
16
16
|
Clone and install:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digestseo/mcp-gsc",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "scripts/glama-start.mjs",
|
|
6
6
|
"bin": {
|
|
@@ -87,6 +87,6 @@
|
|
|
87
87
|
"vitest": "^4.1.11"
|
|
88
88
|
},
|
|
89
89
|
"engines": {
|
|
90
|
-
"node": ">=
|
|
90
|
+
"node": ">=22.0.0"
|
|
91
91
|
}
|
|
92
92
|
}
|
package/src/google.ts
CHANGED
|
@@ -239,11 +239,28 @@ export interface SearchAnalyticsQuery {
|
|
|
239
239
|
dimensionFilterGroups?: DimensionFilterGroup[];
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
function inclusiveCalendarDayCount(startDate: string, endDate: string): number {
|
|
243
|
+
const [startYear, startMonth, startDay] = startDate.split('-').map(Number);
|
|
244
|
+
const [endYear, endMonth, endDay] = endDate.split('-').map(Number);
|
|
245
|
+
const startUtc = Date.UTC(startYear, startMonth - 1, startDay);
|
|
246
|
+
const endUtc = Date.UTC(endYear, endMonth - 1, endDay);
|
|
247
|
+
return Math.floor((endUtc - startUtc) / 86_400_000) + 1;
|
|
248
|
+
}
|
|
249
|
+
|
|
242
250
|
export function assertSearchAnalyticsQueryCompatible(body: SearchAnalyticsQuery): void {
|
|
243
251
|
if (new Set(body.dimensions).size !== body.dimensions.length) {
|
|
244
252
|
throw new Error('Search Analytics dimensions must not contain duplicates.');
|
|
245
253
|
}
|
|
246
254
|
|
|
255
|
+
const hasHourDimension = body.dimensions.includes('hour');
|
|
256
|
+
const usesHourlyData = body.dataState === 'hourly_all';
|
|
257
|
+
if (hasHourDimension && !usesHourlyData) {
|
|
258
|
+
throw new Error('Search Analytics hour dimension requires dataState hourly_all.');
|
|
259
|
+
}
|
|
260
|
+
if (hasHourDimension && inclusiveCalendarDayCount(body.startDate, body.endDate) > 10) {
|
|
261
|
+
throw new Error('Search Analytics hourly queries support at most 10 inclusive calendar days.');
|
|
262
|
+
}
|
|
263
|
+
|
|
247
264
|
const filters = body.dimensionFilterGroups?.flatMap((group) => group.filters) ?? [];
|
|
248
265
|
const hasPageGroupingOrFilter =
|
|
249
266
|
body.dimensions.includes('page') || filters.some((filter) => filter.dimension === 'page');
|
package/src/index.ts
CHANGED
|
@@ -172,14 +172,19 @@ const SITE_DETAIL_OUTPUT_SCHEMA = {
|
|
|
172
172
|
site: z.object(SITE_OUTPUT_SCHEMA),
|
|
173
173
|
};
|
|
174
174
|
|
|
175
|
+
const URL_INSPECTION_SCOPE_NOTE =
|
|
176
|
+
"Google's URL Inspection API reports the version currently known in Google's index; it does not run a live URL test or prove current live-page indexability. The mobileUsabilityResult field is deprecated and may be absent.";
|
|
177
|
+
|
|
175
178
|
const INSPECTION_OUTPUT_SCHEMA = {
|
|
176
179
|
inspection_result: z.unknown(),
|
|
180
|
+
note: z.string(),
|
|
177
181
|
};
|
|
178
182
|
|
|
179
183
|
const INSPECTION_BATCH_OUTPUT_SCHEMA = {
|
|
180
184
|
requested_count: z.number().int().positive(),
|
|
181
185
|
succeeded_count: z.number().int().nonnegative(),
|
|
182
186
|
failed_count: z.number().int().nonnegative(),
|
|
187
|
+
note: z.string(),
|
|
183
188
|
results: z.array(
|
|
184
189
|
z.object({
|
|
185
190
|
inspection_url: z.string(),
|
|
@@ -197,6 +202,10 @@ const SEARCH_ANALYTICS_OUTPUT_SCHEMA = {
|
|
|
197
202
|
row_count: z.number().int().nonnegative(),
|
|
198
203
|
start_row: z.number().int().nonnegative(),
|
|
199
204
|
rows: z.array(z.object(SEARCH_ROW_OUTPUT_SCHEMA)),
|
|
205
|
+
provider_exhaustiveness_guaranteed: z.literal(false).describe(
|
|
206
|
+
'False because the Search Analytics API does not guarantee every data row; Google can return only top rows even after local pagination is exhausted.',
|
|
207
|
+
),
|
|
208
|
+
provider_note: z.string(),
|
|
200
209
|
next_start_row: z.number().int().nonnegative().optional(),
|
|
201
210
|
has_more: z.boolean(),
|
|
202
211
|
truncated: z.boolean(),
|
|
@@ -227,6 +236,10 @@ const SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA = z.object({
|
|
|
227
236
|
rows_fetched: z.number().int().nonnegative(),
|
|
228
237
|
pages_fetched: z.number().int().positive(),
|
|
229
238
|
local_limit_reached: z.boolean(),
|
|
239
|
+
provider_exhaustiveness_guaranteed: z.literal(false).describe(
|
|
240
|
+
'False because Google does not guarantee that Search Analytics exposes every data row.',
|
|
241
|
+
),
|
|
242
|
+
provider_note: z.string(),
|
|
230
243
|
});
|
|
231
244
|
|
|
232
245
|
const RESULT_PAGE_OUTPUT_SCHEMA = z.object({
|
|
@@ -420,11 +433,16 @@ function toolResponse<T extends Record<string, unknown>>(
|
|
|
420
433
|
};
|
|
421
434
|
}
|
|
422
435
|
|
|
436
|
+
const SEARCH_ANALYTICS_PROVIDER_NOTE =
|
|
437
|
+
'Google Search Analytics does not guarantee all data rows and can return only top rows. Local pagination fields describe what this server fetched or bounded; they do not prove provider-level exhaustiveness.';
|
|
438
|
+
|
|
423
439
|
function paginationMetadata(result: PaginatedSearchAnalyticsResult) {
|
|
424
440
|
return {
|
|
425
441
|
rows_fetched: result.rows.length,
|
|
426
442
|
pages_fetched: result.pagesFetched,
|
|
427
443
|
local_limit_reached: result.localLimitReached,
|
|
444
|
+
provider_exhaustiveness_guaranteed: false as const,
|
|
445
|
+
provider_note: SEARCH_ANALYTICS_PROVIDER_NOTE,
|
|
428
446
|
};
|
|
429
447
|
}
|
|
430
448
|
|
|
@@ -508,7 +526,7 @@ const TOOL_CATALOG = [
|
|
|
508
526
|
{
|
|
509
527
|
name: 'analytics.query',
|
|
510
528
|
description:
|
|
511
|
-
'Query Search Console search analytics (impressions, clicks, CTR, average position) over a date range, broken down by query, page, country, device, date, or search appearance. Supports filters and pagination.',
|
|
529
|
+
'Query Search Console search analytics (impressions, clicks, CTR, average position) over a date range, broken down by query, page, country, device, date, or search appearance. Supports filters and pagination. Google does not guarantee every data row and can return only top rows.',
|
|
512
530
|
},
|
|
513
531
|
{
|
|
514
532
|
name: 'insights.page_queries',
|
|
@@ -523,12 +541,12 @@ const TOOL_CATALOG = [
|
|
|
523
541
|
{
|
|
524
542
|
name: 'urls.inspect',
|
|
525
543
|
description:
|
|
526
|
-
"Inspect a single URL
|
|
544
|
+
"Inspect Google's indexed version of a single URL: index status, last crawl, page-fetch/indexing state, canonicals, and rich-results/AMP analysis where available. This is not a live URL test; mobile usability is a deprecated response field. Use urls.inspect_many for 2-10 URLs.",
|
|
527
545
|
},
|
|
528
546
|
{
|
|
529
547
|
name: 'urls.inspect_many',
|
|
530
548
|
description:
|
|
531
|
-
|
|
549
|
+
"Inspect Google's indexed versions of up to 10 URLs sequentially in one MCP call while charging the same URL Inspection safety budget per URL. This does not run live URL tests.",
|
|
532
550
|
},
|
|
533
551
|
{
|
|
534
552
|
name: 'sitemaps.list',
|
|
@@ -719,7 +737,7 @@ class GscMcpRuntime {
|
|
|
719
737
|
'urls.inspect',
|
|
720
738
|
{
|
|
721
739
|
title: 'Inspect URL index status',
|
|
722
|
-
description: `Inspect a single URL
|
|
740
|
+
description: `Inspect the version of a single URL currently known in Google's index. Returns Google's index-status analysis, last crawl, page-fetch/indexing state, canonicals, and rich-results/AMP analysis where available. This API does not test the live URL or prove current live-page indexability, and its mobile-usability result is deprecated. Use this when the user asks 'is X indexed?' or wants Google's indexed-state evidence for one page. For a bounded group of 2-10 URLs, prefer urls.inspect_many; Google still processes one URL Inspection request per URL and applies the same quota semantics.`,
|
|
723
741
|
inputSchema: {
|
|
724
742
|
site_url: z.string().describe(SITE_URL_DESCRIPTION),
|
|
725
743
|
inspection_url: z
|
|
@@ -748,8 +766,9 @@ class GscMcpRuntime {
|
|
|
748
766
|
inspection_url,
|
|
749
767
|
language_code,
|
|
750
768
|
);
|
|
751
|
-
return toolResponse(JSON.stringify(result, null, 2), {
|
|
769
|
+
return toolResponse(JSON.stringify({ note: URL_INSPECTION_SCOPE_NOTE, inspection_result: result }, null, 2), {
|
|
752
770
|
inspection_result: result,
|
|
771
|
+
note: URL_INSPECTION_SCOPE_NOTE,
|
|
753
772
|
});
|
|
754
773
|
},
|
|
755
774
|
);
|
|
@@ -758,7 +777,7 @@ class GscMcpRuntime {
|
|
|
758
777
|
'urls.inspect_many',
|
|
759
778
|
{
|
|
760
779
|
title: 'Inspect multiple URLs',
|
|
761
|
-
description: `Inspect up to 10 URLs from one Search Console property
|
|
780
|
+
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 are sent sequentially to avoid unnecessary bursts. Use this for a small group of important or debugging-target URLs; do not use it to crawl an entire site.`,
|
|
762
781
|
inputSchema: {
|
|
763
782
|
site_url: z.string().describe(SITE_URL_DESCRIPTION),
|
|
764
783
|
inspection_urls: z
|
|
@@ -809,6 +828,7 @@ class GscMcpRuntime {
|
|
|
809
828
|
requested_count: inspection_urls.length,
|
|
810
829
|
succeeded_count: succeededCount,
|
|
811
830
|
failed_count: results.length - succeededCount,
|
|
831
|
+
note: URL_INSPECTION_SCOPE_NOTE,
|
|
812
832
|
results,
|
|
813
833
|
};
|
|
814
834
|
return toolResponse(JSON.stringify(payload, null, 2), payload);
|
|
@@ -848,13 +868,18 @@ class GscMcpRuntime {
|
|
|
848
868
|
title: 'Query search analytics',
|
|
849
869
|
description: [
|
|
850
870
|
'Query Google Search Console search analytics data. Returns',
|
|
851
|
-
'{ row_count, start_row, rows,
|
|
871
|
+
'{ row_count, start_row, rows, provider_exhaustiveness_guaranteed,',
|
|
872
|
+
' provider_note, has_more, truncated, byte_limit_reached }',
|
|
852
873
|
'where dimensioned rows have keys plus clicks, impressions, ctr, and',
|
|
853
874
|
'position. Aggregate rows from dimensions: [] may omit keys because',
|
|
854
875
|
'Google itself omits that field. When has_more is true, the response',
|
|
855
876
|
'includes next_start_row — pass it back as start_row to fetch the next',
|
|
856
877
|
'safe page. Large requested row_limit values are automatically split',
|
|
857
878
|
'into bounded MCP responses rather than failing structured output.',
|
|
879
|
+
'Google does not guarantee that Search Analytics exposes every data',
|
|
880
|
+
'row; it can return only top rows. Local has_more/pagination fields',
|
|
881
|
+
'therefore describe this server\'s fetch window, not provider-level',
|
|
882
|
+
'exhaustiveness.',
|
|
858
883
|
'When Google provides',
|
|
859
884
|
'them, response_aggregation_type and metadata are also included;',
|
|
860
885
|
'metadata may identify the first incomplete date or hour.',
|
|
@@ -891,7 +916,9 @@ class GscMcpRuntime {
|
|
|
891
916
|
' "NEWS_SHOWCASE" filter. Do not group/filter by page or add',
|
|
892
917
|
' another searchAppearance filter for that aggregation mode.',
|
|
893
918
|
'- For hourly breakdowns, include "hour" in dimensions and set',
|
|
894
|
-
' data_state to "hourly_all".
|
|
919
|
+
' data_state to "hourly_all". The hourly date range can span at',
|
|
920
|
+
' most 10 inclusive days, and hourly data',
|
|
921
|
+
' is preliminary.',
|
|
895
922
|
'- Use dimension_filter_groups to filter by country, device, query',
|
|
896
923
|
' content, page URL, or search feature. includingRegex and',
|
|
897
924
|
' excludingRegex use RE2 syntax. For brand vs non-brand splits,',
|
|
@@ -920,7 +947,7 @@ class GscMcpRuntime {
|
|
|
920
947
|
)
|
|
921
948
|
.default(['query'])
|
|
922
949
|
.describe(
|
|
923
|
-
|
|
950
|
+
"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 does not support the query dimension.",
|
|
924
951
|
),
|
|
925
952
|
row_limit: z
|
|
926
953
|
.number()
|
|
@@ -943,7 +970,7 @@ class GscMcpRuntime {
|
|
|
943
970
|
.enum(['all', 'final', 'hourly_all'])
|
|
944
971
|
.default('all')
|
|
945
972
|
.describe(
|
|
946
|
-
"'all' includes fresh (preliminary) data and matches the GSC dashboard; 'final' returns only finalized data; 'hourly_all'
|
|
973
|
+
"'all' includes fresh (preliminary) data and matches the GSC dashboard; 'final' returns only finalized data; 'hourly_all' includes hourly preliminary data and must be used when grouping by the 'hour' dimension. Hour-grouped requests can span at most 10 inclusive days.",
|
|
947
974
|
),
|
|
948
975
|
search_type: z
|
|
949
976
|
.enum(['web', 'image', 'video', 'news', 'discover', 'googleNews'])
|
|
@@ -1032,6 +1059,8 @@ class GscMcpRuntime {
|
|
|
1032
1059
|
row_count: bounded.items.length,
|
|
1033
1060
|
start_row,
|
|
1034
1061
|
rows: bounded.items,
|
|
1062
|
+
provider_exhaustiveness_guaranteed: false,
|
|
1063
|
+
provider_note: SEARCH_ANALYTICS_PROVIDER_NOTE,
|
|
1035
1064
|
has_more: bounded.resultPage.has_more,
|
|
1036
1065
|
truncated: bounded.resultPage.truncated,
|
|
1037
1066
|
byte_limit_reached: bounded.resultPage.byte_limit_reached,
|