@vmandic/searchconsole-mcp 1.0.1 → 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 CHANGED
@@ -64,6 +64,8 @@ Use it when you want:
64
64
 
65
65
  **Input validation** — Tool arguments are validated with Zod (dates, URLs, row limits, allowlisted dimensions).
66
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
+
67
69
  **Clear errors** — Failures return MCP text with `isError: true` and actionable messages (auth, `site_url` format, quota).
68
70
 
69
71
  ---
@@ -681,8 +683,11 @@ searchconsole-mcp [--transport stdio|http] [--host <addr>] [--port <n>] [--versi
681
683
  | `--transport` / `GSC_MCP_TRANSPORT` | `stdio` | `stdio` or `http` |
682
684
  | `--host` / `GSC_MCP_HOST` | `127.0.0.1` | HTTP bind address |
683
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 |
684
687
  | `GOOGLE_APPLICATION_CREDENTIALS` | — | Path to service account JSON |
685
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
+
686
691
  ### Smithery
687
692
 
688
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`.
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.1"});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.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
  },