@digestseo/mcp-gsc 0.4.36 → 0.4.38

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,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.38] - 2026-10-25
11
+
12
+ ### Changed
13
+ - Align Indexing API quotas, structured tool metadata and setup guidance with Google's October 16, 2026 documentation update: 200 default publish requests/day/project, removal of the outdated quota-approval form, and unchanged content eligibility, ownership, spam and quota restrictions.
14
+
15
+ ### Fixed
16
+ - Docker/Glama images now bind Wrangler to container interfaces when explicitly configured by the Docker image, restoring reachability through Docker-published ports while leaving direct npm launches loopback-only by default. Invalid custom bind addresses fail closed.
17
+ - Docker images now install system CA certificates so the Cloudflare workerd runtime can initialize TLS; CI builds and runs a real container, checks host-forwarded health, and confirms unauthenticated MCP still returns 401.
18
+
19
+ ## [0.4.37] - 2026-10-24
20
+
21
+ ### Security
22
+ - Refresh the Cloudflare Agents/MCP runtime peer set to patched MCP SDK, client, and server versions, and raise the minimum Zod version for current security advisories.
23
+
24
+ ### Changed
25
+ - Local launcher setup now provides a cross-platform Node.js command for generating `TOKEN_ENCRYPTION_KEY`, and missing/invalid-key errors surface the same recovery command without logging secret values or requiring OpenSSL.
26
+ - `analytics.compare` now accepts one shared Search Analytics `data_state` for both periods while preserving Google's finalized-data default; callers can explicitly request `all` when they want recent preliminary-capable rows.
27
+
10
28
  ## [0.4.36] - 2026-09-24
11
29
 
12
30
  ### Changed
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). The launcher exits before starting Wrangler if `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, or `TOKEN_ENCRYPTION_KEY` is missing or blank, and it also rejects a nonblank `TOKEN_ENCRYPTION_KEY` unless it is valid base64 that decodes to exactly 32 bytes. Errors identify only the invalid or missing setting, never its secret value, so a broken OAuth session is not started. It keeps secret values in the child process environment and Wrangler loads them through declared local secrets; their values are not copied into Wrangler `--var` command-line arguments. 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
+ **Docker/Glama network binding.** The ordinary `npx` launcher stays loopback-only by default (`MCP_GSC_BIND_IP=127.0.0.1`). The Docker image explicitly sets `MCP_GSC_BIND_IP=0.0.0.0` **inside the container**, because Docker port forwarding cannot reach a service bound only to the container's loopback interface. When publishing a Docker port on your computer, prefer `-p 127.0.0.1:8080:8080` to restrict host access; expose it beyond the host only with appropriate HTTPS/OAuth deployment configuration and an authorized Google redirect URI. `MCP_GSC_BIND_IP` accepts only `127.0.0.1` or `0.0.0.0`; do not use all-interface binding for ordinary local npm usage.
28
+
27
29
  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
30
 
29
31
  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 six mutation tools stay unregistered.
@@ -114,12 +116,12 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 23 tools. Read-onl
114
116
  | **`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. |
115
117
  | **`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. |
116
118
  | **`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. |
117
- | **`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. |
119
+ | **`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. It defaults to finalized Search Analytics rows with `data_state=final`, preserving Google's omitted-`dataState` behavior; pass `all` explicitly when recent preliminary-capable rows are desired. 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. |
118
120
  | **`reports.weekly_digest`** | Read | Generate a plain-language seven-day performance report with movers, top pages, and one recommended action. Query movers compare only rows returned in both weekly Search Analytics result sets; a query missing from one bounded response is not treated as zero because Google does not guarantee every data row. |
119
121
  | **`sites.add`** / **`sites.delete`** | Write | Add or remove a property from the connected account's Search Console site set. `sites.add` returns `ownership_verification_performed: false` because ownership verification is a separate Google Site Verification/Search Console workflow. `sites.delete` removes the property from that account's Search Console site set; it does not delete the website itself. |
120
122
  | **`sitemaps.submit`** / **`sitemaps.delete`** | Write | Submit or remove a sitemap. |
