How to Build a Google Analytics MCP Server

Alejandro Rioja
Alejandro Rioja
5 min read
TL;DR

Try Google's own GA4 MCP server first: it's experimental, runs locally in Python, and covers GA4 on your machine. To build your own, write one run_report tool with the MCP TypeScript SDK and @google-analytics/data, log in with gcloud and the analytics.readonly scope, and register it with claude mcp add. Hosting it for other people is the hard part: you need MCP's OAuth flow, Google's verification for a sensitive scope (unverified apps are capped at 100 users), and caching to stay inside the Data API's per-property quotas.

Free newsletter

Every Wednesday. 28,400+ operators. Zero fluff.

A GA4 MCP server that runs on your laptop is an afternoon’s work. One that other people can sign in to is a much bigger job. This guide gives you the afternoon version, then lists what the bigger one involves, so you can decide which you need.

Table of contents

Open Table of contents

Check the official server first

Google publishes its own Google Analytics MCP server, still marked experimental. It runs on your own computer in Python, and you enable the Google Analytics Admin API and Data API in a Google Cloud project before it works. It already has run_report, run_realtime_report, run_funnel_report and lookups for your accounts, properties, custom dimensions and Google Ads links. If you only need GA4, only on your machine, use it:

bash
gcloud auth application-default login \
  --scopes https://www.googleapis.com/auth/analytics.readonly,https://www.googleapis.com/auth/cloud-platform \
  --client-id-file=YOUR_CLIENT_JSON_FILE

pipx run analytics-mcp

Google’s setup page walks through the Cloud project, and the README has the exact claude mcp add line for Claude Code. There’s no hosted version, so it only works where you can run Python.

Build your own when you want tools shaped around your questions, data from more than one source in a single answer, or a server your team or clients can use.

What you need

  • A Google Cloud project with the Google Analytics Data API enabled (five minutes).
  • An OAuth client ID for a desktop app in that project, downloaded as JSON. That’s the file --client-id-file points to.
  • Node 22 or newer, which the current @google-analytics/data package requires.
  • Viewer access to the GA4 property, on the Google account you’ll sign in with.

Step 1: One tool that runs any report

Start with a single run_report tool. Claude already knows GA4’s metric and dimension names, so one flexible tool covers most questions. Split it into narrower tools later, once you can see which questions you actually ask.

Set up the folder and log in once. The second gcloud command matters: Google’s Analytics APIs reject user credentials that don’t name a quota project.

bash
mkdir ga4-mcp && cd ga4-mcp
npm init -y && npm pkg set type=module
npm install @modelcontextprotocol/sdk @google-analytics/data zod

gcloud auth application-default login \
  --scopes https://www.googleapis.com/auth/analytics.readonly,https://www.googleapis.com/auth/cloud-platform \
  --client-id-file=YOUR_CLIENT_JSON_FILE
gcloud auth application-default set-quota-project YOUR_PROJECT_ID

Then save this as ga4.ts:

typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { BetaAnalyticsDataClient } from '@google-analytics/data';
import { z } from 'zod';

const ga = new BetaAnalyticsDataClient(); // Application Default Credentials
const server = new McpServer({ name: 'ga4', version: '1.0.0' });

server.registerTool(
  'run_report',
  {
    description:
      'Run a GA4 report. Uses GA4 Data API metric and dimension names. ' +
      'Dates accept YYYY-MM-DD, "28daysAgo", "yesterday", "today".',
    inputSchema: {
      propertyId: z.string().describe('Numeric GA4 property ID, e.g. 123456789'),
      metrics: z.array(z.string()).describe('e.g. ["sessions", "engagedSessions"]'),
      dimensions: z.array(z.string()).default([]).describe('e.g. ["sessionDefaultChannelGroup"]'),
      startDate: z.string().default('28daysAgo'),
      endDate: z.string().default('today'),
    },
  },
  async ({ propertyId, metrics, dimensions, startDate, endDate }) => {
    const [res] = await ga.runReport({
      property: `properties/${propertyId}`,
      dateRanges: [{ startDate, endDate }],
      metrics: metrics.map((name) => ({ name })),
      dimensions: dimensions.map((name) => ({ name })),
      limit: 100,
    });
    // Turn GA's nested rows into plain { dimension: value, metric: value } objects.
    const rows = (res.rows ?? []).map((row) => {
      const out: Record<string, string> = {};
      row.dimensionValues?.forEach((v, i) => (out[dimensions[i]] = v.value ?? ''));
      row.metricValues?.forEach((v, i) => (out[metrics[i]] = v.value ?? ''));
      return out;
    });
    return { content: [{ type: 'text', text: JSON.stringify({ rowCount: res.rowCount, rows }) }] };
  },
);

await server.connect(new StdioServerTransport());

There’s no key in the file. The client picks up the login gcloud just saved, and every call runs as you, with your Viewer access.

Step 2: Add it to Claude

In Claude Code, one command registers it. Use the full path to the file. --scope user makes it available in every project; leave it off and it only loads in the folder where you ran the command:

bash
claude mcp add --scope user ga4 -- npx -y tsx /full/path/to/ga4-mcp/ga4.ts

Then ask: “Using property 123456789, which channels drove the most engaged sessions last month?” Claude picks the metrics, calls the tool, and explains the rows. On Claude Desktop the same server goes in the config file instead; my first MCP server guide shows where that lives.

Step 3: Where it gets hard

Everything above runs as you, on your machine. Here’s what changes when it has to run anywhere else:

  1. Hosting and OAuth. claude.ai and the mobile app can’t reach a server on your laptop. A hosted server needs MCP’s OAuth flow, so each user signs in to their own Google account, plus somewhere safe to keep their refresh tokens.
  2. Google’s app verification. analytics.readonly is a sensitive scope. Until Google verifies your app, every user sees an “unverified app” warning and you’re capped at 100 users. Verification means a privacy policy on your own domain, a demo video of the sign-in, a written reason for each scope and a review.
  3. Quotas. The Data API meters tokens per property: 40,000 an hour and 200,000 a day on a standard property, with any one Cloud project capped at 14,000 an hour per property and 10 requests at once. Most requests cost 10 tokens or fewer. Long date ranges, many dimensions and busy properties cost more, and an agent fires reports much faster than a person clicking through GA4. Hosted, every call comes out of your project’s share, so add returnPropertyQuota: true to see what each request costs, cache what repeats, and keep reports small.

SiteWide takes care of the hosting and the sign-in. Its Google verification is still in progress, so GA4 shows the same unverified-app screen there for now. If you’d rather not build any of this, connect SiteWide instead.

Tell Claude to build it

Paste this into Claude Code in an empty folder:

code
Build me a TypeScript MCP server for Google Analytics 4 using @modelcontextprotocol/sdk and @google-analytics/data. Give it one tool, run_report, that takes a property id, metrics, dimensions and a date range. Authenticate with Application Default Credentials. Then give me the exact gcloud command to log in with the analytics.readonly scope, and register the server with `claude mcp add`. Don't invent metric names: link me to Google's API schema page so I can check them.

Before you trust the result, check every metric and dimension it uses against Google’s API schema page. A made-up name looks fine in code and fails only when the API rejects it.

Advertisement
Keep reading

Related posts

Keep reading

Get the AI playbook in your inbox

Every Wednesday. 28,400+ operators. Zero fluff.

↵ ↵ to see all results esc esc to close