analytics-selfhosted-mcp
Self-hosted MCP server for Google Analytics over HTTP, providing URL-addressable access with API key auth. It enables clients to run reports, list accounts/properties, and use compact paginated reports for token efficiency.
README
analytics-selfhosted-mcp
Unofficial / community project. This is not an official Google product.
Official Google Analytics MCP (local stdio only):
googleanalytics/google-analytics-mcp (PyPI:analytics-mcp).
Self-host the same GA Admin/Data tools over Streamable HTTP on Modal, 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.
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:
# 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):
- Enable Google Analytics Admin API
- Enable Google Analytics Data API
Done when:
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
git clone https://github.com/YOUR_GITHUB_USER/analytics-selfhosted-mcp.git
cd analytics-selfhosted-mcp
cp .env.example .env
Edit .env:
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:
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)
- GCP → IAM → Service Accounts → create SA in
GOOGLE_PROJECT_ID - Create a JSON key → save as e.g.
~/secrets/ga-mcp-sa.json(outside this repo) - GA Admin → Account/Property access → add SA email as Viewer on every property you need
Done when (local check):
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:
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:
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
modal deploy modal_ga_mcp.py
Note the printed web URL:
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:
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:
MODAL_MCP_URL=https://YOUR_WORKSPACE--analytics-mcp-ga-web.modal.run/mcp
Step 5 — Verify MCP protocol (auth + tools)
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
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
cp .cursor/mcp.json.example .cursor/mcp.json
# Edit url → your Modal /mcp URL
{
"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:
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)
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:
run_report_compact→ summary +report_id(full payload stored server-side)get_report_rows→ page rowsdiscard_reportwhen finishedget_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 |
| 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.
openssl rand -hex 24 # rotate MCP_API_KEY → update .env + Modal secret + Cursor
Repo layout
| Path | Role |
|---|---|
modal_ga_mcp.py |
Production MCP (parity + compaction) |
report_compact.py |
Summary / paging helpers |
modal_mcp_ping.py |
Optional ping-only deploy |
.env.example |
Env template |
.cursor/mcp.json.example |
Cursor template |
docs/CLICKUP_ISSUE.md |
ClickUp tools/list issue |
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; this repo hosts them remotely and adds compaction. Not affiliated with Google.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.