Skip to main content
Glama
RobertDWhite

gsc-mcp

by RobertDWhite
README.md
# gsc-mcp

MCP server for **Google Search Console** and **IndexNow** — manage search
indexing programmatically instead of driving a browser.

- **Image:** `ghcr.io/robertdwhite/gsc-mcp`, built by CI in this repo.
- **Transport:** MCP streamable HTTP at `/mcp`, bearer-gated by `MCP_TOKEN`.
  `/healthz` is open.

## Tools

| Tool | Does |
| --- | --- |
| `gsc_status()` | Credential check; reports the service-account email |
| `list_sites()` / `add_site(site_url)` | Search Console properties |
| `verification_token(site_url, method)` | Get FILE/META ownership token |
| `verify_site(site_url, method)` | Complete verification — the service account becomes an owner |
| `add_owner(site_url, email)` | Grant a human Google account access (UI + API) |
| `submit_sitemap(site_url, sitemap_url)` / `list_sitemaps(site_url)` | Sitemap management with indexed-count stats |
| `search_analytics(site_url, days, dimensions, row_limit)` | Clicks/impressions/CTR/position by query, page, country, device, date |
| `inspect_url(site_url, url)` | Per-URL index status, last crawl, canonical |
| `submit_indexnow(urls, host, key)` | Push URLs to Bing/DuckDuckGo/Yandex — **works with no Google setup** |

## One-time Google setup (human, ~5 minutes)

1. In [Google Cloud Console](https://console.cloud.google.com), create/reuse a
   project and enable **Google Search Console API** and **Site Verification API**.
2. Create a **service account** (no roles needed) and a **JSON key**.
3. Put the key JSON in the `GSC_SA_JSON` env var (in-cluster: the
   `gsc-mcp-env` secret).

Then, via tools: `add_site` → `verification_token` (place the token on the
site) → `verify_site` → the service account is a verified owner and every
other tool works. `add_owner` grants your personal account the same property.

## Config (env)

| Var | Meaning |
| --- | --- |
| `MCP_TOKEN` | Bearer token required on `/mcp`. Unset = unauthenticated (dev only). |
| `GSC_SA_JSON` | Service-account key JSON (the raw JSON string). |
| `INDEXNOW_HOST` / `INDEXNOW_KEY` | Optional defaults for `submit_indexnow`. |
| `PORT` | Listen port (default 8080). |

## Deploy

Manifests live in
[whitehouse-rke2](https://github.com/RobertDWhite/whitehouse-rke2) under
`apps/ai/gsc-mcp` (endpoint `https://gsc-mcp.internal.white.fm/mcp`, tailnet
only).

## Connecting clients

The server speaks **MCP streamable HTTP** at `/mcp` and expects
`Authorization: Bearer <MCP_TOKEN>`. Anything below works from any machine
that can reach the endpoint (for the house deploy: tailnet/LAN only).

### Claude Code

```sh
claude mcp add --transport http gsc https://gsc-mcp.internal.white.fm/mcp \
  --header "Authorization: Bearer <MCP_TOKEN>"
```

Or directly in `~/.claude.json`:

```json
"mcpServers": {
  "gsc": {
    "type": "http",
    "url": "https://gsc-mcp.internal.white.fm/mcp",
    "headers": { "Authorization": "Bearer <MCP_TOKEN>" }
  }
}
```

### Claude Desktop

The custom-connector UI expects OAuth; this server uses a static bearer, so
bridge it with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) in
`claude_desktop_config.json` (needs Node):

```json
"mcpServers": {
  "gsc": {
    "command": "npx",
    "args": ["-y", "mcp-remote", "https://gsc-mcp.internal.white.fm/mcp",
             "--transport", "http-only",
             "--header", "Authorization: Bearer <MCP_TOKEN>"]
  }
}
```

### Codex (ChatGPT app / Codex CLI)

Codex configures MCP servers as stdio commands — use the same bridge in
`~/.codex/config.toml`:

```toml
[mcp_servers.gsc]
command = "npx"
args = ["-y", "mcp-remote", "https://gsc-mcp.internal.white.fm/mcp",
        "--transport", "http-only",
        "--header", "Authorization: Bearer <MCP_TOKEN>"]
startup_timeout_sec = 60
```

Restart the app after editing.

### Hermes (or any YAML-config MCP client)

```yaml
mcp_servers:
  gsc:
    url: https://gsc-mcp.internal.white.fm/mcp
    headers:
      Authorization: Bearer ${GSC_MCP_TOKEN}
```

### Sanity check without a client

```sh
curl -s https://gsc-mcp.internal.white.fm/healthz   # -> ok (no auth)
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
  https://gsc-mcp.internal.white.fm/mcp             # -> 401 (auth gate works)
```

Note: `claude.ai` web connectors cannot reach a tailnet-only endpoint; expose
an external route first if you need it there.