@digestseo/mcp-gsc 0.4.30 → 0.4.31

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,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.31] - 2026-09-24
11
+
12
+ ### Added
13
+ - Gemini CLI can now install this repository as a native extension, bundling the verified local Streamable HTTP MCP connection with the existing `gsc-weekly-review`, `gsc-indexing-triage`, and `gsc-search-opportunities` Agent Skills.
14
+
15
+ ### Changed
16
+ - Exact Search Analytics country and device filters now reject malformed provider values before reserving local Search Analytics budget, resolving Google credentials, or calling Search Console; substring and RE2 regex operators remain unrestricted.
17
+ - `sites.list` and `sites.get` now expose explicit `verification_state` and `owner` fields alongside Google's raw `permissionLevel`, so agents can distinguish unverified properties and owner-only Indexing API prerequisites before choosing a property workflow.
18
+
10
19
  ## [0.4.30] - 2026-09-23
11
20
 
12
21
  ### Changed
@@ -198,6 +207,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
198
207
  ## [0.4.2] - 2026-09-21
199
208
 
200
209
  ### Fixed
210
+ - Search Analytics now rejects malformed exact-match country/device filters during MCP input validation, before local rate-limit, credential, or Google API work, while preserving contains and regex filter semantics.
201
211
  - Cannibalization totals and impression shares now expose their query/page-row aggregation scope explicitly so clients do not mistake summed page impressions for a true query-level Search Console total.
202
212
  - Discover Search Analytics calls now reject unsupported query grouping/filtering before reaching Google, and query-centric/comparison tools no longer advertise Discover where their contracts depend on query or average-position data that Discover does not provide.
203
213
  - Quick-win guidance now treats Search Console position as an aggregate average-position metric rather than a literal current rank, and metadata no longer implies CTR is an eligibility filter when the implementation does not filter on CTR.
package/README.md CHANGED
@@ -75,6 +75,14 @@ copilot plugin install mcp-gsc@digestseo
75
75
 
76
76
  The marketplace path makes the plugin discoverable and updateable through Copilot's marketplace workflow. Direct repository install also remains supported with `copilot plugin install AKzar1el/mcp-gsc`. The Copilot plugin bundles the same three Agent Skills plus the local loopback MCP configuration. Start the npm launcher before using the plugin's MCP server; the plugin does not start or deploy mcp-gsc for you.
77
77
 
78
+ **Gemini CLI**
79
+
80
+ ```bash
81
+ gemini extensions install https://github.com/AKzar1el/mcp-gsc --ref main
82
+ ```
83
+
84
+ The Gemini CLI extension bundles the verified local loopback MCP configuration plus the same three Agent Skills: `gsc-weekly-review`, `gsc-indexing-triage`, and `gsc-search-opportunities`. Start the npm launcher before using the extension; the extension does not start or deploy mcp-gsc for you.
85
+
78
86
  **Kiro**
79
87
 
