analytics-mcp
It is a read-only MCP server that unifies GA4, Cloudflare, Vercel Analytics, and Google Search Console behind one normalized analytics interface that explains what the numbers mean.
List configured sites and which trackers cover each, without exposing credentials.
List which analytics adapters are configured and whether credentials are present.
Query all bound trackers at once for pageviews, sessions, visitors, clicks, impressions, CTR, and position, reporting each source separately and noting divergences.
Fetch the metrics and dimensions available per source.
Run raw native queries to a single source as an escape hatch.
Dry-run/validate queries for unsupported metrics, truncated ranges, or bad comparisons.
Explain whether a gap between two trackers is normal, deterministically.
Find SEO opportunities ranked by estimated missed clicks and explain why a page underperforms its search position.
Report traffic arriving from AI assistants by engine.
Inspect a page's own title, description, canonical, headings, and status, and track page changes over time with search numbers around each change.
Use bundled resources and prompts that teach non-technical readers what metrics mean and how to write site reports.
Provides access to Cloudflare Web Analytics data, allowing agents to query site traffic and performance metrics alongside other analytics providers.
Provides access to Google Analytics 4 (GA4) web analytics data, allowing agents to query traffic, engagement, and performance metrics through a unified analytics interface.
Provides access to Google Search Console data, allowing agents to query search performance and site metrics alongside web analytics.
Provides access to Vercel Analytics data, allowing agents to query website analytics metrics through the same normalized tool surface.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@analytics-mcpCompare last week's traffic across GA4 and Cloudflare and show discrepancies."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
analytics-mcp
A Model Context Protocol server that puts GA4, Cloudflare Web Analytics, Vercel Analytics and Google Search Console behind one normalized tool surface — and, more importantly, teaches the model what the numbers mean.
Ask "how did the site do last week?" and every tracker you run answers at once, with the gaps between them explained instead of hidden.
Remote: StreamableHTTP + OAuth 2.1, deployable to Vercel — usable from Claude web and any remote MCP client.
Local: the same server over stdio for Claude Code, Cursor, Claude Desktop.
Explains itself: every metric carries a plain-language meaning, and the server knows which discrepancies between trackers are structural.
Why another analytics MCP
There are already good GA4 MCP servers. They share three limits: single source, stdio only, and numbers without meaning. This one is built around the other three answers.
One question, every tracker. GA4 and Cloudflare will never agree on
pageviews. Most tools hide that. query fans out to every source bound to a
site, reports each answer separately, and notes where they diverge — because
the divergence is information, not an error.
The gaps are explained, not averaged. The server codifies why two
trackers differ: one counts in the browser after a cookie banner, another
counts raw requests at the network edge including bots. explain_discrepancy
gives a deterministic verdict on whether a gap is normal, and says plainly when
it has no criterion rather than inventing a number.
Written for someone who has never opened an analytics dashboard. Each
metric ships with a businessMeaning in plain words — "How many times a page
was opened. One person who reads three pages counts three times." — and the
bundled prompts instruct the model to lead with the business answer, never the
metric name. The person reading the answer should not need to know what GA4 is.
Related MCP server: Elevate Analytics MCP
Architecture
┌──────────────────────────────┐
Claude web ───► │ api/mcp.ts (serverless) │ ── OAuth 2.1 + PKCE
├──────────────────────────────┤
Claude Code ───► │ src/index.ts (stdio) │ ── local, no auth
├──────────────────────────────┤
private host ──► │ src/serve.ts (long-lived) │ ── static bearer
└───────────────┬──────────────┘
│ all three call
▼
createServer() ← host-agnostic
│
┌───────────────┼───────────────┐
▼ ▼ ▼
tools + prompts semantics/ + seo/ sources/registry
+ resources knowledge.ts ctr-curve │
(the criterion) │
┌───────────┬─────────┼─────────┐
▼ ▼ ▼ ▼
GA4 Cloudflare Vercel GSCseo/ and semantics/ are analysis over the same four sources, not sources of
their own — which is why adding SEO needed no new credentials and no change to
the adapter contract. A fifth source (a third-party SEO API, say) would be one
new file in sources/ plus one line in the registry.
Two rules hold this together, and the test suite enforces both:
createServer()imports nothing from Vercel, HTTP or stdio. It cannot tell where it runs, so there is no environment branch to rot.Environment switching lives in exactly two places — the entry points and
src/sources/registry.ts. An environmentifinside a tool is a bug.
Honest scope
"Remote" means your deployment with your credentials, not a hosted service. There is no multi-tenant mode: you deploy it, you set your own tokens, you are the only tenant. That is a deliberate limit, not a missing feature — analytics credentials read your whole audience, and pooling them behind someone else's service is the wrong trade.
Tools
Tool | What it does |
| Configured sites and which trackers cover each |
| Which adapters are configured (presence only, never values) |
| Metrics and dimensions available per source |
| Primary. Normalized query fanned out across every bound source |
| Escape hatch: a native payload to one source |
| Is this gap between two trackers normal? Deterministic |
| Dry-run: unsupported metrics, truncated ranges, bad comparisons |
| Cheapest search wins, ranked by estimated missed clicks |
| Is one page underperforming its position? Deterministic |
| Traffic arriving from AI assistants, by engine |
| What one page says about itself: title, description, canonical, headings, status |
| What changed on a page and when, with the search numbers either side |
Every tool is read-only. There are no write tools and none are planned.
Canonical metrics: pageviews, sessions, visitors (site-side) and
clicks, impressions, ctr, position (Google Search). Each maps to its
native name per source; a source that cannot answer one returns a warning in
its slot rather than failing the whole query.
Search and AI visibility
Two things usually sold together, kept apart here because only one of them can honestly be measured.
Search. The click-through curve — how much a position normally earns — is computed from your own Search Console rows, never imported. Published CTR-by-position curves swing wildly by industry, language and which SERP features sit above the results; borrowing one would confidently call your pages broken for missing a benchmark they were never subject to. When your own data is too thin around a position, the tools say so instead of inventing an expectation.
Findings are ranked by estimated missed clicks, not by a score out of 100, because a reader can decide on the first and not the second:
position 6.1 · 16,819 impressions · 6 clicks · CTR 0.04%
your other pages at that position earn 0.9% (measured over 33,970 impressions)
→ underperforming, about 146 clicks missed
"The ranking is fine; the invitation is not."AI visibility. ai_referrals reports sessions that arrived from an AI
assistant, broken down by engine. It states in every response that arrivals are
not citations: no API reports whether an assistant mentioned you, plenty of
people read an answer without clicking, and Search Console folds AI Overview
appearances into ordinary impressions. There is deliberately no "GEO visibility
score" here — it would be a fabricated number wearing a data costume.
Reading the page itself
inspect_page fetches a page and reports what it says about itself, so a
click-through verdict can name a cause rather than prescribe blind. It is
deliberately narrow:
Only hosts bound to the site. The allowlist comes from your own configuration, so no argument can point it at a host you never configured. A
sc-domain:Search Console property covers its subdomains, because that is what the property covers; every other binding matches exactly.A bound name still has to point outward. Every address a host resolves to is checked before connecting, so a forgotten subdomain aimed at the internal network is refused even though its name is legitimately yours.
Redirects are reported, never followed. A 301 on a page that ranks is a finding worth having, and not following it removes a whole class of server-side request forgery at the same time.
No JavaScript is run. A page that builds its title in the browser reports no title here — which is also what a crawler sees, so the finding is correct even though the page looks fine to you.
Mechanical findings only. Title length, missing description, duplicate headings, a canonical pointing elsewhere,
noindex, a non-200 status. Whether the wording deserves the click is a judgement left to the reader.
Did the change work
page_changes is the only tool here that reads something this server wrote
itself. A nightly job records how each page looks and writes only when the
content hash moves, so the store holds a change log rather than a time
series — two or three entries a year for a page nobody edits, instead of 365
identical ones.
That also makes a repeated run harmless, which matters: Vercel documents cron delivery as best effort, able to both miss a run and deliver the same one twice, and it never retries a failure. A duplicate run has to be a no-op.
What is deliberately not stored: metrics. Search Console keeps sixteen months of those and is the system of record; a second, worse copy would be a liability, not an asset. Only page state — which nobody else keeps — is written.
It is optional. With no store configured the tools say they have nothing recorded instead of failing, and everything else works exactly as before.
UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN the store (also used by the cache,
under a separate keyspace)
CRON_SECRET required for the scheduled captureLocally there is no scheduler, so pnpm capture runs the same job on demand.
Sizing: fifty pages captured daily, written only on change, stays under 2K commands a month against Upstash's 500K free tier.
Resources and prompts
analytics://metrics— what every metric means, how each source counts it, and how far two sources normally differ.analytics://metrics/{siteId}— the same, plus expectations measured for one site.interpret-query— how to explain results to a non-technical reader.site-report— end-to-end recipe: query every tracker, reconcile the differences, write what happened / what changed / what deserves attention / what to check next.
Setup
Requires Node 20+ and pnpm.
git clone https://github.com/karenrebecag/analytics-mcp.git
cd analytics-mcp
pnpm install
pnpm build1. Describe your sites
SITES_CONFIG is a JSON array mapping a friendly id to per-source
identifiers. See sites.example.json:
[
{
"id": "marketing-site",
"name": "Marketing website",
"sources": {
"ga4": { "propertyId": "123456789" },
"cloudflare": { "zoneId": "0123…", "host": "www.example.com" },
"gsc": { "siteUrl": "sc-domain:example.com" }
}
}
]Each source accepts an optional host to scope it to one subdomain — a GA4
property and a sc-domain: Search Console property both cover every subdomain
at once, so without it a site named blog.example.com would report the whole
estate under that label.
A site needs only the sources you actually run. Optionally add an
expectations block to record the normal gap between two trackers for that
site, which overrides the generic criterion.
2. Credentials
Copy .env.example to .env and fill in what applies. Every source is
optional — the server runs with one, or all four.
Source | Variable | How to get it |
GA4 |
| Service account key (one line). Add its email as a Viewer on each GA4 property. |
Cloudflare |
| Token with Account Analytics: Read. Add |
Vercel |
| Account settings → Tokens. Not |
Search Console |
| Service account added as a user in Search Console. Leave empty to reuse the GA4 key. |
Cloudflare's two modes matter. With an account id it reads the Web Analytics beacon (browser-side, undercounts like GA4). Without one it reads zone HTTP requests (edge-side, includes bots and assets, routinely several times higher). The server knows the difference and adjusts what it calls a normal discrepancy.
3. Verify against your own data
pnpm probeCalls the cheapest real read on every configured source and writes the raw
responses to scratch/ (gitignored). If a source is misconfigured you find out
here rather than mid-conversation. Nothing it does writes to your accounts.
Run it
Local (Claude Code, Cursor, Claude Desktop)
{
"mcpServers": {
"analytics": {
"command": "node",
"args": ["/absolute/path/to/analytics-mcp/dist/index.js"],
"env": { "SITES_CONFIG": "[…]", "GA4_SERVICE_ACCOUNT_JSON": "{…}" }
}
}
}Use an absolute path. Check it without a client:
printf '%s\n%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"c","version":"1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
| node dist/index.jsRemote (Claude web) — deploy to Vercel
Deploy the repo, then set the environment variables from .env.example on the
project. Beyond your source credentials you need:
Variable | Why |
| Signs every token this server issues. Rotating it invalidates all of them. |
| Your sign-in page — see the contract below. |
| Auth state. Required in production: without it every token exchange fails closed. Vercel's Upstash integration injects |
| Optional. Restrict access to one email domain; empty means no restriction. |
| Optional. Also accept Clerk session tokens as bearers. This is the public PEM (Clerk → API keys → Show JWT public key), so the server never holds a credential that can mint or revoke sessions. |
Then add https://your-deployment/mcp as a custom connector in Claude. The
OAuth flow starts on its own — the 401 carries the WWW-Authenticate header
that triggers it.
The sign-in page contract
This server implements OAuth 2.1 — dynamic client registration, PKCE with S256
pinned, single-use authorization codes, refresh rotation with reuse detection,
and revocation — but it does not authenticate anyone itself. /authorize
validates the request and redirects to FRONTEND_URL, a page you host, which
must:
authenticate the user however you like — Clerk, your own login, anything;
mint an authorization code as a JWT signed with
MCP_SIGNING_SECRET, carryingsub, optionallyemail, and echoing backclient_id,redirect_uri,code_challengeandcode_challenge_method, plus a uniquejti. Issuer<MCP_ISSUER>-oauth, audience<MCP_ISSUER>(defaultanalytics-mcp);redirect back to
redirect_uriwith that code and the originalstate.
Everything after that — verifying PKCE, binding the code to its client, enforcing single use, issuing and rotating tokens — happens here. Keeping the login page outside this repo is what lets the same code serve any identity provider without a fork.
Persistent host (optional)
pnpm serve runs a long-lived HTTP server instead of serverless functions.
It is not the default path; it exists so that reaching a source which needs a
machine that stays up is a new adapter plus this entry, not a rewrite.
Auth is a static SERVE_BEARER_TOKEN, meant to sit behind a reverse proxy.
Without a token it refuses to bind anything but loopback — an
unauthenticated MCP server reachable from the network is a data leak, not a
convenience.
Notes that will save you time
Quotas and cache. GA4 charges tokens per property per day. query caches
each successful per-source result for 5 minutes, keyed by a hash of the
normalized request (QUERY_CACHE_TTL_S to change it). On serverless, set
Upstash — an in-process cache dies with the isolate and buys you nothing.
Slow sources degrade, they do not fail. Each source gets its own timeout
(QUERY_SOURCE_TIMEOUT_MS, default 10s). A source that times out becomes an
entry in errors; the rest of the answer still arrives. A missing source means
no data from that source — never zero traffic.
Row caps truncate silently upstream. Cloudflare returns at most 40 daily
rows, Search Console 1000, GA4 10000. validate_query warns before you read
numbers that quietly left data out.
Criterion is self-calibrating. Both the cross-source discrepancy ranges and the CTR curve are derived from your data or from documented mechanism, never from an imported benchmark. Where no criterion exists, the tools return "no criterion recorded" rather than a plausible guess — a wrong number that sounds right is worse than an honest gap.
Adapters are typed against real captures, never against documentation.
pnpm probe first, then the adapter. Docs drift; captured responses do not.
Development
pnpm verify # typecheck + lint + format + unit tests + build gatespnpm gates builds the project and runs tests/gates/ against the compiled
artifact: a small security suite (secret hygiene, PKCE pinning, redirect_uri
allowlist, fail-closed auth state, no logs on the MCP channel) and a
performance suite (cold start, parallel fan-out, timeout isolation, cache
hits). They are budgets and invariants, not benchmarks. Do not weaken a gate to
make it pass — a red gate means the implementation is wrong.
SPEC.md holds the per-file implementation contracts and the full gate list.
License
MIT. Not affiliated with Google, Cloudflare or Vercel.
Available Tools
2 toolslist_sitesARead-only
List configured sites (id, name, bound source keys). Binding values stay server-side.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The annotations already declare readOnlyHint=true and openWorldHint=true, covering the safety profile. The description adds useful behavioral context beyond that by stating 'Binding values stay server-side', which clarifies that sensitive binding values are not included in the response.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single, compact sentence that front-loads the action and resource, then adds a valuable behavior note. Every word earns its place with no redundancy or filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
This is a simple, zero-parameter read-only list operation. The description names the output fields and clarifies a key behavior, which is sufficient given the absence of an output schema. Minor details like pagination or ordering are not addressed, but they are not clearly essential for a basic site-listing tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters and the schema coverage is 100%, so the schema fully handles parameter semantics. The description adds no parameter meaning, but none is needed; the baseline for a zero-parameter tool applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the action ('List') and the resource ('configured sites') and even enumerates the returned fields (id, name, bound source keys). However, it does not explicitly differentiate this from its sibling 'list_sources', so the agent must rely on the tool name to distinguish them.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies the tool is used to list sites, but it gives no guidance on when to choose this over the sibling tool 'list_sources'. No exclusions, conditions, or alternative-selection guidance is provided.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_sourcesARead-only
List registered analytics adapters and whether their credentials are present.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already convey read-only and open-world behavior, so the description only needs to add context. It does add that the output includes credential presence, which is useful, but it does not clarify details such as whether credentials are validated or whether the list is exhaustive. The description is not contradictory to the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single, compact sentence that front-loads the core purpose and avoids filler. Every word adds meaning.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a parameterless, read-only listing tool with no output schema, the description provides sufficient context: what is listed and what aspect of those items is reported. The annotations cover the safety profile, so nothing critical is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters and the schema coverage is 100%, so there is no parameter meaning for the description to add. The baseline of 4 for a no-parameter tool is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific action ('List') and a specific resource ('registered analytics adapters') with an additional useful detail (credential presence). This clearly distinguishes it from the sibling tool list_sites, which targets a different resource type.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The resource name makes the intended use inferable—use this when you need analytics adapters and their credential status—but the description does not explicitly contrast it with list_sites or state when not to use it. Usage guidance is implied rather than stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
2 tool updates
v0.1.0- First observed
list_sites - First observed
list_sources
TDQS
Scored across 2 tools
The two tools target distinct resources: sites (configured web properties) and sources (adapter credentials). Their descriptions clearly separate these concepts, so an agent is unlikely to confuse them.
Both tools follow the same 'list_[plural noun]' pattern, making the naming fully consistent and predictable.
With only two tools, the server feels very thin for an analytics context, aligning with the borderline category of 1-2 tools. The tools are simple list operations, but the count itself is minimal.
The server covers only read-only listing of sites and sources, with no create, update, or delete operations, nor any analytics data access. For a server named 'analytics-mcp,' this is a significant functional gap.
Maintenance
Related MCP Connectors
Read and edit GA4, Search Console and Google Tag Manager from any MCP client. 29 tools.
MCP server for querying and analyzing data from ad platforms, analytics tools, and spreadsheets
First-party unsampled Shopify analytics MCP — sessions, attribution, funnels, leaks. 26 tools.
Marketing-measurement MCP: dashboards, cross-channel queries, integrations, and knowledge files.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables querying Google Analytics 4 data using natural language through MCP clients like Claude and Cursor, supporting 200+ dimensions and metrics for traffic, user behavior, and e-commerce analysis.MIT
- FlicenseNot gradedqualityCmaintenanceProduction-ready MCP server integrating Google Search Console, GA4, and PageSpeed Insights for SEO and analytics intelligence, enabling natural-language queries to Google analytics data.-
- AlicenseNot gradedqualityAmaintenanceUnifies Google Search Console, Bing Webmaster Tools, GA4, and PageSpeed APIs behind one MCP server, letting an agent join search, indexing, and analytics data across providers while staying read-only and boundary-limited by default.Do What The F*ck You Want To Public
- AlicenseNot gradedqualityCmaintenanceEnables querying Google Analytics 4 properties through natural language, supporting historical and real-time reports, metadata discovery, and property listing via a Vercel-deployed MCP server.42 npmMIT