@digestseo/mcp-gsc 0.4.17 → 0.4.19
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 +13 -0
- package/README.md +3 -3
- package/SETUP.md +1 -1
- package/package.json +1 -1
- package/scripts/glama-start.mjs +3 -4
- package/src/google.ts +18 -2
- package/src/index.ts +34 -4
- package/wrangler.example.jsonc +3 -0
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.19] - 2026-09-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Search Analytics freshness metadata now normalizes Google's camelCase `firstIncompleteDate` / `firstIncompleteHour` wire fields into the stable MCP `first_incomplete_date` / `first_incomplete_hour` response contract.
|
|
14
|
+
- The npm launcher now exits deterministically after its Wrangler child terminates by signal instead of re-signalling itself while its own `SIGINT`/`SIGTERM` listeners are installed, preventing wrapper processes from lingering after Ctrl+C or supervisor shutdown on POSIX systems.
|
|
15
|
+
- ChatGPT local-launcher guidance now explicitly requires Secure MCP Tunnel for the loopback `127.0.0.1` endpoint, or a separately deployed remote Worker, instead of implying that ChatGPT can connect directly to localhost.
|
|
16
|
+
|
|
17
|
+
## [0.4.18] - 2026-09-22
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- The published npm launcher now keeps `GOOGLE_CLIENT_SECRET` and `TOKEN_ENCRYPTION_KEY` out of Wrangler `--var` command-line arguments; Wrangler loads those declared local secrets directly from the child process environment instead.
|
|
21
|
+
- `server.capabilities` now distinguishes stored local Google credentials from live provider authorization, exposing the credential-state basis and explicitly reporting that the discovery call does not probe Google before claiming a connection is usable.
|
|
22
|
+
|
|
10
23
|
## [0.4.17] - 2026-09-22
|
|
11
24
|
|
|
12
25
|
### Added
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ The self-hosting launcher is available as [`@digestseo/mcp-gsc`](https://www.npm
|
|
|
22
22
|
npx -y @digestseo/mcp-gsc
|
|
23
23
|
```
|
|
24
24
|
|
|
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.
|
|
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 keeps `GOOGLE_CLIENT_SECRET` and `TOKEN_ENCRYPTION_KEY` 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
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
28
|
|
|
@@ -62,7 +62,7 @@ claude mcp add --transport http gsc http://127.0.0.1:8080/mcp
|
|
|
62
62
|
|
|
63
63
|
Start the npm launcher before using either install button. Complete its Google OAuth flow on first use.
|
|
64
64
|
|
|
65
|
-
**ChatGPT** — add
|
|
65
|
+
**ChatGPT** — ChatGPT cannot connect directly to this loopback `127.0.0.1` endpoint. Use OpenAI's **Secure MCP Tunnel** for the local launcher, or deploy your own remote Cloudflare Worker and add that `/mcp` URL as a custom MCP app in Developer mode; see [Connect in your AI client](#connect-in-your-ai-client) below.
|
|
66
66
|
|
|
67
67
|
A self-hostable [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **Google Search Console**. Connect it to Claude.ai, Cursor, ChatGPT, or any MCP-compatible client and ask your AI assistant about your site's organic search performance — impressions, clicks, top queries, index status, and sitemap health — straight from your own Google account.
|
|
68
68
|
|
|
@@ -76,7 +76,7 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 22 tools. Read-onl
|
|
|
76
76
|
|
|
77
77
|
| Tool | Access | What it does |
|
|
78
78
|
|---|---|---|
|
|
79
|
-
| **`server.capabilities`** | Read | List every tool this server exposes and report
|
|
79
|
+
| **`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. |
|
|
80
80
|
| **`sites.list`** | Read | List the Search Console properties the connected Google account can access (`siteUrl`, `permissionLevel`). |
|
|
81
81
|
| **`sites.get`** | Read | Retrieve one exact Search Console property and the connected account's permission level for it. |
|
|
82
82
|
| **`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, 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`. |
|
package/SETUP.md
CHANGED
|
@@ -112,7 +112,7 @@ openssl rand -base64 32
|
|
|
112
112
|
|
|
113
113
|
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.
|
|
114
114
|
|
|
115
|
-
For **local development**, put the same three values in `.dev.vars` instead — copy `.dev.vars.example` to `.dev.vars` (it's gitignored):
|
|
115
|
+
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:
|
|
116
116
|
|
|
117
117
|
```
|
|
118
118
|
GOOGLE_CLIENT_ID=...
|
package/package.json
CHANGED
package/scripts/glama-start.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
3
|
+
import { constants, homedir } from 'node:os';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
@@ -16,8 +16,6 @@ const stateDir = process.env.MCP_GSC_STATE_DIR
|
|
|
16
16
|
: resolve(homedir(), '.mcp-gsc', 'state');
|
|
17
17
|
const forwardedVariables = [
|
|
18
18
|
'GOOGLE_CLIENT_ID',
|
|
19
|
-
'GOOGLE_CLIENT_SECRET',
|
|
20
|
-
'TOKEN_ENCRYPTION_KEY',
|
|
21
19
|
'GSC_ACCESS_MODE',
|
|
22
20
|
'GLAMA_INSPECTION_MODE',
|
|
23
21
|
];
|
|
@@ -53,7 +51,8 @@ process.on('SIGTERM', () => forwardSignal('SIGTERM'));
|
|
|
53
51
|
|
|
54
52
|
child.on('exit', (code, signal) => {
|
|
55
53
|
if (signal) {
|
|
56
|
-
|
|
54
|
+
const signalNumber = constants.signals[signal];
|
|
55
|
+
process.exit(signalNumber === undefined ? 1 : 128 + signalNumber);
|
|
57
56
|
} else {
|
|
58
57
|
process.exit(code ?? 1);
|
|
59
58
|
}
|
package/src/google.ts
CHANGED
|
@@ -407,6 +407,11 @@ export interface SearchAnalyticsResponseMetadata {
|
|
|
407
407
|
first_incomplete_hour?: string;
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
interface GoogleSearchAnalyticsResponseMetadata {
|
|
411
|
+
firstIncompleteDate?: string;
|
|
412
|
+
firstIncompleteHour?: string;
|
|
413
|
+
}
|
|
414
|
+
|
|
410
415
|
export interface SearchAnalyticsResponse {
|
|
411
416
|
rows: SearchAnalyticsRow[];
|
|
412
417
|
responseAggregationType?: string;
|
|
@@ -610,14 +615,25 @@ export async function querySearchAnalytics(
|
|
|
610
615
|
const data = (await resp.json()) as {
|
|
611
616
|
rows?: SearchAnalyticsRow[];
|
|
612
617
|
responseAggregationType?: string;
|
|
613
|
-
metadata?:
|
|
618
|
+
metadata?: GoogleSearchAnalyticsResponseMetadata;
|
|
614
619
|
};
|
|
620
|
+
const metadata: SearchAnalyticsResponseMetadata | undefined =
|
|
621
|
+
data.metadata === undefined
|
|
622
|
+
? undefined
|
|
623
|
+
: {
|
|
624
|
+
...(data.metadata.firstIncompleteDate !== undefined
|
|
625
|
+
? { first_incomplete_date: data.metadata.firstIncompleteDate }
|
|
626
|
+
: {}),
|
|
627
|
+
...(data.metadata.firstIncompleteHour !== undefined
|
|
628
|
+
? { first_incomplete_hour: data.metadata.firstIncompleteHour }
|
|
629
|
+
: {}),
|
|
630
|
+
};
|
|
615
631
|
return {
|
|
616
632
|
rows: data.rows ?? [],
|
|
617
633
|
...(data.responseAggregationType !== undefined
|
|
618
634
|
? { responseAggregationType: data.responseAggregationType }
|
|
619
635
|
: {}),
|
|
620
|
-
...(
|
|
636
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
621
637
|
};
|
|
622
638
|
}
|
|
623
639
|
|
package/src/index.ts
CHANGED
|
@@ -191,7 +191,18 @@ const CAPABILITIES_OUTPUT_SCHEMA = {
|
|
|
191
191
|
server: z.string(),
|
|
192
192
|
version: z.string(),
|
|
193
193
|
access_mode: z.enum(['readonly', 'readwrite']),
|
|
194
|
-
auth_status: z.enum(['connected', 'not_connected', 'unknown'])
|
|
194
|
+
auth_status: z.enum(['connected', 'not_connected', 'unknown']).describe(
|
|
195
|
+
"Local credential state: 'connected' means a stored Google refresh credential is present; it does not live-verify that Google still accepts it.",
|
|
196
|
+
),
|
|
197
|
+
auth_status_basis: z.enum([
|
|
198
|
+
'stored_refresh_token',
|
|
199
|
+
'no_stored_refresh_token',
|
|
200
|
+
'local_state_unavailable',
|
|
201
|
+
]),
|
|
202
|
+
provider_auth_live_verified: z.literal(false).describe(
|
|
203
|
+
'Always false because server.capabilities does not make a Google token or API request merely to probe authorization.',
|
|
204
|
+
),
|
|
205
|
+
auth_note: z.string(),
|
|
195
206
|
tools: z.array(
|
|
196
207
|
z.object({
|
|
197
208
|
name: z.string(),
|
|
@@ -719,7 +730,7 @@ const TOOL_CATALOG = [
|
|
|
719
730
|
{
|
|
720
731
|
name: 'server.capabilities',
|
|
721
732
|
description:
|
|
722
|
-
"List every tool this server exposes and report
|
|
733
|
+
"List every tool this server exposes and report local Google credential state. A connected status means a stored refresh credential is present; it is not a live Google authorization check.",
|
|
723
734
|
},
|
|
724
735
|
] as const;
|
|
725
736
|
|
|
@@ -776,32 +787,49 @@ class GscMcpRuntime {
|
|
|
776
787
|
{
|
|
777
788
|
title: 'Get server capabilities and auth status',
|
|
778
789
|
description:
|
|
779
|
-
"List every tool this server exposes, its configured access mode, and
|
|
790
|
+
"List every tool this server exposes, its configured access mode, and local Google credential state. Call this first if you're unsure what tools are available, whether the deployment is read-only, or whether stored Google credentials are configured. auth_status='connected' means a stored refresh credential is present; this tool does not make a Google token/API request and therefore does not live-verify that Google still accepts it. Takes no arguments.",
|
|
780
791
|
inputSchema: {},
|
|
781
792
|
outputSchema: CAPABILITIES_OUTPUT_SCHEMA,
|
|
782
793
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
783
794
|
},
|
|
784
795
|
async () => {
|
|
785
796
|
let authStatus: 'connected' | 'not_connected' | 'unknown';
|
|
797
|
+
let authStatusBasis:
|
|
798
|
+
| 'stored_refresh_token'
|
|
799
|
+
| 'no_stored_refresh_token'
|
|
800
|
+
| 'local_state_unavailable';
|
|
786
801
|
try {
|
|
787
802
|
const googleId = this.props?.google_id;
|
|
788
803
|
if (!googleId) {
|
|
789
804
|
authStatus = 'not_connected';
|
|
805
|
+
authStatusBasis = 'no_stored_refresh_token';
|
|
790
806
|
} else {
|
|
791
807
|
const refreshToken = await getDecryptedRefreshToken(
|
|
792
808
|
this.env,
|
|
793
809
|
googleId,
|
|
794
810
|
);
|
|
795
811
|
authStatus = refreshToken ? 'connected' : 'not_connected';
|
|
812
|
+
authStatusBasis = refreshToken
|
|
813
|
+
? 'stored_refresh_token'
|
|
814
|
+
: 'no_stored_refresh_token';
|
|
796
815
|
}
|
|
797
816
|
} catch {
|
|
798
817
|
authStatus = 'unknown';
|
|
818
|
+
authStatusBasis = 'local_state_unavailable';
|
|
799
819
|
}
|
|
820
|
+
const authNote = authStatus === 'connected'
|
|
821
|
+
? "Stored Google refresh credentials are present, but server.capabilities does not live-verify them with Google. Refresh tokens can expire or be revoked; a Google tool call may still require reconnection."
|
|
822
|
+
: authStatus === 'not_connected'
|
|
823
|
+
? 'No stored Google refresh credential is available. Reconnect this server in your MCP client to sign in with Google.'
|
|
824
|
+
: 'Local Google credential state could not be read. Provider authorization was not checked.';
|
|
800
825
|
const capabilities = {
|
|
801
826
|
server: SERVER_NAME,
|
|
802
827
|
version: SERVER_VERSION,
|
|
803
828
|
access_mode: accessMode,
|
|
804
829
|
auth_status: authStatus,
|
|
830
|
+
auth_status_basis: authStatusBasis,
|
|
831
|
+
provider_auth_live_verified: false as const,
|
|
832
|
+
auth_note: authNote,
|
|
805
833
|
tools: getToolCatalogForAccessMode(TOOL_CATALOG, accessMode),
|
|
806
834
|
provider_limits: {
|
|
807
835
|
generative_ai_performance_report: {
|
|
@@ -809,7 +837,9 @@ class GscMcpRuntime {
|
|
|
809
837
|
note: GENERATIVE_AI_REPORT_API_NOTE,
|
|
810
838
|
},
|
|
811
839
|
},
|
|
812
|
-
hint:
|
|
840
|
+
hint: authStatus === 'connected'
|
|
841
|
+
? 'Stored Google credentials are configured. If a Google tool later reports revoked access, reconnect this server in your MCP client.'
|
|
842
|
+
: 'Reconnect this server in your MCP client to sign in with Google.',
|
|
813
843
|
};
|
|
814
844
|
return toolResponse(JSON.stringify(capabilities, null, 2), capabilities);
|
|
815
845
|
},
|
package/wrangler.example.jsonc
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
"compatibility_date": "2026-05-19",
|
|
9
9
|
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
|
10
10
|
"observability": { "enabled": true },
|
|
11
|
+
"secrets": {
|
|
12
|
+
"required": ["GOOGLE_CLIENT_SECRET", "TOKEN_ENCRYPTION_KEY"]
|
|
13
|
+
},
|
|
11
14
|
"vars": {
|
|
12
15
|
"GSC_ACCESS_MODE": "readwrite"
|
|
13
16
|
},
|