@digestseo/mcp-gsc 0.4.15 → 0.4.17
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 +6 -5
- package/SECURITY.md +5 -3
- package/SETUP.md +2 -2
- package/package.json +1 -1
- package/src/access-mode.ts +8 -1
- package/src/google.ts +36 -0
- package/src/index.ts +105 -10
- package/src/tool-rate-limit.ts +26 -2
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.17] - 2026-09-22
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `indexing.status` reads Google's latest successful Indexing API update/remove notification receipts for a previously submitted URL while explicitly distinguishing notification receipt from crawl, index coverage, indexing completion, or removal completion.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- `sites.delete` now describes and reports Google's actual account-scoped effect: it removes the property from the connected user's Search Console site set rather than implying that the website itself or a global Search Console property is deleted.
|
|
17
|
+
|
|
18
|
+
## [0.4.16] - 2026-09-22
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- 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.
|
|
22
|
+
- `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.
|
|
23
|
+
|
|
10
24
|
## [0.4.15] - 2026-09-22
|
|
11
25
|
|
|
12
26
|
### Fixed
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ It runs on [Cloudflare Workers](https://workers.cloudflare.com/) and ships with
|
|
|
72
72
|
|
|
73
73
|
## Tools
|
|
74
74
|
|
|
75
|
-
By default (`GSC_ACCESS_MODE=readwrite`), this server exposes
|
|
75
|
+
By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 22 tools. Read-only analytics and reporting tools are marked with MCP's `readOnlyHint`; the write tools below can change Search Console properties, sitemaps, or indexing state. Set `GSC_ACCESS_MODE=readonly` to request only the Search Console read-only scope and expose the 16 tools that do not require either write access or the separate Indexing API scope.
|
|
76
76
|
|
|
77
77
|
| Tool | Access | What it does |
|
|
78
78
|
|---|---|---|
|
|
@@ -87,17 +87,18 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 21 tools. Read-onl
|
|
|
87
87
|
| **`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. |
|
|
88
88
|
| **`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. |
|
|
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
|
-
| **`sites.add`** / **`sites.delete`** | Write | Add or remove a
|
|
90
|
+
| **`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. |
|
|
91
91
|
| **`sitemaps.submit`** / **`sitemaps.delete`** | Write | Submit or remove a sitemap. |
|
|
92
|
-
| **`indexing.
|
|
92
|
+
| **`indexing.status`** | Read | Read Google's latest successful Indexing API `URL_UPDATED` / `URL_DELETED` notification receipts for a previously submitted URL. This is notification metadata only, not index coverage, crawl status, indexing completion, or removal completion. It requires `GSC_ACCESS_MODE=readwrite` because Google's metadata endpoint uses the Indexing API OAuth scope. |
|
|
93
|
+
| **`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
94
|
|
|
94
95
|
> **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
96
|
|
|
96
|
-
Read-write mode requests the Google Search Console read-write and Indexing API scopes. Read-only mode requests only `https://www.googleapis.com/auth/webmasters.readonly` (plus `openid` and `email`) and does not register the five write tools. Read-write remains the default so existing deployments retain their current behavior; see [SETUP.md](SETUP.md) to select a mode before connecting users.
|
|
97
|
+
Read-write mode requests the Google Search Console read-write and Indexing API scopes. Read-only mode requests only `https://www.googleapis.com/auth/webmasters.readonly` (plus `openid` and `email`) and does not register the five write tools or the read-only `indexing.status` lookup, because that lookup still requires Google's separate Indexing API scope. Read-write remains the default so existing deployments retain their current behavior; see [SETUP.md](SETUP.md) to select a mode before connecting users.
|
|
97
98
|
|
|
98
99
|
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
100
|
|
|
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.
|
|
101
|
+
> **`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
102
|
|
|
102
103
|
## What you can ask
|
|
103
104
|
|
package/SECURITY.md
CHANGED
|
@@ -51,8 +51,8 @@ Notes:
|
|
|
51
51
|
## Tool rate limits
|
|
52
52
|
|
|
53
53
|
The server enforces the following fixed-window limits before making upstream
|
|
54
|
-
requests. They are per authenticated user and tool category;
|
|
55
|
-
also
|
|
54
|
+
requests. They are per authenticated user and tool category; the Indexing API
|
|
55
|
+
publish and metadata-read categories also have deployment-wide project guards. A rejected invocation returns a retry
|
|
56
56
|
delay and does not disclose another user's usage.
|
|
57
57
|
|
|
58
58
|
| Category | Tools | Limit |
|
|
@@ -61,9 +61,11 @@ delay and does not disclose another user's usage.
|
|
|
61
61
|
| URL inspection | `urls.inspect`, `urls.inspect_many` | 20 inspected URLs per user / 24 hours; batch requests reserve one unit per URL and run at most 3 inspections concurrently |
|
|
62
62
|
| Search Console writes | `sites.add`, `sites.delete`, `sitemaps.submit`, `sitemaps.delete` | 10 per user / hour |
|
|
63
63
|
| Indexing publish | `indexing.request` | 2 per user / 24 hours; 200 per deployment / 24 hours |
|
|
64
|
+
| Indexing metadata | `indexing.status` | 30 per user / minute; 180 per deployment / minute |
|
|
64
65
|
| Weekly digest | `reports.weekly_digest` | 6 per user / hour |
|
|
65
66
|
|
|
66
|
-
The Indexing API's default publish quota is 200 requests per project per day
|
|
67
|
+
The Indexing API's default publish quota is 200 requests per project per day,
|
|
68
|
+
and its default metadata-read quota is 180 requests per project per minute.
|
|
67
69
|
Search Analytics and URL Inspection have separate load and per-site quotas, so
|
|
68
70
|
these limits are intentionally conservative rather than a replacement for
|
|
69
71
|
monitoring the Google Cloud project quota.
|
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
|
---
|
|
@@ -155,7 +155,7 @@ The template also sets `GSC_ACCESS_MODE` to `readwrite`, preserving the historic
|
|
|
155
155
|
}
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
-
Read-only deployments request `webmasters.readonly`, omit the Indexing API scope, and do not register `sites.add`, `sites.delete`, `sitemaps.submit`, `sitemaps.delete`, or `indexing.
|
|
158
|
+
Read-only deployments request `webmasters.readonly`, omit the Indexing API scope, and do not register `sites.add`, `sites.delete`, `sitemaps.submit`, `sitemaps.delete`, `indexing.request`, or the otherwise read-only `indexing.status` lookup. Changing modes affects OAuth grants for future connections; reconnect users after changing the mode so Google grants the matching scope set.
|
|
159
159
|
|
|
160
160
|
`wrangler.jsonc` is gitignored because it contains your account's namespace ids. The `wrangler.example.jsonc` template stays in git.
|
|
161
161
|
|
package/package.json
CHANGED
package/src/access-mode.ts
CHANGED
|
@@ -25,6 +25,13 @@ export const WRITE_TOOL_NAMES = [
|
|
|
25
25
|
'indexing.request',
|
|
26
26
|
] as const;
|
|
27
27
|
|
|
28
|
+
const INDEXING_SCOPE_READ_TOOL_NAMES = ['indexing.status'] as const;
|
|
29
|
+
|
|
30
|
+
const READWRITE_ONLY_TOOL_NAMES = [
|
|
31
|
+
...WRITE_TOOL_NAMES,
|
|
32
|
+
...INDEXING_SCOPE_READ_TOOL_NAMES,
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
28
35
|
export function resolveGscAccessMode(value: string | undefined): GscAccessMode {
|
|
29
36
|
if (value === undefined || value === '') {
|
|
30
37
|
return DEFAULT_GSC_ACCESS_MODE;
|
|
@@ -54,6 +61,6 @@ export function getToolCatalogForAccessMode<T extends { name: string }>(
|
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
return tools.filter(
|
|
57
|
-
(tool) => !(
|
|
64
|
+
(tool) => !(READWRITE_ONLY_TOOL_NAMES as readonly string[]).includes(tool.name),
|
|
58
65
|
);
|
|
59
66
|
}
|
package/src/google.ts
CHANGED
|
@@ -1475,6 +1475,42 @@ export async function requestIndexing(
|
|
|
1475
1475
|
return await resp.json();
|
|
1476
1476
|
}
|
|
1477
1477
|
|
|
1478
|
+
export interface IndexingNotification {
|
|
1479
|
+
url?: string;
|
|
1480
|
+
type?: 'URL_UPDATED' | 'URL_DELETED' | string;
|
|
1481
|
+
notifyTime?: string;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
export interface IndexingNotificationMetadata {
|
|
1485
|
+
url?: string;
|
|
1486
|
+
latestUpdate?: IndexingNotification;
|
|
1487
|
+
latestRemove?: IndexingNotification;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
export async function getIndexingNotificationMetadata(
|
|
1491
|
+
accessToken: string,
|
|
1492
|
+
url: string,
|
|
1493
|
+
): Promise<IndexingNotificationMetadata> {
|
|
1494
|
+
const endpoint = new URL(
|
|
1495
|
+
'https://indexing.googleapis.com/v3/urlNotifications/metadata',
|
|
1496
|
+
);
|
|
1497
|
+
endpoint.searchParams.set('url', url);
|
|
1498
|
+
|
|
1499
|
+
const resp = await fetchGoogleRead(endpoint.toString(), {
|
|
1500
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
1501
|
+
});
|
|
1502
|
+
if (resp.status === 401) {
|
|
1503
|
+
throw new Error(GSC_ACCESS_REVOKED_MESSAGE);
|
|
1504
|
+
}
|
|
1505
|
+
if (!resp.ok) {
|
|
1506
|
+
const text = await resp.text();
|
|
1507
|
+
throw new Error(
|
|
1508
|
+
`Get indexing notification status failed: ${resp.status} ${text}`,
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
return (await resp.json()) as IndexingNotificationMetadata;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1478
1514
|
export interface PerformanceComparisonRow {
|
|
1479
1515
|
key: string;
|
|
1480
1516
|
period_a: { clicks: number; impressions: number; ctr: number; position: number };
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
processCannibalization,
|
|
29
29
|
processContentDecay,
|
|
30
30
|
requestIndexing,
|
|
31
|
+
getIndexingNotificationMetadata,
|
|
31
32
|
processPerformanceComparison,
|
|
32
33
|
type PaginatedSearchAnalyticsResult,
|
|
33
34
|
} from './google';
|
|
@@ -41,7 +42,11 @@ import {
|
|
|
41
42
|
PendingAuthStateStore,
|
|
42
43
|
stashPendingAuth,
|
|
43
44
|
} from './pending-auth-state';
|
|
44
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
enforceToolRateLimit,
|
|
47
|
+
getToolRateLimitUnits,
|
|
48
|
+
type RateLimitedToolName,
|
|
49
|
+
} from './tool-rate-limit';
|
|
45
50
|
export { ToolRateLimiter } from './tool-rate-limiter-do';
|
|
46
51
|
import { generateWeeklyDigest, resolveWeeklyDigestEndDate } from './digest';
|
|
47
52
|
import {
|
|
@@ -213,6 +218,9 @@ const SITE_DETAIL_OUTPUT_SCHEMA = {
|
|
|
213
218
|
const SITE_ADD_VERIFICATION_NOTE =
|
|
214
219
|
"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
220
|
|
|
221
|
+
const INDEXING_PROVIDER_USAGE_NOTE =
|
|
222
|
+
"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.";
|
|
223
|
+
|
|
216
224
|
const URL_INSPECTION_SCOPE_NOTE =
|
|
217
225
|
"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
226
|
|
|
@@ -294,6 +302,13 @@ const SITE_ADD_OUTPUT_SCHEMA = {
|
|
|
294
302
|
ownership_verification_note: z.string(),
|
|
295
303
|
};
|
|
296
304
|
|
|
305
|
+
const SITE_DELETE_OUTPUT_SCHEMA = {
|
|
306
|
+
message: z.string(),
|
|
307
|
+
removed_from_connected_account_site_set: z.literal(true),
|
|
308
|
+
website_content_deleted: z.literal(false),
|
|
309
|
+
provider_scope_note: z.string(),
|
|
310
|
+
};
|
|
311
|
+
|
|
297
312
|
const SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA = z.object({
|
|
298
313
|
rows_fetched: z.number().int().nonnegative(),
|
|
299
314
|
pages_fetched: z.number().int().positive(),
|
|
@@ -440,6 +455,28 @@ const CONTENT_DECAY_OUTPUT_SCHEMA = {
|
|
|
440
455
|
const INDEXING_OUTPUT_SCHEMA = {
|
|
441
456
|
result: z.unknown(),
|
|
442
457
|
note: z.string(),
|
|
458
|
+
provider_default_quota_for_testing_only: z.literal(true),
|
|
459
|
+
provider_usage_approval_required: z.literal(true),
|
|
460
|
+
provider_spam_detection_applies: z.literal(true),
|
|
461
|
+
provider_usage_note: z.string(),
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const INDEXING_NOTIFICATION_SCHEMA = z.object({
|
|
465
|
+
url: z.string().optional(),
|
|
466
|
+
type: z.string().optional(),
|
|
467
|
+
notifyTime: z.string().optional(),
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const INDEXING_STATUS_OUTPUT_SCHEMA = {
|
|
471
|
+
notification_metadata: z.object({
|
|
472
|
+
url: z.string().optional(),
|
|
473
|
+
latestUpdate: INDEXING_NOTIFICATION_SCHEMA.optional(),
|
|
474
|
+
latestRemove: INDEXING_NOTIFICATION_SCHEMA.optional(),
|
|
475
|
+
}),
|
|
476
|
+
notification_receipt_only: z.literal(true),
|
|
477
|
+
index_coverage_report: z.literal(false),
|
|
478
|
+
indexing_or_removal_completion_proven: z.literal(false),
|
|
479
|
+
note: z.string(),
|
|
443
480
|
};
|
|
444
481
|
|
|
445
482
|
const INDEXED_PAGES_OUTPUT_SCHEMA = {
|
|
@@ -593,7 +630,7 @@ const TOOL_CATALOG = [
|
|
|
593
630
|
{
|
|
594
631
|
name: 'sites.delete',
|
|
595
632
|
description:
|
|
596
|
-
|
|
633
|
+
"Remove an existing website property from the connected Google account's Search Console site set. This changes that account's Search Console membership for the property; it does not delete the website itself.",
|
|
597
634
|
},
|
|
598
635
|
{
|
|
599
636
|
name: 'analytics.query',
|
|
@@ -658,7 +695,12 @@ const TOOL_CATALOG = [
|
|
|
658
695
|
{
|
|
659
696
|
name: 'indexing.request',
|
|
660
697
|
description:
|
|
661
|
-
'Request an eligible JobPosting or livestream URL update through Google\'s restricted Indexing API; this is not a general webpage submission tool.',
|
|
698
|
+
'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.',
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
name: 'indexing.status',
|
|
702
|
+
description:
|
|
703
|
+
'Read the latest successful Indexing API update/remove notifications Google received for a previously submitted URL. This reports notification receipt only, not crawl, index coverage, indexing completion, or removal completion.',
|
|
662
704
|
},
|
|
663
705
|
{
|
|
664
706
|
name: 'indexing.list_pages',
|
|
@@ -704,7 +746,7 @@ class GscMcpRuntime {
|
|
|
704
746
|
private async rateLimitError(
|
|
705
747
|
googleId: string,
|
|
706
748
|
toolName: RateLimitedToolName,
|
|
707
|
-
units =
|
|
749
|
+
units = getToolRateLimitUnits(toolName),
|
|
708
750
|
) {
|
|
709
751
|
const result = await enforceToolRateLimit(this.env, googleId, toolName, units);
|
|
710
752
|
if (result.allowed) return null;
|
|
@@ -1402,12 +1444,12 @@ class GscMcpRuntime {
|
|
|
1402
1444
|
this.server.registerTool(
|
|
1403
1445
|
'sites.delete',
|
|
1404
1446
|
{
|
|
1405
|
-
title: '
|
|
1406
|
-
description:
|
|
1447
|
+
title: 'Remove Search Console property from account',
|
|
1448
|
+
description: "Remove an existing website property from the connected Google account's Search Console site set. Google documents this operation as removing the site from the user's Search Console sites; it does not delete website content.",
|
|
1407
1449
|
inputSchema: {
|
|
1408
1450
|
site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
|
|
1409
1451
|
},
|
|
1410
|
-
outputSchema:
|
|
1452
|
+
outputSchema: SITE_DELETE_OUTPUT_SCHEMA,
|
|
1411
1453
|
annotations: WRITE_TOOL_ANNOTATIONS['sites.delete'],
|
|
1412
1454
|
},
|
|
1413
1455
|
async ({ site_url }) => {
|
|
@@ -1416,8 +1458,14 @@ class GscMcpRuntime {
|
|
|
1416
1458
|
if (rateLimitError) return rateLimitError;
|
|
1417
1459
|
const accessToken = await this.getAccessToken(googleId);
|
|
1418
1460
|
await deleteSite(accessToken, site_url);
|
|
1419
|
-
const message = `Successfully
|
|
1420
|
-
|
|
1461
|
+
const message = `Successfully removed site property from the connected account's Search Console site set: ${site_url}`;
|
|
1462
|
+
const payload = {
|
|
1463
|
+
message,
|
|
1464
|
+
removed_from_connected_account_site_set: true as const,
|
|
1465
|
+
website_content_deleted: false as const,
|
|
1466
|
+
provider_scope_note: "Google's Sites.delete method removes the property from the connected user's Search Console site set. It does not delete the website itself.",
|
|
1467
|
+
};
|
|
1468
|
+
return toolResponse(`${message}\n\n${payload.provider_scope_note}`, payload);
|
|
1421
1469
|
},
|
|
1422
1470
|
);
|
|
1423
1471
|
|
|
@@ -1656,11 +1704,54 @@ class GscMcpRuntime {
|
|
|
1656
1704
|
);
|
|
1657
1705
|
|
|
1658
1706
|
if (accessMode === 'readwrite') {
|
|
1707
|
+
this.server.registerTool(
|
|
1708
|
+
'indexing.status',
|
|
1709
|
+
{
|
|
1710
|
+
title: 'Get Indexing API notification status',
|
|
1711
|
+
description: "Read the latest successful Indexing API URL_UPDATED and URL_DELETED notifications Google received for a previously submitted URL within an owner-level Search Console property accessible to the connected Google account. This is a read-only notification-receipt lookup, not an index-coverage report and not proof that Google crawled, indexed, or removed the URL. It is available only in readwrite access mode because Google's metadata endpoint requires the Indexing API OAuth scope.",
|
|
1712
|
+
inputSchema: {
|
|
1713
|
+
site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA.describe(`${SEARCH_CONSOLE_PROPERTY_DESCRIPTION} The connected Google account must be an owner of this property.`),
|
|
1714
|
+
url: z.string().superRefine((value, ctx) => {
|
|
1715
|
+
try {
|
|
1716
|
+
assertIndexingRequestUrl(value);
|
|
1717
|
+
} catch (error) {
|
|
1718
|
+
ctx.addIssue({
|
|
1719
|
+
code: 'custom',
|
|
1720
|
+
message: (error as Error).message,
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
}).describe('A fully qualified HTTP/HTTPS URL previously submitted successfully through the Google Indexing API. It must fall within site_url.'),
|
|
1724
|
+
},
|
|
1725
|
+
outputSchema: INDEXING_STATUS_OUTPUT_SCHEMA,
|
|
1726
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
1727
|
+
},
|
|
1728
|
+
async ({ site_url, url }) => {
|
|
1729
|
+
const googleId = this.requireGoogleId();
|
|
1730
|
+
const rateLimitError = await this.rateLimitError(googleId, 'indexing.status');
|
|
1731
|
+
if (rateLimitError) return rateLimitError;
|
|
1732
|
+
const accessToken = await this.getAccessToken(googleId);
|
|
1733
|
+
const sites = await listSites(accessToken);
|
|
1734
|
+
assertIndexingUrlAuthorized(url, site_url, sites);
|
|
1735
|
+
const notificationMetadata = await getIndexingNotificationMetadata(
|
|
1736
|
+
accessToken,
|
|
1737
|
+
url,
|
|
1738
|
+
);
|
|
1739
|
+
const payload = {
|
|
1740
|
+
notification_metadata: notificationMetadata,
|
|
1741
|
+
notification_receipt_only: true as const,
|
|
1742
|
+
index_coverage_report: false as const,
|
|
1743
|
+
indexing_or_removal_completion_proven: false as const,
|
|
1744
|
+
note: "This reports the latest successful Indexing API notification receipt(s) Google recorded for the URL. It does not report whether Google crawled, indexed, or removed the URL; use URL Inspection for indexed-state evidence.",
|
|
1745
|
+
};
|
|
1746
|
+
return toolResponse(JSON.stringify(payload, null, 2), payload);
|
|
1747
|
+
},
|
|
1748
|
+
);
|
|
1749
|
+
|
|
1659
1750
|
this.server.registerTool(
|
|
1660
1751
|
'indexing.request',
|
|
1661
1752
|
{
|
|
1662
1753
|
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.",
|
|
1754
|
+
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
1755
|
inputSchema: {
|
|
1665
1756
|
site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA.describe(`${SEARCH_CONSOLE_PROPERTY_DESCRIPTION} The connected Google account must be an owner of this property.`),
|
|
1666
1757
|
url: z.string().superRefine((value, ctx) => {
|
|
@@ -1688,6 +1779,10 @@ class GscMcpRuntime {
|
|
|
1688
1779
|
const payload = {
|
|
1689
1780
|
result,
|
|
1690
1781
|
note: "Google accepting this notification does not guarantee the URL will be indexed. Indexing remains at Google's discretion.",
|
|
1782
|
+
provider_default_quota_for_testing_only: true as const,
|
|
1783
|
+
provider_usage_approval_required: true as const,
|
|
1784
|
+
provider_spam_detection_applies: true as const,
|
|
1785
|
+
provider_usage_note: INDEXING_PROVIDER_USAGE_NOTE,
|
|
1691
1786
|
};
|
|
1692
1787
|
return toolResponse(JSON.stringify(payload, null, 2), payload);
|
|
1693
1788
|
},
|
package/src/tool-rate-limit.ts
CHANGED
|
@@ -60,12 +60,22 @@ 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,
|
|
68
69
|
},
|
|
70
|
+
indexing_metadata: {
|
|
71
|
+
category: 'indexing-metadata',
|
|
72
|
+
tools: ['indexing.status'],
|
|
73
|
+
// Google publishes a default metadata-read quota of 180 requests per
|
|
74
|
+
// minute per project. Keep per-user headroom inside that shared ceiling.
|
|
75
|
+
windowMs: MINUTE_MS,
|
|
76
|
+
userLimit: 30,
|
|
77
|
+
projectLimit: 180,
|
|
78
|
+
},
|
|
69
79
|
weekly_digest: {
|
|
70
80
|
category: 'weekly-digest',
|
|
71
81
|
tools: ['reports.weekly_digest'],
|
|
@@ -79,6 +89,16 @@ export type ToolRateLimitCategory = keyof typeof TOOL_RATE_LIMIT_POLICIES;
|
|
|
79
89
|
export type RateLimitedToolName =
|
|
80
90
|
(typeof TOOL_RATE_LIMIT_POLICIES)[ToolRateLimitCategory]['tools'][number];
|
|
81
91
|
|
|
92
|
+
const TOOL_RATE_LIMIT_UNITS: Partial<Record<RateLimitedToolName, number>> = {
|
|
93
|
+
// These helpers can fan one MCP call out into several Search Analytics
|
|
94
|
+
// requests. Reserve the worst-case upstream call count before starting so
|
|
95
|
+
// a single helper cannot bypass the shared 10-minute safety budget.
|
|
96
|
+
'insights.quick_wins': 4,
|
|
97
|
+
'insights.cannibalization': 4,
|
|
98
|
+
'insights.content_decay': 8,
|
|
99
|
+
'analytics.compare': 8,
|
|
100
|
+
};
|
|
101
|
+
|
|
82
102
|
const POLICY_BY_TOOL = new Map<string, ToolRateLimitPolicy>(
|
|
83
103
|
Object.values(TOOL_RATE_LIMIT_POLICIES).flatMap((policy) =>
|
|
84
104
|
policy.tools.map((tool) => [tool, policy] as const),
|
|
@@ -194,6 +214,10 @@ export function getToolRateLimitPolicy(toolName: string): ToolRateLimitPolicy |
|
|
|
194
214
|
return POLICY_BY_TOOL.get(toolName);
|
|
195
215
|
}
|
|
196
216
|
|
|
217
|
+
export function getToolRateLimitUnits(toolName: RateLimitedToolName): number {
|
|
218
|
+
return TOOL_RATE_LIMIT_UNITS[toolName] ?? 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
197
221
|
async function userBucketFor(googleId: string): Promise<string> {
|
|
198
222
|
const digest = await crypto.subtle.digest(
|
|
199
223
|
'SHA-256',
|