@digestseo/mcp-gsc 0.3.1 → 0.4.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/.dev.vars.example +12 -8
- package/CHANGELOG.md +80 -40
- package/LICENSE +21 -21
- package/README.md +111 -96
- package/SECURITY.md +73 -36
- package/SETUP.md +217 -199
- package/package.json +27 -13
- package/scripts/glama-start.mjs +14 -3
- package/src/access-mode.ts +59 -0
- package/src/access-token-lifecycle.ts +107 -0
- package/src/cannibalization-schema.ts +15 -0
- package/src/content-decay-schema.ts +8 -0
- package/src/crypto.ts +80 -58
- package/src/date-validation.ts +84 -0
- package/src/digest.ts +632 -648
- package/src/entrypoint.ts +39 -0
- package/src/google.ts +1205 -716
- package/src/index.ts +1431 -908
- package/src/indexed-pages-range.ts +54 -0
- package/src/indexing-property-authorization.ts +119 -0
- package/src/pending-auth-state.ts +87 -0
- package/src/quick-wins-schema.ts +27 -0
- package/src/storage.ts +65 -98
- package/src/tool-rate-limit.ts +231 -0
- package/src/tool-rate-limiter-do.ts +23 -0
- package/src/write-tool-annotations.ts +43 -0
- package/wrangler.example.jsonc +54 -35
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
|
|
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
|
|
46
|
-
|
|
47
|
-
1.
|
|
48
|
-
2.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
npx wrangler kv namespace create
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
{ "binding": "
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
**
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
**
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
"version": "0.4.0",
|
|
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": "
|
|
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.
|
|
65
|
-
"@modelcontextprotocol/
|
|
66
|
-
"agents": "^0.
|
|
67
|
-
"wrangler": "^4.
|
|
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/
|
|
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"
|
package/scripts/glama-start.mjs
CHANGED
|
@@ -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
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
+
}
|