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.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues