Skip to main content
Glama
jmacaggi-gfm

ads-mcp-server

by jmacaggi-gfm
README.md
# ads-mcp-server

Local MCP server exposing Google Ads + Meta Marketing performance data, campaign settings, and change history to Claude (Cowork) for live daily-dashboard workflows.

---

## What it does

Three tools registered with MCP:

| Tool | Purpose |
|---|---|
| `get_google_ads_report(date_range, breakdown)` | Performance + diagnostics + 56-day series + WoW + 8-week DoW comparisons + campaign settings + change history |
| `get_meta_ads_report(date_range, breakdown)` | Same shape as Google. Conversions filtered to `META_CONVERSION_EVENT_NAME` |
| `get_campaign_settings(platform)` | Settings + change history for `google` / `meta` / `both` |

Architecture: pull ad×day raw once per platform per hour, cache to parquet, derive every aggregation in pandas. No API call per breakdown.

---

## Prerequisites

- macOS / Linux
- Python 3.13 (via pyenv recommended)
- `uv` package manager: `brew install uv`
- Google Ads API access — see [Google Ads API getting started](https://developers.google.com/google-ads/api/docs/get-started/dev-token)
- Meta Marketing API access — see [Marketing API getting started](https://developers.facebook.com/docs/marketing-api/get-started)

### Credential setup links

| Credential | Where to get it |
|---|---|
| `GOOGLE_ADS_DEVELOPER_TOKEN` | Google Ads UI → Tools → API Center |
| `GOOGLE_ADS_CLIENT_ID` / `CLIENT_SECRET` | https://console.cloud.google.com → OAuth 2.0 client (Desktop app) |
| `GOOGLE_ADS_REFRESH_TOKEN` | Run `python -m google.ads.googleads.examples.authentication.generate_user_credentials` after installing `google-ads` |
| `GOOGLE_ADS_CUSTOMER_IDS` | Comma-separated, no dashes. Find in Google Ads UI top-right |
| `GOOGLE_ADS_LOGIN_CUSTOMER_ID` | MCC manager account ID, no dashes |
| `META_APP_ID` / `META_APP_SECRET` | https://developers.facebook.com → My Apps → Settings → Basic |
| `META_ACCESS_TOKEN` | https://business.facebook.com → Business Settings → System Users → Generate New Token (long-lived, with `ads_read`) |
| `META_AD_ACCOUNT_ID` | Meta Ads Manager → top-left account picker. Format: `act_XXXXXXXXX` |

---

## Install

```bash
cd ~/marketing-ds/ads-mcp-server
uv sync --extra dev
```

`uv` creates `.venv/` and installs all deps pinned in `pyproject.toml`.

---

## Configure credentials

Two options:

**Option A — point at existing `.env`** (recommended if you already have keys in `~/marketing-ds/decision_science/.env`):

```bash
export ADS_MCP_ENV_FILE=/Users/jmacaggi/marketing-ds/decision_science/.env
```

**Option B — local `.env`**:

```bash
cp .env.example .env
# fill in the blanks
```

Required keys are listed in `.env.example` with comments explaining each.

---

## Run

Locally for testing:

```bash
uv run ads-mcp-server
```

The server speaks MCP over stdio — Cowork (or any MCP client) spawns it on demand.

---

## Connect to Claude (Cowork)

Add this block to `~/.claude/claude_desktop_config.json` (create if missing):

```json
{
  "mcpServers": {
    "ads": {
      "command": "uv",
      "args": [
        "--directory",
        "/Users/jmacaggi/marketing-ds/ads-mcp-server",
        "run",
        "ads-mcp-server"
      ],
      "env": {
        "ADS_MCP_ENV_FILE": "/Users/jmacaggi/marketing-ds/decision_science/.env"
      }
    }
  }
}
```

Restart Claude/Cowork. Tools `get_google_ads_report`, `get_meta_ads_report`, `get_campaign_settings` should appear.

No background daemon required — Cowork starts/stops the process per session.

---

## Daily pre-warm (recommended for live dashboard)

Cache validity rule: a cache is fresh if it contains **yesterday's date**. Refreshes once per day. The first user query of the day triggers the refresh — and a 56-day pull on a large account can take 1-3 minutes.

To avoid that wait, schedule a pre-warm at 6am via macOS launchd:

```bash
# Install
cp /Users/jmacaggi/marketing-ds/ads-mcp-server/launchd/com.jmacaggi.adsmcp.prewarm.plist \
   ~/Library/LaunchAgents/

launchctl load ~/Library/LaunchAgents/com.jmacaggi.adsmcp.prewarm.plist

# Verify it's scheduled
launchctl list | grep adsmcp

# Trigger immediately (test)
launchctl start com.jmacaggi.adsmcp.prewarm

# Logs
tail -f ~/marketing-ds/ads-mcp-server/logs/prewarm.stdout.log
tail -f ~/marketing-ds/ads-mcp-server/logs/$(date +%Y-%m-%d).log
```

What it does each morning at 6am:
1. Pulls Google Ads perf 56d, settings, change_event 29d → cache
2. (Meta currently disabled — see "Meta status" below)
3. Writes refresh stamp `cache/google_lastrefresh.txt` = today

By the time you open Cowork, Google data is hot. Tool calls return in <1s.

To uninstall:
```bash
launchctl unload ~/Library/LaunchAgents/com.jmacaggi.adsmcp.prewarm.plist
rm ~/Library/LaunchAgents/com.jmacaggi.adsmcp.prewarm.plist
```

---

## Meta status (as of 2026-05-07)

Meta tools (`get_meta_ads_report`, `get_campaign_settings(platform="meta"|"both")`) are **structurally complete but not yet verified end-to-end**.

What works:
- Performance pull is chunked into 7-day windows (avoids the `Service temporarily unavailable / subcode 1504044` "result too large" error)
- Ad-level data is split: `level=campaign` for the 56-day series, `level=ad` for yesterday-only (avoids 5-minute pagination)
- AdSet pull is filtered to `effective_status IN [ACTIVE, PAUSED]` (avoids paginating thousands of archived ad sets)

What blocked us:
- After the first big perf chunked pull, the AdSet pull hit Meta's hourly rate limit (`code 17`, subcode 2446079: "User request limit reached"). Cooldown is typically 10-60 minutes.

To re-test tomorrow morning (after quota resets):
```bash
# Remove --skip-meta from the launchd plist to enable Meta in pre-warm
sed -i '' '/<string>--skip-meta<\/string>/d' \
   ~/Library/LaunchAgents/com.jmacaggi.adsmcp.prewarm.plist
launchctl unload ~/Library/LaunchAgents/com.jmacaggi.adsmcp.prewarm.plist
launchctl load ~/Library/LaunchAgents/com.jmacaggi.adsmcp.prewarm.plist

# Or run manually
ADS_MCP_ENV_FILE=/Users/jmacaggi/marketing-ds/decision_science/.env \
   uv run python scripts/prewarm.py
```

If Meta keeps rate-limiting, fallback options (not yet implemented):
- Async report runs (`async=True`) for the perf pull
- Tighter `effective_status` filter (`ACTIVE` only, drop paused)

If you prefer a long-running background process (optional, not required for Cowork): use `nohup uv run ads-mcp-server > /tmp/ads-mcp.log 2>&1 &` or a launchd plist.

---

## Optional: CSV override (skip the API)

Google Ads UI exports CSV reports without API quota limits. Drop a CSV into `cache/external/` to override the API pull:

```
cache/external/google_2026-05-07.csv
cache/external/meta_2026-05-07.csv
```

If a matching CSV exists AND is newer than the parquet cache, the server loads it instead of calling the API. The response sets `metadata.data_source = "csv_override"` so Cowork knows.

CSV column schema must match the parquet (see `src/ads_mcp_server/google_ads.py` and `meta_ads.py` for column names: `date, campaign_id, campaign_name, ad_id, ad_name, spend, impressions, clicks, conversions, ...`).

---

## Test

```bash
uv run pytest -v
```

All tests are mock-based — no network calls. Covers:
- `classify_campaign` Brand/Non-Brand/Other
- `diagnose` 5-state classifier
- 8-week same-DoW selector picks the right 8 dates
- WoW delta + zero-division
- `actions[]` filter for Meta
- Snapshot diff including null-old-value and pruning
- Missing creds returns clean error (no exception)

---

## Logs

Every API call and error is logged to `logs/YYYY-MM-DD.log` (one file per day).

---

## Troubleshooting

- **`google-ads` install fails**: ensure Python 3.13 (`uv python pin 3.13`) and `pip install grpcio` works on your system. On Apple Silicon: `arch -arm64 uv sync`.
- **Meta token expired**: regenerate the system user token in Business Settings; long-lived tokens last 60 days.
- **Tool not appearing in Cowork**: check `~/Library/Logs/Claude/mcp*.log` for spawn errors. Confirm `uv` is in `PATH` for the GUI process (you may need a full path: `which uv`).
- **Cache stale**: delete `cache/*.parquet` to force fresh pull.

---

## File map

```
src/ads_mcp_server/
  server.py        # MCP entry + tool handlers
  config.py        # env loading, validates creds
  google_ads.py    # 3 GAQL queries: perf, settings, change_event
  meta_ads.py      # Insights + AdSet pull
  snapshots.py     # Meta snapshot diff (Meta has no reliable change API)
  cache.py         # parquet + CSV override
  aggregate.py     # all pandas math
  classify.py      # Brand/NB/Other
  diagnose.py      # 5-state diagnosis
  date_ranges.py   # window resolution + 8wk DoW
  retry.py         # exponential backoff
  logging_setup.py # daily file logs
  schema.py        # response shape constants
```

TDQS

B3.2/5.0

Scored across 3 tools

Disambiguation5/5

Each tool targets a distinct area: campaign settings, Google Ads performance, and Meta Ads performance. No overlap; descriptions clearly differentiate them.

Naming Consistency5/5

All tool names follow a consistent 'get_' prefix with the specific resource (campaign_settings, google_ads_report, meta_ads_report). Consistent snake_case pattern.

Tool Count4/5

Three tools is minimal but appropriate for a focused ads reporting and settings retrieval server. Could be expanded but current count is reasonable for the stated purpose.

Completeness2/5

Only read operations are exposed; no create, update, or delete tools for campaigns or ads. Significant gaps for a full ads management workflow.

Maintenance

ActivitySlowing
ResponsivenessNo issues