@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digestseo/mcp-gsc",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
4
4
  "private": false,
5
5
  "main": "scripts/glama-start.mjs",
6
6
  "bin": {
package/src/digest.ts CHANGED
@@ -1,4 +1,8 @@
1
- import { querySearchAnalytics, type SearchAnalyticsRow } from './google';
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.map((r) => ({
184
- query: r.keys?.[0] ?? '',
185
- clicks: r.clicks,
186
- impressions: r.impressions,
187
- ctr: r.ctr,
188
- position: r.position,
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
- position: number;
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
- ...METRIC_OUTPUT_SCHEMA,
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,