@digestseo/mcp-gsc 0.4.34 → 0.4.36
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 +14 -0
- package/README.md +8 -2
- package/package.json +1 -1
- package/src/date-validation.ts +6 -2
- package/src/google.ts +4 -5
- package/src/index.ts +55 -11
- package/src/indexed-pages-range.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.36] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- `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.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- 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.
|
|
17
|
+
|
|
18
|
+
## [0.4.35] - 2026-09-24
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Search Analytics pagination now treats provider responses shorter than the requested page size as terminal, matching Google's documented contract and avoiding false continuations or unnecessary follow-up requests across direct and higher-level workflows.
|
|
22
|
+
- `insights.page_queries` now rejects exact page URLs outside the supplied Search Console property before reserving local Search Analytics safety budget, resolving Google credentials, or calling the provider, preventing misleading empty cross-property drilldowns.
|
|
23
|
+
|
|
10
24
|
## [0.4.34] - 2026-09-24
|
|
11
25
|
|
|
12
26
|
### Fixed
|
package/README.md
CHANGED
|
@@ -66,6 +66,8 @@ Codex CLI and the Codex IDE extension share MCP configuration, so registering th
|
|
|
66
66
|
|
|
67
67
|
When this repository is installed as a Cursor plugin, Cursor also auto-discovers three bundled Agent Skills that compose the MCP tools into evidence-safe workflows: `gsc-weekly-review`, `gsc-indexing-triage`, and `gsc-search-opportunities`. The **Add to Cursor** button above installs the MCP connection only; the bundled skills are part of the repository's Cursor plugin package.
|
|
68
68
|
|
|
69
|
+
The button and plugin MCP configuration above are for Cursor running on the same machine as the npm launcher. **Cursor Cloud Agents run in isolated cloud VMs, so they cannot reach `127.0.0.1` on your computer.** For Cloud Agents, deploy your own externally reachable HTTPS Worker and add its `/mcp` URL from the MCP controls at [cursor.com/agents](https://cursor.com/agents); Cursor supports HTTP MCP servers and OAuth there. Do not reuse the loopback URL for a Cloud Agent.
|
|
70
|
+
|
|
69
71
|
**GitHub Copilot CLI**
|
|
70
72
|
|
|
71
73
|
```bash
|
|
@@ -89,7 +91,9 @@ The Gemini CLI extension bundles the verified local loopback MCP configuration p
|
|
|
89
91
|
|
|
90
92
|
Start the npm launcher before using either install button or the Copilot plugin. Complete its Google OAuth flow on first use.
|
|
91
93
|
|
|
92
|
-
**ChatGPT** — ChatGPT cannot connect directly to this loopback `127.0.0.1` endpoint.
|
|
94
|
+
**ChatGPT** — ChatGPT cannot connect directly to this loopback `127.0.0.1` endpoint. For private/local use, create an OpenAI **Secure MCP Tunnel**, then run `tunnel-client` on the same machine as the launcher so it can reach `http://127.0.0.1:8080/mcp`; when creating the ChatGPT app, choose the **Tunnel** connection and select that tunnel. The tunnel setup requires an OpenAI `tunnel_id` and runtime API key; see OpenAI's [Secure MCP Tunnel guide](https://developers.openai.com/api/docs/guides/secure-mcp-tunnels). The tunnel keeps the MCP server private and is intended for private/developer-mode connectivity, not public plugin distribution.
|
|
95
|
+
|
|
96
|
+
Alternatively, deploy your own remote Cloudflare Worker and add its HTTPS `/mcp` URL as a custom MCP app in Developer mode; see [Connect in your AI client](#connect-in-your-ai-client) below.
|
|
93
97
|
|
|
94
98
|
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.
|
|
95
99
|
|
|
@@ -104,7 +108,7 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 23 tools. Read-onl
|
|
|
104
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. |
|
|
105
109
|
| **`sites.list`** | Read | List the Search Console properties the connected Google account can access (`siteUrl`, `permissionLevel`). |
|
|
106
110
|
| **`sites.get`** | Read | Retrieve one exact Search Console property and the connected account's permission level for it. |
|
|
107
|
-
| **`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`. |
|
|
108
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. |
|
|
109
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. |
|
|
110
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. |
|
|
@@ -145,6 +149,8 @@ Query regexes are a manual approximation only. Search Console's native Branded/N
|
|
|
145
149
|
|
|
146
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.
|
|
147
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
|
+
|
|
148
154
|
## Connect in your AI client
|
|
149
155
|
|
|
150
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/package.json
CHANGED
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/google.ts
CHANGED
|
@@ -785,10 +785,9 @@ export async function querySearchAnalytics(
|
|
|
785
785
|
|
|
786
786
|
/**
|
|
787
787
|
* Fetches Search Analytics rows in 25,000-row pages for higher-level tools.
|
|
788
|
-
* Google's documented pagination contract
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
* can have more rows even when Search Console's own internal limits apply.
|
|
788
|
+
* Google's documented pagination contract treats a response shorter than the
|
|
789
|
+
* requested rowLimit as exhausted. An exactly full final page still requires
|
|
790
|
+
* a following empty-page probe unless the local safety ceiling is reached.
|
|
792
791
|
*/
|
|
793
792
|
export async function querySearchAnalyticsPaginated(
|
|
794
793
|
accessToken: string,
|
|
@@ -820,7 +819,7 @@ export async function querySearchAnalyticsPaginated(
|
|
|
820
819
|
|
|
821
820
|
const acceptedRows = page.rows.slice(0, requestedRows);
|
|
822
821
|
rows.push(...acceptedRows);
|
|
823
|
-
if (page.rows.length
|
|
822
|
+
if (page.rows.length < requestedRows) {
|
|
824
823
|
return { rows, pagesFetched, localLimitReached: false };
|
|
825
824
|
}
|
|
826
825
|
if (rows.length >= maximumRows || page.rows.length > requestedRows) {
|
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(),
|
|
@@ -698,6 +702,9 @@ const SEARCH_ANALYTICS_PROVIDER_NOTE =
|
|
|
698
702
|
const GENERATIVE_AI_REPORT_API_NOTE =
|
|
699
703
|
"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
704
|
|
|
705
|
+
const MULTIMODAL_REPORT_API_NOTE =
|
|
706
|
+
"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.";
|
|
707
|
+
|
|
701
708
|
function paginationMetadata(result: PaginatedSearchAnalyticsResult) {
|
|
702
709
|
return {
|
|
703
710
|
rows_fetched: result.rows.length,
|
|
@@ -1279,6 +1286,13 @@ class GscMcpRuntime {
|
|
|
1279
1286
|
' included in overall web Search performance data. Do not guess a',
|
|
1280
1287
|
' searchAppearance identifier or claim analytics.query isolates the',
|
|
1281
1288
|
' dedicated Generative AI report.',
|
|
1289
|
+
'- WEB MULTIMODAL REPORT: Search Console now has a web multimodal',
|
|
1290
|
+
' performance filter in its UI for searches such as Lens, Circle to',
|
|
1291
|
+
' Search, image uploads, and Chrome image search. The current',
|
|
1292
|
+
' documented Search Analytics API still exposes only web, image,',
|
|
1293
|
+
' video, news, discover, and googleNews search types, with no',
|
|
1294
|
+
' multimodal selector. Do not pass search_type="multimodal" or guess',
|
|
1295
|
+
' a searchAppearance identifier to isolate this UI report.',
|
|
1282
1296
|
'- Use dimension_filter_groups to filter by country, device, query',
|
|
1283
1297
|
' content, page URL, or search feature. includingRegex and',
|
|
1284
1298
|
' excludingRegex use RE2 syntax. A query regex can provide a manual',
|
|
@@ -1358,7 +1372,7 @@ class GscMcpRuntime {
|
|
|
1358
1372
|
search_type: z
|
|
1359
1373
|
.enum(['web', 'image', 'video', 'news', 'discover', 'googleNews'])
|
|
1360
1374
|
.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.'),
|
|
1375
|
+
.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
1376
|
aggregation_type: z
|
|
1363
1377
|
.enum(['auto', 'byNewsShowcasePanel', 'byPage', 'byProperty'])
|
|
1364
1378
|
.default('auto')
|
|
@@ -1388,6 +1402,11 @@ class GscMcpRuntime {
|
|
|
1388
1402
|
dimension_filter_groups,
|
|
1389
1403
|
}) => {
|
|
1390
1404
|
assertDateRange(start_date, end_date);
|
|
1405
|
+
assertDateNotInFuture(
|
|
1406
|
+
end_date,
|
|
1407
|
+
getSearchConsoleCalendarDate(),
|
|
1408
|
+
'end_date',
|
|
1409
|
+
);
|
|
1391
1410
|
const sourceRowLimit = Math.min(row_limit, MAX_DIRECT_SOURCE_ROWS);
|
|
1392
1411
|
const query = {
|
|
1393
1412
|
startDate: start_date,
|
|
@@ -1408,11 +1427,10 @@ class GscMcpRuntime {
|
|
|
1408
1427
|
if (rateLimitError) return rateLimitError;
|
|
1409
1428
|
const accessToken = await this.getAccessToken(googleId);
|
|
1410
1429
|
const response = await querySearchAnalytics(accessToken, site_url, query);
|
|
1411
|
-
//
|
|
1412
|
-
//
|
|
1413
|
-
// later rows, so preserve a continuation for dimensioned queries.
|
|
1430
|
+
// Google documents a short page as terminal. An exactly full page can
|
|
1431
|
+
// still have a following page, so expose continuation only then.
|
|
1414
1432
|
const sourceMayHaveMore =
|
|
1415
|
-
dimensions.length > 0 && response.rows.length
|
|
1433
|
+
dimensions.length > 0 && response.rows.length === sourceRowLimit;
|
|
1416
1434
|
const bounded = windowSourceRows(
|
|
1417
1435
|
response.rows,
|
|
1418
1436
|
start_row,
|
|
@@ -1440,6 +1458,8 @@ class GscMcpRuntime {
|
|
|
1440
1458
|
provider_note: SEARCH_ANALYTICS_PROVIDER_NOTE,
|
|
1441
1459
|
generative_ai_report_isolatable: false,
|
|
1442
1460
|
generative_ai_note: GENERATIVE_AI_REPORT_API_NOTE,
|
|
1461
|
+
multimodal_report_isolatable: false,
|
|
1462
|
+
multimodal_note: MULTIMODAL_REPORT_API_NOTE,
|
|
1443
1463
|
has_more: bounded.resultPage.has_more,
|
|
1444
1464
|
truncated: bounded.resultPage.truncated,
|
|
1445
1465
|
byte_limit_reached: bounded.resultPage.byte_limit_reached,
|
|
@@ -1463,10 +1483,10 @@ class GscMcpRuntime {
|
|
|
1463
1483
|
'insights.page_queries',
|
|
1464
1484
|
{
|
|
1465
1485
|
title: 'Find queries for a page',
|
|
1466
|
-
description: 'For one exact page URL
|
|
1486
|
+
description: 'For one exact page URL within the supplied Search Console property, return the queries that produced impressions for it over a date range. This wraps an exact page dimension filter so agents do not need to construct analytics.query filter groups manually. Exact page matching is case-sensitive in Search Console. Use row_limit and start_row to page through bounded results; Search Console can still omit anonymized queries. Google Discover and Google News are intentionally unavailable because those reports do not expose query data.',
|
|
1467
1487
|
inputSchema: {
|
|
1468
1488
|
site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
|
|
1469
|
-
page_url: z.string().url().describe('Exact fully-qualified page URL to filter on, e.g. https://example.com/guides/seo/. Search Console exact page filters are case-sensitive.'),
|
|
1489
|
+
page_url: z.string().url().describe('Exact fully-qualified page URL to filter on, e.g. https://example.com/guides/seo/. The URL must belong to site_url; Search Console exact page filters are case-sensitive.'),
|
|
1470
1490
|
start_date: SEARCH_CONSOLE_DATE_SCHEMA.describe(
|
|
1471
1491
|
searchConsoleDateDescription('Start date (inclusive) in YYYY-MM-DD format.'),
|
|
1472
1492
|
),
|
|
@@ -1498,6 +1518,12 @@ class GscMcpRuntime {
|
|
|
1498
1518
|
},
|
|
1499
1519
|
async ({ site_url, page_url, start_date, end_date, search_type, row_limit = 100, start_row = 0 }) => {
|
|
1500
1520
|
assertDateRange(start_date, end_date);
|
|
1521
|
+
assertDateNotInFuture(
|
|
1522
|
+
end_date,
|
|
1523
|
+
getSearchConsoleCalendarDate(),
|
|
1524
|
+
'end_date',
|
|
1525
|
+
);
|
|
1526
|
+
assertUrlWithinSearchConsoleProperty(page_url, site_url, 'page_url');
|
|
1501
1527
|
const googleId = this.requireGoogleId();
|
|
1502
1528
|
const rateLimitError = await this.rateLimitError(googleId, 'insights.page_queries');
|
|
1503
1529
|
if (rateLimitError) return rateLimitError;
|
|
@@ -1582,6 +1608,11 @@ class GscMcpRuntime {
|
|
|
1582
1608
|
},
|
|
1583
1609
|
async ({ site_url, query, start_date, end_date, search_type, row_limit = 100, start_row = 0 }) => {
|
|
1584
1610
|
assertDateRange(start_date, end_date);
|
|
1611
|
+
assertDateNotInFuture(
|
|
1612
|
+
end_date,
|
|
1613
|
+
getSearchConsoleCalendarDate(),
|
|
1614
|
+
'end_date',
|
|
1615
|
+
);
|
|
1585
1616
|
const googleId = this.requireGoogleId();
|
|
1586
1617
|
const rateLimitError = await this.rateLimitError(googleId, 'insights.query_pages');
|
|
1587
1618
|
if (rateLimitError) return rateLimitError;
|
|
@@ -1776,6 +1807,11 @@ class GscMcpRuntime {
|
|
|
1776
1807
|
},
|
|
1777
1808
|
async ({ site_url, start_date, end_date, min_impressions, min_position, max_position, limit = DEFAULT_ANALYSIS_RESULT_LIMIT, start_row = 0 }) => {
|
|
1778
1809
|
assertDateRange(start_date, end_date);
|
|
1810
|
+
assertDateNotInFuture(
|
|
1811
|
+
end_date,
|
|
1812
|
+
getSearchConsoleCalendarDate(),
|
|
1813
|
+
'end_date',
|
|
1814
|
+
);
|
|
1779
1815
|
const googleId = this.requireGoogleId();
|
|
1780
1816
|
const rateLimitError = await this.rateLimitError(googleId, 'insights.quick_wins');
|
|
1781
1817
|
if (rateLimitError) return rateLimitError;
|
|
@@ -1823,6 +1859,11 @@ class GscMcpRuntime {
|
|
|
1823
1859
|
},
|
|
1824
1860
|
async ({ site_url, start_date, end_date, min_impressions, min_page_percentage, limit = DEFAULT_ANALYSIS_RESULT_LIMIT, start_row = 0 }) => {
|
|
1825
1861
|
assertDateRange(start_date, end_date);
|
|
1862
|
+
assertDateNotInFuture(
|
|
1863
|
+
end_date,
|
|
1864
|
+
getSearchConsoleCalendarDate(),
|
|
1865
|
+
'end_date',
|
|
1866
|
+
);
|
|
1826
1867
|
const googleId = this.requireGoogleId();
|
|
1827
1868
|
const rateLimitError = await this.rateLimitError(googleId, 'insights.cannibalization');
|
|
1828
1869
|
if (rateLimitError) return rateLimitError;
|
|
@@ -2124,7 +2165,7 @@ class GscMcpRuntime {
|
|
|
2124
2165
|
pages,
|
|
2125
2166
|
start_row,
|
|
2126
2167
|
row_limit,
|
|
2127
|
-
response.rows.length
|
|
2168
|
+
response.rows.length === sourceRowLimit,
|
|
2128
2169
|
start_row + sourceRowLimit,
|
|
2129
2170
|
);
|
|
2130
2171
|
|
|
@@ -2187,6 +2228,9 @@ class GscMcpRuntime {
|
|
|
2187
2228
|
}) => {
|
|
2188
2229
|
assertDateRange(start_date_a, end_date_a, 'start_date_a', 'end_date_a');
|
|
2189
2230
|
assertDateRange(start_date_b, end_date_b, 'start_date_b', 'end_date_b');
|
|
2231
|
+
const today = getSearchConsoleCalendarDate();
|
|
2232
|
+
assertDateNotInFuture(end_date_a, today, 'end_date_a');
|
|
2233
|
+
assertDateNotInFuture(end_date_b, today, 'end_date_b');
|
|
2190
2234
|
const googleId = this.requireGoogleId();
|
|
2191
2235
|
const rateLimitError = await this.rateLimitError(googleId, 'analytics.compare');
|
|
2192
2236
|
if (rateLimitError) return rateLimitError;
|
|
@@ -2248,12 +2292,12 @@ class GscMcpRuntime {
|
|
|
2248
2292
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
2249
2293
|
},
|
|
2250
2294
|
async ({ site_url, end_date }) => {
|
|
2295
|
+
const today = getSearchConsoleCalendarDate();
|
|
2296
|
+
const resolvedEndDate = resolveWeeklyDigestEndDate(end_date, today);
|
|
2297
|
+
assertDateNotInFuture(resolvedEndDate, today, 'end_date');
|
|
2251
2298
|
const googleId = this.requireGoogleId();
|
|
2252
2299
|
const rateLimitError = await this.rateLimitError(googleId, 'reports.weekly_digest');
|
|
2253
2300
|
if (rateLimitError) return rateLimitError;
|
|
2254
|
-
const today = getSearchConsoleCalendarDate();
|
|
2255
|
-
const resolvedEndDate = resolveWeeklyDigestEndDate(end_date, today);
|
|
2256
|
-
assertDateNotInFuture(resolvedEndDate, today);
|
|
2257
2301
|
|
|
2258
2302
|
try {
|
|
2259
2303
|
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
|
}
|