You probably found this article by Googling some variant of "Meta Ads MCP not working" or "Claude can't connect to Meta Ads." Same boat I was in two nights ago. If you're trying to wire Claude (or a multi-agent Claude Code team) to Meta so it can actually publish campaigns instead of just talk about them, here's the path I found that ships ads today.

TL;DR: Meta's official Ads MCP exists but is gated for most accounts in their gradual rollout (you'll see is_ads_mcp_enabled: false). Claude Code can't even OAuth into it due to a redirect URI mismatch. Pipeboard's open-source meta-ads-mcp is the working path. Pip install it, plug in your own Meta dev app credentials, work around four undocumented Business Manager prerequisites and five Pipeboard quirks, and you can have a Claude agent team pushing PAUSED campaigns to Meta the same night.

The setup

I built a four-Claude-Code-subagent team for paid ads:

  • Research: gathers signals, writes the campaign brief
  • Creative: drafts and scores scripts, generates the final video via my existing video pipeline
  • Campaign: publishes to Meta as PAUSED
  • Performance: scores live ads daily, feeds fatigue signals back to research

Each agent has its own context window and its own tools allowlist. The campaign agent is the one that needs to actually talk to Meta. That's the agent this article is about.

The first night the campaign agent ran end to end, it pushed a real PAUSED campaign to a real ad account: campaign ID, adset, ad, creative, all live, waiting for a human to flip the toggle to ON. Cost of getting there: roughly six hours of debugging the Meta + Claude Code + agent layer. Most of those hours are documented below so you don't repeat them.

Discovery 1: Meta's "official" MCP is broken for Claude Code (and gated for most accounts in Claude Desktop)

This is where two of those hours went.

Meta released an official MCP server in April 2026. Multiple third-party guides cite the URL as mcp.meta.com/ads/<business-id>. That URL is fake. 404 on every call.

The real URL is https://mcp.facebook.com/ads, with no business ID in the path. Verified by curling it directly:

$ curl -sS -i https://mcp.facebook.com/ads
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.facebook.com/.well-known/oauth-protected-resource/ads",
                  scope="ads_management ads_read catalog_management business_management pages_show_list"
{"title":"Authentication Required","detail":"Failed to authenticate MCP request","status":401}

That WWW-Authenticate: Bearer resource_metadata=... header is the signature of a working OAuth-protected MCP. The URL is correct. The OAuth layer above is what is broken.

Adding it to Claude Code: claude mcp add meta --transport http https://mcp.facebook.com/ads --scope project returns "meta-ads connected." Then trying to authenticate via the in-session /mcp menu fails with:

SDK auth failed: The provided redirect_uris are not registered for this client.

Meta's MCP OAuth does not support dynamic client registration. Claude Code's OAuth flow uses a dynamically-allocated redirect URI (because every Claude Code session is potentially a different client). Meta's app review process expects pre-registered URIs. The two cannot meet. You cannot OAuth into Meta's official MCP from Claude Code.

Claude Desktop is different. Desktop's OAuth flow doesn't have the redirect-URI problem, so it connects cleanly. But then you call ads_get_ad_accounts and get the honest answer:

"is_ads_mcp_enabled": false,
"is_ads_mcp_disabled_reason": "Ads MCP is gradually being rolled out. 
  Please check back at a later date to use Ads MCP with this Ad Account."

Meta launched the MCP in open beta but didn't open it to all advertisers at the same time. Smaller accounts (mine had $0 in spend) are later in the rollout. Until Meta flips the flag, Desktop gets you a connector that can list pages and accounts but cannot actually create campaigns, fetch insights, or push creative.

So:

  • Claude Code → official MCP: blocked by OAuth
  • Claude Desktop → official MCP: connects, but operations gated on is_ads_mcp_enabled
  • For 95% of operators right now: neither path works

What does work: Pipeboard.

Discovery 2: Pipeboard's MCP is the working path

