Meta Ads MCP
<div align="center">
# Meta Ads MCP
**An MCP server for the Meta Marketing API — built for people running real ad spend.**
118 typed tools across Ads, Insights, Pixels/CAPI, Pages, Instagram, WhatsApp Business, Catalogs, Audiences, Leads and Billing — with the token kept out of your config files and a confirmation gate on every destructive call.
[](LICENSE)
[](https://www.python.org/downloads/)
[](https://modelcontextprotocol.io)
[](https://developers.facebook.com/docs/marketing-apis/)
[](#remote-deployment)
</div>
---
## Why this one
There are several Meta Ads MCP servers out there. This one is built around a specific assumption: **the person using it is spending real money**, possibly on behalf of clients, and a mis-fired tool call costs more than an apology.
That shapes three decisions the code makes differently:
| | How most setups do it | How this one does it |
|---|---|---|
| **Token storage** | Pasted into a JSON config or `.env` that sits in plaintext on disk | OS credential store first (Windows Credential Manager / macOS Keychain via `keyring`), `.env` only as fallback |
| **Destructive calls** | The model calls `delete_campaign` and it deletes | `confirm=True` required — the refusal names the operation and the target, so the intent shows up in the transcript before anything happens |
| **Errors** | Meta's opaque `code=3` / `subcode=1487348` reaches you raw | Decoded into a message that says what to actually do |
Plus a global `MCP_READ_ONLY` kill switch — useful when the server is reachable from a phone.
---
## Quick start
**Requirements:** Python 3.11+, a Meta [System User token](#getting-a-token), and an MCP client (Claude Code, Claude Desktop, Cursor…).
<details open>
<summary><b>Windows</b></summary>
```powershell
git clone https://github.com/AbnerZx/meta-ads-mcp.git
cd meta-ads-mcp
powershell -ExecutionPolicy Bypass -File .\install.ps1
```
The installer checks your Python, creates `.venv`, installs dependencies, prompts for your token (masked input, stored in Credential Manager, never written to disk), and prints the exact registration command to run next.
</details>
<details>
<summary><b>macOS / Linux</b></summary>
```bash
git clone https://github.com/AbnerZx/meta-ads-mcp.git
cd meta-ads-mcp
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
python -m keyring set meta-ads-mcp access_token # paste token when prompted
```
</details>
Then register it with your client:
```bash
claude mcp add meta-ads --scope user -- /path/to/.venv/bin/python /path/to/src/server.py
```
Restart the client. Tools appear as `mcp__meta-ads__*`.
**Verify it works** — ask your assistant:
> *"List my Meta ad accounts."*
---
## What's in it
118 tools across 12 modules:
| Module | Tools | What it covers |
|---|:--:|---|
| **`ads`** | 29 | Campaigns, ad sets, ads — create, update, duplicate, pause, activate, delete, schedules, previews |
| **`catalog`** | 12 | Product catalogs, feeds, product sets, diagnostics |
| **`instagram`** | 12 | Media, insights, publishing (image/video), comment moderation |
| **`pages`** | 11 | Posts, insights, publishing, comment moderation |
| **`creatives`** | 10 | Link & carousel creatives, image/video upload, previews |
| **`whatsapp`** | 9 | Business accounts, phone numbers, message templates, sending |
| **`audiences`** | 8 | Custom & saved audiences, applying them to ad sets |
| **`pixels`** | 7 | Pixel management, stats, sharing, **Conversions API events** |
| **`insights`** | 6 | Reports, period comparison, async report lifecycle |
| **`billing`** | 6 | Spend, limits, funding source, invoices, cross-account summary |
| **`leads`** | 5 | Lead forms and lead retrieval |
| **`graph`** | 3 | `graph_call` escape hatch, pagination, token debugging |
> **`graph_call` matters more than it looks.** The Meta Marketing API is enormous and changes often. Rather than pretend 118 tools cover it, there's a typed escape hatch for any endpoint that isn't wrapped yet — so you're never blocked waiting on this repo.
---
## Conventions
Worth knowing before your first call:
- **Ad account IDs** are auto-prefixed with `act_` — pass `123456` or `act_123456`, both work.
- **Budgets are integers in account-currency cents.** `R$ 10,00` → `1000`. This is Meta's convention, not ours.
- **Destructive ops need `confirm=True`.** Without it you get a blocked-operation error naming the target.
- **The token never travels through tool arguments.** It's loaded once at startup from the credential store.
---
## Security model
The threats this is designed against, and what handles each:
| Asset | Risk if exposed | Control |
|---|---|---|
| Meta access token | Full control of every ad account the token can reach | OS credential store (encrypted at rest); never logged, never serialized, never returned by a tool |
| Server logs | Token leaking through a stack trace | Regex scrubbing in `client.py`; response bodies are not logged |
| Docker image | Secrets baked into build layers | `.dockerignore` excludes `.env`, `.git`, secrets |
| Public endpoint | Brute force, DNS rebinding | Bearer auth with constant-time compare, global rate limit, `MCP_PUBLIC_HOSTS` allowlist |
| Destructive calls | A campaign paused or deleted by mistake | `confirm=True` per call + `MCP_READ_ONLY` global switch |
| CAPI payloads | PII sent in the clear | SHA-256 hashing applied client-side before transmission |
Found a vulnerability? See [SECURITY.md](SECURITY.md) — please don't open a public issue.
### Getting a token
Use a **System User token**, not a personal one. It doesn't expire, it's revocable independently of your own account, and Meta gives you a per-user audit trail.
Business Manager → **Business settings** → **Users** → **System users** → *Add* → assign only the ad accounts and Pages you actually need → **Generate new token**.
Typical scopes: `ads_management`, `ads_read`, `business_management`, `read_insights`. Add `pages_*`, `instagram_*`, `leads_retrieval`, `whatsapp_business_*` or `catalog_management` only if you use those modules. Meta shows the token exactly once.
---
## Configuration
All optional except the token. See [`.env.example`](.env.example).
| Variable | Default | Purpose |
|---|---|---|
| `META_ADS_TOKEN` | — | Token, if you're not using the credential store |
| `META_DEFAULT_BUSINESS_ID` | — | Fallback Business Manager ID |
| `META_API_VERSION` | `v21.0` | Graph API version |
| `META_REQUEST_TIMEOUT` | `60` | Read timeout, seconds — Meta's validation paths can sit 20–40s |
| `META_RATE_LIMIT_RPS` | `5` | Local rate limiter |
| `MCP_READ_ONLY` | `false` | `true` blocks every write, regardless of `confirm` |
| `MCP_PUBLIC_HOSTS` | — | Host allowlist — **required** for public HTTP deploys |
| `MCP_RATE_LIMIT_RPM` | `60` | Per-minute cap on the HTTP transport |
---
## Remote deployment
The server speaks two transports: **stdio** for local clients, **streamable-http** for remote ones. The HTTP mode lets you reach your ad accounts from Claude.ai on mobile via a Custom Connector.
[**→ Full deployment guide (DEPLOY.md)**](DEPLOY.md) — Fly.io walkthrough, bearer secret generation, secret rotation, and an incident-response checklist.
Short version:
```bash
fly launch --no-deploy --copy-config # edit fly.toml first: app name + MCP_PUBLIC_HOSTS
fly secrets set MCP_BEARER_SECRET="..." META_ADS_TOKEN="..."
fly deploy
```
`fly.toml` ships with `MCP_READ_ONLY = 'true'`. Leave it that way until you've confirmed the deployment works.
---
## Project layout
```
src/
├── server.py # stdio entrypoint
├── http_server.py # streamable-http transport, bearer auth, rate limiting
├── mcp_instance.py # shared FastMCP instance + transport security
├── client.py # Graph API client — retries, backoff, log scrubbing
├── auth.py # token loading (credential store → env fallback)
├── safety.py # confirmation gate, ID normalization, input validation
└── tools/ # 12 modules, 118 tools
```
---
## Contributing
Issues and pull requests are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Especially useful: Graph API error subcodes you've hit that aren't decoded yet, and coverage gaps you're currently working around with `graph_call`.
## License
[MIT](LICENSE) © Abner Ferreira
<sub>Not affiliated with or endorsed by Meta Platforms, Inc. "Meta", "Facebook", "Instagram" and "WhatsApp" are trademarks of Meta Platforms, Inc.</sub>
TDQS
Scored across 118 tools
The domain prefixes help, but several tools have fuzzy boundaries: insights_get overlaps with insights_get_account_summary and billing spend tools, ads_get_ad_preview overlaps with creatives_preview, and catalog_list is easy to mistake for catalog_list_products. Descriptions usually clarify the intended entity, but the agent has to rely on reading closely rather than clear separation.
Most tools follow a consistent domain_prefix + verb_noun pattern (ads_list_campaigns, ads_create_campaign, ads_pause_adset, wa_send_text_message). There are a few deviations like billing_summary_all_accounts (missing verb), bare catalog_list/creatives_list, and the generic insights_get, but these are minor against the overall predictable structure.
118 tools is far beyond the extreme threshold and makes the server unwieldy for an agent to navigate. The broad coverage across Ads, Catalogs, Creatives, IG, Pages, WhatsApp, Leads, Pixels, Billing, and Graph API would be much better scoped as several smaller, focused servers.
The core ads lifecycle—campaigns, ad sets, ads, creatives, insights—is well covered. However, several included subdomains are incomplete: catalogs and product sets can be created but not updated/deleted, custom audiences are read-only, and creatives have no update path, which are notable gaps that could block agents working outside the main campaign path.