@digestseo/mcp-gsc 0.4.12 → 0.4.14

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,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.14] - 2026-09-22
11
+
12
+ ### Fixed
13
+ - `sites.add` now explicitly reports that adding a Search Console property does not perform ownership verification, preventing agents from confusing the Search Console Sites API with Google's separate Site Verification workflow.
14
+ - MCP write annotations now classify `sites.add` and `sitemaps.submit` as additive (`destructiveHint: false`) while keeping the delete tools destructive, matching the protocol's risk vocabulary instead of labeling every external mutation as destructive.
15
+
16
+ ## [0.4.13] - 2026-09-22
17
+
18
+ ### Fixed
19
+ - 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.
20
+ - 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.
21
+ - 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.
22
+
10
23
  ## [0.4.12] - 2026-09-22
11
24
 
12
25
  ### 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,7 +77,7 @@ 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. 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. |
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
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. |
@@ -83,7 +85,7 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 21 tools. Read-onl
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. |
85
87
  | **`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. |
86
- | **`sites.add`** / **`sites.delete`** | Write | Add or remove a Search Console property. |
88
+ | **`sites.add`** / **`sites.delete`** | Write | Add or remove a Search Console property. `sites.add` only adds the property to the connected account's Search Console site set; it returns `ownership_verification_performed: false` because ownership verification is a separate Google Site Verification/Search Console workflow. |
87
89
  | **`sitemaps.submit`** / **`sitemaps.delete`** | Write | Submit or remove a sitemap. |
88
90
  | **`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. |
89
91
 
@@ -110,6 +112,8 @@ Once connected, ask your assistant things like:
110
112
 
111
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.
112
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
+
113
117
  ## Connect in your AI client
114
118
 
115
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/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.12",
3
+ "version": "0.4.14",
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/index.ts CHANGED
@@ -193,6 +193,12 @@ const CAPABILITIES_OUTPUT_SCHEMA = {
193
193
  description: z.string(),
194
194
  }),
195
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
+ }),
196
202
  hint: z.string(),
197
203
  };
198
204
 
@@ -204,6 +210,9 @@ const SITE_DETAIL_OUTPUT_SCHEMA = {
204
210
  site: z.object(SITE_OUTPUT_SCHEMA),
205
211
  };
206
212
 
213
+ const SITE_ADD_VERIFICATION_NOTE =
214
+ "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.";
215
+
207
216
  const URL_INSPECTION_SCOPE_NOTE =
208
217
  "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.";
209
218
 
@@ -243,6 +252,10 @@ const SEARCH_ANALYTICS_OUTPUT_SCHEMA = {
243
252
  'False because the Search Analytics API does not guarantee every data row; Google can return only top rows even after local pagination is exhausted.',
244
253
  ),
245
254
  provider_note: z.string(),
255
+ generative_ai_report_isolatable: z.literal(false).describe(
256
+ 'False because the current documented Search Analytics API exposes no dedicated Generative AI report search type or filter selector.',
257
+ ),
258
+ generative_ai_note: z.string(),
246
259
  next_start_row: z.number().int().nonnegative().optional(),
247
260
  has_more: z.boolean(),
248
261
  truncated: z.boolean(),
@@ -269,6 +282,12 @@ const MESSAGE_OUTPUT_SCHEMA = {
269
282
  message: z.string(),
270
283
  };
271
284
 
