Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,10 @@ system_tests/local_test_setup
pylintrc
pylintrc.test

.agents/skills
.agents/skills

# Local OAuth client JSONs and ADC files — never commit credentials
*oauth_client*.json
client_secret*.json
application_default_credentials.json
google-analytics-adc.json
96 changes: 96 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,102 @@ Here are some sample prompts to get you started:
what are the custom dimensions and custom metrics in my property?
```

---

## Setup for Pulse teammates

> Pulse-specific instructions. Pairs with the Pulse fork of [google-ads-mcp](https://github.com/pulse-integrated/google-ads-mcp). Same `~/Projects/mcp/` parent folder convention. If a teammate has already set up the Google Ads MCP on this Mac, most of the prerequisites are already done.

### Prerequisites

- **Claude Code installed** (not the regular Claude desktop app — different installer)
- **Homebrew installed**
- **A user account with access** to the GA4 properties you want to query (Pulse defaults to Sean's identity, since he has property-level access across our managed accounts)

### Steps

1. **Install gcloud and uv** if you don't have them:

```bash
brew install --cask google-cloud-sdk
brew install uv
```

2. **Clone the repo** anywhere:

```bash
git clone https://github.com/pulse-integrated/google-analytics-mcp.git
cd google-analytics-mcp
```

3. **Install dependencies:**

```bash
uv sync
```

4. **Get the OAuth client JSON.** If Pulse already created an OAuth client for the Google Ads MCP, you can reuse it — same GCP project, same client ID and secret work for both APIs. Otherwise:

- https://console.cloud.google.com/apis/credentials
- Create OAuth client → Application type: **Desktop app**
- Download the JSON file. Don't commit it.

5. **Enable the two GA APIs** in the GCP project (one-time, only needs to be done by one teammate):

- https://console.cloud.google.com/apis/library/analyticsadmin.googleapis.com
- https://console.cloud.google.com/apis/library/analyticsdata.googleapis.com

6. **Run the credential setup helper:**

```bash
uv run setup_credentials.py
```

It'll ask for the OAuth client JSON path, open a browser, and walk you through sign-in. Sign in with the Google account that has GA4 access. When done, the script prints the path to your ADC file (typically `~/.config/gcloud/application_default_credentials.json`).

7. **Register the MCP with Claude Code.** Open `~/.claude.json` and add the entry to your top-level `mcpServers` block (merge alongside any existing entries like `google-ads`):

```json
{
"mcpServers": {
"google-analytics": {
"type": "stdio",
"command": "/opt/homebrew/bin/uv",
"args": [
"--directory",
"/absolute/path/to/google-analytics-mcp",
"run",
"analytics-mcp"
],
"env": {
"GOOGLE_APPLICATION_CREDENTIALS": "/Users/YOU/.config/gcloud/application_default_credentials.json"
}
}
}
}
```

8. **Restart Claude Code** so the new MCP loads.

9. **Smoke test** from a terminal in the repo root:

```bash
GOOGLE_APPLICATION_CREDENTIALS="$HOME/.config/gcloud/application_default_credentials.json" \
uv run python -c "from google.analytics.admin import AnalyticsAdminServiceClient; \
c = AnalyticsAdminServiceClient(); \
print([(a.display_name, a.account) for a in c.list_account_summaries()])"
```

You should see a list of `(account_name, account_id)` pairs. That confirms the MCP can reach the GA4 Admin API. If it errors, the message usually points at the fix (often a missing scope on the OAuth flow).

### Trade-offs worth knowing

- **Shared identity:** if all teammates use the same ADC file (e.g. Sean's), reports will look like Sean made them in audit logs. For read-only reporting this is fine.
- **API enablement is GCP-project-wide:** enabling the two GA APIs in the project covers every teammate using that OAuth client. Only one person needs to do step 5.
- **No mutation tools:** this MCP is read-only. We're not building goals, audiences, or admin mutations into it for now.

---

## Contributing ✨

Contributions welcome! See the [Contributing Guide](CONTRIBUTING.md).
95 changes: 95 additions & 0 deletions context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Google Analytics MCP — Pulse Integrated

## What this project is

A fork of Google's official [google-analytics-mcp](https://github.com/googleanalytics/google-analytics-mcp), wired up under the Pulse Integrated org. Pairs with the [Google Ads MCP](../googleads/) — together they feed the **Friday recap skill**, which ties ad spend to on-site behavior in plain-English client emails.

## Why we're building this

The Friday recap that summarizes Google Ads performance is incomplete without the on-site half of the story. Knowing a client spent $4,200 on Google Ads tells you nothing about whether those clicks actually did anything — did people land, browse, convert? The GA4 MCP closes that loop.

## What's in this folder

- [`analytics_mcp/`](analytics_mcp/) — the MCP server itself (unchanged from upstream so we can `git pull upstream main` cleanly)
- [`tools/admin/`](analytics_mcp/tools/admin/) — account/property discovery: `get_account_summaries`, `get_property_details`, `list_google_ads_links` (the critical one for client mapping), `list_property_annotations`
- [`tools/reporting/`](analytics_mcp/tools/reporting/) — reports: `run_report`, `run_realtime_report`, `run_funnel_report`, `run_conversions_report`, `get_custom_dimensions_and_metrics`
- [`README.md`](README.md) — upstream README + Pulse teammate onboarding section
- [`setup_credentials.py`](setup_credentials.py) — interactive wrapper around `gcloud auth application-default login` with the right scopes
- [`pyproject.toml`](pyproject.toml) — Python 3.10+, managed by `uv`

## How the pieces connect

```
Claude Code (stdio)
analytics-mcp ──reads──▶ ~/.config/gcloud/application_default_credentials.json
GA4 Admin API + GA4 Data API
get_account_summaries → [{account: "...", properties: [{name: "...", id: "..."}]}]
list_google_ads_links(property_id) → [{customer_id: "1234567890", ...}]
run_report(property_id, dimensions, metrics, date_ranges) → table of values
```

## Auth model — different from Ads, on purpose

The Google Ads MCP used a hand-rolled `google-ads.yaml` with OAuth refresh tokens. This MCP uses Google's **Application Default Credentials (ADC)** flow instead — the standard library reads from `~/.config/gcloud/application_default_credentials.json` and refreshes the token automatically.

Why the difference:
- The upstream GA MCP is built on Google's ADC convention. Diverging means rewriting Google's code, fighting the merge train every time upstream updates.
- ADC tokens refresh longer than the 7-day testing-mode OAuth quirk on the Ads side. Published consent screen → indefinite life.

Reuses the same OAuth client we created for Ads — same GCP project, same client ID. Just needs the `analytics.readonly` and `cloud-platform` scopes added.

## Status

| Phase | Status |
|---|---|
| Repo forked into pulse-integrated | ✅ |
| `uv` deps installed | ✅ |
| `gcloud` CLI installed | ✅ |
| `setup_credentials.py` helper written | ✅ |
| MCP registered in `~/.claude.json` | ✅ |
| ADC generated (Luke or Sean runs `setup_credentials.py`) | ⏳ |
| Enable GA Admin + Data APIs in GCP project | ⏳ — needs Luke (1-click each) |
| Restart Claude Code | ⏳ |
| Smoke test (`get_account_summaries`) | ⏳ |
| Update `/friday-ads-recap` skill to layer GA4 data | ⏳ |
| Pulse teammate onboarding section in README | ⏳ |

## Friday recap integration

Once both MCPs are live, the recap skill will add a **"What happened on-site this week"** section to each per-client email:

- Paid sessions from `google/cpc` (this week vs last week)
- Conversion rate of paid traffic
- Total conversions attributed to `google/cpc`
- Top 3 landing pages for paid traffic
- Average session duration / engagement rate

`list_google_ads_links(property_id)` returns the Google Ads customer IDs linked to each GA4 property — solving the Ads ↔ GA4 client mapping problem without us building it by hand.

## Naming

- **GA4** = Google Analytics 4 (the only currently supported GA version)
- **ADC** = Application Default Credentials, Google's local-machine credential file
- **Property** = a GA4 site/app (analogous to a Google Ads account but flat — no MCC layer)
- **`property_id`** = a numeric ID like `123456789` (no leading "properties/" in the MCP calls)

## Tools used by the recap skill (from this MCP)

- `get_account_summaries` — discover available properties
- `list_google_ads_links` — map Ads customer IDs to GA4 properties
- `run_report` — pull all the metrics for the email

## Working with Claude on this project

Same defaults as the Google Ads MCP and `~/Projects/wealth/CLAUDE.md`:
- Plain English; outputs and visual behavior over diffs
- Make reasonable calls and flag assumptions instead of blocking
- End-of-turn summary: 1–2 sentences, what changed and what's next

Future sessions: read this file first to orient. Update in place when status changes or design decisions land.
140 changes: 140 additions & 0 deletions setup_credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Interactive helper to set up Google Analytics MCP credentials.

This wraps `gcloud auth application-default login` with the right scopes,
writes the OAuth client JSON to the repo root (gitignored), and prints the
path you'll plug into `~/.claude.json` as GOOGLE_APPLICATION_CREDENTIALS.

Two flows:

(1) Bring your own OAuth client JSON
You've already created a "Desktop app" OAuth client in Google Cloud
Console and downloaded the client JSON. Drop the path in when asked
and we use it for the browser sign-in.

(2) Use existing ADC
You've already run `gcloud auth application-default login` previously
(maybe for another tool). Skip the auth flow and just point the MCP
at the existing ADC file.

Run from the repo root:
uv run setup_credentials.py
"""

from __future__ import annotations

import json
import os
import shutil
import subprocess
import sys
from pathlib import Path


SCOPES = [
"https://www.googleapis.com/auth/analytics.readonly",
"https://www.googleapis.com/auth/cloud-platform",
]
DEFAULT_ADC_PATH = (
Path.home() / ".config" / "gcloud" / "application_default_credentials.json"
)


def ask(prompt: str, default: str | None = None) -> str:
suffix = f" [{default}]" if default else ""
value = input(f"{prompt}{suffix}: ").strip()
return value or (default or "")


def main() -> None:
print("\n=== Google Analytics MCP credential setup ===\n")

if not shutil.which("gcloud"):
print("ERROR: gcloud CLI not found on PATH.")
print(" Install with: brew install --cask google-cloud-sdk")
sys.exit(1)

if DEFAULT_ADC_PATH.is_file():
print(f"Found existing ADC file at: {DEFAULT_ADC_PATH}")
reuse = ask("Reuse it without re-authenticating? (y/N)", "n").lower()
if reuse.startswith("y"):
_verify_and_finish(DEFAULT_ADC_PATH)
return

print()
print("Step 1/3 — OAuth client JSON")
print(" Create at: https://console.cloud.google.com/apis/credentials")
print(" Type: 'Desktop app' (or 'Web app' with http://localhost redirect)")
print(" Then click 'Download JSON' and paste the file path below.")
print()
print(" If you already have the OAuth client we set up for Google Ads,")
print(" you can reuse that JSON here — same project, same client works.")
client_path = ask("Path to OAuth client JSON")
if not client_path or not Path(client_path).is_file():
print(f"ERROR: not a file: {client_path}")
sys.exit(1)

print()
print("Step 2/3 — Enable required APIs in the Google Cloud project")
print(" Open: https://console.cloud.google.com/apis/library")
print(" Search for and enable BOTH:")
print(" - Google Analytics Data API")
print(" - Google Analytics Admin API")
input(" Press Enter when both are enabled...")

print()
print("Step 3/3 — Browser authorization")
print(" A browser window will open. Sign in with the Google account that")
print(" has access to your GA4 properties, then click 'Allow'.\n")
input(" Press Enter to launch the browser...")

cmd = [
"gcloud",
"auth",
"application-default",
"login",
f"--scopes={','.join(SCOPES)}",
f"--client-id-file={client_path}",
]
print(f" Running: {' '.join(cmd)}\n")
result = subprocess.run(cmd)
if result.returncode != 0:
print("ERROR: gcloud auth failed.")
sys.exit(1)

if not DEFAULT_ADC_PATH.is_file():
print(f"ERROR: ADC file not found at {DEFAULT_ADC_PATH} after auth.")
sys.exit(1)

_verify_and_finish(DEFAULT_ADC_PATH)


def _verify_and_finish(adc_path: Path) -> None:
try:
data = json.loads(adc_path.read_text())
except Exception as exc:
print(f"ERROR: couldn't parse {adc_path}: {exc}")
sys.exit(1)

if "refresh_token" not in data and data.get("type") != "service_account":
print("WARNING: no refresh_token in the ADC file. The MCP may need")
print("re-auth often. Re-run this script if queries start failing.")

print()
print("✓ ADC ready.")
print(f" Path: {adc_path}")
print()
print("Plug this into ~/.claude.json under env.GOOGLE_APPLICATION_CREDENTIALS:")
print(f' "GOOGLE_APPLICATION_CREDENTIALS": "{adc_path}"')
print()
print("Smoke test:")
print(
' GOOGLE_APPLICATION_CREDENTIALS="'
+ str(adc_path)
+ '" uv run python -c "from google.analytics.admin import '
'AnalyticsAdminServiceClient; c = AnalyticsAdminServiceClient(); '
'print([a.account for a in c.list_account_summaries()])"'
)


if __name__ == "__main__":
main()
Loading