@digestseo/mcp-gsc 0.4.15 → 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 +6 -0
- package/README.md +2 -2
- package/SETUP.md +1 -1
- package/package.json +1 -1
- package/src/index.ts +19 -4
- package/src/tool-rate-limit.ts +17 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ 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
|
+
|
|
10
16
|
## [0.4.15] - 2026-09-22
|
|
11
17
|
|
|
12
18
|
### Fixed
|
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 21 tools. Read-onl
|
|
|
89
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. |
|
|
90
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. |
|
|
91
91
|
| **`sitemaps.submit`** / **`sitemaps.delete`** | Write | Submit or remove a sitemap. |
|
|
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. |
|
|
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. |
|
|
93
93
|
|
|
94
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.
|
|
95
95
|
|
|
@@ -97,7 +97,7 @@ Read-write mode requests the Google Search Console read-write and Indexing API s
|
|
|
97
97
|
|
|
98
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.
|
|
99
99
|
|
|
100
|
-
> **`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.
|
|
101
101
|
|
|
102
102
|
## What you can ask
|
|
103
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
|
|
|
@@ -440,6 +447,10 @@ const CONTENT_DECAY_OUTPUT_SCHEMA = {
|
|
|
440
447
|
const INDEXING_OUTPUT_SCHEMA = {
|
|
441
448
|
result: z.unknown(),
|
|
442
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(),
|
|
443
454
|
};
|
|
444
455
|
|
|
445
456
|
const INDEXED_PAGES_OUTPUT_SCHEMA = {
|
|
@@ -658,7 +669,7 @@ const TOOL_CATALOG = [
|
|
|
658
669
|
{
|
|
659
670
|
name: 'indexing.request',
|
|
660
671
|
description:
|
|
661
|
-
'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.',
|
|
662
673
|
},
|
|
663
674
|
{
|
|
664
675
|
name: 'indexing.list_pages',
|
|
@@ -704,7 +715,7 @@ class GscMcpRuntime {
|
|
|
704
715
|
private async rateLimitError(
|
|
705
716
|
googleId: string,
|
|
706
717
|
toolName: RateLimitedToolName,
|
|
707
|
-
units =
|
|
718
|
+
units = getToolRateLimitUnits(toolName),
|
|
708
719
|
) {
|
|
709
720
|
const result = await enforceToolRateLimit(this.env, googleId, toolName, units);
|
|
710
721
|
if (result.allowed) return null;
|
|
@@ -1660,7 +1671,7 @@ class GscMcpRuntime {
|
|
|
1660
1671
|
'indexing.request',
|
|
1661
1672
|
{
|
|
1662
1673
|
title: 'Request Indexing',
|
|
1663
|
-
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.",
|
|
1664
1675
|
inputSchema: {
|
|
1665
1676
|
site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA.describe(`${SEARCH_CONSOLE_PROPERTY_DESCRIPTION} The connected Google account must be an owner of this property.`),
|
|
1666
1677
|
url: z.string().superRefine((value, ctx) => {
|
|
@@ -1688,6 +1699,10 @@ class GscMcpRuntime {
|
|
|
1688
1699
|
const payload = {
|
|
1689
1700
|
result,
|
|
1690
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,
|
|
1691
1706
|
};
|
|
1692
1707
|
return toolResponse(JSON.stringify(payload, null, 2), payload);
|
|
1693
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',
|