285
+ const SITE_ADD_OUTPUT_SCHEMA = {
286
+ message: z.string(),
287
+ ownership_verification_performed: z.literal(false),
288
+ ownership_verification_note: z.string(),
289
+ };
290
+
272
291
  const SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA = z.object({
273
292
  rows_fetched: z.number().int().nonnegative(),
274
293
  pages_fetched: z.number().int().positive(),
@@ -480,6 +499,9 @@ function toolResponse<T extends Record<string, unknown>>(
480
499
  const SEARCH_ANALYTICS_PROVIDER_NOTE =
481
500
  '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.';
482
501
 
502
+ const GENERATIVE_AI_REPORT_API_NOTE =
503
+ "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.";
504
+
483
505
  function paginationMetadata(result: PaginatedSearchAnalyticsResult) {
484
506
  return {
485
507
  rows_fetched: result.rows.length,
@@ -560,7 +582,7 @@ const TOOL_CATALOG = [
560
582
  {
561
583
  name: 'sites.add',
562
584
  description:
563
- 'Add a new website property to your Google Search Console account.',
585
+ 'Add a website property to the connected Google account\'s Search Console site set. This does not verify ownership; Google handles ownership verification through a separate Site Verification/Search Console workflow.',
564
586
  },
565
587
  {
566
588
  name: 'sites.delete',
@@ -570,7 +592,7 @@ const TOOL_CATALOG = [
570
592
  {
571
593
  name: 'analytics.query',
572
594
  description:
573
- '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.',
595
+ '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.',
574
596
  },
575
597
  {
576
598
  name: 'insights.page_queries',
@@ -733,6 +755,12 @@ class GscMcpRuntime {
733
755
  access_mode: accessMode,
734
756
  auth_status: authStatus,
735
757
  tools: getToolCatalogForAccessMode(TOOL_CATALOG, accessMode),
758
+ provider_limits: {
759
+ generative_ai_performance_report: {
760
+ dedicated_api_supported: false as const,
761
+ note: GENERATIVE_AI_REPORT_API_NOTE,
762
+ },
763
+ },
736
764
  hint: "If auth_status is not 'connected', the user should reconnect this server in their MCP client to sign in with Google.",
737
765
  };
738
766
  return toolResponse(JSON.stringify(capabilities, null, 2), capabilities);
@@ -993,6 +1021,13 @@ class GscMcpRuntime {
993
1021
  ' value in a separate query and group by page/query/country/device/',
994
1022
  ' date/hour as needed. Do not guess new appearance identifiers;',
995
1023
  ' discover the values Google returns for the property first.',
1024
+ '- GENERATIVE AI REPORT: Search Console now has dedicated',
1025
+ ' Generative AI performance reports in its UI, but the current',
1026
+ ' documented Search Analytics API exposes no dedicated Generative',
1027
+ ' AI search type or filter selector. AI Overviews and AI Mode remain',
1028
+ ' included in overall web Search performance data. Do not guess a',
1029
+ ' searchAppearance identifier or claim analytics.query isolates the',
1030
+ ' dedicated Generative AI report.',
996
1031
  '- Use dimension_filter_groups to filter by country, device, query',
997
1032
  ' content, page URL, or search feature. includingRegex and',
998
1033
  ' excludingRegex use RE2 syntax. A query regex can provide a manual',
@@ -1054,7 +1089,7 @@ class GscMcpRuntime {
1054
1089
  search_type: z
1055
1090
  .enum(['web', 'image', 'video', 'news', 'discover', 'googleNews'])
1056
1091
  .default('web')
1057
- .describe('Which search index to query. Defaults to web. Google Discover and Google News do not support query grouping/filtering or average position.'),
1092
+ .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.'),
1058
1093
  aggregation_type: z
1059
1094
  .enum(['auto', 'byNewsShowcasePanel', 'byPage', 'byProperty'])
1060
1095
  .default('auto')
@@ -1148,6 +1183,8 @@ class GscMcpRuntime {
1148
1183
  : {}),
1149
1184
  provider_exhaustiveness_guaranteed: false,
1150
1185
  provider_note: SEARCH_ANALYTICS_PROVIDER_NOTE,
1186
+ generative_ai_report_isolatable: false,
1187
+ generative_ai_note: GENERATIVE_AI_REPORT_API_NOTE,
1151
1188
  has_more: bounded.resultPage.has_more,
1152
1189
  truncated: bounded.resultPage.truncated,
1153
1190
  byte_limit_reached: bounded.resultPage.byte_limit_reached,
@@ -1328,11 +1365,11 @@ class GscMcpRuntime {
1328
1365
  'sites.add',
1329
1366
  {
1330
1367
  title: 'Add Search Console property',
1331
- description: 'Add a new website property to your Google Search Console account. Note: Domain properties require sc-domain prefix (e.g., sc-domain:example.com), URL-prefix properties require full URL (e.g., https://example.com/).',
1368
+ description: 'Add a website property to the connected Google account\'s Search Console site set. This does not verify ownership. Domain properties require an sc-domain prefix (e.g., sc-domain:example.com); URL-prefix properties require a full URL (e.g., https://example.com/).',
1332
1369
  inputSchema: {
1333
1370
  site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1334
1371
  },
1335
- outputSchema: MESSAGE_OUTPUT_SCHEMA,
1372
+ outputSchema: SITE_ADD_OUTPUT_SCHEMA,
1336
1373
  annotations: WRITE_TOOL_ANNOTATIONS['sites.add'],
1337
1374
  },
1338
1375
  async ({ site_url }) => {
@@ -1342,7 +1379,12 @@ class GscMcpRuntime {
1342
1379
  const accessToken = await this.getAccessToken(googleId);
1343
1380
  await addSite(accessToken, site_url);
1344
1381
  const message = `Successfully added site property: ${site_url}`;
1345
- return toolResponse(message, { message });
1382
+ const payload = {
1383
+ message,
1384
+ ownership_verification_performed: false as const,
1385
+ ownership_verification_note: SITE_ADD_VERIFICATION_NOTE,
1386
+ };
1387
+ return toolResponse(`${message}\n\n${SITE_ADD_VERIFICATION_NOTE}`, payload);
1346
1388
  },
1347
1389
  );
1348
1390
 
@@ -1,17 +1,18 @@
1
1
  /**
2
2
  * MCP hints for each mutation exposed by this server.
3
3
  *
4
- * Search Console's sites and sitemaps APIs use PUT for additions and DELETE
5
- * for removals, so repeating the same request has the same intended effect.
6
- * Indexing publishes a URL_UPDATED notification with POST; each call can
7
- * consume quota and update Google's notification state, so it is not marked
8
- * idempotent. Every mutation is marked destructive so MCP clients require
9
- * explicit confirmation before changing external Google state.
4
+ * MCP defines destructiveHint=false for non-read-only tools that perform only
5
+ * additive updates. Search Console's site addition and sitemap submission are
6
+ * additive PUT operations; the corresponding DELETE operations remain
7
+ * destructive. Indexing publishes a URL_UPDATED notification with POST; each
8
+ * call can consume quota and change Google's indexing state, so it remains
9
+ * conservative (destructive + non-idempotent). Tool annotations are client
10
+ * hints, not an enforcement or confirmation mechanism.
10
11
  */
11
12
  export const WRITE_TOOL_ANNOTATIONS = {
12
13
  'sites.add': {
13
14
  readOnlyHint: false,
14
- destructiveHint: true,
15
+ destructiveHint: false,
15
16
  idempotentHint: true,
16
17
  openWorldHint: true,
17
18
  },
@@ -23,7 +24,7 @@ export const WRITE_TOOL_ANNOTATIONS = {
23
24
  },
24
25
  'sitemaps.submit': {
25
26
  readOnlyHint: false,
26
- destructiveHint: true,
27
+ destructiveHint: false,
27
28
  idempotentHint: true,
28
29
  openWorldHint: true,
29
30
  },