@digestseo/mcp-gsc 0.4.11 → 0.4.13

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.13] - 2026-09-22
11
+
12
+ ### Fixed
13
+ - The published `npx` launcher now persists local Wrangler/KV/Durable Object state in a stable per-user directory instead of the versioned npm package cache, preserving the encrypted Google session across later package upgrades and ordinary npm cache cleanup; `MCP_GSC_STATE_DIR` can override the location. The first release with this change may require one Google reconnect because older launcher releases used the package cache for local state.
14
+ - Official MCP Registry discovery no longer advertises the owner-hosted remote while that deployment's tool contract lags the current npm package; the verified npm launcher remains the Registry installation path until hosted parity is restored.
15
+ - Search Analytics now exposes a machine-readable Generative AI report boundary instead of inviting agents to invent an unsupported selector: the current documented API has no dedicated Generative AI search type/filter, while AI Overviews and AI Mode remain included in overall web Search performance data.
16
+
17
+ ## [0.4.12] - 2026-09-22
18
+
19
+ ### Fixed
20
+ - `urls.inspect_many` now runs URL Inspection requests with bounded concurrency of three instead of strictly sequentially, reducing batch latency while preserving per-URL safety-budget accounting, input-order results, per-URL failures, and batch-fatal Google-access revocation.
21
+ - Search Analytics now rejects `searchAppearance` combined with another grouping dimension and documents Google's required two-step appearance discovery/filter workflow instead of sending invalid grouped requests to the provider.
22
+ - Brand/non-brand Search Analytics guidance now distinguishes caller-supplied regex segmentation from Search Console's AI-assisted native Branded/Non-branded filter, which the Search Analytics API does not expose.
23
+
10
24
  ## [0.4.11] - 2026-09-22
11
25
 
12
26
  ### Fixed
package/README.md CHANGED
@@ -24,6 +24,8 @@ npx -y @digestseo/mcp-gsc
24
24
 
25
25
  The npm launcher starts a loopback-only **Streamable HTTP** Worker on `127.0.0.1` (port `8080` by default); it is not a stdio MCP process. You can run the `npx` command from any directory because the launcher resolves its bundled Worker configuration from the installed package. Clients that launch the package should connect to `http://127.0.0.1:8080/mcp` after supplying the Google OAuth and token-encryption environment variables described in [SETUP.md](SETUP.md). Before the first OAuth sign-in, authorize the exact callback `http://127.0.0.1:8080/google/callback` in Google Cloud (or use the same custom `PORT` you launch with); the launcher prints both URLs at startup.
26
26
 
27
+ Local KV and Durable Object state, including the encrypted Google session, is stored in `~/.mcp-gsc/state` by default instead of inside the versioned npm cache. That keeps the local connection available across package upgrades and ordinary npm cache cleanup. Set `MCP_GSC_STATE_DIR` to use another directory; keep the same `TOKEN_ENCRYPTION_KEY` when reusing existing state because stored refresh tokens are encrypted with that key. The launcher prints the effective state directory at startup. The first release with this stable path may require one Google reconnect because older launcher releases stored local state under the npm package cache.
28
+
27
29
  The local launcher defaults to `GSC_ACCESS_MODE=readwrite`. For least-privilege analytics/reporting-only use, set `GSC_ACCESS_MODE=readonly` in the environment before running `npx`; the launcher forwards that value into the local Worker so OAuth requests only the read-only Search Console scope and the five mutation tools stay unregistered.
28
30
 
29
31
  For Google OAuth and Cloudflare deployment configuration, follow [SETUP.md](SETUP.md).
@@ -75,10 +77,10 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 21 tools. Read-onl
75
77
  | **`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. |
76
78
  | **`sites.list`** | Read | List the Search Console properties the connected Google account can access (`siteUrl`, `permissionLevel`). |
77
79
  | **`sites.get`** | Read | Retrieve one exact Search Console property and the connected account's permission level for it. |
78
- | **`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. For `search_type: "discover"` or `"googleNews"`, `position_supported` is `false`; those reports expose clicks, impressions, and CTR but not average position, and query grouping/filtering is unavailable. |
80
+ | **`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. 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, 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`. |
79
81
  | **`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. |
80
82
  | **`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. |
