@digestseo/mcp-gsc 0.4.3 → 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 +5 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/google.ts +17 -0
- package/src/index.ts +27 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,11 @@ 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.
|
|
9
13
|
|
|
10
14
|
## [0.4.3] - 2026-09-21
|
|
11
15
|
|
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ 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
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
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. |
|
package/package.json
CHANGED
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
|
@@ -202,6 +202,10 @@ const SEARCH_ANALYTICS_OUTPUT_SCHEMA = {
|
|
|
202
202
|
row_count: z.number().int().nonnegative(),
|
|
203
203
|
start_row: z.number().int().nonnegative(),
|
|
204
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(),
|
|
205
209
|
next_start_row: z.number().int().nonnegative().optional(),
|
|
206
210
|
has_more: z.boolean(),
|
|
207
211
|
truncated: z.boolean(),
|
|
@@ -232,6 +236,10 @@ const SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA = z.object({
|
|
|
232
236
|
rows_fetched: z.number().int().nonnegative(),
|
|
233
237
|
pages_fetched: z.number().int().positive(),
|
|
234
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(),
|
|
235
243
|
});
|
|
236
244
|
|
|
237
245
|
const RESULT_PAGE_OUTPUT_SCHEMA = z.object({
|
|
@@ -425,11 +433,16 @@ function toolResponse<T extends Record<string, unknown>>(
|
|
|
425
433
|
};
|
|
426
434
|
}
|
|
427
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
|
+
|
|
428
439
|
function paginationMetadata(result: PaginatedSearchAnalyticsResult) {
|
|
429
440
|
return {
|
|
430
441
|
rows_fetched: result.rows.length,
|
|
431
442
|
pages_fetched: result.pagesFetched,
|
|
432
443
|
local_limit_reached: result.localLimitReached,
|
|
444
|
+
provider_exhaustiveness_guaranteed: false as const,
|
|
445
|
+
provider_note: SEARCH_ANALYTICS_PROVIDER_NOTE,
|
|
433
446
|
};
|
|
434
447
|
}
|
|
435
448
|
|
|
@@ -513,7 +526,7 @@ const TOOL_CATALOG = [
|
|
|
513
526
|
{
|
|
514
527
|
name: 'analytics.query',
|
|
515
528
|
description:
|
|
516
|
-
'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.',
|
|
517
530
|
},
|
|
518
531
|
{
|
|
519
532
|
name: 'insights.page_queries',
|
|
@@ -855,13 +868,18 @@ class GscMcpRuntime {
|
|
|
855
868
|
title: 'Query search analytics',
|
|
856
869
|
description: [
|
|
857
870
|
'Query Google Search Console search analytics data. Returns',
|
|
858
|
-
'{ row_count, start_row, rows,
|
|
871
|
+
'{ row_count, start_row, rows, provider_exhaustiveness_guaranteed,',
|
|
872
|
+
' provider_note, has_more, truncated, byte_limit_reached }',
|
|
859
873
|
'where dimensioned rows have keys plus clicks, impressions, ctr, and',
|
|
860
874
|
'position. Aggregate rows from dimensions: [] may omit keys because',
|
|
861
875
|
'Google itself omits that field. When has_more is true, the response',
|
|
862
876
|
'includes next_start_row — pass it back as start_row to fetch the next',
|
|
863
877
|
'safe page. Large requested row_limit values are automatically split',
|
|
864
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.',
|
|
865
883
|
'When Google provides',
|
|
866
884
|
'them, response_aggregation_type and metadata are also included;',
|
|
867
885
|
'metadata may identify the first incomplete date or hour.',
|
|
@@ -898,7 +916,9 @@ class GscMcpRuntime {
|
|
|
898
916
|
' "NEWS_SHOWCASE" filter. Do not group/filter by page or add',
|
|
899
917
|
' another searchAppearance filter for that aggregation mode.',
|
|
900
918
|
'- For hourly breakdowns, include "hour" in dimensions and set',
|
|
901
|
-
' 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.',
|
|
902
922
|
'- Use dimension_filter_groups to filter by country, device, query',
|
|
903
923
|
' content, page URL, or search feature. includingRegex and',
|
|
904
924
|
' excludingRegex use RE2 syntax. For brand vs non-brand splits,',
|
|
@@ -927,7 +947,7 @@ class GscMcpRuntime {
|
|
|
927
947
|
)
|
|
928
948
|
.default(['query'])
|
|
929
949
|
.describe(
|
|
930
|
-
|
|
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.",
|
|
931
951
|
),
|
|
932
952
|
row_limit: z
|
|
933
953
|
.number()
|
|
@@ -950,7 +970,7 @@ class GscMcpRuntime {
|
|
|
950
970
|
.enum(['all', 'final', 'hourly_all'])
|
|
951
971
|
.default('all')
|
|
952
972
|
.describe(
|
|
953
|
-
"'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.",
|
|
954
974
|
),
|
|
955
975
|
search_type: z
|
|
956
976
|
.enum(['web', 'image', 'video', 'news', 'discover', 'googleNews'])
|
|
@@ -1039,6 +1059,8 @@ class GscMcpRuntime {
|
|
|
1039
1059
|
row_count: bounded.items.length,
|
|
1040
1060
|
start_row,
|
|
1041
1061
|
rows: bounded.items,
|
|
1062
|
+
provider_exhaustiveness_guaranteed: false,
|
|
1063
|
+
provider_note: SEARCH_ANALYTICS_PROVIDER_NOTE,
|
|
1042
1064
|
has_more: bounded.resultPage.has_more,
|
|
1043
1065
|
truncated: bounded.resultPage.truncated,
|
|
1044
1066
|
byte_limit_reached: bounded.resultPage.byte_limit_reached,
|