@digestseo/mcp-gsc 0.4.35 → 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 +17 -0
- package/README.md +4 -2
- package/SETUP.md +5 -3
- package/package.json +4 -4
- package/scripts/glama-start.mjs +9 -2
- package/src/date-validation.ts +6 -2
- package/src/index.ts +67 -6
- package/src/indexed-pages-range.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ 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
|
+
|
|
19
|
+
## [0.4.36] - 2026-09-24
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- `analytics.query` now exposes an explicit `multimodal_report_isolatable: false` boundary and matching runtime guidance because Search Console's new web multimodal filter is currently UI-only; the documented Search Analytics API still exposes only web/image/video/news/discover/googleNews search types.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- User-supplied Search Analytics ranges now reject future end dates against Search Console's Pacific calendar before reserving local safety budget, resolving Google credentials, or calling the provider; the same boundary now applies to explicit indexed-page ranges and weekly-digest input.
|
|
26
|
+
|
|
10
27
|
## [0.4.35] - 2026-09-24
|
|
11
28
|
|
|
12
29
|
### Fixed
|
package/README.md
CHANGED
|
@@ -108,13 +108,13 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 23 tools. Read-onl
|
|
|
108
108
|
| **`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. |
|
|
109
109
|
| **`sites.list`** | Read | List the Search Console properties the connected Google account can access (`siteUrl`, `permissionLevel`). |
|
|
110
110
|
| **`sites.get`** | Read | Retrieve one exact Search Console property and the connected account's permission level for it. |
|
|
111
|
-
| **`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, so returned rows can omit `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`. |
|
|
111
|
+
| **`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, so returned rows can omit `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`. Search Console's new web multimodal UI filter is likewise not exposed as a documented Search Analytics API selector; `multimodal_report_isolatable` is therefore `false`. |
|
|
112
112
|
| **`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. |
|
|
113
113
|
| **`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. |
|
|
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. |
|
|
@@ -149,6 +149,8 @@ Query regexes are a manual approximation only. Search Console's native Branded/N
|
|
|
149
149
|
|
|
150
150
|
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.
|
|
151
151
|
|
|
152
|
+
As of September 24, 2026, Search Console also has a web multimodal performance filter in the UI for searches such as Lens, Circle to Search, image uploads, and Chrome image search. The current Search Analytics API and Discovery document still expose only `web`, `image`, `video`, `news`, `discover`, and `googleNews` search types, so this MCP does not invent `search_type: "multimodal"` or guess a `searchAppearance` value. Use the Search Console UI for isolated multimodal reporting until Google documents API access.
|
|
153
|
+
|
|
152
154
|
## Connect in your AI client
|
|
153
155
|
|
|
154
156
|
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
|
@@ -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/date-validation.ts
CHANGED
|
@@ -81,10 +81,14 @@ export function assertDateRange(
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
export function assertDateNotInFuture(
|
|
84
|
+
export function assertDateNotInFuture(
|
|
85
|
+
date: string,
|
|
86
|
+
today: string,
|
|
87
|
+
dateName = 'End date',
|
|
88
|
+
): void {
|
|
85
89
|
if (date > today) {
|
|
86
90
|
throw new Error(
|
|
87
|
-
|
|
91
|
+
`${dateName} must be today or earlier (current Search Console Pacific date: ${today}). Google Search Console has no data for dates that have not happened yet.`,
|
|
88
92
|
);
|
|
89
93
|
}
|
|
90
94
|
}
|
package/src/index.ts
CHANGED
|
@@ -402,6 +402,10 @@ const SEARCH_ANALYTICS_OUTPUT_SCHEMA = {
|
|
|
402
402
|
'False because the current documented Search Analytics API exposes no dedicated Generative AI report search type or filter selector.',
|
|
403
403
|
),
|
|
404
404
|
generative_ai_note: z.string(),
|
|
405
|
+
multimodal_report_isolatable: z.literal(false).describe(
|
|
406
|
+
'False because the current documented Search Analytics API exposes no dedicated web multimodal search type or filter selector.',
|
|
407
|
+
),
|
|
408
|
+
multimodal_note: z.string(),
|
|
405
409
|
next_start_row: z.number().int().nonnegative().optional(),
|
|
406
410
|
has_more: z.boolean(),
|
|
407
411
|
truncated: z.boolean(),
|
|
@@ -650,6 +654,12 @@ const COMMON_RETURNED_ROWS_COMPARISON_NOTE =
|
|
|
650
654
|
const PERFORMANCE_COMPARISON_OUTPUT_SCHEMA = {
|
|
651
655
|
comparison_scope: z.literal('common_returned_rows_only'),
|
|
652
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
|
+
),
|
|
653
663
|
comparisons: z.array(
|
|
654
664
|
z.object({
|
|
655
665
|
key: z.string(),
|
|
@@ -698,6 +708,9 @@ const SEARCH_ANALYTICS_PROVIDER_NOTE =
|
|
|
698
708
|
const GENERATIVE_AI_REPORT_API_NOTE =
|
|
699
709
|
"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.";
|
|
700
710
|
|
|
711
|
+
const MULTIMODAL_REPORT_API_NOTE =
|
|
712
|
+
"Search Console's web multimodal performance filter is currently available in the Search Console UI, but the documented Search Analytics API does not expose a multimodal search type or filter selector. Current API search types remain web, image, video, news, discover, and googleNews. Do not pass search_type='multimodal' or guess a searchAppearance identifier to isolate Lens, Circle to Search, image-upload, or Chrome image-search traffic.";
|
|
713
|
+
|
|
701
714
|
function paginationMetadata(result: PaginatedSearchAnalyticsResult) {
|
|
702
715
|
return {
|
|
703
716
|
rows_fetched: result.rows.length,
|
|
@@ -875,7 +888,7 @@ const TOOL_CATALOG = [
|
|
|
875
888
|
{
|
|
876
889
|
name: 'analytics.compare',
|
|
877
890
|
description:
|
|
878
|
-
'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.',
|
|
879
892
|
},
|
|
880
893
|
{
|
|
881
894
|
name: 'reports.weekly_digest',
|
|
@@ -1279,6 +1292,13 @@ class GscMcpRuntime {
|
|
|
1279
1292
|
' included in overall web Search performance data. Do not guess a',
|
|
1280
1293
|
' searchAppearance identifier or claim analytics.query isolates the',
|
|
1281
1294
|
' dedicated Generative AI report.',
|
|
1295
|
+
'- WEB MULTIMODAL REPORT: Search Console now has a web multimodal',
|
|
1296
|
+
' performance filter in its UI for searches such as Lens, Circle to',
|
|
1297
|
+
' Search, image uploads, and Chrome image search. The current',
|
|
1298
|
+
' documented Search Analytics API still exposes only web, image,',
|
|
1299
|
+
' video, news, discover, and googleNews search types, with no',
|
|
1300
|
+
' multimodal selector. Do not pass search_type="multimodal" or guess',
|
|
1301
|
+
' a searchAppearance identifier to isolate this UI report.',
|
|
1282
1302
|
'- Use dimension_filter_groups to filter by country, device, query',
|
|
1283
1303
|
' content, page URL, or search feature. includingRegex and',
|
|
1284
1304
|
' excludingRegex use RE2 syntax. A query regex can provide a manual',
|
|
@@ -1358,7 +1378,7 @@ class GscMcpRuntime {
|
|
|
1358
1378
|
search_type: z
|
|
1359
1379
|
.enum(['web', 'image', 'video', 'news', 'discover', 'googleNews'])
|
|
1360
1380
|
.default('web')
|
|
1361
|
-
.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.'),
|
|
1381
|
+
.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 or the Search Console UI web multimodal filter as a search type.'),
|
|
1362
1382
|
aggregation_type: z
|
|
1363
1383
|
.enum(['auto', 'byNewsShowcasePanel', 'byPage', 'byProperty'])
|
|
1364
1384
|
.default('auto')
|
|
@@ -1388,6 +1408,11 @@ class GscMcpRuntime {
|
|
|
1388
1408
|
dimension_filter_groups,
|
|
1389
1409
|
}) => {
|
|
1390
1410
|
assertDateRange(start_date, end_date);
|
|
1411
|
+
assertDateNotInFuture(
|
|
1412
|
+
end_date,
|
|
1413
|
+
getSearchConsoleCalendarDate(),
|
|
1414
|
+
'end_date',
|
|
1415
|
+
);
|
|
1391
1416
|
const sourceRowLimit = Math.min(row_limit, MAX_DIRECT_SOURCE_ROWS);
|
|
1392
1417
|
const query = {
|
|
1393
1418
|
startDate: start_date,
|
|
@@ -1439,6 +1464,8 @@ class GscMcpRuntime {
|
|
|
1439
1464
|
provider_note: SEARCH_ANALYTICS_PROVIDER_NOTE,
|
|
1440
1465
|
generative_ai_report_isolatable: false,
|
|
1441
1466
|
generative_ai_note: GENERATIVE_AI_REPORT_API_NOTE,
|
|
1467
|
+
multimodal_report_isolatable: false,
|
|
1468
|
+
multimodal_note: MULTIMODAL_REPORT_API_NOTE,
|
|
1442
1469
|
has_more: bounded.resultPage.has_more,
|
|
1443
1470
|
truncated: bounded.resultPage.truncated,
|
|
1444
1471
|
byte_limit_reached: bounded.resultPage.byte_limit_reached,
|
|
@@ -1497,6 +1524,11 @@ class GscMcpRuntime {
|
|
|
1497
1524
|
},
|
|
1498
1525
|
async ({ site_url, page_url, start_date, end_date, search_type, row_limit = 100, start_row = 0 }) => {
|
|
1499
1526
|
assertDateRange(start_date, end_date);
|
|
1527
|
+
assertDateNotInFuture(
|
|
1528
|
+
end_date,
|
|
1529
|
+
getSearchConsoleCalendarDate(),
|
|
1530
|
+
'end_date',
|
|
1531
|
+
);
|
|
1500
1532
|
assertUrlWithinSearchConsoleProperty(page_url, site_url, 'page_url');
|
|
1501
1533
|
const googleId = this.requireGoogleId();
|
|
1502
1534
|
const rateLimitError = await this.rateLimitError(googleId, 'insights.page_queries');
|
|
@@ -1582,6 +1614,11 @@ class GscMcpRuntime {
|
|
|
1582
1614
|
},
|
|
1583
1615
|
async ({ site_url, query, start_date, end_date, search_type, row_limit = 100, start_row = 0 }) => {
|
|
1584
1616
|
assertDateRange(start_date, end_date);
|
|
1617
|
+
assertDateNotInFuture(
|
|
1618
|
+
end_date,
|
|
1619
|
+
getSearchConsoleCalendarDate(),
|
|
1620
|
+
'end_date',
|
|
1621
|
+
);
|
|
1585
1622
|
const googleId = this.requireGoogleId();
|
|
1586
1623
|
const rateLimitError = await this.rateLimitError(googleId, 'insights.query_pages');
|
|
1587
1624
|
if (rateLimitError) return rateLimitError;
|
|
@@ -1776,6 +1813,11 @@ class GscMcpRuntime {
|
|
|
1776
1813
|
},
|
|
1777
1814
|
async ({ site_url, start_date, end_date, min_impressions, min_position, max_position, limit = DEFAULT_ANALYSIS_RESULT_LIMIT, start_row = 0 }) => {
|
|
1778
1815
|
assertDateRange(start_date, end_date);
|
|
1816
|
+
assertDateNotInFuture(
|
|
1817
|
+
end_date,
|
|
1818
|
+
getSearchConsoleCalendarDate(),
|
|
1819
|
+
'end_date',
|
|
1820
|
+
);
|
|
1779
1821
|
const googleId = this.requireGoogleId();
|
|
1780
1822
|
const rateLimitError = await this.rateLimitError(googleId, 'insights.quick_wins');
|
|
1781
1823
|
if (rateLimitError) return rateLimitError;
|
|
@@ -1823,6 +1865,11 @@ class GscMcpRuntime {
|
|
|
1823
1865
|
},
|
|
1824
1866
|
async ({ site_url, start_date, end_date, min_impressions, min_page_percentage, limit = DEFAULT_ANALYSIS_RESULT_LIMIT, start_row = 0 }) => {
|
|
1825
1867
|
assertDateRange(start_date, end_date);
|
|
1868
|
+
assertDateNotInFuture(
|
|
1869
|
+
end_date,
|
|
1870
|
+
getSearchConsoleCalendarDate(),
|
|
1871
|
+
'end_date',
|
|
1872
|
+
);
|
|
1826
1873
|
const googleId = this.requireGoogleId();
|
|
1827
1874
|
const rateLimitError = await this.rateLimitError(googleId, 'insights.cannibalization');
|
|
1828
1875
|
if (rateLimitError) return rateLimitError;
|
|
@@ -2141,7 +2188,7 @@ class GscMcpRuntime {
|
|
|
2141
2188
|
'analytics.compare',
|
|
2142
2189
|
{
|
|
2143
2190
|
title: 'Compare Performance Between Periods',
|
|
2144
|
-
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.',
|
|
2145
2192
|
inputSchema: {
|
|
2146
2193
|
site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
|
|
2147
2194
|
start_date_a: SEARCH_CONSOLE_DATE_SCHEMA.describe(
|
|
@@ -2161,6 +2208,12 @@ class GscMcpRuntime {
|
|
|
2161
2208
|
.enum(['web', 'image', 'video', 'news'])
|
|
2162
2209
|
.default('web')
|
|
2163
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
|
+
),
|
|
2164
2217
|
dimension_filter_groups: z
|
|
2165
2218
|
.array(SEARCH_ANALYTICS_FILTER_GROUP_SCHEMA)
|
|
2166
2219
|
.optional()
|
|
@@ -2181,12 +2234,16 @@ class GscMcpRuntime {
|
|
|
2181
2234
|
end_date_b,
|
|
2182
2235
|
dimension,
|
|
2183
2236
|
search_type,
|
|
2237
|
+
data_state,
|
|
2184
2238
|
dimension_filter_groups,
|
|
2185
2239
|
limit = DEFAULT_ANALYSIS_RESULT_LIMIT,
|
|
2186
2240
|
start_row = 0,
|
|
2187
2241
|
}) => {
|
|
2188
2242
|
assertDateRange(start_date_a, end_date_a, 'start_date_a', 'end_date_a');
|
|
2189
2243
|
assertDateRange(start_date_b, end_date_b, 'start_date_b', 'end_date_b');
|
|
2244
|
+
const today = getSearchConsoleCalendarDate();
|
|
2245
|
+
assertDateNotInFuture(end_date_a, today, 'end_date_a');
|
|
2246
|
+
assertDateNotInFuture(end_date_b, today, 'end_date_b');
|
|
2190
2247
|
const googleId = this.requireGoogleId();
|
|
2191
2248
|
const rateLimitError = await this.rateLimitError(googleId, 'analytics.compare');
|
|
2192
2249
|
if (rateLimitError) return rateLimitError;
|
|
@@ -2197,6 +2254,7 @@ class GscMcpRuntime {
|
|
|
2197
2254
|
endDate: end_date_a,
|
|
2198
2255
|
dimensions: [dimension],
|
|
2199
2256
|
type: search_type,
|
|
2257
|
+
dataState: data_state,
|
|
2200
2258
|
...(dimension_filter_groups !== undefined
|
|
2201
2259
|
? { dimensionFilterGroups: dimension_filter_groups }
|
|
2202
2260
|
: {}),
|
|
@@ -2207,6 +2265,7 @@ class GscMcpRuntime {
|
|
|
2207
2265
|
endDate: end_date_b,
|
|
2208
2266
|
dimensions: [dimension],
|
|
2209
2267
|
type: search_type,
|
|
2268
|
+
dataState: data_state,
|
|
2210
2269
|
...(dimension_filter_groups !== undefined
|
|
2211
2270
|
? { dimensionFilterGroups: dimension_filter_groups }
|
|
2212
2271
|
: {}),
|
|
@@ -2217,6 +2276,8 @@ class GscMcpRuntime {
|
|
|
2217
2276
|
const payload = {
|
|
2218
2277
|
comparison_scope: 'common_returned_rows_only' as const,
|
|
2219
2278
|
comparison_note: COMMON_RETURNED_ROWS_COMPARISON_NOTE,
|
|
2279
|
+
data_state,
|
|
2280
|
+
preliminary_data_possible: data_state !== 'final',
|
|
2220
2281
|
comparisons: bounded.items,
|
|
2221
2282
|
pagination: {
|
|
2222
2283
|
period_a: paginationMetadata(sourceA),
|
|
@@ -2248,12 +2309,12 @@ class GscMcpRuntime {
|
|
|
2248
2309
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
2249
2310
|
},
|
|
2250
2311
|
async ({ site_url, end_date }) => {
|
|
2312
|
+
const today = getSearchConsoleCalendarDate();
|
|
2313
|
+
const resolvedEndDate = resolveWeeklyDigestEndDate(end_date, today);
|
|
2314
|
+
assertDateNotInFuture(resolvedEndDate, today, 'end_date');
|
|
2251
2315
|
const googleId = this.requireGoogleId();
|
|
2252
2316
|
const rateLimitError = await this.rateLimitError(googleId, 'reports.weekly_digest');
|
|
2253
2317
|
if (rateLimitError) return rateLimitError;
|
|
2254
|
-
const today = getSearchConsoleCalendarDate();
|
|
2255
|
-
const resolvedEndDate = resolveWeeklyDigestEndDate(end_date, today);
|
|
2256
|
-
assertDateNotInFuture(resolvedEndDate, today);
|
|
2257
2318
|
|
|
2258
2319
|
try {
|
|
2259
2320
|
const markdown = await generateWeeklyDigest(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertDateRange } from './date-validation';
|
|
1
|
+
import { assertDateNotInFuture, assertDateRange } from './date-validation';
|
|
2
2
|
|
|
3
3
|
export interface IndexedPagesDateRange {
|
|
4
4
|
startDate: string;
|
|
@@ -26,6 +26,7 @@ export function resolveIndexedPagesDateRange(
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
if (!startDate) {
|
|
29
|
+
assertDateNotInFuture(endDate!, today, 'end_date');
|
|
29
30
|
return {
|
|
30
31
|
startDate: addUtcDays(endDate!, -29),
|
|
31
32
|
endDate: endDate!,
|
|
@@ -50,5 +51,6 @@ export function resolveIndexedPagesDateRange(
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
assertDateRange(startDate, endDate);
|
|
54
|
+
assertDateNotInFuture(endDate, today, 'end_date');
|
|
53
55
|
return { startDate, endDate };
|
|
54
56
|
}
|