Skip to main content
Glama
russjeffery

google-search-console-mcp

by russjeffery
README.md
# Google Search Console MCP

An MCP server for the [Google Search Console API](https://developers.google.com/webmaster-tools/v1/api_reference_index): search performance data, URL index status, sitemap management, and property listing.

One codebase runs three ways — **stdio** (local, through `npx`), **Streamable HTTP** (self-hosted), and **Cloudflare Workers** (hosted at a URL). It implements MCP [`2026-07-28`](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) and falls back automatically to `2025-11-25`, `2025-06-18`, and `2025-03-26`, so it works with clients on either side of the protocol change.

Zero runtime dependencies.

---

## Quick start

You need Google credentials either way, so start there. Open the setup page in a browser:

```
https://search-console-mcp.russjeffery.com/setup
```

The page walks you through creating a Google OAuth client, runs the consent flow, verifies the credentials against the live API, and gives you a ready-to-paste config block for your MCP client. Nothing is installed, and the server stores nothing.

To do the same thing locally instead, with the page served from `127.0.0.1`:

```bash
npx google-search-console-mcp auth
```

Then choose how to run the server:

- **Locally**, by pasting the printed stdio config into your client. See [Run the server](#run-the-server).
- **Against the hosted endpoint**, with no install at all. See [Hosted endpoint](#hosted-endpoint).

---

## Hosted endpoint

A public deployment runs at:

```
https://search-console-mcp.russjeffery.com/mcp
```

To get a credential blob for it, open [the setup page](https://search-console-mcp.russjeffery.com/setup) in a browser.

It runs in bring-your-own-credential mode. You send your own credential blob as the bearer token, the server uses it for that one request, and it stores nothing. Two people pointing the same client at this URL see only their own Search Console properties.

Add it to a client like this, replacing `CREDENTIAL_BLOB` with the blob the setup page gave you:

```json
{
  "mcpServers": {
    "google-search-console": {
      "type": "http",
      "url": "https://search-console-mcp.russjeffery.com/mcp",
      "headers": { "Authorization": "Bearer CREDENTIAL_BLOB" }
    }
  }
}
```

In the Claude web or desktop UI, add the same URL and header under **Settings → Connectors → Add custom connector**.

The deployment answers on these routes:

| Route | Behavior |
|---|---|
| `POST /mcp` | The MCP endpoint. Data tools require an `Authorization: Bearer` header carrying your blob. |
| `GET /setup` | The credential setup page. It creates blobs in your browser and stores nothing on the server. |
| `GET /health`, `GET /` | Server name, version, endpoint path, setup URL, and supported protocol versions. No authentication, no credentialed data. |
| `GET /mcp`, `DELETE /mcp` | `405`, as the 2026-07-28 revision prescribes. There is no GET stream and no session to delete. |

A request that arrives with no bearer token isn't refused outright. Discovery — `initialize`, `tools/list`, `resources/read`, and the `check_setup` tool — answers normally and returns setup instructions in place of the usual guidance, so a client that isn't configured yet still connects and its agent can explain what to do. Only `tools/call` for a data tool returns `401`. A deployment with `MCP_SHARED_SECRET` set is the exception: it requires the secret on every request, including discovery.

Two things to know before you point production work at it:

- **You are sending your credentials to someone else's host.** The blob grants Search Console access to your account until you revoke it. The code that receives it is in this repository, and it stores nothing. If that tradeoff doesn't suit you, run the server yourself. The stdio and self-hosted paths are equivalent in every other respect.
- **It's a personal deployment with no uptime commitment.** For anything you depend on, deploy your own copy. See [Cloudflare Workers](#cloudflare-workers).

The hosted deployment allows no browser origins, so browser-based MCP clients are refused. Ordinary MCP clients send no `Origin` header and are unaffected. The setup page sits outside that check, because it holds no credentials of its own and exists to be opened in a browser.

---

## Tools

The server exposes every method in the Search Console API v1, plus two composites and a setup check:

| Tool | Does | API method |
|---|---|---|
| `list_sites` | All properties you can access, with permission levels | `sites.list` |
| `get_site` | One property and your permission on it | `sites.get` |
| `query_search_analytics` | Clicks, impressions, CTR, and position — grouped, filtered, paged | `searchanalytics.query` |
| `compare_search_analytics` | Two periods with per-row and total deltas | *composite* |
| `list_sitemaps` | Submitted sitemaps, or the children of a sitemap index | `sitemaps.list` |
| `get_sitemap` | One sitemap's status and submitted and indexed counts | `sitemaps.get` |
| `submit_sitemap` | Submit or resubmit a sitemap | `sitemaps.submit` |
| `delete_sitemap` | Unsubmit a sitemap | `sitemaps.delete` |
| `inspect_url` | Full index status for one URL | `urlInspection.index.inspect` |
| `inspect_urls` | Up to 25 URLs concurrently, with a coverage-state summary | *composite* |
| `check_setup` | Whether credentials are present, still valid, and which properties they reach | *diagnostic* |

Site verification and the `sites.add` and `sites.delete` methods are deliberately not exposed. Adding and verifying a property is a browser flow that doesn't belong in an agent tool.

The server also serves prompts (`performance_review`, `indexing_audit`, `query_opportunities`, and `sitemap_health`) and resources (`gsc://guide/search-analytics`, `gsc://guide/url-inspection`, and `gsc://guide/sitemaps`) that agents can read on demand.

---

## Authentication

### Create a Google OAuth client

You do this once. The server can't do it for you, because Google requires a human in their console.

1. Open the [Google Cloud Console](https://console.cloud.google.com/) and select or create a project.
2. Enable the [Search Console API](https://console.cloud.google.com/apis/library/searchconsole.googleapis.com) for that project.
3. Open [Google Auth Platform](https://console.cloud.google.com/auth/overview). If the project has no consent screen yet, click **Get started** and fill in the four panels:
   - **App information**: any app name, and your own address as the user support email.
   - **Audience**: *External*, unless this is a Workspace account and every user of the app is inside it.
   - **Contact information**: your email address, for Google's notices about the project.
   - **Finish**: agree to the User Data Policy, then click **Create**.
4. Go to [Data access](https://console.cloud.google.com/auth/scopes), click **Add or remove scopes**, and add the scope you want: `https://www.googleapis.com/auth/webmasters` for full access, or `https://www.googleapis.com/auth/webmasters.readonly` for read-only. Click **Update**, then **Save**.
5. Go to [Audience](https://console.cloud.google.com/auth/audience). Under **Test users**, click **Add users** and add the Google account that owns the Search Console properties. An *External* app in testing refuses every account that isn't listed there.
6. Go to **Clients**, click **Create client**, and choose the application type for where you run the setup flow:
   - For a setup page on a server, such as the hosted endpoint, choose **Web application**. On that client, under **Authorized redirect URIs**, click **Add URI** and paste that server's callback address. The setup page shows the exact address to copy, and Google refuses the sign-in unless it matches character for character.
   - For `npx google-search-console-mcp auth` on your own machine, choose **Desktop app**. It needs no redirect URI, because Google accepts any loopback port for that client type.
7. Copy the **Client ID** and the **Client secret**.

While the consent screen is in *Testing*, Google expires refresh tokens after seven days, and you have to run setup again weekly. Publishing the app, under **Audience → Publish app**, makes them durable.

Google classes both `webmasters` scopes as [sensitive](https://developers.google.com/identity/protocols/oauth2/production-readiness/sensitive-scope-verification), so a published app that hasn't been through verification shows an *unverified app* warning ahead of the consent screen, and is capped at 100 users. You can continue past the warning under **Advanced**. For a client only you sign in to, that's the whole cost; verification matters once you hand the client to other people.

### Run the setup flow

The setup page is the same in both places. Paste in the client ID and secret, then choose full or read-only access. The flow runs Google's consent screen and exchanges the code for a refresh token with PKCE. It then calls `list_sites` to prove the credentials work, showing you the exact properties they can reach.

The final page gives you the credential blob and ready-to-paste config for Claude Desktop, Claude Code, and remote deployments, each with a copy button.

#### In a browser, with nothing installed

Open `/setup` on any deployment of this server, including [the hosted endpoint](#hosted-endpoint):

```
https://search-console-mcp.russjeffery.com/setup
```

The page prepares the authorization request in your own browser and keeps the client ID, client secret, and PKCE verifier in `sessionStorage` until Google redirects back. The server holds no part of a pending flow. Your client secret reaches it once, in the request that trades Google's authorization code for a refresh token, and isn't stored.

#### From a terminal

```bash
npx google-search-console-mcp auth
```

This serves the same page from `127.0.0.1` and opens it for you, then prints the blob and config to your terminal as well. On a headless machine or over SSH, run `auth --terminal` for the prompt-driven version instead.

### The credential blob

What you get back is a *credential blob*: base64url-encoded JSON holding your client ID, client secret, and refresh token.

```
eyJ2IjoxLCJjcmVkZW50aWFscyI6eyJ0eXBlIjoib2F1dGhfcmVmcmVzaF90b2tlbiIsImNsaWVu…
```

**Treat the blob as a password.** Anyone holding it has your Search Console access until you revoke it at [Google Account permissions](https://myaccount.google.com/permissions).

It's a single opaque string so that one value carries everything the server needs. That way it goes straight into an environment variable or an `Authorization` header, with no credentials file on disk.

### Alternatives to the OAuth flow

**Service account.** This suits CI and team-owned properties. Create a service account in Google Cloud, then add its `client_email` address as a user on the property in Search Console, under **Settings → Users and permissions**. Encode the downloaded key file directly:

```bash
base64 -i service-account.json | tr -d '\n'
```

The server accepts a raw service-account key as the blob, with no envelope needed.

**Existing access token.** Set the blob to `{"type":"access_token","access_token":"ya29..."}`. No refresh is possible, so this suits only short-lived scripts.

### Scopes

The flow requests one of two scopes:

| Scope | Grants |
|---|---|
| `https://www.googleapis.com/auth/webmasters.readonly` | Everything except sitemap submit and delete |
| `https://www.googleapis.com/auth/webmasters` | Full access (default) |

Choosing read-only on the setup page requests the narrower scope. The `--read-only` flag on the server is a separate, independent block that rejects mutating tools before they reach the API.

---

## Run the server

### Local (stdio)

Paste the config that `auth` printed into your client, replacing `CREDENTIAL_BLOB` with your own blob:

```json
{
  "mcpServers": {
    "google-search-console": {
      "command": "npx",
      "args": ["-y", "google-search-console-mcp"],
      "env": { "GSC_CREDENTIALS": "CREDENTIAL_BLOB" }
    }
  }
}
```

Config file locations differ by client:

| Client | Path |
|---|---|
| Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
| Claude Code | Run `claude mcp add google-search-console --env GSC_CREDENTIALS=CREDENTIAL_BLOB -- npx -y google-search-console-mcp` |
| Cursor | `~/.cursor/mcp.json` |
| VS Code | `.vscode/mcp.json` |

To skip the `npx` download on every launch, install the package globally:

```bash
npm install -g google-search-console-mcp
```

### Self-hosted HTTP

Start a local Streamable HTTP server with your own credentials:

```bash
GSC_CREDENTIALS=CREDENTIAL_BLOB npx google-search-console-mcp http --port 8787
```

That serves `POST http://127.0.0.1:8787/mcp`, and the setup page at `http://127.0.0.1:8787/setup`. Pass `--no-setup-ui` to leave the page off. It binds to loopback by default. Pass `--host 0.0.0.0` only if you mean to expose it, and put TLS in front of it when you do.

Browser-based clients are refused unless you name them, because a server holding its own credentials would otherwise be drivable by any page you visit. Ordinary MCP clients send no `Origin` header and are unaffected. A browser client needs its origin listed:

```bash
npx google-search-console-mcp http --allowed-origins http://localhost:6274
```

A rejected origin gets a `403` that the browser can't read, because a refusal carries no CORS headers by design. It surfaces as a generic CORS failure, so check the server's `Origins:` startup line when a browser client can't connect. To turn the check off, pass `--allowed-origins '*'`.

### Cloudflare Workers

Deploy your own copy:

```bash
git clone https://github.com/russjeffery/google-search-console-mcp.git
cd google-search-console-mcp
npm install
npx wrangler deploy
```

Your endpoint is `https://google-search-console-mcp.SUBDOMAIN.workers.dev/mcp`, where `SUBDOMAIN` is your workers.dev subdomain. The setup page is at `/setup` on the same host, so anyone you share the deployment with can mint their own blob without installing anything.

**By default the Worker stores no secrets.** Each client sends its own credential blob as the bearer token, so a shared deployment never holds anyone's Google credentials. Different users of the same URL see only their own properties.

For a private, single-tenant deployment, set both secrets instead:

```bash
npx wrangler secret put GSC_CREDENTIALS     # your blob
npx wrangler secret put MCP_SHARED_SECRET   # token clients must present
```

Clients then send the shared secret rather than a blob.

The Worker reads these optional `vars` from `wrangler.jsonc`:

| Variable | Effect |
|---|---|
| `MCP_ENDPOINT` | Path to serve on. Default `/mcp` |
| `GSC_READ_ONLY` | `"1"` disables sitemap submit and delete |
| `ALLOWED_ORIGINS` | Comma-separated browser origins. Unset means non-browser clients only; `*` allows any |
| `MCP_STRICT_HEADERS` | `"0"` relaxes 2026-07-28 header-mirroring validation |
| `SETUP_UI` | `"0"` stops serving the setup page |
| `SETUP_PATH` | Path for the setup page. Default `/setup` |

#### Serve it from your own domain

To use a custom hostname, add a named environment to `wrangler.jsonc` with the hostname as a custom-domain route. The `prod` environment already there is the one behind [the hosted endpoint](#hosted-endpoint), so copy its shape and change the pattern:

```jsonc
"env": {
  "prod": {
    "name": "google-search-console-mcp",
    "routes": [
      { "pattern": "mcp.example.com", "custom_domain": true }
    ],
    "vars": { "MCP_ENDPOINT": "/mcp" }
  }
}
```

Then deploy that environment:

```bash
npx wrangler deploy --env prod
```

Wrangler creates the DNS record and the edge certificate itself. The zone has to already be on the same Cloudflare account. Keeping the route out of the top level means a plain `wrangler deploy` still works for anyone else who clones this repository. Environments don't inherit `vars`, so repeat any you need. Secrets are per-environment too, so pass `--env prod` to `wrangler secret put` as well.

### Connect a client to a remote server

Point the client at your deployment's URL and send the blob as a bearer token:

```json
{
  "mcpServers": {
    "google-search-console": {
      "type": "http",
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer CREDENTIAL_BLOB" }
    }
  }
}
```

In the Claude web or desktop UI, add it under **Settings → Connectors → Add custom connector**.

To print that config filled in for your own deployment, run:

```bash
npx google-search-console-mcp config --url https://mcp.example.com/mcp
```

---

## CLI

```
google-search-console-mcp [command] [options]

  stdio     Run as a stdio MCP server (default)
  http      Run a local Streamable HTTP MCP server
  auth      Guided setup in your browser: OAuth flow, blob, client config
  config    Print client config for existing credentials
  doctor    Verify credentials by calling the API
  help      Show usage
```

When something isn't working, run `doctor` first. It separates "the credentials are wrong" from "the client can't launch the server." `doctor` exits non-zero on a credential problem, so it also works in a health check.

The `stdio` command behaves differently: missing or unreadable credentials don't stop it. It starts anyway, lists its tools, and replaces its usual instructions with setup guidance, so the agent connected to it can explain the problem and walk you through the fix. Data tools return an error naming the remedy; `check_setup` returns the full diagnosis. A line on stderr says `NOT CONFIGURED` when the server comes up this way.

The commands accept these options: `--credentials`, `--site`, `--read-only`, `--port`, `--host`, `--endpoint`, `--secret`, `--allowed-origins`, `--loose-headers`, `--no-setup-ui`, `--url`, `--terminal`, and `--no-browser`.

Four of them are worth a note:

- `--allowed-origins` takes a comma-separated list, and unset means non-browser clients only. Entries are matched case-insensitively, and a trailing slash is ignored.
- `--site` sets a default property so tools can omit `siteUrl`. That's convenient when a deployment only ever covers one site.
- `--loose-headers` relaxes the 2026-07-28 header-mirroring checks, the same as setting `MCP_STRICT_HEADERS=0`.
- `--no-setup-ui` stops the `http` command serving the setup page at `/setup`.

---

## Protocol support

The 2026-07-28 revision changed Streamable HTTP substantially: no `initialize` handshake, no sessions, no `Mcp-Session-Id` header, no GET stream, and per-request metadata in `params._meta` mirrored into HTTP headers. The official TypeScript SDK doesn't implement it yet, so the protocol layer here is hand-written and dual-era.

| Client speaks | Server behavior |
|---|---|
| `2026-07-28` | Stateless. Validates `_meta`, `MCP-Protocol-Version`, `Mcp-Method`, and `Mcp-Name`. Answers `server/discover`. Results carry `resultType` and `serverInfo`. |
| `2025-11-25` and earlier | Standard `initialize` handshake. No session ID is issued, because the server is stateless either way. |

The era is detected per request. A request carrying modern `_meta` is served as modern, and an `initialize` selects legacy. `GET` and `DELETE` on the endpoint return `405`, as the revision prescribes.

Header validation is strict by default, per the specification. When a client sends modern `_meta` without mirroring the headers, set `MCP_STRICT_HEADERS=0` or pass `--loose-headers` rather than downgrading the protocol version.

**On authorization.** The specification's OAuth 2.1 flow assumes the server is a resource server with its own authorization server. This server instead uses the bearer token to carry your Google credentials directly. The [specification permits](https://modelcontextprotocol.io/specification/2026-07-28/basic/index#auth) custom strategies, and it means a hosted deployment holds no secrets and needs no user database. The tradeoff is that clients expecting automatic OAuth discovery need the header configured manually, as shown earlier.

---

## Data caveats

Four properties of Search Console data cause most wrong conclusions. The tool descriptions and the bundled skill cover these in depth. In brief:

1. **Data lags about three days.** Use `lastDays` and the tools pick a safe window. A range ending today shows a decline that isn't real.
2. **Query data is privacy-filtered.** Grouping by `query` silently drops rare queries, so query-level clicks never sum to the property total. That gap isn't lost traffic.
3. **Position is inverted.** Position 3 beats position 8, so a negative change is an improvement. The `compare_search_analytics` tool returns an explicit `improved` flag.
4. **Averages cancel out.** Flat headline numbers routinely hide large offsetting movements. Group by page or query before concluding that nothing changed.

### Quotas

Google enforces two limits that shape how you query:

- Search analytics: about 1,200 queries per minute per property.
- URL inspection: about 2,000 per day per property. This is the tighter of the two, so sample deliberately.

### Not available through the API

Google exposes no API for the aggregate **Index Coverage** report, live URL testing, requesting indexing, Core Web Vitals, manual actions, security issues, links reports, or removals, so none of them are here. Per-URL `inspect_url` calls are the closest substitute for coverage questions.

---

## Agent skill

The `skills/google-search-console/` directory holds a ready-to-install skill that teaches an agent how to use these tools well: the preceding caveats, a diagnostic ladder for traffic changes, opportunity-finding heuristics, and a coverage-state lookup table.

```bash
cp -r skills/google-search-console ~/.claude/skills/
```

The same reference material is available at runtime through the server's `gsc://guide/*` resources, so agents without the skill installed can still read it.

---

## Development

```bash
npm install
npm run build       # compile to dist/
npm run typecheck
npm test
npm run cf:dev      # Worker locally through wrangler
```

To check the HTTP transport by hand, start the server and send it a `tools/list` call:

```bash
GSC_CREDENTIALS=CREDENTIAL_BLOB npm run build && node dist/bin/cli.js http &

curl -s http://127.0.0.1:8787/mcp \
  -H 'content-type: application/json' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: tools/list' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}' | jq '.result.tools[].name'
```

---

## Troubleshooting

| Symptom | Cause and fix |
|---|---|
| `invalid_grant` | The refresh token was revoked, or the consent screen is in *Testing* mode, which expires tokens after seven days. Re-run `auth`, and publish the app to stop it recurring. |
| `403 insufficient permission` on one property | The `siteUrl` value doesn't match exactly. Run `list_sites` and copy the string verbatim. `https://example.com/` and `sc-domain:example.com` are different properties. |
| `403` mentioning the API being disabled | Enable the Search Console API in the Google Cloud project that issued the credentials. |
| Empty `list_sites` result | You authenticated successfully as a Google account with no properties. You most likely picked the wrong account at the consent screen. |
| Traffic appears to drop sharply in the last few days | The data isn't final yet. Use `lastDays`. |
| The server connects but every data tool fails | It started without usable credentials. Ask the agent to call `check_setup`, or run `npx google-search-console-mcp doctor` in a terminal for the same diagnosis. |
| The agent says the server isn't configured | `check_setup` reports which of the three cases it is: credentials missing, unreadable, or rejected by Google. Its `howToFix` list is the remedy for that case. |
| `-32020 HeaderMismatch` | The client sends modern `_meta` without mirroring the headers. Set `MCP_STRICT_HEADERS=0`. |

---

## Security

- The credential blob is your Google access. Don't commit it, and don't paste it into shared documents. Revoke it at [Google Account permissions](https://myaccount.google.com/permissions).
- HTTP mode binds to `127.0.0.1` by default and validates `Origin` against `ALLOWED_ORIGINS`, which helps prevent DNS rebinding. Unset means no browser origin is allowed, so list them explicitly or use `*` to opt out of the check. The `/health`, `/`, and `/setup` routes are exempt, because they expose no credentialed capability.
- The setup page is stateless. A pending authorization lives in the browser's `sessionStorage`, guarded by a PKCE verifier and a `state` value the page checks against Google's response, so the server never holds a half-finished flow. Set `SETUP_UI=0` to turn the page off entirely.
- Shared-secret comparison is length-checked and constant-time.
- The default Worker deployment stores no credentials at all.
- The `--read-only` flag and `GSC_READ_ONLY=1` block sitemap mutation independently of the granted OAuth scope.

## License

MIT

TDQS

A4.4/5.0

Scored across 11 tools

Disambiguation5/5

Each tool maps to a distinct resource or action: sites, analytics, sitemaps, URL inspection, and setup. The only close pairs are list/get for sites and sitemaps plus inspect_url/inspect_urls, but plural vs. singular and single vs. batch behavior make them unambiguous.

Naming Consistency5/5

Tool names consistently follow a verb_noun pattern with snake_case throughout: list_sites, get_site, submit_sitemap, inspect_url, check_setup. Plural and singular forms match the tool's behavior, so the naming convention is predictable and coherent.

Tool Count5/5

Eleven tools is well-scoped for the Google Search Console domain. Each tool covers a meaningful capability without bloating the surface, and the count aligns with the major API areas: sites, analytics, sitemaps, URL inspection, and configuration.

Completeness4/5

The set covers the primary Search Console workflows well: property listing, search analytics with comparison, full sitemap lifecycle, and single/batch URL inspection. Minor gaps exist such as adding or deleting properties and deeper permission management, but these are not core to typical agent use and can be worked around.

Maintenance

ActivitySlowing
ResponsivenessNo issues