@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digestseo/mcp-gsc",
3
- "version": "0.4.32",
3
+ "version": "0.4.34",
4
4
  "private": false,
5
5
  "main": "scripts/glama-start.mjs",
6
6
  "bin": {
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, provider_exhaustiveness_guaranteed,',
1192
- ' provider_note, has_more, truncated, byte_limit_reached }',
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. Aggregate rows from dimensions: [] may omit keys because',
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 response = await querySearchAnalytics(accessToken, site_url, {
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;