81
- | **`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. |
83
+ | **`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. |
82
84
  | **`sitemaps.list`** / **`sitemaps.get`** | Read | List submitted sitemaps or retrieve one sitemap's details. `lastSubmitted` is when the sitemap was submitted to Search Console; `lastDownloaded` is when Google last downloaded the sitemap. Neither value is the sitemap file's modification/deploy time or a page crawl/indexing timestamp. |
83
85
  | **`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 compare only pages returned in both periods and do not turn one-sided row absence into zero traffic. |
84
86
  | **`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. `analytics.compare` compares only dimension keys returned in both period responses; a key absent from one non-exhaustive Search Analytics response is not assumed to have zero metrics. Because its comparison contract includes average position, it excludes Discover and Google News. `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. |
@@ -105,9 +107,13 @@ Once connected, ask your assistant things like:
105
107
  - *"Which high-impression query/page rows have an average position between 5 and 15? Show me those optimization opportunities."*
106
108
  - *"Give me a weekly digest for `sc-domain:example.com` ending today."*
107
109
  - *"Do any of my sitemaps have errors or warnings?"*
108
- - *"Split my clicks into brand vs non-brand using a regex on the query."*
110
+ - *"Using this brand regex I provide, approximate my clicks as brand vs non-brand."*
109
111
  - *"How is my site doing in Google Discover vs regular web search?"*
110
112
 
113
+ Query regexes are a manual approximation only. Search Console's native Branded/Non-branded filter uses Google's own AI-assisted classification (including language variants, typos, and associated products/services), and that native classifier is not exposed as a Search Analytics API filter.
114
+
115
+ 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.
116
+
111
117
  ## Connect in your AI client
112
118
 
113
119
  Once you've deployed the server (see **[SETUP.md](SETUP.md)**), connect it by pasting your Worker's `/mcp` URL into your client:
package/SECURITY.md CHANGED
@@ -58,7 +58,7 @@ delay and does not disclose another user's usage.
58
58
  | Category | Tools | Limit |
59
59
  |---|---|---|
60
60
  | Search Analytics | `analytics.query`, `insights.quick_wins`, `insights.cannibalization`, `insights.content_decay`, `indexing.list_pages`, `analytics.compare` | 30 per user / 10 minutes |
61
- | URL inspection | `urls.inspect`, `urls.inspect_many` | 20 inspected URLs per user / 24 hours; batch requests reserve one unit per URL |
61
+ | URL inspection | `urls.inspect`, `urls.inspect_many` | 20 inspected URLs per user / 24 hours; batch requests reserve one unit per URL and run at most 3 inspections concurrently |
62
62
  | Search Console writes | `sites.add`, `sites.delete`, `sitemaps.submit`, `sitemaps.delete` | 10 per user / hour |
63
63
  | Indexing publish | `indexing.request` | 2 per user / 24 hours; 200 per deployment / 24 hours |
64
64
  | Weekly digest | `reports.weekly_digest` | 6 per user / hour |
package/SETUP.md CHANGED
@@ -81,6 +81,7 @@ npx wrangler login
81
81
  - Replace `<your-worker>` with your Worker's name + subdomain. If you don't know it yet, deploy once (Step 6) to see the assigned `*.workers.dev` URL, then come back and add it here. The default Worker name is `mcp-gsc` (set in `wrangler.jsonc`).
82
82
  - The `http://localhost:8787/google/callback` entry is for local development with `npm run dev`.
83
83
  - The `http://127.0.0.1:8080/google/callback` entry is for the published `npx -y @digestseo/mcp-gsc` launcher, whose Registry transport is `http://127.0.0.1:8080/mcp` by default. If you set `PORT`, replace `8080` with that exact port.
