@digestseo/mcp-gsc 0.4.32 → 0.4.34
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 +14 -0
- package/package.json +1 -1
- package/src/index.ts +52 -9
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.34] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `analytics.query` now rejects documented-invalid cross-field request combinations before reserving local Search Analytics safety budget or resolving Google credentials, while retaining the lower-level provider compatibility guard for internal callers.
|
|
14
|
+
- `indexing.status`, `indexing.request`, and `indexing.remove` now reject URLs outside the supplied Search Console property before reserving local safety budget, resolving Google credentials, or calling Search Console for owner authorization.
|
|
15
|
+
|
|
16
|
+
## [0.4.33] - 2026-09-24
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `analytics.query` responses now echo the ordered requested `dimensions` and `search_type`, so `rows[].keys` remain self-describing when responses are saved, forwarded, or consumed outside the original request context.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- `analytics.query` now rejects duplicate grouping dimensions and mixed `searchAppearance` grouping at MCP input validation, before local rate-limit, Google credential, or provider work.
|
|
23
|
+
|
|
10
24
|
## [0.4.32] - 2026-09-24
|
|
11
25
|
|
|
12
26
|
### Added
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
getIndexingNotificationMetadata,
|
|
35
35
|
processPerformanceComparison,
|
|
36
36
|
assertSearchAnalyticsFilterCompatible,
|
|
37
|
+
assertSearchAnalyticsQueryCompatible,
|
|
37
38
|
type PaginatedSearchAnalyticsResult,
|
|
38
39
|
} from './google';
|
|
39
40
|
import {
|
|
@@ -365,6 +366,24 @@ const SEARCH_ANALYTICS_OUTPUT_SCHEMA = {
|
|
|
365
366
|
row_count: z.number().int().nonnegative(),
|
|
366
367
|
start_row: z.number().int().nonnegative(),
|
|
367
368
|
rows: z.array(z.object(SEARCH_ROW_OUTPUT_SCHEMA)),
|
|
369
|
+
dimensions: z
|
|
370
|
+
.array(
|
|
371
|
+
z.enum([
|
|
372
|
+
'query',
|
|
373
|
+
'page',
|
|
374
|
+
'country',
|
|
375
|
+
'device',
|
|
376
|
+
'date',
|
|
377
|
+
'hour',
|
|
378
|
+
'searchAppearance',
|
|
379
|
+
]),
|
|
380
|
+
)
|
|
381
|
+
.describe(
|
|
382
|
+
'Ordered labels for rows[].keys. Each key position corresponds to the dimension at the same index. Empty for aggregate dimensions: [] queries.',
|
|
383
|
+
),
|
|
384
|
+
search_type: z
|
|
385
|
+
.enum(['web', 'image', 'video', 'news', 'discover', 'googleNews'])
|
|
386
|
+
.describe('The Search Analytics search type requested for this response.'),
|
|
368
387
|
data_state: z.enum(['all', 'final', 'hourly_all']).describe(
|
|
369
388
|
'The Search Analytics dataState requested from Google for this response.',
|
|
370
389
|
),
|
|
@@ -1188,10 +1207,13 @@ class GscMcpRuntime {
|
|
|
1188
1207
|
title: 'Query search analytics',
|
|
1189
1208
|
description: [
|
|
1190
1209
|
'Query Google Search Console search analytics data. Returns',
|
|
1191
|
-
'{ row_count, start_row, rows,
|
|
1192
|
-
' provider_note, has_more,
|
|
1210
|
+
'{ row_count, start_row, rows, dimensions, search_type,',
|
|
1211
|
+
' provider_exhaustiveness_guaranteed, provider_note, has_more,',
|
|
1212
|
+
' truncated, byte_limit_reached }',
|
|
1193
1213
|
'where dimensioned rows have keys plus clicks, impressions, ctr, and',
|
|
1194
|
-
'position.
|
|
1214
|
+
'position. The echoed dimensions array labels rows[].keys in order,',
|
|
1215
|
+
'so saved or forwarded responses remain self-describing. Aggregate',
|
|
1216
|
+
'rows from dimensions: [] may omit keys because',
|
|
1195
1217
|
'Google itself omits that field. When has_more is true, the response',
|
|
1196
1218
|
'includes next_start_row — pass it back as start_row to fetch the next',
|
|
1197
1219
|
'safe page. Large requested row_limit values are automatically split',
|
|
@@ -1292,6 +1314,20 @@ class GscMcpRuntime {
|
|
|
1292
1314
|
'searchAppearance',
|
|
1293
1315
|
]),
|
|
1294
1316
|
)
|
|
1317
|
+
.superRefine((dimensions, ctx) => {
|
|
1318
|
+
if (new Set(dimensions).size !== dimensions.length) {
|
|
1319
|
+
ctx.addIssue({
|
|
1320
|
+
code: 'custom',
|
|
1321
|
+
message: 'Search Analytics dimensions must not contain duplicates.',
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
if (dimensions.includes('searchAppearance') && dimensions.length > 1) {
|
|
1325
|
+
ctx.addIssue({
|
|
1326
|
+
code: 'custom',
|
|
1327
|
+
message: 'Search Analytics searchAppearance must be the only grouping dimension.',
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
})
|
|
1295
1331
|
.default(['query'])
|
|
1296
1332
|
.describe(
|
|
1297
1333
|
"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.",
|
|
@@ -1352,12 +1388,8 @@ class GscMcpRuntime {
|
|
|
1352
1388
|
dimension_filter_groups,
|
|
1353
1389
|
}) => {
|
|
1354
1390
|
assertDateRange(start_date, end_date);
|
|
1355
|
-
const googleId = this.requireGoogleId();
|
|
1356
|
-
const rateLimitError = await this.rateLimitError(googleId, 'analytics.query');
|
|
1357
|
-
if (rateLimitError) return rateLimitError;
|
|
1358
|
-
const accessToken = await this.getAccessToken(googleId);
|
|
1359
1391
|
const sourceRowLimit = Math.min(row_limit, MAX_DIRECT_SOURCE_ROWS);
|
|
1360
|
-
const
|
|
1392
|
+
const query = {
|
|
1361
1393
|
startDate: start_date,
|
|
1362
1394
|
endDate: end_date,
|
|
1363
1395
|
dimensions,
|
|
@@ -1369,7 +1401,13 @@ class GscMcpRuntime {
|
|
|
1369
1401
|
...(dimension_filter_groups !== undefined
|
|
1370
1402
|
? { dimensionFilterGroups: dimension_filter_groups }
|
|
1371
1403
|
: {}),
|
|
1372
|
-
}
|
|
1404
|
+
};
|
|
1405
|
+
assertSearchAnalyticsQueryCompatible(query);
|
|
1406
|
+
const googleId = this.requireGoogleId();
|
|
1407
|
+
const rateLimitError = await this.rateLimitError(googleId, 'analytics.query');
|
|
1408
|
+
if (rateLimitError) return rateLimitError;
|
|
1409
|
+
const accessToken = await this.getAccessToken(googleId);
|
|
1410
|
+
const response = await querySearchAnalytics(accessToken, site_url, query);
|
|
1373
1411
|
// Search Analytics pagination is complete only after Google returns an
|
|
1374
1412
|
// explicit empty page. A short non-empty page can still be followed by
|
|
1375
1413
|
// later rows, so preserve a continuation for dimensioned queries.
|
|
@@ -1386,6 +1424,8 @@ class GscMcpRuntime {
|
|
|
1386
1424
|
row_count: bounded.items.length,
|
|
1387
1425
|
start_row,
|
|
1388
1426
|
rows: bounded.items,
|
|
1427
|
+
dimensions,
|
|
1428
|
+
search_type,
|
|
1389
1429
|
data_state,
|
|
1390
1430
|
preliminary_data_possible: data_state !== 'final',
|
|
1391
1431
|
position_supported:
|
|
@@ -1920,6 +1960,7 @@ class GscMcpRuntime {
|
|
|
1920
1960
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
1921
1961
|
},
|
|
1922
1962
|
async ({ site_url, url }) => {
|
|
1963
|
+
assertUrlWithinSearchConsoleProperty(url, site_url);
|
|
1923
1964
|
const googleId = this.requireGoogleId();
|
|
1924
1965
|
const rateLimitError = await this.rateLimitError(googleId, 'indexing.status');
|
|
1925
1966
|
if (rateLimitError) return rateLimitError;
|
|
@@ -1963,6 +2004,7 @@ class GscMcpRuntime {
|
|
|
1963
2004
|
annotations: WRITE_TOOL_ANNOTATIONS['indexing.request'],
|
|
1964
2005
|
},
|
|
1965
2006
|
async ({ site_url, url }) => {
|
|
2007
|
+
assertUrlWithinSearchConsoleProperty(url, site_url);
|
|
1966
2008
|
const googleId = this.requireGoogleId();
|
|
1967
2009
|
const rateLimitError = await this.rateLimitError(googleId, 'indexing.request');
|
|
1968
2010
|
if (rateLimitError) return rateLimitError;
|
|
@@ -2004,6 +2046,7 @@ class GscMcpRuntime {
|
|
|
2004
2046
|
annotations: WRITE_TOOL_ANNOTATIONS['indexing.remove'],
|
|
2005
2047
|
},
|
|
2006
2048
|
async ({ site_url, url }) => {
|
|
2049
|
+
assertUrlWithinSearchConsoleProperty(url, site_url);
|
|
2007
2050
|
const googleId = this.requireGoogleId();
|
|
2008
2051
|
const rateLimitError = await this.rateLimitError(googleId, 'indexing.remove');
|
|
2009
2052
|
if (rateLimitError) return rateLimitError;
|