Skip to main content
Glama
README.md
# GA4 MCP Server

Production-ready, multi-tenant **Google Analytics 4** MCP server for Claude.

This is the first connector in a larger platform. Shared infrastructure (auth, clients, Google OAuth token storage, authorization, logging, errors) is reusable. GA4-specific code stays under `src/connectors/ga4` and `src/mcp/ga4`. Do not treat this as a monolithic Google Marketing MCP.

## Architecture

```text
Claude
  |  MCP OAuth 2.1 + Streamable HTTP
  v
Next.js / Vercel  (/mcp)
  |-- MCP tools (ga4_*)
  |-- Authorization (client_members only)
  |-- GA4 service
  |-- Google OAuth token manager
  v
Supabase
  |-- clients
  |-- client_members   (application identity)
  |-- google_connections (Google credential, not app identity)
  |-- ga4_properties
```

Application users are Supabase Auth users. Tenant access is **only** `client_members(user_id, client_id, role)`. A Google-connected account is a client-owned credential and is never the application user.

## Local setup

```bash
npm install
cp .env.example .env.local
# fill in the values documented below
npx supabase start   # or link a remote project
npx supabase db reset --local
npm run dev
```

Open [http://localhost:3000](http://localhost:3000), create an application account, create a client, then connect Google Analytics.

## Google Cloud setup

1. Create or select a Google Cloud project.
2. Enable **Google Analytics Data API** and **Google Analytics Admin API**.
3. Configure the OAuth consent screen (External or Internal).
4. Create **OAuth 2.0 Client ID** credentials (Web application).
5. Authorized redirect URI:
   - Local: `http://localhost:3000/api/auth/google/callback`
   - Production: `https://<your-domain>/api/auth/google/callback`
6. Request only GA4 scopes:
   - `https://www.googleapis.com/auth/analytics.readonly`
   - `openid`
   - `email`

Do not add DV360 or Google Ads scopes to this connector.

## GA4 setup

1. Sign in to this app with email/password (application identity).
2. Create a client (you become `owner` in `client_members`).
3. Optionally add other application users as `owner` or `member`.
4. Click **Connect Google Analytics**. This attaches a Google credential to the **client**.
5. Link one or more GA4 properties discovered from that Google account.

A member who did not click Connect can still query the client if they have a `client_members` row.

## Supabase setup

This repo includes SQL migrations in `supabase/migrations`. Create them with the CLI; do not invent filenames.

Local:

```bash
npx supabase start
npx supabase db reset --local
```

Remote:

```bash
npx supabase link --project-ref <project-ref>
npx supabase db push
```

The service role key is server-only. Never expose `SUPABASE_SERVICE_ROLE_KEY` or Google refresh tokens to the browser.

## Environment variables

See [`.env.example`](.env.example). Required:

| Variable | Purpose |
| --- | --- |
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase project URL |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Browser/server user client |
| `SUPABASE_SERVICE_ROLE_KEY` | Server-side admin (never `NEXT_PUBLIC_`) |
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | Google OAuth app |
| `GOOGLE_REDIRECT_URI` | Must match Google Cloud |
| `GOOGLE_OAUTH_SCOPES` | GA4-only scopes |
| `MCP_BASE_URL` | Public origin, e.g. `https://your-app.vercel.app` |
| `MCP_TOKEN_SIGNING_SECRET` | Signs MCP access tokens |
| `TOKEN_ENCRYPTION_KEY` | 64 hex chars (32 bytes) for Google token encryption |

Do not put a GA4 property ID in `.env`. Properties belong to clients in Supabase.

Generate a token encryption key:

```bash
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
```

## MCP setup (Claude)

Remote endpoint:

```text
https://<your-domain>/mcp
```

This server implements MCP OAuth 2.1:

- Protected resource metadata: `/.well-known/oauth-protected-resource`
- Authorization server metadata: `/.well-known/oauth-authorization-server`
- Authorize: `/oauth/authorize` (application login + consent)
- Token: `/oauth/token` (authorization_code + refresh_token, PKCE S256)
- Register: `/oauth/register` (DCR fallback)
- CIMD is advertised (`client_id_metadata_document_supported`)

In Claude: **Settings → Connectors → Add custom connector** → paste `https://<your-domain>/mcp`. Claude will run OAuth. After you approve, tools are available.

Local inspector:

```bash
npx @modelcontextprotocol/inspector
```

Connect with Streamable HTTP to `http://localhost:3000/mcp`. You still need a valid MCP Bearer token from the OAuth flow.

## Tools

| Tool | Purpose |
| --- | --- |
| `ga4_run_report` | Primary report (dates, metrics, dimensions, limit, order, basic filter) |
| `ga4_realtime` | Realtime active users / traffic breakdown |
| `ga4_metadata` | Discover metrics and dimensions for a property |
| `ga4_list_properties` | Properties visible to the client's Google connection |
| `ga4_compare_periods` | Current vs previous period with abs/% change |

Every tool requires `client_id`. Property-scoped tools also require a linked `property_id`. Authorization always checks `client_members` before Google is called.

### Example: `ga4_run_report`

```json
{
  "client_id": "11111111-1111-1111-1111-111111111111",
  "property_id": "123456789",
  "date_start": "2026-08-01",
  "date_end": "2026-08-15",
  "dimensions": ["country"],
  "metrics": ["activeUsers", "sessions", "totalRevenue"],
  "limit": 10
}
```

### Example: `ga4_compare_periods`

```json
{
  "client_id": "11111111-1111-1111-1111-111111111111",
  "property_id": "123456789",
  "current_start": "2026-08-01",
  "current_end": "2026-08-15",
  "previous_start": "2026-07-17",
  "previous_end": "2026-07-31",
  "metrics": ["activeUsers", "sessions", "totalRevenue"]
}
```

Claude can then answer questions such as:

- How many users did we have yesterday?
- Top 10 countries this month
- Compare this month with last month
- How many active users are on the site right now?

## Testing

```bash
npm test
```

Tests use mocked Google and Supabase. They never use production OAuth tokens.

## Vercel

Deploy the Next.js app and set the same environment variables. `MCP_BASE_URL` and `GOOGLE_REDIRECT_URI` must use the production domain. The `/mcp` route runs on the Node.js runtime with a 60s max duration.

## Project layout

```text
src/
  auth/              application authentication
  authorization/     authorizeClient via client_members
  oauth/             Google OAuth + token manager
  mcp-oauth/         MCP OAuth 2.1 authorization server
  connectors/ga4/    Google Analytics APIs
  mcp/ga4/           MCP tools
  repositories/      Supabase data access
  app/mcp/           Streamable HTTP transport
```

Future connectors (DV360, Google Ads) should add `src/connectors/<name>` and `src/mcp/<name>` and reuse the shared core. Do not create a generic Google API dumping ground.