@digestseo/mcp-gsc 0.4.31 → 0.4.32

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,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.32] - 2026-09-24
11
+
12
+ ### Added
13
+ - `urls.inspect` and successful `urls.inspect_many` items now include a conservative, schema-discoverable `index_status_summary` derived from Google's documented index-status verdict and key crawl/indexing fields while preserving the complete raw provider response.
14
+
15
+ ### Changed
16
+ - Successful `sitemaps.submit` and `sitemaps.delete` results now expose explicit Search Console effect boundaries so agents do not mistake submission for completed processing/indexing or Search Console removal for deletion of the hosted sitemap file/deindexing.
17
+
10
18
  ## [0.4.31] - 2026-09-24
11
19
 
12
20
  ### Added
package/README.md CHANGED
@@ -78,7 +78,7 @@ The marketplace path makes the plugin discoverable and updateable through Copilo
78
78
  **Gemini CLI**
79
79
 
80
80
  ```bash
81
- gemini extensions install https://github.com/AKzar1el/mcp-gsc --ref main
81
+ gemini extensions install https://github.com/AKzar1el/mcp-gsc
82
82
  ```
83
83
 
84
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digestseo/mcp-gsc",
3
- "version": "0.4.31",
3
+ "version": "0.4.32",
4
4
  "private": false,
5
5
  "main": "scripts/glama-start.mjs",
