@digestseo/mcp-gsc 0.4.16 → 0.4.18
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/scripts/glama-start.mjs +0 -2
- package/src/access-mode.ts +8 -1
- package/src/google.ts +36 -0
- package/src/index.ts +120 -10
- package/src/tool-rate-limit.ts +9 -0
- package/wrangler.example.jsonc +3 -0
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.18] - 2026-09-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- The published npm launcher now keeps `GOOGLE_CLIENT_SECRET` and `TOKEN_ENCRYPTION_KEY` out of Wrangler `--var` command-line arguments; Wrangler loads those declared local secrets directly from the child process environment instead.
|
|
14
|
+
- `server.capabilities` now distinguishes stored local Google credentials from live provider authorization, exposing the credential-state basis and explicitly reporting that the discovery call does not probe Google before claiming a connection is usable.
|
|
15
|
+
|
|
16
|
+
## [0.4.17] - 2026-09-22
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- `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.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- `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.
|
|
23
|
+
|
|
10
24
|
## [0.4.16] - 2026-09-22
|
|
11
25
|
|
|
12
26
|
### Fixed
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ The self-hosting launcher is available as [`@digestseo/mcp-gsc`](https://www.npm
|
|
|
22
22
|
npx -y @digestseo/mcp-gsc
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
The npm launcher starts a loopback-only **Streamable HTTP** Worker on `127.0.0.1` (port `8080` by default); it is not a stdio MCP process. You can run the `npx` command from any directory because the launcher resolves its bundled Worker configuration from the installed package. Clients that launch the package should connect to `http://127.0.0.1:8080/mcp` after supplying the Google OAuth and token-encryption environment variables described in [SETUP.md](SETUP.md). Before the first OAuth sign-in, authorize the exact callback `http://127.0.0.1:8080/google/callback` in Google Cloud (or use the same custom `PORT` you launch with); the launcher prints both URLs at startup.
|
|
25
|
+
The npm launcher starts a loopback-only **Streamable HTTP** Worker on `127.0.0.1` (port `8080` by default); it is not a stdio MCP process. You can run the `npx` command from any directory because the launcher resolves its bundled Worker configuration from the installed package. Clients that launch the package should connect to `http://127.0.0.1:8080/mcp` after supplying the Google OAuth and token-encryption environment variables described in [SETUP.md](SETUP.md). The launcher keeps `GOOGLE_CLIENT_SECRET` and `TOKEN_ENCRYPTION_KEY` in the child process environment and Wrangler loads them through declared local secrets; their values are not copied into Wrangler `--var` command-line arguments. Before the first OAuth sign-in, authorize the exact callback `http://127.0.0.1:8080/google/callback` in Google Cloud (or use the same custom `PORT` you launch with); the launcher prints both URLs at startup.
|
|
26
26
|
|
|
27
27
|
Local KV and Durable Object state, including the encrypted Google session, is stored in `~/.mcp-gsc/state` by default instead of inside the versioned npm cache. That keeps the local connection available across package upgrades and ordinary npm cache cleanup. Set `MCP_GSC_STATE_DIR` to use another directory; keep the same `TOKEN_ENCRYPTION_KEY` when reusing existing state because stored refresh tokens are encrypted with that key. The launcher prints the effective state directory at startup. The first release with this stable path may require one Google reconnect because older launcher releases stored local state under the npm package cache.
|
|
28
28
|
|
|
@@ -72,11 +72,11 @@ 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
|
|---|---|---|
|
|
79
|
-
| **`server.capabilities`** | Read | List every tool this server exposes and report
|
|
79
|
+
| **`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. |
|
|
80
80
|
| **`sites.list`** | Read | List the Search Console properties the connected Google account can access (`siteUrl`, `permissionLevel`). |
|
|
81
81
|
| **`sites.get`** | Read | Retrieve one exact Search Console property and the connected account's permission level for it. |
|
|
82
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`. |
|
|
@@ -87,13 +87,14 @@ 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.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. |
|
|
92
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
|
|
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
|
@@ -112,7 +112,7 @@ openssl rand -base64 32
|
|
|
112
112
|
|
|
113
113
|
Paste that value when prompted. Keep it safe — if you lose it, the refresh tokens already in KV become undecryptable and every user has to reconnect.
|
|
114
114
|
|
|
115
|
-
For **local development**, put the same three values in `.dev.vars` instead — copy `.dev.vars.example` to `.dev.vars` (it's gitignored):
|
|
115
|
+
For **local development**, put the same three values in `.dev.vars` instead — copy `.dev.vars.example` to `.dev.vars` (it's gitignored). The published npm launcher can also read them from the process environment; its bundled Wrangler config declares `GOOGLE_CLIENT_SECRET` and `TOKEN_ENCRYPTION_KEY` as required local secrets so those sensitive values do not need to be serialized into `--var` command-line arguments:
|
|
116
116
|
|
|
117
117
|
```
|
|
118
118
|
GOOGLE_CLIENT_ID=...
|
|
@@ -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/scripts/glama-start.mjs
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';
|
|
@@ -190,7 +191,18 @@ const CAPABILITIES_OUTPUT_SCHEMA = {
|
|
|
190
191
|
server: z.string(),
|
|
191
192
|
version: z.string(),
|
|
192
193
|
access_mode: z.enum(['readonly', 'readwrite']),
|
|
193
|
-
auth_status: z.enum(['connected', 'not_connected', 'unknown'])
|
|
194
|
+
auth_status: z.enum(['connected', 'not_connected', 'unknown']).describe(
|
|
195
|
+
"Local credential state: 'connected' means a stored Google refresh credential is present; it does not live-verify that Google still accepts it.",
|
|
196
|
+
),
|
|
197
|
+
auth_status_basis: z.enum([
|
|
198
|
+
'stored_refresh_token',
|
|
199
|
+
'no_stored_refresh_token',
|
|
200
|
+
'local_state_unavailable',
|
|
201
|
+
]),
|
|
202
|
+
provider_auth_live_verified: z.literal(false).describe(
|
|
203
|
+
'Always false because server.capabilities does not make a Google token or API request merely to probe authorization.',
|
|
204
|
+
),
|
|
205
|
+
auth_note: z.string(),
|
|
194
206
|
tools: z.array(
|
|
195
207
|
z.object({
|
|
196
208
|
name: z.string(),
|
|
@@ -301,6 +313,13 @@ const SITE_ADD_OUTPUT_SCHEMA = {
|
|
|
301
313
|
ownership_verification_note: z.string(),
|
|
302
314
|
};
|
|
303
315
|
|
|
316
|
+
const SITE_DELETE_OUTPUT_SCHEMA = {
|
|
317
|
+
message: z.string(),
|
|
318
|
+
removed_from_connected_account_site_set: z.literal(true),
|
|
319
|
+
website_content_deleted: z.literal(false),
|
|
320
|
+
provider_scope_note: z.string(),
|
|
321
|
+
};
|
|
322
|
+
|
|
304
323
|
const SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA = z.object({
|
|
305
324
|
rows_fetched: z.number().int().nonnegative(),
|
|
306
325
|
pages_fetched: z.number().int().positive(),
|
|
@@ -453,6 +472,24 @@ const INDEXING_OUTPUT_SCHEMA = {
|
|
|
453
472
|
provider_usage_note: z.string(),
|
|
454
473
|
};
|
|
455
474
|
|
|
475
|
+
const INDEXING_NOTIFICATION_SCHEMA = z.object({
|
|
476
|
+
url: z.string().optional(),
|
|
477
|
+
type: z.string().optional(),
|
|
478
|
+
notifyTime: z.string().optional(),
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const INDEXING_STATUS_OUTPUT_SCHEMA = {
|
|
482
|
+
notification_metadata: z.object({
|
|
483
|
+
url: z.string().optional(),
|
|
484
|
+
latestUpdate: INDEXING_NOTIFICATION_SCHEMA.optional(),
|
|
485
|
+
latestRemove: INDEXING_NOTIFICATION_SCHEMA.optional(),
|
|
486
|
+
}),
|
|
487
|
+
notification_receipt_only: z.literal(true),
|
|
488
|
+
index_coverage_report: z.literal(false),
|
|
489
|
+
indexing_or_removal_completion_proven: z.literal(false),
|
|
490
|
+
note: z.string(),
|
|
491
|
+
};
|
|
492
|
+
|
|
456
493
|
const INDEXED_PAGES_OUTPUT_SCHEMA = {
|
|
457
494
|
pages: z.array(z.object({ page: z.string(), ...METRIC_OUTPUT_SCHEMA })),
|
|
458
495
|
note: z.string(),
|
|
@@ -604,7 +641,7 @@ const TOOL_CATALOG = [
|
|
|
604
641
|
{
|
|
605
642
|
name: 'sites.delete',
|
|
606
643
|
description:
|
|
607
|
-
|
|
644
|
+
"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.",
|
|
608
645
|
},
|
|
609
646
|
{
|
|
610
647
|
name: 'analytics.query',
|
|
@@ -671,6 +708,11 @@ const TOOL_CATALOG = [
|
|
|
671
708
|
description:
|
|
672
709
|
'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.',
|
|
673
710
|
},
|
|
711
|
+
{
|
|
712
|
+
name: 'indexing.status',
|
|
713
|
+
description:
|
|
714
|
+
'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.',
|
|
715
|
+
},
|
|
674
716
|
{
|
|
675
717
|
name: 'indexing.list_pages',
|
|
676
718
|
description: SEARCH_VISIBLE_PAGES_DESCRIPTION,
|
|
@@ -688,7 +730,7 @@ const TOOL_CATALOG = [
|
|
|
688
730
|
{
|
|
689
731
|
name: 'server.capabilities',
|
|
690
732
|
description:
|
|
691
|
-
"List every tool this server exposes and report
|
|
733
|
+
"List every tool this server exposes and report local Google credential state. A connected status means a stored refresh credential is present; it is not a live Google authorization check.",
|
|
692
734
|
},
|
|
693
735
|
] as const;
|
|
694
736
|
|
|
@@ -745,32 +787,49 @@ class GscMcpRuntime {
|
|
|
745
787
|
{
|
|
746
788
|
title: 'Get server capabilities and auth status',
|
|
747
789
|
description:
|
|
748
|
-
"List every tool this server exposes, its configured access mode, and
|
|
790
|
+
"List every tool this server exposes, its configured access mode, and local Google credential state. Call this first if you're unsure what tools are available, whether the deployment is read-only, or whether stored Google credentials are configured. auth_status='connected' means a stored refresh credential is present; this tool does not make a Google token/API request and therefore does not live-verify that Google still accepts it. Takes no arguments.",
|
|
749
791
|
inputSchema: {},
|
|
750
792
|
outputSchema: CAPABILITIES_OUTPUT_SCHEMA,
|
|
751
793
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
752
794
|
},
|
|
753
795
|
async () => {
|
|
754
796
|
let authStatus: 'connected' | 'not_connected' | 'unknown';
|
|
797
|
+
let authStatusBasis:
|
|
798
|
+
| 'stored_refresh_token'
|
|
799
|
+
| 'no_stored_refresh_token'
|
|
800
|
+
| 'local_state_unavailable';
|
|
755
801
|
try {
|
|
756
802
|
const googleId = this.props?.google_id;
|
|
757
803
|
if (!googleId) {
|
|
758
804
|
authStatus = 'not_connected';
|
|
805
|
+
authStatusBasis = 'no_stored_refresh_token';
|
|
759
806
|
} else {
|
|
760
807
|
const refreshToken = await getDecryptedRefreshToken(
|
|
761
808
|
this.env,
|
|
762
809
|
googleId,
|
|
763
810
|
);
|
|
764
811
|
authStatus = refreshToken ? 'connected' : 'not_connected';
|
|
812
|
+
authStatusBasis = refreshToken
|
|
813
|
+
? 'stored_refresh_token'
|
|
814
|
+
: 'no_stored_refresh_token';
|
|
765
815
|
}
|
|
766
816
|
} catch {
|
|
767
817
|
authStatus = 'unknown';
|
|
818
|
+
authStatusBasis = 'local_state_unavailable';
|
|
768
819
|
}
|
|
820
|
+
const authNote = authStatus === 'connected'
|
|
821
|
+
? "Stored Google refresh credentials are present, but server.capabilities does not live-verify them with Google. Refresh tokens can expire or be revoked; a Google tool call may still require reconnection."
|
|
822
|
+
: authStatus === 'not_connected'
|
|
823
|
+
? 'No stored Google refresh credential is available. Reconnect this server in your MCP client to sign in with Google.'
|
|
824
|
+
: 'Local Google credential state could not be read. Provider authorization was not checked.';
|
|
769
825
|
const capabilities = {
|
|
770
826
|
server: SERVER_NAME,
|
|
771
827
|
version: SERVER_VERSION,
|
|
772
828
|
access_mode: accessMode,
|
|
773
829
|
auth_status: authStatus,
|
|
830
|
+
auth_status_basis: authStatusBasis,
|
|
831
|
+
provider_auth_live_verified: false as const,
|
|
832
|
+
auth_note: authNote,
|
|
774
833
|
tools: getToolCatalogForAccessMode(TOOL_CATALOG, accessMode),
|
|
775
834
|
provider_limits: {
|
|
776
835
|
generative_ai_performance_report: {
|
|
@@ -778,7 +837,9 @@ class GscMcpRuntime {
|
|
|
778
837
|
note: GENERATIVE_AI_REPORT_API_NOTE,
|
|
779
838
|
},
|
|
780
839
|
},
|
|
781
|
-
hint:
|
|
840
|
+
hint: authStatus === 'connected'
|
|
841
|
+
? 'Stored Google credentials are configured. If a Google tool later reports revoked access, reconnect this server in your MCP client.'
|
|
842
|
+
: 'Reconnect this server in your MCP client to sign in with Google.',
|
|
782
843
|
};
|
|
783
844
|
return toolResponse(JSON.stringify(capabilities, null, 2), capabilities);
|
|
784
845
|
},
|
|
@@ -1413,12 +1474,12 @@ class GscMcpRuntime {
|
|
|
1413
1474
|
this.server.registerTool(
|
|
1414
1475
|
'sites.delete',
|
|
1415
1476
|
{
|
|
1416
|
-
title: '
|
|
1417
|
-
description:
|
|
1477
|
+
title: 'Remove Search Console property from account',
|
|
1478
|
+
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.",
|
|
1418
1479
|
inputSchema: {
|
|
1419
1480
|
site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
|
|
1420
1481
|
},
|
|
1421
|
-
outputSchema:
|
|
1482
|
+
outputSchema: SITE_DELETE_OUTPUT_SCHEMA,
|
|
1422
1483
|
annotations: WRITE_TOOL_ANNOTATIONS['sites.delete'],
|
|
1423
1484
|
},
|
|
1424
1485
|
async ({ site_url }) => {
|
|
@@ -1427,8 +1488,14 @@ class GscMcpRuntime {
|
|
|
1427
1488
|
if (rateLimitError) return rateLimitError;
|
|
1428
1489
|
const accessToken = await this.getAccessToken(googleId);
|
|
1429
1490
|
await deleteSite(accessToken, site_url);
|
|
1430
|
-
const message = `Successfully
|
|
1431
|
-
|
|
1491
|
+
const message = `Successfully removed site property from the connected account's Search Console site set: ${site_url}`;
|
|
1492
|
+
const payload = {
|
|
1493
|
+
message,
|
|
1494
|
+
removed_from_connected_account_site_set: true as const,
|
|
1495
|
+
website_content_deleted: false as const,
|
|
1496
|
+
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.",
|
|
1497
|
+
};
|
|
1498
|
+
return toolResponse(`${message}\n\n${payload.provider_scope_note}`, payload);
|
|
1432
1499
|
},
|
|
1433
1500
|
);
|
|
1434
1501
|
|
|
@@ -1667,6 +1734,49 @@ class GscMcpRuntime {
|
|
|
1667
1734
|
);
|
|
1668
1735
|
|
|
1669
1736
|
if (accessMode === 'readwrite') {
|
|
1737
|
+
this.server.registerTool(
|
|
1738
|
+
'indexing.status',
|
|
1739
|
+
{
|
|
1740
|
+
title: 'Get Indexing API notification status',
|
|
1741
|
+
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.",
|
|
1742
|
+
inputSchema: {
|
|
1743
|
+
site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA.describe(`${SEARCH_CONSOLE_PROPERTY_DESCRIPTION} The connected Google account must be an owner of this property.`),
|
|
1744
|
+
url: z.string().superRefine((value, ctx) => {
|
|
1745
|
+
try {
|
|
1746
|
+
assertIndexingRequestUrl(value);
|
|
1747
|
+
} catch (error) {
|
|
1748
|
+
ctx.addIssue({
|
|
1749
|
+
code: 'custom',
|
|
1750
|
+
message: (error as Error).message,
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
}).describe('A fully qualified HTTP/HTTPS URL previously submitted successfully through the Google Indexing API. It must fall within site_url.'),
|
|
1754
|
+
},
|
|
1755
|
+
outputSchema: INDEXING_STATUS_OUTPUT_SCHEMA,
|
|
1756
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
1757
|
+
},
|
|
1758
|
+
async ({ site_url, url }) => {
|
|
1759
|
+
const googleId = this.requireGoogleId();
|
|
1760
|
+
const rateLimitError = await this.rateLimitError(googleId, 'indexing.status');
|
|
1761
|
+
if (rateLimitError) return rateLimitError;
|
|
1762
|
+
const accessToken = await this.getAccessToken(googleId);
|
|
1763
|
+
const sites = await listSites(accessToken);
|
|
1764
|
+
assertIndexingUrlAuthorized(url, site_url, sites);
|
|
1765
|
+
const notificationMetadata = await getIndexingNotificationMetadata(
|
|
1766
|
+
accessToken,
|
|
1767
|
+
url,
|
|
1768
|
+
);
|
|
1769
|
+
const payload = {
|
|
1770
|
+
notification_metadata: notificationMetadata,
|
|
1771
|
+
notification_receipt_only: true as const,
|
|
1772
|
+
index_coverage_report: false as const,
|
|
1773
|
+
indexing_or_removal_completion_proven: false as const,
|
|
1774
|
+
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.",
|
|
1775
|
+
};
|
|
1776
|
+
return toolResponse(JSON.stringify(payload, null, 2), payload);
|
|
1777
|
+
},
|
|
1778
|
+
);
|
|
1779
|
+
|
|
1670
1780
|
this.server.registerTool(
|
|
1671
1781
|
'indexing.request',
|
|
1672
1782
|
{
|
package/src/tool-rate-limit.ts
CHANGED
|
@@ -67,6 +67,15 @@ export const TOOL_RATE_LIMIT_POLICIES = {
|
|
|
67
67
|
userLimit: 2,
|
|
68
68
|
projectLimit: 200,
|
|
69
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
|
+
},
|
|
70
79
|
weekly_digest: {
|
|
71
80
|
category: 'weekly-digest',
|
|
72
81
|
tools: ['reports.weekly_digest'],
|
package/wrangler.example.jsonc
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
"compatibility_date": "2026-05-19",
|
|
9
9
|
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
|
10
10
|
"observability": { "enabled": true },
|
|
11
|
+
"secrets": {
|
|
12
|
+
"required": ["GOOGLE_CLIENT_SECRET", "TOKEN_ENCRYPTION_KEY"]
|
|
13
|
+
},
|
|
11
14
|
"vars": {
|
|
12
15
|
"GSC_ACCESS_MODE": "readwrite"
|
|
13
16
|
},
|