Skip to main content
Glama
karenrebecag

analytics-mcp

by karenrebecag

analytics-mcp

CI License: MIT Node 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     GSC

seo/ 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:

  1. createServer() imports nothing from Vercel, HTTP or stdio. It cannot tell where it runs, so there is no environment branch to rot.

  2. Environment switching lives in exactly two places — the entry points and src/sources/registry.ts. An environment if inside 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

list_sites

Configured sites and which trackers cover each

list_sources

Which adapters are configured (presence only, never values)

get_schema

Metrics and dimensions available per source

query

Primary. Normalized query fanned out across every bound source

query_raw

Escape hatch: a native payload to one source

explain_discrepancy

Is this gap between two trackers normal? Deterministic

validate_query

Dry-run: unsupported metrics, truncated ranges, bad comparisons

seo_opportunities

Cheapest search wins, ranked by estimated missed clicks

explain_ctr_gap

Is one page underperforming its position? Deterministic

ai_referrals

Traffic arriving from AI assistants, by engine

inspect_page

What one page says about itself: title, description, canonical, headings, status

page_changes

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 capture

Locally 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 build

1. 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

GA4_SERVICE_ACCOUNT_JSON

Service account key (one line). Add its email as a Viewer on each GA4 property.

Cloudflare

CLOUDFLARE_API_TOKEN

Token with Account Analytics: Read. Add CLOUDFLARE_ACCOUNT_ID to read Web Analytics (RUM); without it the adapter reads zone HTTP analytics instead.

Vercel

VC_API_TOKEN

Account settings → Tokens. Not VERCEL_API_TOKEN: Vercel reserves the VERCEL_ prefix and rejects it as invalid on its own deployments. Both names are read elsewhere.

Search Console

GSC_SERVICE_ACCOUNT_JSON

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 probe

Calls 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.js

Remote (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

MCP_SIGNING_SECRET

Signs every token this server issues. Rotating it invalidates all of them.

FRONTEND_URL

Your sign-in page — see the contract below.

UPSTASH_REDIS_REST_URL + _TOKEN

Auth state. Required in production: without it every token exchange fails closed. Vercel's Upstash integration injects KV_REST_API_*, which is also accepted.

ALLOWED_EMAIL_DOMAIN

Optional. Restrict access to one email domain; empty means no restriction.

CLERK_JWT_KEY

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. CLERK_SECRET_KEY works too, but grants far more than verification needs.

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:

  1. authenticate the user however you like — Clerk, your own login, anything;

  2. mint an authorization code as a JWT signed with MCP_SIGNING_SECRET, carrying sub, optionally email, and echoing back client_id, redirect_uri, code_challenge and code_challenge_method, plus a unique jti. Issuer <MCP_ISSUER>-oauth, audience <MCP_ISSUER> (default analytics-mcp);

  3. redirect back to redirect_uri with that code and the original state.

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 gates

pnpm 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 tools
list_sitesA
Read-only

List configured sites (id, name, bound source keys). Binding values stay server-side.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A3.7/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters4/5

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.

Purpose4/5

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.

Usage Guidelines2/5

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_sourcesA
Read-only

List registered analytics adapters and whether their credentials are present.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.1/5.0
Behavior3/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines3/5

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.

  1. 2 tool updatesv0.1.0
    • First observedlist_sites
    • First observedlist_sources

TDQS

A3.8/5.0

Scored across 2 tools

Disambiguation5/5

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.

Naming Consistency5/5

Both tools follow the same 'list_[plural noun]' pattern, making the naming fully consistent and predictable.

Tool Count3/5

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.

Completeness2/5

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

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables 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
  • F
    license
    Not graded
    quality
    C
    maintenance
    Production-ready MCP server integrating Google Search Console, GA4, and PageSpeed Insights for SEO and analytics intelligence, enabling natural-language queries to Google analytics data.
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    Unifies 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
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables 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 npm
    MIT