@digestseo/mcp-gsc 0.4.24 → 0.4.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/digest.ts +14 -8
- package/src/google.ts +20 -6
- package/src/index.ts +13 -4
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.25] - 2026-09-23
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Access-mode onboarding and Official MCP Registry metadata now identify all six read-write mutation tools after `indexing.remove` became part of the public tool surface.
|
|
14
|
+
- `analytics.query` now accepts provider-valid Discover and Google News rows with no `position` field instead of failing MCP structured-output validation; Search Console does not record average position for those surfaces.
|
|
15
|
+
|
|
10
16
|
## [0.4.24] - 2026-09-23
|
|
11
17
|
|
|
12
18
|
### Added
|
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ By default (`GSC_ACCESS_MODE=readwrite`), this server exposes 23 tools. Read-onl
|
|
|
79
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
|
-
| **`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`. |
|
|
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, so returned rows can omit `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`. |
|
|
83
83
|
| **`insights.page_queries`** / **`insights.query_pages`** | Read | Drill from one exact page to its Search Console queries, or from one exact query to the pages Google surfaced for it, with `row_limit` / `start_row` pagination. These query-centric helpers support web, image, video, and the Search results News tab; Discover and the Google News report are excluded because they do not expose query data. |
|
|
84
84
|
| **`urls.inspect`** | Read | Google's URL Inspection report for the version of one page currently known in Google's index. It is not a live URL test; mobile-usability output is deprecated. |
|
|
85
85
|
| **`urls.inspect_many`** | Read | Inspect Google's indexed versions of up to 10 URLs with bounded concurrency of 3 in one call while preserving input-order results. Each URL still consumes one Google URL Inspection request and one unit of the server's shared inspection safety budget; this does not run live URL tests. |
|
package/package.json
CHANGED
package/src/digest.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
hasRecordedPosition,
|
|
3
|
+
querySearchAnalytics,
|
|
4
|
+
type SearchAnalyticsRow,
|
|
5
|
+
} from './google';
|
|
2
6
|
import type { GoogleAccessTokenProvider } from './access-token-lifecycle';
|
|
3
7
|
import { getSearchConsoleCalendarDate } from './date-validation';
|
|
4
8
|
|
|
@@ -180,13 +184,15 @@ function totalsFromRow(row: SearchAnalyticsRow | undefined): SiteTotals {
|
|
|
180
184
|
}
|
|
181
185
|
|
|
182
186
|
function rowsToQueries(rows: SearchAnalyticsRow[]): QueryRow[] {
|
|
183
|
-
return rows
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
return rows
|
|
188
|
+
.filter(hasRecordedPosition)
|
|
189
|
+
.map((r) => ({
|
|
190
|
+
query: r.keys?.[0] ?? '',
|
|
191
|
+
clicks: r.clicks,
|
|
192
|
+
impressions: r.impressions,
|
|
193
|
+
ctr: r.ctr,
|
|
194
|
+
position: r.position,
|
|
195
|
+
}));
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
function rowsToPages(rows: SearchAnalyticsRow[]): PageRow[] {
|
package/src/google.ts
CHANGED
|
@@ -399,7 +399,20 @@ export interface SearchAnalyticsRow {
|
|
|
399
399
|
clicks: number;
|
|
400
400
|
impressions: number;
|
|
401
401
|
ctr: number;
|
|
402
|
-
|
|
402
|
+
/**
|
|
403
|
+
* Average position when Google records it. Search Console does not record
|
|
404
|
+
* position for Discover or the Google News app/news.google.com, so those
|
|
405
|
+
* Search Analytics rows can legitimately omit this field.
|
|
406
|
+
*/
|
|
407
|
+
position?: number;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export type PositionedSearchAnalyticsRow = SearchAnalyticsRow & { position: number };
|
|
411
|
+
|
|
412
|
+
export function hasRecordedPosition(
|
|
413
|
+
row: SearchAnalyticsRow,
|
|
414
|
+
): row is PositionedSearchAnalyticsRow {
|
|
415
|
+
return row.position !== undefined;
|
|
403
416
|
}
|
|
404
417
|
|
|
405
418
|
export interface SearchAnalyticsResponseMetadata {
|
|
@@ -812,6 +825,7 @@ export function processQuickWins(
|
|
|
812
825
|
maxPosition: number,
|
|
813
826
|
): QuickWinResult[] {
|
|
814
827
|
return rows
|
|
828
|
+
.filter(hasRecordedPosition)
|
|
815
829
|
.filter(
|
|
816
830
|
(row) =>
|
|
817
831
|
(row.keys?.length ?? 0) >= 2 &&
|
|
@@ -860,7 +874,7 @@ export function processCannibalization(
|
|
|
860
874
|
): CannibalizationResult[] {
|
|
861
875
|
const queryGroups = new Map<string, Array<{ page: string; clicks: number; impressions: number; ctr: number; position: number }>>();
|
|
862
876
|
for (const row of rows) {
|
|
863
|
-
if ((row.keys?.length ?? 0) < 2) continue;
|
|
877
|
+
if ((row.keys?.length ?? 0) < 2 || row.position === undefined) continue;
|
|
864
878
|
const query = row.keys![0];
|
|
865
879
|
const page = row.keys![1];
|
|
866
880
|
if (!queryGroups.has(query)) {
|
|
@@ -1010,7 +1024,7 @@ export function processContentDecay(
|
|
|
1010
1024
|
): DecayPageResult[] {
|
|
1011
1025
|
const recentMap = new Map<string, { clicks: number; impressions: number; ctr: number; position: number }>();
|
|
1012
1026
|
for (const row of recentRows) {
|
|
1013
|
-
if ((row.keys?.length ?? 0) < 1) continue;
|
|
1027
|
+
if ((row.keys?.length ?? 0) < 1 || row.position === undefined) continue;
|
|
1014
1028
|
recentMap.set(row.keys![0], {
|
|
1015
1029
|
clicks: row.clicks,
|
|
1016
1030
|
impressions: row.impressions,
|
|
@@ -1022,7 +1036,7 @@ export function processContentDecay(
|
|
|
1022
1036
|
const decayCandidates: DecayPageResult[] = [];
|
|
1023
1037
|
|
|
1024
1038
|
for (const row of previousRows) {
|
|
1025
|
-
if ((row.keys?.length ?? 0) < 1) continue;
|
|
1039
|
+
if ((row.keys?.length ?? 0) < 1 || row.position === undefined) continue;
|
|
1026
1040
|
const page = row.keys![0];
|
|
1027
1041
|
const prevClicks = row.clicks;
|
|
1028
1042
|
const prevImps = row.impressions;
|
|
@@ -1688,7 +1702,7 @@ export function processPerformanceComparison(
|
|
|
1688
1702
|
): PerformanceComparisonRow[] {
|
|
1689
1703
|
const mapB = new Map<string, { clicks: number; impressions: number; ctr: number; position: number }>();
|
|
1690
1704
|
for (const row of rowsB) {
|
|
1691
|
-
if ((row.keys?.length ?? 0) < 1) continue;
|
|
1705
|
+
if ((row.keys?.length ?? 0) < 1 || row.position === undefined) continue;
|
|
1692
1706
|
mapB.set(row.keys![0], {
|
|
1693
1707
|
clicks: row.clicks,
|
|
1694
1708
|
impressions: row.impressions,
|
|
@@ -1700,7 +1714,7 @@ export function processPerformanceComparison(
|
|
|
1700
1714
|
const comparison: PerformanceComparisonRow[] = [];
|
|
1701
1715
|
|
|
1702
1716
|
for (const row of rowsA) {
|
|
1703
|
-
if ((row.keys?.length ?? 0) < 1) continue;
|
|
1717
|
+
if ((row.keys?.length ?? 0) < 1 || row.position === undefined) continue;
|
|
1704
1718
|
const key = row.keys![0];
|
|
1705
1719
|
const clicksA = row.clicks;
|
|
1706
1720
|
const impsA = row.impressions;
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
submitSitemap,
|
|
25
25
|
deleteSitemap,
|
|
26
26
|
getSitemap,
|
|
27
|
+
hasRecordedPosition,
|
|
27
28
|
processQuickWins,
|
|
28
29
|
processCannibalization,
|
|
29
30
|
processContentDecay,
|
|
@@ -134,7 +135,15 @@ const SEARCH_ROW_OUTPUT_SCHEMA = {
|
|
|
134
135
|
.describe(
|
|
135
136
|
'Dimension values returned by Google. Aggregate queries with dimensions: [] may legitimately omit this field.',
|
|
136
137
|
),
|
|
137
|
-
|
|
138
|
+
clicks: z.number(),
|
|
139
|
+
impressions: z.number(),
|
|
140
|
+
ctr: z.number(),
|
|
141
|
+
position: z
|
|
142
|
+
.number()
|
|
143
|
+
.optional()
|
|
144
|
+
.describe(
|
|
145
|
+
'Average position when Google records it. Omitted for Discover and Google News, where Search Console does not record position.',
|
|
146
|
+
),
|
|
138
147
|
};
|
|
139
148
|
|
|
140
149
|
const SITE_OUTPUT_SCHEMA = {
|
|
@@ -1364,7 +1373,7 @@ class GscMcpRuntime {
|
|
|
1364
1373
|
],
|
|
1365
1374
|
}, { maxRows: sourceRowLimit });
|
|
1366
1375
|
const rows = source.rows
|
|
1367
|
-
.filter((row) => (row.keys?.length ?? 0) > 0)
|
|
1376
|
+
.filter((row) => (row.keys?.length ?? 0) > 0 && hasRecordedPosition(row))
|
|
1368
1377
|
.map((row) => ({
|
|
1369
1378
|
query: row.keys![0],
|
|
1370
1379
|
clicks: row.clicks,
|
|
@@ -1448,7 +1457,7 @@ class GscMcpRuntime {
|
|
|
1448
1457
|
],
|
|
1449
1458
|
}, { maxRows: sourceRowLimit });
|
|
1450
1459
|
const rows = source.rows
|
|
1451
|
-
.filter((row) => (row.keys?.length ?? 0) > 0)
|
|
1460
|
+
.filter((row) => (row.keys?.length ?? 0) > 0 && hasRecordedPosition(row))
|
|
1452
1461
|
.map((row) => ({
|
|
1453
1462
|
page: row.keys![0],
|
|
1454
1463
|
clicks: row.clicks,
|
|
@@ -1940,7 +1949,7 @@ class GscMcpRuntime {
|
|
|
1940
1949
|
});
|
|
1941
1950
|
|
|
1942
1951
|
const pages = response.rows
|
|
1943
|
-
.filter((row) => (row.keys?.length ?? 0) > 0)
|
|
1952
|
+
.filter((row) => (row.keys?.length ?? 0) > 0 && hasRecordedPosition(row))
|
|
1944
1953
|
.map((row) => ({
|
|
1945
1954
|
page: row.keys![0],
|
|
1946
1955
|
clicks: row.clicks,
|