Pipeboard's open-source meta-ads-mcp is pip-installable, runs as a stdio MCP, and talks directly to Meta's Graph API. It bypasses Meta's MCP-rollout gating entirely because it isn't using the MCP API surface. It's using the regular Graph API surface that has been open for years.

Setup:

  1. pip install meta-ads-mcp (or use uvx)
  2. Create a Meta developer app. Call it whatever you want. Generate a long-lived access token through Meta's dev-console widget (exchanged via app secret, so it never expires).
  3. Wire it into your project's .mcp.json:
{
  "mcpServers": {
    "meta-ads": {
      "type": "stdio",
      "command": "meta-ads-mcp",
      "args": [],
      "env": {
        "META_APP_ID": "<your app id>",
        "META_APP_SECRET": "<your app secret>",
        "META_ACCESS_TOKEN": "<your long-lived token>"
      }
    }
  }
}
  1. Verify end to end with a direct Graph API call:
curl "https://graph.facebook.com/v22.0/me/adaccounts?fields=id,name,account_status,currency&access_token=$TOKEN"

If that returns your ad accounts, you're connected.

One token, permanent until you revoke it. No SaaS dependency. Pipeboard offers their own SaaS layer if you want it, but skipping it gives you full control over your own credentials and rate limits.

Discovery 3: Subagents can't call the Pipeboard MCP. The lead session has to.

This is the architectural finding that bit hardest.

In Claude Code, MCP tools are deferred. They're loaded on demand via the ToolSearch tool. The intuitive fix looks correct on paper: add ToolSearch and the mcp__meta-ads__* wildcard to the subagent's frontmatter, then have the subagent call ToolSearch at phase start.

In practice, that doesn't work for stdio MCPs like Pipeboard's meta-ads-mcp. The subagent has tool permission via the wildcard, but stdio MCP servers run in the lead Claude Code session's process. Subagents spawn in a separate context that can't reach the stdio process. You get permission without execution.

The working pattern: the lead Claude Code session is the only one that talks to Meta.

Restructure your agents:

  • The campaign subagent's job becomes preparation, not execution. It assembles the campaign brief, ad copy, scene plan, targeting parameters, budget, all the inputs to the Meta API calls, and writes them to a structured file or returns them as structured output.
  • The lead session reads that output and invokes the Pipeboard MCP tools directly to create the campaign, adset, creative, and ad.
  • The subagent does the "thinking" work (focused context, role-specific reasoning). The lead does the "doing" work for any MCP-dependent step.

This is awkward at first because you want the campaign subagent to feel autonomous. But it's the pattern that actually ships. Treat the lead as the orchestrator and subagents as planners.

Alternative if you want subagent autonomy: write a small Python wrapper that talks directly to Meta's Graph API (no MCP needed), and have subagents shell out to it via Bash. Sidesteps the propagation issue but requires you to maintain a small Meta SDK of your own. Most operators don't need this; the lead-session pattern is simpler.

The principle generalizes to any stdio MCP, not just Pipeboard. Hosted HTTP MCPs may behave differently. But for any locally-installed stdio MCP, treat the lead as your only entry point.

Discovery 4: Business Manager has four undocumented prerequisites

Auth worked. MCP tools loaded. The campaign agent drafted its policy review, ad copy, scene plan, targeting, and budget. Then it started executing API calls. Four hours of failed calls later, I had a punch list of Business Manager configurations nobody documents in one place.

Each missed prerequisite costs roughly 30 minutes of error-log triage to diagnose. Together, these are the four checks to run before any Meta API call in a fresh ad account.

Prerequisite 1: The Meta dev app must be in Live mode. A dev-mode app fails at ad creative creation with error code 1885183:

"Ads creative post was created by an app that is in development mode. It must be in public to create this ad."

Flipping to Live requires a Privacy Policy URL, Terms of Service URL, an app icon, and a category. None of this is needed for testing; all of it is needed for actually publishing.

