Skip to main content
Glama
lil-j

meta-ads-mcp

by lil-j
README.md
# Meta Ads MCP

This repository contains two deliberately separate local MCP servers:

- `meta-ads-mcp` is the default read-only reporting server. It uses
  `ads_read` and cannot change Meta.
- `meta-ads-writer-mcp` is an optional, guarded creation server. It can upload
  reviewed images and create only new `PAUSED` ads from a validated manifest.

Both run over `stdio`. The writer reuses the same private credential store, but
it must be registered as a separate MCP server so enabling it never expands the
read-only server's tool set.

It also includes the `meta-ads-upload-qa` Codex skill under
[`skills/meta-ads-upload-qa`](skills/meta-ads-upload-qa). The skill coordinates
the two servers, requires explicit write authorization, renders every requested
placement preview, and keeps ads paused when visual QA does not pass.

## What it can do

| Tool                  | Purpose                                                      |
| --------------------- | ------------------------------------------------------------ |
| `health_check`        | Validate authorization and inspect granted permissions       |
| `list_ad_accounts`    | List accessible ad accounts                                  |
| `get_ad_account`      | Read account configuration and status                        |
| `list_campaigns`      | Read campaign budgets, objectives, schedules, and delivery   |
| `list_ad_sets`        | Read targeting, optimization, attribution, and budgets       |
| `list_ads`            | Read ads and attached creative details                       |
| `list_ad_creatives`   | Read creative copy, images, calls to action, and asset feeds |
| `get_ad_previews`     | Generate Meta-rendered placement previews for visual QA      |
| `get_insights`        | Query paginated performance data and breakdowns              |
| `compare_performance` | Compare matched entities across equal-length periods         |
| `export_insights`     | Write paginated CSV or JSON reports locally                  |

`compare_performance` calculates spend, delivery, CTR, CPC, CPM, conversions,
cost per conversion, conversion value, and ROAS deltas. `export_insights`
flattens Meta action arrays by `action_type`, safely quotes CSV values, and
protects spreadsheet programs from formula injection.

## Safety model

- Meta access is read-only. The server never creates, updates, pauses, resumes,
  or deletes campaigns, ad sets, ads, or creatives.
- OAuth requests only `ads_read`.
- The app secret and access token live outside the repository under
  `~/.config/meta-ads-mcp`, with owner-only file permissions.
- Graph API requests send the token in the `Authorization` header.
- Meta pagination URLs are never returned because they can contain credentials.
- `get_ad_previews` returns Meta's ephemeral preview URLs and, by default,
  renders each successful placement to a labeled PNG image directly in the MCP
  response. This lets an agent inspect placement overlays without controlling
  the user's browser. Set `render_images` to `false` when only URLs are needed,
  or `include_iframe_html` to `true` when raw embed HTML is useful for
  debugging. Rendering uses an isolated temporary Chrome/Chromium profile and
  deletes its local files after the response. It does not edit the ad.
- The renderer discovers Chrome/Chromium automatically. Set
  `META_ADS_PREVIEW_BROWSER_PATH` to an executable path to override discovery.
- `export_insights` creates a new local file and never overwrites an existing
  export. It cannot modify Meta.
- The server never logs tokens or app secrets.

## Guarded writer

The optional writer exposes five tools:

| Tool                    | Purpose                                                        |
| ----------------------- | -------------------------------------------------------------- |
| `plan_ad_upload`        | Validate local 4:5/9:16 files, hash them, and save a plan      |
| `validate_ad_upload`    | Read and verify the account, campaign, ad set, Page, and IG ID |
| `upload_images`         | Upload only the files fingerprinted in the validated plan      |
| `upload_assets`         | Upload fingerprinted images and MP4 videos in a validated plan |
| `create_ads_paused`     | Create placement or carousel ads hard-coded `PAUSED`           |

It has no edit, publish, pause, resume, archive, or delete tools. Both write
phases require a specific confirmation string after the plan is reviewed.
Progress is saved after every successful Meta response, so the same plan can be
retried without intentionally duplicating completed items. A partial failure is
never rolled back or cleaned up automatically. Before each real creative or ad
creation, the writer sends the exact payload through Meta's `validate_only`
execution option.

Every creative validation and creation request explicitly sends
`contextual_multi_ads` with `enroll_status` set to `OPT_OUT`, so new ads do not
participate in multi-advertiser ad units by default.

The manifest is JSON and uses absolute media paths. Existing placement-image
entries remain backward compatible:

```json
{
  "version": 1,
  "account_id": "123",
  "campaign_id": "456",
  "campaign_name": "Main campaign",
  "adset_id": "789",
  "adset_name": "Testing",
  "page_id": "101",
  "instagram_user_id": "202",
  "link": "https://example.com/landing",
  "display_link": "https://apps.apple.com/",
  "call_to_action_type": "DOWNLOAD",
  "ads": [
    {
      "name": "Concept 01",
      "title": "Headline ✨",
      "body": "Primary text 🧡",
      "description": "Description 📲",
      "feed_image_path": "/absolute/path/concept_F45.png",
      "vertical_image_path": "/absolute/path/concept_V916.png"
    }
  ]
}
```