121
123
  | **`indexing.status`** | Read | Read Google's latest successful Indexing API `URL_UPDATED` / `URL_DELETED` notification receipts for a previously submitted URL. This is notification metadata only, not index coverage, crawl status, indexing completion, or removal completion. It requires `GSC_ACCESS_MODE=readwrite` because Google's metadata endpoint uses the Indexing API OAuth scope. |
122
- | **`indexing.request`** | Write | Requests indexing through Google's Indexing API. Google currently restricts this API to pages containing `JobPosting` structured data or livestream pages containing `BroadcastEvent` inside `VideoObject`. It is not available for general webpage submission. Google's default 200 publish requests/day/project is onboarding/testing capacity rather than ongoing-use approval; additional approval is required for usage/resource provisioning, and submissions are spam-screened. |
124
+ | **`indexing.request`** | Write | Requests indexing through Google's Indexing API. Google currently restricts this API to pages containing `JobPosting` structured data or livestream pages containing `BroadcastEvent` inside `VideoObject`. It is not available for general webpage submission. Google's documented default is 200 publish requests/day/project shared by updates and removals. Eligible content types, API authorization, quota limits and spam policies still apply; this quota is not permission to submit other page types or exceed Google's limits. |
123
125
  | **`indexing.remove`** | Write | Request a `URL_DELETED` notification for a previously eligible Indexing API page. Before removal, the URL must already return HTTP 404/410 or expose a robots `noindex` meta directive. Google's receipt does not prove removal completed; use URL Inspection for indexed-state evidence. |
124
126
 
125
127
  Search Analytics calendar dates (`start_date`, `end_date`, and comparison-period dates) use Google's Pacific Time calendar (`America/Los_Angeles`, UTC-8/UTC-7 depending on daylight saving time), not the caller's local timezone or UTC. This also governs the server's generated/default Search Analytics date ranges.
@@ -130,7 +132,7 @@ Read-write mode requests the Google Search Console read-write and Indexing API s
130
132
 
131
133
  Large analytics responses are bounded at the MCP boundary instead of being generated and then discarded by clients with structured-content limits. List-style tools expose `start_row` / `next_start_row` where applicable; ranked analytical tools expose `limit`, `start_row`, and `result_page` metadata including `has_more`, `truncated`, and `byte_limit_reached`. Continue paging while `has_more` is true rather than assuming one response is complete.
132
134
 
