SEM Campaign Manager
Exposes a Facebook Ads Manager-shaped mock API with campaign listing, account summaries, underperformance flagging, change planning, dry-run deletions, and an approve/reject review loop for proposed campaign modifications.
Provides a Meta/Facebook Ads Manager-style campaign management interface, including tools to query campaigns, ad sets, and ads, retrieve performance insights and daily trends, flag underperformers, and propose/review budget, creative, and pause/resume changes through a human approval workflow.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@SEM Campaign ManagerPull my account summary and list underperforming campaigns."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
SEM Campaign Manager — Mock MCP Server
A local MCP server exposing 103 mocked Facebook Ads Manager–style campaigns (Campaign → Ad Set → Ad, with 7-day insights), plus tools to flag underperformers and run a propose → human-review → apply optimization loop.
Built to plug into a morning triage workflow: pull fresh performance data, flag what's losing money, let the AI draft fixes, you approve, changes apply.
It also carries a DEMO_MODE toggle system for a conference talk — flip
one env var and restart to switch specific tools/errors/schemas between a
"before" (bad) and "after" (good) state, without editing code between
takes. See Demo mode below.
Setup
pip install -r requirements.txt
python server.py # runs the MCP server over stdio + a dashboard on :5050Point Claude Desktop / Claude Code at it, e.g. in claude_desktop_config.json:
{
"mcpServers": {
"sem-campaign-manager": {
"command": "python",
"args": ["/absolute/path/to/files-mcp-project/server.py"]
}
}
}Running server.py also starts a small Flask review dashboard at
http://127.0.0.1:5050 in a background thread — same process, same
in-memory store as the MCP tools, so approving a change on the dashboard
is instantly visible to Claude and vice versa. DASHBOARD=0 disables it;
DASHBOARD_PORT=<port> changes the port.
Related MCP server: facebook-business-mcp
Data model
Mirrors Meta Marketing API field names/objectives (OUTCOME_SALES,
OUTCOME_LEADS, etc.) so swapping the mock store for real API calls later
is mostly a matter of rewriting store.py's data source — the tool
interface (server.py) shouldn't need to change.
Campaign: id, name, status, objective, daily_budget, bid_strategy, target_cpa, target_roas, insights_7d,
last_activity_date, nestedadsetsAd Set: targeting (age/gender/geo/interests), optimization_goal, billing_event, daily_budget, insights_7d,
daily_trend_7d, nestedadsAd: creative (headline, primary_text, CTA,
image_hashfilename), insights_7d
100 campaigns are generated with a seeded random mix (mock_data.py, RNG
isolated in its own random.Random(SEED) instance so it's reproducible
regardless of what else happens to be imported): ~30% strong, ~40%
average, ~30% engineered to be underperforming in a specific, diagnosable
way (declining trend, ad fatigue, weak CTR, or straight-up bad ROAS/CPA).
Each of ~20 brands' ~5 campaigns share one pseudo CRM client (bucketed by
brand), so per-client tools have a real portfolio to work with.
On top of those 100, demo/demo_data.py hand-seeds 3 campaigns for one
memorable client, IKEA (client_id: client_ikea_demo,
monthly_budget_cap: $500):
Campaign | Status | Story |
| ACTIVE | High CPA ($94.28 vs $40 target, 2.36x) + low CTR — the flagship demo campaign |
| ACTIVE | Healthy / strong performer — no flags, "no action needed" |
| PAUSED | Stale, |
Tools
Querying
list_campaigns(status, objective, min_spend, max_spend, sort_by, order, limit, offset)get_campaign(campaign_id)— full nested detailget_account_summary()— portfolio rollup, good first call each morningget_campaign_daily_trend(campaign_id)— 7-day daily breakdown per ad setget_client_balance(campaign_id)— the owning client's budget cap, current daily budget, remaining headroom (demo: grounding toggle, see below)get_product_image_info(campaign_id)— ad image filenames/dimensions; filenames vary from descriptive to opaque, deliberately
Flagging
get_underperforming_campaigns(roas_threshold, cpa_multiplier, ctr_threshold, frequency_threshold, status, limit)— returns campaigns with specific flags (low_roas,high_cpa,low_ctr,ad_fatigue,declining_trend) and a plain-English reason for each
Planning (read-only — nothing here proposes or applies anything)
plan_campaign_changes(client_id)— structured multi-step plan across every campaign one client ownsdry_run_delete(campaign_ids)— preview what deleting campaign(s) would affect (name, status, last activity), without deleting
Proposing changes (draft only, nothing applied)
propose_budget_change(campaign_id, new_daily_budget, reason)(demo: tool-description / error-hint / reasoning toggles, see below)propose_creative_update(ad_id, headline, primary_text, cta, reason)propose_pause_resume(target_type, target_id, action, reason)
Human review (only path that mutates data)
list_pending_changes(status)review_change(change_id, decision, note)—decisionisapproveorreject
Resources
demo://past-decisions— readsresources/past_decisions.mdlive (not cached) on every request. Seeded with one rejected proposal: "this client runs a big sale every first weekend of the month, don't touch budget then." Delete/restore the file (canonical copy atdemo/seed_past_decisions.md) to demo before/after without restarting the server — see Feedback loop below.
Demo mode
Isolate exactly one before/after concept per clip; every other concept
stays in its good state regardless of which mode you pick (see
demo/demo_config.py). default = every concept in its good state.
Easiest way to switch takes: edit DEMO_MODE.txt (plain text, project
root, just the mode name) and reconnect the MCP server in your client.
Claude Desktop's connector panel usually has a per-server reconnect
action that respawns the process without a full quit/reopen — much
faster than editing JSON. A full quit/reopen always works too if your
client doesn't expose that.
grounding_beforeIf DEMO_MODE.txt is empty, it falls back to the DEMO_MODE env var
(useful for one-off terminal runs without touching the file):
DEMO_MODE=grounding_before python server.pyEither way, the server has to actually restart to pick up the new value
— there's no live-reload. Check stderr on startup to confirm which mode
and source actually took effect: [demo_config] DEMO_MODE='grounding_before' (from DEMO_MODE.txt).
| What changes | Test prompt |
|
| "Should we increase the budget for IKEA's Summer Sale campaign?" |
|
| "IKEA's Summer Sale campaign has a CPA problem, reduce its budget." |
| A budget-cap violation returns | "Bump IKEA's Summer Sale campaign budget up to $600." |
|
| "IKEA's Summer Sale campaign has a high CPA, cut the budget." |
Data readiness (image filenames)
No toggle — both examples coexist on CMP-4471 so one get_product_image_info("CMP-4471") call shows the contrast: ad 1's file is img_4471_final_v2.jpg (opaque, versioned export — can't identify the product from the name alone), ad 2's is red_sneaker_summer_2020_collection.jpg (fully descriptive). Ask the model to identify the product in each ad and screenshot the difference in confidence/accuracy.
Human-in-the-loop dashboard
http://127.0.0.1:5050 starts with one pending proposal (a budget cut on
CMP-4471) and one already-approved change (a creative update on CMP-4472)
visible at the same time — no setup needed for the screenshot. Approve/
Reject buttons call review_change directly; the page auto-refreshes
every 5s, so a change proposed live through Claude appears without
reloading.
Planning & dry run
plan_campaign_changes("client_ikea_demo") # 3-step plan: cut CMP-4471's budget,
# leave CMP-4472 alone, review CMP-4473 for archival
dry_run_delete(["CMP-4473"]) # name, status, last_activity_date -- nothing deletedFeedback loop
resources/past_decisions.md exists by default (full rejection reason
loaded). To demo the "before" state:
mv resources/past_decisions.md resources/past_decisions.md.bakAsk the model to propose a change for CMP-4471 dated around the first
weekend of a month — without the resource, it has no reason to hold off.
Restore the file (mv resources/past_decisions.md.bak resources/past_decisions.md,
or re-copy from demo/seed_past_decisions.md) and ask again; the resource
reads live, so no restart is needed between the two takes.
How this maps to the morning flow
Trigger the prompt → agent calls
get_account_summary()thenget_underperforming_campaigns()to see what's actually losing money and why.Agent inspects specifics with
get_campaign()/get_campaign_daily_trend()/get_client_balance()/plan_campaign_changes(), then drafts fixes with thepropose_*tools — nothing changes yet.You review via
list_pending_changes()or the dashboard at:5050.You approve or reject with
review_change()(or the dashboard buttons, which call the same method). Only approved changes call_apply_change()instore.py.
Swapping in the real Meta Marketing API
Everything the AI calls goes through store.py. To go live:
Replace
generate_dataset()inmock_data.pywith real calls toGET /act_{id}/campaignsand/insights(cache/refresh per session).Replace
demo/demo_data.py's client bucketing with real CRM/client data.Point
_apply_change()at the real update endpoints (POST /{campaign_id}for budget,POST /{ad_id}for creative/status).Everything else — the tool surface, the flagging logic, the propose/approve queue, the dashboard — stays the same.
This server cannot be deployed
Maintenance
Related MCP Connectors
Meta Ads MCP (Facebook + Instagram) - analyze performance, manage budgets, pause campaigns.
Meta Ads MCP: bulk-create Facebook & Instagram ads across accounts from Drive folders, guardrailed.
AI agents that manage paid ads on Meta, LinkedIn, and Google Ads from any MCP client.
Google Ads MCP with 20,000+ account peer context and staged approve-then-execute writes.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceConnects AI assistants to Facebook's Ads API to enable natural language queries for campaign data, insights, and performance metrics. It allows users to manage ad accounts and retrieve detailed analytics like impressions, clicks, and spend through MCP-compatible interfaces.-
- AlicenseNot gradedqualityCmaintenanceUnofficial MCP server implementation for Facebook Business API. Enables LLMs to manage ad campaigns and ads via natural language.2MIT
- AlicenseNot gradedqualityDmaintenanceEnables management of OpenAI Ads campaigns, ad groups, ads, and insights through MCP, with approval-gated live mutations for safe write operations.33MIT
- AlicenseNot gradedqualityDmaintenanceEnables reading and mutating Google Ads campaigns via MCP tools, and includes structured marketer workflows for search term mining, budget optimization, and weekly reviews.835MIT