6
6
  "bin": {
package/src/google.ts CHANGED
@@ -548,6 +548,70 @@ export async function inspectUrl(
548
548
  return data.inspectionResult ?? null;
549
549
  }
550
550
 
551
+ export type UrlInspectionIndexedState = 'indexed' | 'not_indexed' | 'unknown';
552
+
553
+ export interface UrlInspectionIndexStatusSummary {
554
+ indexed_state: UrlInspectionIndexedState;
555
+ verdict?: string;
556
+ coverage_state?: string;
557
+ robots_txt_state?: string;
558
+ indexing_state?: string;
559
+ last_crawl_time?: string;
560
+ page_fetch_state?: string;
561
+ google_canonical?: string;
562
+ user_canonical?: string;
563
+ crawled_as?: string;
564
+ }
565
+
566
+ function asUnknownRecord(value: unknown): Record<string, unknown> | undefined {
567
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
568
+ ? (value as Record<string, unknown>)
569
+ : undefined;
570
+ }
571
+
572
+ function optionalStringField(
573
+ record: Record<string, unknown> | undefined,
574
+ key: string,
575
+ ): string | undefined {
576
+ const value = record?.[key];
577
+ return typeof value === 'string' ? value : undefined;
578
+ }
579
+
580
+ export function summarizeUrlInspectionIndexStatus(
581
+ inspectionResult: unknown,
582
+ ): UrlInspectionIndexStatusSummary {
583
+ const result = asUnknownRecord(inspectionResult);
584
+ const indexStatus = asUnknownRecord(result?.indexStatusResult);
585
+ const verdict = optionalStringField(indexStatus, 'verdict');
586
+ const coverageState = optionalStringField(indexStatus, 'coverageState');
587
+ const robotsTxtState = optionalStringField(indexStatus, 'robotsTxtState');
588
+ const indexingState = optionalStringField(indexStatus, 'indexingState');
589
+ const lastCrawlTime = optionalStringField(indexStatus, 'lastCrawlTime');
590
+ const pageFetchState = optionalStringField(indexStatus, 'pageFetchState');
591
+ const googleCanonical = optionalStringField(indexStatus, 'googleCanonical');
592
+ const userCanonical = optionalStringField(indexStatus, 'userCanonical');
593
+ const crawledAs = optionalStringField(indexStatus, 'crawledAs');
594
+ const indexedState: UrlInspectionIndexedState =
595
+ verdict === 'PASS'
596
+ ? 'indexed'
597
+ : verdict === 'FAIL' || verdict === 'NEUTRAL'
598
+ ? 'not_indexed'
599
+ : 'unknown';
600
+
601
+ return {
602
+ indexed_state: indexedState,
603
+ ...(verdict !== undefined ? { verdict } : {}),
604
+ ...(coverageState !== undefined ? { coverage_state: coverageState } : {}),
605
+ ...(robotsTxtState !== undefined ? { robots_txt_state: robotsTxtState } : {}),
606
+ ...(indexingState !== undefined ? { indexing_state: indexingState } : {}),
607
+ ...(lastCrawlTime !== undefined ? { last_crawl_time: lastCrawlTime } : {}),
608
+ ...(pageFetchState !== undefined ? { page_fetch_state: pageFetchState } : {}),
609
+ ...(googleCanonical !== undefined ? { google_canonical: googleCanonical } : {}),
610
+ ...(userCanonical !== undefined ? { user_canonical: userCanonical } : {}),
611
+ ...(crawledAs !== undefined ? { crawled_as: crawledAs } : {}),
612
+ };
613
+ }
614
+
551
615
  export interface UrlInspectionBatchResult {
552
616
  inspectionUrl: string;
553
617
  inspectionResult?: unknown;
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  MCP_RECONNECT_INSTRUCTION,
14
14
  inspectUrl,
15
15
  inspectUrlsBoundedConcurrently,
16
+ summarizeUrlInspectionIndexStatus,
16
17
  URL_INSPECTION_BATCH_CONCURRENCY,
17
18
  listSitemaps,
18
19
  listSites,
@@ -319,8 +320,24 @@ const INDEXING_PROVIDER_USAGE_NOTE =
319
320
  const URL_INSPECTION_SCOPE_NOTE =
320
321
  "Google's URL Inspection API reports the version currently known in Google's index; it does not run a live URL test or prove current live-page indexability. The mobileUsabilityResult field is deprecated and may be absent.";
321
322
 
323
+ const URL_INSPECTION_INDEX_STATUS_SUMMARY_SCHEMA = z.object({
324
+ indexed_state: z.enum(['indexed', 'not_indexed', 'unknown']).describe(
325
+ "Conservative state derived only from Google's indexStatusResult.verdict: PASS => indexed, FAIL/NEUTRAL => not_indexed, and missing/unknown/reserved verdicts => unknown.",
326
+ ),
327
+ verdict: z.string().optional(),
328
+ coverage_state: z.string().optional(),
329
+ robots_txt_state: z.string().optional(),
330
+ indexing_state: z.string().optional(),
331
+ last_crawl_time: z.string().optional(),
332
+ page_fetch_state: z.string().optional(),
333
+ google_canonical: z.string().optional(),
334
+ user_canonical: z.string().optional(),
335
+ crawled_as: z.string().optional(),
336
+ });
337
+
322
338
  const INSPECTION_OUTPUT_SCHEMA = {
323
339
  inspection_result: z.unknown(),
340
+ index_status_summary: URL_INSPECTION_INDEX_STATUS_SUMMARY_SCHEMA,
324
341
  note: z.string(),
325
342
  };
326
343
 
@@ -333,6 +350,7 @@ const INSPECTION_BATCH_OUTPUT_SCHEMA = {
333
350
  z.object({
334
351
  inspection_url: z.string(),
335
352
  inspection_result: z.unknown().optional(),
353
+ index_status_summary: URL_INSPECTION_INDEX_STATUS_SUMMARY_SCHEMA.optional(),
336
354
  error: z.string().optional(),
337
355
  }),
338
356
  ),
@@ -404,6 +422,22 @@ const SITE_DELETE_OUTPUT_SCHEMA = {
404
422
  provider_scope_note: z.string(),
405
423
  };
406
424
 
425
+ const SITEMAP_SUBMIT_OUTPUT_SCHEMA = {
426
+ message: z.string(),
427
+ submitted_to_search_console: z.literal(true),
428
+ sitemap_processing_completion_proven: z.literal(false),
429
+ page_indexing_proven: z.literal(false),
430
+ provider_scope_note: z.string(),
431
+ };
432
+
433
+ const SITEMAP_DELETE_OUTPUT_SCHEMA = {
434
+ message: z.string(),
435
+ removed_from_search_console: z.literal(true),
436
+ sitemap_file_deleted: z.literal(false),
437
+ page_deindexing_proven: z.literal(false),
438
+ provider_scope_note: z.string(),
439
+ };
440
+
407
441
  const SEARCH_ANALYTICS_PAGINATION_OUTPUT_SCHEMA = z.object({
408
442
  rows_fetched: z.number().int().nonnegative(),
409
443
  pages_fetched: z.number().int().positive(),
@@ -758,12 +792,12 @@ const TOOL_CATALOG = [
758
792
  {
759
793
  name: 'urls.inspect',
760
794
  description:
761
- "Inspect Google's indexed version of a single URL: index status, last crawl, page-fetch/indexing state, canonicals, and rich-results/AMP analysis where available. This is not a live URL test; mobile usability is a deprecated response field. Use urls.inspect_many for 2-10 URLs.",
795
+ "Inspect Google's indexed version of a single URL: a conservative normalized index-status summary plus Google's raw index status, last crawl, page-fetch/indexing state, canonicals, and rich-results/AMP analysis where available. This is not a live URL test; mobile usability is a deprecated response field. Use urls.inspect_many for 2-10 URLs.",
762
796
  },
763
797
  {
764
798
  name: 'urls.inspect_many',
765
799
  description:
766
- `Inspect Google's indexed versions of up to 10 URLs with bounded concurrency of ${URL_INSPECTION_BATCH_CONCURRENCY} in one MCP call while charging the same URL Inspection safety budget per URL. This does not run live URL tests.`,
800
+ `Inspect Google's indexed versions of up to 10 URLs with a conservative normalized index-status summary per successful item and bounded concurrency of ${URL_INSPECTION_BATCH_CONCURRENCY} in one MCP call while charging the same URL Inspection safety budget per URL. This does not run live URL tests.`,
767
801
  },
768
802
  {
769
803
  name: 'sitemaps.list',
@@ -773,12 +807,12 @@ const TOOL_CATALOG = [
773
807
  {
774
808
  name: 'sitemaps.submit',
775
809
  description:
776
- 'Submit a new sitemap to your Google Search Console account.',
810
+ 'Submit a sitemap URL to a Search Console property. A successful API response proves the Search Console submission request succeeded; it does not prove sitemap processing completed or that any page was indexed.',
777
811
  },
778
812
  {
779
813
  name: 'sitemaps.delete',
780
814
  description:
781
- 'Remove/delete a submitted sitemap from your Google Search Console account.',
815
+ 'Remove a submitted sitemap from a Search Console property. This removes the Search Console sitemap entry; it does not delete the remotely hosted sitemap file or prove that any page was deindexed.',
782
816
  },
783
817
  {
784
818
  name: 'sitemaps.get',
@@ -1012,7 +1046,7 @@ class GscMcpRuntime {
1012
1046
  'urls.inspect',
1013
1047
  {
1014
1048
  title: 'Inspect URL index status',
1015
- description: `Inspect the version of a single URL currently known in Google's index. Returns Google's index-status analysis, last crawl, page-fetch/indexing state, canonicals, and rich-results/AMP analysis where available. This API does not test the live URL or prove current live-page indexability, and its mobile-usability result is deprecated. Use this when the user asks 'is X indexed?' or wants Google's indexed-state evidence for one page. For a bounded group of 2-10 URLs, prefer urls.inspect_many; Google still processes one URL Inspection request per URL and applies the same quota semantics.`,
1049
+ description: `Inspect the version of a single URL currently known in Google's index. Returns a conservative normalized index-status summary alongside Google's raw index-status analysis, last crawl, page-fetch/indexing state, canonicals, and rich-results/AMP analysis where available. This API does not test the live URL or prove current live-page indexability, and its mobile-usability result is deprecated. Use this when the user asks 'is X indexed?' or wants Google's indexed-state evidence for one page. For a bounded group of 2-10 URLs, prefer urls.inspect_many; Google still processes one URL Inspection request per URL and applies the same quota semantics.`,
1016
1050
  inputSchema: {
1017
1051
  site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1018
1052
  inspection_url: z
@@ -1041,8 +1075,10 @@ class GscMcpRuntime {
1041
1075
  inspection_url,
1042
1076
  language_code,
1043
1077
  );
1044
- return toolResponse(JSON.stringify({ note: URL_INSPECTION_SCOPE_NOTE, inspection_result: result }, null, 2), {
1078
+ const indexStatusSummary = summarizeUrlInspectionIndexStatus(result);
1079
+ return toolResponse(JSON.stringify({ note: URL_INSPECTION_SCOPE_NOTE, index_status_summary: indexStatusSummary, inspection_result: result }, null, 2), {
1045
1080
  inspection_result: result,
1081
+ index_status_summary: indexStatusSummary,
1046
1082
  note: URL_INSPECTION_SCOPE_NOTE,
1047
1083
  });
1048
1084
  },
@@ -1052,7 +1088,7 @@ class GscMcpRuntime {
1052
1088
  'urls.inspect_many',
1053
1089
  {
1054
1090
  title: 'Inspect multiple URLs',
1055
- description: `Inspect the versions of up to 10 URLs currently known in Google's index from one Search Console property. This API does not run live URL tests. Google still processes one URL Inspection request per URL, so every requested URL consumes one quota unit and one unit of this server's shared URL-inspection safety budget. Requests use bounded concurrency of ${URL_INSPECTION_BATCH_CONCURRENCY}; results preserve input order, and a Google-access revocation stops later chunks from starting. Use this for a small group of important or debugging-target URLs; do not use it to crawl an entire site.`,
1091
+ description: `Inspect the versions of up to 10 URLs currently known in Google's index from one Search Console property. Each successful item includes a conservative normalized index-status summary alongside Google's raw response. This API does not run live URL tests. Google still processes one URL Inspection request per URL, so every requested URL consumes one quota unit and one unit of this server's shared URL-inspection safety budget. Requests use bounded concurrency of ${URL_INSPECTION_BATCH_CONCURRENCY}; results preserve input order, and a Google-access revocation stops later chunks from starting. Use this for a small group of important or debugging-target URLs; do not use it to crawl an entire site.`,
1056
1092
  inputSchema: {
1057
1093
  site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1058
1094
  inspection_urls: z
@@ -1096,7 +1132,12 @@ class GscMcpRuntime {
1096
1132
  const results = batchResults.map((result) => ({
1097
1133
  inspection_url: result.inspectionUrl,
1098
1134
  ...(result.inspectionResult !== undefined
1099
- ? { inspection_result: result.inspectionResult }
1135
+ ? {
1136
+ inspection_result: result.inspectionResult,
1137
+ index_status_summary: summarizeUrlInspectionIndexStatus(
1138
+ result.inspectionResult,
1139
+ ),
1140
+ }
1100
1141
  : {}),
1101
1142
  ...(result.error !== undefined ? { error: result.error } : {}),
1102
1143
  }));
@@ -1606,12 +1647,12 @@ class GscMcpRuntime {
1606
1647
  'sitemaps.submit',
1607
1648
  {
1608
1649
  title: 'Submit sitemap',
1609
- description: 'Submit a new sitemap to your Google Search Console account.',
1650
+ description: 'Submit a sitemap URL to a Search Console property. A successful API response means the Search Console submission request succeeded; it does not prove sitemap processing completed or that any page was indexed.',
1610
1651
  inputSchema: {
1611
1652
  site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1612
1653
  feedpath: SITEMAP_URL_SCHEMA.describe('The full HTTP/HTTPS URL of the sitemap file to submit, e.g. https://example.com/sitemap.xml'),
1613
1654
  },
1614
- outputSchema: MESSAGE_OUTPUT_SCHEMA,
1655
+ outputSchema: SITEMAP_SUBMIT_OUTPUT_SCHEMA,
1615
1656
  annotations: WRITE_TOOL_ANNOTATIONS['sitemaps.submit'],
1616
1657
  },
1617
1658
  async ({ site_url, feedpath }) => {
@@ -1620,8 +1661,15 @@ class GscMcpRuntime {
1620
1661
  if (rateLimitError) return rateLimitError;
1621
1662
  const accessToken = await this.getAccessToken(googleId);
1622
1663
  await submitSitemap(accessToken, site_url, feedpath);
1623
- const message = `Successfully submitted sitemap: ${feedpath} for site: ${site_url}`;
1624
- return toolResponse(message, { message });
1664
+ const message = `Successfully submitted sitemap to Search Console: ${feedpath} for site: ${site_url}`;
1665
+ const payload = {
1666
+ message,
1667
+ submitted_to_search_console: true as const,
1668
+ sitemap_processing_completion_proven: false as const,
1669
+ page_indexing_proven: false as const,
1670
+ provider_scope_note: "Google's Sitemaps.submit method returns an empty body on success. That success confirms the Search Console submission operation, not completed sitemap processing or page indexing.",
1671
+ };
1672
+ return toolResponse(`${message}\n\n${payload.provider_scope_note}`, payload);
1625
1673
  },
1626
1674
  );
1627
1675
 
@@ -1629,12 +1677,12 @@ class GscMcpRuntime {
1629
1677
  'sitemaps.delete',
1630
1678
  {
1631
1679
  title: 'Delete sitemap',
1632
- description: 'Remove/delete a submitted sitemap from your Google Search Console account.',
1680
+ description: 'Remove a submitted sitemap from a Search Console property. This removes the Search Console sitemap entry; it does not delete the remotely hosted sitemap file or prove that any page was deindexed.',
1633
1681
  inputSchema: {
1634
1682
  site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1635
1683
  feedpath: SITEMAP_URL_SCHEMA.describe('The full HTTP/HTTPS URL of the sitemap file to delete, e.g. https://example.com/sitemap.xml'),
1636
1684
  },
1637
- outputSchema: MESSAGE_OUTPUT_SCHEMA,
1685
+ outputSchema: SITEMAP_DELETE_OUTPUT_SCHEMA,
1638
1686
  annotations: WRITE_TOOL_ANNOTATIONS['sitemaps.delete'],
1639
1687
  },
1640
1688
  async ({ site_url, feedpath }) => {
@@ -1643,8 +1691,15 @@ class GscMcpRuntime {
1643
1691
  if (rateLimitError) return rateLimitError;
1644
1692
  const accessToken = await this.getAccessToken(googleId);
1645
1693
  await deleteSitemap(accessToken, site_url, feedpath);
1646
- const message = `Successfully deleted sitemap: ${feedpath} for site: ${site_url}`;
1647
- return toolResponse(message, { message });
1694
+ const message = `Successfully removed sitemap from Search Console: ${feedpath} for site: ${site_url}`;
1695
+ const payload = {
1696
+ message,
1697
+ removed_from_search_console: true as const,
1698
+ sitemap_file_deleted: false as const,
1699
+ page_deindexing_proven: false as const,
1700
+ provider_scope_note: "Google's Sitemaps.delete method removes the submitted sitemap from the Search Console property. It does not delete the remotely hosted sitemap file or prove that any page was deindexed.",
1701
+ };
1702
+ return toolResponse(`${message}\n\n${payload.provider_scope_note}`, payload);
1648
1703
  },
1649
1704
  );
1650
1705
  }