Prerequisite 2: The Facebook page must be assigned to the ad account. In Business Settings, navigate to Ad Accounts → [your ad account] → Pages, and add the page. Without this, the API returns:

"(#200) Your access token does not have pages_read_engagement permissions, or your page role is not permitted to create ads for the page."

The UI quirk that bit me: connecting from the page side (the page's "Connect assets" panel) is the wrong direction. It only lets you connect Instagram or WhatsApp to the page. Ad-account-to-page connection happens from the ad account side. I lost twenty minutes on this directionality before figuring it out.

Prerequisite 3: Instagram either assigned to the ad account, or excluded from placements. If the page has a paired Instagram account, Meta auto-resolves the IG when creating the creative. If the ad account does not have permission to that IG account, the API returns error 1815199:

"Ad Account Has No Access To Instagram Account."

Two fixes: assign the IG account to the ad account in Business Settings → Instagram Accounts (preserves IG placements), OR set targeting.publisher_platforms=["facebook"] on the adset (excludes IG entirely for this run). The latter is a tactical bypass for the first night. The former is the right long-term answer.

Prerequisite 4: Meta Pixel match quality at 0.5 or higher (only if optimizing for offsite conversions). If your landing page doesn't have a Pixel yet, sidestep by using optimization_goal=LINK_CLICKS for the first run. Once a Pixel ships, this becomes a real gating check.

Add a Phase 0 pre-publish audit to your campaign agent that runs all four checks in 30 seconds at the start of every run. Catches a misconfigured Business Manager in seconds, not in hours.

Discovery 5: Pipeboard's MCP has five specific quirks

Even after Business Manager was right, the next 45 minutes were spent learning Pipeboard's idiosyncrasies. The five:

Quirk 1: BOOK_TRAVEL is now BOOK_NOW. Meta renamed the call-to-action enum. Pipeboard's defaults still reference the old name. Override explicitly. The wrong enum does not return an error. It just renders the wrong button text on the published ad.

Quirk 2: Pipeboard has no video upload tool. get_ad_video is read-only. create_ad_creative accepts a video_id but no video_path. There is no upload_ad_video exposed. Solution: bypass MCP entirely for video upload and POST directly to Graph API.

r = requests.post(
    f'https://graph.facebook.com/v22.0/{account_id}/advideos',
    data={'access_token': token, 'name': 'my-video'},
    files={'source': ('final.mp4', open(video_path, 'rb'), 'video/mp4')},
    timeout=300,
)
video_id = r.json()['id']

Works for files under 100 MB. For larger files, switch to chunked upload (?upload_phase=start, transfer, finish).

Quirk 3: image_hash and video_id together at the top level get rejected. Pipeboard's create_ad_creative returns "Only one media source allowed" when both are passed. The Graph API accepts both when nested inside object_story_spec.video_data.

spec = {
    "page_id": "<page_id>",
    "video_data": {
        "video_id": video_id,
        "image_hash": image_hash,  # the thumbnail
        "title": "<headline>",
        "message": "<primary_text>",
        "link_description": "<description>",
        "call_to_action": {
            "type": "BOOK_NOW",
            "value": {"link": "<your landing page>"}
        }
    }
}

Use direct Graph API for the creative call. Use MCP for the easier calls (create_campaign, create_adset, create_ad).

Quirk 4: Auto-CBO at $10/day. Passing campaign_budget_optimization=False to create_campaign does NOT make the campaign use ad-set-level budgeting. Pipeboard silently auto-assigns daily_budget=1000 (cents = $10/day) at the campaign level. To get true ABO, pass use_adset_level_budgets=True AND omit campaign-level budget. Set it on the adset instead.

Quirk 5: Deprecated placements. Pre-specifying granular facebook_positions like ["feed", "video_feeds", "instream_video"] returns error 2490562 in API v22+:

"Facebook Video Feeds Placement Is Deprecated."

Omit the field entirely. Meta picks the right surfaces automatically.

Discovery 6: Meta's targeting catalog is dying

This is the lesson that surprised me most.

The initial targeting recipe my campaign agent had embedded (coaches and consultants with high-ticket personal brands) listed interests like Personal branding, Sales funnels, ClickFunnels, Kajabi, Mighty Networks, and the behavior Business decision makers. Half of them no longer exist in Meta's catalog. The agent discovered this by calling search_interests for each and getting back empty arrays.

The valid substitutes the agent found:

  • Marketing automation (6003295328342)
  • Digital marketing (6003127206524)
  • Influencer marketing (6003488208457)
  • Entrepreneurship (6003371567474)
  • Creative entrepreneurship (6003102820440)
  • Behavior: Small business owners (6002714898572)

What's gone: the granular B2B platform interests (ClickFunnels, Kajabi, Mighty Networks). The buyer-stage behaviors (Business decision makers). Even "Coaching" still exists in the catalog but is categorized under Sports and Outdoors, not business. Try targeting it and you'll get the wrong audience.

Creative-as-targeting under Meta's Andromeda is the new model. Your ad copy IS the targeting signal. Granular interest stacking is going the way of broad match for paid search: still works, increasingly worse than just letting the platform figure it out.

Any time you embed a static targeting recipe in an agent definition, version-stamp it with the date the IDs were validated. Add a Phase 0 instruction telling the agent to call validate_interests for any ID it's about to use. The catalog is a moving target.

Where this leaves us

The Meta campaign was PAUSED in my ad account by 1:30 AM, ready for a human to flip live after the payment method and Pixel were in place. The infrastructure that got it there is reusable for the next 100 campaigns:

  • Pipeboard's meta-ads-mcp wired into .mcp.json with your own Meta dev app credentials
  • A campaign subagent that prepares structured inputs (campaign brief, ad copy, targeting, budget) and a lead session that executes the Meta API calls
  • A Phase 0 pre-publish audit that checks the four Business Manager prerequisites in 30 seconds
  • A short list of Pipeboard quirks to work around, plus the direct Graph API calls for video upload and creative creation
  • A validate_interests call before any static targeting recipe runs

The marginal cost of the next campaign drops significantly because what you paid for the first night was the system that produces campaigns, not the campaign itself.

What I'd hand the next operator hitting "Meta Ads MCP not working":

  1. Don't fight Meta's official MCP. It's gated for most accounts; even if you get past Desktop OAuth, you'll hit is_ads_mcp_enabled: false.
  2. Pipeboard's meta-ads-mcp is the working path. Local stdio MCP, your own Meta app, no SaaS dependency.
  3. Subagents can't call stdio MCPs. The lead session does the Meta calls. Restructure subagents as planners that prepare structured inputs; the lead executes the MCP tools. Or build a Python wrapper around the Graph API for subagent autonomy if you really need it.
  4. Run the Business Manager 4-prerequisite check before any API call. App in Live mode, page-to-ad-account, IG assignment or FB-only placements, Pixel match quality.
  5. Treat Pipeboard's quirks as five named workarounds: enum updates, no video upload (use Graph API), media nesting in object_story_spec, auto-CBO suppression, deprecated placements.
  6. Validate Meta targeting IDs at runtime. Last quarter's interest list is wrong this quarter.

The path I'd skip rebuilding is the OAuth rabbit hole at the official MCP. It will get fixed eventually; in the meantime, every hour spent there is an hour not spent shipping ads.


Two ways forward if you're building this:

  1. Free community at skool.com/creative-core-ai-7628. Agent definitions, Pipeboard config templates, and the verified working sequences walked through there. If you're wiring an agent team to Meta, this saves you the kind of night I had.
  2. Book a free diagnostic at creativecore.ai/book. 30 to 45 minutes. We score your marketing across eight dimensions, model funnel economics at 3, 6, and 12 months, and figure out whether an agent stack for paid ads is the right call for your business right now or whether you should keep things manual a while longer. Real numbers, not vibes.