Skip to main content
Glama
ukituki

analytics-selfhosted-mcp

by ukituki
README.md
# analytics-selfhosted-mcp

> **Unofficial / community project.** This is **not** an official Google product.  
> Official Google Analytics MCP (local **stdio** only):  
> **[googleanalytics/google-analytics-mcp](https://github.com/googleanalytics/google-analytics-mcp)** (PyPI: `analytics-mcp`).

Self-host the **same GA Admin/Data tools** over **Streamable HTTP** on [Modal](https://modal.com), so clients that only accept a public URL (Cursor remote MCP, agents, etc.) can call Google Analytics with an API key — plus **token-efficient report compaction** that upstream does not ship.

| | Official Google MCP | This project |
|--|---------------------|--------------|
| Status | Official | **Unofficial** wrapper / host |
| Transport | stdio | Streamable HTTP (`https://…/mcp`) |
| Runs on | Your laptop | Modal (or any host you deploy) |
| Auth to MCP | Process trust | Shared API key |
| Extra | — | Compact reports + paged rows |

**When to use official:** one machine, Cursor/Claude Desktop via `command` + stdio.  
**When to use this:** you need a **URL-addressable** Analytics MCP.

ClickUp App Center auth works but tools do not load yet (client never calls `tools/list`). Details: [docs/CLICKUP_ISSUE.md](docs/CLICKUP_ISSUE.md).

---

## Quick path (what “done” looks like)

| Step | Done when… |
|------|------------|
| 0. Prerequisites | `uv`, `modal`, GCP APIs enabled |
| 1. Env file | `.env` exists, keys set, **not** in git |
| 2. Google credentials | SA (or ADC) can list GA accounts |
| 3. Modal secret | Secret `analytics-mcp-creds` created |
| 4. Deploy | `/healthz` returns JSON `status: ok` |
| 5. MCP smoke | `tools/list` returns **15** tool names |
| 6. GA smoke | `get_account_summaries` returns accounts/properties |
| 7. Cursor | Chat can call `ping` → `pong` |

Do not skip the **Done when** checks — each later step depends on the previous one.

---

## Use cases and suggested queries

Ask these in Cursor (or any MCP client) after step 7. Complexity increases downward.

### Level 1 — Connectivity & inventory

| Use case | Suggested prompt |
|----------|------------------|
| Prove MCP works | “Call `ping` on the Analytics MCP.” |
| List what I can access | “Use `get_account_summaries` and list my GA accounts and property IDs.” |
| Inspect one property | “Call `get_property_details` for property `PROPERTY_ID` and summarize timezone, currency, and industry.” |

**Pass:** you get real account/property names (not 401 / empty / credential errors).

### Level 2 — Simple reporting

| Use case | Suggested prompt |
|----------|------------------|
| Last 7 days traffic | “For property `PROPERTY_ID`, run a report for the last 7 days with dimension `date` and metrics `sessions`, `totalUsers`. Prefer `run_report_compact`.” |
| Top channels | “Same property, last 28 days: dimension `sessionDefaultChannelGroup`, metrics `sessions` and `conversions`. Summarize the top 5.” |
| Realtime | “Call `run_realtime_report` for `PROPERTY_ID` with metric `activeUsers` and tell me what’s live now.” |

**Pass:** summary includes a `report_id` (compact) or clear row values; numbers look plausible vs GA UI.

### Level 3 — Metadata & quality

| Use case | Suggested prompt |
|----------|------------------|
| Custom definitions | “Use `get_custom_dimensions_and_metrics` on `PROPERTY_ID` and list custom metrics I might use in reports.” |
| Annotations | “Call `list_property_annotations` for `PROPERTY_ID` for the last 90 days and relate them to traffic dips.” |
| Ads links | “List Google Ads links for `PROPERTY_ID` with `list_google_ads_links`.” |

**Pass:** tool returns structured data (or a clear empty list if none configured).

### Level 4 — Funnels, conversions, large data

| Use case | Suggested prompt |
|----------|------------------|
| Conversions | “Use `run_conversions_report` for `PROPERTY_ID` last 30 days and rank conversion events.” |
| Funnel | “Build a `run_funnel_report` for signup: page `/` → `/pricing` → `/signup` (adjust paths to my site).” |
| Large report without blowing context | “Run a high-cardinality report with `run_report_compact`, then page with `get_report_rows` (offset 0, limit 50), then `discard_report`.” |

**Pass:** compact path returns small summary + `report_id`; paging returns slices; discard succeeds.

### Level 5 — Analysis workflows

| Use case | Suggested prompt |
|----------|------------------|
| Week-over-week | “Compare this week vs last week sessions and users for `PROPERTY_ID`; call out biggest day-over-day changes.” |
| Landing page triage | “Top landing pages by sessions last 14 days; flag pages with high sessions and low engagement (use engagement metrics available on the property).” |
| Incident narrative | “Combine annotations + daily sessions for the last 60 days into a short incident timeline.” |

**Pass:** answers cite tool results (property id, dates, metrics), not invented UI screenshots.

---

## Step-by-step setup

### Step 0 — Prerequisites

Install:

```bash
# Python 3.12+ recommended
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install modal
modal setup   # browser login
```

In Google Cloud (same project you will put in `.env`):

1. Enable **Google Analytics Admin API**
2. Enable **Google Analytics Data API**

**Done when:**

```bash
uv --version          # prints a version
modal --version       # prints a version
modal profile current # shows your logged-in profile / workspace
```

**If it fails:** re-run `modal setup`; confirm the GCP project has billing/API enablement as required by Google.

---

### Step 1 — Clone and create `.env`

```bash
git clone https://github.com/YOUR_GITHUB_USER/analytics-selfhosted-mcp.git
cd analytics-selfhosted-mcp
cp .env.example .env
```

Edit `.env`:

```bash
GOOGLE_PROJECT_ID=your-gcp-project-id
MCP_API_KEY=$(openssl rand -hex 24)
ANALYTICS_MCP_API_KEY=$MCP_API_KEY   # must match MCP_API_KEY
```

**Done when:**

```bash
set -a && source .env && set +a
test -n "$GOOGLE_PROJECT_ID" && test -n "$MCP_API_KEY" && test "$MCP_API_KEY" = "$ANALYTICS_MCP_API_KEY" && echo "env ok"
git check-ignore -v .env   # must show .gitignore rule
```

**If it fails:** keys empty or mismatched — fix `.env`. If `.env` is not ignored, stop and fix `.gitignore` before any commit.

---

### Step 2 — Google credentials (service account recommended)

1. GCP → **IAM → Service Accounts** → create SA in `GOOGLE_PROJECT_ID`
2. Create a JSON key → save as e.g. `~/secrets/ga-mcp-sa.json` (**outside** this repo)
3. GA Admin → **Account/Property access** → add SA email as **Viewer** on every property you need

**Done when (local check):**

```bash
export GOOGLE_APPLICATION_CREDENTIALS=~/secrets/ga-mcp-sa.json
# Optional: quick Admin API sanity via gcloud / any GA client you prefer.
# On Modal, success is verified in Step 6 (get_account_summaries).
ls -la "$GOOGLE_APPLICATION_CREDENTIALS"   # file exists, contains "client_email"
```

**If it fails later with empty accounts / 403:** SA missing Viewer on the GA property, or wrong JSON in the Modal secret.

Temporary alternative: user ADC (`gcloud auth application-default login` with Analytics readonly scopes). Prefer SA for cloud.

---

### Step 3 — Modal secret

Secret name must be exactly `analytics-mcp-creds`:

```bash
set -a && source .env && set +a

modal secret create analytics-mcp-creds \
  MCP_API_KEY="$MCP_API_KEY" \
  GOOGLE_PROJECT_ID="$GOOGLE_PROJECT_ID" \
  GOOGLE_APPLICATION_CREDENTIALS_JSON="$(cat ~/secrets/ga-mcp-sa.json)" \
  --force
```

**Done when:**

```bash
modal secret list | grep analytics-mcp-creds
```

**If it fails:** path to JSON wrong; or JSON not pasted as **file contents** (must start with `{`, not a filesystem path string).

---

### Step 4 — Deploy

```bash
modal deploy modal_ga_mcp.py
```

Note the printed web URL:

```text
https://YOUR_WORKSPACE--analytics-mcp-ga-web.modal.run
```

(The Modal **app id** remains `analytics-mcp-ga` so existing deployments keep a stable hostname.)

**Done when:**

```bash
curl -sS "https://YOUR_WORKSPACE--analytics-mcp-ga-web.modal.run/healthz"
# Expect: {"status":"ok","app":"analytics-mcp-ga"}
```

**If it fails:** check Modal dashboard logs for import/boot errors; confirm secret name spelling.

Optional: set in `.env`:

```bash
MODAL_MCP_URL=https://YOUR_WORKSPACE--analytics-mcp-ga-web.modal.run/mcp
```

---

### Step 5 — Verify MCP protocol (auth + tools)

```bash
set -a && source .env && set +a
URL="${MODAL_MCP_URL:-https://YOUR_WORKSPACE--analytics-mcp-ga-web.modal.run/mcp}"
KEY="$MCP_API_KEY"

curl -sS -D /tmp/mcp.hdr -o /tmp/mcp.init -X POST "$URL" \
  -H "Authorization: $KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"setup","version":"0"}}}'

# Expect HTTP 200 and a mcp-session-id header
grep -i mcp-session-id /tmp/mcp.hdr
cat /tmp/mcp.init

SID=$(awk 'BEGIN{IGNORECASE=1} /^mcp-session-id:/{print $2}' /tmp/mcp.hdr | tr -d '\r')

curl -sS -X POST "$URL" \
  -H "Authorization: $KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "mcp-session-id: $SID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
  | tee /tmp/mcp.tools
```

**Done when:** `/tmp/mcp.tools` lists **15** tools, including:

- Upstream (9): `get_account_summaries`, `list_google_ads_links`, `get_property_details`, `list_property_annotations`, `get_custom_dimensions_and_metrics`, `run_report`, `run_realtime_report`, `run_funnel_report`, `run_conversions_report`
- Host (6): `ping`, `run_report_compact`, `get_report_rows`, `get_report_summary`, `get_report_full`, `discard_report`

**If it fails:**

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| HTTP 401 | Wrong key | Match `.env` ↔ Modal secret; recreate secret with `--force` |
| HTTP 307 loop | Old client hitting wrong slash handling | Use `/mcp` (no slash) or `/mcp/` (both should work on current deploy) |
| HTTP 406 | Accept header | Send `Accept: application/json` on POST |
| Empty / error body on initialize | App crash on boot | Modal logs for the `web` function |
| Fewer than 15 tools | Stale deploy | Re-run `modal deploy modal_ga_mcp.py` |

---

### Step 6 — Verify Google Analytics access

```bash
set -a && source .env && set +a
export MODAL_MCP_URL="${MODAL_MCP_URL:-https://YOUR_WORKSPACE--analytics-mcp-ga-web.modal.run/mcp}"

modal run modal_ga_mcp.py::test_account_summaries
# Then pick a property id from the output:
modal run modal_ga_mcp.py::test_run_report --property-id=YOUR_GA4_PROPERTY_ID
modal run modal_ga_mcp.py::test_run_report_compact --property-id=YOUR_GA4_PROPERTY_ID
```

**Done when:** account summaries show your properties; report/compact tests print row data / a `report_id`.

**If it fails:**

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| Empty `[]` accounts | SA not granted on GA | Add Viewer on account/property |
| 403 / permission errors | Wrong project or APIs off | Enable Admin + Data APIs; fix `GOOGLE_PROJECT_ID` |
| Credential JSON errors | Bad secret payload | Secret value must be raw JSON object |

---

### Step 7 — Connect Cursor

```bash
cp .cursor/mcp.json.example .cursor/mcp.json
# Edit url → your Modal /mcp URL
```

```json
{
  "mcpServers": {
    "analytics-selfhosted-mcp": {
      "url": "https://YOUR_WORKSPACE--analytics-mcp-ga-web.modal.run/mcp",
      "headers": {
        "x-api-key": "${env:ANALYTICS_MCP_API_KEY}"
      }
    }
  }
}
```

Cursor **does not** load project `.env` for remote MCP headers. Export before launching Cursor:

```bash
set -a && source .env && set +a
export ANALYTICS_MCP_API_KEY
# launch Cursor from this shell, or put ANALYTICS_MCP_API_KEY in your shell profile / direnv
```

Reload MCP servers in Cursor.

**Done when:** MCP shows tools; in chat: “Call `ping`” → `pong`.

**If it fails:**

| Symptom | Fix |
|---------|-----|
| Unauthorized / tools missing | Env var not visible to Cursor — launch from a shell where `echo $ANALYTICS_MCP_API_KEY` is non-empty |
| Wrong URL | Must end with `/mcp`; copy from Modal deploy output |
| Stale config | Restart Cursor or reload MCP after editing `mcp.json` |

Some hosts prefer `Authorization` instead of `x-api-key` (raw key or `Bearer …`). This server accepts both.

---

## How data moves (read once)

```text
Client  --tools/call-->  Modal MCP  --GA API-->  Google
                ^                         |
                +---- tool result JSON ---+
```

Tool results still enter the **model context**. Large `run_report` payloads can blow the context window.

**Prefer for big reports:**

1. `run_report_compact` → summary + `report_id` (full payload stored server-side)
2. `get_report_rows` → page rows
3. `discard_report` when finished  
4. `get_report_full(..., confirm=true)` only if you intentionally want everything in-context

---

## Troubleshooting (cheat sheet)

| Problem | Check |
|---------|--------|
| 401 on `/mcp` | Key mismatch `.env` vs Modal secret |
| Healthz OK, MCP fails | Auth headers; path `/mcp` |
| Tools list OK, GA empty | SA Viewer on property; APIs enabled |
| Cursor can’t auth | `ANALYTICS_MCP_API_KEY` not in Cursor’s environment |
| ClickUp “no tools” | Known client gap — [docs/CLICKUP_ISSUE.md](docs/CLICKUP_ISSUE.md) |
| High Modal bill | `min_containers=1` keeps a warm replica; change only if you accept cold starts |

Still stuck: Modal function logs for `analytics-mcp-ga` / `web`, plus the curl transcript from Step 5.

---

## Security

**Never commit:** `.env`, ADC/SA JSON, `client_secret_*.json`, `.cursor/mcp.json`.

Checklist: [docs/PUBLISHING.md](docs/PUBLISHING.md).

```bash
openssl rand -hex 24   # rotate MCP_API_KEY → update .env + Modal secret + Cursor
```

---

## Repo layout

| Path | Role |
|------|------|
| [`modal_ga_mcp.py`](modal_ga_mcp.py) | Production MCP (parity + compaction) |
| [`report_compact.py`](report_compact.py) | Summary / paging helpers |
| [`modal_mcp_ping.py`](modal_mcp_ping.py) | Optional ping-only deploy |
| [`.env.example`](.env.example) | Env template |
| [`.cursor/mcp.json.example`](.cursor/mcp.json.example) | Cursor template |
| [`docs/CLICKUP_ISSUE.md`](docs/CLICKUP_ISSUE.md) | ClickUp tools/list issue |
| [`docs/PUBLISHING.md`](docs/PUBLISHING.md) | Public-repo security checklist |

Optional ping-only: `modal deploy modal_mcp_ping.py` (same `analytics-mcp-creds` secret).

---

## License

MIT. GA tool callables come from Google’s [`analytics-mcp`](https://github.com/googleanalytics/google-analytics-mcp); this repo hosts them remotely and adds compaction. Not affiliated with Google.