@digestseo/mcp-gsc 0.3.1

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.
@@ -0,0 +1,8 @@
1
+ # Local development secrets for `wrangler dev`.
2
+ # Copy this file to `.dev.vars` (gitignored) and fill in your own values.
3
+ # In production these are set with `wrangler secret put <NAME>` instead.
4
+ # See SETUP.md for how to obtain each value.
5
+
6
+ GOOGLE_CLIENT_ID=
7
+ GOOGLE_CLIENT_SECRET=
8
+ TOKEN_ENCRYPTION_KEY=
package/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] - 2026-06-10
9
+
10
+ ### Added
11
+ - `get_capabilities` tool — returns the tool catalog and a non-destructive auth-status probe for better first-run discovery in lazy-loading clients.
12
+ - **Pagination** for `query_search_analytics`: new `start_row` parameter (Google's `startRow`), plus a `next_start_row` field in the response whenever a full page came back.
13
+ - **Parameter descriptions on every tool input** — including the `sc-domain:` vs URL-prefix property format trap, the YYYY-MM-DD date format, and the 2–3 day GSC data lag — so MCP clients make correct calls on the first try.
14
+ - **MCP tool annotations**: all five tools now declare `readOnlyHint: true` and human-readable titles.
15
+ - **Offline unit tests** (`npm run test:unit`, 15 tests): crypto roundtrip/tamper checks, OAuth URL construction, refresh-token error mapping, and Search Console request encoding — no deployment needed.
16
+ - **GitHub Actions CI**: typecheck + unit tests on every push and pull request.
17
+ - `SECURITY.md` with a private-reporting channel and a full data-handling inventory.
18
+
19
+ ### Changed
20
+ - `query_search_analytics` now returns compact JSON shaped as `{ row_count, start_row, rows, next_start_row? }` instead of a pretty-printed bare array — roughly 3× fewer tokens for the same data.
21
+ - Default `row_limit` lowered from 1000 to **100**. Most conversational questions need far fewer rows; bulk consumers can still request up to 25,000 and paginate.
22
+ - Input validation tightened: dates must be `YYYY-MM-DD`, `row_limit` must be an integer in 1–25000.
23
+ - Error messages no longer hardcode Claude.ai — they now say "your MCP client (e.g. Claude.ai → Settings → Connectors)".
24
+ - The version reported by the MCP server and `get_capabilities` is now read from `package.json` (single source of truth).
25
+ - A failure inside the OAuth provider's `completeAuthorization` now returns a clean 500 instead of an unhandled exception.
26
+
27
+ ### Fixed
28
+ - Server name: the MCP server now identifies as `mcp-gsc` (in the `McpServer` name and the `GET /` response body) to match the repo, package, and worker name — it was previously `hosted-gsc-mcp`.
29
+ - A transient Google error no longer deletes a user's stored credentials. `refreshAccessToken` now treats only a definitive `invalid_grant` as a revocation; other non-OK responses (bare 400/401, 5xx, network) propagate as plain errors, so a temporary blip can no longer trigger `deleteUser`.
30
+
31
+ ## [0.1.0] - 2026-06-02
32
+
33
+ Initial public release — a hosted MCP server for Google Search Console, self-hostable on Cloudflare Workers.
34
+
35
+ ### Added
36
+ - One-click Google OAuth onboarding (`/authorize` → Google → `/google/callback`): connect with your Google account, no API keys to copy.
37
+ - Four read-only tools: `list_sites`, `query_search_analytics`, `inspect_url`, `list_sitemaps`.
38
+ - Bring-your-own Google OAuth credentials and Cloudflare KV — see [SETUP.md](SETUP.md).
39
+ - AES-GCM encryption of stored Google refresh tokens (`src/crypto.ts`).
40
+ - Structural smoke tests (`npm run test:smoke`).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DigestSEO
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 ADDED
@@ -0,0 +1,139 @@
1
+ # mcp-gsc
2
+
3
+ <!-- Logo asset for marketplace submissions: assets/logo-400.png (400x400). -->
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+ [![MCP](https://img.shields.io/badge/MCP-remote%20server-8A2BE2)](https://modelcontextprotocol.io)
7
+ [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-io.github.AKzar1el%2Fmcp--gsc-blue)](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.AKzar1el/mcp-gsc)
8
+ [![CI](https://github.com/AKzar1el/mcp-gsc/actions/workflows/ci.yml/badge.svg)](https://github.com/AKzar1el/mcp-gsc/actions/workflows/ci.yml)
9
+ [![npm version](https://img.shields.io/npm/v/%40digestseo%2Fmcp-gsc.svg)](https://www.npmjs.com/package/@digestseo/mcp-gsc)
10
+ [![smithery badge](https://smithery.ai/badge/digestseo/mcp-gsc)](https://smithery.ai/servers/digestseo/mcp-gsc)
11
+
12
+ Part of the **[DigestSEO](https://digestseo.com/)** suite of open-source SEO tools.
13
+
14
+ - Product: [digestseo.com/gsc-mcp](https://digestseo.com/gsc-mcp/)
15
+ - Support: [digestseo.com/support](https://digestseo.com/support/)
16
+ - Privacy: [digestseo.com/privacy](https://digestseo.com/privacy/)
17
+
18
+ The self-hosting launcher is available as [`@digestseo/mcp-gsc`](https://www.npmjs.com/package/@digestseo/mcp-gsc):
19
+
20
+ ```bash
21
+ npx -y @digestseo/mcp-gsc
22
+ ```
23
+
24
+ For Google OAuth and Cloudflare deployment configuration, follow [SETUP.md](SETUP.md).
25
+
26
+ ### Connect in 30 seconds
27
+
28
+ Every deployed instance exposes the same endpoint shape:
29
+
30
+ ```
31
+ https://<your-worker>.workers.dev/mcp
32
+ ```
33
+
34
+ The DigestSEO-hosted instance is available at:
35
+
36
+ ```
37
+ https://mcp-gsc.digestseo.com/mcp
38
+ ```
39
+
40
+ **Claude Code**
41
+
42
+ ```bash
43
+ claude mcp add --transport http gsc https://<your-worker>.workers.dev/mcp
44
+ ```
45
+
46
+ **Cursor**
47
+
48
+ [![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=gsc&config=eyJ1cmwiOiJodHRwczovL21jcC1nc2MuZGlnZXN0c2VvLmNvbS9tY3AifQ%3D%3D)
49
+
50
+ **ChatGPT** — add it as a custom connector; see [Connect in your AI client](#connect-in-your-ai-client) below.
51
+
52
+ A self-hostable [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **Google Search Console**. Connect it to Claude.ai, Cursor, ChatGPT, or any MCP-compatible client and ask your AI assistant about your site's organic search performance — impressions, clicks, top queries, index status, and sitemap health — straight from your own Google account.
53
+
54
+ It runs on [Cloudflare Workers](https://workers.cloudflare.com/) and ships with one-click Google OAuth onboarding: connect the server in your client, sign in with Google once, grant the requested Google Search Console permissions, and you're done. No API keys to copy around and no service-account JSON to manage.
55
+
56
+ > **Prefer zero setup?** The hosted version — with automatic weekly email digests delivered to your inbox — is at **[digestseo.com](https://digestseo.com)**. This repository is the open-source core you can run yourself.
57
+
58
+ ## Tools
59
+
60
+ This server exposes 17 tools. Read-only analytics and reporting tools are marked with MCP's `readOnlyHint`; the write tools below can change Search Console properties, sitemaps, or indexing state.
61
+
62
+ | Tool | Access | What it does |
63
+ |---|---|---|
64
+ | **`server.capabilities`** | Read | List every tool this server exposes and report whether your Google connection is currently authenticated (`connected` / `not_connected`). Takes no arguments — a good first call for discovery. |
65
+ | **`sites.list`** | Read | List the Search Console properties the connected Google account can access (`siteUrl`, `permissionLevel`). |
66
+ | **`analytics.query`** | Read | Impressions, clicks, CTR, and average position over a date range, with dimensions, filters, pagination, and selectable search type. |
67
+ | **`urls.inspect`** | Read | Google's URL Inspection report for a single page. |
68
+ | **`sitemaps.list`** / **`sitemaps.get`** | Read | List submitted sitemaps or retrieve one sitemap's details. |
69
+ | **`insights.quick_wins`** / **`insights.cannibalization`** / **`insights.content_decay`** | Read | Surface optimization opportunities, competing pages, and declining content. |
70
+ | **`indexing.list_pages`** / **`analytics.compare`** | Read | Analyze pages receiving impressions and compare two periods. |
71
+ | **`reports.weekly_digest`** | Read | Generate a plain-language seven-day performance report with movers, top pages, and one recommended action. |
72
+ | **`sites.add`** / **`sites.delete`** | Write | Add or remove a Search Console property. |
73
+ | **`sitemaps.submit`** / **`sitemaps.delete`** | Write | Submit or remove a sitemap. |
74
+ | **`indexing.request`** | Write | Request indexing for an eligible URL through the Google Indexing API. |
75
+
76
+ The server requests Google Search Console and Indexing API scopes. Use a Google account with only the property access you intend to delegate, and review write-tool calls before approving them.
77
+
78
+ ## What you can ask
79
+
80
+ Once connected, ask your assistant things like:
81
+
82
+ - *"What are my top 20 queries by clicks in the last 28 days?"*
83
+ - *"Compare impressions for example.com this month vs last month — which pages dropped?"*
84
+ - *"Is `https://example.com/pricing` indexed? When was it last crawled?"*
85
+ - *"Which queries does my blog rank position 5–15 for? Those are my quick wins."*
86
+ - *"Give me a weekly digest for `sc-domain:example.com` ending today."*
87
+ - *"Do any of my sitemaps have errors or warnings?"*
88
+ - *"Split my clicks into brand vs non-brand using a regex on the query."*
89
+ - *"How is my site doing in Google Discover vs regular web search?"*
90
+
91
+ ## Connect in your AI client
92
+
93
+ Once you've deployed the server (see **[SETUP.md](SETUP.md)**), connect it by pasting your Worker's `/mcp` URL into your client:
94
+
95
+ ```
96
+ https://<your-worker>.workers.dev/mcp
97
+ ```
98
+
99
+ - **Claude.ai** — Settings → Connectors → **Add custom connector** → paste the `/mcp` URL. Leave Client ID and Client Secret blank. On first use, Claude opens a Google sign-in flow; grant read access and the connector turns green.
100
+ - **Cursor** — add it as a custom MCP server pointing at the same `/mcp` URL.
101
+ - **ChatGPT** (with connector/MCP support) — add a custom connector with the `/mcp` URL.
102
+
103
+ Any MCP-compatible client works — they all point at the same `/mcp` endpoint and share the same OAuth flow.
104
+
105
+ ## Setup
106
+
107
+ Self-hosting means bringing your own Google OAuth credentials and Cloudflare account. The full, copy-pasteable walkthrough is in **[SETUP.md](SETUP.md)** — including an important note about Google's OAuth verification and the 7-day refresh-token limit while your app is unverified.
108
+
109
+ Quick shape:
110
+
111
+ ```bash
112
+ git clone https://github.com/<you>/mcp-gsc.git
113
+ cd mcp-gsc
114
+ npm install
115
+ cp wrangler.example.jsonc wrangler.jsonc # then paste in your KV ids
116
+ # set GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET / TOKEN_ENCRYPTION_KEY as secrets
117
+ npm run deploy
118
+ ```
119
+
120
+ See **[SETUP.md](SETUP.md)** for every step in detail.
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ npm install
126
+ npm test # typecheck + offline unit tests (what CI runs)
127
+ npm run dev # wrangler dev (local)
128
+ npm run test:smoke # structural smoke tests against a deployment
129
+ ```
130
+
131
+ Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
132
+
133
+ ## License
134
+
135
+ [MIT](LICENSE) © 2026 DigestSEO.
136
+
137
+ ---
138
+
139
+ *Built and maintained by [Tomi Šeregi](https://tomiseregi.si), the builder behind [digestseo.com](https://digestseo.com) — weekly SEO digests for non-technical site owners.*
package/SECURITY.md ADDED
@@ -0,0 +1,36 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Please **do not open a public issue** for security problems. Instead, use
6
+ [GitHub's private vulnerability reporting](https://github.com/AKzar1el/mcp-gsc/security/advisories/new)
7
+ on this repository. You'll get a response within a few days.
8
+
9
+ ## What this server stores and how
10
+
11
+ A deployed instance of `mcp-gsc` handles Google OAuth credentials on behalf of
12
+ its users. The full data inventory:
13
+
14
+ | Data | Where | Protection |
15
+ |---|---|---|
16
+ | Google **refresh token** (per user) | Workers KV (`USER_KV`) | Encrypted at rest with **AES-256-GCM** (`src/crypto.ts`) using the `TOKEN_ENCRYPTION_KEY` secret; a fresh random IV per encryption. |
17
+ | Google account **id and email** (per user) | Workers KV (`USER_KV`) | Stored in plaintext alongside the encrypted token, used only to key and label the record. |
18
+ | Pending OAuth state | Workers KV (`OAUTH_KV`) | Single-use nonce, expires automatically after 10 minutes. |
19
+ | MCP client tokens | Workers KV (`OAUTH_KV`) | Managed by [`@cloudflare/workers-oauth-provider`](https://github.com/cloudflare/workers-oauth-provider). |
20
+ | Google **access tokens** | In-memory only (Durable Object) | Never written to storage; expire within an hour. |
21
+
22
+ Notes:
23
+
24
+ - The server requests only the **read-only** scope
25
+ `https://www.googleapis.com/auth/webmasters.readonly` (plus `openid email`
26
+ for identifying the account). It cannot modify Search Console data.
27
+ - Secrets (`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `TOKEN_ENCRYPTION_KEY`)
28
+ are read from Worker secrets / `.dev.vars`, never from the repository.
29
+ - When Google reports a refresh token as revoked (`invalid_grant`), the stored
30
+ user record is deleted immediately.
31
+ - Search Console data is fetched on demand and returned to the MCP client;
32
+ it is never persisted by this server.
33
+
34
+ ## Supported versions
35
+
36
+ Only the latest release on `main` is supported with security fixes.
package/SETUP.md ADDED
@@ -0,0 +1,201 @@
1
+ # Setup — self-hosting mcp-gsc
2
+
3
+ This guide walks you through deploying your own instance of `mcp-gsc` on Cloudflare Workers with your own Google OAuth credentials ("bring your own OAuth"). Budget about 30–40 minutes the first time.
4
+
5
+ By the end you'll have a Worker at `https://<your-worker>.workers.dev/mcp` that you can connect as a custom MCP connector in Claude.ai, Cursor, or ChatGPT.
6
+
7
+ > **Read [Step 7](#step-7--important-google-verification) before you start.** While your Google OAuth app is unverified, refresh tokens expire after **7 days** and you're capped at **100 users**. This is the single biggest reason self-hosting a Google Search Console MCP is heavier than a key-based MCP — it's how Google's OAuth works for the sensitive `webmasters` and `indexing` scopes, not a limitation of this project.
8
+
9
+ ## Prerequisites
10
+
11
+ - A [Cloudflare account](https://dash.cloudflare.com/sign-up) (the free Workers plan is enough to start).
12
+ - A [Google account](https://accounts.google.com/) with access to the Search Console properties you want to query.
13
+ - Node.js 20+ and npm.
14
+ - The Wrangler CLI — installed for you by `npm install`; invoke it with `npx wrangler …`.
15
+
16
+ Clone and install:
17
+
18
+ ```bash
19
+ git clone https://github.com/<you>/mcp-gsc.git
20
+ cd mcp-gsc
21
+ npm install
22
+ ```
23
+
24
+ Authenticate Wrangler with Cloudflare (opens a browser):
25
+
26
+ ```bash
27
+ npx wrangler login
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Step 1 — Create a Google Cloud project and enable the Search Console API
33
+
34
+ 1. Go to the [Google Cloud Console](https://console.cloud.google.com/).
35
+ 2. Create a new project (top bar → project dropdown → **New Project**). Name it anything, e.g. `mcp-gsc`.
36
+ 3. With that project selected, enable the **Google Search Console API**:
37
+ - Go to **APIs & Services → Library**.
38
+ - Search for **"Google Search Console API"**.
39
+ - Open it and click **Enable**.
40
+
41
+ Direct link: <https://console.cloud.google.com/apis/library/searchconsole.googleapis.com>
42
+
43
+ ---
44
+
45
+ ## Step 2 — Configure the OAuth consent screen
46
+
47
+ 1. Go to **APIs & Services → OAuth consent screen**.
48
+ 2. Choose **User type: External**, then **Create**.
49
+ 3. Fill in the required app info (app name, user support email, developer contact email). The app name is what users see on the Google sign-in screen.
50
+ 4. On the **Scopes** step, click **Add or remove scopes** and add these scopes:
51
+
52
+ ```
53
+ https://www.googleapis.com/auth/webmasters
54
+ https://www.googleapis.com/auth/indexing
55
+ ```
56
+
57
+ These are **sensitive** scopes. They grant read-write access to Search Console data and the Google Indexing API (required to manage sites, sitemaps, and request URL crawling).
58
+ 5. On the **Test users** step, click **Add users** and add your own Google email address (and any teammates who need access while the app is in Testing).
59
+ 6. Save. Leave the **Publishing status** as **Testing** for now — see [Step 7](#step-7--important-google-verification).
60
+
61
+ ---
62
+
63
+ ## Step 3 — Create an OAuth client (Web application)
64
+
65
+ 1. Go to **APIs & Services → Credentials → Create credentials → OAuth client ID**.
66
+ 2. **Application type: Web application.**
67
+ 3. Under **Authorized redirect URIs**, add both of these:
68
+
69
+ ```
70
+ https://<your-worker>.workers.dev/google/callback
71
+ http://localhost:8787/google/callback
72
+ ```
73
+
74
+ - Replace `<your-worker>` with your Worker's name + subdomain. If you don't know it yet, deploy once (Step 6) to see the assigned `*.workers.dev` URL, then come back and add it here. The default Worker name is `mcp-gsc` (set in `wrangler.jsonc`).
75
+ - The `http://localhost:8787/google/callback` entry is for local development with `npm run dev`.
76
+ - The path must be exactly `/google/callback` — that's the route this server handles.
77
+ - The DigestSEO-hosted instance also requires this exact callback URI in the OAuth client configured on its Worker: `https://mcp-gsc.digestseo.com/google/callback`.
78
+ 4. Click **Create**. Copy the **Client ID** and **Client secret** — you'll set them as secrets in the next step.
79
+
80
+ ---
81
+
82
+ ## Step 4 — Set the Worker secrets
83
+
84
+ This server reads three secrets. **Never commit these to git** — set them with `wrangler secret put`, which stores them encrypted in Cloudflare:
85
+
86
+ ```bash
87
+ npx wrangler secret put GOOGLE_CLIENT_ID
88
+ # paste the Client ID from Step 3
89
+
90
+ npx wrangler secret put GOOGLE_CLIENT_SECRET
91
+ # paste the Client secret from Step 3
92
+
93
+ npx wrangler secret put TOKEN_ENCRYPTION_KEY
94
+ # paste the key generated below
95
+ ```
96
+
97
+ `TOKEN_ENCRYPTION_KEY` is the **AES-256 key** used by `src/crypto.ts` to encrypt each user's Google refresh token before it's stored in KV. Generate a fresh 32-byte key, base64-encoded:
98
+
99
+ ```bash
100
+ openssl rand -base64 32
101
+ ```
102
+
103
+ Paste that value when prompted. Keep it safe — if you lose it, the refresh tokens already in KV become undecryptable and every user has to reconnect.
104
+
105
+ For **local development**, put the same three values in `.dev.vars` instead — copy `.dev.vars.example` to `.dev.vars` (it's gitignored):
106
+
107
+ ```
108
+ GOOGLE_CLIENT_ID=...
109
+ GOOGLE_CLIENT_SECRET=...
110
+ TOKEN_ENCRYPTION_KEY=...
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Step 5 — Create the KV namespaces
116
+
117
+ The server uses two Workers KV namespaces: `OAUTH_KV` (pending OAuth state and issued tokens) and `USER_KV` (encrypted refresh tokens, keyed per user).
118
+
119
+ ```bash
120
+ npx wrangler kv namespace create OAUTH_KV
121
+ npx wrangler kv namespace create USER_KV
122
+ ```
123
+
124
+ Each command prints an `id`. Copy the template to your real config and paste the two ids in:
125
+
126
+ ```bash
127
+ cp wrangler.example.jsonc wrangler.jsonc
128
+ ```
129
+
130
+ Then edit `wrangler.jsonc` and replace the placeholders:
131
+
132
+ ```jsonc
133
+ "kv_namespaces": [
134
+ { "binding": "OAUTH_KV", "id": "YOUR_OAUTH_KV_ID" }, // ← paste OAUTH_KV id
135
+ { "binding": "USER_KV", "id": "YOUR_USER_KV_ID" } // ← paste USER_KV id
136
+ ]
137
+ ```
138
+
139
+ `wrangler.jsonc` is gitignored because it contains your account's namespace ids. The `wrangler.example.jsonc` template stays in git.
140
+
141
+ ---
142
+
143
+ ## Step 6 — Apply migrations, deploy, and verify
144
+
145
+ The Durable Object migration declared in `wrangler.jsonc` (`migrations` → tag `v1`, `new_sqlite_classes: ["GscMcpAgent"]`) is applied automatically the first time you deploy. Deploy:
146
+
147
+ ```bash
148
+ npm run deploy
149
+ ```
150
+
151
+ Wrangler prints your Worker URL, e.g. `https://mcp-gsc.<your-subdomain>.workers.dev`. If you didn't know that host in Step 3, add `https://<that-host>/google/callback` to the Authorized redirect URIs now, then deploy again.
152
+
153
+ Smoke-check the deployment:
154
+
155
+ ```bash
156
+ curl https://<your-worker>.workers.dev/healthz
157
+ # → ok
158
+ ```
159
+
160
+ Opening the root URL in a browser also prints the `/mcp` connect URL.
161
+
162
+ Now connect it in **Claude.ai**:
163
+
164
+ 1. Settings → **Connectors** → **Add custom connector**.
165
+ 2. Paste `https://<your-worker>.workers.dev/mcp`.
166
+ 3. Leave **Client ID** and **Client Secret** blank.
167
+ 4. Click through; Claude opens a Google sign-in flow. Sign in with a Google account you added as a **test user** in Step 2, and grant read access to your Search Console properties.
168
+ 5. The connector turns green. Ask: *"What sites do I have in Search Console?"*
169
+
170
+ The same `/mcp` URL works in Cursor and ChatGPT.
171
+
172
+ ---
173
+
174
+ ## Step 7 — Important: Google verification
175
+
176
+ **Read this carefully — it determines whether your self-hosted instance keeps working past a week.**
177
+
178
+ While your OAuth app's **Publishing status** is **Testing** (where it starts, and where Step 2 leaves it), three limits apply:
179
+
180
+ - **Unverified-app screen.** Users see Google's *"Google hasn't verified this app"* warning during sign-in and must click **Advanced → Go to \<app\> (unsafe)** to continue. That's expected for an unverified app — it isn't a problem with this server.
181
+ - **100-user cap.** At most 100 Google accounts can ever authorize the app.
182
+ - **Refresh tokens expire after 7 days.** This is the big one. In Testing mode, Google expires every refresh token **7 days** after it's issued. When that happens the tools start returning *"Google access revoked. Please reconnect this connector in Claude.ai"*, and the user has to reconnect to get a fresh token. **Any automation or scheduled job you build on top will break every 7 days** until you fix this.
183
+
184
+ To remove all three limits you must move the app to **Publishing status: In production**:
185
+
186
+ - In **APIs & Services → OAuth consent screen**, click **Publish app**.
187
+ - Because `webmasters` and `indexing` are **sensitive scopes**, Google requires **OAuth verification**: you submit the app for review, justify the scopes, and (for sensitive/restricted scopes) may need to verify domain ownership and complete a security assessment. **This review can take days to several weeks.**
188
+ - Once the app is **In production and verified**, the unverified-app screen goes away, the 100-user cap is lifted, and refresh tokens stop expiring on the 7-day clock.
189
+
190
+ **Bottom line:** for personal use with one or two Google accounts, Testing mode is fine as long as you don't mind reconnecting roughly every 7 days. For anything shared or automated, you'll want to complete Google's verification — and that, not the code, is the heaviest part of self-hosting a Google Search Console MCP.
191
+
192
+ > Don't want to deal with verification at all? The hosted version at **[digestseo.com](https://digestseo.com)** has already been through it (and adds automatic weekly email digests on top).
193
+
194
+ ---
195
+
196
+ ## Troubleshooting
197
+
198
+ - **`redirect_uri_mismatch`** — the redirect URI in Step 3 must match your Worker host exactly, including `https://` and the `/google/callback` path. Check for typos and trailing slashes.
199
+ - **`Google did not return a refresh_token`** — you need a fresh consent. This server requests `access_type=offline` with `prompt=consent`, but if you've authorized before, remove the app from your [Google Account permissions](https://myaccount.google.com/permissions) and reconnect to force a new refresh token.
200
+ - **"Google access revoked" after about a week** — that's the 7-day Testing-mode expiry from Step 7. Reconnect, or move the app to production.
201
+ - **401 on `/mcp`** — expected when unauthenticated. Connect through your MCP client's OAuth flow rather than calling `/mcp` directly in a browser.
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@digestseo/mcp-gsc",
3
+ "version": "0.3.1",
4
+ "private": false,
5
+ "main": "scripts/glama-start.mjs",
6
+ "bin": {
7
+ "mcp-gsc": "scripts/glama-start.mjs"
8
+ },
9
+ "license": "MIT",
10
+ "description": "Self-hostable Google Search Console MCP server for analytics, URL inspection, sitemaps, indexing, and SEO reporting on Cloudflare Workers.",
11
+ "type": "module",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/AKzar1el/mcp-gsc.git"
15
+ },
16
+ "homepage": "https://digestseo.com/gsc-mcp/",
17
+ "author": {
18
+ "name": "DigestSEO",
19
+ "url": "https://digestseo.com/"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/AKzar1el/mcp-gsc/issues"
23
+ },
24
+ "files": [
25
+ "src",
26
+ "scripts/glama-start.mjs",
27
+ "wrangler.example.jsonc",
28
+ ".dev.vars.example",
29
+ "README.md",
30
+ "SETUP.md",
31
+ "SECURITY.md",
32
+ "CHANGELOG.md",
33
+ "LICENSE"
34
+ ],
35
+ "keywords": [
36
+ "mcp",
37
+ "model-context-protocol",
38
+ "google-search-console",
39
+ "google-search-console-mcp",
40
+ "search-console",
41
+ "search-analytics",
42
+ "indexing-api",
43
+ "seo",
44
+ "cloudflare-workers",
45
+ "claude"
46
+ ],
47
+ "scripts": {
48
+ "dev": "wrangler dev",
49
+ "deploy": "wrangler deploy",
50
+ "build": "npm run typecheck",
51
+ "start": "node scripts/glama-start.mjs",
52
+ "typecheck": "tsc --noEmit",
53
+ "test": "npm run typecheck && npm run test:unit",
54
+ "test:unit": "tsx --test tests/unit.test.ts",
55
+ "test:smoke": "node --test tests/smoke.test.mjs",
56
+ "prepublishOnly": "npm test"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public",
60
+ "registry": "https://registry.npmjs.org/"
61
+ },
62
+ "mcpName": "io.github.AKzar1el/mcp-gsc",
63
+ "dependencies": {
64
+ "@cloudflare/workers-oauth-provider": "^0.8.1",
65
+ "@modelcontextprotocol/sdk": "^1.29.0",
66
+ "agents": "^0.13.1",
67
+ "wrangler": "^4.93.0",
68
+ "zod": "^4.4.3"
69
+ },
70
+ "devDependencies": {
71
+ "@cloudflare/workers-types": "^4.20260520.1",
72
+ "tsx": "^4.20.0",
73
+ "typescript": "^6.0.3"
74
+ },
75
+ "engines": {
76
+ "node": ">=20.0.0"
77
+ }
78
+ }
@@ -0,0 +1,36 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+
4
+ const port = process.env.PORT || '8080';
5
+ const forwardedVariables = ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', 'TOKEN_ENCRYPTION_KEY'];
6
+ const args = [
7
+ 'dev',
8
+ '--config',
9
+ 'wrangler.example.jsonc',
10
+ '--local',
11
+ '--ip',
12
+ '0.0.0.0',
13
+ '--port',
14
+ port,
15
+ ];
16
+
17
+ for (const name of forwardedVariables) {
18
+ if (process.env[name]) {
19
+ args.push('--var', `${name}:${process.env[name]}`);
20
+ }
21
+ }
22
+
23
+ const wranglerCli = createRequire(import.meta.url).resolve('wrangler/bin/wrangler.js');
24
+ const child = spawn(process.execPath, [wranglerCli, ...args], { stdio: 'inherit' });
25
+
26
+ const forwardSignal = (signal) => child.kill(signal);
27
+ process.on('SIGINT', () => forwardSignal('SIGINT'));
28
+ process.on('SIGTERM', () => forwardSignal('SIGTERM'));
29
+
30
+ child.on('exit', (code, signal) => {
31
+ if (signal) {
32
+ process.kill(process.pid, signal);
33
+ } else {
34
+ process.exit(code ?? 1);
35
+ }
36
+ });
package/src/crypto.ts ADDED
@@ -0,0 +1,58 @@
1
+ function base64Decode(b64: string): Uint8Array<ArrayBuffer> {
2
+ const binary = atob(b64);
3
+ const buf = new ArrayBuffer(binary.length);
4
+ const bytes = new Uint8Array(buf);
5
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
6
+ return bytes;
7
+ }
8
+
9
+ function base64Encode(bytes: Uint8Array): string {
10
+ let binary = '';
11
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
12
+ return btoa(binary);
13
+ }
14
+
15
+ async function importKey(base64Key: string): Promise<CryptoKey> {
16
+ const keyBytes = base64Decode(base64Key);
17
+ return crypto.subtle.importKey(
18
+ 'raw',
19
+ keyBytes,
20
+ { name: 'AES-GCM' },
21
+ false,
22
+ ['encrypt', 'decrypt'],
23
+ );
24
+ }
25
+
26
+ export async function encryptToken(
27
+ plaintext: string,
28
+ base64Key: string,
29
+ ): Promise<{ ciphertext: string; iv: string }> {
30
+ const key = await importKey(base64Key);
31
+ const iv = crypto.getRandomValues(new Uint8Array(12));
32
+ const encoded = new TextEncoder().encode(plaintext);
33
+ const ciphertext = await crypto.subtle.encrypt(
34
+ { name: 'AES-GCM', iv },
35
+ key,
36
+ encoded,
37
+ );
38
+ return {
39
+ ciphertext: base64Encode(new Uint8Array(ciphertext)),
40
+ iv: base64Encode(iv),
41
+ };
42
+ }
43
+
44
+ export async function decryptToken(
45
+ ciphertext: string,
46
+ iv: string,
47
+ base64Key: string,
48
+ ): Promise<string> {
49
+ const key = await importKey(base64Key);
50
+ const ctBytes = base64Decode(ciphertext);
51
+ const ivBytes = base64Decode(iv);
52
+ const decrypted = await crypto.subtle.decrypt(
53
+ { name: 'AES-GCM', iv: ivBytes },
54
+ key,
55
+ ctBytes,
56
+ );
57
+ return new TextDecoder().decode(decrypted);
58
+ }