@digestseo/mcp-gsc 0.4.36 → 0.4.37
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 +9 -0
- package/README.md +1 -1
- package/SETUP.md +5 -3
- package/package.json +4 -4
- package/scripts/glama-start.mjs +9 -2
- package/src/index.ts +19 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.37] - 2026-10-24
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- 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.
|
|
17
|
+
- `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.
|
|
18
|
+
|
|
10
19
|
## [0.4.36] - 2026-09-24
|
|
11
20
|
|
|
12
21
|
### Changed
|
package/README.md
CHANGED
|
@@ -114,7 +114,7 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 23 tools. Read-onl
|
|
|
114
114
|
| **`urls.inspect_many`** | Read | Inspect Google's indexed versions of up to 10 URLs with bounded concurrency of 3 in one call while preserving input-order results. Each URL still consumes one Google URL Inspection request and one unit of the server's shared inspection safety budget; this does not run live URL tests. |
|
|
115
115
|
| **`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
116
|
| **`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. |
|
|
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. 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
118
|
| **`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
119
|
| **`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
120
|
| **`sitemaps.submit`** / **`sitemaps.delete`** | Write | Submit or remove a sitemap. |
|
package/SETUP.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.4.37",
|
|
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.
|
|
79
|
-
"agents": "
|
|
78
|
+
"@modelcontextprotocol/server": "2.3.1",
|
|
79
|
+
"agents": "0.29.0",
|
|
80
80
|
"wrangler": "^4.136.3",
|
|
81
|
-
"zod": "^4.
|
|
81
|
+
"zod": "^4.6.7"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
84
|
"@cloudflare/vitest-plugin": "^1.2.3",
|
package/scripts/glama-start.mjs
CHANGED
|
@@ -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,7 +34,7 @@ if (
|
|
|
27
34
|
|| Buffer.from(tokenEncryptionKey, 'base64').byteLength !== 32
|
|
28
35
|
) {
|
|
29
36
|
console.error(
|
|
30
|
-
|
|
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
|
}
|
package/src/index.ts
CHANGED
|
@@ -654,6 +654,12 @@ const COMMON_RETURNED_ROWS_COMPARISON_NOTE =
|
|
|
654
654
|
const PERFORMANCE_COMPARISON_OUTPUT_SCHEMA = {
|
|
655
655
|
comparison_scope: z.literal('common_returned_rows_only'),
|
|
656
656
|
comparison_note: z.string(),
|
|
657
|
+
data_state: z.enum(['all', 'final']).describe(
|
|
658
|
+
'The Search Analytics dataState applied identically to both comparison periods.',
|
|
659
|
+
),
|
|
660
|
+
preliminary_data_possible: z.boolean().describe(
|
|
661
|
+
'True when data_state=all, because recent Search Analytics rows can still be incomplete and subject to change.',
|
|
662
|
+
),
|
|
657
663
|
comparisons: z.array(
|
|
658
664
|
z.object({
|
|
659
665
|
key: z.string(),
|
|
@@ -882,7 +888,7 @@ const TOOL_CATALOG = [
|
|
|
882
888
|
{
|
|
883
889
|
name: 'analytics.compare',
|
|
884
890
|
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.',
|
|
891
|
+
'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
892
|
},
|
|
887
893
|
{
|
|
888
894
|
name: 'reports.weekly_digest',
|
|
@@ -2182,7 +2188,7 @@ class GscMcpRuntime {
|
|
|
2182
2188
|
'analytics.compare',
|
|
2183
2189
|
{
|
|
2184
2190
|
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.',
|
|
2191
|
+
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
2192
|
inputSchema: {
|
|
2187
2193
|
site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
|
|
2188
2194
|
start_date_a: SEARCH_CONSOLE_DATE_SCHEMA.describe(
|
|
@@ -2202,6 +2208,12 @@ class GscMcpRuntime {
|
|
|
2202
2208
|
.enum(['web', 'image', 'video', 'news'])
|
|
2203
2209
|
.default('web')
|
|
2204
2210
|
.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.'),
|
|
2211
|
+
data_state: z
|
|
2212
|
+
.enum(['all', 'final'])
|
|
2213
|
+
.default('final')
|
|
2214
|
+
.describe(
|
|
2215
|
+
"'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.",
|
|
2216
|
+
),
|
|
2205
2217
|
dimension_filter_groups: z
|
|
2206
2218
|
.array(SEARCH_ANALYTICS_FILTER_GROUP_SCHEMA)
|
|
2207
2219
|
.optional()
|
|
@@ -2222,6 +2234,7 @@ class GscMcpRuntime {
|
|
|
2222
2234
|
end_date_b,
|
|
2223
2235
|
dimension,
|
|
2224
2236
|
search_type,
|
|
2237
|
+
data_state,
|
|
2225
2238
|
dimension_filter_groups,
|
|
2226
2239
|
limit = DEFAULT_ANALYSIS_RESULT_LIMIT,
|
|
2227
2240
|
start_row = 0,
|
|
@@ -2241,6 +2254,7 @@ class GscMcpRuntime {
|
|
|
2241
2254
|
endDate: end_date_a,
|
|
2242
2255
|
dimensions: [dimension],
|
|
2243
2256
|
type: search_type,
|
|
2257
|
+
dataState: data_state,
|
|
2244
2258
|
...(dimension_filter_groups !== undefined
|
|
2245
2259
|
? { dimensionFilterGroups: dimension_filter_groups }
|
|
2246
2260
|
: {}),
|
|
@@ -2251,6 +2265,7 @@ class GscMcpRuntime {
|
|
|
2251
2265
|
endDate: end_date_b,
|
|
2252
2266
|
dimensions: [dimension],
|
|
2253
2267
|
type: search_type,
|
|
2268
|
+
dataState: data_state,
|
|
2254
2269
|
...(dimension_filter_groups !== undefined
|
|
2255
2270
|
? { dimensionFilterGroups: dimension_filter_groups }
|
|
2256
2271
|
: {}),
|
|
@@ -2261,6 +2276,8 @@ class GscMcpRuntime {
|
|
|
2261
2276
|
const payload = {
|
|
2262
2277
|
comparison_scope: 'common_returned_rows_only' as const,
|
|
2263
2278
|
comparison_note: COMMON_RETURNED_ROWS_COMPARISON_NOTE,
|
|
2279
|
+
data_state,
|
|
2280
|
+
preliminary_data_possible: data_state !== 'final',
|
|
2264
2281
|
comparisons: bounded.items,
|
|
2265
2282
|
pagination: {
|
|
2266
2283
|
period_a: paginationMetadata(sourceA),
|