80
88
  [![Add to Kiro](https://kiro.dev/images/add-to-kiro.svg)](https://kiro.dev/launch/mcp/add?name=mcp-gsc&config=%7B%22url%22%3A%22http%3A%2F%2F127.0.0.1%3A8080%2Fmcp%22%2C%22disabled%22%3Afalse%2C%22autoApprove%22%3A%5B%5D%7D)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digestseo/mcp-gsc",
3
- "version": "0.4.30",
3
+ "version": "0.4.31",
4
4
  "private": false,
5
5
  "main": "scripts/glama-start.mjs",
6
6
  "bin": {
package/src/google.ts CHANGED
@@ -314,6 +314,27 @@ export interface DimensionFilter {
314
314
  expression: string;
315
315
  }
316
316
 
317
+ export function assertSearchAnalyticsFilterCompatible(
318
+ filter: DimensionFilter,
319
+ ): void {
320
+ if (filter.operator !== 'equals' && filter.operator !== 'notEquals') return;
321
+
322
+ if (filter.dimension === 'country' && !/^[A-Za-z]{3}$/.test(filter.expression)) {
323
+ throw new Error(
324
+ 'Search Analytics exact country filters must use a three-letter ISO 3166-1 alpha-3 code.',
325
+ );
326
+ }
327
+
328
+ if (
329
+ filter.dimension === 'device' &&
330
+ !['DESKTOP', 'MOBILE', 'TABLET'].includes(filter.expression.toUpperCase())
331
+ ) {
332
+ throw new Error(
333
+ 'Search Analytics exact device filters must use DESKTOP, MOBILE, or TABLET.',
334
+ );
335
+ }
336
+ }
337
+
317
338
  export interface DimensionFilterGroup {
318
339
  groupType: 'and';
319
340
  filters: DimensionFilter[];
@@ -369,6 +390,7 @@ export function assertSearchAnalyticsQueryCompatible(body: SearchAnalyticsQuery)
369
390
  if (filter.expression.length > 4096) {
370
391
  throw new Error('Search Analytics filter expressions must be at most 4096 characters.');
371
392
  }
393
+ assertSearchAnalyticsFilterCompatible(filter);
372
394
  }
373
395
 
374
396
  if (body.aggregationType === 'byPage' && hasPageGroupingOrFilter) {
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  requestIndexingRemoval,
33
33
  getIndexingNotificationMetadata,
34
34
  processPerformanceComparison,
35
+ assertSearchAnalyticsFilterCompatible,
35
36
  type PaginatedSearchAnalyticsResult,
36
37
  } from './google';
37
38
  import {
@@ -149,35 +150,57 @@ const SEARCH_ROW_OUTPUT_SCHEMA = {
149
150
  ),
150
151
  };
151
152
 
152
- const SEARCH_ANALYTICS_FILTER_SCHEMA = z.object({
153
- dimension: z.enum([
154
- 'query',
155
- 'page',
156
- 'country',
157
- 'device',
158
- 'searchAppearance',
159
- ]),
160
- operator: z
161
- .enum([
162
- 'equals',
163
- 'notEquals',
164
- 'contains',
165
- 'notContains',
166
- 'includingRegex',
167
- 'excludingRegex',
168
- ])
169
- .default('equals'),
170
- expression: z.string().max(4096),
171
- });
153
+ const SEARCH_ANALYTICS_FILTER_SCHEMA = z
154
+ .object({
155
+ dimension: z.enum([
156
+ 'query',
157
+ 'page',
158
+ 'country',
159
+ 'device',
160
+ 'searchAppearance',
161
+ ]),
162
+ operator: z
163
+ .enum([
164
+ 'equals',
165
+ 'notEquals',
166
+ 'contains',
167
+ 'notContains',
168
+ 'includingRegex',
169
+ 'excludingRegex',
170
+ ])
171
+ .default('equals'),
172
+ expression: z.string().max(4096),
173
+ })
174
+ .superRefine((filter, ctx) => {
175
+ try {
176
+ assertSearchAnalyticsFilterCompatible(filter);
177
+ } catch (error) {
178
+ ctx.addIssue({
179
+ code: 'custom',
180
+ path: ['expression'],
181
+ message: (error as Error).message,
182
+ });
183
+ }
184
+ });
172
185
 
173
186
  const SEARCH_ANALYTICS_FILTER_GROUP_SCHEMA = z.object({
174
187
  groupType: z.literal('and').default('and'),
175
188
  filters: z.array(SEARCH_ANALYTICS_FILTER_SCHEMA),
176
189
  });
177
190
 
191
+ const SITE_VERIFICATION_STATE_SCHEMA = z
192
+ .enum(['verified', 'unverified', 'unknown'])
193
+ .describe(
194
+ "Derived from Google's permissionLevel. siteOwner/siteFullUser/siteRestrictedUser are verified, siteUnverifiedUser is unverified, and any future undocumented permission level is reported as unknown.",
195
+ );
196
+
178
197
  const SITE_OUTPUT_SCHEMA = {
179
198
  siteUrl: z.string(),
180
199
  permissionLevel: z.string(),
200
+ verification_state: SITE_VERIFICATION_STATE_SCHEMA,
201
+ owner: z.boolean().describe(
202
+ "True only when Google returns permissionLevel 'siteOwner'. Restricted Indexing API workflows require owner-level Search Console access.",
203
+ ),
181
204
  };
182
205
 
183
206
  const LISTED_SITE_OUTPUT_SCHEMA = {
@@ -190,6 +213,26 @@ const LISTED_SITE_OUTPUT_SCHEMA = {
190
213
  const PLATFORM_PROPERTY_API_NOTE =
191
214
  "Google Search Console now has platform properties for social/video accounts in its UI, but the current Search Console API documentation still defines siteUrl using URL-prefix and sc-domain website-property forms. mcp-gsc preserves any other identifier returned by sites.list but will not pass an undocumented property identifier to API tools until Google publishes that contract.";
192
215
 
216
+ function sitePermissionState(permissionLevel: string): {
217
+ verification_state: 'verified' | 'unverified' | 'unknown';
218
+ owner: boolean;
219
+ } {
220
+ if (permissionLevel === 'siteUnverifiedUser') {
221
+ return { verification_state: 'unverified', owner: false };
222
+ }
223
+ if (
224
+ permissionLevel === 'siteOwner' ||
225
+ permissionLevel === 'siteFullUser' ||
226
+ permissionLevel === 'siteRestrictedUser'
227
+ ) {
228
+ return {
229
+ verification_state: 'verified',
230
+ owner: permissionLevel === 'siteOwner',
231
+ };
232
+ }
233
+ return { verification_state: 'unknown', owner: false };
234
+ }
235
+
193
236
  const SITEMAP_PROVIDER_NOTE =
194
237
  "Google's lastSubmitted is the time the sitemap was submitted to Search Console; it is not the sitemap file's generation, deployment, or last-modified time. lastDownloaded is the time Google last downloaded the sitemap; it is not a page crawl or indexing timestamp.";
195
238
 
@@ -914,7 +957,7 @@ class GscMcpRuntime {
914
957
  {
915
958
  title: 'List Search Console properties',
916
959
  description:
917
- "List the Google Search Console properties (sites) the connected Google account has access to. Each entry preserves Google's siteUrl and permissionLevel and adds api_identifier_kind plus mcp_site_url_accepted. Google now exposes platform properties for social/video accounts in the Search Console UI, but its current API documentation still defines siteUrl using URL-prefix and sc-domain website-property forms; identifiers outside those documented forms are preserved here but marked unsupported rather than guessed. Call this when the user asks 'what sites do I have?' or 'what properties are connected?', or before another tool that requires site_url.",
960
+ "List the Google Search Console properties (sites) the connected Google account has access to. Each entry preserves Google's siteUrl and permissionLevel, derives verification_state plus owner from Google's documented permission levels, and adds api_identifier_kind plus mcp_site_url_accepted. Treat verification_state='unverified' as not ready for normal property-scoped Search Console workflows; owner=true is required by this server's restricted Indexing API workflows. Google now exposes platform properties for social/video accounts in the Search Console UI, but its current API documentation still defines siteUrl using URL-prefix and sc-domain website-property forms; identifiers outside those documented forms are preserved here but marked unsupported rather than guessed. Call this when the user asks 'what sites do I have?' or 'what properties are connected?', or before another tool that requires site_url.",
918
961
  inputSchema: {},
919
962
  outputSchema: SITES_OUTPUT_SCHEMA,
920
963
  annotations: READ_ONLY_ANNOTATIONS,
@@ -927,6 +970,7 @@ class GscMcpRuntime {
927
970
  const mcpSiteUrlAccepted = apiIdentifierKind !== 'undocumented';
928
971
  return {
929
972
  ...site,
973
+ ...sitePermissionState(site.permissionLevel),
930
974
  api_identifier_kind: apiIdentifierKind,
931
975
  mcp_site_url_accepted: mcpSiteUrlAccepted,
932
976
  ...(!mcpSiteUrlAccepted
@@ -943,7 +987,7 @@ class GscMcpRuntime {
943
987
  {
944
988
  title: 'Get Search Console property',
945
989
  description:
946
- "Retrieve one exact Google Search Console property and the connected account's permission level for it. Use this when the user has already named a property and you need to confirm that exact property or its access level without listing every property first.",
990
+ "Retrieve one exact Google Search Console property and its connected-account access state. The response preserves Google's permissionLevel and derives verification_state plus owner so callers can distinguish an unverified property and recognize owner-only Indexing API prerequisites without listing every property first.",
947
991
  inputSchema: {
948
992
  site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
949
993
  },
@@ -954,7 +998,13 @@ class GscMcpRuntime {
954
998
  const googleId = this.requireGoogleId();
955
999
  const accessToken = await this.getAccessToken(googleId);
956
1000
  const site = await getSite(accessToken, site_url);
957
- return toolResponse(JSON.stringify(site, null, 2), { site });
1001
+ const siteWithPermissionState = {
1002
+ ...site,
1003
+ ...sitePermissionState(site.permissionLevel),
1004
+ };
1005
+ return toolResponse(JSON.stringify(siteWithPermissionState, null, 2), {
1006
+ site: siteWithPermissionState,
1007
+ });
958
1008
  },
959
1009
  );
960
1010