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.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。