84
+ - The `npx` launcher persists its local KV and Durable Object state in `~/.mcp-gsc/state` by default so npm package upgrades or cache cleanup do not discard the encrypted Google session. Set `MCP_GSC_STATE_DIR` to use another directory, and keep the same `TOKEN_ENCRYPTION_KEY` when reusing existing state. The first release with this stable path may require one Google reconnect because older launcher releases stored local state under the npm package cache.
84
85
  - The Worker derives Google's redirect URI from the MCP request URL. Google requires an exact authorized-URI match, so keep the same scheme, host, port, and `/google/callback` path. `localhost` and `127.0.0.1` are different hosts for this check.
85
86
  - The path must be exactly `/google/callback` — that's the route this server handles.
86
87
  - The DigestSEO-hosted instance also requires this exact callback URI in the OAuth client configured on its Worker: `https://mcp-gsc.digestseo.com/google/callback`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digestseo/mcp-gsc",
3
- "version": "0.4.11",
3
+ "version": "0.4.13",
4
4
  "private": false,
5
5
  "main": "scripts/glama-start.mjs",
6
6
  "bin": {
@@ -1,5 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createRequire } from 'node:module';
3
+ import { homedir } from 'node:os';
4
+ import { resolve } from 'node:path';
3
5
  import { fileURLToPath } from 'node:url';
4
6
 
5
7
  const port = process.env.PORT || '8080';
@@ -9,6 +11,9 @@ const googleRedirectUri = `${localBaseUrl}/google/callback`;
9
11
  const wranglerConfig = fileURLToPath(
10
12
  new URL('../wrangler.example.jsonc', import.meta.url),
11
13
  );
