@digestseo/mcp-gsc 0.3.1 → 0.4.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.
package/SETUP.md CHANGED
@@ -1,201 +1,219 @@
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`.
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 remote MCP integration in Claude, 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 Google Auth Platform
46
+
47
+ 1. In the Google Cloud console, go to **Google Auth platform → Branding**.
48
+ 2. If Google says the Auth platform is not configured yet, click **Get Started** and complete the setup wizard:
49
+ - **App Information:** enter the app name and user support email.
50
+ - **Audience:** choose **External**.
51
+ - **Contact Information:** enter the developer contact email.
52
+ - **Finish:** review the Google API Services User Data Policy, agree if appropriate, then click **Continue → Create**.
53
+ If the Auth platform is already configured, review the existing **Branding** and **Audience** settings instead of starting over.
54
+ 3. Go to **Google Auth platform → Audience**. Under **Test users**, click **Add users** and add your own Google email address (and any teammates who need access while the app is in Testing).
55
+ 4. Go to **Google Auth platform → Data Access → Add or remove scopes**. Choose the access mode for this deployment and add the matching scope set:
56
+
57
+ | `GSC_ACCESS_MODE` | Scopes to add |
58
+ |---|---|
59
+ | `readwrite` (default) | `https://www.googleapis.com/auth/webmasters` and `https://www.googleapis.com/auth/indexing` |
60
+ | `readonly` | `https://www.googleapis.com/auth/webmasters.readonly` |
61
+
62
+ `readwrite` preserves the full tool surface: it grants Search Console read-write access and the Indexing API scope required to manage sites, sitemaps, and request URL crawling. `readonly` requests only the Search Console read-only scope and omits the mutation tools.
63
+
64
+ Note that the Indexing API itself is narrow: Google currently restricts it to pages containing `JobPosting` structured data or livestream pages containing `BroadcastEvent` inside `VideoObject`. It is not available for general webpage submission — the `indexing.request` tool checks a page's structured data before submitting and returns an error for ineligible URLs.
65
+ 5. Save the Data Access changes. In **Google Auth platform → Audience**, leave **Publishing status** as **Testing** for now — see [Step 7](#step-7--important-google-verification).
66
+
67
+ ---
68
+
69
+ ## Step 3 — Create an OAuth client (Web application)
70
+
71
+ 1. Go to **Google Auth platform → Clients → Create Client**.
72
+ 2. **Application type: Web application.**
73
+ 3. Under **Authorized redirect URIs**, add the entries for the ways you will run mcp-gsc:
74
+
75
+ ```
76
+ https://<your-worker>.workers.dev/google/callback
77
+ http://localhost:8787/google/callback
78
+ http://127.0.0.1:8080/google/callback
79
+ ```
80
+
81
+ - 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`).
82
+ - The `http://localhost:8787/google/callback` entry is for local development with `npm run dev`.
83
+ - The `http://127.0.0.1:8080/google/callback` entry is for the published `npx -y @digestseo/mcp-gsc` launcher, whose Registry transport is `http://127.0.0.1:8080/mcp` by default. If you set `PORT`, replace `8080` with that exact port.
84
+ - The Worker derives Google's redirect URI from the MCP request URL. Google requires an exact authorized-URI match, so keep the same scheme, host, port, and `/google/callback` path. `localhost` and `127.0.0.1` are different hosts for this check.
76
85
  - The path must be exactly `/google/callback` — that's the route this server handles.
77
86
  - 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.
87
+ 4. Click **Create**. Copy the **Client ID** and **Client secret** — you'll set them as secrets in the next step.
88
+
89
+ ---
90
+
91
+ ## Step 4 — Set the Worker secrets
92
+
93
+ This server reads three secrets. **Never commit these to git** — set them with `wrangler secret put`, which stores them encrypted in Cloudflare:
94
+
95
+ ```bash
96
+ npx wrangler secret put GOOGLE_CLIENT_ID
97
+ # paste the Client ID from Step 3
98
+
99
+ npx wrangler secret put GOOGLE_CLIENT_SECRET
100
+ # paste the Client secret from Step 3
101
+
102
+ npx wrangler secret put TOKEN_ENCRYPTION_KEY
103
+ # paste the key generated below
104
+ ```
105
+
106
+ `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:
107
+
108
+ ```bash
109
+ openssl rand -base64 32
110
+ ```
111
+
112
+ 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.
113
+
114
+ For **local development**, put the same three values in `.dev.vars` instead — copy `.dev.vars.example` to `.dev.vars` (it's gitignored):
115
+
116
+ ```
117
+ GOOGLE_CLIENT_ID=...
118
+ GOOGLE_CLIENT_SECRET=...
119
+ TOKEN_ENCRYPTION_KEY=...
120
+ GSC_ACCESS_MODE=readwrite
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Step 5 — Create the KV namespaces
126
+
127
+ The server uses two Workers KV namespaces: `OAUTH_KV` (data required by the OAuth provider) and `USER_KV` (encrypted refresh tokens, keyed per user). Pending OAuth state is stored separately in a Durable Object so each nonce can be consumed exactly once.
128
+
129
+ ```bash
130
+ npx wrangler kv namespace create OAUTH_KV
131
+ npx wrangler kv namespace create USER_KV
132
+ ```
133
+
134
+ Each command prints an `id`. Copy the template to your real config and paste the two ids in:
135
+
136
+ ```bash
137
+ cp wrangler.example.jsonc wrangler.jsonc
138
+ ```
139
+
140
+ Then edit `wrangler.jsonc` and replace the placeholders:
141
+
142
+ ```jsonc
143
+ "kv_namespaces": [
144
+ { "binding": "OAUTH_KV", "id": "YOUR_OAUTH_KV_ID" }, // ← paste OAUTH_KV id
145
+ { "binding": "USER_KV", "id": "YOUR_USER_KV_ID" } // ← paste USER_KV id
146
+ ]
147
+ ```
148
+
149
+ The template also sets `GSC_ACCESS_MODE` to `readwrite`, preserving the historical behavior. To deploy least-privilege read-only access, set it to `readonly` before deploying:
150
+
151
+ ```jsonc
152
+ "vars": {
153
+ "GSC_ACCESS_MODE": "readonly"
154
+ }
155
+ ```
156
+
157
+ Read-only deployments request `webmasters.readonly`, omit the Indexing API scope, and do not register `sites.add`, `sites.delete`, `sitemaps.submit`, `sitemaps.delete`, or `indexing.request`. Changing modes affects OAuth grants for future connections; reconnect users after changing the mode so Google grants the matching scope set.
158
+
159
+ `wrangler.jsonc` is gitignored because it contains your account's namespace ids. The `wrangler.example.jsonc` template stays in git.
160
+
161
+ ---
162
+
163
+ ## Step 6 — Apply migrations, deploy, and verify
164
+
165
+ The Durable Object migrations declared in `wrangler.jsonc` are applied automatically when you deploy: `v1` creates the historical `GscMcpAgent` class, `v2` adds `PendingAuthState` for atomic OAuth-state consumption, and `v3` adds `ToolRateLimiter` for server-side per-user tool limits. The MCP endpoint now uses Cloudflare's stateless MCP handler; `GscMcpAgent` remains as an unused compatibility shell so existing deployments can upgrade without a destructive Durable Object deletion migration. Do not remove or rename the existing binding/migration history. Deploy:
166
+
167
+ ```bash
168
+ npm run deploy
169
+ ```
170
+
171
+ 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.
172
+
173
+ Smoke-check the deployment:
174
+
175
+ ```bash
176
+ curl https://<your-worker>.workers.dev/healthz
177
+ # → ok
178
+ ```
179
+
180
+ Opening the root URL in a browser also prints the `/mcp` connect URL.
181
+
182
+ Now connect it in your client:
183
+
184
+ - **Claude.ai / Claude Desktop** — go to **Customize → Connectors**, click **+ → Add custom connector**, enter a name, and paste `https://<your-worker>.workers.dev/mcp`. Leave the optional advanced OAuth Client ID/Secret fields blank. On Team/Enterprise, an Owner or Primary Owner must first add the custom Web connector from **Organization settings → Connectors**; members then connect it from Customize → Connectors. On first connection, Claude opens the Google sign-in flow.
185
+ - **Cursor** — add a remote **Streamable HTTP** MCP server with the same `/mcp` URL. Cursor supports OAuth for remote HTTP servers.
186
+ - **ChatGPT** — enable **Developer mode**, then create a custom MCP **app** from **Settings → Apps → Create** (admins/owners can also use **Workspace settings → Apps → Create**). Provide the `/mcp` endpoint, select the applicable authentication option, click **Scan Tools**, complete OAuth, then create the app. Full MCP including write/modify tools is currently available to Business and Enterprise/Edu; Pro custom MCP access is read/fetch-only, so use `GSC_ACCESS_MODE=readonly` for that path.
187
+
188
+ After the client starts OAuth, sign in with a Google account you added as a **test user** in Step 2 and grant the access requested by your selected deployment mode. Then ask: *"What sites do I have in Search Console?"*
189
+
190
+ ---
191
+
192
+ ## Step 7 — Important: Google verification
193
+
194
+ **Read this carefully — it determines whether your self-hosted instance keeps working past a week.**
195
+
196
+ While your OAuth app's **Publishing status** is **Testing** (where it starts, and where Step 2 leaves it), three limits apply:
197
+
198
+ - **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.
199
+ - **100-user cap.** At most 100 Google accounts can ever authorize the app.
200
+ - **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.
201
+
202
+ To remove all three limits you must move the app to **Publishing status: In production** and complete any verification Google requires for the scopes you use:
203
+
204
+ - In **Google Auth platform → Audience**, click **Publish app** and confirm the move to production.
205
+ - Google requires public production apps that use **sensitive or restricted scopes** to complete OAuth verification. Follow **Google Auth platform → Verification Center** for the requirements that apply to your selected scopes. Google's additional security assessment applies to **restricted** scopes; do not assume every sensitive scope requires one. Review time varies by the checks your app requires.
206
+ - 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.
207
+
208
+ **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.
209
+
210
+ > 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).
211
+
212
+ ---
213
+
214
+ ## Troubleshooting
215
+
216
+ - **`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.
217
+ - **`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.
218
+ - **"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.
219
+ - **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 CHANGED
@@ -1,6 +1,6 @@
1
- {
1
+ {
2
2
  "name": "@digestseo/mcp-gsc",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "main": "scripts/glama-start.mjs",
6
6
  "bin": {
@@ -9,10 +9,10 @@
9
9
  "license": "MIT",
10
10
  "description": "Self-hostable Google Search Console MCP server for analytics, URL inspection, sitemaps, indexing, and SEO reporting on Cloudflare Workers.",
11
11
  "type": "module",
12
- "repository": {
13
- "type": "git",
14
- "url": "git+https://github.com/AKzar1el/mcp-gsc.git"
15
- },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/AKzar1el/mcp-gsc.git"
15
+ },
16
16
  "homepage": "https://digestseo.com/gsc-mcp/",
17
17
  "author": {
18
18
  "name": "DigestSEO",
@@ -50,7 +50,19 @@
50
50
  "build": "npm run typecheck",
51
51
  "start": "node scripts/glama-start.mjs",
52
52
  "typecheck": "tsc --noEmit",
53
- "test": "npm run typecheck && npm run test:unit",
53
+ "test:oauth-state": "tsx --test tests/pending-auth-state.test.ts",
54
+ "test:comparison": "tsx --test tests/performance-comparison.test.ts",
55
+ "test:annotations": "tsx --test tests/write-tool-annotations.test.ts",
56
+ "test:worker": "vitest run --config vitest.worker.config.ts",
57
+ "test:access-mode": "tsx --test tests/access-mode.test.ts",
58
+ "test:access-token-lifecycle": "tsx --test tests/access-token-lifecycle.test.ts",
59
+ "test:analytics-response-metadata": "tsx --test tests/analytics-response-metadata.test.ts",
60
+ "test:indexing-authorization": "tsx --test tests/indexing-property-authorization.test.ts",
61
+ "test:indexing-eligibility": "tsx --test tests/indexing-eligibility.test.ts",
62
+ "test:insight-pagination": "tsx --test tests/insight-pagination.test.ts",
63
+ "test:tool-rate-limits": "tsx --test tests/tool-rate-limit.test.ts",
64
+ "test": "npm run typecheck && npm run test:metadata && npm run test:oauth-state && npm run test:comparison && npm run test:annotations && npm run test:worker && npm run test:access-mode && npm run test:access-token-lifecycle && npm run test:analytics-response-metadata && npm run test:indexing-authorization && npm run test:indexing-eligibility && npm run test:insight-pagination && npm run test:tool-rate-limits && npm run test:unit",
65
+ "test:metadata": "tsx --test tests/metadata.test.ts",
54
66
  "test:unit": "tsx --test tests/unit.test.ts",
55
67
  "test:smoke": "node --test tests/smoke.test.mjs",
56
68
  "prepublishOnly": "npm test"
@@ -61,16 +73,18 @@
61
73
  },
62
74
  "mcpName": "io.github.AKzar1el/mcp-gsc",
63
75
  "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",
76
+ "@cloudflare/workers-oauth-provider": "^0.10.3",
77
+ "@modelcontextprotocol/server": "2.0.0",
78
+ "agents": "^0.24.0",
79
+ "wrangler": "^4.135.0",
68
80
  "zod": "^4.4.3"
69
81
  },
70
82
  "devDependencies": {
71
- "@cloudflare/workers-types": "^4.20260520.1",
83
+ "@cloudflare/vitest-plugin": "^1.1.13",
84
+ "@cloudflare/workers-types": "^5.20260919.1",
72
85
  "tsx": "^4.20.0",
73
- "typescript": "^6.0.3"
86
+ "typescript": "^6.0.3",
87
+ "vitest": "^4.1.11"
74
88
  },
75
89
  "engines": {
76
90
  "node": ">=20.0.0"
@@ -2,14 +2,22 @@ import { spawn } from 'node:child_process';
2
2
  import { createRequire } from 'node:module';
3
3
 
4
4
  const port = process.env.PORT || '8080';
5
- const forwardedVariables = ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', 'TOKEN_ENCRYPTION_KEY'];
5
+ const localBaseUrl = `http://127.0.0.1:${port}`;
6
+ const mcpUrl = `${localBaseUrl}/mcp`;
7
+ const googleRedirectUri = `${localBaseUrl}/google/callback`;
8
+ const forwardedVariables = [
9
+ 'GOOGLE_CLIENT_ID',
10
+ 'GOOGLE_CLIENT_SECRET',
11
+ 'TOKEN_ENCRYPTION_KEY',
12
+ 'GLAMA_INSPECTION_MODE',
13
+ ];
6
14
  const args = [
7
15
  'dev',
8
16
  '--config',
9
17
  'wrangler.example.jsonc',
10
18
  '--local',
11
19
  '--ip',
12
- '0.0.0.0',
20
+ '127.0.0.1',
13
21
  '--port',
14
22
  port,
15
23
  ];
@@ -20,7 +28,10 @@ for (const name of forwardedVariables) {
20
28
  }
21
29
  }
22
30
 
23
- const wranglerCli = createRequire(import.meta.url).resolve('wrangler/bin/wrangler.js');
31
+ console.error(`[mcp-gsc] Local MCP endpoint: ${mcpUrl}`);
32
+ console.error(`[mcp-gsc] Google OAuth redirect URI: ${googleRedirectUri}`);
33
+
34
+ const wranglerCli = createRequire(import.meta.url).resolve('wrangler');
24
35
  const child = spawn(process.execPath, [wranglerCli, ...args], { stdio: 'inherit' });
25
36
 
26
37
  const forwardSignal = (signal) => child.kill(signal);
@@ -0,0 +1,59 @@
1
+ export const GSC_ACCESS_MODES = ['readonly', 'readwrite'] as const;
2
+
3
+ export type GscAccessMode = (typeof GSC_ACCESS_MODES)[number];
4
+
5
+ export const DEFAULT_GSC_ACCESS_MODE: GscAccessMode = 'readwrite';
6
+
7
+ const BASE_OAUTH_SCOPES = ['openid', 'email'] as const;
8
+
9
+ const READONLY_GOOGLE_OAUTH_SCOPES = [
10
+ ...BASE_OAUTH_SCOPES,
11
+ 'https://www.googleapis.com/auth/webmasters.readonly',
12
+ ] as const;
13
+
14
+ const READWRITE_GOOGLE_OAUTH_SCOPES = [
15
+ ...BASE_OAUTH_SCOPES,
16
+ 'https://www.googleapis.com/auth/webmasters',
17
+ 'https://www.googleapis.com/auth/indexing',
18
+ ] as const;
19
+
20
+ export const WRITE_TOOL_NAMES = [
21
+ 'sites.add',
22
+ 'sites.delete',
23
+ 'sitemaps.submit',
24
+ 'sitemaps.delete',
25
+ 'indexing.request',
26
+ ] as const;
27
+
28
+ export function resolveGscAccessMode(value: string | undefined): GscAccessMode {
29
+ if (value === undefined || value === '') {
30
+ return DEFAULT_GSC_ACCESS_MODE;
31
+ }
32
+
33
+ if ((GSC_ACCESS_MODES as readonly string[]).includes(value)) {
34
+ return value as GscAccessMode;
35
+ }
36
+
37
+ throw new Error(
38
+ 'Invalid GSC_ACCESS_MODE configuration. Expected "readonly" or "readwrite".',
39
+ );
40
+ }
41
+
42
+ export function getGoogleOAuthScopes(mode: GscAccessMode): readonly string[] {
43
+ return mode === 'readonly'
44
+ ? READONLY_GOOGLE_OAUTH_SCOPES
45
+ : READWRITE_GOOGLE_OAUTH_SCOPES;
46
+ }
47
+
48
+ export function getToolCatalogForAccessMode<T extends { name: string }>(
49
+ tools: readonly T[],
50
+ mode: GscAccessMode,
51
+ ): T[] {
52
+ if (mode === 'readwrite') {
53
+ return [...tools];
54
+ }
55
+
56
+ return tools.filter(
57
+ (tool) => !(WRITE_TOOL_NAMES as readonly string[]).includes(tool.name),
58
+ );
59
+ }