@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.
- package/README.md +105 -29
- package/dist/server.js +10 -7
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Search Console MCP
|
|
2
2
|
|
|
3
3
|
[](https://github.com/vmandic/searchconsole-mcp/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/vmandic/searchconsole-mcp/releases/latest)
|
|
5
|
+
[](https://www.npmjs.com/package/@vmandic/searchconsole-mcp)
|
|
4
6
|
[](LICENSE)
|
|
5
7
|
[](package.json)
|
|
6
8
|
[](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
|
-
|
|
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
|
-
|
|
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 —
|
|
263
|
+
### Option C — Global CLI from a local build
|
|
227
264
|
|
|
228
|
-
|
|
265
|
+
From a clone after `npm run build`:
|
|
229
266
|
|
|
230
267
|
```bash
|
|
231
|
-
|
|
268
|
+
npm link -g
|
|
269
|
+
searchconsole-mcp --help
|
|
232
270
|
```
|
|
233
271
|
|
|
234
|
-
|
|
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.
|
|
313
|
-
2.
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
|
3
|
-
|
|
4
|
-
`))
|
|
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=${
|
|
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
|
|
33
|
-
`)),process.exit(1));var
|
|
34
|
-
`)),process.exit(0));async function
|
|
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.
|
|
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
|
},
|