@vmandic/searchconsole-mcp 1.0.0 → 1.1.0

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.
Files changed (3) hide show
  1. package/README.md +105 -29
  2. package/dist/server.js +10 -7
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Search Console MCP
2
2
 
3
3
  [![CI](https://github.com/vmandic/searchconsole-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/vmandic/searchconsole-mcp/actions/workflows/ci.yml)
4
+ [![GitHub release](https://img.shields.io/github/v/release/vmandic/searchconsole-mcp?include_prereleases)](https://github.com/vmandic/searchconsole-mcp/releases/latest)
5
+ [![npm version](https://img.shields.io/npm/v/@vmandic/searchconsole-mcp.svg)](https://www.npmjs.com/package/@vmandic/searchconsole-mcp)
4
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
7
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](package.json)
6
8
  [![MCP](https://img.shields.io/badge/MCP-Model%20Context%20Protocol-6366f1)](https://modelcontextprotocol.io)
@@ -33,6 +35,7 @@ Connect Cursor, Claude Desktop, or any MCP client to your GSC properties: search
33
35
  - [Configuration](#configuration)
34
36
  - [Troubleshooting](#troubleshooting)
35
37
  - [Development](#development)
38
+ - [Releases and npm package](#releases-and-npm-package)
36
39
  - [License](#license)
37
40
 
38
41
  ---
@@ -61,6 +64,8 @@ Use it when you want:
61
64
 
62
65
  **Input validation** — Tool arguments are validated with Zod (dates, URLs, row limits, allowlisted dimensions).
63
66
 
67
+ **Optional TOON output** — Set `GSC_OUTPUT_FORMAT=toon` to return compact [TOON](https://github.com/toon-format/toon) payloads (fewer tokens on search analytics and list tools).
68
+
64
69
  **Clear errors** — Failures return MCP text with `isError: true` and actionable messages (auth, `site_url` format, quota).
65
70
 
66
71
  ---
@@ -135,6 +140,8 @@ When finished, summarize: clone path, GCP project ID, config file edited, and th
135
140
 
136
141
  Use this if you prefer to run commands yourself.
137
142
 
143
+ **Fastest path (npm):** install from [@vmandic/searchconsole-mcp](https://www.npmjs.com/package/@vmandic/searchconsole-mcp), complete [Google authentication](#google-authentication), then [connect your MCP client](#connect-your-mcp-client) with `npx` (see [Option A — Install from npm](#option-a--install-from-npm-recommended)).
144
+
138
145
  **1. Install and build** (from source):
139
146
 
140
147
  ```bash
@@ -197,7 +204,44 @@ Optional: [Google Cloud SDK](https://cloud.google.com/sdk) (`gcloud`) for the in
197
204
 
198
205
  ## Installation
199
206
 
200
- ### Option A Run from a clone (recommended for development)
207
+ Published on npm as [**`@vmandic/searchconsole-mcp`**](https://www.npmjs.com/package/@vmandic/searchconsole-mcp). The name is scoped because npm rejects unscoped `searchconsole-mcp` as too similar to [`search-console-mcp`](https://www.npmjs.com/package/search-console-mcp) (a different package).
208
+
209
+ ### Option A — Install from npm (recommended)
210
+
211
+ No clone required. You still need [Google authentication](#google-authentication) on the machine.
212
+
213
+ **Run once (smoke test):**
214
+
215
+ ```bash
216
+ npx -y @vmandic/searchconsole-mcp --help
217
+ npx -y @vmandic/searchconsole-mcp --version
218
+ ```
219
+
220
+ **Global CLI (optional):**
221
+
222
+ ```bash
223
+ npm install -g @vmandic/searchconsole-mcp
224
+ searchconsole-mcp --help
225
+ ```
226
+
227
+ **MCP client (stdio via `npx`)** — works in Cursor, Claude Code, Copilot, Codex, Claude Desktop. Example for **Cursor** (`~/.cursor/mcp.json`):
228
+
229
+ ```json
230
+ {
231
+ "mcpServers": {
232
+ "searchconsole-mcp": {
233
+ "command": "npx",
234
+ "args": ["-y", "@vmandic/searchconsole-mcp"]
235
+ }
236
+ }
237
+ }
238
+ ```
239
+
240
+ After `npm install -g`, you can use `"command": "searchconsole-mcp"` and `"args": []` instead.
241
+
242
+ Restart the MCP client, then try *“List my Search Console properties”* (`gsc_list_sites`).
243
+
244
+ ### Option B — Run from a clone (development)
201
245
 
202
246
  ```bash
203
247
  git clone https://github.com/vmandic/searchconsole-mcp.git
@@ -214,24 +258,18 @@ node dist/server.js --help
214
258
  node dist/server.js --version
215
259
  ```
216
260
 
217
- ### Option B Global CLI after build
218
-
219
- ```bash
220
- npm link -g
221
- searchconsole-mcp --help
222
- ```
223
-
224
- Then point your MCP client at `searchconsole-mcp` instead of `node …/dist/server.js`.
261
+ Point your MCP client at `node /absolute/path/to/searchconsole-mcp/dist/server.js` (see [Connect your MCP client](#connect-your-mcp-client)).
225
262
 
226
- ### Option C — `npx` (when published to npm)
263
+ ### Option C — Global CLI from a local build
227
264
 
228
- The package is published under the **`@vmandic`** scope because npm blocks the unscoped name as too similar to [`search-console-mcp`](https://www.npmjs.com/package/search-console-mcp).
265
+ From a clone after `npm run build`:
229
266
 
230
267
  ```bash
231
- npx -y @vmandic/searchconsole-mcp
268
+ npm link -g
269
+ searchconsole-mcp --help
232
270
  ```
233
271
 
234
- Until the package is on npm, use Option A or B from a local clone.
272
+ Then use `"command": "searchconsole-mcp"` in MCP config instead of `node …/dist/server.js`.
235
273
 
236
274
  ---
237
275
 
@@ -309,9 +347,10 @@ Every client runs the **same local Node.js MCP server** (`searchconsole-mcp`). T
309
347
 
310
348
  **Before you connect any client**
311
349
 
312
- 1. Run `npm run build` so `dist/server.js` exists.
313
- 2. Complete [Google authentication](#google-authentication) (ADC) once on the machine.
314
- 3. Use an **absolute path** to `dist/server.js` in config (or `searchconsole-mcp` after `npm link -g`).
350
+ 1. Complete [Google authentication](#google-authentication) (ADC) once on the machine.
351
+ 2. Choose how to start the server:
352
+ - **npm:** [Option A](#option-a--install-from-npm-recommended) `npx -y @vmandic/searchconsole-mcp` or global `searchconsole-mcp`
353
+ - **Clone:** run `npm run build` so `dist/server.js` exists, then use an **absolute path** in config (or `npm link -g` / Option C)
315
354
 
316
355
  **If you use more than one client**, set up **Claude Code first**. You will reuse the same binary and credentials; doing auth and paths once avoids confusion when you add Cursor, Copilot, or Codex.
317
356
 
@@ -377,7 +416,20 @@ Edit **`~/.cursor/mcp.json`** (or MCP settings in the project):
377
416
  }
378
417
  ```
379
418
 
380
- After `npm link -g`:
419
+ **From npm (`npx`, no clone):**
420
+
421
+ ```json
422
+ {
423
+ "mcpServers": {
424
+ "searchconsole-mcp": {
425
+ "command": "npx",
426
+ "args": ["-y", "@vmandic/searchconsole-mcp"]
427
+ }
428
+ }
429
+ }
430
+ ```
431
+
432
+ **After `npm link -g` or `npm install -g @vmandic/searchconsole-mcp`:**
381
433
 
382
434
  ```json
383
435
  "command": "searchconsole-mcp",
@@ -578,18 +630,20 @@ This server can access **your** Search Console data using **your** Google creden
578
630
 
579
631
  ## Transports: stdio vs HTTP
580
632
 
581
- ```
582
- ┌─────────────┐ stdin/stdout (JSON-RPC) ┌──────────────┐
583
- MCP client │ ◄──────────────────────────────► │ searchconsole-mcp │
584
- (Cursor) │ default: stdio │ + Google │
585
- └─────────────┘ │ Search │
586
- │ Console API│
587
- ┌─────────────┐ HTTP POST/GET /mcp └──────────────┘
588
- MCP client │ ◄──────────────────────────────► ▲
589
- │ (optional) │ --transport http │
590
- └─────────────┘ │
591
- ADC /
592
- service account
633
+ ```mermaid
634
+ flowchart LR
635
+ subgraph clients [MCP clients]
636
+ C["MCP client (Cursor, Claude, …)"]
637
+ H["MCP client (optional)"]
638
+ end
639
+ S["searchconsole-mcp\nNode process"]
640
+ G["Google Search\nConsole API"]
641
+ A["ADC / service account"]
642
+
643
+ C <-->|"stdio: stdin/stdout JSON-RPC (default)"| S
644
+ H <-->|"HTTP: POST/GET /mcp (--transport http)"| S
645
+ S --> G
646
+ A -.->|credentials| S
593
647
  ```
594
648
 
595
649
  ### Stdio (default, recommended)
@@ -629,8 +683,11 @@ searchconsole-mcp [--transport stdio|http] [--host <addr>] [--port <n>] [--versi
629
683
  | `--transport` / `GSC_MCP_TRANSPORT` | `stdio` | `stdio` or `http` |
630
684
  | `--host` / `GSC_MCP_HOST` | `127.0.0.1` | HTTP bind address |
631
685
  | `--port` / `GSC_MCP_PORT` | `3000` | HTTP port |
686
+ | `GSC_OUTPUT_FORMAT` | `json` | Tool result encoding: `json` (default) or `toon` ([TOON](https://github.com/toon-format/toon)) for fewer tokens on tabular GSC data |
632
687
  | `GOOGLE_APPLICATION_CREDENTIALS` | — | Path to service account JSON |
633
688
 
689
+ **TOON output** — Set `GSC_OUTPUT_FORMAT=toon` in the MCP server env (Cursor `env` block, Claude config, etc.). Successful tool responses start with `format: toon` followed by TOON-encoded data. Search analytics rows use tab-separated tabular encoding with dimension names from your request (e.g. `query`, `page`). URL inspection stays JSON because nested payloads rarely benefit from TOON.
690
+
634
691
  ### Smithery
635
692
 
636
693
  [Smithery](https://smithery.ai/) is a registry for discovering and installing MCP servers in compatible clients. [smithery.yaml](smithery.yaml) tells Smithery to run this server over stdio via `npx -y @vmandic/searchconsole-mcp`.
@@ -692,6 +749,25 @@ Contributions welcome via [issues](https://github.com/vmandic/searchconsole-mcp/
692
749
 
693
750
  ---
694
751
 
752
+ ## Releases and npm package
753
+
754
+ | What | Where |
755
+ |------|--------|
756
+ | **Version history** | [CHANGELOG.md](CHANGELOG.md) |
757
+ | **GitHub Releases** (tags, notes) | [github.com/vmandic/searchconsole-mcp/releases](https://github.com/vmandic/searchconsole-mcp/releases) |
758
+ | **Latest release** | [releases/latest](https://github.com/vmandic/searchconsole-mcp/releases/latest) |
759
+ | **npm package** (install) | [@vmandic/searchconsole-mcp](https://www.npmjs.com/package/@vmandic/searchconsole-mcp) |
760
+
761
+ Install the published build:
762
+
763
+ ```bash
764
+ npx -y @vmandic/searchconsole-mcp
765
+ ```
766
+
767
+ **GitHub Packages** ([packages](https://github.com/vmandic/searchconsole-mcp/packages)) is not used for distribution; this project publishes to the public npm registry. See [docs/RELEASES.md](docs/RELEASES.md) for maintainer release steps and version alignment.
768
+
769
+ ---
770
+
695
771
  ## License
696
772
 
697
773
  [MIT](LICENSE) — Copyright (c) Vedran Mandić and contributors.
package/dist/server.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
- var ut=Object.defineProperty;var u=(t,e)=>()=>(t&&(e=t(t=0)),e);var dt=(t,e)=>{for(var o in e)ut(t,o,{get:e[o],enumerable:!0})};var F,A,R,$,N,G=u(()=>{"use strict";F="searchconsole-mcp",A="https://www.googleapis.com/auth/webmasters.readonly",R=A,$="https://www.googleapis.com/auth/analytics.readonly,https://www.googleapis.com/auth/cloud-platform,"+A,N="1.0.0"});function mt(t){if(!(t instanceof Error))return;let e=t,o=e.response?.data?.error?.status;if(typeof o=="string")return o;if(typeof e.code=="string")return e.code}function b(t){if(!(t instanceof Error))return"An unexpected error occurred.";let e=mt(t),o=t.message;return e==="UNAUTHENTICATED"||o.includes("UNAUTHENTICATED")||o.includes("Could not load the default credentials")||o.includes("insufficient authentication scopes")?`Authentication failed. Run: gcloud auth application-default login --scopes=${R}`:e==="PERMISSION_DENIED"||o.includes("PERMISSION_DENIED")||o.includes("Forbidden")?"Permission denied. Ensure your Google account has access to this Search Console property.":e==="NOT_FOUND"||o.includes("NOT_FOUND")?'Site or resource not found. Check site_url matches GSC (e.g. "https://example.com/" with trailing slash).':e==="RESOURCE_EXHAUSTED"||o.includes("RESOURCE_EXHAUSTED")||o.includes("quota")?"API quota exceeded. Please wait a moment and try again.":e==="INVALID_ARGUMENT"||o.includes("INVALID_ARGUMENT")?"Invalid request parameters. Check site_url, dates, and dimension names.":o.replace(/projects\/[^\s/]+/g,"projects/***").replace(/\/home\/[^\s/]+/g,"/home/***").replace(/\/Users\/[^\s/]+/g,"/Users/***").replace(/at\s+.+\(.+:\d+:\d+\)/g,"").trim()||"An unexpected error occurred."}function S(t){return b(t)}var O=u(()=>{"use strict";G()});import{searchconsole as St}from"@googleapis/searchconsole";function g(t){return K||(M||(M=St({version:"v1",auth:t})),M)}var M,K,x=u(()=>{"use strict";M=null,K=null});async function Q(t,e){return(await g(t).searchanalytics.query({siteUrl:e.site_url,requestBody:{startDate:e.start_date,endDate:e.end_date,dimensions:e.dimensions,type:e.type,rowLimit:e.row_limit,startRow:e.start_row,dimensionFilterGroups:e.dimension_filter_groups,aggregationType:e.aggregation_type,dataState:e.data_state}})).data}var Z=u(()=>{"use strict";x()});async function tt(t,e){return(await g(t).urlInspection.index.inspect({requestBody:{siteUrl:e.site_url,inspectionUrl:e.inspection_url,languageCode:e.language_code??"en-US"}})).data}var et=u(()=>{"use strict";x()});async function ot(t,e){return(await g(t).sitemaps.list({siteUrl:e})).data}var rt=u(()=>{"use strict";x()});async function st(t){return(await g(t).sites.list({})).data}var nt=u(()=>{"use strict";x()});import{z as s}from"zod";var it,k,yt,wt,Tt,xt,p,E,U,at=u(()=>{"use strict";it=s.string().regex(/^\d{4}-\d{2}-\d{2}$/,"Expected YYYY-MM-DD"),k=s.string().min(1).max(2048).refine(t=>t.startsWith("sc-domain:")||/^https?:\/\//i.test(t),{message:"site_url must start with https:// or sc-domain:"}),yt=s.enum(["query","page","country","device","searchAppearance","date"]),wt=s.enum(["web","image","video","news","discover","googleNews"]),Tt=s.object({dimension:s.string().min(1).max(64),operator:s.string().min(1).max(32),expression:s.string().min(1).max(512)}),xt=s.object({groupType:s.string().max(32).optional(),filters:s.array(Tt).max(20).optional()}),p=s.object({site_url:k,start_date:it,end_date:it,dimensions:s.array(yt).max(5).optional(),type:wt.optional(),row_limit:s.number().int().min(1).max(25e3).optional(),start_row:s.number().int().min(0).max(24999).optional(),dimension_filter_groups:s.array(xt).max(5).optional(),aggregation_type:s.enum(["auto","byProperty","byPage"]).optional(),data_state:s.enum(["final","all"]).optional()}),E=s.object({site_url:k,inspection_url:s.string().url().max(2048),language_code:s.string().min(2).max(16).optional()}),U=s.object({site_url:k})});var ct={};dt(ct,{registerGscTools:()=>Ct});function I(t){return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}function Et(t){return{content:[{type:"text",text:t}],isError:!0}}function D(t,e){return async o=>{let r=t.safeParse(o);if(!r.success)return Et(`Invalid request parameters. ${JSON.stringify(r.error.flatten(),null,2)}`);try{return await e(r.data)}catch(i){return{content:[{type:"text",text:b(i)}],isError:!0}}}}function Ct(t,e){t.tool("gsc_mcp_server_ping","Liveness check for this MCP server process (local Node.js). Returns pong. Does not call Google Search Console.",{},async()=>({content:[{type:"text",text:"pong"}]})),t.tool("gsc_list_sites","Lists all sites (properties) the authenticated user has access to in Google Search Console.",{},async()=>{try{return I(await st(e))}catch(o){return{content:[{type:"text",text:b(o)}],isError:!0}}}),t.tool("gsc_search_analytics","Queries Google Search Console search analytics data \u2014 impressions, clicks, CTR, and position for queries, pages, countries, and devices.",{site_url:p.shape.site_url,start_date:p.shape.start_date,end_date:p.shape.end_date,dimensions:p.shape.dimensions,type:p.shape.type,row_limit:p.shape.row_limit,start_row:p.shape.start_row,dimension_filter_groups:p.shape.dimension_filter_groups,aggregation_type:p.shape.aggregation_type,data_state:p.shape.data_state},D(p,async o=>I(await Q(e,o)))),t.tool("gsc_inspect_url","Inspects a URL in Google Search Console \u2014 index status, crawl info, mobile usability, and rich results.",{site_url:E.shape.site_url,inspection_url:E.shape.inspection_url,language_code:E.shape.language_code},D(E,async o=>I(await tt(e,o)))),t.tool("gsc_list_sitemaps","Lists all sitemaps submitted for a site in Google Search Console.",{site_url:U.shape.site_url},D(U,async({site_url:o})=>I(await ot(e,o))))}var pt=u(()=>{"use strict";O();Z();et();rt();nt();at()});function V(){let t=process.stdout.write.bind(process.stdout),e=process.stderr.write.bind(process.stderr);return console.log=(...o)=>{e(Buffer.from(o.join(" ")+`
3
- `))},console.info=console.log,console.debug=console.log,console.warn=(...o)=>{e(Buffer.from("[WARN] "+o.join(" ")+`
4
- `))},process.stdout.write=((o,r,i)=>(typeof o=="string"?o:o?.toString?.()??"").includes('"jsonrpc"')?t(o,r,i):e(o,r,i)),{writeStdout:t,writeStderr:e}}G();var _="127.0.0.1";var Y=["stdio","http"];function H(t,e,o){let r=t.indexOf(e);if(r!==-1&&t[r+1])return t[r+1];if(o)return process.env[o]}function J(t){t(`searchconsole-mcp \u2014 Google Search Console MCP server (read-only)
2
+ var dt=Object.defineProperty;var d=(t,o)=>()=>(t&&(o=t(t=0)),o);var mt=(t,o)=>{for(var e in o)dt(t,e,{get:o[e],enumerable:!0})};var V,G,O,$,v,b=d(()=>{"use strict";V="searchconsole-mcp",G="https://www.googleapis.com/auth/webmasters.readonly",O=G,$="https://www.googleapis.com/auth/analytics.readonly,https://www.googleapis.com/auth/cloud-platform,"+G,v="1.1.0"});function ft(t){if(!(t instanceof Error))return;let o=t,e=o.response?.data?.error?.status;if(typeof e=="string")return e;if(typeof o.code=="string")return o.code}function k(t){if(!(t instanceof Error))return"An unexpected error occurred.";let o=ft(t),e=t.message;return o==="UNAUTHENTICATED"||e.includes("UNAUTHENTICATED")||e.includes("Could not load the default credentials")||e.includes("insufficient authentication scopes")?`Authentication failed. Run: gcloud auth application-default login --scopes=${O}`:o==="PERMISSION_DENIED"||e.includes("PERMISSION_DENIED")||e.includes("Forbidden")?"Permission denied. Ensure your Google account has access to this Search Console property.":o==="NOT_FOUND"||e.includes("NOT_FOUND")?'Site or resource not found. Check site_url matches GSC (e.g. "https://example.com/" with trailing slash).':o==="RESOURCE_EXHAUSTED"||e.includes("RESOURCE_EXHAUSTED")||e.includes("quota")?"API quota exceeded. Please wait a moment and try again.":o==="INVALID_ARGUMENT"||e.includes("INVALID_ARGUMENT")?"Invalid request parameters. Check site_url, dates, and dimension names.":e.replace(/projects\/[^\s/]+/g,"projects/***").replace(/\/home\/[^\s/]+/g,"/home/***").replace(/\/Users\/[^\s/]+/g,"/Users/***").replace(/at\s+.+\(.+:\d+:\d+\)/g,"").trim()||"An unexpected error occurred."}function S(t){return k(t)}var I=d(()=>{"use strict";b()});import{encode as St}from"@toon-format/toon";function wt(){return process.env.GSC_OUTPUT_FORMAT?.trim().toLowerCase()==="toon"?"toon":"json"}function P(t,o={}){let e=wt(),r=o.kind??"default";if(e==="json"||r==="inspect")return{content:[{type:"text",text:JSON.stringify(t,null,2)}]};let n=r==="search_analytics"?xt(t,o.dimensions):t,a=St(n,{delimiter:" "});return{content:[{type:"text",text:`${Tt}toon
3
+
4
+ ${a}`}]}}function xt(t,o){if(!t||typeof t!="object"||Array.isArray(t))return t;let e=t,r=e.rows;if(!Array.isArray(r))return t;let n=r.map(a=>Et(a,o));return{...e,rows:n}}function Et(t,o){if(!t||typeof t!="object"||Array.isArray(t))return t;let e=t,r=Array.isArray(e.keys)?e.keys:[],n={};r.forEach((a,m)=>{let T=o?.[m]??`key${m}`;n[T]=a});for(let[a,m]of Object.entries(e))a!=="keys"&&(n[a]=m);return n}var Tt,K=d(()=>{"use strict";Tt="format: "});import{searchconsole as Pt}from"@googleapis/searchconsole";function g(t){return Q||(H||(H=Pt({version:"v1",auth:t})),H)}var H,Q,C=d(()=>{"use strict";H=null,Q=null});async function Z(t,o){return(await g(t).searchanalytics.query({siteUrl:o.site_url,requestBody:{startDate:o.start_date,endDate:o.end_date,dimensions:o.dimensions,type:o.type,rowLimit:o.row_limit,startRow:o.start_row,dimensionFilterGroups:o.dimension_filter_groups,aggregationType:o.aggregation_type,dataState:o.data_state}})).data}var tt=d(()=>{"use strict";C()});async function et(t,o){return(await g(t).urlInspection.index.inspect({requestBody:{siteUrl:o.site_url,inspectionUrl:o.inspection_url,languageCode:o.language_code??"en-US"}})).data}var ot=d(()=>{"use strict";C()});async function rt(t,o){return(await g(t).sitemaps.list({siteUrl:o})).data}var nt=d(()=>{"use strict";C()});async function st(t){return(await g(t).sites.list({})).data}var it=d(()=>{"use strict";C()});import{z as s}from"zod";var at,M,Ct,At,Rt,Gt,l,A,U,ct=d(()=>{"use strict";at=s.string().regex(/^\d{4}-\d{2}-\d{2}$/,"Expected YYYY-MM-DD"),M=s.string().min(1).max(2048).refine(t=>t.startsWith("sc-domain:")||/^https?:\/\//i.test(t),{message:"site_url must start with https:// or sc-domain:"}),Ct=s.enum(["query","page","country","device","searchAppearance","date"]),At=s.enum(["web","image","video","news","discover","googleNews"]),Rt=s.object({dimension:s.string().min(1).max(64),operator:s.string().min(1).max(32),expression:s.string().min(1).max(512)}),Gt=s.object({groupType:s.string().max(32).optional(),filters:s.array(Rt).max(20).optional()}),l=s.object({site_url:M,start_date:at,end_date:at,dimensions:s.array(Ct).max(5).optional(),type:At.optional(),row_limit:s.number().int().min(1).max(25e3).optional(),start_row:s.number().int().min(0).max(24999).optional(),dimension_filter_groups:s.array(Gt).max(5).optional(),aggregation_type:s.enum(["auto","byProperty","byPage"]).optional(),data_state:s.enum(["final","all"]).optional()}),A=s.object({site_url:M,inspection_url:s.string().url().max(2048),language_code:s.string().min(2).max(16).optional()}),U=s.object({site_url:M})});var pt={};mt(pt,{registerGscTools:()=>bt});function Ot(t){return{content:[{type:"text",text:t}],isError:!0}}function D(t,o){return async e=>{let r=t.safeParse(e);if(!r.success)return Ot(`Invalid request parameters. ${JSON.stringify(r.error.flatten(),null,2)}`);try{return await o(r.data)}catch(n){return{content:[{type:"text",text:k(n)}],isError:!0}}}}function bt(t,o){t.tool("gsc_mcp_server_ping","Liveness check for this MCP server process (local Node.js). Returns pong. Does not call Google Search Console.",{},async()=>({content:[{type:"text",text:"pong"}]})),t.tool("gsc_list_sites","Lists all sites (properties) the authenticated user has access to in Google Search Console.",{},async()=>{try{return P(await st(o))}catch(e){return{content:[{type:"text",text:k(e)}],isError:!0}}}),t.tool("gsc_search_analytics","Queries Google Search Console search analytics data \u2014 impressions, clicks, CTR, and position for queries, pages, countries, and devices.",{site_url:l.shape.site_url,start_date:l.shape.start_date,end_date:l.shape.end_date,dimensions:l.shape.dimensions,type:l.shape.type,row_limit:l.shape.row_limit,start_row:l.shape.start_row,dimension_filter_groups:l.shape.dimension_filter_groups,aggregation_type:l.shape.aggregation_type,data_state:l.shape.data_state},D(l,async e=>P(await Z(o,e),{kind:"search_analytics",dimensions:e.dimensions}))),t.tool("gsc_inspect_url","Inspects a URL in Google Search Console \u2014 index status, crawl info, mobile usability, and rich results.",{site_url:A.shape.site_url,inspection_url:A.shape.inspection_url,language_code:A.shape.language_code},D(A,async e=>P(await et(o,e),{kind:"inspect"}))),t.tool("gsc_list_sitemaps","Lists all sitemaps submitted for a site in Google Search Console.",{site_url:U.shape.site_url},D(U,async({site_url:e})=>P(await rt(o,e))))}var lt=d(()=>{"use strict";I();K();tt();ot();nt();it();ct()});function B(){let t=process.stdout.write.bind(process.stdout),o=process.stderr.write.bind(process.stderr);return console.log=(...e)=>{o(Buffer.from(e.join(" ")+`
5
+ `))},console.info=console.log,console.debug=console.log,console.warn=(...e)=>{o(Buffer.from("[WARN] "+e.join(" ")+`
6
+ `))},process.stdout.write=((e,r,n)=>(typeof e=="string"?e:e?.toString?.()??"").includes('"jsonrpc"')?t(e,r,n):o(e,r,n)),{writeStdout:t,writeStderr:o}}b();var _="127.0.0.1";var Y=["stdio","http"];function N(t,o,e){let r=t.indexOf(o);if(r!==-1&&t[r+1])return t[r+1];if(e)return process.env[e]}function J(t){t(`searchconsole-mcp \u2014 Google Search Console MCP server (read-only)
5
7
 
6
8
  Usage: searchconsole-mcp [options]
7
9
 
@@ -17,9 +19,10 @@ Environment:
17
19
  GSC_MCP_TRANSPORT Same as --transport
18
20
  GSC_MCP_PORT Same as --port
19
21
  GSC_MCP_HOST Same as --host
22
+ GSC_OUTPUT_FORMAT Tool payload format: json (default) or toon
20
23
 
21
24
  Auth (Application Default Credentials):
22
- gcloud auth application-default login --scopes=${R}
25
+ gcloud auth application-default login --scopes=${O}
23
26
  (If you also use Analytics MCP: --scopes=${$})
24
27
 
25
28
  HTTP security:
@@ -29,6 +32,6 @@ HTTP security:
29
32
  Examples:
30
33
  npx -y @vmandic/searchconsole-mcp
31
34
  node dist/server.js --transport http --port 3000
32
- `)}function q(t){return"error"in t}function W(t){if(t.includes("--help")||t.includes("-h"))return{transport:"stdio",host:_,port:3e3,showHelp:!0,showVersion:!1};if(t.includes("--version")||t.includes("-v"))return{transport:"stdio",host:_,port:3e3,showHelp:!1,showVersion:!0};let e=H(t,"--transport","GSC_MCP_TRANSPORT")??"stdio";if(!Y.includes(e))return{error:`Unknown transport: ${e}. Valid: ${Y.join(", ")}`};let o=H(t,"--port","GSC_MCP_PORT")??"3000",r=parseInt(o,10);if(Number.isNaN(r)||r<1||r>65535)return{error:`Invalid port: ${o}`};let i=H(t,"--host","GSC_MCP_HOST")??_;return!i||i.includes("/")||i.includes(" ")?{error:`Invalid host: ${i}`}:{transport:e,host:i,port:r,showHelp:!1,showVersion:!1}}G();async function X(t,e){let o=[],r=0;for await(let i of t){let d=i;if(r+=d.length,r>e)return{ok:!1,status:413,jsonRpcMessage:"Payload too large"};o.push(d)}try{return{ok:!0,body:JSON.parse(Buffer.concat(o).toString("utf8"))}}catch{return{ok:!1,status:400,jsonRpcMessage:"Parse error"}}}O();function T(t,e,o){return{status:t,body:JSON.stringify({jsonrpc:"2.0",error:{code:e,message:o},id:null})}}function ht(t){t.setHeader("X-Content-Type-Options","nosniff")}function _t(t){return t==="0.0.0.0"||t==="::"||t==="[::]"}async function z(t,e){let o=t.host??_,r=t.port,i=await import("@modelcontextprotocol/sdk/server/streamableHttp.js"),d=await import("node:http"),{randomUUID:P}=await import("node:crypto"),{isInitializeRequest:v}=await import("@modelcontextprotocol/sdk/types.js"),a={},y=d.createServer(async(l,n)=>{if(ht(n),new URL(l.url??"/",`http://${o}:${r}`).pathname!=="/mcp"){n.writeHead(404,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Not found. Use /mcp"}));return}let f=l.headers["mcp-session-id"];if(l.method==="POST"){let w=await X(l,4194304);if(!w.ok){let c=w.status===413?T(413,-32e3,w.jsonRpcMessage):T(400,-32700,w.jsonRpcMessage);n.writeHead(c.status,{"Content-Type":"application/json"}),n.end(c.body);return}let B=w.body;try{let c;if(f&&a[f])c=a[f];else if(!f&&v(B)){if(Object.keys(a).length>=32){let m=T(503,-32e3,"Too many active sessions");n.writeHead(m.status,{"Content-Type":"application/json"}),n.end(m.body);return}c=new i.StreamableHTTPServerTransport({sessionIdGenerator:()=>P(),onsessioninitialized:m=>{a[m]=c}}),c.onclose=()=>{let m=c.sessionId;m&&a[m]&&delete a[m]},await e().connect(c)}else{let h=T(400,-32e3,"Bad Request: No valid session ID provided");n.writeHead(h.status,{"Content-Type":"application/json"}),n.end(h.body);return}await c.handleRequest(l,n,B)}catch(c){if(console.error("[searchconsole-mcp] Error handling MCP request:",S(c)),!n.headersSent){let h=T(500,-32603,"Internal server error");n.writeHead(h.status,{"Content-Type":"application/json"}),n.end(h.body)}}return}if(l.method==="GET"||l.method==="DELETE"){if(!f||!a[f]){n.writeHead(400,{"Content-Type":"text/plain"}),n.end("Invalid or missing session ID");return}await a[f].handleRequest(l,n);return}n.writeHead(405,{"Content-Type":"text/plain"}),n.end("Method not allowed")});y.listen(r,o,()=>{console.error(`[searchconsole-mcp] Streamable HTTP server listening on http://${o}:${r}/mcp`),_t(o)&&console.error("[searchconsole-mcp] WARNING: HTTP is bound to all interfaces. Anyone on the network can use your Google credentials via MCP.")});let j=async()=>{console.error("[searchconsole-mcp] Shutting down HTTP server...");for(let l of Object.keys(a)){try{await a[l].close()}catch{}delete a[l]}y.close(),process.exit(0)};process.on("SIGTERM",j),process.on("SIGINT",j)}O();var{writeStdout:lt,writeStderr:Pt}=V(),At=process.argv.slice(2),L=W(At);q(L)&&(Pt(Buffer.from(`[searchconsole-mcp] Error: ${L.error}
33
- `)),process.exit(1));var C=L;C.showHelp&&(J(t=>lt(Buffer.from(t))),process.exit(0));C.showVersion&&(lt(Buffer.from(N+`
34
- `)),process.exit(0));async function Rt(){let t=await import("@modelcontextprotocol/sdk/server/mcp.js"),e=await import("@modelcontextprotocol/sdk/server/stdio.js"),o=await import("google-auth-library"),r=await Promise.resolve().then(()=>(pt(),ct)),i=new o.GoogleAuth({scopes:[A]});function d(){let y=new t.McpServer({name:F,version:N});return r.registerGscTools(y,i),y}if(C.transport==="http"){await z({host:C.host,port:C.port},d);return}let P=d(),v=new e.StdioServerTransport;await P.connect(v),console.error("[searchconsole-mcp] Server started, waiting for connections...");let a=()=>{console.error("[searchconsole-mcp] Shutting down..."),P.close().then(()=>process.exit(0))};process.on("SIGTERM",a),process.on("SIGINT",a)}process.on("uncaughtException",t=>{console.error("[searchconsole-mcp] Uncaught exception:",S(t))});process.on("unhandledRejection",t=>{console.error("[searchconsole-mcp] Unhandled rejection:",S(t))});Rt().catch(t=>{console.error("[searchconsole-mcp] Fatal error:",S(t)),process.exit(1)});
35
+ `)}function X(t){return"error"in t}function q(t){if(t.includes("--help")||t.includes("-h"))return{transport:"stdio",host:_,port:3e3,showHelp:!0,showVersion:!1};if(t.includes("--version")||t.includes("-v"))return{transport:"stdio",host:_,port:3e3,showHelp:!1,showVersion:!0};let o=N(t,"--transport","GSC_MCP_TRANSPORT")??"stdio";if(!Y.includes(o))return{error:`Unknown transport: ${o}. Valid: ${Y.join(", ")}`};let e=N(t,"--port","GSC_MCP_PORT")??"3000",r=parseInt(e,10);if(Number.isNaN(r)||r<1||r>65535)return{error:`Invalid port: ${e}`};let n=N(t,"--host","GSC_MCP_HOST")??_;return!n||n.includes("/")||n.includes(" ")?{error:`Invalid host: ${n}`}:{transport:o,host:n,port:r,showHelp:!1,showVersion:!1}}b();async function W(t,o){let e=[],r=0;for await(let n of t){let a=n;if(r+=a.length,r>o)return{ok:!1,status:413,jsonRpcMessage:"Payload too large"};e.push(a)}try{return{ok:!0,body:JSON.parse(Buffer.concat(e).toString("utf8"))}}catch{return{ok:!1,status:400,jsonRpcMessage:"Parse error"}}}I();function E(t,o,e){return{status:t,body:JSON.stringify({jsonrpc:"2.0",error:{code:o,message:e},id:null})}}function yt(t){t.setHeader("X-Content-Type-Options","nosniff")}function _t(t){return t==="0.0.0.0"||t==="::"||t==="[::]"}async function z(t,o){let e=t.host??_,r=t.port,n=await import("@modelcontextprotocol/sdk/server/streamableHttp.js"),a=await import("node:http"),{randomUUID:m}=await import("node:crypto"),{isInitializeRequest:T}=await import("@modelcontextprotocol/sdk/types.js"),c={},w=a.createServer(async(u,i)=>{if(yt(i),new URL(u.url??"/",`http://${e}:${r}`).pathname!=="/mcp"){i.writeHead(404,{"Content-Type":"application/json"}),i.end(JSON.stringify({error:"Not found. Use /mcp"}));return}let h=u.headers["mcp-session-id"];if(u.method==="POST"){let x=await W(u,4194304);if(!x.ok){let p=x.status===413?E(413,-32e3,x.jsonRpcMessage):E(400,-32700,x.jsonRpcMessage);i.writeHead(p.status,{"Content-Type":"application/json"}),i.end(p.body);return}let F=x.body;try{let p;if(h&&c[h])p=c[h];else if(!h&&T(F)){if(Object.keys(c).length>=32){let f=E(503,-32e3,"Too many active sessions");i.writeHead(f.status,{"Content-Type":"application/json"}),i.end(f.body);return}p=new n.StreamableHTTPServerTransport({sessionIdGenerator:()=>m(),onsessioninitialized:f=>{c[f]=p}}),p.onclose=()=>{let f=p.sessionId;f&&c[f]&&delete c[f]},await o().connect(p)}else{let y=E(400,-32e3,"Bad Request: No valid session ID provided");i.writeHead(y.status,{"Content-Type":"application/json"}),i.end(y.body);return}await p.handleRequest(u,i,F)}catch(p){if(console.error("[searchconsole-mcp] Error handling MCP request:",S(p)),!i.headersSent){let y=E(500,-32603,"Internal server error");i.writeHead(y.status,{"Content-Type":"application/json"}),i.end(y.body)}}return}if(u.method==="GET"||u.method==="DELETE"){if(!h||!c[h]){i.writeHead(400,{"Content-Type":"text/plain"}),i.end("Invalid or missing session ID");return}await c[h].handleRequest(u,i);return}i.writeHead(405,{"Content-Type":"text/plain"}),i.end("Method not allowed")});w.listen(r,e,()=>{console.error(`[searchconsole-mcp] Streamable HTTP server listening on http://${e}:${r}/mcp`),_t(e)&&console.error("[searchconsole-mcp] WARNING: HTTP is bound to all interfaces. Anyone on the network can use your Google credentials via MCP.")});let L=async()=>{console.error("[searchconsole-mcp] Shutting down HTTP server...");for(let u of Object.keys(c)){try{await c[u].close()}catch{}delete c[u]}w.close(),process.exit(0)};process.on("SIGTERM",L),process.on("SIGINT",L)}I();var{writeStdout:ut,writeStderr:kt}=B(),It=process.argv.slice(2),j=q(It);X(j)&&(kt(Buffer.from(`[searchconsole-mcp] Error: ${j.error}
36
+ `)),process.exit(1));var R=j;R.showHelp&&(J(t=>ut(Buffer.from(t))),process.exit(0));R.showVersion&&(ut(Buffer.from(v+`
37
+ `)),process.exit(0));async function vt(){let t=await import("@modelcontextprotocol/sdk/server/mcp.js"),o=await import("@modelcontextprotocol/sdk/server/stdio.js"),e=await import("google-auth-library"),r=await Promise.resolve().then(()=>(lt(),pt)),n=new e.GoogleAuth({scopes:[G]});function a(){let w=new t.McpServer({name:V,version:v});return r.registerGscTools(w,n),w}if(R.transport==="http"){await z({host:R.host,port:R.port},a);return}let m=a(),T=new o.StdioServerTransport;await m.connect(T),console.error("[searchconsole-mcp] Server started, waiting for connections...");let c=()=>{console.error("[searchconsole-mcp] Shutting down..."),m.close().then(()=>process.exit(0))};process.on("SIGTERM",c),process.on("SIGINT",c)}process.on("uncaughtException",t=>{console.error("[searchconsole-mcp] Uncaught exception:",S(t))});process.on("unhandledRejection",t=>{console.error("[searchconsole-mcp] Unhandled rejection:",S(t))});vt().catch(t=>{console.error("[searchconsole-mcp] Fatal error:",S(t)),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmandic/searchconsole-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Read-only Google Search Console MCP server for Cursor and other MCP clients.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,6 +49,7 @@
49
49
  "dependencies": {
50
50
  "@googleapis/searchconsole": "^6.0.1",
51
51
  "@modelcontextprotocol/sdk": "^1.27.1",
52
+ "@toon-format/toon": "^2.3.0",
52
53
  "google-auth-library": "^10.6.2",
53
54
  "zod": "^3.25.0"
54
55
  },