gsclaw 0.0.0-stage → 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anton Tuyakhov and GSClaw contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,177 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="assets/logo.svg" width="88" height="88" alt="">
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ <h1 align="center">GSClaw</h1>
6
+
7
+ <p align="center"><strong>Sink your claws into your Search Console data.</strong></p>
8
+
9
+ <p align="center">
10
+ An open-source MCP server that gives Claude, ChatGPT, Cursor and other AI assistants first-party
11
+ access to Google Search Console. Deploy it in one click, connect it from claude.ai, and ask your
12
+ SEO questions in plain language.
13
+ </p>
14
+
15
+ <p align="center">
16
+ <a href="https://github.com/tuyakhov/gsclaw/actions/workflows/ci.yml"><img src="https://github.com/tuyakhov/gsclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
17
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-0d9488" alt="MIT license"></a>
18
+ <a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-remote%20%2B%20stdio-7c3aed" alt="MCP: remote and stdio"></a>
19
+ </p>
20
+
21
+ <p align="center">
22
+ <picture>
23
+ <source media="(prefers-color-scheme: dark)" srcset="docs/images/overview-dark.png">
24
+ <img src="docs/images/overview.png" width="860" alt="The GSClaw dashboard: clicks, impressions, CTR and position with a daily trend chart and top queries">
25
+ </picture>
26
+ </p>
27
+
28
+ ## Ask your AI things like
29
+
30
+ - _"Which queries are stuck on page two with lots of impressions, and what should I change on each
31
+ page?"_
32
+ - _"Compare the last 28 days with the same period last year. What drove the difference?"_
33
+ - _"Where do two of my pages compete for the same query?"_
34
+ - _"Which blog posts have been losing clicks for three months straight?"_
35
+ - _"Is /pricing indexed? Which canonical did Google choose, and when was it last crawled?"_
36
+ - _"Run a full SEO health check on my site."_
37
+
38
+ ## Why GSClaw
39
+
40
+ - **Works where you already chat.** A remote MCP endpoint over HTTPS for claude.ai (web, desktop
41
+ and mobile), ChatGPT, Claude Code, Cursor and VS Code, plus local stdio.
42
+ - **Answers, not raw rows.** Analysis tools find striking-distance keywords, CTR gaps,
43
+ cannibalization and decaying content, and compare periods, returning compact results that fit
44
+ in your AI's context.
45
+ - **Your data stays yours.** GSClaw runs on your own account and talks straight to Google's API.
46
+ There is no third-party service and no database; it stores nothing.
47
+ - **One-click deploys.** Vercel, Netlify, Cloudflare Workers, Render, Railway, DigitalOcean, or
48
+ any Docker host.
49
+ - **A dashboard included.** See the same numbers your AI sees, find opportunities, and copy
50
+ connection snippets.
51
+ - **Secure by default.** Refuses to start without authentication, signs clients in with OAuth
52
+ 2.1, and stays read-only unless you enable writes.
53
+
54
+ ## Get started
55
+
56
+ 1. **Create a Google service account** and add its email to your Search Console properties
57
+ ([setup guide](docs/setup.md), about 10 minutes).
58
+ 2. **Deploy** with a button and paste two values: `GOOGLE_SERVICE_ACCOUNT_JSON` (the key) and
59
+ `GSCLAW_ACCESS_TOKEN` (a long random secret; Render and Railway generate it for you).
60
+
61
+ [![Deploy with Vercel](https://vercel.com/button)](<https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ftuyakhov%2Fgsclaw&env=GOOGLE_SERVICE_ACCOUNT_JSON,GSCLAW_ACCESS_TOKEN&envDescription=Service-account%20key%20JSON%20and%20a%20long%20random%20access%20token%20(openssl%20rand%20-hex%2032)&envLink=https%3A%2F%2Fgithub.com%2Ftuyakhov%2Fgsclaw%2Fblob%2Fmain%2Fdocs%2Fsetup.md&project-name=gsclaw&repository-name=gsclaw>)
62
+ [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/tuyakhov/gsclaw)
63
+ [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/tuyakhov/gsclaw)
64
+ [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/tuyakhov/gsclaw)
65
+ [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/gsclaw?utm_medium=integration&utm_source=button&utm_campaign=gsclaw)
66
+ [![Deploy to DigitalOcean](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/tuyakhov/gsclaw/tree/main)
67
+
68
+ Or run the Docker image anywhere:
69
+
70
+ ```bash
71
+ docker run -p 3000:3000 \
72
+ -e GOOGLE_SERVICE_ACCOUNT_JSON="$(cat service-account.json)" \
73
+ -e GSCLAW_ACCESS_TOKEN="$(openssl rand -hex 32)" \
74
+ ghcr.io/tuyakhov/gsclaw
75
+ ```
76
+
77
+ Platform notes (free tiers, timeouts, updates): [deploy guide](docs/deploy/README.md).
78
+
79
+ 3. **Connect your AI.** Open your deployment, sign in with the access token, and copy a snippet
80
+ from the **Connect** page. For claude.ai: Settings → Connectors → Add custom connector → paste
81
+ `https://<your-deployment>/mcp` → Connect, then approve with your access token.
82
+
83
+ Several people with different Search Console access? Use [Google OAuth mode](docs/oauth.md)
84
+ instead: everyone signs in with their own Google account and only sees their own properties.
85
+
86
+ ## Connect any client
87
+
88
+ | Client | How |
89
+ | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- |
90
+ | claude.ai, Claude Desktop & mobile | Settings → Connectors → Add custom connector → `https://<your-deployment>/mcp` |
91
+ | Claude Code | `claude mcp add --transport http gsclaw https://<your-deployment>/mcp`, then `/mcp` to sign in |
92
+ | Cursor | `{ "mcpServers": { "gsclaw": { "url": "https://<your-deployment>/mcp" } } }` in `~/.cursor/mcp.json` |
93
+ | VS Code | `{ "servers": { "gsclaw": { "type": "http", "url": "https://<your-deployment>/mcp" } } }` in `.vscode/mcp.json` |
94
+ | ChatGPT | Add a custom connector (developer mode) with the same URL and OAuth |
95
+ | Local (stdio) | `claude mcp add gsclaw -e GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json -- npx -y gsclaw` |
96
+
97
+ Each remote client opens a GSClaw sign-in page the first time: enter your access token, or sign
98
+ in with Google in OAuth mode. Clients that prefer headers can send
99
+ `Authorization: Bearer <token>` instead; the dashboard's Connect page has every variant.
100
+
101
+ ## Tools
102
+
103
+ | Tool | What it does |
104
+ | ------------------------------------ | ------------------------------------------------------------------------------------------- |
105
+ | `list_sites` | Properties the server can access, with permission levels |
106
+ | `search_analytics` | Raw performance query: any dimensions, filters (incl. regex), search types, auto-pagination |
107
+ | `compare_periods` | Current vs previous period or year over year, with top gainers and losers |
108
+ | `striking_distance_keywords` | Queries at positions ~8–20 with meaningful impressions, plus potential clicks |
109
+ | `ctr_opportunities` | High-impression rows with CTR well below the expected CTR for their position |
110
+ | `keyword_cannibalization` | Queries where several of your pages split impressions |
111
+ | `content_decay` | Pages with sustained click decline across consecutive windows |
112
+ | `page_report` | One page: totals vs previous period, trend, top queries and index status |
113
+ | `inspect_url` / `batch_inspect_urls` | URL Inspection: index status, canonicals, last crawl, rich results |
114
+ | `list_sitemaps` / `get_sitemap` | Sitemap status, errors, warnings and submitted counts |
115
+ | `submit_sitemap` / `delete_sitemap` | Write tools, only when `GSCLAW_ALLOW_WRITES=true` |
116
+
117
+ Prompt: `seo_health_check` runs a full property review with these tools. Every tool returns
118
+ readable text plus structured data, and carries MCP annotations that tell clients which tools only
119
+ read.
120
+
121
+ ## Dashboard
122
+
123
+ Every deployment serves a small dashboard at `/` (turn it off with `GSCLAW_DASHBOARD=false`). It
124
+ calls the exact same tool functions as the MCP server, so its numbers always match what your AI
125
+ sees, and every table exports to CSV.
126
+
127
+ <table>
128
+ <tr>
129
+ <td width="50%"><img src="docs/images/opportunities.png" alt="Opportunities: striking-distance keywords with potential clicks and Ask AI buttons"></td>
130
+ <td width="50%"><img src="docs/images/connect.png" alt="Connect: copy-paste snippets for claude.ai, Claude Code, Cursor and more"></td>
131
+ </tr>
132
+ <tr>
133
+ <td><strong>Opportunities.</strong> Striking distance, CTR gaps, cannibalization and content decay. "Ask AI" copies a ready-made prompt.</td>
134
+ <td><strong>Connect.</strong> Snippets for every client, with secrets hidden until you reveal them.</td>
135
+ </tr>
136
+ <tr>
137
+ <td><img src="docs/images/setup.png" alt="Setup and health: live Google connection check and visible properties"></td>
138
+ <td><img src="docs/images/overview-dark.png" alt="The Overview page in dark mode"></td>
139
+ </tr>
140
+ <tr>
141
+ <td><strong>Setup & health.</strong> A live Google check, which properties are visible, and what to fix.</td>
142
+ <td><strong>Light and dark, desktop and phone.</strong> Plus Indexing (sitemaps, URL Inspection) and Activity (recent tool calls).</td>
143
+ </tr>
144
+ </table>
145
+
146
+ ## Authentication modes
147
+
148
+ GSClaw never runs open: it refuses to start until one of these is configured.
149
+
150
+ | | Service account (default) | Google OAuth |
151
+ | -------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
152
+ | Who sees what | Everyone you give the token to sees the service account's properties | Each person sees the properties their own Google account can access |
153
+ | Clients sign in with | The access token (OAuth sign-in page, bearer header or secret URL) | Their Google account |
154
+ | Best for | You, or a team sharing the same properties | Agencies and teams where people have different access |
155
+ | Required variables | `GOOGLE_SERVICE_ACCOUNT_JSON`, `GSCLAW_ACCESS_TOKEN` | `GOOGLE_OAUTH_CLIENT_ID`, `GOOGLE_OAUTH_CLIENT_SECRET`, `GSCLAW_ENCRYPTION_KEY`, `PUBLIC_BASE_URL` |
156
+ | Guide | [Setup guide](docs/setup.md) | [Google OAuth mode](docs/oauth.md) |
157
+
158
+ Every setting is documented in [.env.example](.env.example).
159
+
160
+ ## Documentation
161
+
162
+ - [Setup guide](docs/setup.md): Google Cloud, service account, first deploy
163
+ - [Google OAuth mode](docs/oauth.md): multi-user sign-in with Google
164
+ - [Deploy guide](docs/deploy/README.md): per-platform notes and updating
165
+ - [Troubleshooting](docs/troubleshooting.md): common errors and fixes
166
+ - [Architecture](docs/ARCHITECTURE.md): how it's built and why
167
+ - [Security](SECURITY.md): security model and how to report a vulnerability
168
+
169
+ ## Contributing
170
+
171
+ Issues and pull requests are welcome. `pnpm build && pnpm demo` runs the server and dashboard on
172
+ synthetic data with no Google account. See [CONTRIBUTING.md](CONTRIBUTING.md).
173
+
174
+ ## License
175
+
176
+ [MIT](LICENSE). GSClaw is an independent project, not affiliated with or endorsed by Google or
177
+ OpenClaw. Google Search Console is a trademark of Google LLC.
@@ -0,0 +1,15 @@
1
+ import { t as createApp } from "../app-DdGjQNXR.js";
2
+ //#region src/adapters/netlify.ts
3
+ let app;
4
+ function readEnv() {
5
+ return globalThis.Netlify?.env?.toObject?.() ?? process.env;
6
+ }
7
+ /** Netlify Functions (v2) handler; `netlify/functions/gsclaw.mjs` re-exports it with its route config. */
8
+ function netlifyFetch(request) {
9
+ app ??= createApp(readEnv(), { name: "netlify" });
10
+ return app.fetch(request);
11
+ }
12
+ //#endregion
13
+ export { netlifyFetch };
14
+
15
+ //# sourceMappingURL=netlify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"netlify.js","names":[],"sources":["../../src/adapters/netlify.ts"],"sourcesContent":["import { createApp, type App } from '../core/app.js';\nimport type { Env } from '../core/config.js';\n\ninterface NetlifyGlobal {\n env?: { toObject?: () => Record<string, string> };\n}\n\nlet app: App | undefined;\n\nfunction readEnv(): Env {\n const netlify = (globalThis as { Netlify?: NetlifyGlobal }).Netlify;\n return netlify?.env?.toObject?.() ?? process.env;\n}\n\n/** Netlify Functions (v2) handler; `netlify/functions/gsclaw.mjs` re-exports it with its route config. */\nexport function netlifyFetch(request: Request): Promise<Response> {\n app ??= createApp(readEnv(), { name: 'netlify' });\n return app.fetch(request);\n}\n"],"mappings":";;AAOA,IAAI;AAEJ,SAAS,UAAe;CAEtB,OADiB,WAA2C,SAC5C,KAAK,WAAW,KAAK,QAAQ;AAC/C;;AAGA,SAAgB,aAAa,SAAqC;CAChE,QAAQ,UAAU,QAAQ,GAAG,EAAE,MAAM,UAAU,CAAC;CAChD,OAAO,IAAI,MAAM,OAAO;AAC1B"}
@@ -0,0 +1,2 @@
1
+ import { n as startNodeServer, t as SetupError } from "../node-http-DW9OenCi.js";
2
+ export { SetupError, startNodeServer };
@@ -0,0 +1,37 @@
1
+ import { c as stderrWriter, n as createRuntime, o as formatConfigErrors, r as buildMcpServer, s as loadConfig } from "../app-DdGjQNXR.js";
2
+ import { t as SetupError } from "../node-http-DW9OenCi.js";
3
+ import { readFileSync } from "node:fs";
4
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
5
+ //#region src/adapters/stdio.ts
6
+ /**
7
+ * Local stdio server for Claude Code, Cursor, Claude Desktop, etc. No access token is needed: the
8
+ * client launches this process itself. The key can also come from a file via
9
+ * GOOGLE_APPLICATION_CREDENTIALS.
10
+ */
11
+ async function runStdio(opts = {}) {
12
+ const env = { ...opts.env ?? process.env };
13
+ if (!env.GOOGLE_SERVICE_ACCOUNT_JSON && env.GOOGLE_APPLICATION_CREDENTIALS) try {
14
+ env.GOOGLE_SERVICE_ACCOUNT_JSON = readFileSync(env.GOOGLE_APPLICATION_CREDENTIALS, "utf8");
15
+ } catch (error) {
16
+ throw new SetupError(`Could not read GOOGLE_APPLICATION_CREDENTIALS (${env.GOOGLE_APPLICATION_CREDENTIALS}): ${error.message}`);
17
+ }
18
+ const loaded = loadConfig(env, { transport: "stdio" });
19
+ if (!loaded.ok) throw new SetupError(formatConfigErrors(loaded.errors));
20
+ const runtime = createRuntime(loaded.config, {
21
+ fetch: opts.fetch,
22
+ logWriter: stderrWriter,
23
+ logFields: { transport: "stdio" }
24
+ });
25
+ for (const warning of loaded.warnings) runtime.logger.warn(warning);
26
+ serveStdio(() => buildMcpServer({
27
+ tools: runtime.owner.tools,
28
+ ctx: runtime.owner.ctx,
29
+ logger: runtime.logger,
30
+ activity: runtime.activity,
31
+ client: "stdio"
32
+ }));
33
+ }
34
+ //#endregion
35
+ export { runStdio };
36
+
37
+ //# sourceMappingURL=stdio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio.js","names":[],"sources":["../../src/adapters/stdio.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { serveStdio } from '@modelcontextprotocol/server/stdio';\nimport { formatConfigErrors, loadConfig, type Env } from '../core/config.js';\nimport { stderrWriter } from '../core/log.js';\nimport { buildMcpServer } from '../core/mcp.js';\nimport { createRuntime } from '../core/runtime.js';\nimport { SetupError } from './node-http.js';\n\n/**\n * Local stdio server for Claude Code, Cursor, Claude Desktop, etc. No access token is needed: the\n * client launches this process itself. The key can also come from a file via\n * GOOGLE_APPLICATION_CREDENTIALS.\n */\nexport async function runStdio(opts: { env?: Env; fetch?: typeof fetch } = {}): Promise<void> {\n const env = { ...(opts.env ?? process.env) };\n if (!env.GOOGLE_SERVICE_ACCOUNT_JSON && env.GOOGLE_APPLICATION_CREDENTIALS) {\n try {\n env.GOOGLE_SERVICE_ACCOUNT_JSON = readFileSync(env.GOOGLE_APPLICATION_CREDENTIALS, 'utf8');\n } catch (error) {\n throw new SetupError(\n `Could not read GOOGLE_APPLICATION_CREDENTIALS (${env.GOOGLE_APPLICATION_CREDENTIALS}): ${(error as Error).message}`,\n );\n }\n }\n\n const loaded = loadConfig(env, { transport: 'stdio' });\n if (!loaded.ok) throw new SetupError(formatConfigErrors(loaded.errors));\n\n // stdout carries the protocol; logs must go to stderr.\n const runtime = createRuntime(loaded.config, {\n fetch: opts.fetch,\n logWriter: stderrWriter,\n logFields: { transport: 'stdio' },\n });\n for (const warning of loaded.warnings) runtime.logger.warn(warning);\n\n serveStdio(() =>\n buildMcpServer({\n tools: runtime.owner!.tools,\n ctx: runtime.owner!.ctx,\n logger: runtime.logger,\n activity: runtime.activity,\n client: 'stdio',\n }),\n );\n}\n"],"mappings":";;;;;;;;;;AAaA,eAAsB,SAAS,OAA4C,CAAC,GAAkB;CAC5F,MAAM,MAAM,EAAE,GAAI,KAAK,OAAO,QAAQ,IAAK;CAC3C,IAAI,CAAC,IAAI,+BAA+B,IAAI,gCAC1C,IAAI;EACF,IAAI,8BAA8B,aAAa,IAAI,gCAAgC,MAAM;CAC3F,SAAS,OAAO;EACd,MAAM,IAAI,WACR,kDAAkD,IAAI,+BAA+B,KAAM,MAAgB,SAC7G;CACF;CAGF,MAAM,SAAS,WAAW,KAAK,EAAE,WAAW,QAAQ,CAAC;CACrD,IAAI,CAAC,OAAO,IAAI,MAAM,IAAI,WAAW,mBAAmB,OAAO,MAAM,CAAC;CAGtE,MAAM,UAAU,cAAc,OAAO,QAAQ;EAC3C,OAAO,KAAK;EACZ,WAAW;EACX,WAAW,EAAE,WAAW,QAAQ;CAClC,CAAC;CACD,KAAK,MAAM,WAAW,OAAO,UAAU,QAAQ,OAAO,KAAK,OAAO;CAElE,iBACE,eAAe;EACb,OAAO,QAAQ,MAAO;EACtB,KAAK,QAAQ,MAAO;EACpB,QAAQ,QAAQ;EAChB,UAAU,QAAQ;EAClB,QAAQ;CACV,CAAC,CACH;AACF"}
@@ -0,0 +1,15 @@
1
+ import { t as createApp } from "../app-DdGjQNXR.js";
2
+ //#region src/adapters/vercel.ts
3
+ let app;
4
+ /**
5
+ * Vercel Function (Node.js runtime, Fluid Compute) using the web-standard `fetch` export.
6
+ * `api/index.js` re-exports this, and vercel.json rewrites every path to it.
7
+ */
8
+ var vercel_default = { fetch(request) {
9
+ app ??= createApp(process.env, { name: "vercel" });
10
+ return app.fetch(request);
11
+ } };
12
+ //#endregion
13
+ export { vercel_default as default };
14
+
15
+ //# sourceMappingURL=vercel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vercel.js","names":[],"sources":["../../src/adapters/vercel.ts"],"sourcesContent":["import { createApp, type App } from '../core/app.js';\n\nlet app: App | undefined;\n\n/**\n * Vercel Function (Node.js runtime, Fluid Compute) using the web-standard `fetch` export.\n * `api/index.js` re-exports this, and vercel.json rewrites every path to it.\n */\nexport default {\n fetch(request: Request): Promise<Response> {\n app ??= createApp(process.env, { name: 'vercel' });\n return app.fetch(request);\n },\n};\n"],"mappings":";;AAEA,IAAI;;;;;AAMJ,IAAA,iBAAe,EACb,MAAM,SAAqC;CACzC,QAAQ,UAAU,QAAQ,KAAK,EAAE,MAAM,SAAS,CAAC;CACjD,OAAO,IAAI,MAAM,OAAO;AAC1B,EACF"}