Skip to main content
Glama
oussch702

safe-meta-ads-mcp

by oussch702
README.md
# safe-meta-ads-mcp

[![test](https://github.com/oussch702/safe-meta-ads-mcp/actions/workflows/test.yml/badge.svg)](https://github.com/oussch702/safe-meta-ads-mcp/actions/workflows/test.yml)
![Node.js 20 or later](https://img.shields.io/badge/node-%3E%3D20-339933)
![Two runtime dependencies](https://img.shields.io/badge/dependencies-2-2ea44f)
[![MIT license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)

![safe-meta-ads-mcp lets Claude build Meta ad campaigns that stay paused until you type the phrase that allows spend.](assets/launch.png)

An MCP server for Meta ads. It lets Claude, or any other MCP client, read your ad accounts and build campaigns, ad sets, creatives and ads through Meta's official Marketing API, with guards that keep an agent from turning on spend by accident.

Everything it creates starts paused, whatever the agent asks for. Pausing is always allowed. Turning anything on needs the exact phrase `ACTIVATE AND ALLOW SPEND`, which the agent is told to ask you for, and when your MCP client can show a form, the server also asks you there, with the daily budget at stake, before anything goes live. Set a daily budget ceiling and it refuses anything that would take a campaign above it. The access token stays inside the server: no tool returns it, and it is stripped from every result and every error.

We built it for the launches we run. One build for a real product launch created 21 ad sets, 456 creatives and 455 ads, and every ad set and ad was created paused, waiting for a person to say go. It uploaded 563 images and videos on the way. Two of the videos came back with HTTP 413, too large to send in a single request, so this server uploads video in chunks, the way Meta's own SDK does, and resumes an interrupted upload where it stopped. The write-up, with how the guards work and how to keep the token out of the chat: [Meta ads MCP: Claude builds the campaign, and nothing spends until you say go](https://aistrikeforce.com/meta-ads-mcp).

## See it in under a minute

https://github.com/user-attachments/assets/dc779ea4-b847-49ff-b29d-0416a158fe8c

## Quick start

You need Node.js 20 or later and a Meta access token with the `ads_management` permission. The [FAQ](#faq) explains how to get one.

**Claude Code.** On a Mac, keep the token in the Keychain. The first command asks for it and does not echo it.

```bash
security add-generic-password -s safe-meta-ads-mcp -a meta -w
claude mcp add meta-ads -- npx -y github:oussch702/safe-meta-ads-mcp --keychain safe-meta-ads-mcp --max-daily-budget 5000
```

Elsewhere, pass the token from your shell's environment instead:

```bash
claude mcp add meta-ads -e META_ACCESS_TOKEN="$META_ACCESS_TOKEN" -- npx -y github:oussch702/safe-meta-ads-mcp --max-daily-budget 5000
```

**Claude Desktop.** Open Settings, then Developer, then Edit Config, and add the server to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "meta-ads": {
      "command": "npx",
      "args": ["-y", "github:oussch702/safe-meta-ads-mcp", "--keychain", "safe-meta-ads-mcp", "--max-daily-budget", "5000"]
    }
  }
}
```

Without the Keychain, drop the two `--keychain` arguments and add `"env": { "META_ACCESS_TOKEN": "your token" }` next to `args`. Restart Claude Desktop afterwards.

The first start downloads the server and its two dependencies. If your client gives up while that happens, run `npx -y github:oussch702/safe-meta-ads-mcp --version` once in a terminal, then start it again.

Then ask Claude to run `meta_diagnose`. It reports what the token can do, the state of your ad account, and the Facebook Pages you can advertise with.

## What it does

- **Reads your accounts.** Ad accounts, campaigns, ad sets and ads with their status, budget and spend, and insights for any period, split by day, level or breakdowns such as age, gender, country or placement.
- **Builds campaigns, paused.** It creates campaigns, ad sets, creatives and ads, and duplicates ads. The create tools have no status input, and the server sets `PAUSED` on every campaign, ad set, ad and copy it creates, so nothing an agent passes can start delivery.
- **Turns things on only with the phrase.** `meta_set_status` pauses anything at once. Activating needs `confirm` set to exactly `ACTIVATE AND ALLOW SPEND`, and when the client supports MCP elicitation, a yes from you in a form that shows the ID and the daily budget at stake, never text the agent wrote. With `--max-daily-budget`, the server first reads from Meta what the campaign would spend per day once the object is on, and refuses when that is above your ceiling.
- **Uploads media.** Images go up in one request. Videos go up in chunks, with the progress saved after each one, so an interrupted upload resumes from the last chunk Meta confirmed, even after a restart. The tool then waits until Meta has processed the video. It only sends files that are images or videos by their content, not just by their name.
- **Respects Meta's rate limits.** It reads the usage headers on Meta's answers, slows down as usage climbs, waits out a limit for as long as Meta says it lasts (up to `--max-wait`), and retries the errors Meta marks as temporary, except when something is being created.
- **Explains errors.** Meta's error codes come back as a plain sentence followed by Meta's own message, for example: *Meta rejected the access token: it has expired. Create a new token, then restart the server.*

## Tools

Read tools, always available:

| Tool | What it does |
| --- | --- |
| `meta_list_ad_accounts` | The ad accounts the token can use, with status, currency and time zone. |
| `meta_list_campaigns` | Campaigns with status, budget, and spend and impressions for a period. |
| `meta_list_ad_sets` | Ad sets of an account or a campaign, with budget, optimization goal and spend. |
| `meta_list_ads` | Ads of an account, campaign or ad set, with review feedback and delivery issues. |
| `meta_get_insights` | Spend, reach, clicks, costs and actions for a date range, by level, by day or by breakdown. |
| `meta_diagnose` | What the token can do: permissions, account status, payment method, and the Pages it can advertise with. |

Write tools, left out with `--read-only`:

| Tool | What it does |
| --- | --- |
| `meta_create_campaign` | Creates a campaign, paused, with an optional campaign budget. |
| `meta_create_ad_set` | Creates an ad set with targeting, budget and optimization goal, paused. |
| `meta_create_creative` | Creates a website-link creative from one image or one video, posted as your Facebook Page, with an optional Instagram account. |
| `meta_create_ad` | Creates an ad from an ad set and a creative, paused. |
| `meta_upload_image` | Uploads an image to the ad account and returns its hash. |
| `meta_upload_video` | Uploads a video in chunks, resumes after an interruption, and waits for processing. |
| `meta_duplicate_ad` | Copies an ad into the same ad set or another one, paused. |
| `meta_set_status` | Pauses anything. Activates only with the phrase and under the budget ceiling, and asks you directly when the client can. |

## Example session

An example, with made-up names and IDs, on a server started with `--max-daily-budget 5000`:

```text
You     Set up a traffic campaign for the spring sale: 20 USD a day, people in the
        US aged 25 and over, with hero.jpg from the ads folder.

Claude  meta_create_campaign   "Spring sale", OUTCOME_TRAFFIC, daily_budget 2000
          campaign 2001, PAUSED
        meta_create_ad_set     campaign 2001, "US 25+", LINK_CLICKS, US, age_min 25
          ad set 2002, PAUSED
        meta_upload_image      ads/hero.jpg
          image_hash 6f1c09e2
        meta_create_creative   Page 3001, https://example.com/spring, image 6f1c09e2
          creative 2003
        meta_create_ad         ad set 2002, creative 2003
          ad 2004, PAUSED
        meta_set_status        campaign 2001, ACTIVE
          Not activated. No confirmation phrase was given. Turning this on can spend
          money, so the person has to approve it: ask them to type ACTIVATE AND ALLOW
          SPEND, then call again with confirm set to exactly that. Nothing was changed.

        Everything is built and paused. To start spending 20 USD a day, type
        ACTIVATE AND ALLOW SPEND.

You     ACTIVATE AND ALLOW SPEND

Claude  meta_set_status        campaign 2001, ACTIVE, confirm "ACTIVATE AND ALLOW SPEND"
          ACTIVE, daily budget 2000 checked against the ceiling of 5000
        meta_set_status        ad set 2002, ACTIVE, confirm "ACTIVATE AND ALLOW SPEND"
        meta_set_status        ad 2004, ACTIVE, confirm "ACTIVATE AND ALLOW SPEND"
          The ad goes live once Meta has reviewed it.
```

## Options

The token comes from the `META_ACCESS_TOKEN` environment variable, unless `--keychain` names a Keychain item.

| Option | What it does |
| --- | --- |
| `--ad-account <id>` | Default ad account, with or without `act_`. Every tool can still name another one. |
| `--read-only` | Registers only the six read tools. Nothing can be created or changed. |
| `--max-daily-budget <n>` | Refuses to create anything with a daily budget above `n`, and to turn on anything that would take a campaign above `n` a day: its campaign budget, or the total of its active ad sets. It is not a cap on the whole account. Budgets are in the currency's smallest unit, as Meta counts them: 5000 is 50.00 USD, or 5,000 JPY. Lifetime budgets and campaigns with scheduled budget increases are refused while it is set; increases scheduled on a single ad set are not counted. Meta may spend up to 75% over a daily budget on a given day and balances it over the week, so the ceiling limits the budget, not a single day's spend. |
| `--keychain <service>` | Reads the token from this macOS Keychain item. |
| `--graph-version <v>` | Graph API version. Default `v26.0`, the current one. |
| `--max-wait <seconds>` | How long one call may wait out a rate limit or video processing before it returns. Default 120. |
| `--state-dir <dir>` | Where interrupted video uploads keep their progress. Default `~/.safe-meta-ads-mcp`. |

## What it cannot do

- **Delete anything.** No tool deletes or archives a campaign, ad set, ad or file.
- **Edit what already exists**, apart from pausing and activating. Budgets, targeting and creatives of existing objects are changed in Ads Manager.
- **Build every ad format.** Creatives are website-link ads with one image or one video. Carousels, collections, catalog ads, lead forms and dynamic creative are not covered.
- **Stop an agent that ignores its instructions, on every client.** The phrase is a deliberate step, not a password. When your client supports MCP elicitation, the server asks you directly and the agent cannot answer for you. Otherwise, Claude Desktop and Claude Code still ask you before each tool call unless you have allowed that tool permanently, so keep `meta_set_status` on ask, and use `--read-only` when you only need reports.
- **Run very large reports.** Insights use Meta's synchronous endpoint. Meta's asynchronous report jobs are not supported.
- **Retry a create after a lost connection or a temporary error.** Either way the object may exist or not. The server says so instead of retrying, and the agent should list before trying again. Rate limits are the exception: Meta refuses those calls outright, so they are retried.
- **Touch billing or business settings.** Payment methods, spending limits and Business Manager permissions stay in Meta's own tools.

## FAQ

**Can an AI agent spend my ad budget?**
Not by accident. Everything an agent creates here starts paused, and the create tools have no way to ask for anything else. The one tool that turns ads on refuses unless it receives the exact phrase `ACTIVATE AND ALLOW SPEND`, which the agent is instructed to ask you for, and clients that support MCP elicitation also show you a yes or no before anything goes live. With `--max-daily-budget`, activation is refused whenever it would take a campaign above your ceiling, phrase or not. If you only want reports, `--read-only` removes every write tool.

**Does it work with Instagram ads?**
Yes. Instagram ads use the same campaigns, ad sets and ads. Leave `publisher_platforms` out of the ad set's targeting and Meta uses all its default platforms, Instagram included, or set it to `["instagram"]` to run on Instagram only. Pass `instagram_user_id` when you create the creative to show your Instagram account on those placements.

**How do I get a Meta access token?**
Use a system user, the kind of account Meta provides for software that calls its API.

1. In [Meta for Developers](https://developers.facebook.com/apps/), create an app and connect it to your business portfolio.
2. In Meta Business Suite, open Business settings, then Users, then System users. Add a system user and assign it your ad account and the Facebook Page your ads run as, with permission to advertise.
3. Generate a token for the system user and your app, with the `ads_management`, `ads_read` and `pages_read_engagement` permissions.
4. Store it in the macOS Keychain or in `META_ACCESS_TOKEN`, then run `meta_diagnose` to see what it can do.

If Meta answers that your app is still in Development mode, switch it to Live in Meta for Developers before you create creatives.

**Is this official?**
No. It is an independent open-source project by AI Strike Force, not made or endorsed by Meta or Anthropic. It calls Meta's official Marketing API with your own token, so Meta's terms and limits apply as they would to any app you build.

## Contributing

Issues and pull requests are welcome. Run `npm test` before sending a change. The tests run against a fake Graph API and never call Meta.

## License

MIT

---

Built by [AI Strike Force](https://aistrikeforce.com), an AI automation agency. We publish the tools and findings that come out of the systems we run.