Skip to main content
Glama
README.md
# SCMCP — Multi-tenant Google Search Console MCP

A hosted **MCP server** that lets any number of users connect Claude to their
own Google Search Console data — with custom SEO analyses raw GSC can't do:
quick wins, content decay, cannibalisation, CTR outliers, branded splits, folder
roll-ups, intent classification and algorithm-update correlation.

Each user signs in with Google, mints scoped access tokens, and connects Claude.
Every token can read only its owner's data. All tools are **read-only**.

Built by reusing the MCP server from [SearchPulse](https://spacemendigital.com).

---

## Architecture

```
Google sign-in ──► NextAuth ──► Account (OAuth tokens, ENCRYPTED at rest)
                                     │
Claude ──HTTP──► /api/mcp ──► token → userId (+ site scope)  ──► tools ──► GSC API
                     │
              IP + per-token rate limits · revocation · expiry · audit
```

- **Login:** Google OAuth (`webmasters.readonly`), sessions as signed JWTs.
- **Per-user MCP tokens:** `scmcp_` + 256 bits of entropy, stored only as a
  SHA-256 hash. Multiple named tokens per user, each optionally **site-scoped**
  and **expiring**, individually **revocable**, with last-used/IP/count audit.
- **State:** Postgres via Prisma.

---

## MCP hardening

| Measure | Where |
|---|---|
| Tokens stored as SHA-256 hashes only (256-bit entropy) | `lib/mcp-token.ts` |
| Format precheck before any DB hit | `lib/mcp-access.ts` |
| Revoked / expired tokens never authenticate | `lib/mcp-access.ts` |
| One generic 401 for missing/bad/revoked/expired (no enumeration) | `api/mcp/route.ts` |
| Per-token **site allow-list** enforced centrally on every call | `lib/tool-runner.ts` |
| Per-IP + per-token **rate limiting**; DB-backed, degrades to a per-instance in-memory limiter if the DB is down (not fully open) | `lib/rate-limit.ts` |
| Client IP taken only from proxy-controlled headers + IP-validated, so a spoofed `X-Forwarded-For` can't evade the limit or fan out rows | `lib/mcp-access.ts` |
| Use auditing (last used, IP, count) | `lib/mcp-access.ts` |
| Google OAuth tokens **encrypted at rest** (AES-256-GCM); decrypt failure returns null, never raw ciphertext | `lib/auth.ts`, `lib/encryption.ts` |
| Read-only tool surface (no write tools) | `lib/tools.ts` |
| `no-store` + `nosniff` + `no-referrer`; top-level try/catch so errors never leak internals | `api/mcp/route.ts` |
| Header auth preferred; `?key=` fallback for URL-only connectors, disable with `MCP_ALLOW_QUERY_KEY=false` | `lib/mcp-access.ts` |
| Same-origin (CSRF) check on token create/revoke + console routes | `lib/request-guard.ts` |

---

## Setup

### 1. Database

Create a Postgres database (e.g. [Neon](https://neon.tech) free tier) and copy
its connection string.

### 2. Google Cloud

1. [Google Cloud Console](https://console.cloud.google.com) → create/select a project.
2. **Enable APIs** → **Google Search Console API**.
3. **Credentials → Create OAuth client ID → Web application.**
4. **Authorized redirect URIs:**
   - `http://localhost:3000/api/auth/callback/google` (dev)
   - `https://your-app.vercel.app/api/auth/callback/google` (prod)
5. **OAuth consent screen:** External; add users as **Test users** (fine for a
   private, unverified app).

### 3. Configure & run

```bash
npm install
cp .env.example .env.local     # fill in all values
npm run db:push                # create the tables
npm run dev
```

- Landing: <http://localhost:3000>
- Sign in: <http://localhost:3000/login>
- Dashboard (tokens + playground): <http://localhost:3000/dashboard>
- Health: <http://localhost:3000/api/health>
- MCP endpoint: `http://localhost:3000/api/mcp`

Generate secrets with `openssl rand -base64 32` (for `NEXTAUTH_SECRET` and
`ENCRYPTION_KEY`).

---

## Deploy to Vercel + Neon

1. Push to a Git repo and **Import** in [Vercel](https://vercel.com). Add the
   **Neon** integration (or paste a `DATABASE_URL`).
2. Set env vars: `DATABASE_URL`, `NEXTAUTH_URL` (your prod URL),
   `NEXTAUTH_SECRET`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `ENCRYPTION_KEY`.
3. Run the schema once against the prod DB: `npm run db:push` (locally with the
   prod `DATABASE_URL`, or a one-off job).
4. Add the prod redirect URI in Google Cloud. Deploy.

---

## Using it

1. A user visits your URL, signs in with Google, lands on **/dashboard**.
2. They **create a token** (optionally scoped to specific sites / with an expiry)
   and copy it once.
3. They connect Claude with the snippet the dashboard shows:

```bash
claude mcp add --transport http scmcp "https://your-app.vercel.app/api/mcp" --header "Authorization: Bearer <TOKEN>"
```

Then ask: *"What are my quick wins this month for sc-domain:example.com?"*

The dashboard also has a **tool playground** (session-authenticated) to run any
tool against your own data without opening Claude.

---

## Tools

**Live GSC API** — `list_gsc_properties` · `gsc_totals` · `gsc_top_queries` ·
`gsc_top_pages` · `gsc_performance_timeseries` · `gsc_compare_queries` ·
`gsc_compare_pages` · `gsc_page_queries` · `gsc_query_pages` · `gsc_breakdown` ·
`gsc_quick_wins` · `gsc_content_decay` · `gsc_cannibalization` ·
`gsc_new_lost_keywords` · `gsc_ctr_outliers` · `gsc_branded_split` ·
`gsc_folder_performance` · `gsc_query_intent` · `list_algorithm_updates`

**BigQuery bulk export** (historical, unsampled, no 1000-row cap) — `bq_status` ·
`bq_totals` · `bq_top_queries` · `bq_top_pages` · `bq_timeseries` ·
`bq_by_country` · `bq_by_device` · `bq_page_queries` · `bq_query_pages` ·
`bq_custom_query`

Add more in [`src/lib/tools.ts`](src/lib/tools.ts) / [`src/lib/tools-bq.ts`](src/lib/tools-bq.ts)
— one entry per tool; scope enforcement and both transports pick it up automatically.

## BigQuery connector

Search Console's [Bulk Data Export](https://support.google.com/webmasters/answer/12917675)
continuously streams your full, unsampled GSC data into BigQuery. SCMCP queries
that export so Claude can pull historical/large data beyond the live API's limits.

- **No new secrets.** Queries run with the user's **own OAuth token** (SCMCP adds
  the `bigquery.readonly` scope) against the user's **own** Google Cloud project.
  There are no service-account keys to store.
- **Setup (per user):** in Search Console → Settings → *Bulk data export*, point
  the export at a GCP project. Then in the SCMCP dashboard → **Analytics → BigQuery**,
  enter your **project id**, **dataset** (starts with `searchconsole`) and **location**.
  Because BigQuery access is a new scope, existing users **sign out and back in** once.
- **Safety:** every query uses BigQuery **NAMED parameters** (injection-safe),
  caps **bytes billed** (`BQ_MAX_BYTES_BILLED`, default 10 GB), and honours a
  token's site scope. `bq_custom_query` runs a single read-only `SELECT` and is
  disabled on site-scoped tokens.

Ask Claude: *"Pull last 12 months of top queries from BigQuery for sc-domain:example.com."*

## Analytics dashboard

`/dashboard/analytics` renders your data as charts (Recharts): metric cards with
prev-period deltas, a dual-axis performance line chart with a metric toggle,
sortable/searchable top-pages and top-queries tables, and country/device bars.
A **source toggle** switches the whole view between the **Live GSC API** and
**BigQuery**. It's session-authenticated — no MCP token needed to view your own data.

## Stack

Next.js 16 · NextAuth v5 (Google) · Prisma + Postgres · `mcp-handler` ·
`@modelcontextprotocol/sdk` · `googleapis` · `zod`.