@digestseo/mcp-gsc 0.4.14 → 0.4.16
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 +12 -0
- package/README.md +15 -13
- package/SETUP.md +1 -1
- package/package.json +1 -1
- package/src/index.ts +35 -9
- package/src/tool-rate-limit.ts +17 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.16] - 2026-09-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- High-level Search Analytics helpers now reserve their worst-case upstream request fan-out against the shared local safety budget, so paginated comparison/cannibalization/decay workflows cannot bypass the 10-minute limiter by hiding multiple Google API calls behind one MCP tool call.
|
|
14
|
+
- `indexing.request` now exposes Google's current Indexing API usage boundary: the default 200 publish requests/day/project is onboarding/testing capacity rather than ongoing-use approval, ongoing usage/resource provisioning requires approval, and all submissions are subject to spam detection.
|
|
15
|
+
|
|
16
|
+
## [0.4.15] - 2026-09-22
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- `analytics.query` now distinguishes fresh preliminary Search Analytics data from finalized-data lag, echoes the requested `data_state`, and reports whether preliminary data is possible instead of implying a blanket 2-3 day delay.
|
|
20
|
+
- Repo-owned quick-connect configs and client docs now use the verified loopback npm launcher instead of routing new users to the intentionally unadvertised hosted remote while that deployment lags the current package.
|
|
21
|
+
|
|
10
22
|
## [0.4.14] - 2026-09-22
|
|
11
23
|
|
|
12
24
|
### Fixed
|
package/README.md
CHANGED
|
@@ -30,35 +30,37 @@ The local launcher defaults to `GSC_ACCESS_MODE=readwrite`. For least-privilege
|
|
|
30
30
|
|
|
31
31
|
For Google OAuth and Cloudflare deployment configuration, follow [SETUP.md](SETUP.md).
|
|
32
32
|
|
|
33
|
-
### Connect
|
|
33
|
+
### Connect the local launcher
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
For the current verified quick-connect path, start the npm launcher first after supplying the Google OAuth and token-encryption environment variables described in [SETUP.md](SETUP.md):
|
|
36
36
|
|
|
37
|
-
```
|
|
38
|
-
|
|
37
|
+
```bash
|
|
38
|
+
npx -y @digestseo/mcp-gsc
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Then connect your MCP client to the launcher's loopback endpoint:
|
|
42
42
|
|
|
43
43
|
```
|
|
44
|
-
|
|
44
|
+
http://127.0.0.1:8080/mcp
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
Self-hosted Cloudflare Worker deployments continue to use `https://<your-worker>.workers.dev/mcp`. The repo does not currently advertise the owner-hosted remote as a quick-connect path because its deployed tool contract is behind the published package.
|
|
48
|
+
|
|
47
49
|
**Claude Code**
|
|
48
50
|
|
|
49
51
|
```bash
|
|
50
|
-
claude mcp add --transport http gsc
|
|
52
|
+
claude mcp add --transport http gsc http://127.0.0.1:8080/mcp
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
**Cursor**
|
|
54
56
|
|
|
55
|
-
[](https://cursor.com/en/install-mcp?name=gsc&config=
|
|
57
|
+
[](https://cursor.com/en/install-mcp?name=gsc&config=eyJ1cmwiOiJodHRwOi8vMTI3LjAuMC4xOjgwODAvbWNwIn0%3D)
|
|
56
58
|
|
|
57
59
|
**Kiro**
|
|
58
60
|
|
|
59
|
-
[](https://kiro.dev/launch/mcp/add?name=mcp-gsc&config=%7B%22url%22%3A%
|
|
61
|
+
[](https://kiro.dev/launch/mcp/add?name=mcp-gsc&config=%7B%22url%22%3A%22http%3A%2F%2F127.0.0.1%3A8080%2Fmcp%22%2C%22disabled%22%3Afalse%2C%22autoApprove%22%3A%5B%5D%7D)
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
Start the npm launcher before using either install button. Complete its Google OAuth flow on first use.
|
|
62
64
|
|
|
63
65
|
**ChatGPT** — add it as a custom MCP app in developer mode; see [Connect in your AI client](#connect-in-your-ai-client) below.
|
|
64
66
|
|
|
@@ -77,7 +79,7 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 21 tools. Read-onl
|
|
|
77
79
|
| **`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. |
|
|
78
80
|
| **`sites.list`** | Read | List the Search Console properties the connected Google account can access (`siteUrl`, `permissionLevel`). |
|
|
79
81
|
| **`sites.get`** | Read | Retrieve one exact Search Console property and the connected account's permission level for it. |
|
|
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`. |
|
|
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`. |
|
|
81
83
|
| **`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. |
|
|
82
84
|
| **`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. |
|
|
83
85
|
| **`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. |
|
|
@@ -87,7 +89,7 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 21 tools. Read-onl
|
|
|
87
89
|
| **`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. |
|
|
88
90
|
| **`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. |
|
|
89
91
|
| **`sitemaps.submit`** / **`sitemaps.delete`** | Write | Submit or remove a sitemap. |
|
|
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. |
|
|
92
|
+
| **`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. Google's default 200 publish requests/day/project is onboarding/testing capacity rather than ongoing-use approval; additional approval is required for usage/resource provisioning, and submissions are spam-screened. |
|
|
91
93
|
|
|
92
94
|
> **Platform properties.** Search Console now supports platform properties for social/video accounts such as Instagram, TikTok, X, and YouTube in its UI. Google's current Search Console API documentation still defines `siteUrl` using URL-prefix and `sc-domain:` website-property forms and does not publish a platform-property API identifier contract. `sites.list` therefore preserves every identifier Google returns and marks whether it matches the documented API grammar; the other tools deliberately reject undocumented forms instead of inventing an identifier. Use the Search Console UI/export for a platform property until Google documents API support.
|
|
93
95
|
|
|
@@ -95,7 +97,7 @@ Read-write mode requests the Google Search Console read-write and Indexing API s
|
|
|
95
97
|
|
|
96
98
|
Large analytics responses are bounded at the MCP boundary instead of being generated and then discarded by clients with structured-content limits. List-style tools expose `start_row` / `next_start_row` where applicable; ranked analytical tools expose `limit`, `start_row`, and `result_page` metadata including `has_more`, `truncated`, and `byte_limit_reached`. Continue paging while `has_more` is true rather than assuming one response is complete.
|
|
97
99
|
|
|
98
|
-
> **`indexing.request` eligibility.** Google's Indexing API is not a general-purpose page submission tool — as of this writing, Google's own documentation limits it to two content types: pages with `JobPosting` structured data, and livestream pages with `BroadcastEvent` structured data nested inside `VideoObject`. Before submitting, the server fetches the target URL and checks static JSON-LD, Microdata, and RDFa markup for one of those two types; if neither is present (or the page can't be fetched), it returns an error explaining why the URL is ineligible instead of calling the Indexing API. Structured data injected only after client-side JavaScript runs cannot be confirmed by this bounded preflight and therefore fails closed. A successful submission is only an acknowledgment that Google received the notification — it does not guarantee the URL will be indexed.
|
|
100
|
+
> **`indexing.request` eligibility and provider approval.** Google's Indexing API is not a general-purpose page submission tool — as of this writing, Google's own documentation limits it to two content types: pages with `JobPosting` structured data, and livestream pages with `BroadcastEvent` structured data nested inside `VideoObject`. Before submitting, the server fetches the target URL and checks static JSON-LD, Microdata, and RDFa markup for one of those two types; if neither is present (or the page can't be fetched), it returns an error explaining why the URL is ineligible instead of calling the Indexing API. Structured data injected only after client-side JavaScript runs cannot be confirmed by this bounded preflight and therefore fails closed. Google's default 200 publish requests/day/project is for onboarding and submission testing; using the API beyond that onboarding/testing context requires Google's usage/resource-provisioning approval. Google also subjects every submission to spam detection and warns that abuse or attempts to exceed quotas through multiple accounts or other means can revoke access. A successful submission is only an acknowledgment that Google received the notification — it does not guarantee the URL will be indexed.
|
|
99
101
|
|
|
100
102
|
## What you can ask
|
|
101
103
|
|
package/SETUP.md
CHANGED
|
@@ -61,7 +61,7 @@ npx wrangler login
|
|
|
61
61
|
|
|
62
62
|
`readwrite` preserves the full tool surface: it grants Search Console read-write access and the Indexing API scope required to manage sites, sitemaps, and request URL crawling. `readonly` requests only the Search Console read-only scope and omits the mutation tools.
|
|
63
63
|
|
|
64
|
-
Note that the Indexing API itself is narrow: Google currently restricts it to pages containing `JobPosting` structured data or livestream pages containing `BroadcastEvent` inside `VideoObject`. It is not available for general webpage submission — the `indexing.request` tool checks a page's structured data before submitting and returns an error for ineligible URLs.
|
|
64
|
+
Note that the Indexing API itself is narrow: Google currently restricts it to pages containing `JobPosting` structured data or livestream pages containing `BroadcastEvent` inside `VideoObject`. It is not available for general webpage submission — the `indexing.request` tool checks a page's structured data before submitting and returns an error for ineligible URLs. Google's default **200 publish requests/day/project** is explicitly for onboarding and submission testing; ongoing usage/resource provisioning requires additional Google approval. All submissions are subject to spam detection, and Google warns that abuse or attempts to exceed quotas through multiple accounts or other means can result in revoked access. The server's own lower safety limits do not constitute Google approval or increase provider quota.
|
|
65
65
|
5. Save the Data Access changes. In **Google Auth platform → Audience**, leave **Publishing status** as **Testing** for now — see [Step 7](#step-7--important-google-verification).
|
|
66
66
|
|
|
67
67
|
---
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -41,7 +41,11 @@ import {
|
|
|
41
41
|
PendingAuthStateStore,
|
|
42
42
|
stashPendingAuth,
|
|
43
43
|
} from './pending-auth-state';
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
enforceToolRateLimit,
|
|
46
|
+
getToolRateLimitUnits,
|
|
47
|
+
type RateLimitedToolName,
|
|
48
|
+
} from './tool-rate-limit';
|
|
45
49
|
export { ToolRateLimiter } from './tool-rate-limiter-do';
|
|
46
50
|
import { generateWeeklyDigest, resolveWeeklyDigestEndDate } from './digest';
|
|
47
51
|
import {
|
|
@@ -213,6 +217,9 @@ const SITE_DETAIL_OUTPUT_SCHEMA = {
|
|
|
213
217
|
const SITE_ADD_VERIFICATION_NOTE =
|
|
214
218
|
"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
219
|
|
|
220
|
+
const INDEXING_PROVIDER_USAGE_NOTE =
|
|
221
|
+
"Google describes the Indexing API's default 200 publish-requests-per-day project quota as onboarding/testing capacity, not approval for ongoing usage. Additional approval is required for usage/resource provisioning. Google also applies spam detection to all submissions and warns that abuse or attempts to exceed quotas through multiple accounts or other means can result in revoked access.";
|
|
222
|
+
|
|
216
223
|
const URL_INSPECTION_SCOPE_NOTE =
|
|
217
224
|
"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.";
|
|
218
225
|
|
|
@@ -244,6 +251,12 @@ const SEARCH_ANALYTICS_OUTPUT_SCHEMA = {
|
|
|
244
251
|
row_count: z.number().int().nonnegative(),
|
|
245
252
|
start_row: z.number().int().nonnegative(),
|
|
246
253
|
rows: z.array(z.object(SEARCH_ROW_OUTPUT_SCHEMA)),
|
|
254
|
+
data_state: z.enum(['all', 'final', 'hourly_all']).describe(
|
|
255
|
+
'The Search Analytics dataState requested from Google for this response.',
|
|
256
|
+
),
|
|
257
|
+
preliminary_data_possible: z.boolean().describe(
|
|
258
|
+
'True when the requested data state can include fresh data that is still being collected and processed. This does not assert that every returned row is incomplete.',
|
|
259
|
+
),
|
|
247
260
|
position_supported: z.boolean().describe(
|
|
248
261
|
'Whether average position is supported for the selected Search Analytics search type. False for Google Discover and Google News.',
|
|
249
262
|
),
|
|
@@ -434,6 +447,10 @@ const CONTENT_DECAY_OUTPUT_SCHEMA = {
|
|
|
434
447
|
const INDEXING_OUTPUT_SCHEMA = {
|
|
435
448
|
result: z.unknown(),
|
|
436
449
|
note: z.string(),
|
|
450
|
+
provider_default_quota_for_testing_only: z.literal(true),
|
|
451
|
+
provider_usage_approval_required: z.literal(true),
|
|
452
|
+
provider_spam_detection_applies: z.literal(true),
|
|
453
|
+
provider_usage_note: z.string(),
|
|
437
454
|
};
|
|
438
455
|
|
|
439
456
|
const INDEXED_PAGES_OUTPUT_SCHEMA = {
|
|
@@ -652,7 +669,7 @@ const TOOL_CATALOG = [
|
|
|
652
669
|
{
|
|
653
670
|
name: 'indexing.request',
|
|
654
671
|
description:
|
|
655
|
-
'Request an eligible JobPosting or livestream URL update through Google\'s restricted Indexing API; this is not a general webpage submission tool.',
|
|
672
|
+
'Request an eligible JobPosting or livestream URL update through Google\'s restricted Indexing API; this is not a general webpage submission tool. Google treats the default publish quota as onboarding/testing capacity, requires approval for ongoing usage/resource provisioning, and spam-screens submissions.',
|
|
656
673
|
},
|
|
657
674
|
{
|
|
658
675
|
name: 'indexing.list_pages',
|
|
@@ -698,7 +715,7 @@ class GscMcpRuntime {
|
|
|
698
715
|
private async rateLimitError(
|
|
699
716
|
googleId: string,
|
|
700
717
|
toolName: RateLimitedToolName,
|
|
701
|
-
units =
|
|
718
|
+
units = getToolRateLimitUnits(toolName),
|
|
702
719
|
) {
|
|
703
720
|
const result = await enforceToolRateLimit(this.env, googleId, toolName, units);
|
|
704
721
|
if (result.allowed) return null;
|
|
@@ -989,10 +1006,13 @@ class GscMcpRuntime {
|
|
|
989
1006
|
' matching dimension. Expect the returned rows to cover only a',
|
|
990
1007
|
' subset of total impressions; this is normal Google behavior, not',
|
|
991
1008
|
' a data error.',
|
|
992
|
-
'- DATA FRESHNESS:
|
|
993
|
-
'
|
|
994
|
-
'
|
|
995
|
-
'
|
|
1009
|
+
'- DATA FRESHNESS: data_state "all" can include fresh preliminary',
|
|
1010
|
+
' data instead of imposing the finalized-data lag; "final" returns',
|
|
1011
|
+
' only finalized data and can therefore lag recent activity. When',
|
|
1012
|
+
' Google returns metadata.first_incomplete_date or',
|
|
1013
|
+
' metadata.first_incomplete_hour for date/hour groupings, treat',
|
|
1014
|
+
' that boundary and later rows as still subject to change. The',
|
|
1015
|
+
' response also echoes data_state and preliminary_data_possible.',
|
|
996
1016
|
'- AVERAGE POSITION is impression-weighted. To compute an overall',
|
|
997
1017
|
' position across multiple rows, use',
|
|
998
1018
|
' sum(position * impressions) / sum(impressions). Never plain-average',
|
|
@@ -1045,7 +1065,7 @@ class GscMcpRuntime {
|
|
|
1045
1065
|
site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
|
|
1046
1066
|
start_date: SEARCH_CONSOLE_DATE_SCHEMA.describe('Start date (inclusive) in YYYY-MM-DD format.'),
|
|
1047
1067
|
end_date: SEARCH_CONSOLE_DATE_SCHEMA.describe(
|
|
1048
|
-
'End date (inclusive) in YYYY-MM-DD format.
|
|
1068
|
+
'End date (inclusive) in YYYY-MM-DD format. Recent dates are allowed: data_state all/hourly_all can include preliminary data, while final returns only finalized data.',
|
|
1049
1069
|
),
|
|
1050
1070
|
dimensions: z
|
|
1051
1071
|
.array(
|
|
@@ -1173,6 +1193,8 @@ class GscMcpRuntime {
|
|
|
1173
1193
|
row_count: bounded.items.length,
|
|
1174
1194
|
start_row,
|
|
1175
1195
|
rows: bounded.items,
|
|
1196
|
+
data_state,
|
|
1197
|
+
preliminary_data_possible: data_state !== 'final',
|
|
1176
1198
|
position_supported:
|
|
1177
1199
|
search_type !== 'discover' && search_type !== 'googleNews',
|
|
1178
1200
|
...(search_type === 'discover' || search_type === 'googleNews'
|
|
@@ -1649,7 +1671,7 @@ class GscMcpRuntime {
|
|
|
1649
1671
|
'indexing.request',
|
|
1650
1672
|
{
|
|
1651
1673
|
title: 'Request Indexing',
|
|
1652
|
-
description: "Requests indexing through Google's Indexing API for a URL within an owner-level Search Console property accessible to the connected Google account. 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.",
|
|
1674
|
+
description: "Requests indexing through Google's Indexing API for a URL within an owner-level Search Console property accessible to the connected Google account. 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. Google's default 200 publish-requests-per-day project quota is for onboarding/testing rather than ongoing-use approval; additional approval is required for usage/resource provisioning, and all submissions are subject to spam detection.",
|
|
1653
1675
|
inputSchema: {
|
|
1654
1676
|
site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA.describe(`${SEARCH_CONSOLE_PROPERTY_DESCRIPTION} The connected Google account must be an owner of this property.`),
|
|
1655
1677
|
url: z.string().superRefine((value, ctx) => {
|
|
@@ -1677,6 +1699,10 @@ class GscMcpRuntime {
|
|
|
1677
1699
|
const payload = {
|
|
1678
1700
|
result,
|
|
1679
1701
|
note: "Google accepting this notification does not guarantee the URL will be indexed. Indexing remains at Google's discretion.",
|
|
1702
|
+
provider_default_quota_for_testing_only: true as const,
|
|
1703
|
+
provider_usage_approval_required: true as const,
|
|
1704
|
+
provider_spam_detection_applies: true as const,
|
|
1705
|
+
provider_usage_note: INDEXING_PROVIDER_USAGE_NOTE,
|
|
1680
1706
|
};
|
|
1681
1707
|
return toolResponse(JSON.stringify(payload, null, 2), payload);
|
|
1682
1708
|
},
|
package/src/tool-rate-limit.ts
CHANGED
|
@@ -60,8 +60,9 @@ export const TOOL_RATE_LIMIT_POLICIES = {
|
|
|
60
60
|
indexing_request: {
|
|
61
61
|
category: 'indexing-request',
|
|
62
62
|
tools: ['indexing.request'],
|
|
63
|
-
// Google
|
|
64
|
-
//
|
|
63
|
+
// Google's 200 publish/day default is onboarding/testing quota and does
|
|
64
|
+
// not itself authorize ongoing use. Mirror that number only as a local
|
|
65
|
+
// safety ceiling; two per user reserves it for up to 100 users.
|
|
65
66
|
windowMs: DAY_MS,
|
|
66
67
|
userLimit: 2,
|
|
67
68
|
projectLimit: 200,
|
|
@@ -79,6 +80,16 @@ export type ToolRateLimitCategory = keyof typeof TOOL_RATE_LIMIT_POLICIES;
|
|
|
79
80
|
export type RateLimitedToolName =
|
|
80
81
|
(typeof TOOL_RATE_LIMIT_POLICIES)[ToolRateLimitCategory]['tools'][number];
|
|
81
82
|
|
|
83
|
+
const TOOL_RATE_LIMIT_UNITS: Partial<Record<RateLimitedToolName, number>> = {
|
|
84
|
+
// These helpers can fan one MCP call out into several Search Analytics
|
|
85
|
+
// requests. Reserve the worst-case upstream call count before starting so
|
|
86
|
+
// a single helper cannot bypass the shared 10-minute safety budget.
|
|
87
|
+
'insights.quick_wins': 4,
|
|
88
|
+
'insights.cannibalization': 4,
|
|
89
|
+
'insights.content_decay': 8,
|
|
90
|
+
'analytics.compare': 8,
|
|
91
|
+
};
|
|
92
|
+
|
|
82
93
|
const POLICY_BY_TOOL = new Map<string, ToolRateLimitPolicy>(
|
|
83
94
|
Object.values(TOOL_RATE_LIMIT_POLICIES).flatMap((policy) =>
|
|
84
95
|
policy.tools.map((tool) => [tool, policy] as const),
|
|
@@ -194,6 +205,10 @@ export function getToolRateLimitPolicy(toolName: string): ToolRateLimitPolicy |
|
|
|
194
205
|
return POLICY_BY_TOOL.get(toolName);
|
|
195
206
|
}
|
|
196
207
|
|
|
208
|
+
export function getToolRateLimitUnits(toolName: RateLimitedToolName): number {
|
|
209
|
+
return TOOL_RATE_LIMIT_UNITS[toolName] ?? 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
197
212
|
async function userBucketFor(googleId: string): Promise<string> {
|
|
198
213
|
const digest = await crypto.subtle.digest(
|
|
199
214
|
'SHA-256',
|