14
+ const stateDir = process.env.MCP_GSC_STATE_DIR
15
+ ? resolve(process.env.MCP_GSC_STATE_DIR)
16
+ : resolve(homedir(), '.mcp-gsc', 'state');
12
17
  const forwardedVariables = [
13
18
  'GOOGLE_CLIENT_ID',
14
19
  'GOOGLE_CLIENT_SECRET',
@@ -20,6 +25,8 @@ const args = [
20
25
  'dev',
21
26
  '--config',
22
27
  wranglerConfig,
28
+ '--persist-to',
29
+ stateDir,
23
30
  '--local',
24
31
  '--ip',
25
32
  '127.0.0.1',
@@ -35,6 +42,7 @@ for (const name of forwardedVariables) {
35
42
 
36
43
  console.error(`[mcp-gsc] Local MCP endpoint: ${mcpUrl}`);
37
44
  console.error(`[mcp-gsc] Google OAuth redirect URI: ${googleRedirectUri}`);
45
+ console.error(`[mcp-gsc] Local state directory: ${stateDir}`);
38
46
 
39
47
  const wranglerCli = createRequire(import.meta.url).resolve('wrangler');
40
48
  const child = spawn(process.execPath, [wranglerCli, ...args], { stdio: 'inherit' });
package/src/google.ts CHANGED
@@ -309,6 +309,12 @@ export function assertSearchAnalyticsQueryCompatible(body: SearchAnalyticsQuery)
309
309
  throw new Error('Search Analytics dimensions must not contain duplicates.');
310
310
  }
311
311
 
312
+ if (body.dimensions.includes('searchAppearance') && body.dimensions.length > 1) {
313
+ throw new Error(
314
+ 'Search Analytics searchAppearance must be the only grouping dimension. Discover an appearance value with dimensions ["searchAppearance"], then filter by that value in a separate query when grouping by page, query, country, device, date, or hour.',
315
+ );
316
+ }
317
+
312
318
  const hasHourDimension = body.dimensions.includes('hour');
313
319
  const usesHourlyData = body.dataState === 'hourly_all';
314
320
  if (hasHourDimension && !usesHourlyData) {
@@ -467,7 +473,9 @@ export interface UrlInspectionBatchResult {
467
473
  error?: string;
468
474
  }
469
475
 
470
- export async function inspectUrlsSequentially(
476
+ export const URL_INSPECTION_BATCH_CONCURRENCY = 3;
477
+
478
+ export async function inspectUrlsBoundedConcurrently(
471
479
  accessToken: string,
472
480
  siteUrl: string,
473
481
  inspectionUrls: readonly string[],
@@ -475,31 +483,53 @@ export async function inspectUrlsSequentially(
475
483
  ): Promise<UrlInspectionBatchResult[]> {
476
484
  const results: UrlInspectionBatchResult[] = [];
477
485
 
478
- for (const inspectionUrl of inspectionUrls) {
479
- try {
480
- results.push({
481
- inspectionUrl,
482
- inspectionResult: await inspectUrl(
483
- accessToken,
484
- siteUrl,
485
- inspectionUrl,
486
- languageCode,
487
- ),
488
- });
489
- } catch (error) {
490
- if (error instanceof Error && error.message === GSC_ACCESS_REVOKED_MESSAGE) {
491
- throw error;
492
- }
493
- results.push({
494
- inspectionUrl,
495
- error: error instanceof Error ? error.message : 'URL inspection failed.',
496
- });
486
+ for (
487
+ let offset = 0;
488
+ offset < inspectionUrls.length;
489
+ offset += URL_INSPECTION_BATCH_CONCURRENCY
490
+ ) {
491
+ const chunk = inspectionUrls.slice(offset, offset + URL_INSPECTION_BATCH_CONCURRENCY);
492
+ const chunkResults = await Promise.all(
493
+ chunk.map(async (inspectionUrl): Promise<UrlInspectionBatchResult | Error> => {
494
+ try {
495
+ return {
496
+ inspectionUrl,
497
+ inspectionResult: await inspectUrl(
498
+ accessToken,
499
+ siteUrl,
500
+ inspectionUrl,
501
+ languageCode,
502
+ ),
503
+ };
504
+ } catch (error) {
505
+ if (error instanceof Error && error.message === GSC_ACCESS_REVOKED_MESSAGE) {
506
+ return error;
507
+ }
508
+ return {
509
+ inspectionUrl,
510
+ error: error instanceof Error ? error.message : 'URL inspection failed.',
511
+ };
512
+ }
513
+ }),
514
+ );
515
+
516
+ const accessRevoked = chunkResults.find((result): result is Error => result instanceof Error);
517
+ if (accessRevoked) {
518
+ throw accessRevoked;
497
519
  }
520
+ results.push(
521
+ ...chunkResults.filter(
522
+ (result): result is UrlInspectionBatchResult => !(result instanceof Error),
523
+ ),
524
+ );
498
525
  }
499
526
 
500
527
  return results;
501
528
  }
502
529
 
530
+ // Kept for compatibility with callers that imported the original helper directly.
531
+ export const inspectUrlsSequentially = inspectUrlsBoundedConcurrently;
532
+
503
533
  export interface SitemapEntry {
504
534
  path: string;
505
535
  lastSubmitted?: string | null;
package/src/index.ts CHANGED
@@ -12,7 +12,8 @@ import {
12
12
  GSC_ACCESS_REVOKED_MESSAGE,
13
13
  MCP_RECONNECT_INSTRUCTION,
14
14
  inspectUrl,
15
- inspectUrlsSequentially,
15
+ inspectUrlsBoundedConcurrently,
16
+ URL_INSPECTION_BATCH_CONCURRENCY,
16
17
  listSitemaps,
17
18
  listSites,
18
19
  getSite,
@@ -192,6 +193,12 @@ const CAPABILITIES_OUTPUT_SCHEMA = {
192
193
  description: z.string(),
193
194
  }),
194
195
  ),
196
+ provider_limits: z.object({
197
+ generative_ai_performance_report: z.object({
198
+ dedicated_api_supported: z.literal(false),
199
+ note: z.string(),
200
+ }),
201
+ }),
195
202
  hint: z.string(),
196
203
  };
197
204
 
@@ -242,6 +249,10 @@ const SEARCH_ANALYTICS_OUTPUT_SCHEMA = {
242
249
  'False because the Search Analytics API does not guarantee every data row; Google can return only top rows even after local pagination is exhausted.',
243
250
  ),
244
251
  provider_note: z.string(),
252
+ generative_ai_report_isolatable: z.literal(false).describe(
253
+ 'False because the current documented Search Analytics API exposes no dedicated Generative AI report search type or filter selector.',
254
+ ),
255
+ generative_ai_note: z.string(),
245
256
  next_start_row: z.number().int().nonnegative().optional(),
246
257
  has_more: z.boolean(),
247
258
  truncated: z.boolean(),
@@ -479,6 +490,9 @@ function toolResponse<T extends Record<string, unknown>>(
479
490
  const SEARCH_ANALYTICS_PROVIDER_NOTE =
480
491
  '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.';
481
492
 
493
+ const GENERATIVE_AI_REPORT_API_NOTE =
494
+ "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.";
495
+
482
496
  function paginationMetadata(result: PaginatedSearchAnalyticsResult) {
483
497
  return {
484
498
  rows_fetched: result.rows.length,
@@ -569,7 +583,7 @@ const TOOL_CATALOG = [
569
583
  {
570
584
  name: 'analytics.query',
571
585
  description:
572
- '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.',
586
+ '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. The current documented API does not expose a dedicated Generative AI performance-report selector; do not invent one.',
573
587
  },
574
588
  {
575
589
  name: 'insights.page_queries',
@@ -589,7 +603,7 @@ const TOOL_CATALOG = [
589
603
  {
590
604
  name: 'urls.inspect_many',
591
605
  description:
592
- "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.",
606
+ `Inspect Google's indexed versions of up to 10 URLs with bounded concurrency of ${URL_INSPECTION_BATCH_CONCURRENCY} in one MCP call while charging the same URL Inspection safety budget per URL. This does not run live URL tests.`,
593
607
  },
594
608
  {
595
609
  name: 'sitemaps.list',
@@ -732,6 +746,12 @@ class GscMcpRuntime {
732
746
  access_mode: accessMode,
733
747
  auth_status: authStatus,
734
748
  tools: getToolCatalogForAccessMode(TOOL_CATALOG, accessMode),
749
+ provider_limits: {
750
+ generative_ai_performance_report: {
751
+ dedicated_api_supported: false as const,
752
+ note: GENERATIVE_AI_REPORT_API_NOTE,
753
+ },
754
+ },
735
755
  hint: "If auth_status is not 'connected', the user should reconnect this server in their MCP client to sign in with Google.",
736
756
  };
737
757
  return toolResponse(JSON.stringify(capabilities, null, 2), capabilities);
@@ -836,7 +856,7 @@ class GscMcpRuntime {
836
856
  'urls.inspect_many',
837
857
  {
838
858
  title: 'Inspect multiple URLs',
839
- 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.`,
859
+ 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 use bounded concurrency of ${URL_INSPECTION_BATCH_CONCURRENCY}; results preserve input order, and a Google-access revocation stops later chunks from starting. Use this for a small group of important or debugging-target URLs; do not use it to crawl an entire site.`,
840
860
  inputSchema: {
841
861
  site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
842
862
  inspection_urls: z
@@ -873,7 +893,7 @@ class GscMcpRuntime {
873
893
  if (rateLimitError) return rateLimitError;
874
894
 
875
895
  const accessToken = await this.getAccessToken(googleId);
876
- const batchResults = await inspectUrlsSequentially(
896
+ const batchResults = await inspectUrlsBoundedConcurrently(
877
897
  accessToken,
878
898
  site_url,
879
899
  inspection_urls,
@@ -985,10 +1005,29 @@ class GscMcpRuntime {
985
1005
  ' data_state to "hourly_all". The hourly date range can span at',
986
1006
  ' most 10 inclusive days, and hourly data',
987
1007
  ' is preliminary.',
1008
+ '- SEARCH APPEARANCE uses a two-step workflow. To discover the',
1009
+ ' property-specific appearance values Google currently exposes,',
1010
+ ' call with dimensions: ["searchAppearance"] and no other grouping',
1011
+ ' dimension. Then filter to one exact returned searchAppearance',
1012
+ ' value in a separate query and group by page/query/country/device/',
1013
+ ' date/hour as needed. Do not guess new appearance identifiers;',
1014
+ ' discover the values Google returns for the property first.',
1015
+ '- GENERATIVE AI REPORT: Search Console now has dedicated',
1016
+ ' Generative AI performance reports in its UI, but the current',
1017
+ ' documented Search Analytics API exposes no dedicated Generative',
1018
+ ' AI search type or filter selector. AI Overviews and AI Mode remain',
1019
+ ' included in overall web Search performance data. Do not guess a',
1020
+ ' searchAppearance identifier or claim analytics.query isolates the',
1021
+ ' dedicated Generative AI report.',
988
1022
  '- Use dimension_filter_groups to filter by country, device, query',
989
1023
  ' content, page URL, or search feature. includingRegex and',
990
- ' excludingRegex use RE2 syntax. For brand vs non-brand splits,',
991
- " pass a single regex filter on the 'query' dimension.",
1024
+ ' excludingRegex use RE2 syntax. A query regex can provide a manual',
1025
+ ' brand/non-brand approximation when the caller supplies the brand',
1026
+ " pattern. It is not equivalent to Search Console's native Branded/",
1027
+ ' Non-branded filter: Google describes that classification as',
1028
+ ' AI-assisted (including language variants, typos, and associated',
1029
+ ' products/services), and the Search Analytics API does not expose',
1030
+ ' that native classifier as a filter.',
992
1031
  "- data_state defaults to 'all' which matches the GSC dashboard.",
993
1032
  " Pass 'final' only when the user explicitly asks for stable,",
994
1033
  ' non-preliminary data.',
@@ -1013,7 +1052,7 @@ class GscMcpRuntime {
1013
1052
  )
1014
1053
  .default(['query'])
1015
1054
  .describe(
1016
- "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 and Google News do not support the query dimension.",
1055
+ "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.",
1017
1056
  ),
1018
1057
  row_limit: z
1019
1058
  .number()
@@ -1041,7 +1080,7 @@ class GscMcpRuntime {
1041
1080
  search_type: z
1042
1081
  .enum(['web', 'image', 'video', 'news', 'discover', 'googleNews'])
1043
1082
  .default('web')
1044
- .describe('Which search index to query. Defaults to web. Google Discover and Google News do not support query grouping/filtering or average position.'),
1083
+ .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.'),
1045
1084
  aggregation_type: z
1046
1085
  .enum(['auto', 'byNewsShowcasePanel', 'byPage', 'byProperty'])
1047
1086
  .default('auto')
@@ -1135,6 +1174,8 @@ class GscMcpRuntime {
1135
1174
  : {}),
1136
1175
  provider_exhaustiveness_guaranteed: false,
1137
1176
  provider_note: SEARCH_ANALYTICS_PROVIDER_NOTE,
1177
+ generative_ai_report_isolatable: false,
1178
+ generative_ai_note: GENERATIVE_AI_REPORT_API_NOTE,
1138
1179
  has_more: bounded.resultPage.has_more,
1139
1180
  truncated: bounded.resultPage.truncated,
1140
1181
  byte_limit_reached: bounded.resultPage.byte_limit_reached,
@@ -1732,7 +1773,7 @@ class GscMcpRuntime {
1732
1773
  )
1733
1774
  .optional()
1734
1775
  .describe(
1735
- "Optional Search Console filters applied identically to both periods. Use query regex filters for brand/non-brand comparisons; countries use ISO 3166-1 alpha-3 codes.",
1776
+ "Optional Search Console filters applied identically to both periods. A caller-supplied query regex can approximate a manual brand/non-brand split, but it is not equivalent to Search Console's AI-assisted native Branded/Non-branded filter, which the Search Analytics API does not expose. Countries use ISO 3166-1 alpha-3 codes.",
1736
1777
  ),
1737
1778
  limit: ANALYSIS_RESULT_LIMIT_SCHEMA,
1738
1779
  start_row: RESULT_START_ROW_SCHEMA,