@digestseo/mcp-gsc 0.4.35 → 0.4.36

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,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.36] - 2026-09-24
11
+
12
+ ### Changed
13
+ - `analytics.query` now exposes an explicit `multimodal_report_isolatable: false` boundary and matching runtime guidance because Search Console's new web multimodal filter is currently UI-only; the documented Search Analytics API still exposes only web/image/video/news/discover/googleNews search types.
14
+
15
+ ### Fixed
16
+ - User-supplied Search Analytics ranges now reject future end dates against Search Console's Pacific calendar before reserving local safety budget, resolving Google credentials, or calling the provider; the same boundary now applies to explicit indexed-page ranges and weekly-digest input.
17
+
10
18
  ## [0.4.35] - 2026-09-24
11
19
 
12
20
  ### Fixed
package/README.md CHANGED
@@ -108,7 +108,7 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 23 tools. Read-onl
108
108
  | **`server.capabilities`** | Read | List every tool this server exposes and report local Google credential state. `connected` means a stored refresh credential is present; the response also exposes `auth_status_basis` and `provider_auth_live_verified: false` because this discovery call does not probe Google. A later Google tool call can still require reconnection if the credential expired or was revoked. Takes no arguments. |
109
109
  | **`sites.list`** | Read | List the Search Console properties the connected Google account can access (`siteUrl`, `permissionLevel`). |
110
110
  | **`sites.get`** | Read | Retrieve one exact Search Console property and the connected account's permission level for it. |
111
- | **`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. `data_state: "all"` can include fresh preliminary data, while `final` returns only finalized data; responses echo `data_state` plus `preliminary_data_possible`, and date/hour groupings preserve Google's incomplete-data boundary metadata when returned. 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, so returned rows can omit `position`, and query grouping/filtering is unavailable. Search Console's dedicated Generative AI performance reports are not exposed by the current documented Search Analytics API as a dedicated search type or filter selector; `generative_ai_report_isolatable` is therefore `false`. |
111
+ | **`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. `data_state: "all"` can include fresh preliminary data, while `final` returns only finalized data; responses echo `data_state` plus `preliminary_data_possible`, and date/hour groupings preserve Google's incomplete-data boundary metadata when returned. 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, so returned rows can omit `position`, and query grouping/filtering is unavailable. Search Console's dedicated Generative AI performance reports are not exposed by the current documented Search Analytics API as a dedicated search type or filter selector; `generative_ai_report_isolatable` is therefore `false`. Search Console's new web multimodal UI filter is likewise not exposed as a documented Search Analytics API selector; `multimodal_report_isolatable` is therefore `false`. |
112
112
  | **`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. |
113
113
  | **`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. |