133
- > **Indexing API eligibility and provider approval.** Google's Indexing API is not a general-purpose page submission tool — as of this writing, Google's own documentation limits it to two content types: pages with `JobPosting` structured data, and livestream pages with `BroadcastEvent` structured data nested inside `VideoObject`. Before `indexing.request`, the server fetches the target URL and checks static JSON-LD, Microdata, and RDFa markup for one of those two types; if neither is present (or the page can't be fetched), it returns an error explaining why the URL is ineligible instead of calling the Indexing API. Structured data injected only after client-side JavaScript runs cannot be confirmed by this bounded preflight and therefore fails closed. Before `indexing.remove`, the server instead confirms Google's removal precondition: the URL must already return HTTP 404/410 or expose a robots `noindex` meta directive. Update and removal notifications share the same publish quota. Google's default 200 publish requests/day/project is for onboarding and submission testing; using the API beyond that onboarding/testing context requires Google's usage/resource-provisioning approval. Google also subjects every submission to spam detection and warns that abuse or attempts to exceed quotas through multiple accounts or other means can revoke access. A successful notification is only an acknowledgment that Google received it — it does not guarantee indexing or removal completed.
135
+ > **Indexing API eligibility and quotas.** Google's Indexing API is not a general-purpose page submission tool — as of this writing, Google's own documentation limits it to two content types: pages with `JobPosting` structured data, and livestream pages with `BroadcastEvent` structured data nested inside `VideoObject`. Before `indexing.request`, the server fetches the target URL and checks static JSON-LD, Microdata, and RDFa markup for one of those two types; if neither is present (or the page can't be fetched), it returns an error explaining why the URL is ineligible instead of calling the Indexing API. Structured data injected only after client-side JavaScript runs cannot be confirmed by this bounded preflight and therefore fails closed. Before `indexing.remove`, the server instead confirms Google's removal precondition: the URL must already return HTTP 404/410 or expose a robots `noindex` meta directive. Update and removal notifications share the same publish quota. Google's current documentation lists a default 200 publish requests/day/project. Its October 16, 2026 update removed an outdated quota request form, but this does not authorize exceeding published quotas or submitting unsupported content. Check your project's API access and current quota in Google Cloud Console. Google also subjects every submission to spam detection and warns that abuse or attempts to exceed quotas through multiple accounts or other means can revoke access. A successful notification is only an acknowledgment that Google received it — it does not guarantee indexing or removal completed.
134
136
 
135
137
  ## What you can ask
136
138
 
package/SETUP.md CHANGED
@@ -61,7 +61,7 @@ npx wrangler login
61
61
 
62
62
  `readwrite` preserves the full tool surface: it grants Search Console read-write access and the Indexing API scope required to manage sites, sitemaps, and request URL crawling. `readonly` requests only the Search Console read-only scope and omits the mutation tools.
63
63
 
64
- Note that the Indexing API itself is narrow: Google currently restricts it to pages containing `JobPosting` structured data or livestream pages containing `BroadcastEvent` inside `VideoObject`. It is not available for general webpage submission - the `indexing.request` tool checks a page's structured data before submitting and returns an error for ineligible URLs. `indexing.remove` is for previously eligible pages and requires the URL to already return HTTP 404/410 or expose a robots `noindex` meta directive before it sends Google's `URL_DELETED` notification. Google's default **200 publish requests/day/project** is shared by update and removal notifications and is explicitly for onboarding and submission testing; ongoing usage/resource provisioning requires additional Google approval. All submissions are subject to spam detection, and Google warns that abuse or attempts to exceed quotas through multiple accounts or other means can result in revoked access. The server's own lower safety limits do not constitute Google approval or increase provider quota.
64
+ Note that the Indexing API itself is narrow: Google currently restricts it to pages containing `JobPosting` structured data or livestream pages containing `BroadcastEvent` inside `VideoObject`. It is not available for general webpage submission - the `indexing.request` tool checks a page's structured data before submitting and returns an error for ineligible URLs. `indexing.remove` is for previously eligible pages and requires the URL to already return HTTP 404/410 or expose a robots `noindex` meta directive before it sends Google's `URL_DELETED` notification. Google's documented default **200 publish requests/day/project** is shared by update and removal notifications. The October 16, 2026 Google documentation update removed an outdated quota request form; check project-specific API access and quotas in Google Cloud Console, and never exceed permitted limits. All submissions are subject to spam detection, and Google warns that abuse or attempts to exceed quotas through multiple accounts or other means can result in revoked access. The server's own lower safety limits do not increase Google quota or bypass provider restrictions.
65
65
  5. Save the Data Access changes. In **Google Auth platform → Audience**, leave **Publishing status** as **Testing** for now — see [Step 7](#step-7--important-google-verification).
66
66
 
67
67
  ---
@@ -103,15 +103,17 @@ npx wrangler secret put TOKEN_ENCRYPTION_KEY
103
103
  # paste the key generated below
104
104
  ```
105
105
 
106
- `TOKEN_ENCRYPTION_KEY` is the **AES-256 key** used by `src/crypto.ts` to encrypt each user's Google refresh token before it's stored in KV. Generate a fresh 32-byte key, base64-encoded:
106
+ `TOKEN_ENCRYPTION_KEY` is the **AES-256 key** used by `src/crypto.ts` to encrypt each user's Google refresh token before it's stored in KV. Generate a fresh 32-byte key, base64-encoded. Node.js is already required by this project, so this command works on Windows, macOS, and Linux without an extra OpenSSL install:
107
107
 
108
108
  ```bash
109
- openssl rand -base64 32
109
+ node -e "console.log(require('node:crypto').randomBytes(32).toString('base64'))"
110
110
  ```
111
111
 
112
+ If OpenSSL is already installed, `openssl rand -base64 32` is equivalent.
113
+
112
114
  Paste that value when prompted. Keep it safe — if you lose it, the refresh tokens already in KV become undecryptable and every user has to reconnect.
113
115
 
114
- The published `npx` launcher validates this exact key format before starting Wrangler. A malformed base64 value or a value that does not decode to exactly 32 bytes is rejected without printing the secret value.
116
+ The published `npx` launcher validates this exact key format before starting Wrangler. A missing or malformed key is rejected without printing the secret value, and the error repeats the cross-platform Node.js generation command so local setup can recover without another tool dependency.
115
117
 
116
118
  For **local development**, put the same three values in `.dev.vars` instead — copy `.dev.vars.example` to `.dev.vars` (it's gitignored). The published npm launcher can also read them from the process environment; its bundled Wrangler config declares `GOOGLE_CLIENT_SECRET` and `TOKEN_ENCRYPTION_KEY` as required local secrets so those sensitive values do not need to be serialized into `--var` command-line arguments:
117
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digestseo/mcp-gsc",
3
- "version": "0.4.36",
3
+ "version": "0.4.38",
4
4
  "private": false,
5
5
  "main": "scripts/glama-start.mjs",
6
6
  "bin": {
@@ -75,10 +75,10 @@
75
75
  "mcpName": "io.github.AKzar1el/mcp-gsc",
76
76
  "dependencies": {
77
77
  "@cloudflare/workers-oauth-provider": "^0.10.3",
78
- "@modelcontextprotocol/server": "2.0.0",
79
- "agents": "^0.24.0",
78
+ "@modelcontextprotocol/server": "2.3.1",
79
+ "agents": "0.29.0",
80
80
  "wrangler": "^4.136.3",
81
- "zod": "^4.4.3"
81
+ "zod": "^4.6.7"
82
82
  },
83
83
  "devDependencies": {
84
84
  "@cloudflare/vitest-plugin": "^1.2.3",
@@ -4,6 +4,8 @@ import { constants, homedir } from 'node:os';
4
4
  import { resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
 
7
+ const tokenEncryptionKeyGenerationCommand =
8
+ `node -e "console.log(require('node:crypto').randomBytes(32).toString('base64'))"`;
7
9
  const requiredEnvironmentVariables = [
8
10
  'GOOGLE_CLIENT_ID',
9
11
  'GOOGLE_CLIENT_SECRET',
@@ -13,8 +15,13 @@ const missingRequiredEnvironmentVariables = requiredEnvironmentVariables.filter(
13
15
  (name) => !process.env[name]?.trim(),
14
16
  );
15
17
  if (missingRequiredEnvironmentVariables.length > 0) {
18
+ const tokenEncryptionKeyHint = missingRequiredEnvironmentVariables.includes(
19
+ 'TOKEN_ENCRYPTION_KEY',
20
+ )
21
+ ? ` Generate one with: ${tokenEncryptionKeyGenerationCommand}`
22
+ : '';
16
23
  console.error(
17
- `[mcp-gsc] Missing required environment variables: ${missingRequiredEnvironmentVariables.join(', ')}. See SETUP.md for local launcher configuration.`,
24
+ `[mcp-gsc] Missing required environment variables: ${missingRequiredEnvironmentVariables.join(', ')}. See SETUP.md for local launcher configuration.${tokenEncryptionKeyHint}`,
18
25
  );
19
26
  process.exit(1);
20
27
  }
@@ -27,11 +34,18 @@ if (
27
34
  || Buffer.from(tokenEncryptionKey, 'base64').byteLength !== 32
28
35
  ) {
29
36
  console.error(
30
- '[mcp-gsc] Invalid TOKEN_ENCRYPTION_KEY: expected a valid base64-encoded 32-byte (256-bit) AES key. See SETUP.md for local launcher configuration.',
37
+ `[mcp-gsc] Invalid TOKEN_ENCRYPTION_KEY: expected a valid base64-encoded 32-byte (256-bit) AES key. See SETUP.md for local launcher configuration. Generate one with: ${tokenEncryptionKeyGenerationCommand}`,
31
38
  );
32
39
  process.exit(1);
33
40
  }
34
41
 
42
+ const bindIp = process.env.MCP_GSC_BIND_IP?.trim() || '127.0.0.1';
43
+ if (bindIp !== '127.0.0.1' && bindIp !== '0.0.0.0') {
44
+ console.error(
45
+ '[mcp-gsc] Invalid MCP_GSC_BIND_IP: expected 127.0.0.1 (safe local default) or 0.0.0.0 (explicit container/network binding).',
46
+ );
47
+ process.exit(1);
48
+ }
35
49
  const port = process.env.PORT || '8080';
36
50
  const localBaseUrl = `http://127.0.0.1:${port}`;
37
51
  const mcpUrl = `${localBaseUrl}/mcp`;
@@ -55,7 +69,7 @@ const args = [
55
69
  stateDir,
56
70
  '--local',
57
71
  '--ip',
58
- '127.0.0.1',
72
+ bindIp,
59
73
  '--port',
60
74
  port,
61
75
  ];
package/src/index.ts CHANGED
@@ -316,7 +316,7 @@ const SITE_ADD_VERIFICATION_NOTE =
316
316
  "Google Search Console Sites.add only adds the property to the user's Search Console site set; it does not verify ownership. Ownership verification is a separate Google Site Verification/Search Console workflow.";
317
317
 
318
318
  const INDEXING_PROVIDER_USAGE_NOTE =
319
- "Google describes the Indexing API's default 200 publish-requests-per-day project quota as onboarding/testing capacity, not approval for ongoing usage. Additional approval is required for usage/resource provisioning. Google also applies spam detection to all submissions and warns that abuse or attempts to exceed quotas through multiple accounts or other means can result in revoked access.";
319
+ "Google's current Indexing API quota documentation lists 200 publish requests per day per project, shared by update and removal notifications. Google's October 16, 2026 update removed the previous quota-approval request form; this does not authorize exceeding Google's quota, submitting ineligible content, or bypassing required project/API permissions. Google applies spam detection and may revoke access for abuse or attempts to circumvent limits.";
320
320
 
321
321
  const URL_INSPECTION_SCOPE_NOTE =
322
322
  "Google's URL Inspection API reports the version currently known in Google's index; it does not run a live URL test or prove current live-page indexability. The mobileUsabilityResult field is deprecated and may be absent.";
@@ -607,8 +607,9 @@ const CONTENT_DECAY_OUTPUT_SCHEMA = {
607
607
  const INDEXING_OUTPUT_SCHEMA = {
608
608
  result: z.unknown(),
609
609
  note: z.string(),
610
- provider_default_quota_for_testing_only: z.literal(true),
611
- provider_usage_approval_required: z.literal(true),
610
+ provider_default_publish_requests_per_day: z.literal(200),
611
+ provider_default_quota_for_testing_only: z.literal(false),
612
+ provider_usage_approval_required: z.literal(false),
612
613
  provider_spam_detection_applies: z.literal(true),
613
614
  provider_usage_note: z.string(),
614
615
  };
@@ -654,6 +655,12 @@ const COMMON_RETURNED_ROWS_COMPARISON_NOTE =
654
655
  const PERFORMANCE_COMPARISON_OUTPUT_SCHEMA = {
655
656
  comparison_scope: z.literal('common_returned_rows_only'),
656
657
  comparison_note: z.string(),
658
+ data_state: z.enum(['all', 'final']).describe(
659
+ 'The Search Analytics dataState applied identically to both comparison periods.',
660
+ ),
661
+ preliminary_data_possible: z.boolean().describe(
662
+ 'True when data_state=all, because recent Search Analytics rows can still be incomplete and subject to change.',
663
+ ),
657
664
  comparisons: z.array(
658
665
  z.object({
659
666
  key: z.string(),
@@ -863,7 +870,7 @@ const TOOL_CATALOG = [
863
870
  {
864
871
  name: 'indexing.request',
865
872
  description:
866
- 'Request an eligible JobPosting or livestream URL update through Google\'s restricted Indexing API; this is not a general webpage submission tool. Google treats the default publish quota as onboarding/testing capacity, requires approval for ongoing usage/resource provisioning, and spam-screens submissions.',
873
+ 'Request an eligible JobPosting or livestream URL update through Google\'s restricted Indexing API; this is not a general webpage submission tool. Google documents a default quota of 200 publish requests/day/project; eligible content restrictions, project permissions, quota limits, and spam screening still apply.',
867
874
  },
868
875
  {
869
876
  name: 'indexing.remove',
@@ -882,7 +889,7 @@ const TOOL_CATALOG = [
882
889
  {
883
890
  name: 'analytics.compare',
884
891
  description:
885
- 'Compare Search Console performance metrics between two date ranges for dimension keys returned in both Search Analytics responses; one-sided row absence is not treated as zero.',
892
+ 'Compare Search Console performance metrics between two date ranges for dimension keys returned in both Search Analytics responses; one-sided row absence is not treated as zero. The same search type, optional finalized/all data state, and filters are applied to both periods.',
886
893
  },
887
894
  {
888
895
  name: 'reports.weekly_digest',
@@ -2027,7 +2034,7 @@ class GscMcpRuntime {
2027
2034
  'indexing.request',
2028
2035
  {
2029
2036
  title: 'Request Indexing',
2030
- description: "Requests indexing through Google's Indexing API for a URL within an owner-level Search Console property accessible to the connected Google account. Google currently restricts this API to pages containing JobPosting structured data or livestream pages containing BroadcastEvent inside VideoObject. It is not available for general webpage submission. Google's default 200 publish-requests-per-day project quota is for onboarding/testing rather than ongoing-use approval; additional approval is required for usage/resource provisioning, and all submissions are subject to spam detection.",
2037
+ description: "Requests indexing through Google's Indexing API for a URL within an owner-level Search Console property accessible to the connected Google account. Google currently restricts this API to pages containing JobPosting structured data or livestream pages containing BroadcastEvent inside VideoObject. It is not available for general webpage submission. Google documents a default of 200 publish requests/day/project, shared by update and removal notifications. Eligibility, project authorization, quota limits and spam detection still apply; the published default does not authorize higher-volume or ineligible submissions.",
2031
2038
  inputSchema: {
2032
2039
  site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA.describe(`${SEARCH_CONSOLE_PROPERTY_DESCRIPTION} The connected Google account must be an owner of this property.`),
2033
2040
  url: z.string().superRefine((value, ctx) => {
@@ -2056,8 +2063,9 @@ class GscMcpRuntime {
2056
2063
  const payload = {
2057
2064
  result,
2058
2065
  note: "Google accepting this notification does not guarantee the URL will be indexed. Indexing remains at Google's discretion.",
2059
- provider_default_quota_for_testing_only: true as const,
2060
- provider_usage_approval_required: true as const,
2066
+ provider_default_publish_requests_per_day: 200 as const,
2067
+ provider_default_quota_for_testing_only: false as const,
2068
+ provider_usage_approval_required: false as const,
2061
2069
  provider_spam_detection_applies: true as const,
2062
2070
  provider_usage_note: INDEXING_PROVIDER_USAGE_NOTE,
2063
2071
  };
@@ -2098,8 +2106,9 @@ class GscMcpRuntime {
2098
2106
  const payload = {
2099
2107
  result,
2100
2108
  note: "Google accepting this URL_DELETED notification only confirms receipt. It does not prove the URL was removed from Google's index; use URL Inspection for indexed-state evidence.",
2101
- provider_default_quota_for_testing_only: true as const,
2102
- provider_usage_approval_required: true as const,
2109
+ provider_default_publish_requests_per_day: 200 as const,
2110
+ provider_default_quota_for_testing_only: false as const,
2111
+ provider_usage_approval_required: false as const,
2103
2112
  provider_spam_detection_applies: true as const,
2104
2113
  provider_usage_note: INDEXING_PROVIDER_USAGE_NOTE,
2105
2114
  removal_completion_proven: false as const,
@@ -2182,7 +2191,7 @@ class GscMcpRuntime {
2182
2191
  'analytics.compare',
2183
2192
  {
2184
2193
  title: 'Compare Performance Between Periods',
2185
- description: 'Compare Search Console performance metrics (clicks, impressions, CTR, average position) between two distinct date ranges (Period A vs Period B) for a selected dimension (query, page, country, device). Only dimension keys returned in both Search Analytics period responses are compared; a key missing from one response is not treated as zero because Google does not guarantee every data row. Apply the same search type and optional dimension filters to both periods. Results are ranked by largest absolute click change, then impression change, and safely paged with limit/start_row. Percentage change is null when an explicitly returned baseline row has zero and the comparison value differs. Discover and Google News are intentionally unavailable because this comparison contract includes average position, which those reports do not support.',
2194
+ description: 'Compare Search Console performance metrics (clicks, impressions, CTR, average position) between two distinct date ranges (Period A vs Period B) for a selected dimension (query, page, country, device). Only dimension keys returned in both Search Analytics period responses are compared; a key missing from one response is not treated as zero because Google does not guarantee every data row. Apply the same search type, data state, and optional dimension filters to both periods. data_state defaults to final, preserving Google\'s finalized-only behavior when dataState is omitted; use all explicitly when recent preliminary-capable rows are desired. Results are ranked by largest absolute click change, then impression change, and safely paged with limit/start_row. Percentage change is null when an explicitly returned baseline row has zero and the comparison value differs. Discover and Google News are intentionally unavailable because this comparison contract includes average position, which those reports do not support.',
2186
2195
  inputSchema: {
2187
2196
  site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
2188
2197
  start_date_a: SEARCH_CONSOLE_DATE_SCHEMA.describe(
@@ -2202,6 +2211,12 @@ class GscMcpRuntime {
2202
2211
  .enum(['web', 'image', 'video', 'news'])
2203
2212
  .default('web')
2204
2213
  .describe('Which Search Console search index to compare. The same search type is used for both periods. Discover and Google News are excluded because this tool returns average-position comparisons.'),
2214
+ data_state: z
2215
+ .enum(['all', 'final'])
2216
+ .default('final')
2217
+ .describe(
2218
+ "'final' requests finalized data only and preserves Google's behavior when dataState is omitted; 'all' can include fresh preliminary Search Analytics data. The same state is applied to both comparison periods.",
2219
+ ),
2205
2220
  dimension_filter_groups: z
2206
2221
  .array(SEARCH_ANALYTICS_FILTER_GROUP_SCHEMA)
2207
2222
  .optional()
@@ -2222,6 +2237,7 @@ class GscMcpRuntime {
2222
2237
  end_date_b,
2223
2238
  dimension,
2224
2239
  search_type,
2240
+ data_state,
2225
2241
  dimension_filter_groups,
2226
2242
  limit = DEFAULT_ANALYSIS_RESULT_LIMIT,
2227
2243
  start_row = 0,
@@ -2241,6 +2257,7 @@ class GscMcpRuntime {
2241
2257
  endDate: end_date_a,
2242
2258
  dimensions: [dimension],
2243
2259
  type: search_type,
2260
+ dataState: data_state,
2244
2261
  ...(dimension_filter_groups !== undefined
2245
2262
  ? { dimensionFilterGroups: dimension_filter_groups }
2246
2263
  : {}),
@@ -2251,6 +2268,7 @@ class GscMcpRuntime {
2251
2268
  endDate: end_date_b,
2252
2269
  dimensions: [dimension],
2253
2270
  type: search_type,
2271
+ dataState: data_state,
2254
2272
  ...(dimension_filter_groups !== undefined
2255
2273
  ? { dimensionFilterGroups: dimension_filter_groups }
2256
2274
  : {}),
@@ -2261,6 +2279,8 @@ class GscMcpRuntime {
2261
2279
  const payload = {
2262
2280
  comparison_scope: 'common_returned_rows_only' as const,
2263
2281
  comparison_note: COMMON_RETURNED_ROWS_COMPARISON_NOTE,
2282
+ data_state,
2283
+ preliminary_data_possible: data_state !== 'final',
2264
2284
  comparisons: bounded.items,
2265
2285
  pagination: {
2266
2286
  period_a: paginationMetadata(sourceA),