Placement image and video ads can use two to five primary-text options in one
creative by replacing the singular `"body"` field with a `"bodies"` array:

```json
{
  "name": "Concept 01",
  "title": "Headline ✨",
  "bodies": [
    "Primary text option A 🧡",
    "Primary text option B 🧡"
  ],
  "description": "Description 📲",
  "feed_image_path": "/absolute/path/concept_F45.png",
  "vertical_image_path": "/absolute/path/concept_V916.png"
}
```

Placement videos use `"format": "video"` with `feed_video_path` (1:1 or 4:5)
and `vertical_video_path` (9:16). Carousels use `"format": "carousel"` and
two to ten square PNG/JPEG card objects under `cards`, each with an
`image_path` and optional card title, description, or HTTPS link.

Story-only image ads use `"format": "story_image"` with one
`vertical_image_path` (9:16):

```json
{
  "name": "Story concept",
  "format": "story_image",
  "title": "One Dinner. Two Plates. 🧡",
  "body": "Family recipes with a baby-friendly version 🍽️",
  "description": "7-day free trial • Then $29/year 🧡",
  "vertical_image_path": "/absolute/path/concept_V916.png"
}
```

The writer validates the target ad set before upload and rejects
`story_image` plans unless every explicitly configured placement is a
Facebook, Instagram, or Messenger Story. Automatic placements, feeds, Reels,
Search, Audience Network, and Threads are rejected. This prevents a
single-asset Story creative from silently delivering outside Stories.

The campaign and ad-set names are checked as well as their IDs. Images must be
PNG or JPEG and no larger than 30 MB. MP4 videos are limited to 1 GB by the
writer. A plan is invalidated if any fingerprinted file changes.
Campaigns and ad sets must already exist in the Marketing API; unpublished
Ads Manager drafts cannot be targeted.

## Requirements

- Node.js 20 or newer
- A Meta developer app with access to the Marketing API
- A Meta user who can access the required ad accounts

## Install

```bash
git clone https://github.com/lil-j/meta-ads-mcp.git
cd meta-ads-mcp
npm install
npm run build
```

## Authorize Meta

In the Meta app dashboard, enable a Facebook Login web flow and add this exact
Valid OAuth Redirect URI:

```text
http://localhost:53682/callback
```

Start the local setup flow:

```bash
META_APP_ID=your_app_id npm run auth -- setup
```

The helper opens a local browser page. Enter the app secret there, then authorize
Meta. The form posts only to `127.0.0.1`; credentials are not sent to the MCP
client or this repository.

Meta user tokens are time-limited. Reauthorize when `health_check` reports an
expired token:

```bash
npm run auth -- login
```

## Connect an MCP client

For Codex CLI, register the built server using its absolute path:

```bash
codex mcp add meta-ads -- node /absolute/path/to/meta-ads-mcp/dist/index.js
```

For clients that use a JSON MCP configuration:

```json
{
  "mcpServers": {
    "meta-ads": {
      "command": "node",
      "args": ["/absolute/path/to/meta-ads-mcp/dist/index.js"]
    }
  }
}
```

Restart or refresh the MCP client after changing its configuration.

Register the writer separately only when paused-ad creation is needed:

```bash
codex mcp add meta-ads-writer -- node /absolute/path/to/meta-ads-mcp/dist/writer-index.js
```

The configured token must grant `ads_management` for the writer. The included
authorization helper intentionally requests only `ads_read`; it does not widen
the default server's access.

## Install the Codex skill

Copy the included skill into your personal Codex skills directory:

```bash
mkdir -p ~/.codex/skills
cp -R skills/meta-ads-upload-qa ~/.codex/skills/
```

Restart or refresh Codex after installing or updating the skill.

## Development

```bash
npm run typecheck
npm run build
npm test
```

The test suite uses fake credentials and local mock transports. It does not call
Meta or require access to an ad account.

## Exports

Exports are written to `~/Downloads/Meta Ads` by default. Set
`META_ADS_EXPORT_DIR` to use a different directory. Files are created with
owner-only permissions and receive a numeric suffix if a name already exists.

## Environment overrides

- `META_ADS_CONFIG_DIR`
- `META_OAUTH_CLIENT_FILE`
- `META_ACCESS_TOKEN_FILE`
- `META_ACCESS_TOKEN`
- `META_APP_SECRET`
- `META_GRAPH_API_VERSION` (defaults to `v25.0`)
- `META_GRAPH_BASE_URL`
- `META_ADS_EXPORT_DIR`
- `META_ADS_WRITER_PLAN_DIR`
- `META_OAUTH_PORT`

## Security

Never commit Meta credentials or exported ad data. See [SECURITY.md](SECURITY.md)
for reporting instructions and additional guidance. CI runs Gitleaks on every
push and pull request as an additional guard against committed credentials.