114
114
  | **`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. |
@@ -149,6 +149,8 @@ Query regexes are a manual approximation only. Search Console's native Branded/N
149
149
 
150
150
  Search Console also has dedicated Generative AI performance reports for Search and Discover. The current documented Search Analytics API does not expose a dedicated Generative AI search type or filter selector, so this MCP does not guess one. AI Overviews and AI Mode remain included in overall web Search performance data; use the Search Console UI for the dedicated Generative AI report until Google documents API access.
151
151
 
152
+ As of September 24, 2026, Search Console also has a web multimodal performance filter in the UI for searches such as Lens, Circle to Search, image uploads, and Chrome image search. The current Search Analytics API and Discovery document still expose only `web`, `image`, `video`, `news`, `discover`, and `googleNews` search types, so this MCP does not invent `search_type: "multimodal"` or guess a `searchAppearance` value. Use the Search Console UI for isolated multimodal reporting until Google documents API access.
153
+
152
154
  ## Connect in your AI client
153
155
 
154
156
  Once you've deployed the server (see **[SETUP.md](SETUP.md)**), connect it by pasting your Worker's `/mcp` URL into your client:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digestseo/mcp-gsc",
3
- "version": "0.4.35",
3
+ "version": "0.4.36",
4
4
  "private": false,
5
5
  "main": "scripts/glama-start.mjs",
6
6
  "bin": {
@@ -81,10 +81,14 @@ export function assertDateRange(
81
81
  }
82
82
  }
83
83
 
84
- export function assertDateNotInFuture(date: string, today: string): void {
84
+ export function assertDateNotInFuture(
85
+ date: string,
86
+ today: string,
87
+ dateName = 'End date',
88
+ ): void {
85
89
  if (date > today) {
86
90
  throw new Error(
87
- 'End date must be today or earlier. Google Search Console has no data for dates that have not happened yet.',
91
+ `${dateName} must be today or earlier (current Search Console Pacific date: ${today}). Google Search Console has no data for dates that have not happened yet.`,
88
92
  );
89
93
  }
90
94
  }
package/src/index.ts CHANGED
@@ -402,6 +402,10 @@ const SEARCH_ANALYTICS_OUTPUT_SCHEMA = {
402
402
  'False because the current documented Search Analytics API exposes no dedicated Generative AI report search type or filter selector.',
403
403
  ),
404
404
  generative_ai_note: z.string(),
405
+ multimodal_report_isolatable: z.literal(false).describe(
406
+ 'False because the current documented Search Analytics API exposes no dedicated web multimodal search type or filter selector.',
407
+ ),
408
+ multimodal_note: z.string(),
405
409
  next_start_row: z.number().int().nonnegative().optional(),
406
410
  has_more: z.boolean(),
407
411
  truncated: z.boolean(),
@@ -698,6 +702,9 @@ const SEARCH_ANALYTICS_PROVIDER_NOTE =
698
702
  const GENERATIVE_AI_REPORT_API_NOTE =
699
703
  "Search Console's dedicated Generative AI performance reports are not exposed by the current documented Search Analytics API as a dedicated search type or filter selector. AI Overviews and AI Mode remain included in overall web Search performance data. Do not infer isolated Generative AI metrics from analytics.query or guess a searchAppearance identifier.";
700
704
 
705
+ const MULTIMODAL_REPORT_API_NOTE =
706
+ "Search Console's web multimodal performance filter is currently available in the Search Console UI, but the documented Search Analytics API does not expose a multimodal search type or filter selector. Current API search types remain web, image, video, news, discover, and googleNews. Do not pass search_type='multimodal' or guess a searchAppearance identifier to isolate Lens, Circle to Search, image-upload, or Chrome image-search traffic.";
707
+
701
708
  function paginationMetadata(result: PaginatedSearchAnalyticsResult) {
702
709
  return {
703
710
  rows_fetched: result.rows.length,
@@ -1279,6 +1286,13 @@ class GscMcpRuntime {
1279
1286
  ' included in overall web Search performance data. Do not guess a',
1280
1287
  ' searchAppearance identifier or claim analytics.query isolates the',
1281
1288
  ' dedicated Generative AI report.',
1289
+ '- WEB MULTIMODAL REPORT: Search Console now has a web multimodal',
1290
+ ' performance filter in its UI for searches such as Lens, Circle to',
1291
+ ' Search, image uploads, and Chrome image search. The current',
1292
+ ' documented Search Analytics API still exposes only web, image,',
1293
+ ' video, news, discover, and googleNews search types, with no',
1294
+ ' multimodal selector. Do not pass search_type="multimodal" or guess',
1295
+ ' a searchAppearance identifier to isolate this UI report.',
1282
1296
  '- Use dimension_filter_groups to filter by country, device, query',
1283
1297
  ' content, page URL, or search feature. includingRegex and',
1284
1298
  ' excludingRegex use RE2 syntax. A query regex can provide a manual',
@@ -1358,7 +1372,7 @@ class GscMcpRuntime {
1358
1372
  search_type: z
1359
1373
  .enum(['web', 'image', 'video', 'news', 'discover', 'googleNews'])
1360
1374
  .default('web')
1361
- .describe('Which documented Search Analytics search index to query. Defaults to web. Google Discover and Google News do not support query grouping/filtering or average position. The current API does not expose a dedicated Generative AI performance-report search type.'),
1375
+ .describe('Which documented Search Analytics search index to query. Defaults to web. Google Discover and Google News do not support query grouping/filtering or average position. The current API does not expose a dedicated Generative AI performance-report search type or the Search Console UI web multimodal filter as a search type.'),
1362
1376
  aggregation_type: z
1363
1377
  .enum(['auto', 'byNewsShowcasePanel', 'byPage', 'byProperty'])
1364
1378
  .default('auto')
@@ -1388,6 +1402,11 @@ class GscMcpRuntime {
1388
1402
  dimension_filter_groups,
1389
1403
  }) => {
1390
1404
  assertDateRange(start_date, end_date);
1405
+ assertDateNotInFuture(
1406
+ end_date,
1407
+ getSearchConsoleCalendarDate(),
1408
+ 'end_date',
1409
+ );
1391
1410
  const sourceRowLimit = Math.min(row_limit, MAX_DIRECT_SOURCE_ROWS);
1392
1411
  const query = {
1393
1412
  startDate: start_date,
@@ -1439,6 +1458,8 @@ class GscMcpRuntime {
1439
1458
  provider_note: SEARCH_ANALYTICS_PROVIDER_NOTE,
1440
1459
  generative_ai_report_isolatable: false,
1441
1460
  generative_ai_note: GENERATIVE_AI_REPORT_API_NOTE,
1461
+ multimodal_report_isolatable: false,
1462
+ multimodal_note: MULTIMODAL_REPORT_API_NOTE,
1442
1463
  has_more: bounded.resultPage.has_more,
1443
1464
  truncated: bounded.resultPage.truncated,
1444
1465
  byte_limit_reached: bounded.resultPage.byte_limit_reached,
@@ -1497,6 +1518,11 @@ class GscMcpRuntime {
1497
1518
  },
1498
1519
  async ({ site_url, page_url, start_date, end_date, search_type, row_limit = 100, start_row = 0 }) => {
1499
1520
  assertDateRange(start_date, end_date);
1521
+ assertDateNotInFuture(
1522
+ end_date,
1523
+ getSearchConsoleCalendarDate(),
1524
+ 'end_date',
1525
+ );
1500
1526
  assertUrlWithinSearchConsoleProperty(page_url, site_url, 'page_url');
1501
1527
  const googleId = this.requireGoogleId();
1502
1528
  const rateLimitError = await this.rateLimitError(googleId, 'insights.page_queries');
@@ -1582,6 +1608,11 @@ class GscMcpRuntime {
1582
1608
  },
1583
1609
  async ({ site_url, query, start_date, end_date, search_type, row_limit = 100, start_row = 0 }) => {
1584
1610
  assertDateRange(start_date, end_date);
1611
+ assertDateNotInFuture(
1612
+ end_date,
1613
+ getSearchConsoleCalendarDate(),
1614
+ 'end_date',
1615
+ );
1585
1616
  const googleId = this.requireGoogleId();
1586
1617
  const rateLimitError = await this.rateLimitError(googleId, 'insights.query_pages');
1587
1618
  if (rateLimitError) return rateLimitError;
@@ -1776,6 +1807,11 @@ class GscMcpRuntime {
1776
1807
  },
1777
1808
  async ({ site_url, start_date, end_date, min_impressions, min_position, max_position, limit = DEFAULT_ANALYSIS_RESULT_LIMIT, start_row = 0 }) => {
1778
1809
  assertDateRange(start_date, end_date);
1810
+ assertDateNotInFuture(
1811
+ end_date,
1812
+ getSearchConsoleCalendarDate(),
1813
+ 'end_date',
1814
+ );
1779
1815
  const googleId = this.requireGoogleId();
1780
1816
  const rateLimitError = await this.rateLimitError(googleId, 'insights.quick_wins');
1781
1817
  if (rateLimitError) return rateLimitError;
@@ -1823,6 +1859,11 @@ class GscMcpRuntime {
1823
1859
  },
1824
1860
  async ({ site_url, start_date, end_date, min_impressions, min_page_percentage, limit = DEFAULT_ANALYSIS_RESULT_LIMIT, start_row = 0 }) => {
1825
1861
  assertDateRange(start_date, end_date);
1862
+ assertDateNotInFuture(
1863
+ end_date,
1864
+ getSearchConsoleCalendarDate(),
1865
+ 'end_date',
1866
+ );
1826
1867
  const googleId = this.requireGoogleId();
1827
1868
  const rateLimitError = await this.rateLimitError(googleId, 'insights.cannibalization');
1828
1869
  if (rateLimitError) return rateLimitError;
@@ -2187,6 +2228,9 @@ class GscMcpRuntime {
2187
2228
  }) => {
2188
2229
  assertDateRange(start_date_a, end_date_a, 'start_date_a', 'end_date_a');
2189
2230
  assertDateRange(start_date_b, end_date_b, 'start_date_b', 'end_date_b');
2231
+ const today = getSearchConsoleCalendarDate();
2232
+ assertDateNotInFuture(end_date_a, today, 'end_date_a');
2233
+ assertDateNotInFuture(end_date_b, today, 'end_date_b');
2190
2234
  const googleId = this.requireGoogleId();
2191
2235
  const rateLimitError = await this.rateLimitError(googleId, 'analytics.compare');
2192
2236
  if (rateLimitError) return rateLimitError;
@@ -2248,12 +2292,12 @@ class GscMcpRuntime {
2248
2292
  annotations: READ_ONLY_ANNOTATIONS,
2249
2293
  },
2250
2294
  async ({ site_url, end_date }) => {
2295
+ const today = getSearchConsoleCalendarDate();
2296
+ const resolvedEndDate = resolveWeeklyDigestEndDate(end_date, today);
2297
+ assertDateNotInFuture(resolvedEndDate, today, 'end_date');
2251
2298
  const googleId = this.requireGoogleId();
2252
2299
  const rateLimitError = await this.rateLimitError(googleId, 'reports.weekly_digest');
2253
2300
  if (rateLimitError) return rateLimitError;
2254
- const today = getSearchConsoleCalendarDate();
2255
- const resolvedEndDate = resolveWeeklyDigestEndDate(end_date, today);
2256
- assertDateNotInFuture(resolvedEndDate, today);
2257
2301
 
2258
2302
  try {
2259
2303
  const markdown = await generateWeeklyDigest(
@@ -1,4 +1,4 @@
1
- import { assertDateRange } from './date-validation';
1
+ import { assertDateNotInFuture, assertDateRange } from './date-validation';
2
2
 
3
3
  export interface IndexedPagesDateRange {
4
4
  startDate: string;
@@ -26,6 +26,7 @@ export function resolveIndexedPagesDateRange(
26
26
  }
27
27
 
28
28
  if (!startDate) {
29
+ assertDateNotInFuture(endDate!, today, 'end_date');
29
30
  return {
30
31
  startDate: addUtcDays(endDate!, -29),
31
32
  endDate: endDate!,
@@ -50,5 +51,6 @@ export function resolveIndexedPagesDateRange(
50
51
  }
51
52
 
52
53
  assertDateRange(startDate, endDate);
54
+ assertDateNotInFuture(endDate, today, 'end_date');
53
55
  return { startDate, endDate };
54
56
  }