@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digestseo/mcp-gsc",
3
- "version": "0.4.15",
3
+ "version": "0.4.16",
4
4
  "private": false,
5
5
  "main": "scripts/glama-start.mjs",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -41,7 +41,11 @@ import {
41
41
  PendingAuthStateStore,
42
42
  stashPendingAuth,
43
43
  } from './pending-auth-state';
44
- import { enforceToolRateLimit, type RateLimitedToolName } from './tool-rate-limit';
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 = 1,
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
  },
@@ -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 defaults to 200 publish requests per project per day. Two per
64
- // user reserves the default quota for up to 100 authenticated users.
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',