@digestseo/mcp-gsc 0.4.33 → 0.4.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/google.ts +4 -5
- package/src/index.ts +19 -13
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.35] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Search Analytics pagination now treats provider responses shorter than the requested page size as terminal, matching Google's documented contract and avoiding false continuations or unnecessary follow-up requests across direct and higher-level workflows.
|
|
14
|
+
- `insights.page_queries` now rejects exact page URLs outside the supplied Search Console property before reserving local Search Analytics safety budget, resolving Google credentials, or calling the provider, preventing misleading empty cross-property drilldowns.
|
|
15
|
+
|
|
16
|
+
## [0.4.34] - 2026-09-24
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- `analytics.query` now rejects documented-invalid cross-field request combinations before reserving local Search Analytics safety budget or resolving Google credentials, while retaining the lower-level provider compatibility guard for internal callers.
|
|
20
|
+
- `indexing.status`, `indexing.request`, and `indexing.remove` now reject URLs outside the supplied Search Console property before reserving local safety budget, resolving Google credentials, or calling Search Console for owner authorization.
|
|
21
|
+
|
|
10
22
|
## [0.4.33] - 2026-09-24
|
|
11
23
|
|
|
12
24
|
### Changed
|
package/README.md
CHANGED
|
@@ -66,6 +66,8 @@ Codex CLI and the Codex IDE extension share MCP configuration, so registering th
|
|
|
66
66
|
|
|
67
67
|
When this repository is installed as a Cursor plugin, Cursor also auto-discovers three bundled Agent Skills that compose the MCP tools into evidence-safe workflows: `gsc-weekly-review`, `gsc-indexing-triage`, and `gsc-search-opportunities`. The **Add to Cursor** button above installs the MCP connection only; the bundled skills are part of the repository's Cursor plugin package.
|
|
68
68
|
|
|
69
|
+
The button and plugin MCP configuration above are for Cursor running on the same machine as the npm launcher. **Cursor Cloud Agents run in isolated cloud VMs, so they cannot reach `127.0.0.1` on your computer.** For Cloud Agents, deploy your own externally reachable HTTPS Worker and add its `/mcp` URL from the MCP controls at [cursor.com/agents](https://cursor.com/agents); Cursor supports HTTP MCP servers and OAuth there. Do not reuse the loopback URL for a Cloud Agent.
|
|
70
|
+
|
|
69
71
|
**GitHub Copilot CLI**
|
|
70
72
|
|
|
71
73
|
```bash
|
|
@@ -89,7 +91,9 @@ The Gemini CLI extension bundles the verified local loopback MCP configuration p
|
|
|
89
91
|
|
|
90
92
|
Start the npm launcher before using either install button or the Copilot plugin. Complete its Google OAuth flow on first use.
|
|
91
93
|
|
|
92
|
-
**ChatGPT** — ChatGPT cannot connect directly to this loopback `127.0.0.1` endpoint.
|
|
94
|
+
**ChatGPT** — ChatGPT cannot connect directly to this loopback `127.0.0.1` endpoint. For private/local use, create an OpenAI **Secure MCP Tunnel**, then run `tunnel-client` on the same machine as the launcher so it can reach `http://127.0.0.1:8080/mcp`; when creating the ChatGPT app, choose the **Tunnel** connection and select that tunnel. The tunnel setup requires an OpenAI `tunnel_id` and runtime API key; see OpenAI's [Secure MCP Tunnel guide](https://developers.openai.com/api/docs/guides/secure-mcp-tunnels). The tunnel keeps the MCP server private and is intended for private/developer-mode connectivity, not public plugin distribution.
|
|
95
|
+
|
|
96
|
+
Alternatively, deploy your own remote Cloudflare Worker and add its HTTPS `/mcp` URL as a custom MCP app in Developer mode; see [Connect in your AI client](#connect-in-your-ai-client) below.
|
|
93
97
|
|
|
94
98
|
A self-hostable [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **Google Search Console**. Connect it to Claude.ai, Cursor, ChatGPT, or any MCP-compatible client and ask your AI assistant about your site's organic search performance — impressions, clicks, top queries, index status, and sitemap health — straight from your own Google account.
|
|
95
99
|
|
package/package.json
CHANGED
package/src/google.ts
CHANGED
|
@@ -785,10 +785,9 @@ export async function querySearchAnalytics(
|
|
|
785
785
|
|
|
786
786
|
/**
|
|
787
787
|
* Fetches Search Analytics rows in 25,000-row pages for higher-level tools.
|
|
788
|
-
* Google's documented pagination contract
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
* can have more rows even when Search Console's own internal limits apply.
|
|
788
|
+
* Google's documented pagination contract treats a response shorter than the
|
|
789
|
+
* requested rowLimit as exhausted. An exactly full final page still requires
|
|
790
|
+
* a following empty-page probe unless the local safety ceiling is reached.
|
|
792
791
|
*/
|
|
793
792
|
export async function querySearchAnalyticsPaginated(
|
|
794
793
|
accessToken: string,
|
|
@@ -820,7 +819,7 @@ export async function querySearchAnalyticsPaginated(
|
|
|
820
819
|
|
|
821
820
|
const acceptedRows = page.rows.slice(0, requestedRows);
|
|
822
821
|
rows.push(...acceptedRows);
|
|
823
|
-
if (page.rows.length
|
|
822
|
+
if (page.rows.length < requestedRows) {
|
|
824
823
|
return { rows, pagesFetched, localLimitReached: false };
|
|
825
824
|
}
|
|
826
825
|
if (rows.length >= maximumRows || page.rows.length > requestedRows) {
|
package/src/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
getIndexingNotificationMetadata,
|
|
35
35
|
processPerformanceComparison,
|
|
36
36
|
assertSearchAnalyticsFilterCompatible,
|
|
37
|
+
assertSearchAnalyticsQueryCompatible,
|
|
37
38
|
type PaginatedSearchAnalyticsResult,
|
|
38
39
|
} from './google';
|
|
39
40
|
import {
|
|
@@ -1387,12 +1388,8 @@ class GscMcpRuntime {
|
|
|
1387
1388
|
dimension_filter_groups,
|
|
1388
1389
|
}) => {
|
|
1389
1390
|
assertDateRange(start_date, end_date);
|
|
1390
|
-
const googleId = this.requireGoogleId();
|
|
1391
|
-
const rateLimitError = await this.rateLimitError(googleId, 'analytics.query');
|
|
1392
|
-
if (rateLimitError) return rateLimitError;
|
|
1393
|
-
const accessToken = await this.getAccessToken(googleId);
|
|
1394
1391
|
const sourceRowLimit = Math.min(row_limit, MAX_DIRECT_SOURCE_ROWS);
|
|
1395
|
-
const
|
|
1392
|
+
const query = {
|
|
1396
1393
|
startDate: start_date,
|
|
1397
1394
|
endDate: end_date,
|
|
1398
1395
|
dimensions,
|
|
@@ -1404,12 +1401,17 @@ class GscMcpRuntime {
|
|
|
1404
1401
|
...(dimension_filter_groups !== undefined
|
|
1405
1402
|
? { dimensionFilterGroups: dimension_filter_groups }
|
|
1406
1403
|
: {}),
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1404
|
+
};
|
|
1405
|
+
assertSearchAnalyticsQueryCompatible(query);
|
|
1406
|
+
const googleId = this.requireGoogleId();
|
|
1407
|
+
const rateLimitError = await this.rateLimitError(googleId, 'analytics.query');
|
|
1408
|
+
if (rateLimitError) return rateLimitError;
|
|
1409
|
+
const accessToken = await this.getAccessToken(googleId);
|
|
1410
|
+
const response = await querySearchAnalytics(accessToken, site_url, query);
|
|
1411
|
+
// Google documents a short page as terminal. An exactly full page can
|
|
1412
|
+
// still have a following page, so expose continuation only then.
|
|
1411
1413
|
const sourceMayHaveMore =
|
|
1412
|
-
dimensions.length > 0 && response.rows.length
|
|
1414
|
+
dimensions.length > 0 && response.rows.length === sourceRowLimit;
|
|
1413
1415
|
const bounded = windowSourceRows(
|
|
1414
1416
|
response.rows,
|
|
1415
1417
|
start_row,
|
|
@@ -1460,10 +1462,10 @@ class GscMcpRuntime {
|
|
|
1460
1462
|
'insights.page_queries',
|
|
1461
1463
|
{
|
|
1462
1464
|
title: 'Find queries for a page',
|
|
1463
|
-
description: 'For one exact page URL
|
|
1465
|
+
description: 'For one exact page URL within the supplied Search Console property, return the queries that produced impressions for it over a date range. This wraps an exact page dimension filter so agents do not need to construct analytics.query filter groups manually. Exact page matching is case-sensitive in Search Console. Use row_limit and start_row to page through bounded results; Search Console can still omit anonymized queries. Google Discover and Google News are intentionally unavailable because those reports do not expose query data.',
|
|
1464
1466
|
inputSchema: {
|
|
1465
1467
|
site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
|
|
1466
|
-
page_url: z.string().url().describe('Exact fully-qualified page URL to filter on, e.g. https://example.com/guides/seo/. Search Console exact page filters are case-sensitive.'),
|
|
1468
|
+
page_url: z.string().url().describe('Exact fully-qualified page URL to filter on, e.g. https://example.com/guides/seo/. The URL must belong to site_url; Search Console exact page filters are case-sensitive.'),
|
|
1467
1469
|
start_date: SEARCH_CONSOLE_DATE_SCHEMA.describe(
|
|
1468
1470
|
searchConsoleDateDescription('Start date (inclusive) in YYYY-MM-DD format.'),
|
|
1469
1471
|
),
|
|
@@ -1495,6 +1497,7 @@ class GscMcpRuntime {
|
|
|
1495
1497
|
},
|
|
1496
1498
|
async ({ site_url, page_url, start_date, end_date, search_type, row_limit = 100, start_row = 0 }) => {
|
|
1497
1499
|
assertDateRange(start_date, end_date);
|
|
1500
|
+
assertUrlWithinSearchConsoleProperty(page_url, site_url, 'page_url');
|
|
1498
1501
|
const googleId = this.requireGoogleId();
|
|
1499
1502
|
const rateLimitError = await this.rateLimitError(googleId, 'insights.page_queries');
|
|
1500
1503
|
if (rateLimitError) return rateLimitError;
|
|
@@ -1957,6 +1960,7 @@ class GscMcpRuntime {
|
|
|
1957
1960
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
1958
1961
|
},
|
|
1959
1962
|
async ({ site_url, url }) => {
|
|
1963
|
+
assertUrlWithinSearchConsoleProperty(url, site_url);
|
|
1960
1964
|
const googleId = this.requireGoogleId();
|
|
1961
1965
|
const rateLimitError = await this.rateLimitError(googleId, 'indexing.status');
|
|
1962
1966
|
if (rateLimitError) return rateLimitError;
|
|
@@ -2000,6 +2004,7 @@ class GscMcpRuntime {
|
|
|
2000
2004
|
annotations: WRITE_TOOL_ANNOTATIONS['indexing.request'],
|
|
2001
2005
|
},
|
|
2002
2006
|
async ({ site_url, url }) => {
|
|
2007
|
+
assertUrlWithinSearchConsoleProperty(url, site_url);
|
|
2003
2008
|
const googleId = this.requireGoogleId();
|
|
2004
2009
|
const rateLimitError = await this.rateLimitError(googleId, 'indexing.request');
|
|
2005
2010
|
if (rateLimitError) return rateLimitError;
|
|
@@ -2041,6 +2046,7 @@ class GscMcpRuntime {
|
|
|
2041
2046
|
annotations: WRITE_TOOL_ANNOTATIONS['indexing.remove'],
|
|
2042
2047
|
},
|
|
2043
2048
|
async ({ site_url, url }) => {
|
|
2049
|
+
assertUrlWithinSearchConsoleProperty(url, site_url);
|
|
2044
2050
|
const googleId = this.requireGoogleId();
|
|
2045
2051
|
const rateLimitError = await this.rateLimitError(googleId, 'indexing.remove');
|
|
2046
2052
|
if (rateLimitError) return rateLimitError;
|
|
@@ -2118,7 +2124,7 @@ class GscMcpRuntime {
|
|
|
2118
2124
|
pages,
|
|
2119
2125
|
start_row,
|
|
2120
2126
|
row_limit,
|
|
2121
|
-
response.rows.length
|
|
2127
|
+
response.rows.length === sourceRowLimit,
|
|
2122
2128
|
start_row + sourceRowLimit,
|
|
2123
2129
|
);
|
|
2124
2130
|
|