Insights Connector
Hosted MCP server that lets AI assistants read GA4 and Search Console, read-only, per user
$ deploy --project insights-connector
HTTP/2 200 OK
Key Technical Highlights
9 read-only MCP tools (4 GA4, 4 Search Console, 1 account) behind a single wrapper: auth, entitlement, rate limit, Google token, call, error mapping, usage log. No tool can bypass it, so every call is enforced and audited identically.
Built its own OAuth 2.1 authorization server (discovery, dynamic client registration, PKCE S256, audience-bound tokens, rotating refresh tokens with reuse detection) so Google tokens never reach the AI client. Rejected simple token passthrough because it widens the trust boundary.
Read-only Google scopes only; Google refresh tokens encrypted with AES-256-GCM using versioned keys, our own tokens stored as SHA-256 hashes, no analytics payloads persisted. A DB leak yields no usable credentials.
295 automated tests (Vitest) across OAuth, connect flow, quotas, kill switch and admin; real-client testing then caught a consent-POST bug (Origin: null) that unit tests could not, fixed with a regression test.
Per-plan quotas as data (trial: 20 calls/min, 200/day, 1,000 rows/call), Postgres-backed counters instead of Redis, plus a global kill switch and retention cron (usage events pruned after 90 days).
The Case Study
The Problem
AI assistants are good at analysing analytics, but they cannot see the data. The existing options were service-account setups or local MCP servers that need a terminal. I wanted a user to add one URL to their assistant, sign in with Google once, and ask questions about their GA4 and Search Console data, without handing a third-party model a Google credential.
The Numbers
- 9 tools, all read-only:
ga4_list_properties,ga4_get_metadata,ga4_run_report,ga4_run_realtime_report,gsc_list_sites,gsc_search_analytics,gsc_inspect_url,gsc_list_sitemaps,account_status. - Access tokens last 1 hour, refresh tokens 30 days (rotating), auth codes 5 minutes.
- 295 passing tests at the last logged run; 14 Postgres tables across 4 Drizzle migrations.
- Trial plan: 20 calls/minute, 200/day, 1,000 rows per call. Internal plan: 60/minute, 5,000/day.
- Verified on production: a real Claude chat made 3 tool calls (list properties, two GA4 reports) and answered with live data from a production news-publisher property.
Decisions and Trade-offs
Two OAuth layers. The AI client authenticates to my server; my server holds the Google token. Rejected: passing the Google token through to the client, which would let any client act as the user at Google. Tokens are audience-bound to the /mcp URL, and /mcp never accepts a third-party token.
Read-only scopes. Only analytics.readonly and webmasters.readonly. Rejected: write scopes for convenience features. A smaller trust surface is easier to defend and to get through Google verification.
Hash what you can, encrypt what you must. Our tokens and codes are SHA-256 hashed (never need the plaintext). Google refresh tokens must be reused, so they are AES-256-GCM encrypted with versioned keys to allow rotation.
Postgres counters over Redis. Rate limits and quotas live in Postgres. Rejected Redis for a PoC: one fewer service, at the cost of a write per call.
Plain fetch over googleapis. The Google layer uses Zod-parsed REST calls. Rejected the official client: smaller bundle and trivial mocking. Upstream error bodies are parsed and discarded so they cannot leak into logs.
How It Was Verified
Unit and integration tests cover PKCE failures, code replay, refresh rotation and reuse, redirect-URI matching, entitlements and the kill switch. Beyond that I ran smoke tests against production with a real Google account (token refresh, all tools, error paths), then a real Claude connector. That caught a bug the tests missed: browsers send Origin: null after a no-referrer policy, so my CSRF check rejected the consent POST. The fix, a same-origin referrer policy plus a strict Sec-Fetch-Site check, shipped with regression tests.
Honest Limits and What I'd Change
This is a proof of concept. The Google consent screen is published but unverified (capped at 100 users, warning screen), and it runs on Vercel Hobby and my personal accounts. Only Claude is verified end to end; ChatGPT, Cursor and VS Code are untested. Key rotation and rollback are documented in a runbook but not yet drilled. There is no billing. I have no measured business-impact figures, only the working verification above.
My Role
Sole author and owner: architecture, OAuth server, Google data layer, admin dashboard, deployment, DNS, and the migration and runbook docs. Built over two days (2026-10-05 to 2026-10-06), all 29 commits under my name.