MCP Server

AdAdvisor MCP Server

ai.adadvisor/mcp-server
Data & Analytics Marketing & Advertising Public & reachable MCP 2026-07-28

What this MCP does

Manages Meta advertising campaigns, creatives, audiences, lead forms, budgets, statuses, and performance reporting.

change_entity_budget
Change Entity Budget
Update the budget of a Meta campaign or ad set. Takes effect immediately. Provide exactly one of daily_budget or lifetime_budget in the account's currency (e.g. 50.00 = $50/day). You cannot switch between budget types — the entity must already use the type you are updating. Check with list_campaigns or list_adsets first. Safety: changes >2× the current value (in either direction) are rejected unless force=True. This prevents an LLM hallucinating a 10× budget increase. Idempotent: setting the same budget again succeeds without error. All changes are tracked with before/after state snapshots for auditability.
Destructive Open world Idempotent
Input schema
{'type': 'object', 'required': ['account_id', 'entity_type', 'entity_id'], 'properties': {'force': {'type': 'boolean', 'default': False, 'description': 'Bypass the >100% delta guardrail. By default, budget changes of more than 2× the current value (in either direction) are rejected to prevent accidental runaway edits. Set True to intentionally apply a large change.'}, 'entity_id': {'type': 'string', 'minLength': 1, 'description': 'The campaign or ad set ID (from list_campaigns, list_adsets, or get_performance — never invent IDs)'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': "Ad account ID. Must be the account where entity_id lives — Meta returns code 100 (parameter error) on a mismatch. Check via list_campaigns or list_adsets if you're not sure."}, 'entity_type': {'enum': ['campaign', 'adset'], 'type': 'string', 'description': "'campaign' or 'adset'. Ads do not have budgets."}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is changing this budget (e.g. 'doubling the winning ad set after 3-day soak'). Logged for telemetry."}, 'daily_budget': {'anyOf': [{'type': 'number', 'exclusiveMinimum': 0}, {'type': 'null'}], 'default': None, 'description': 'New daily budget in account currency (e.g. 50.00 = $50/day). Provide this OR lifetime_budget, not both.'}, 'lifetime_budget': {'anyOf': [{'type': 'number', 'exclusiveMinimum': 0}, {'type': 'null'}], 'default': None, 'description': 'New lifetime budget in account currency (e.g. 500.00 = $500 total). Provide this OR daily_budget, not both.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'type': 'object', 'required': ['entity_type', 'entity_id', 'account_id', 'currency'], 'properties': {'status': {'type': 'string', 'const': 'success', 'default': 'success'}, 'currency': {'type': 'string', 'description': 'Account currency (ISO 4217).'}, 'entity_id': {'type': 'string'}, 'update_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Audit-record ID — the entity_updates row created for this change.'}, 'account_id': {'type': 'string'}, 'next_steps': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Recommended follow-up tool call(s) for the LLM. Required on every successful mutation per CLAUDE.md; left Optional here because some tools (e.g. update_entity) currently omit it.'}, 'entity_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Human-readable name of the entity.'}, 'entity_type': {'enum': ['campaign', 'adset'], 'type': 'string'}, 'state_after': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Entity state snapshot after the change (shape varies by entity type).'}, 'daily_budget': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'New daily budget in account currency.'}, 'state_before': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Entity state snapshot before the change (shape varies by entity type).'}, 'lifetime_budget': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'New lifetime budget in account currency.'}}, 'description': 'Result of updating a campaign or adset budget.', 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id'], 'properties': {'status': {'type': 'string', 'const': 'not_yet_landed'}, 'message': {'type': 'string', 'description': 'One sentence to relay: what is available until the sync completes and why this request needs more.'}, 'account_id': {'type': 'string'}, 'sync_state': {'anyOf': [{'type': 'object', 'required': ['landed'], 'properties': {'basis': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "What the verdict rests on: 'run' (a sync record), 'legacy' (the data_synced flag) or 'none'."}, 'phase': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'initial | catchup | landed | failed; null without a sync record.'}, 'landed': {'type': 'boolean', 'description': 'True once every stream of the first sync has completed.'}, 'streams': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Per-stream status: pending | running | completed | failed.'}, 'task_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'failed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'landed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'streams_done': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams completed so far.'}, 'streams_total': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams the first sync covers.'}, 'insights_chunks': {'anyOf': [{'type': 'object', 'additionalProperties': {'anyOf': [{'type': 'integer'}, {'type': 'null'}]}}, {'type': 'null'}], 'default': None, 'description': '{done, total} chunks of the insights backfill.'}, 'catchup_started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': "Progress of an account's first sync — ``SyncState.as_dict()``\n(``app.services.meta_ads_data.landed``) field for field.", 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'How far the first sync is.'}, 'live_window_days': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Days of insights history a Live Read answers (the live window).'}}, 'description': "What a façade-backed tool answers instead of its normal payload when\nthe account's first sync has not completed and the question is outside\nwhat a Live Read may answer — ``NotYetLanded.to_mcp()`` field for field.\nA normal result, never ``isError``: relay ``message`` and, when asked how\nfar along the sync is, ``sync_state``.", 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id', 'retry_after'], 'properties': {'status': {'type': 'string', 'const': 'live_read_unavailable'}, 'message': {'type': 'string'}, 'account_id': {'type': 'string'}, 'retry_after': {'type': 'integer', 'description': 'Seconds to wait before retrying.'}}, 'description': 'Meta could not be read for the account right now (throttled, time\nbudget exceeded) — ``LiveReadUnavailable.to_mcp()`` field for field.\nA normal result, never ``isError``; retry after ``retry_after`` seconds.', 'additionalProperties': True}]}
change_entity_status
Change Entity Status
Pause or resume one or more Meta campaigns, ad sets, or ads. Takes effect immediately. Live mutation — entities stop or start delivering on Meta right away. Idempotent: pausing an already-paused entity (or resuming an already-active entity) succeeds without error. Batch behavior: each entity is processed independently. A failure on one entity does not abort the others — inspect the per-item ``status`` in the returned ``results`` list. All changes are tracked with before/after state snapshots for auditability. Confirm current status with list_campaigns, list_adsets, or list_ads first.
Destructive Open world Idempotent
Input schema
{'type': 'object', 'required': ['account_id', 'entity_type', 'entity_ids', 'action'], 'properties': {'action': {'enum': ['pause', 'resume'], 'type': 'string', 'description': "'pause' stops delivery (sets PAUSED), 'resume' reactivates (sets ACTIVE). For ARCHIVED/DELETED, use update_entity."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID. Must match the account each entity_id lives under — passing an entity_id from a different account causes Meta to return code 100 (parameter error). When unsure, look the IDs up via list_campaigns / list_adsets / list_ads first.'}, 'entity_ids': {'type': 'array', 'items': {'type': 'string'}, 'minItems': 1, 'description': 'One or more entity IDs to update (from list_campaigns, list_adsets, list_ads, or get_performance — never invent IDs). All IDs must be of the same entity_type. Pass a list with one element for a single update, or many elements to apply the same action to all of them in one call.'}, 'entity_type': {'enum': ['campaign', 'adset', 'ad'], 'type': 'string', 'description': "Entity type: 'campaign', 'adset', or 'ad'"}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is making this change (e.g. 'pausing fatigued ads with CTR < 0.5%'). Logged for telemetry."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['status', 'action', 'entity_type', 'account_id', 'all_succeeded', 'success_count', 'error_count', 'results'], 'properties': {'action': {'enum': ['pause', 'resume'], 'type': 'string'}, 'status': {'enum': ['success', 'partial'], 'type': 'string'}, 'results': {'type': 'array', 'items': {'type': 'object', 'required': ['entity_id', 'status'], 'properties': {'error': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Populated when status='error' — human-readable failure reason."}, 'status': {'enum': ['success', 'error'], 'type': 'string'}, 'entity_id': {'type': 'string'}, 'update_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'entity_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'state_after': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None}, 'state_before': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None}}, 'description': 'Per-entity result inside a batch mutation response.', 'additionalProperties': True}, 'description': 'Per-entity results — one entry per requested entity_id.'}, 'account_id': {'type': 'string'}, 'next_steps': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Recommended follow-up tool call(s) for the LLM. Required on every successful mutation per CLAUDE.md; left Optional here because some tools (e.g. update_entity) currently omit it.'}, 'entity_type': {'enum': ['campaign', 'adset', 'ad'], 'type': 'string'}, 'error_count': {'type': 'integer'}, 'all_succeeded': {'type': 'boolean', 'description': 'True if every item succeeded.'}, 'success_count': {'type': 'integer'}}, 'description': "Result of pausing or resuming entities.\n\n`status='success'` means every item succeeded. `status='partial'` means\nsome failed (inspect each `results[i].status`).", 'additionalProperties': True}
check_lead_gen_readiness
Check Lead-Gen Readiness
Check whether this account can actually run lead gen — BEFORE building anything. Lead generation has four independent prerequisites, and each one fails at a different point with a different, unhelpful Meta error. Run this first and you learn all four at once instead of discovering them one campaign at a time: 1. A Facebook Page the account can advertise from. 2. ads_management + pages_manage_ads on the token — to create forms and lead ad sets. 3. leads_retrieval + lead access on the Page — to read the leads back. A token can have everything else and still not be able to see a single lead. 4. The Page owner has accepted Meta's Lead Ads Terms of Service, at https://www.facebook.com/ads/leadgen/tos — this one is not grantable through OAuth and must be done by a Page admin. Returns can_create_forms / can_retrieve_leads plus a `blockers` list naming what to do about each. Call it before create_lead_form when a user is setting up lead gen for the first time, or whenever a lead tool returns a permission error.
Read only Open world
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'page_id': {'anyOf': [{'type': 'string', 'minLength': 1}, {'type': 'null'}], 'default': None, 'description': "Page to check. Omit to use the account's default promote_page."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['account_id'], 'properties': {'note': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Advisory caveats about interpreting this verdict.'}, 'error': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Degraded-read detail from the Page listing, if any.'}, 'page_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'blockers': {'type': 'array', 'items': {'type': 'object', 'required': ['reason', 'detail', 'user_action'], 'properties': {'detail': {'type': 'string', 'description': 'What is wrong, in plain English.'}, 'reason': {'type': 'string', 'description': 'Machine key: no_page | no_page_token | no_advertise_task | no_manage_leads_task | missing_scopes | leadgen_tos_not_accepted | lead_retrieval_not_available'}, 'user_action': {'type': 'string', 'description': 'What the human must do to clear it.'}}, 'description': 'One prerequisite currently failing, with what to do about it.'}}, 'page_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'account_id': {'type': 'string'}, 'next_steps': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Suggested next tool calls when fully ready.'}, 'can_advertise': {'type': 'boolean', 'default': False}, 'granted_scopes': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'null means the permission lookup failed — unknown, not empty.'}, 'missing_scopes': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None}, 'required_scopes': {'type': 'array', 'items': {'type': 'string'}}, 'can_create_forms': {'type': 'boolean', 'default': False}, 'can_retrieve_leads': {'type': 'boolean', 'default': False}, 'has_advertise_task': {'type': 'boolean', 'default': False}, 'leadgen_tos_accepted': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': "Whether the Page has accepted Meta's Lead Generation Terms of Service. null = could not be determined (treat as unknown, NOT as false). False blocks every lead-ads write for the Page until a Page admin accepts at https://www.facebook.com/ads/leadgen/tos."}, 'page_token_available': {'type': 'boolean', 'default': False}, 'has_manage_leads_task': {'type': 'boolean', 'default': False}, 'lead_retrieval_available': {'type': 'boolean', 'default': False}}, 'description': 'Readiness verdict for lead generation on one ad account + Page.'}
complete_upload
Complete Upload
Push S3-uploaded files to Meta — internal helper for the Upload Creatives widget. Prefer calling upload_creatives, which opens the widget and orchestrates the full S3 → Meta flow. This tool downloads each file from S3 and uploads to Meta's ad images/videos API, returning per-item results with image hashes and video IDs.
Open world
Input schema
{'type': 'object', 'required': ['account_id', 'files'], 'properties': {'files': {'type': 'array', 'items': {'type': 'object', 'required': ['s3_key', 'filename', 'content_type'], 'properties': {'s3_key': {'type': 'string', 'minLength': 1, 'description': "S3 object key returned by init_upload (e.g. 'tmp/<acct>/<task>/<id>.jpg')."}, 'filename': {'type': 'string', 'minLength': 1, 'description': 'Original file name (used as the Meta asset name).'}, 'content_type': {'type': 'string', 'minLength': 1, 'description': 'MIME type (must match what was sent to S3).'}}, 'description': 'A file already uploaded to S3, ready to be pushed to Meta.'}, 'minItems': 1, 'description': "Files already uploaded to S3 by the widget — each is downloaded and pushed to Meta's ad images/videos API."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Meta ad account ID'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['results'], 'properties': {'status': {'type': 'string', 'const': 'success', 'default': 'success'}, 'results': {'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}, 'description': 'Per-file Meta-registration outcome. Each carries image_hash or video_id plus name/dimensions/duration where applicable. Use the ID fields directly in create_creative.'}}, 'description': 'Result of complete_upload — registers uploaded S3 bytes with Meta.', 'additionalProperties': True}
create_ad
Create Ad
Create one or more ads by attaching creatives to ad sets. NOT idempotent — each call creates new ads. Typical workflow: 1. search_ad_images / search_ad_videos — find an asset 2. create_creative(account_id, creatives=[creative_spec]) — build creatives → creative_ids 3. create_ad(account_id, ads=[{adset_id, creative_id, name}]) — create this tool Shortcut (reuse existing creative): 1. list_creatives(account_id) — find an existing creative_id 2. create_ad(account_id, ads=[{adset_id, creative_id, name}]) All ads are created as PAUSED by default. Activate with change_entity_status. Accepts a list to support A/B testing (multiple creatives, same ad set). Conversion-tracked campaigns (OUTCOME_SALES, OUTCOME_LEADS via website) MUST set conversion_domain to the registrable domain of the destination URL (e.g. 'example.com', not 'https://shop.example.com/path'). Meta rejects otherwise with subcode 2490408 ('Missing conversion domain'). Skip for awareness/engagement campaigns and lead-form (instant form) ads. Common errors: - Invalid creative_id: verify with list_creatives or create a new one - Invalid adset_id: verify with list_adsets - 'Missing conversion domain' / subcode 2490408: pass conversion_domain - OAuthException / token expiry: ask user to reconnect their Meta account - Permission error: app may lack ad_management permission
Open world
Input schema
{'type': 'object', 'required': ['account_id', 'ads'], 'properties': {'ads': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'adset_id', 'creative_id'], 'properties': {'name': {'type': 'string', 'minLength': 1, 'description': 'Display name for the ad (shown in Ads Manager).'}, 'status': {'type': 'string', 'const': 'PAUSED', 'default': 'PAUSED', 'description': "Initial status. Locked to PAUSED to enforce the AdAdvisor policy that all newly-created ads must be reviewed before going live. Activate explicitly after review with change_entity_status(action='resume')."}, 'adset_id': {'type': 'string', 'minLength': 1, 'description': 'Ad set ID to attach this ad to (from list_adsets).'}, 'creative_id': {'type': 'string', 'minLength': 1, 'description': 'Creative ID to use for this ad. Obtain via create_creative or list_creatives.'}, 'conversion_domain': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Second-level domain (e.g. 'example.com', NOT 'https://shop.example.com/path') where conversions are recorded. REQUIRED by Meta for any ad in a campaign that shares data with a pixel — Meta returns subcode 2490408 ('Missing conversion domain') if omitted. Set to the registrable domain of the creative's destination URL: e.g. for a creative linking to https://shop.example.com/checkout, pass conversion_domain='example.com'. Optional for awareness/engagement campaigns and lead-form ads (which use Meta's hosted form, not a tracked website)."}}, 'description': 'Specification for creating a single Meta ad.'}, 'minItems': 1, 'description': 'One or more ad specs to create. Each attaches a creative to an ad set.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is creating these ads (e.g. 'wiring 5 new creatives into the BOFU retargeting ad set'). Logged for telemetry."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['status', 'all_succeeded', 'account_id', 'error_count', 'created_count', 'ads'], 'properties': {'ads': {'type': 'array', 'items': {'type': 'object', 'required': ['ad_id', 'name', 'adset_id', 'creative_id', 'status', 'update_id'], 'properties': {'name': {'type': 'string'}, 'ad_id': {'type': 'string'}, 'status': {'type': 'string', 'description': 'Initial status — PAUSED unless caller overrode.'}, 'adset_id': {'type': 'string'}, 'update_id': {'type': 'string'}, 'creative_id': {'type': 'string'}, 'conversion_domain': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'The verified domain attached to this ad (required by Meta on conversion-tracked ads).'}}, 'description': 'Successfully-created ad in a create_ad batch response.', 'additionalProperties': True}, 'description': 'Per-ad records on success. Note: field name is `ads` (not `created`).'}, 'errors': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}}, {'type': 'null'}], 'default': None, 'description': 'Per-item failure records. Each carries `name` plus the keys from format_tool_error() (`error`, `exception_type`, `meta_error?`, `hint?`). Present when error_count > 0.'}, 'status': {'enum': ['success', 'partial'], 'type': 'string'}, 'warnings': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Non-fatal configuration problems found before the write — most often a lead-gen mismatch between the creative and its ad set. The ads WERE created (PAUSED); resolve these before resuming them, or they will run without collecting leads.'}, 'account_id': {'type': 'string'}, 'next_steps': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Recommended follow-up tool call(s) for the LLM. Required on every successful mutation per CLAUDE.md; left Optional here because some tools (e.g. update_entity) currently omit it.'}, 'error_count': {'type': 'integer'}, 'all_succeeded': {'type': 'boolean', 'description': 'True if every item succeeded (no entries in `errors`).'}, 'created_count': {'type': 'integer'}}, 'description': 'Result of create_ad — N ads created (all PAUSED unless overridden).', 'additionalProperties': True}
create_adset
Create Ad Set(s)
Create one or more Meta ad sets. NOT idempotent — each call creates new ad sets. All ad sets are created as PAUSED. The targeting spec is assembled server-side from flat inputs (same format as update_adset_targeting) and validated via /reachestimate before creation. The campaign_id can be any existing campaign in the account — use list_campaigns to find one, or create_campaign to make a new one. Workflow (new campaign): 1. create_campaign(...) to get a campaign_id 2. search_targeting(search_type='interests', query='...') to find IDs 3. create_adset(adsets=[{campaign_id, countries, interests, ...}]) 4. change_entity_status(action='resume') to activate Workflow (existing campaign): 1. list_campaigns to find the campaign_id 2. create_adset(adsets=[{campaign_id, countries, interests, ...}]) 3. change_entity_status(action='resume') to activate Pixel / promoted_object: For OUTCOME_SALES campaigns, the promoted_object (pixel + conversion event) is auto-resolved if omitted — but it picks the first pixel on the account, which may not be correct if the account has multiple pixels for different sites. Call get_pixel_health or check the pixels array from list_ad_accounts to identify the right pixel by its hosts, then pass it explicitly via promoted_object={pixel_id, custom_event_type}.
Open world
Input schema
{'type': 'object', 'required': ['account_id', 'adsets'], 'properties': {'adsets': {'type': 'array', 'items': {'type': 'object', 'required': ['campaign_id'], 'properties': {'name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Ad set name. Omit to auto-generate from targeting: ADADVISOR || {audience_type} || {summary} || {geo} || {demographics} || AUTO.'}, 'zips': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': "Zip key from search_targeting geolocation results, e.g. 'US:94304'"}}, 'description': "A zip code geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': "Zip code targets from search_targeting(search_type='geolocation')"}, 'cities': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': 'City key from search_targeting geolocation results'}, 'radius': {'anyOf': [{'type': 'integer', 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Radius around city center (10-50 mi / 17-80 km)'}, 'distance_unit': {'anyOf': [{'enum': ['mile', 'kilometer'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Unit for radius'}}, 'description': "A city geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': "City targets from search_targeting(search_type='geolocation')"}, 'income': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Income targets from search_targeting(search_type='income')"}, 'age_max': {'anyOf': [{'type': 'integer', 'maximum': 65, 'minimum': 18}, {'type': 'null'}], 'default': None, 'description': 'Maximum age (18-65). NOTE: Meta rejects age_max<65 when advantage_audience=True (the default). To cap age below 65, also pass advantage_audience=False.'}, 'age_min': {'anyOf': [{'type': 'integer', 'maximum': 65, 'minimum': 18}, {'type': 'null'}], 'default': None, 'description': 'Minimum age (18-65)'}, 'genders': {'anyOf': [{'type': 'array', 'items': {'type': 'integer'}}, {'type': 'null'}], 'default': None, 'description': '[1]=male only, [2]=female only, [1,2]=both. Omit for all genders.'}, 'regions': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': 'Region key from search_targeting geolocation results'}}, 'description': "A region geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': "Region targets from search_targeting(search_type='geolocation')"}, 'end_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO datetime for ad set end (required with lifetime_budget)'}, 'behaviors': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Behavior targets from search_targeting(search_type='behaviors')"}, 'countries': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "ISO country codes, e.g. ['US', 'CA']"}, 'dsa_payor': {'anyOf': [{'type': 'string', 'maxLength': 512}, {'type': 'null'}], 'default': None, 'description': "The payor (entity paying for the ads) of all ads in this ad set. REQUIRED for ad sets targeting the EU or associated territories — Meta rejects with subcode 3858081 ('Missing DSA payor') otherwise. Auto-populated from the ad account's default_dsa_payor when omitted. Max 512 chars."}, 'interests': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Interest targets from search_targeting(search_type='interests')"}, 'bid_amount': {'anyOf': [{'type': 'number', 'exclusiveMinimum': 0}, {'type': 'null'}], 'default': None, 'description': 'For COST_CAP: the cost-per-result goal in account currency (e.g. 10.0 = $10/result). For LOWEST_COST_WITH_BID_CAP: the max bid per auction in account currency. Do NOT set with LOWEST_COST_WITH_MIN_ROAS (use roas_average_floor instead).'}, 'industries': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Industry targets from search_targeting(search_type='industries')"}, 'start_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO datetime for ad set start'}, 'campaign_id': {'type': 'string', 'minLength': 1, 'description': 'Parent campaign ID (from list_campaigns or create_campaign)'}, 'life_events': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Life event targets from search_targeting(search_type='life_events')"}, 'bid_strategy': {'anyOf': [{'enum': ['LOWEST_COST_WITHOUT_CAP', 'LOWEST_COST_WITH_BID_CAP', 'COST_CAP', 'LOWEST_COST_WITH_MIN_ROAS'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Bid strategy. Defaults to LOWEST_COST_WITHOUT_CAP (auto-bid). COST_CAP requires bid_amount (cost-per-result goal). LOWEST_COST_WITH_BID_CAP requires bid_amount (max bid per auction). LOWEST_COST_WITH_MIN_ROAS requires roas_average_floor and optimization_goal=VALUE.'}, 'daily_budget': {'anyOf': [{'type': 'number', 'exclusiveMinimum': 0}, {'type': 'null'}], 'default': None, 'description': 'Daily budget in account currency. Only if campaign has NO campaign-level budget.'}, 'billing_event': {'anyOf': [{'enum': ['IMPRESSIONS', 'LINK_CLICKS', 'THRUPLAY'], 'type': 'string'}, {'type': 'null'}], 'default': 'IMPRESSIONS', 'description': 'Billing event. Defaults to IMPRESSIONS.'}, 'work_employers': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Work employer targets from search_targeting(search_type='work_employers')"}, 'work_positions': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Work position targets from search_targeting(search_type='work_positions')"}, 'daily_spend_cap': {'anyOf': [{'type': 'number', 'minimum': 0}, {'type': 'null'}], 'default': None, 'description': "CBO only: daily spend ceiling for this ad set, in account currency. Requires the parent campaign to own a daily_budget. Meta enforces a minimum of $1 — passing 0 fails with subcode 1885056 ('maximum spend limit must be at least $1.00'). To remove an existing cap, clear it in Ads Manager."}, 'dsa_beneficiary': {'anyOf': [{'type': 'string', 'maxLength': 512}, {'type': 'null'}], 'default': None, 'description': "The beneficiary (entity benefiting from the ads) of all ads in this ad set. REQUIRED for ad sets targeting the EU or associated territories — Meta rejects with subcode 3858079 ('Missing DSA beneficiary') otherwise. Auto-populated from the ad account's default_dsa_beneficiary when omitted. Max 512 chars."}, 'lifetime_budget': {'anyOf': [{'type': 'number', 'exclusiveMinimum': 0}, {'type': 'null'}], 'default': None, 'description': 'Lifetime budget in account currency. Requires end_time.'}, 'promoted_object': {'anyOf': [{'type': 'object', 'properties': {'page_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Facebook page ID (for engagement/message objectives)'}, 'pixel_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Meta pixel ID for conversion tracking'}, 'application_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'App ID (for app promotion objectives)'}, 'object_store_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'App Store / Google Play URL (required with application_id for app promotion)'}, 'custom_event_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Conversion event, e.g. 'PURCHASE', 'LEAD', 'ADD_TO_CART'"}}, 'description': 'Conversion tracking config for an ad set.\n\nAuto-resolved for OUTCOME_SALES campaigns if omitted (picks account pixel + PURCHASE).', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Conversion tracking config. Auto-resolved for OUTCOME_SALES campaigns if omitted.'}, 'custom_audiences': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': 'Custom audiences to target (from list_custom_audiences). Includes lookalike audiences, website custom audiences, customer lists, etc. When advantage_audience=True these are applied as Advantage+ *signals* (Meta may deliver beyond them), not hard filters; set advantage_audience=False to restrict delivery strictly to them.'}, 'destination_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'WEBSITE, APP, MESSENGER, etc.'}, 'device_platforms': {'anyOf': [{'type': 'array', 'items': {'enum': ['mobile', 'desktop'], 'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "Device types. Omit for both. Call ads REQUIRE ['mobile'] — a desktop user cannot place a phone call from an ad."}, 'optimization_goal': {'anyOf': [{'enum': ['OFFSITE_CONVERSIONS', 'LINK_CLICKS', 'IMPRESSIONS', 'REACH', 'LEAD_GENERATION', 'QUALITY_LEAD', 'QUALITY_CALL', 'LANDING_PAGE_VIEWS', 'VALUE', 'CONVERSATIONS'], 'type': 'string'}, {'type': 'null'}], 'default': 'OFFSITE_CONVERSIONS', 'description': 'Optimization goal. Defaults to OFFSITE_CONVERSIONS. Use CONVERSATIONS for messaging-destination ad sets (destination_type=WHATSAPP / MESSENGER / INSTAGRAM_DIRECT) — Meta requires it on Click-to-Messenger / Click-to-WhatsApp / Click-to-Instagram / Click-to-MultiDestination ad sets.\nLead generation:\n- LEAD_GENERATION optimizes for lead VOLUME. Pair with destination_type=ON_AD and promoted_object={page_id}.\n- QUALITY_LEAD optimizes for lead QUALITY (Conversion Leads). Needs destination_type=ON_AD or MESSENGER and promoted_object={page_id}, and only works once the advertiser sends lead-stage events back via the Conversions API — without that CRM feedback Meta has no quality signal to optimize on.\n- QUALITY_CALL optimizes for call quality on call ads (destination_type=PHONE_CALL, promoted_object={page_id}).\nMeta does NOT allow changing optimization_goal on a campaign that has already published; duplicate the ad set and change it on the copy instead.'}, 'advantage_audience': {'type': 'boolean', 'default': True, 'description': "Enable Advantage+ audience expansion (Meta's ML broadens targeting beyond your spec). DEFAULTS TO TRUE — recommended for most cases. Set to False when you need a hard demographic cap (age_max<65, narrow gender) or any constraint Meta must respect exactly. Meta rejects advantage_audience=True combined with age_max<65. When True, any custom audiences / lookalikes / detailed targeting you pass are automatically submitted as Advantage+ *signals* (the 'relaxed setup'); they cannot be hard controls while Advantage+ is on (Meta rejects that as subcode 1359202) — set False to use them as controls."}, 'excluded_behaviors': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': 'Behaviors to exclude from targeting'}, 'excluded_interests': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': 'Interests to exclude from targeting'}, 'facebook_positions': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "Facebook surfaces, e.g. ['feed','video_feeds','story','reels', 'marketplace','search']. Requires 'facebook' in publisher_platforms."}, 'lifetime_spend_cap': {'anyOf': [{'type': 'number', 'minimum': 0}, {'type': 'null'}], 'default': None, 'description': 'CBO only: lifetime spend ceiling for this ad set, in account currency. Requires the parent campaign to own a lifetime_budget. Meta enforces a minimum (subcode 1885056) — passing 0 will be rejected. To remove an existing cap, clear it in Ads Manager.'}, 'roas_average_floor': {'anyOf': [{'type': 'number', 'maximum': 1000.0, 'minimum': 0.01}, {'type': 'null'}], 'default': None, 'description': 'Minimum ROAS floor for LOWEST_COST_WITH_MIN_ROAS strategy. Expressed as a multiplier: 1.5 = 150% ROAS (get $1.50 back per $1 spent). Valid range: 0.01 to 1000.0. Requires optimization_goal=VALUE.'}, 'instagram_positions': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "Instagram surfaces, e.g. ['stream','story','reels','explore']. Requires 'instagram' in publisher_platforms."}, 'is_dynamic_creative': {'type': 'boolean', 'default': False, 'description': "When true, the ad set runs as a Dynamic Creative ad set: Meta auto-combines variants from each ad's asset_feed_spec to find the best-performing combination per viewer. REQUIRED on the parent ad set for any creative built with multi-variant copy (messages / headlines / descriptions arrays in CreativeSpec). Note: dynamic-creative ad sets accept only ONE ad. Defaults to false."}, 'publisher_platforms': {'anyOf': [{'type': 'array', 'items': {'enum': ['facebook', 'instagram', 'audience_network', 'messenger', 'threads'], 'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "Restrict which platforms the ad set may deliver on. Omit for Advantage+ placements (recommended — Meta optimizes across all of them). Call ads REQUIRE ['facebook']."}, 'daily_min_spend_target': {'anyOf': [{'type': 'number', 'minimum': 0}, {'type': 'null'}], 'default': None, 'description': 'CBO only: minimum daily spend Meta will try to allocate to this ad set, in account currency. Requires the parent campaign to own a daily_budget (do not pair with an ad-set-level daily_budget). Best-effort, not a hard floor. Note: duplicate_adset(deep_copy=True) carries this over from the source ad set — if the sum of sibling floors exceeds the campaign budget, Meta blocks activation with subcode 1885648. Pass 0 to clear the inherited floor.'}, 'excluded_geo_locations': {'anyOf': [{'type': 'object', 'properties': {'zips': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': "Zip key from search_targeting geolocation results, e.g. 'US:94304'"}}, 'description': "A zip code geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': 'Zip codes to exclude'}, 'cities': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': 'City key from search_targeting geolocation results'}, 'radius': {'anyOf': [{'type': 'integer', 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Radius around city center (10-50 mi / 17-80 km)'}, 'distance_unit': {'anyOf': [{'enum': ['mile', 'kilometer'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Unit for radius'}}, 'description': "A city geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': 'Cities to exclude'}, 'regions': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': 'Region key from search_targeting geolocation results'}}, 'description': "A region geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': 'Regions to exclude'}, 'countries': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "ISO country codes to exclude, e.g. ['MX']"}}, 'description': 'Geo locations to exclude from targeting.'}, {'type': 'null'}], 'default': None, 'description': 'Geo locations to exclude from targeting'}, 'excluded_custom_audiences': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': 'Custom audiences to EXCLUDE from targeting (from list_custom_audiences).'}, 'lifetime_min_spend_target': {'anyOf': [{'type': 'number', 'minimum': 0}, {'type': 'null'}], 'default': None, 'description': 'CBO only: minimum lifetime spend Meta will try to allocate to this ad set, in account currency. Requires the parent campaign to own a lifetime_budget. Best-effort, not a hard floor. Pass 0 to clear.'}, 'audience_network_positions': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "Audience Network surfaces, e.g. ['classic','rewarded_video']. Requires 'audience_network' in publisher_platforms."}}, 'description': 'Specification for creating a single Meta ad set.'}, 'minItems': 1, 'description': 'Array of ad set specs to create. Required: name, campaign_id, at least one geo field. Optional: targeting, budgets, config.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is creating these ad sets (e.g. 'Overnight launch: 3-tier ABO for new lead-gen campaign'). Logged for telemetry."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['status', 'all_succeeded', 'account_id', 'error_count', 'currency', 'created_count', 'created'], 'properties': {'errors': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}}, {'type': 'null'}], 'default': None, 'description': 'Per-item failure records. Each carries `name` plus the keys from format_tool_error() (`error`, `exception_type`, `meta_error?`, `hint?`). Present when error_count > 0.'}, 'status': {'enum': ['success', 'partial'], 'type': 'string'}, 'created': {'type': 'array', 'items': {'type': 'object', 'required': ['index', 'adset_id', 'name', 'campaign_id', 'status', 'update_id', 'state_after'], 'properties': {'name': {'type': 'string'}, 'index': {'type': 'integer'}, 'status': {'type': 'string', 'const': 'PAUSED'}, 'adset_id': {'type': 'string'}, 'update_id': {'type': 'string'}, 'campaign_id': {'type': 'string'}, 'state_after': {'type': 'object', 'additionalProperties': True}}, 'description': 'Successfully-created ad set in a create_adset batch response.', 'additionalProperties': True}}, 'currency': {'type': 'string'}, 'account_id': {'type': 'string'}, 'next_steps': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Recommended follow-up tool call(s) for the LLM. Required on every successful mutation per CLAUDE.md; left Optional here because some tools (e.g. update_entity) currently omit it.'}, 'error_count': {'type': 'integer'}, 'all_succeeded': {'type': 'boolean', 'description': 'True if every item succeeded (no entries in `errors`).'}, 'created_count': {'type': 'integer'}}, 'description': 'Result of create_adset — N ad sets created (all PAUSED).', 'additionalProperties': True}
create_campaign
Create Campaign(s)
Create one or more Meta ad campaigns. NOT idempotent — each call creates new campaigns. All campaigns are created as PAUSED. After creation, add ad sets with create_adset, then activate with change_entity_status(action='resume'). For most use cases, use objective=OUTCOME_SALES with budget at the campaign level and LOWEST_COST_WITHOUT_CAP bid strategy. promoted_object goes on the AD SET, not here — the only exception is OUTCOME_APP_PROMOTION, where Meta requires it at campaign level for iOS 14+. Setting it here for OUTCOME_LEADS or OUTCOME_SALES is rejected with subcode 1815023. For a lead campaign, set promoted_object={'page_id': ...} on create_adset instead (or {'pixel_id': ..., 'custom_event_type': 'LEAD'} for website leads). Set spend_cap as a safety ceiling whenever this is called by an agent.
Open world
Input schema
{'type': 'object', 'required': ['account_id', 'campaigns'], 'properties': {'campaigns': {'type': 'array', 'items': {'type': 'object', 'required': ['objective'], 'properties': {'name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Campaign name. Omit to auto-generate a structured name: ADADVISOR || CBO/ABO || BOF/MOF/TOF || {strategy} || {business} || {date}.'}, 'adlabels': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}}, {'type': 'null'}], 'default': None, 'description': "Optional Meta ad labels for cross-campaign tagging/analytics rollups. JSON array of label specs, e.g. [{'name': 'BlackFriday2026'}]."}, 'objective': {'enum': ['OUTCOME_SALES', 'OUTCOME_LEADS', 'OUTCOME_ENGAGEMENT', 'OUTCOME_AWARENESS', 'OUTCOME_TRAFFIC', 'OUTCOME_APP_PROMOTION'], 'type': 'string', 'description': 'Campaign objective'}, 'spend_cap': {'anyOf': [{'type': 'number', 'exclusiveMinimum': 0}, {'type': 'null'}], 'default': None, 'description': 'Lifetime spend ceiling in account currency (e.g. 5000.0 = $5,000 max total). Hard cap — Meta stops delivery when reached. Acts as a safety rail; recommended whenever an automated agent is creating campaigns.'}, 'stop_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO datetime for campaign end. Required with lifetime_budget.'}, 'start_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "ISO datetime for campaign start, e.g. '2025-01-15T00:00:00-0500'"}, 'buying_type': {'anyOf': [{'enum': ['AUCTION', 'RESERVED'], 'type': 'string'}, {'type': 'null'}], 'default': 'AUCTION', 'description': 'Buying type (default AUCTION)'}, 'bid_strategy': {'anyOf': [{'enum': ['LOWEST_COST_WITHOUT_CAP', 'LOWEST_COST_WITH_BID_CAP', 'COST_CAP', 'LOWEST_COST_WITH_MIN_ROAS'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Bid strategy. Defaults to LOWEST_COST_WITHOUT_CAP (auto-bid). Cap values (bid_amount, roas_average_floor) are set at the ad set level, not here.'}, 'daily_budget': {'anyOf': [{'type': 'number', 'exclusiveMinimum': 0}, {'type': 'null'}], 'default': None, 'description': 'Daily budget in account currency (e.g. 50.0 = $50/day). Mutually exclusive with lifetime_budget. Budget can be set at campaign OR adset level, not both.'}, 'strategy_label': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Short 1-2 word label for the campaign strategy, used in auto-generated names. Examples: 'Prospecting', 'Retargeting', 'Scaling', 'Conversions'. Defaults to 'Prospecting' if name is omitted."}, 'lifetime_budget': {'anyOf': [{'type': 'number', 'exclusiveMinimum': 0}, {'type': 'null'}], 'default': None, 'description': 'Lifetime budget in account currency. Requires stop_time. Mutually exclusive with daily_budget.'}, 'promoted_object': {'anyOf': [{'type': 'object', 'properties': {'page_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Facebook page ID (for engagement/message objectives)'}, 'pixel_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Meta pixel ID for conversion tracking'}, 'application_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'App ID (for app promotion objectives)'}, 'object_store_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'App Store / Google Play URL (required with application_id for app promotion)'}, 'custom_event_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Conversion event, e.g. 'PURCHASE', 'LEAD', 'ADD_TO_CART'"}}, 'description': 'Conversion tracking config for an ad set.\n\nAuto-resolved for OUTCOME_SALES campaigns if omitted (picks account pixel + PURCHASE).', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Conversion tracking config at the CAMPAIGN level. Meta requires this only for iOS 14+ app promotion — set application_id + object_store_url on OUTCOME_APP_PROMOTION.\nDo NOT set it for OUTCOME_LEADS or OUTCOME_SALES: those belong on the AD SET (AdSetSpec.promoted_object), and putting one here is rejected with subcode 1815023.'}, 'source_campaign_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Existing campaign ID whose settings to copy as the template for the new campaign. Different from duplicate_campaign — that copies the live entity AND its children; this only seeds the campaign-level config.'}, 'special_ad_categories': {'anyOf': [{'type': 'array', 'items': {'enum': ['NONE', 'HOUSING', 'EMPLOYMENT', 'FINANCIAL_PRODUCTS_SERVICES', 'ISSUES_ELECTIONS_POLITICS'], 'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "Special ad categories. Allowed values: NONE, EMPLOYMENT, HOUSING, FINANCIAL_PRODUCTS_SERVICES (replaces the legacy CREDIT input from Jan 14 2025), ISSUES_ELECTIONS_POLITICS. Use ['NONE'] or omit for standard ads. When any value other than NONE is supplied, special_ad_category_country becomes required."}, 'is_skadnetwork_attribution': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': "Set True for iOS app campaigns that must use Apple's SKAdNetwork attribution (post-iOS 14.5). Required for OUTCOME_APP_PROMOTION targeting iOS."}, 'special_ad_category_country': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "ISO country codes scoping the special_ad_categories. Required by Meta for international housing/credit/employment campaigns outside the US (e.g. ['CA', 'GB']). Omit for US-only or non-special campaigns."}, 'is_adset_budget_sharing_enabled': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': "Ad set budget sharing (ABS). Only meaningful when budget is at the ad set level (no daily_budget / lifetime_budget on this spec). Required by Meta v24+ for ABO campaigns: pass True to let ad sets share up to 20% of budget with siblings, False to keep each ad set's budget isolated. Omit when setting a campaign-level budget (CBO)."}}, 'description': 'Specification for creating a single Meta campaign.'}, 'minItems': 1, 'description': 'Array of campaign specs to create. Required: objective. Optional: name (auto-generated if omitted), budgets, bid_strategy, schedule, special_ad_categories, promoted_object (ONLY for OUTCOME_APP_PROMOTION — for OUTCOME_LEADS and OUTCOME_SALES it belongs on the ad set), spend_cap (lifetime ceiling), is_skadnetwork_attribution (iOS), source_campaign_id, adlabels.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID where the new campaign(s) will live. Verify the account is reachable via list_ad_accounts first.'}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is creating these campaigns (e.g. 'Q2 retargeting test for cart abandoners'). Logged for telemetry."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['status', 'account_id', 'currency', 'all_succeeded', 'created_count', 'error_count', 'created'], 'properties': {'errors': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}}, {'type': 'null'}], 'default': None, 'description': 'Per-item failure records (name + format_tool_error keys). Present when error_count > 0.'}, 'status': {'enum': ['success', 'partial'], 'type': 'string'}, 'created': {'type': 'array', 'items': {'type': 'object', 'required': ['index', 'campaign_id', 'name', 'objective', 'status', 'update_id', 'state_after'], 'properties': {'name': {'type': 'string'}, 'index': {'type': 'integer', 'description': 'Position of this spec in the input array.'}, 'status': {'type': 'string', 'const': 'PAUSED', 'description': 'Always PAUSED — Meta requires explicit activation via change_entity_status.'}, 'objective': {'type': 'string'}, 'update_id': {'type': 'string'}, 'campaign_id': {'type': 'string'}, 'state_after': {'type': 'object', 'description': 'Snapshot of the new campaign as Meta returned it.', 'additionalProperties': True}}, 'description': 'Successfully-created campaign in a create_campaign batch response.', 'additionalProperties': True}}, 'currency': {'type': 'string'}, 'account_id': {'type': 'string'}, 'next_steps': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Recommended follow-up tool call(s) for the LLM. Required on every successful mutation per CLAUDE.md; left Optional here because some tools (e.g. update_entity) currently omit it.'}, 'error_count': {'type': 'integer'}, 'all_succeeded': {'type': 'boolean', 'description': 'True if every spec succeeded.'}, 'created_count': {'type': 'integer'}}, 'description': "Result of create_campaign — N campaigns created (all PAUSED).\n\n`status='success'` means every spec succeeded. `status='partial'` means\nsome specs failed (inspect the per-item entries in `errors`).", 'additionalProperties': True}
create_creative
Create Ad Creative
Create one or more Meta ad creatives. NOT idempotent — each call creates new creatives. Pass a list of ``creatives`` to create several in a single tool call (e.g. five image variants for A/B testing, or one creative per hash returned by the multi-select asset picker). Each item is processed independently and an error on one does not abort the others — inspect the per-item ``status`` in the returned ``results`` list. Batching rule of thumb: if you have N image_hashes / video_ids in hand from one picker turn, make ONE create_creative call with N items in ``creatives``. Calling this tool N times is wasteful and counts against the user's quota. Supports three formats per item: - image_link: single image link ad (requires image_hash + link) - video: video ad (requires video_id only — the thumbnail is resolved server-side from the video at create time. If Meta is still processing the video, the item fails with a wait-and-retry error. To pin a custom thumbnail, pass thumbnail_image_hash.) - existing_post: promote an existing FB/IG post (requires object_story_id) PLACEMENT ASSET CUSTOMIZATION: when the user has multiple aspect-ratio cuts of the SAME ad concept (1:1 for Feed, 9:16 for Stories, 16:9 for video feeds), build ONE creative with placement_videos / placement_images mapping each cut to its placement_groups — never one creative per cut. One asset must carry the DEFAULT group (catch-all, also covers Reels). The parent ad set must NOT be dynamic-creative. Different concepts/hooks still get separate creatives. page_id is auto-resolved per account — do NOT ask the user for it. For video format: do NOT pass thumbnail_url and do NOT ask the user to pick a thumbnail — the server resolves a fresh one from the video. thumbnail_url values from search_ad_videos results are signed CDN links that expire; they are discarded and re-resolved server-side. Typical full workflow (multi-variant): 1. search_ad_images / search_ad_videos — user picks up to 10 assets 2. create_creative(account_id, creatives=[{...}, {...}, ...]) — one item per asset 3. create_ad(account_id, ads=[{adset_id, creative_id, name}, ...]) IMAGE HASH PROVENANCE RULE. Every ``image_hash`` you pass MUST originate from one of these in the current conversation: - a ``search_ad_images`` result (the user selected it from the picker, or the tool returned it for a query you ran), OR - a ``complete_upload`` response (``results[].image_hash``). Do NOT reuse hashes from prior conversations or from training context. If you have a hash you're unsure about, call ``search_ad_images(account_id, queries=['<hash>'])`` first to confirm it exists in this account before passing it here. Skipping this rule is the single biggest cause of the wrong creative ending up on the wrong ad. Common Meta API errors (per item): - "Invalid image hash" — re-upload the image or verify image_hash with search_ad_images - "Invalid OAuth token" / code 190 — user must reconnect their Meta account - "Permission denied" / code 200 — check ad account permissions
Open world
Input schema
{'type': 'object', 'required': ['account_id', 'creatives'], 'properties': {'creatives': {'type': 'array', 'items': {'type': 'object', 'required': ['format'], 'properties': {'link': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Destination URL for the ad (e.g. 'https://example.com/shop'). Required for image_link format unless lead_gen_form_id is set (lead-form creatives use 'http://fb.me/' automatically). Optional for video (omit for awareness-only video)."}, 'name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Creative name in the account's creative library. Strongly recommended: list_creatives can only be searched by name, so an unnamed creative is effectively unfindable afterwards."}, 'format': {'enum': ['image_link', 'video', 'carousel', 'existing_post'], 'type': 'string', 'description': "Creative format: 'image_link' (static image ad), 'video' (video ad), 'carousel' (2-10 swipeable cards, each with its own asset and destination), or 'existing_post' (promote an existing Facebook post)."}, 'message': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Primary ad text (body copy) shown above the image/video. Mutually exclusive with 'messages' (multi-variant)."}, 'page_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Facebook Page ID to post the ad from. Omit to auto-resolve from existing creatives on this account.'}, 'headline': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Headline text. For image_link: shown below the image in the link preview. For video: mapped to video_data.title. Mutually exclusive with 'headlines' (multi-variant)."}, 'messages': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}, 'maxItems': 5}, {'type': 'null'}], 'default': None, 'description': "Multiple body copy variants (up to 5). Meta optimizes which variant to show per viewer. Produces asset_feed_spec instead of inline copy. Requires a dynamic creative ad set (is_dynamic_creative=true). Mutually exclusive with 'message'."}, 'url_tags': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "UTM tracking parameters appended to the link URL, e.g. 'utm_source=facebook&utm_medium=paid'."}, 'video_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Video ID from search_ad_videos. Required for video format.'}, 'headlines': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}, 'maxItems': 5}, {'type': 'null'}], 'default': None, 'description': "Multiple headline variants (up to 5). Meta optimizes which to show per viewer. Mutually exclusive with 'headline'."}, 'image_hash': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Image hash from upload_ad_image or search_ad_images. Required for image_link format.'}, 'description': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Description text shown below the headline in the link preview. Mutually exclusive with 'descriptions' (multi-variant)."}, 'descriptions': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}, 'maxItems': 5}, {'type': 'null'}], 'default': None, 'description': "Multiple description variants (up to 5). Meta optimizes which to show per viewer. Mutually exclusive with 'description'."}, 'phone_number': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Business phone number in E.164 form, e.g. '+15551234567'. REQUIRED when call_to_action_type='CALL_NOW' (call ads) and rejected otherwise. The MCP emits Meta's documented call_to_action.value = {'link': 'tel:<number>'} shape. Pair with an ad set using destination_type='PHONE_CALL', optimization_goal='QUALITY_CALL', device_platforms=['mobile'] and publisher_platforms=['facebook']."}, 'thumbnail_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL custom thumbnail URL — only for a stable, publicly hosted image (e.g. your own CDN). Do NOT copy thumbnail_url values from search_ad_videos results: those are signed Meta CDN links that expire within minutes, so they are discarded and re-resolved server-side anyway. Omit to let the server resolve the video's processed thumbnail at create time (recommended). Provide this OR thumbnail_image_hash (not both)."}, 'carousel_cards': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'properties': {'link': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Destination for this card. Required unless the creative sets lead_gen_form_id (lead carousels use Meta's fb.me placeholder)."}, 'headline': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Card headline (Meta's link_data `name`)."}, 'video_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Video ID from upload_ad_video / search_ad_videos. A video card must ALSO set image_hash as its thumbnail — Meta requires one alongside child_attachments.video_id.'}, 'image_hash': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Image hash from upload_ad_image / search_ad_images.'}, 'description': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Card sub-headline.'}, 'call_to_action_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Per-card CTA. Defaults to the creative-level call_to_action_type.'}}, 'description': 'One card in a carousel creative.\n\nEach card is its own image or video with its own headline, description and\ndestination — that is what makes carousels worth using over a single image.', 'additionalProperties': False}, 'maxItems': 10, 'minItems': 2}, {'type': 'null'}], 'default': None, 'description': "Cards for format='carousel'. Meta allows 2-10. With lead_gen_form_id set, every card points at that same form — Meta does not support different forms per card."}, 'object_story_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Existing Facebook post ID in the format '<PAGE_ID>_<POST_ID>'. For existing_post format. Provide this OR instagram_post_url OR facebook_post_url."}, 'lead_gen_form_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Meta Lead Form ID to attach to the creative — get one from list_lead_forms, create one with create_lead_form, or create it in Meta Ads Manager. Required when the parent ad set is configured for lead generation (optimization_goal=LEAD_GENERATION/QUALITY_LEAD with destination_type=ON_AD). When set: the link is auto-overridden to 'http://fb.me/', and call_to_action.value embeds the form ID. The CTA type must be one of APPLY_NOW, DOWNLOAD, GET_QUOTE, LEARN_MORE, SIGN_UP, or SUBSCRIBE (defaults to SIGN_UP). Works with image_link, video, carousel (every card gets the same form — Meta allows only one per carousel) and existing_post (the form rides on a top-level call_to_action, since a boosted post has no object_story_spec)."}, 'placement_images': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['image_hash', 'placement_groups'], 'properties': {'image_hash': {'type': 'string', 'minLength': 1, 'description': 'Image hash from upload_ad_image / search_ad_images.'}, 'placement_groups': {'type': 'array', 'items': {'enum': ['FEED', 'STORY', 'RIGHT_COLUMN', 'MARKETPLACE', 'VIDEO_FEEDS', 'SEARCH', 'EXPLORE', 'AUDIENCE_NETWORK', 'DEFAULT'], 'type': 'string'}, 'minItems': 1, 'description': "Placement group(s) this image serves. Match the asset's aspect ratio to the placement: FEED for 1:1/4:5, STORY for 9:16. Use DEFAULT on exactly one asset as the catch-all for every placement not claimed by another asset."}}, 'description': 'One image mapped to the placement groups it should serve.'}, 'maxItems': 10, 'minItems': 2}, {'type': 'null'}], 'default': None, 'description': "PLACEMENT ASSET CUSTOMIZATION (format='image_link' only): serve a different image per placement from ONE creative. Pass 2-10 entries, each {image_hash, placement_groups}. Mark exactly one entry DEFAULT as the catch-all. Mutually exclusive with image_hash and multi-variant copy. Requires 'link'. The parent ad set must have is_dynamic_creative=false (the default). When a user has uploaded multiple aspect-ratio cuts of the SAME concept, use this — do NOT create one creative per cut."}, 'placement_videos': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['video_id', 'placement_groups'], 'properties': {'video_id': {'type': 'string', 'minLength': 1, 'description': 'Video ID from search_ad_videos / complete_upload.'}, 'placement_groups': {'type': 'array', 'items': {'enum': ['FEED', 'STORY', 'RIGHT_COLUMN', 'MARKETPLACE', 'VIDEO_FEEDS', 'SEARCH', 'EXPLORE', 'AUDIENCE_NETWORK', 'DEFAULT'], 'type': 'string'}, 'minItems': 1, 'description': "Placement group(s) this video serves. Match the asset's aspect ratio to the placement: FEED for 1:1/4:5, STORY for 9:16, VIDEO_FEEDS for 16:9. Use DEFAULT on exactly one asset as the catch-all for every placement not claimed by another asset (including Reels)."}}, 'description': 'One video mapped to the placement groups it should serve.'}, 'maxItems': 10, 'minItems': 2}, {'type': 'null'}], 'default': None, 'description': "PLACEMENT ASSET CUSTOMIZATION (format='video' only): serve a different video per placement from ONE creative — e.g. the 1:1 cut in Feed, the 9:16 cut in Stories, the 16:9 cut in video feeds — instead of creating one ad per aspect ratio. Pass 2-10 entries, each {video_id, placement_groups}. Mark exactly one entry DEFAULT as the catch-all. Mutually exclusive with video_id, thumbnail fields, and multi-variant copy (messages/headlines/descriptions). Requires 'link'. The parent ad set must have is_dynamic_creative=false (the default). Thumbnails are resolved server-side per video. When a user has uploaded multiple aspect-ratio cuts of the SAME concept, use this — do NOT create one creative per cut."}, 'facebook_post_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Facebook post URL (e.g. share link, permalink, or /posts/ URL). For existing_post format. Auto-resolved to object_story_id. Provide this OR object_story_id OR instagram_post_url.'}, 'instagram_user_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Instagram account ID for Instagram placement (optional).'}, 'instagram_post_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Instagram post URL (e.g. 'https://www.instagram.com/p/DVb6nP5Cbm7/'). For existing_post format. Auto-resolved to the Instagram media ID. Provide this OR object_story_id OR facebook_post_url."}, 'call_to_action_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Call-to-action button label, e.g. 'SHOP_NOW', 'LEARN_MORE', 'SIGN_UP', 'GET_OFFER'. Omit to use Meta's default."}, 'thumbnail_image_hash': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL image hash to pin as the video thumbnail (from upload_ad_image or search_ad_images). Omit to let the server resolve the video's processed thumbnail at create time (recommended). Provide this OR thumbnail_url (not both)."}}, 'description': 'Specification for creating a Meta ad creative.\n\nSupports image_link, video, carousel, and existing_post formats.\nMulti-variant copy (asset_feed_spec) is supported via messages/headlines/descriptions arrays.'}, 'minItems': 1, 'description': "One or more creative specifications. Pass a list with one element for a single creative, or many for a batch.\n\nALWAYS BATCH when the user picks multiple assets in the same picker turn. The asset picker (search_ad_images / search_ad_videos) supports multi-select up to 10 — when the user selects N hashes/video_ids, build N CreativeSpecs and pass them all in a single create_creative call. Do NOT call create_creative once per hash; that wastes the user's tool quota.\n\nEach item is processed independently; a failure on one does not abort the others (inspect per-item ``status`` in the returned ``results``).\n\nThree formats supported per item:\n- format='video': requires video_id only. The thumbnail is resolved server-side from the video at create time — do NOT copy thumbnail_url values from search_ad_videos results (signed CDN links that expire; they are discarded and re-resolved anyway). To pin a custom thumbnail, pass thumbnail_image_hash.\n- format='image_link': requires image_hash and link\n- format='existing_post': requires object_story_id OR instagram_post_url OR facebook_post_url\n\nMULTIPLE ASPECT RATIOS OF THE SAME CONCEPT (e.g. the user uploaded 1:1 + 9:16 + 16:9 cuts of one ad): build ONE creative with placement_videos (or placement_images) mapping each cut to its placements — do NOT create one creative per cut, that fragments delivery into separate ads. Example: placement_videos=[{video_id: '<1x1>', placement_groups: ['FEED']}, {video_id: '<9x16>', placement_groups: ['STORY']}, {video_id: '<16x9>', placement_groups: ['DEFAULT']}]. Requires link; exactly one asset must carry DEFAULT (the catch-all, also covers Reels). Distinct concepts/hooks still get separate creatives — placement assets are for cuts of the SAME concept.\nOptional copy fields: message, headline, description, call_to_action_type, url_tags.\nFor multi-variant copy on a single creative: use messages[], headlines[], descriptions[] (max 5 each).\nFor lead-generation ads: set lead_gen_form_id (create the form in Meta Ads Manager, or via list_lead_forms / create_lead_form when enabled). The tool auto-substitutes the placeholder link 'http://fb.me/' and embeds the form ID in call_to_action.value — required for any ad set whose optimization_goal is LEAD_GENERATION/QUALITY_LEAD with destination_type=ON_AD.\npage_id is auto-resolved — do NOT ask the user for it."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is creating these creatives (e.g. '5-variant hook test for new BFCM angle'). Logged for telemetry."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['status', 'all_succeeded', 'account_id', 'success_count', 'error_count', 'creative_ids', 'results'], 'properties': {'status': {'enum': ['success', 'partial'], 'type': 'string'}, 'results': {'type': 'array', 'items': {'type': 'object', 'required': ['index', 'status'], 'properties': {'error': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'index': {'type': 'integer'}, 'detail': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Raw exception string for debugging. May leak stack-trace context.'}, 'format': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Creative format that was built (image_link, video, existing_post, etc.).'}, 'status': {'enum': ['success', 'error'], 'type': 'string'}, 'update_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'creative_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'page_id_used': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': "Per-creative result inside create_creative.results.\n\nUnlike other batch creation tools, create_creative interleaves successes\nand errors in a single `results` list. Inspect each item's `status`.", 'additionalProperties': True}, 'description': 'Per-creative results — one entry per input spec.'}, 'account_id': {'type': 'string'}, 'next_steps': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Recommended follow-up tool call(s) for the LLM. Required on every successful mutation per CLAUDE.md; left Optional here because some tools (e.g. update_entity) currently omit it.'}, 'error_count': {'type': 'integer'}, 'creative_ids': {'type': 'array', 'items': {'type': 'string'}, 'description': 'IDs of successfully-created creatives. Pass these to create_ad.'}, 'all_succeeded': {'type': 'boolean'}, 'success_count': {'type': 'integer'}}, 'description': 'Result of create_creative — N creatives created (interleaved results).', 'additionalProperties': True}
create_lead_form
Create Lead Form(s)
Create one or more Meta Lead Forms (instant forms) on a Facebook Page. NOT idempotent — each call creates new forms. Forms cannot be deleted, only ARCHIVED, so prefer one well-tested form over many trial forms. Lead forms are page-scoped: the create endpoint is /{page_id}/leadgen_forms. If page_id is omitted on a spec, the service auto-resolves the first promote_pages entry for the account. Call list_pages first if the account has multiple pages and you need to pin a specific one. REQUIRED fields per spec: name, questions, privacy_policy (url + link_text), and follow_up_action_url. The follow_up_action_url is enforced by Meta even though it isn't documented in the public Marketing API guide — omitting it returns 'Missing field(s): FollowUpActionURL'. Use these forms in a creative by passing the form ID directly: create_creative(creatives=[{format:'image_link', image_hash:'...', lead_gen_form_id:'<form_id>'}]) The MCP layer auto-substitutes the placeholder link 'http://fb.me/' and embeds the form ID in call_to_action.value as Meta requires. CTA defaults to SIGN_UP; override with call_to_action_type=APPLY_NOW / DOWNLOAD / GET_QUOTE / LEARN_MORE / SUBSCRIBE. Pair with create_campaign(objective='OUTCOME_LEADS') and create_adset(optimization_goal='LEAD_GENERATION', destination_type='ON_AD', promoted_object={page_id: '<the form's Page id>'}). promoted_object is REQUIRED on an OUTCOME_LEADS ad set — omitting it is rejected before the ad set is created. Requires the token to have ads_management + pages_manage_ads, and the ADVERTISE task on the target page.
Open world
Input schema
{'type': 'object', 'required': ['account_id', 'forms'], 'properties': {'forms': {'type': 'array', 'items': {'type': 'object', 'required': ['name', 'questions', 'privacy_policy', 'follow_up_action_url'], 'properties': {'name': {'type': 'string', 'minLength': 1, 'description': 'Form name shown in the Forms Library.'}, 'locale': {'type': 'string', 'default': 'en_US', 'description': "Form locale, e.g. en_US, fr_FR, ar_AR, pt_BR. Case-insensitive. Must be one of Meta's 32 supported locales — an unsupported value is rejected before the form is created."}, 'page_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Facebook Page ID to create the form on. If omitted, the service auto-resolves the first promote_pages entry for the ad account (use list_pages to inspect candidates and pin a specific one).'}, 'questions': {'type': 'array', 'items': {'type': 'object', 'required': ['type'], 'properties': {'key': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Stable lowercase question key (used in the lead webhook payload). Auto-generated for standard types. Recommended for CUSTOM so downstream systems can map answers reliably.'}, 'type': {'enum': ['FULL_NAME', 'FIRST_NAME', 'LAST_NAME', 'EMAIL', 'PHONE', 'CUSTOM', 'DATE_TIME', 'STORE_LOOKUP', 'ID_AR_DNI', 'ID_CPF', 'ID_CL_RUT', 'ID_CO_CC', 'ID_EC_CI', 'ID_PE_DNI'], 'type': 'string', 'description': 'Question type. Standard types (FULL_NAME / EMAIL / PHONE / FIRST_NAME / LAST_NAME) auto-fill the label and key. Use CUSTOM for free-text or dropdown questions, DATE_TIME for appointment scheduling, STORE_LOOKUP for store-locator (requires context_provider_type=LOCATION_MANAGER), and ID_* for the country-specific national ID inputs.'}, 'label': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Question text shown to the user. Required for CUSTOM, DATE_TIME, and STORE_LOOKUP.'}, 'options': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['value'], 'properties': {'key': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Stable option key — this is what comes back in the lead payload. Derived from `value` when omitted; pass it explicitly when a downstream CRM expects a particular key.'}, 'value': {'type': 'string', 'description': 'Display label shown to the user.'}}, 'description': 'A single dropdown option on a CUSTOM lead form question.'}}, {'type': 'null'}], 'default': None, 'description': 'Dropdown options for CUSTOM. Omit to render a free-text input.'}, 'inline_context': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Helper text shown directly below the field.'}, 'context_provider_type': {'anyOf': [{'type': 'string', 'const': 'LOCATION_MANAGER'}, {'type': 'null'}], 'default': None, 'description': 'Required for STORE_LOOKUP. Set to LOCATION_MANAGER.'}, 'conditional_questions_choices': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "On a DEPENDENT question: which of the parent's option keys must be selected for this question to appear."}, 'conditional_questions_group_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Groups a parent question with the follow-ups that depend on it. Use the same id on the parent and its dependents.'}, 'dependent_conditional_questions': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}}, {'type': 'null'}], 'default': None, 'description': 'On a PARENT question: the follow-up questions it can reveal, each shaped like a normal question object.'}}, 'description': 'A single question on a Meta Lead Form (instant form).'}, 'minItems': 1, 'description': 'Ordered list of questions shown to the user. Common starting set: FULL_NAME, EMAIL, PHONE, plus 1-3 CUSTOM qualifier questions. Every extra question costs volume and buys intent — 3-5 is the usual sweet spot.'}, 'context_card': {'anyOf': [{'type': 'object', 'required': ['title', 'content'], 'properties': {'style': {'enum': ['PARAGRAPH_STYLE', 'LIST_STYLE'], 'type': 'string', 'default': 'PARAGRAPH_STYLE', 'description': 'PARAGRAPH_STYLE renders `content` as prose; LIST_STYLE renders it as bullet points.'}, 'title': {'type': 'string', 'minLength': 1, 'description': 'Headline on the intro screen.'}, 'content': {'type': 'array', 'items': {'type': 'string'}, 'minItems': 1, 'description': 'Body copy. One entry for PARAGRAPH_STYLE; one entry per bullet for LIST_STYLE.'}, 'button_text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Label on the button that advances to the questions.'}, 'cover_photo_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Photo ID for the intro screen image. Defaults to the ad's creative."}}, 'description': "The intro screen shown before a lead form's questions.\n\nOptional, but the single biggest lever on lead quality that costs nothing:\nit sets expectations before someone starts filling in the form.", 'additionalProperties': False}, {'type': 'null'}], 'default': None, 'description': 'Intro screen shown before the questions — headline, body copy and button label. Omit for a form that opens straight on the questions. Setting expectations here is the cheapest lead-quality lever available.'}, 'privacy_policy': {'type': 'object', 'required': ['url', 'link_text'], 'properties': {'url': {'type': 'string', 'minLength': 1, 'description': "HTTPS URL to the advertiser's privacy policy."}, 'link_text': {'type': 'string', 'minLength': 1, 'description': "Anchor text shown in the form (e.g. 'Privacy Policy')."}}, 'description': 'Privacy policy URL + link text. Required by Meta.'}, 'thank_you_page': {'anyOf': [{'type': 'object', 'required': ['button_type'], 'properties': {'body': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Subheading / supporting text.'}, 'title': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Headline shown on the thank-you page.'}, 'button_text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Button label, e.g. 'Visit website'. REQUIRED for every button_type except NONE — Meta does not supply a default and rejects the form with '(#100) Button text is missing for Thank You Page'."}, 'button_type': {'enum': ['VIEW_WEBSITE', 'CALL_BUSINESS', 'DOWNLOAD', 'MESSAGE_BUSINESS', 'VIEW_ON_FACEBOOK', 'NONE'], 'type': 'string', 'description': 'CTA on the thank-you page. VIEW_WEBSITE requires website_url; CALL_BUSINESS requires business_phone_number (plus country_code); VIEW_ON_FACEBOOK is the button type gated-file downloads require; NONE shows no button.'}, 'website_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Required when button_type is VIEW_WEBSITE.'}, 'country_code': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "ISO country code for business_phone_number, e.g. 'US'. Meta rejects CALL_BUSINESS without it in most locales."}, 'enable_messenger': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': 'Offer a Messenger thread from the thank-you page.'}, 'business_phone_number': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Required when button_type is CALL_BUSINESS (E.164 format).'}}, 'description': 'Thank-you page shown after the user submits the form.\n\nThis is the last moment a lead is warm, so the button matters: sending\nthem to a booking page or a phone call converts far better than leaving\nthem on the default confirmation.', 'additionalProperties': False}, {'type': 'null'}], 'default': None, 'description': 'Thank-you page config. If omitted, Meta shows the default copy.'}, 'custom_disclaimer': {'anyOf': [{'type': 'object', 'required': ['title'], 'properties': {'title': {'type': 'string', 'description': 'Heading for the disclaimer block.'}, 'body_text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Body copy shown under the title. Sent to Meta as the nested body.text it expects.'}, 'checkboxes': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key', 'text'], 'properties': {'key': {'type': 'string', 'minLength': 1, 'description': "Stable checkbox key. This is what comes back in a lead's custom_disclaimer_responses, so it is the consent record — keep it meaningful and stable."}, 'text': {'type': 'string', 'description': 'Consent text shown next to the checkbox.'}, 'is_required': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': 'When true the form cannot be submitted without ticking it.'}, 'is_checked_by_default': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': 'Pre-ticks the box. Many jurisdictions do not treat a pre-ticked box as valid consent — check before using this.'}}, 'description': 'One consent checkbox inside a custom disclaimer.', 'additionalProperties': False}}, {'type': 'null'}], 'default': None, 'description': 'Consent checkboxes. Omit for a disclaimer that is informational only (no explicit opt-in captured).'}}, 'description': "Advertiser's own consent / legal text, shown before submission.\n\nRequired in practice for regulated verticals (finance, insurance,\nhealthcare, automotive in several markets) where a plain privacy-policy\nlink is not sufficient consent. Answers come back on each lead as\ncustom_disclaimer_responses — see list_leads.", 'additionalProperties': False}, {'type': 'null'}], 'default': None, 'description': "Advertiser's own consent text and opt-in checkboxes, shown before submission. Needed by regulated verticals where a privacy-policy link alone is not adequate consent. Responses come back per lead as custom_disclaimer_responses (list_leads)."}, 'tracking_parameters': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Flat {key: value} object passed back on every lead webhook. Useful for source attribution. Note: Meta returns this as an array on read, but it MUST be sent as an object on create.'}, 'follow_up_action_url': {'type': 'string', 'minLength': 1, 'description': "HTTPS URL surfaced after submission (e.g. calendar booking link or advertiser homepage). Required by Meta even though the public Marketing API guide does not list it — omitting raises OAuthException code 100 / subcode 1892085 'Missing field(s): FollowUpActionURL'. If unsure, pass the same URL as privacy_policy.url's domain root (e.g. https://example.com)."}, 'is_optimized_for_quality': {'type': 'boolean', 'default': False, 'description': 'Adds a review-and-confirm step before submission. Recommended for high-intent funnels (test drives, demos) where lead quality matters more than volume.'}, 'should_enforce_work_email': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': 'Reject free consumer email domains (gmail.com, etc.) on the EMAIL question. The standard B2B lead-quality filter — cuts volume sharply, so pair it with a volume check.'}, 'is_phone_sms_verify_enabled': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': 'Require the user to confirm their phone number by SMS. Strong filter against fake numbers where phone follow-up is the point.'}, 'allow_organic_lead_retrieval': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': 'Allow leads submitted from organic (non-ad) reach of the post to be retrieved alongside paid leads.'}, 'question_page_custom_headline': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Custom headline above the question list.'}, 'block_display_for_non_targeted_viewer': {'type': 'boolean', 'default': False, 'description': "When true, only people inside the ad's targeting can see / submit the form (filters organic and out-of-target views)."}}, 'description': 'Specification for creating a single Meta Lead Form (instant form).', 'additionalProperties': False}, 'minItems': 1, 'description': 'Array of lead form specs to create. Each form requires name, questions, and privacy_policy. page_id is optional (auto-resolved from account if omitted — use list_pages to inspect candidates).'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is creating these lead forms (e.g. 'PodoStart Bundle + 2 fallback offers for BUC lead-gen launch'). Logged for telemetry."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['status', 'all_succeeded', 'account_id', 'error_count', 'created_count', 'created'], 'properties': {'errors': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}}, {'type': 'null'}], 'default': None, 'description': 'Per-item failure records. Each carries `name` plus the keys from format_tool_error() (`error`, `exception_type`, `meta_error?`, `hint?`). Present when error_count > 0.'}, 'status': {'enum': ['success', 'partial'], 'type': 'string'}, 'created': {'type': 'array', 'items': {'type': 'object', 'required': ['index', 'lead_form_id', 'name', 'update_id', 'state_after'], 'properties': {'name': {'type': 'string'}, 'index': {'type': 'integer'}, 'page_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'page_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'update_id': {'type': 'string'}, 'state_after': {'type': 'object', 'additionalProperties': True}, 'lead_form_id': {'type': 'string'}}, 'description': 'Successfully-created lead form in a create_lead_form batch response.', 'additionalProperties': True}}, 'account_id': {'type': 'string'}, 'next_steps': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Recommended follow-up tool call(s) for the LLM. Required on every successful mutation per CLAUDE.md; left Optional here because some tools (e.g. update_entity) currently omit it.'}, 'error_count': {'type': 'integer'}, 'all_succeeded': {'type': 'boolean', 'description': 'True if every item succeeded (no entries in `errors`).'}, 'created_count': {'type': 'integer'}}, 'description': 'Result of create_lead_form — N forms created on a Facebook Page.', 'additionalProperties': True}
create_lead_form_audience
Create Lead-Form Audience
Build a custom audience from how people interacted with a lead form. This is what turns lead gen into a funnel instead of a one-shot: the people who opened your form and didn't finish are the warmest audience the campaign creates, and submitters should usually be EXCLUDED from prospecting so you stop paying to re-acquire them. NOT idempotent — each call creates a new audience. Audiences take a few hours to populate and need ~1000 people before Meta will deliver against them. Typical uses: - Retarget drop-offs: event_name='lead_generation_dropoff' - Exclude existing leads: event_name='lead_generation_submitted', then pass the audience to update_adset_targeting as an exclusion - Seed a lookalike: create with 'lead_generation_submitted', then create_lookalike_audience from it
Open world
Input schema
{'type': 'object', 'required': ['account_id', 'name', 'form_ids', 'event_name'], 'properties': {'name': {'type': 'string', 'minLength': 1, 'description': "Audience name, e.g. 'Form openers - not submitted - 30d'."}, 'source': {'enum': ['facebook', 'instagram'], 'type': 'string', 'default': 'facebook', 'description': 'Which surface the form was filled on. Instagram lead forms are a separate event source from Facebook ones.'}, 'form_ids': {'type': 'array', 'items': {'type': 'string'}, 'minItems': 1, 'description': 'Lead form IDs to build the audience from (list_lead_forms).'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'event_name': {'enum': ['lead_generation_submitted', 'lead_generation_opened', 'lead_generation_dropoff'], 'type': 'string', 'description': 'Which interaction to capture. lead_generation_submitted = completed the form (EXCLUDE these from prospecting so you stop paying for leads you already have; use as a lookalike seed). lead_generation_opened = opened it. lead_generation_dropoff = opened but did not submit — the highest-intent retargeting pool a lead campaign produces.'}, 'description': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Optional note stored on the audience.'}, 'retention_days': {'type': 'integer', 'default': 90, 'maximum': 90, 'minimum': 1, 'description': 'How long someone stays in the audience. Meta caps lead-form engagement audiences at 90 days (unlike Page or site audiences).'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['next_steps', 'audience_id', 'name', 'update_id', 'event_name', 'form_ids', 'retention_days', 'source'], 'properties': {'name': {'type': 'string'}, 'source': {'type': 'string', 'description': "'facebook' or 'instagram' — which surface the form was filled on."}, 'status': {'type': 'string', 'const': 'success', 'default': 'success'}, 'form_ids': {'type': 'array', 'items': {'type': 'string'}, 'description': 'Lead forms the audience was built from.'}, 'update_id': {'type': 'string'}, 'event_name': {'type': 'string', 'description': 'Which form interaction the audience captures.'}, 'next_steps': {'type': 'string', 'description': 'Recommended follow-up tool call.'}, 'audience_id': {'type': 'string'}, 'retention_days': {'type': 'integer', 'description': 'How long someone stays in the audience (Meta caps this at 90).'}, 'operation_status': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': "Meta's operation_status object for population state (None until ready)."}, 'approximate_count_lower_bound': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Lower bound of approximate audience size. None until Meta finishes populating.'}, 'approximate_count_upper_bound': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None}}, 'description': "Result of create_lead_form_audience — lead-form engagement audience.\n\nDeliberately its own model rather than reusing the website one: a\nlead-form audience has no pixel and no prefill, so annotating the tool\nwith CreateWebsiteAudienceResponse made FastMCP's output validation fail\non the missing `event_name`/`pixel_id`/`prefill` — AFTER the audience had\nalready been created on Meta. The caller saw an error for a mutation that\nhad succeeded, which invites a retry and a duplicate audience.", 'additionalProperties': True}
create_lookalike_audience
Create Lookalike Audience
Create a lookalike audience from an existing custom audience seed. NOT idempotent — each call creates a new audience on Meta. Lookalike audiences take 1-6 hours to fully populate. Common use cases: - 1% lookalike of purchasers: Find people most similar to your buyers - 5% lookalike for broader reach: Larger audience, less precise match - Tiered lookalikes: starting_ratio=0.01, ratio=0.03 targets 1%-3% band (excludes the top 1% you're already targeting separately) Workflow: 1. list_custom_audiences(account_id) — find a seed audience 2. create_lookalike_audience(origin_audience_id=..., country='US', ratio=0.01) 3. create_adset(..., custom_audiences=[{id: '<lookalike_id>', name: '...'}])
Open world
Input schema
{'type': 'object', 'required': ['account_id', 'name', 'origin_audience_id', 'country'], 'properties': {'name': {'type': 'string', 'minLength': 1, 'description': "Audience name (e.g. '1% US Lookalike - Purchasers')"}, 'ratio': {'anyOf': [{'type': 'number', 'maximum': 0.2, 'minimum': 0.01}, {'type': 'null'}], 'default': None, 'description': 'Top percentage of population to target (0.01 = 1%, 0.05 = 5%, max 0.20 = 20%). Smaller = more similar. Defaults to 0.01 (1%).'}, 'country': {'type': 'string', 'maxLength': 2, 'minLength': 2, 'description': "ISO country code to find lookalike people in (e.g. 'US', 'CA', 'GB')."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is creating this lookalike (e.g. '1% LAL of high-LTV purchasers for prospecting'). Logged for telemetry."}, 'starting_ratio': {'anyOf': [{'type': 'number', 'maximum': 0.19, 'minimum': 0.0}, {'type': 'null'}], 'default': None, 'description': 'Start percentage for tiered lookalikes. E.g. starting_ratio=0.01, ratio=0.03 targets the 1%-3% band. Must be less than ratio.'}, 'origin_audience_id': {'type': 'string', 'minLength': 1, 'description': 'Seed audience ID from list_custom_audiences. Must have at least 100 members.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['next_steps', 'audience_id', 'name', 'update_id', 'origin_audience_id', 'country', 'ratio'], 'properties': {'band': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Human-readable size band (e.g. '1-2%') — convenience field."}, 'name': {'type': 'string'}, 'ratio': {'type': 'number', 'description': 'Similarity ratio (0.01 = top 1% most similar).'}, 'status': {'type': 'string', 'const': 'success', 'default': 'success'}, 'country': {'type': 'string', 'description': 'ISO country code the lookalike was built for.'}, 'update_id': {'type': 'string'}, 'next_steps': {'type': 'string', 'description': 'Recommended follow-up tool call.'}, 'audience_id': {'type': 'string'}, 'operation_status': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': "Meta's operation_status object for population state (None until ready)."}, 'origin_audience_id': {'type': 'string', 'description': 'ID of the seed custom audience.'}, 'approximate_count_lower_bound': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Lower bound of approximate audience size. None until Meta finishes populating.'}, 'approximate_count_upper_bound': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None}}, 'description': 'Result of create_lookalike_audience — seed-derived prospecting audience.', 'additionalProperties': True}
create_website_audience
Create Website Audience
Create a website custom audience based on pixel events. NOT idempotent — each call creates a new audience on Meta. Common use cases: - Exclude recent purchasers: event_name='Purchase', retention_days=90 - Target cart abandoners: create one audience for AddToCart (include) and one for Purchase (exclude), then use both in create_adset - All website visitors: omit event_name, retention_days=30 - High-intent visitors: event_name='ViewContent', retention_days=14 After creation, use the returned audience in ad set targeting: - create_adset(..., custom_audiences=[{id: '<id>', name: '<name>'}]) - create_adset(..., excluded_custom_audiences=[{id: '<id>', name: '<name>'}]) - update_adset_targeting(..., custom_audiences=[{id: '<id>', name: '<name>'}])
Open world
Input schema
{'type': 'object', 'required': ['account_id', 'name', 'retention_days'], 'properties': {'name': {'type': 'string', 'minLength': 1, 'description': "Audience name (e.g. 'Purchasers - Last 90 Days', 'Cart Abandoners - 30 Days')"}, 'prefill': {'type': 'boolean', 'default': True, 'description': 'Include historical website activity before audience creation. Defaults to true.'}, 'pixel_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Pixel ID to use. Omit to auto-resolve from the account (most accounts have one pixel).'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'event_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Pixel event to match. Common events: Purchase, AddToCart, ViewContent, InitiateCheckout, Lead, CompleteRegistration, Search, AddPaymentInfo, AddToWishlist, PageView. Omit to match ALL website visitors.'}, 'description': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Optional description for the audience.'}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is creating this audience (e.g. 'exclude 90-day purchasers from prospecting'). Logged for telemetry."}, 'retention_days': {'type': 'integer', 'maximum': 180, 'minimum': 1, 'description': 'How many days to keep people in the audience (1-180). E.g. 30, 60, 90, 180.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['next_steps', 'audience_id', 'name', 'update_id', 'event_name', 'retention_days', 'pixel_id', 'prefill'], 'properties': {'name': {'type': 'string'}, 'status': {'type': 'string', 'const': 'success', 'default': 'success'}, 'prefill': {'type': 'boolean', 'description': 'True if Meta backfilled the audience with historical pixel data.'}, 'pixel_id': {'type': 'string'}, 'update_id': {'type': 'string'}, 'event_name': {'type': 'string', 'description': "The pixel event the audience tracks (e.g. 'Purchase'). 'All website visitors' when unfiltered."}, 'next_steps': {'type': 'string', 'description': 'Recommended follow-up tool call.'}, 'audience_id': {'type': 'string'}, 'retention_days': {'type': 'integer', 'description': 'How long users stay in the audience after the matching event.'}, 'operation_status': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': "Meta's operation_status object for population state (None until ready)."}, 'approximate_count_lower_bound': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Lower bound of approximate audience size. None until Meta finishes populating.'}, 'approximate_count_upper_bound': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None}}, 'description': 'Result of create_website_audience — pixel-event-based custom audience.', 'additionalProperties': True}
duplicate_ad
Duplicate Ad
Duplicate a Meta ad into the same or a different ad set. NOT idempotent — each call creates a new ad. The new ad inherits the original's creative and tracking specs. Use target_adset_id to place the copy in any existing ad set — not just duplicated ones. Common use cases: A/B test the same creative under different targeting, or add a proven ad to a new ad set. To activate, call change_entity_status(action='resume') after duplication.
Open world
Input schema
{'type': 'object', 'required': ['account_id', 'ad_id'], 'properties': {'ad_id': {'type': 'string', 'minLength': 1, 'description': 'The ad ID to duplicate (from list_ads or get_performance)'}, 'reason': {'anyOf': [{'enum': ['COPY', 'SCALE', 'TEST', 'ISOLATE'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Why the ad is being duplicated. Appears in the auto-generated name. Defaults to COPY.'}, 'status': {'enum': ['PAUSED', 'ACTIVE'], 'type': 'string', 'default': 'PAUSED', 'description': 'Initial status for the new ad. Defaults to PAUSED.'}, 'new_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Name for the new ad. Defaults to '{original_name} || {REASON} || {date}'."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is duplicating this ad (e.g. 'A/B the winning creative under a new audience'). Logged for telemetry."}, 'target_adset_id': {'anyOf': [{'type': 'string', 'minLength': 1}, {'type': 'null'}], 'default': None, 'description': "Ad set to place the copy in. Can be any existing ad set (from list_adsets, create_adset, or duplicate_adset). Defaults to the original's ad set."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['next_steps', 'account_id', 'original_ad_id', 'ad_status'], 'properties': {'status': {'type': 'string', 'const': 'success', 'default': 'success'}, 'new_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'ad_status': {'enum': ['PAUSED', 'ACTIVE'], 'type': 'string'}, 'new_ad_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'update_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Audit-record ID — the entity_updates row created for this change.'}, 'account_id': {'type': 'string'}, 'next_steps': {'type': 'string', 'description': 'Recommended follow-up tool call.'}, 'entity_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Human-readable name of the entity.'}, 'state_after': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Entity state snapshot after the change (shape varies by entity type).'}, 'state_before': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Entity state snapshot before the change (shape varies by entity type).'}, 'original_ad_id': {'type': 'string'}, 'target_adset_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': 'Result of duplicating a Meta ad.', 'additionalProperties': True}
duplicate_adset
Duplicate Ad Set
Duplicate a Meta ad set into the same or a different campaign. NOT idempotent — each call creates a new ad set. By default duplicates the ad set AND all its ads (deep_copy=true). Set deep_copy=false to create an empty shell, then selectively add ads with duplicate_ad. The new ad set inherits the original's budget and targeting. Use target_campaign_id to place the copy in any existing campaign — not just duplicated ones. Common use cases: move a top-performing ad set into a scaling campaign, or populate a new campaign with cherry-picked ad sets. For EU-targeted ad sets, supply dsa_beneficiary and dsa_payor so the new ad set complies with the EU Digital Services Act (Meta rejects with subcodes 3858079 / 3858081 otherwise).
Open world
Input schema
{'type': 'object', 'required': ['account_id', 'adset_id'], 'properties': {'reason': {'anyOf': [{'enum': ['COPY', 'SCALE', 'TEST', 'ISOLATE'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Why the ad set is being duplicated. Appears in the auto-generated name. Defaults to COPY.'}, 'status': {'enum': ['PAUSED', 'ACTIVE'], 'type': 'string', 'default': 'PAUSED', 'description': 'Initial status for the new ad set. Defaults to PAUSED.'}, 'adset_id': {'type': 'string', 'minLength': 1, 'description': 'The ad set ID to duplicate (from list_adsets or get_performance — never invent IDs)'}, 'new_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Name for the new ad set. Defaults to '{original_name} || {REASON} || {date}'."}, 'deep_copy': {'type': 'boolean', 'default': True, 'description': 'If true (default), duplicate all ads under the ad set. If false, create an empty ad set shell.'}, 'dsa_payor': {'anyOf': [{'type': 'string', 'maxLength': 512}, {'type': 'null'}], 'default': None, 'description': "DSA payor for the new ad set. Required when copying into an EU-targeted context if the source ad set or destination campaign doesn't already carry one (Meta subcode 3858081). Max 512 chars."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': "Ad account ID where adset_id lives. Meta returns code 100 (parameter error) if the account doesn't own the ad set. Look it up via list_adsets when in doubt."}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is duplicating (e.g. 'cloning into geo-isolated campaign for incrementality test'). Logged for telemetry."}, 'dsa_beneficiary': {'anyOf': [{'type': 'string', 'maxLength': 512}, {'type': 'null'}], 'default': None, 'description': "DSA beneficiary for the new ad set. Required when copying into an EU-targeted context if the source ad set or destination campaign doesn't already carry one (Meta subcode 3858079). Max 512 chars."}, 'target_campaign_id': {'anyOf': [{'type': 'string', 'minLength': 1}, {'type': 'null'}], 'default': None, 'description': "Campaign to place the copy in. Can be any existing campaign (from list_campaigns, create_campaign, or duplicate_campaign). Defaults to the original's campaign."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['account_id', 'original_adset_id', 'deep_copy', 'adset_status'], 'properties': {'status': {'type': 'string', 'const': 'success', 'default': 'success'}, 'new_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'warnings': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None}, 'deep_copy': {'type': 'boolean'}, 'update_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Audit-record ID — the entity_updates row created for this change.'}, 'account_id': {'type': 'string'}, 'next_steps': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Recommended follow-up tool call(s) for the LLM. Required on every successful mutation per CLAUDE.md; left Optional here because some tools (e.g. update_entity) currently omit it.'}, 'entity_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Human-readable name of the entity.'}, 'state_after': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Entity state snapshot after the change (shape varies by entity type).'}, 'adset_status': {'enum': ['PAUSED', 'ACTIVE'], 'type': 'string'}, 'new_adset_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'state_before': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Entity state snapshot before the change (shape varies by entity type).'}, 'original_adset_id': {'type': 'string'}, 'target_campaign_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Campaign the new adset belongs to. None means same campaign as the original.'}}, 'description': 'Result of duplicating a Meta ad set.', 'additionalProperties': True}
duplicate_campaign
Duplicate Campaign
Duplicate a Meta campaign, creating a new copy in the same ad account. NOT idempotent — each call creates a new campaign. By default duplicates the campaign AND all its ad sets and ads (deep_copy=true). Set deep_copy=false to create an empty shell, then selectively add ad sets with duplicate_adset. The new campaign inherits the original's budgets and is created PAUSED so you can review before activating. Typical workflow: 1. duplicate_campaign → new campaign (PAUSED) 2. change_entity_budget → adjust budget 3. change_entity_status(action='resume') → activate
Open world
Input schema
{'type': 'object', 'required': ['account_id', 'campaign_id'], 'properties': {'reason': {'anyOf': [{'enum': ['COPY', 'SCALE', 'TEST', 'ISOLATE'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Why the campaign is being duplicated. Appears in the auto-generated name. Defaults to COPY.'}, 'status': {'enum': ['PAUSED', 'ACTIVE'], 'type': 'string', 'default': 'PAUSED', 'description': 'Initial status for the new campaign. Defaults to PAUSED.'}, 'new_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Name for the new campaign. Defaults to '{original_name} || {REASON} || {date}'."}, 'deep_copy': {'type': 'boolean', 'default': True, 'description': 'If true (default), duplicate all ad sets and ads. If false, create an empty campaign shell.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': "Ad account ID where campaign_id lives. Meta returns code 100 (parameter error) if the account doesn't own the campaign. Look it up via list_campaigns when in doubt."}, 'campaign_id': {'type': 'string', 'minLength': 1, 'description': 'The campaign ID to duplicate (from list_campaigns or get_performance — never invent IDs)'}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is duplicating (e.g. 'scaling top-performing creative into new geo'). Logged for telemetry."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['account_id', 'original_campaign_id', 'deep_copy', 'campaign_status'], 'properties': {'status': {'enum': ['success', 'partial_success'], 'type': 'string', 'default': 'success', 'description': "'partial_success' when deep_copy was requested but some ad sets or ads failed to copy (see adsets_failed / ads_failed and the *_copy_failures lists)."}, 'new_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'warnings': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Non-fatal issues during duplication (e.g. some children failed).'}, 'ads_total': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Ads on the source campaign (deep_copy only).'}, 'deep_copy': {'type': 'boolean', 'description': 'True if all child ad sets / ads were copied.'}, 'update_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Audit-record ID — the entity_updates row created for this change.'}, 'account_id': {'type': 'string'}, 'ads_copied': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Ads that actually landed in the new campaign (deep_copy only).'}, 'ads_failed': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Ads that failed to copy (deep_copy only).'}, 'next_steps': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Recommended follow-up tool call(s) for the LLM. Required on every successful mutation per CLAUDE.md; left Optional here because some tools (e.g. update_entity) currently omit it.'}, 'entity_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Human-readable name of the entity.'}, 'state_after': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Entity state snapshot after the change (shape varies by entity type).'}, 'adsets_total': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Ad sets on the source campaign (deep_copy only).'}, 'state_before': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Entity state snapshot before the change (shape varies by entity type).'}, 'adsets_copied': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Ad sets that actually landed in the new campaign (deep_copy only).'}, 'adsets_failed': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Ad sets that failed to copy entirely — all their ads were lost too.'}, 'campaign_status': {'enum': ['PAUSED', 'ACTIVE'], 'type': 'string', 'description': 'Initial status of the new campaign (typically PAUSED).'}, 'new_campaign_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ID of the new campaign. None only if Meta returned an unparseable response.'}, 'ad_copy_failures': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}}, {'type': 'null'}], 'default': None, 'description': 'Per-ad structured failures (ad_id, ad_name, meta_error, hint) for individual ads that failed in the per-ad fallback path.'}, 'adset_copy_failures': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}}, {'type': 'null'}], 'default': None, 'description': 'Per-ad-set structured failures (adset_id, adset_name, meta_error, hint) for ad sets that failed to copy entirely.'}, 'original_campaign_id': {'type': 'string'}}, 'description': 'Result of duplicating a Meta campaign.', 'additionalProperties': True}
estimate_audience_size
Estimate Audience Size
Estimate the audience size for a targeting spec without creating anything. Use this to explore how big an audience would be before committing to creating an ad set. Helps with strategy planning — e.g. "how many people in the US aged 25-45 are interested in yoga?" Returns estimated reach (users count). Note: estimates are approximate and may return -1 for very new or niche audiences. At least one geo field (countries, cities, regions, or zips) is required.
Read only Open world
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'zips': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': "Zip key from search_targeting geolocation results, e.g. 'US:94304'"}}, 'description': "A zip code geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': "Zip code targets from search_targeting(search_type='geolocation')."}, 'cities': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': 'City key from search_targeting geolocation results'}, 'radius': {'anyOf': [{'type': 'integer', 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Radius around city center (10-50 mi / 17-80 km)'}, 'distance_unit': {'anyOf': [{'enum': ['mile', 'kilometer'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Unit for radius'}}, 'description': "A city geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': "City targets from search_targeting(search_type='geolocation')."}, 'age_max': {'anyOf': [{'type': 'integer', 'maximum': 65, 'minimum': 18}, {'type': 'null'}], 'default': None, 'description': 'Maximum age (18-65).'}, 'age_min': {'anyOf': [{'type': 'integer', 'maximum': 65, 'minimum': 18}, {'type': 'null'}], 'default': None, 'description': 'Minimum age (18-65).'}, 'genders': {'anyOf': [{'type': 'array', 'items': {'type': 'integer'}}, {'type': 'null'}], 'default': None, 'description': '[1]=male only, [2]=female only, [1,2]=both. Omit for all.'}, 'regions': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': 'Region key from search_targeting geolocation results'}}, 'description': "A region geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': "Region targets from search_targeting(search_type='geolocation')."}, 'behaviors': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Behavior targets from search_targeting(search_type='behaviors')."}, 'countries': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "ISO country codes, e.g. ['US', 'CA']. At least one geo field required."}, 'interests': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Interest targets from search_targeting(search_type='interests')."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'custom_audiences': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': 'Custom audiences to include (from list_custom_audiences).'}, 'excluded_custom_audiences': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': 'Custom audiences to exclude (from list_custom_audiences).'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['account_id', 'estimate_ready', 'targeting_summary', 'targeting_spec'], 'properties': {'account_id': {'type': 'string'}, 'estimate_ready': {'type': 'boolean', 'description': 'True when Meta has a reliable estimate. False means try again with broader targeting.'}, 'targeting_spec': {'type': 'object', 'description': 'Final targeting_spec that was estimated.', 'additionalProperties': True}, 'targeting_summary': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'array', 'items': {}}], 'description': 'Human-readable summary of the resolved targeting spec. May be a dict of structured fields or a list of bullet strings depending on the version of the underlying targeting service.'}, 'users_lower_bound': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': "Lower bound of estimated reach. None when Meta couldn't estimate."}, 'users_upper_bound': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None}}, 'description': 'Audience-size estimate for a targeting spec — no entity created.', 'additionalProperties': True}
get_lead_form
Get Lead Form
Inspect one Meta Lead Form in full — every question, disclaimer and screen. Use this whenever you need to know what a form actually ASKS. A lead form's questions CANNOT be edited after creation, so reading an existing form is the only way to judge whether it fits a campaign — and the only way to decide whether to reuse it or build a new one. Returns the complete form: questions (type, key, label, dropdown options, helper text), the intro context_card, the thank_you_page, legal_content (custom consent checkboxes), privacy_policy_url, tracking_parameters, follow_up_action_url, the quality/gating flags, and lifetime lead counts. `questions[].key` is the field name that appears in the submitted lead payload — match on it when mapping a form to a CRM. Typical uses: - "Which form does this ad use, and what does it ask?" — read lead_gen_form_id from list_creatives, then call this. - Before reusing a form: confirm the questions suit the new offer. - Diagnosing low lead quality: too few questions, or no is_optimized_for_quality. Calls Meta directly (lead forms are not ingested into the tenant DB) and needs a Page access token, so the connected user must have a role on the owning Page.
Read only Open world
Input schema
{'type': 'object', 'required': ['account_id', 'lead_form_id'], 'properties': {'page_id': {'anyOf': [{'type': 'string', 'minLength': 1}, {'type': 'null'}], 'default': None, 'description': "Facebook Page that owns the form. Omit to use the account's default promote_page. Only needed when the account has several Pages and the form lives on a non-default one (see list_pages)."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'lead_form_id': {'type': 'string', 'minLength': 1, 'description': "Lead form ID — from list_lead_forms, from create_lead_form's response, or from a creative's lead_gen_form_id (list_creatives)."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['id'], 'properties': {'id': {'type': 'string'}, 'name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'locale': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'status': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ACTIVE, ARCHIVED, DELETED, or DRAFT.'}, 'page_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'page_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'questions': {'type': 'array', 'items': {'type': 'object', 'properties': {'key': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Stable field key — this is the name that appears in the lead payload.'}, 'type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Question type, e.g. EMAIL, PHONE, FULL_NAME, CUSTOM. Non-CUSTOM types are prefilled by Meta from the user's profile."}, 'label': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Question text shown to the user.'}, 'options': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}}, {'type': 'null'}], 'default': None, 'description': 'Dropdown choices for CUSTOM questions, as [{key, value}].'}, 'inline_context': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Helper text shown beneath the question.'}}, 'description': 'One question as Meta stores it on an existing lead form.', 'additionalProperties': True}, 'description': 'Every question on the form, in display order.'}, 'leads_count': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Total leads collected to date (all time, not date-scoped).'}, 'context_card': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'The intro screen shown before the questions (title, style, content, button_text).'}, 'created_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'legal_content': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Custom disclaimers / consent checkboxes attached to the form.'}, 'thank_you_page': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'The completion screen shown after submission.'}, 'allow_organic_lead': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': 'True when the form can also be submitted from organic (non-ad) traffic.'}, 'privacy_policy_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'expired_leads_count': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': "Leads that have aged past Meta's 90-day retrieval window."}, 'organic_leads_count': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None}, 'tracking_parameters': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Flat key/value map echoed back on every lead for this form.'}, 'follow_up_action_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'follow_up_action_text': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'is_optimized_for_quality': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': 'True for the higher-intent form type (adds a review step before submit). Trades volume for lead quality.'}, 'question_page_custom_headline': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'block_display_for_non_targeted_viewer': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None}}, 'description': "Full detail for a single lead form, from get_lead_form.\n\nA lead form's questions cannot be edited after creation, so this read is\nthe only way to inspect what an in-market form actually asks.", 'additionalProperties': True}
get_performance
Get Performance
Get performance metrics — how are my campaigns/ads doing? Use this to compare entities, find top or bottom performers, check spend and ROAS, or get an overview of account performance. Each row includes the ad set's primary Result (Meta Ads Manager's KPI column): result_count, result_value, cost_per_result, plus result_action_type (raw Meta event) and conversion_result_name (humanized label, e.g. "Purchase", "Subscribe", "Lead"). PREFER these when reporting performance — they reflect what the user sees in Ads Manager. Legacy purchases / leads / revenue / cpa remain for backward compat but will be 0 on Subscribe / Lead / custom-conversion campaigns where result_count is the real KPI. For mixed-KPI entities (has_multiple_conversions=True — common on campaigns/creatives whose child adsets optimize for different goals), response_format='full' additionally returns kpi_breakdown: an array of per-KPI buckets {action_type, conversion_result_name, result_count, result_value, cost_per_result, spend} sorted by result_count DESC. Use it to answer "which KPI is driving this campaign" without a follow-up call. Also returns: id, name, status, objective, objective_label, spend, roas, cpl, cpc, cpm, ctr, clicks, impressions, daily_budget, days_active, has_multiple_conversions. Use response_format='concise' to reduce tokens. Once the account's first sync has completed, data refreshes every 30 minutes and may lag behind real-time. Before that (data_synced=false in list_ad_accounts) the answer is read live from Meta — see provenance — and is complete for the last 90 days and current entities; days_active is null until the sync completes, and a window reaching further back than 90 days, or include_lead_breakdown, returns status='not_yet_landed' with the sync_state instead of rows.
Read only
Input schema
{'type': 'object', 'required': ['account_id', 'level'], 'properties': {'level': {'enum': ['campaign', 'adset', 'ad'], 'type': 'string', 'description': "Hierarchy level: 'campaign', 'adset', or 'ad'"}, 'date_to': {'anyOf': [{'type': 'string', 'pattern': '^\\d{4}-\\d{2}-\\d{2}$'}, {'type': 'null'}], 'default': None, 'description': 'End date (YYYY-MM-DD). Defaults to yesterday.'}, 'adset_id': {'anyOf': [{'type': 'string', 'minLength': 1}, {'type': 'null'}], 'default': None, 'description': "Filter ads to this ad set. Only used when level is 'ad'."}, 'date_from': {'anyOf': [{'type': 'string', 'pattern': '^\\d{4}-\\d{2}-\\d{2}$'}, {'type': 'null'}], 'default': None, 'description': 'Start date (YYYY-MM-DD). Defaults to 45 days ago.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'campaign_id': {'anyOf': [{'type': 'string', 'minLength': 1}, {'type': 'null'}], 'default': None, 'description': "Filter ad sets or ads to this campaign. Ignored when level is 'campaign'."}, 'response_format': {'enum': ['full', 'concise'], 'type': 'string', 'default': 'full', 'description': "'concise' keeps key metrics only: id, name, status, spend, result_count, cost_per_result, conversion_result_name, result_action_type, objective_label, roas, revenue, cpa, cpl, leads, ctr, daily_budget. 'full' returns everything."}, 'include_lead_breakdown': {'type': 'boolean', 'default': False, 'description': "Add lead_action_breakdown to each row: leads split by the action type that produced them (instant form vs website pixel vs offline/CRM). Use when an account runs more than one lead destination and you need to tell them apart. NOTE: '*_grouped' types are roll-ups — never sum a grouped type together with the types it rolls up."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'type': 'object', 'required': ['account_id', 'currency', 'level', 'date_from', 'date_to', 'count', 'results'], 'properties': {'count': {'type': 'integer'}, 'level': {'enum': ['campaign', 'adset', 'ad'], 'type': 'string'}, 'date_to': {'type': 'string', 'description': 'Range end (YYYY-MM-DD).'}, 'results': {'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}, 'description': "Per-entity KPI rows. Each has id, name, status, spend, revenue, roas, ctr, plus the Meta 'Results' fields when response_format='full'."}, 'currency': {'type': 'string', 'description': 'Account currency (ISO 4217).'}, 'date_from': {'type': 'string', 'description': 'Range start (YYYY-MM-DD).'}, 'account_id': {'type': 'string'}, 'provenance': {'anyOf': [{'type': 'object', 'required': ['source'], 'properties': {'as_of': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO-8601 timestamp the data is current as of.'}, 'source': {'enum': ['mirror', 'live', 'live_only'], 'type': 'string', 'description': "'mirror' = the synced copy (refreshed every ~30 min); 'live' = read from Meta just now because the account's first sync has not completed; 'live_only' = data Meta alone holds."}, 'stale_reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Set when the synced copy can no longer advance: 'token_invalid' (reconnect Meta) or 'disconnected'. Null when healthy."}}, 'description': 'Where an account-data answer came from and as of when.\n\nEvery read served by the Meta Ads Data Service carries this. The row shape\nnever depends on the source — only this block does. A read the service\ncannot answer for an account whose first sync has not completed (history\nbeyond the live window, too many entities) is answered with\n``{"status": "not_yet_landed", "message", "sync_state", ...}`` instead of\nthe tool\'s normal payload; ``{"status": "live_read_unavailable",\n"retry_after"}`` means Meta could not be read right now — retry later.\nThose two payloads are ``NotYetLandedResponse`` and\n``LiveReadUnavailableResponse`` below, and a tool that can answer with\nthem declares it through ``Refusable[...]``.', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Source and freshness of this answer.'}, 'last_synced': {'anyOf': [{'type': 'number'}, {'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "When this account's data was last refreshed — the synced copy's last ingestion, or the read time for a live answer. Unix timestamp (float) or ISO string depending on the sync source."}, 'data_available_to': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'data_available_from': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Earliest date for which we have synced data (None if never synced).'}}, 'description': "Aggregated KPIs for entities at a given hierarchy level.\n\n`results` rows include id / name / status / spend / revenue / roas / ctr\nplus result_count / result_value / cost_per_result / result_action_type /\nconversion_result_name when response_format='full'. Concise mode strips\nlow-info fields. When entities span mixed conversion KPIs, each row also\ncarries `kpi_breakdown` (sorted DESC by volume).", 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id'], 'properties': {'status': {'type': 'string', 'const': 'not_yet_landed'}, 'message': {'type': 'string', 'description': 'One sentence to relay: what is available until the sync completes and why this request needs more.'}, 'account_id': {'type': 'string'}, 'sync_state': {'anyOf': [{'type': 'object', 'required': ['landed'], 'properties': {'basis': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "What the verdict rests on: 'run' (a sync record), 'legacy' (the data_synced flag) or 'none'."}, 'phase': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'initial | catchup | landed | failed; null without a sync record.'}, 'landed': {'type': 'boolean', 'description': 'True once every stream of the first sync has completed.'}, 'streams': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Per-stream status: pending | running | completed | failed.'}, 'task_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'failed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'landed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'streams_done': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams completed so far.'}, 'streams_total': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams the first sync covers.'}, 'insights_chunks': {'anyOf': [{'type': 'object', 'additionalProperties': {'anyOf': [{'type': 'integer'}, {'type': 'null'}]}}, {'type': 'null'}], 'default': None, 'description': '{done, total} chunks of the insights backfill.'}, 'catchup_started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': "Progress of an account's first sync — ``SyncState.as_dict()``\n(``app.services.meta_ads_data.landed``) field for field.", 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'How far the first sync is.'}, 'live_window_days': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Days of insights history a Live Read answers (the live window).'}}, 'description': "What a façade-backed tool answers instead of its normal payload when\nthe account's first sync has not completed and the question is outside\nwhat a Live Read may answer — ``NotYetLanded.to_mcp()`` field for field.\nA normal result, never ``isError``: relay ``message`` and, when asked how\nfar along the sync is, ``sync_state``.", 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id', 'retry_after'], 'properties': {'status': {'type': 'string', 'const': 'live_read_unavailable'}, 'message': {'type': 'string'}, 'account_id': {'type': 'string'}, 'retry_after': {'type': 'integer', 'description': 'Seconds to wait before retrying.'}}, 'description': 'Meta could not be read for the account right now (throttled, time\nbudget exceeded) — ``LiveReadUnavailable.to_mcp()`` field for field.\nA normal result, never ``isError``; retry after ``retry_after`` seconds.', 'additionalProperties': True}]}
get_pixel_health
Get Pixel Health
Get pixel health for a Meta ad account — metadata, event counts, source and host breakdown. Returns the account's pixel(s) with: - Metadata: name, last_fired_time, automatic matching config, first-party cookie status - Event stats (last 7 days): per-event counts (PageView, Purchase, AddToCart, etc.) - Source breakdown (last 7 days): browser pixel vs Conversions API (CAPI) event counts - Host breakdown (last 7 days): which domains/sites the pixel fires on Use this to diagnose tracking issues: missing events, low CAPI coverage, stale pixels, or disabled automatic advanced matching. The pixels array from list_ad_accounts provides basic info (id, name, last_fired_time). Use this tool for the full diagnostic breakdown.
Read only Open world
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['account_id', 'pixel_count', 'pixels'], 'properties': {'pixels': {'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}, 'description': 'Per-pixel summary: id, name, last_fired_time, code (Meta health code), events_by_type (PageView, Purchase, etc.) and events_by_source (browser, server/CAPI). Use to diagnose tracking gaps.'}, 'account_id': {'type': 'string'}, 'pixel_count': {'type': 'integer'}}, 'description': 'Per-pixel health snapshot for an ad account.', 'additionalProperties': True}
get_timeseries
Get Timeseries
Get daily trend data — spot performance shifts day over day. Use this to see how a campaign, ad set, or ad is trending, detect when a performance change started, or compare day-over-day patterns. Each day includes the entity's primary Result (Meta Ads Manager's KPI column): result_count, result_value, cost_per_result, plus result_action_type (raw Meta event) and conversion_result_name (humanized label). PREFER these when reporting trends — they reflect what the user sees in Ads Manager. Legacy purchases / leads / revenue / cpa remain for backward compat but will be 0 on Subscribe / Lead / custom-conversion entities where result_count is the real KPI. When a day spans multiple KPIs (has_multiple_conversions=True on that day — rare for adsets/ads, common on campaign-level queries), response_format='full' additionally returns kpi_breakdown for that day: an array of per-KPI buckets sorted by result_count DESC. Use it to see the per-KPI split rather than a single aggregated Result. Also returns per day: spend, roas, cpl, cpc, cpm, ctr, clicks, impressions, funnel conversion rates, frequency. Use response_format='concise' to reduce tokens. For account-level totals, use get_performance at campaign level instead. Once the account's first sync has completed, data refreshes every 30 minutes and may lag behind real-time; before that the series is read live from Meta (see provenance) and a start_date older than 90 days returns status='not_yet_landed' with the sync_state instead of days.
Read only
Input schema
{'type': 'object', 'required': ['account_id', 'entity_type', 'entity_id'], 'properties': {'end_date': {'anyOf': [{'type': 'string', 'pattern': '^\\d{4}-\\d{2}-\\d{2}$'}, {'type': 'null'}], 'default': None, 'description': 'End date (YYYY-MM-DD). Defaults to yesterday.'}, 'entity_id': {'type': 'string', 'minLength': 1, 'description': 'The specific campaign, ad set, or ad ID (from list_campaigns, list_adsets, list_ads, or get_performance). Must NOT be the account ID.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'start_date': {'anyOf': [{'type': 'string', 'pattern': '^\\d{4}-\\d{2}-\\d{2}$'}, {'type': 'null'}], 'default': None, 'description': 'Start date (YYYY-MM-DD). Defaults to 45 days ago.'}, 'entity_type': {'enum': ['campaign', 'adset', 'ad'], 'type': 'string', 'description': "Entity type: 'campaign', 'adset', or 'ad'. Account-level is NOT supported."}, 'response_format': {'enum': ['full', 'concise'], 'type': 'string', 'default': 'full', 'description': "'concise' keeps key daily metrics: date, spend, result_count, cost_per_result, conversion_result_name, result_action_type, roas, revenue, purchases, cpa, clicks, ctr. 'full' returns all fields."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'type': 'object', 'required': ['entity_type', 'entity_id', 'account_id', 'currency', 'start_date', 'end_date', 'days_fetched', 'days'], 'properties': {'days': {'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}, 'description': 'One entry per calendar day in the requested range, oldest first. Each contains date_start, spend, impressions, clicks, result_count, result_value, cost_per_result. Days with no activity are present with zero metrics.'}, 'currency': {'type': 'string'}, 'end_date': {'type': 'string'}, 'entity_id': {'type': 'string'}, 'account_id': {'type': 'string'}, 'provenance': {'anyOf': [{'type': 'object', 'required': ['source'], 'properties': {'as_of': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO-8601 timestamp the data is current as of.'}, 'source': {'enum': ['mirror', 'live', 'live_only'], 'type': 'string', 'description': "'mirror' = the synced copy (refreshed every ~30 min); 'live' = read from Meta just now because the account's first sync has not completed; 'live_only' = data Meta alone holds."}, 'stale_reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Set when the synced copy can no longer advance: 'token_invalid' (reconnect Meta) or 'disconnected'. Null when healthy."}}, 'description': 'Where an account-data answer came from and as of when.\n\nEvery read served by the Meta Ads Data Service carries this. The row shape\nnever depends on the source — only this block does. A read the service\ncannot answer for an account whose first sync has not completed (history\nbeyond the live window, too many entities) is answered with\n``{"status": "not_yet_landed", "message", "sync_state", ...}`` instead of\nthe tool\'s normal payload; ``{"status": "live_read_unavailable",\n"retry_after"}`` means Meta could not be read right now — retry later.\nThose two payloads are ``NotYetLandedResponse`` and\n``LiveReadUnavailableResponse`` below, and a tool that can answer with\nthem declares it through ``Refusable[...]``.', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Source and freshness of this answer.'}, 'start_date': {'type': 'string', 'description': 'ISO date.'}, 'entity_type': {'enum': ['campaign', 'adset', 'ad'], 'type': 'string'}, 'days_fetched': {'type': 'integer', 'description': 'Number of days actually present in `days`.'}}, 'description': 'Daily metrics for a single entity across a date range.', 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id'], 'properties': {'status': {'type': 'string', 'const': 'not_yet_landed'}, 'message': {'type': 'string', 'description': 'One sentence to relay: what is available until the sync completes and why this request needs more.'}, 'account_id': {'type': 'string'}, 'sync_state': {'anyOf': [{'type': 'object', 'required': ['landed'], 'properties': {'basis': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "What the verdict rests on: 'run' (a sync record), 'legacy' (the data_synced flag) or 'none'."}, 'phase': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'initial | catchup | landed | failed; null without a sync record.'}, 'landed': {'type': 'boolean', 'description': 'True once every stream of the first sync has completed.'}, 'streams': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Per-stream status: pending | running | completed | failed.'}, 'task_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'failed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'landed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'streams_done': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams completed so far.'}, 'streams_total': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams the first sync covers.'}, 'insights_chunks': {'anyOf': [{'type': 'object', 'additionalProperties': {'anyOf': [{'type': 'integer'}, {'type': 'null'}]}}, {'type': 'null'}], 'default': None, 'description': '{done, total} chunks of the insights backfill.'}, 'catchup_started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': "Progress of an account's first sync — ``SyncState.as_dict()``\n(``app.services.meta_ads_data.landed``) field for field.", 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'How far the first sync is.'}, 'live_window_days': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Days of insights history a Live Read answers (the live window).'}}, 'description': "What a façade-backed tool answers instead of its normal payload when\nthe account's first sync has not completed and the question is outside\nwhat a Live Read may answer — ``NotYetLanded.to_mcp()`` field for field.\nA normal result, never ``isError``: relay ``message`` and, when asked how\nfar along the sync is, ``sync_state``.", 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id', 'retry_after'], 'properties': {'status': {'type': 'string', 'const': 'live_read_unavailable'}, 'message': {'type': 'string'}, 'account_id': {'type': 'string'}, 'retry_after': {'type': 'integer', 'description': 'Seconds to wait before retrying.'}}, 'description': 'Meta could not be read for the account right now (throttled, time\nbudget exceeded) — ``LiveReadUnavailable.to_mcp()`` field for field.\nA normal result, never ``isError``; retry after ``retry_after`` seconds.', 'additionalProperties': True}]}
init_upload
Init Upload
Get presigned S3 URLs for direct upload — internal helper for the Upload Creatives widget. Prefer calling upload_creatives, which opens the widget and orchestrates the full S3 → Meta flow. This tool returns presigned PUT URLs for each file; the widget uploads directly to S3 using these URLs, then calls complete_upload to push the bytes to Meta.
Open world
Input schema
{'type': 'object', 'required': ['account_id', 'files'], 'properties': {'files': {'type': 'array', 'items': {'type': 'object', 'required': ['filename', 'content_type', 'size'], 'properties': {'size': {'type': 'integer', 'minimum': 0, 'description': 'File size in bytes. Max 30 MB for images, 4 GB for videos — exceeding either cap rejects the file with a per-item error.'}, 'filename': {'type': 'string', 'minLength': 1, 'description': "Original file name shown to the user (e.g. 'hero.jpg')."}, 'content_type': {'type': 'string', 'minLength': 1, 'description': 'MIME type. Allowed: image/jpeg, image/png, video/mp4, video/quicktime.'}}, 'description': 'A file the upload widget intends to send to S3 (one per queued item).'}, 'minItems': 1, 'description': 'Files the widget intends to upload (one per queued asset).'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Meta ad account ID'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['task_id', 'files'], 'properties': {'files': {'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}, 'description': 'Per-file presigned upload metadata. Each entry has name, presigned PUT URL, content_type, and expiry timestamp.'}, 'status': {'type': 'string', 'const': 'success', 'default': 'success'}, 'task_id': {'type': 'string', 'description': 'Pass back to complete_upload once every file has uploaded.'}}, 'description': 'Result of init_upload — presigned S3 PUT URLs for client-side upload.', 'additionalProperties': True}
list_ad_accounts
List Ad Accounts
Discover and list all Meta ad accounts the user can access — start here. This is the entry point for every session. Call this FIRST to find available ad accounts and get the ad_account_id values required by all other tools. Returns each account's name, numeric ID, platform type, business name, currency, data_synced / landed / sync_state, and pixel summary (id, name, last_fired_time, and hosts for each pixel on the account). Use the hosts to identify which pixel belongs to which website/business. data_synced=false means the account's first Meta sync has not completed: history older than 90 days is still syncing. While live_reads_enabled is true, every read tool then answers live from Meta and is complete for the last 90 days and for current entities — do not wait for data_synced to analyse recent performance. A question the live path cannot answer (history beyond 90 days, too many entities) comes back as status='not_yet_landed' with the sync_state; sync_state.streams_done / streams_total is what to relay when the user asks how far along the sync is. While live_reads_enabled is false such accounts read the partial synced copy, as before. After listing accounts, read the resource adadvisor://account/{ad_account_id}/context to get business-specific thresholds (break-even ROAS, AOV, target CPL, daily budget cap) and the AI-generated business research report.
Read only Open world
Input schema
{'type': 'object', 'properties': {}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['count', 'accounts'], 'properties': {'count': {'type': 'integer', 'description': 'Number of accounts returned.'}, 'accounts': {'type': 'array', 'items': {'type': 'object', 'required': ['ad_account_id', 'ad_account_name', 'account_type', 'is_active', 'data_synced', 'business_name', 'business_id'], 'properties': {'landed': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': 'The switch the data service routes on: true = every read comes from the synced copy; false = the first sync is still running. Null for non-Meta accounts or when the sync record is unreadable.'}, 'pixels': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id'], 'properties': {'id': {'type': 'string', 'description': 'Pixel ID.'}, 'name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Pixel display name.'}, 'hosts': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}}, {'type': 'null'}], 'default': None, 'description': 'Domains the pixel fires on, sorted by hit count.'}, 'last_fired_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO-8601 timestamp of the last firing.'}}, 'description': 'Pixel summary attached to an ad account.', 'additionalProperties': True}}, {'type': 'null'}], 'default': None, 'description': 'Pixel summary (Meta accounts only).'}, 'currency': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO-4217 currency.'}, 'timezone': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'IANA timezone name.'}, 'is_active': {'type': 'boolean', 'description': 'Whether the account is connected and active.'}, 'sync_state': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Progress of the first sync (Meta accounts): landed, phase (initial | catchup | landed | failed), streams {name: pending | running | completed | failed}, streams_done / streams_total, insights_chunks {done, total}, started_at, landed_at, failed_at. Relay streams_done/streams_total when the user asks how far along the sync is.'}, 'business_id': {'type': 'string', 'description': 'Internal business UUID.'}, 'data_synced': {'type': 'boolean', 'description': "False until the account's first Meta sync has completed in full. While false (and live_reads_enabled is true) reads are served live from Meta and are complete for the last 90 days and current entities; only older history is still syncing."}, 'account_type': {'type': 'string', 'description': "Platform type, e.g. 'meta', 'tiktok'."}, 'ad_account_id': {'type': 'string', 'description': 'Numeric Meta ad account ID.'}, 'business_name': {'type': 'string', 'description': 'AdAdvisor-side business name.'}, 'ad_account_name': {'type': 'string', 'description': 'Human-readable account name.'}}, 'description': 'One ad account row from list_ad_accounts.', 'additionalProperties': True}}, 'live_reads_enabled': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': 'Environment switch. True: accounts whose first sync has not completed are read live from Meta (90-day window). False: such accounts read the partial synced copy, as before.'}}, 'description': 'Response from list_ad_accounts.', 'additionalProperties': True}
list_ads
List Ads
List ads — show which creatives are running and their setup. Use this to see what ads exist, review creative details, or answer "what ads are in this ad set/campaign?" questions. Returns: id, name, campaign_id, adset_id, status, creative, tracking_specs, schedule. `creative` is a REFERENCE — normally just {"id": "..."} — not the creative itself. To see what the ad actually runs (its copy, media, and any lead_gen_form_id), take creative.id and call: list_creatives(account_id, creative_ids=['<creative.id>']) Pagination: response includes total / count / offset / has_more / next_offset. When has_more is true, call again with offset=next_offset to fetch the next page. Does NOT include performance metrics — use get_performance for that.
Read only
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'limit': {'anyOf': [{'type': 'integer', 'maximum': 200, 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Max results to return per page (default 50, max 200).'}, 'offset': {'type': 'integer', 'default': 0, 'minimum': 0, 'description': 'Number of results to skip for pagination. Use next_offset from a prior response to fetch the next page.'}, 'search': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Filter by name (case-insensitive substring match). Omit to return all.'}, 'status': {'anyOf': [{'enum': ['ACTIVE', 'PAUSED', 'DELETED', 'ARCHIVED', 'DISAPPROVED', 'PENDING_REVIEW', 'PREAPPROVED', 'PENDING_BILLING_INFO', 'CAMPAIGN_PAUSED', 'ADSET_PAUSED', 'IN_PROCESS', 'WITH_ISSUES'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Filter by effective status. Includes the configured statuses (ACTIVE / PAUSED / ARCHIVED / DELETED) plus Meta-derived ones (DISAPPROVED, PENDING_REVIEW, WITH_ISSUES, IN_PROCESS, ADSET_PAUSED, CAMPAIGN_PAUSED). Omit for all.'}, 'adset_id': {'anyOf': [{'type': 'string', 'minLength': 1}, {'type': 'null'}], 'default': None, 'description': 'Filter to ads in this ad set. Omit to return all ads (optionally filtered by campaign_id).'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'campaign_id': {'anyOf': [{'type': 'string', 'minLength': 1}, {'type': 'null'}], 'default': None, 'description': 'Filter to ads in this campaign. Omit to return all ads in the account.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'type': 'object', 'required': ['total', 'count', 'offset', 'has_more', 'account_id', 'currency', 'ads'], 'properties': {'ads': {'type': 'array', 'items': {'type': 'object', 'required': ['id'], 'properties': {'id': {'type': 'string'}, 'name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'status': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'adset_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'creative': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': "Reference to the ad's creative — normally just {'id': '...'}, NOT the creative's contents. Pass creative.id to list_creatives(creative_ids=[...]) to get the copy, media and lead_gen_form_id."}, 'campaign_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'created_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'updated_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'tracking_specs': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}}, {'type': 'null'}], 'default': None}}, 'description': 'One ad row from list_ads.', 'additionalProperties': True}}, 'count': {'type': 'integer', 'description': 'Number of rows in this response.'}, 'total': {'type': 'integer', 'description': 'Total matching rows across all pages.'}, 'offset': {'type': 'integer', 'description': 'Pagination offset of this page.'}, 'message': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Set when the result page is empty but rows exist — explains what the filters excluded. Relay it rather than concluding the entity is missing.'}, 'currency': {'type': 'string'}, 'has_more': {'type': 'boolean', 'description': 'True when there are more rows past this page.'}, 'account_id': {'type': 'string'}, 'provenance': {'anyOf': [{'type': 'object', 'required': ['source'], 'properties': {'as_of': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO-8601 timestamp the data is current as of.'}, 'source': {'enum': ['mirror', 'live', 'live_only'], 'type': 'string', 'description': "'mirror' = the synced copy (refreshed every ~30 min); 'live' = read from Meta just now because the account's first sync has not completed; 'live_only' = data Meta alone holds."}, 'stale_reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Set when the synced copy can no longer advance: 'token_invalid' (reconnect Meta) or 'disconnected'. Null when healthy."}}, 'description': 'Where an account-data answer came from and as of when.\n\nEvery read served by the Meta Ads Data Service carries this. The row shape\nnever depends on the source — only this block does. A read the service\ncannot answer for an account whose first sync has not completed (history\nbeyond the live window, too many entities) is answered with\n``{"status": "not_yet_landed", "message", "sync_state", ...}`` instead of\nthe tool\'s normal payload; ``{"status": "live_read_unavailable",\n"retry_after"}`` means Meta could not be read right now — retry later.\nThose two payloads are ``NotYetLandedResponse`` and\n``LiveReadUnavailableResponse`` below, and a tool that can answer with\nthem declares it through ``Refusable[...]``.', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Source and freshness of this answer.'}, 'next_offset': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': "Pass to the next call's offset parameter (null when has_more is false)."}, 'total_unfiltered': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': "How many ads exist in scope before discretionary filtering. Zero means the scope genuinely has none; non-zero with an empty result means the filters excluded them all. A just-created (always PAUSED) ad never matches status='ACTIVE'."}}, 'description': 'Response from list_ads.', 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id'], 'properties': {'status': {'type': 'string', 'const': 'not_yet_landed'}, 'message': {'type': 'string', 'description': 'One sentence to relay: what is available until the sync completes and why this request needs more.'}, 'account_id': {'type': 'string'}, 'sync_state': {'anyOf': [{'type': 'object', 'required': ['landed'], 'properties': {'basis': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "What the verdict rests on: 'run' (a sync record), 'legacy' (the data_synced flag) or 'none'."}, 'phase': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'initial | catchup | landed | failed; null without a sync record.'}, 'landed': {'type': 'boolean', 'description': 'True once every stream of the first sync has completed.'}, 'streams': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Per-stream status: pending | running | completed | failed.'}, 'task_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'failed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'landed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'streams_done': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams completed so far.'}, 'streams_total': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams the first sync covers.'}, 'insights_chunks': {'anyOf': [{'type': 'object', 'additionalProperties': {'anyOf': [{'type': 'integer'}, {'type': 'null'}]}}, {'type': 'null'}], 'default': None, 'description': '{done, total} chunks of the insights backfill.'}, 'catchup_started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': "Progress of an account's first sync — ``SyncState.as_dict()``\n(``app.services.meta_ads_data.landed``) field for field.", 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'How far the first sync is.'}, 'live_window_days': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Days of insights history a Live Read answers (the live window).'}}, 'description': "What a façade-backed tool answers instead of its normal payload when\nthe account's first sync has not completed and the question is outside\nwhat a Live Read may answer — ``NotYetLanded.to_mcp()`` field for field.\nA normal result, never ``isError``: relay ``message`` and, when asked how\nfar along the sync is, ``sync_state``.", 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id', 'retry_after'], 'properties': {'status': {'type': 'string', 'const': 'live_read_unavailable'}, 'message': {'type': 'string'}, 'account_id': {'type': 'string'}, 'retry_after': {'type': 'integer', 'description': 'Seconds to wait before retrying.'}}, 'description': 'Meta could not be read for the account right now (throttled, time\nbudget exceeded) — ``LiveReadUnavailable.to_mcp()`` field for field.\nA normal result, never ``isError``; retry after ``retry_after`` seconds.', 'additionalProperties': True}]}
list_adsets
List Ad Sets
List ad sets — show targeting, budgets, and optimization config. Use this to see what ad sets exist, review targeting and setup, or answer "what ad sets are in this campaign?" questions. Returns: id, name, campaign_id, status, targeting, daily_budget, lifetime_budget, budget_remaining, bid_amount, bid_strategy, optimization_goal, destination_type, billing_event, promoted_object, schedule, and CBO ad-set-level spend controls (daily_min_spend_target, daily_spend_cap, lifetime_min_spend_target, lifetime_spend_cap). The spend controls populate from the 30-min refresh — null on rows synced before the field landed. For lead generation, destination_type + promoted_object are what tell you where the leads actually go: ON_AD = instant form on the ad itself (promoted_object.page_id), WEBSITE = an offsite landing page (promoted_object.pixel_id + custom_event_type=LEAD), MESSENGER / INSTAGRAM_DIRECT / WHATSAPP = a messaging thread, PHONE_CALL = a call. destination_type is null on ad sets last synced before it was ingested — re-sync to populate it. Pagination: response includes total / count / offset / has_more / next_offset. When has_more is true, call again with offset=next_offset to fetch the next page. Does NOT include performance metrics — use get_performance for that.
Read only
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'limit': {'anyOf': [{'type': 'integer', 'maximum': 200, 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Max results to return per page (default 50, max 200).'}, 'offset': {'type': 'integer', 'default': 0, 'minimum': 0, 'description': 'Number of results to skip for pagination. Use next_offset from a prior response to fetch the next page.'}, 'search': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Filter by name (case-insensitive substring match). Omit to return all.'}, 'status': {'anyOf': [{'enum': ['ACTIVE', 'PAUSED', 'DELETED', 'ARCHIVED', 'CAMPAIGN_PAUSED', 'IN_PROCESS', 'WITH_ISSUES'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Filter by effective status. Includes the configured statuses (ACTIVE / PAUSED / ARCHIVED / DELETED) plus Meta-derived ones (CAMPAIGN_PAUSED, WITH_ISSUES, IN_PROCESS). Omit for all.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'campaign_id': {'anyOf': [{'type': 'string', 'minLength': 1}, {'type': 'null'}], 'default': None, 'description': 'Filter to ad sets in this campaign. Omit to return all ad sets in the account.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'type': 'object', 'required': ['total', 'count', 'offset', 'has_more', 'account_id', 'currency', 'adsets'], 'properties': {'count': {'type': 'integer', 'description': 'Number of rows in this response.'}, 'total': {'type': 'integer', 'description': 'Total matching rows across all pages.'}, 'adsets': {'type': 'array', 'items': {'type': 'object', 'required': ['id'], 'properties': {'id': {'type': 'string'}, 'name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'status': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'end_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'targeting': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Raw Meta targeting spec.'}, 'bid_amount': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'Bid amount in account currency.'}, 'start_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'campaign_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'bid_strategy': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'created_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'daily_budget': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'Daily budget in account currency.'}, 'updated_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'billing_event': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'daily_spend_cap': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'CBO only: daily spend ceiling for this ad set, in account currency. Null when unset OR pre-sync.'}, 'lifetime_budget': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'Lifetime budget in account currency.'}, 'promoted_object': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Conversion tracking config (pixel + event).'}, 'budget_remaining': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'Remaining budget in account currency.'}, 'destination_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Where the conversion happens. ON_AD = instant form on the ad (lead ads); WEBSITE = offsite landing page; MESSENGER / INSTAGRAM_DIRECT / WHATSAPP = messaging thread; PHONE_CALL = call ad. Null on ad sets last synced before this field was ingested.'}, 'optimization_goal': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'lifetime_spend_cap': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'CBO only: lifetime spend ceiling for this ad set, in account currency. Null when unset OR pre-sync.'}, 'daily_min_spend_target': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'CBO only: minimum daily spend Meta tries to allocate to this ad set, in account currency. Null when CBO floor is unset OR the row predates the field landing in the sync.'}, 'lifetime_min_spend_target': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'CBO only: minimum lifetime spend Meta tries to allocate to this ad set, in account currency. Null when unset OR pre-sync.'}}, 'description': 'One ad set row from list_adsets.', 'additionalProperties': True}}, 'offset': {'type': 'integer', 'description': 'Pagination offset of this page.'}, 'message': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Set when the result page is empty but rows exist — explains what the filters excluded. Relay it rather than concluding the entity is missing.'}, 'currency': {'type': 'string'}, 'has_more': {'type': 'boolean', 'description': 'True when there are more rows past this page.'}, 'account_id': {'type': 'string'}, 'provenance': {'anyOf': [{'type': 'object', 'required': ['source'], 'properties': {'as_of': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO-8601 timestamp the data is current as of.'}, 'source': {'enum': ['mirror', 'live', 'live_only'], 'type': 'string', 'description': "'mirror' = the synced copy (refreshed every ~30 min); 'live' = read from Meta just now because the account's first sync has not completed; 'live_only' = data Meta alone holds."}, 'stale_reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Set when the synced copy can no longer advance: 'token_invalid' (reconnect Meta) or 'disconnected'. Null when healthy."}}, 'description': 'Where an account-data answer came from and as of when.\n\nEvery read served by the Meta Ads Data Service carries this. The row shape\nnever depends on the source — only this block does. A read the service\ncannot answer for an account whose first sync has not completed (history\nbeyond the live window, too many entities) is answered with\n``{"status": "not_yet_landed", "message", "sync_state", ...}`` instead of\nthe tool\'s normal payload; ``{"status": "live_read_unavailable",\n"retry_after"}`` means Meta could not be read right now — retry later.\nThose two payloads are ``NotYetLandedResponse`` and\n``LiveReadUnavailableResponse`` below, and a tool that can answer with\nthem declares it through ``Refusable[...]``.', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Source and freshness of this answer.'}, 'next_offset': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': "Pass to the next call's offset parameter (null when has_more is false)."}, 'total_unfiltered': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': "How many ad sets exist in scope before discretionary filtering. Zero means the scope genuinely has none; non-zero with an empty result means the filters excluded them all. A just-created (always PAUSED) ad set never matches status='ACTIVE'."}}, 'description': 'Response from list_adsets.', 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id'], 'properties': {'status': {'type': 'string', 'const': 'not_yet_landed'}, 'message': {'type': 'string', 'description': 'One sentence to relay: what is available until the sync completes and why this request needs more.'}, 'account_id': {'type': 'string'}, 'sync_state': {'anyOf': [{'type': 'object', 'required': ['landed'], 'properties': {'basis': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "What the verdict rests on: 'run' (a sync record), 'legacy' (the data_synced flag) or 'none'."}, 'phase': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'initial | catchup | landed | failed; null without a sync record.'}, 'landed': {'type': 'boolean', 'description': 'True once every stream of the first sync has completed.'}, 'streams': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Per-stream status: pending | running | completed | failed.'}, 'task_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'failed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'landed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'streams_done': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams completed so far.'}, 'streams_total': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams the first sync covers.'}, 'insights_chunks': {'anyOf': [{'type': 'object', 'additionalProperties': {'anyOf': [{'type': 'integer'}, {'type': 'null'}]}}, {'type': 'null'}], 'default': None, 'description': '{done, total} chunks of the insights backfill.'}, 'catchup_started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': "Progress of an account's first sync — ``SyncState.as_dict()``\n(``app.services.meta_ads_data.landed``) field for field.", 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'How far the first sync is.'}, 'live_window_days': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Days of insights history a Live Read answers (the live window).'}}, 'description': "What a façade-backed tool answers instead of its normal payload when\nthe account's first sync has not completed and the question is outside\nwhat a Live Read may answer — ``NotYetLanded.to_mcp()`` field for field.\nA normal result, never ``isError``: relay ``message`` and, when asked how\nfar along the sync is, ``sync_state``.", 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id', 'retry_after'], 'properties': {'status': {'type': 'string', 'const': 'live_read_unavailable'}, 'message': {'type': 'string'}, 'account_id': {'type': 'string'}, 'retry_after': {'type': 'integer', 'description': 'Seconds to wait before retrying.'}}, 'description': 'Meta could not be read for the account right now (throttled, time\nbudget exceeded) — ``LiveReadUnavailable.to_mcp()`` field for field.\nA normal result, never ``isError``; retry after ``retry_after`` seconds.', 'additionalProperties': True}]}
list_campaigns
List Campaigns
List campaigns — show what campaigns exist, their config and budgets. Use this to see what campaigns are running, review setup before making changes, or answer "what campaigns do I have?" questions. Returns: id, name, status, objective, daily_budget, lifetime_budget, budget_remaining, spend_cap, bid_strategy, buying_type, pacing_type, schedule. Pagination: response includes total / count / offset / has_more / next_offset. When has_more is true, call again with offset=next_offset to fetch the next page. Does NOT include performance metrics — use get_performance for that.
Read only
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'limit': {'anyOf': [{'type': 'integer', 'maximum': 200, 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Max results to return per page (default 50, max 200).'}, 'offset': {'type': 'integer', 'default': 0, 'minimum': 0, 'description': 'Number of results to skip for pagination. Pair with limit to page through results — use the returned next_offset on each call.'}, 'search': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Filter by name (case-insensitive substring match). Omit to return all.'}, 'status': {'anyOf': [{'enum': ['ACTIVE', 'PAUSED', 'DELETED', 'ARCHIVED', 'IN_PROCESS', 'WITH_ISSUES'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Filter by effective status. Includes the configured statuses (ACTIVE / PAUSED / ARCHIVED / DELETED) plus Meta-derived ones (WITH_ISSUES, IN_PROCESS). Omit for all.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'type': 'object', 'required': ['total', 'count', 'offset', 'has_more', 'account_id', 'currency', 'campaigns'], 'properties': {'count': {'type': 'integer', 'description': 'Number of rows in this response.'}, 'total': {'type': 'integer', 'description': 'Total matching rows across all pages.'}, 'offset': {'type': 'integer', 'description': 'Pagination offset of this page.'}, 'message': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Set when the result page is empty but campaigns exist — explains what the filters excluded. Relay it rather than concluding a campaign is missing.'}, 'currency': {'type': 'string', 'description': 'Account currency (ISO-4217).'}, 'has_more': {'type': 'boolean', 'description': 'True when there are more rows past this page.'}, 'campaigns': {'type': 'array', 'items': {'type': 'object', 'required': ['id'], 'properties': {'id': {'type': 'string'}, 'name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'status': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Effective status (or fallback to configured status).'}, 'objective': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'spend_cap': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'Lifetime spend cap in account currency.'}, 'stop_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'start_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'buying_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'pacing_type': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None}, 'bid_strategy': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'created_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'daily_budget': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'Daily budget in account currency.'}, 'updated_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'lifetime_budget': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'Lifetime budget in account currency.'}, 'budget_remaining': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'Remaining budget in account currency.'}, 'special_ad_category': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'NONE, HOUSING, EMPLOYMENT, CREDIT, ISSUES_ELECTIONS_POLITICS, ONLINE_GAMBLING_AND_GAMING or FINANCIAL_PRODUCTS_SERVICES. Anything other than NONE/null means Meta restricts targeting on every ad set in this campaign — age, gender, ZIP and most detailed-targeting options are unavailable and will be rejected.'}, 'special_ad_category_country': {'anyOf': [{}, {'type': 'null'}], 'default': None, 'description': 'Countries the special ad category applies to.'}}, 'description': 'One campaign row from list_campaigns.', 'additionalProperties': True}}, 'account_id': {'type': 'string'}, 'provenance': {'anyOf': [{'type': 'object', 'required': ['source'], 'properties': {'as_of': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO-8601 timestamp the data is current as of.'}, 'source': {'enum': ['mirror', 'live', 'live_only'], 'type': 'string', 'description': "'mirror' = the synced copy (refreshed every ~30 min); 'live' = read from Meta just now because the account's first sync has not completed; 'live_only' = data Meta alone holds."}, 'stale_reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Set when the synced copy can no longer advance: 'token_invalid' (reconnect Meta) or 'disconnected'. Null when healthy."}}, 'description': 'Where an account-data answer came from and as of when.\n\nEvery read served by the Meta Ads Data Service carries this. The row shape\nnever depends on the source — only this block does. A read the service\ncannot answer for an account whose first sync has not completed (history\nbeyond the live window, too many entities) is answered with\n``{"status": "not_yet_landed", "message", "sync_state", ...}`` instead of\nthe tool\'s normal payload; ``{"status": "live_read_unavailable",\n"retry_after"}`` means Meta could not be read right now — retry later.\nThose two payloads are ``NotYetLandedResponse`` and\n``LiveReadUnavailableResponse`` below, and a tool that can answer with\nthem declares it through ``Refusable[...]``.', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Source and freshness of this answer.'}, 'next_offset': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': "Pass to the next call's offset parameter (null when has_more is false)."}, 'total_unfiltered': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': "How many campaigns the account has before `search`/`status` filtering. A zero here means the account genuinely has no campaigns; a non-zero with an empty result means the filters excluded them all — e.g. a just-created (always PAUSED) campaign will never match status='ACTIVE'."}}, 'description': 'Response from list_campaigns.', 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id'], 'properties': {'status': {'type': 'string', 'const': 'not_yet_landed'}, 'message': {'type': 'string', 'description': 'One sentence to relay: what is available until the sync completes and why this request needs more.'}, 'account_id': {'type': 'string'}, 'sync_state': {'anyOf': [{'type': 'object', 'required': ['landed'], 'properties': {'basis': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "What the verdict rests on: 'run' (a sync record), 'legacy' (the data_synced flag) or 'none'."}, 'phase': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'initial | catchup | landed | failed; null without a sync record.'}, 'landed': {'type': 'boolean', 'description': 'True once every stream of the first sync has completed.'}, 'streams': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Per-stream status: pending | running | completed | failed.'}, 'task_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'failed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'landed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'streams_done': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams completed so far.'}, 'streams_total': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams the first sync covers.'}, 'insights_chunks': {'anyOf': [{'type': 'object', 'additionalProperties': {'anyOf': [{'type': 'integer'}, {'type': 'null'}]}}, {'type': 'null'}], 'default': None, 'description': '{done, total} chunks of the insights backfill.'}, 'catchup_started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': "Progress of an account's first sync — ``SyncState.as_dict()``\n(``app.services.meta_ads_data.landed``) field for field.", 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'How far the first sync is.'}, 'live_window_days': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Days of insights history a Live Read answers (the live window).'}}, 'description': "What a façade-backed tool answers instead of its normal payload when\nthe account's first sync has not completed and the question is outside\nwhat a Live Read may answer — ``NotYetLanded.to_mcp()`` field for field.\nA normal result, never ``isError``: relay ``message`` and, when asked how\nfar along the sync is, ``sync_state``.", 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id', 'retry_after'], 'properties': {'status': {'type': 'string', 'const': 'live_read_unavailable'}, 'message': {'type': 'string'}, 'account_id': {'type': 'string'}, 'retry_after': {'type': 'integer', 'description': 'Seconds to wait before retrying.'}}, 'description': 'Meta could not be read for the account right now (throttled, time\nbudget exceeded) — ``LiveReadUnavailable.to_mcp()`` field for field.\nA normal result, never ``isError``; retry after ``retry_after`` seconds.', 'additionalProperties': True}]}
list_creatives
List Ad Creatives
List existing ad creatives for an account — use to find a creative_id for reuse with create_ad. Queries the tenant DB directly — fast indexed query, no metrics aggregation. Returns id, name, status, object_type, body (truncated unless creative_ids is set), headline, call_to_action_type, link_url, url_tags, image_hash, video_id, actor_id (page_id), instagram_user_id, effective_object_story_id, and lead_gen_form_id. lead_gen_form_id is the Meta Lead Form this creative collects leads into (null for non-lead ads). It answers "which form does this ad use?" — pass it to get_lead_form to see the actual questions. Carousels can point different cards at different forms, so lead_gen_form_ids holds all of them and lead_gen_form_id is the first. Pagination: response includes total / count / offset / has_more / next_offset. When has_more is true, call again with offset=next_offset to fetch the next page. Typical workflow (reuse existing creative): 1. list_creatives(account_id) — find existing creative_id 2. create_ad(account_id, ads=[{adset_id, creative_id, name}]) — attach to ad set Resolving what an ad runs: 1. list_ads(account_id) — read creative.id off the ad 2. list_creatives(account_id, creative_ids=['<that id>'])
Read only
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'limit': {'anyOf': [{'type': 'integer', 'maximum': 100, 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Max results to return per page (default 25, max 100).'}, 'offset': {'type': 'integer', 'default': 0, 'minimum': 0, 'description': 'Number of results to skip for pagination. Use next_offset from a prior response to fetch the next page.'}, 'search': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Filter by name (case-insensitive substring match). Omit to browse most recent.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'object_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Filter by object_type: IMAGE, VIDEO, SHARE, etc. Omit for all types.'}, 'creative_ids': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Look up specific creatives by id — e.g. the creative.id returned by list_ads, so you can resolve what an ad is actually running. When set, the full (untruncated) body and the raw object_story_spec / call_to_action are returned as well.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'type': 'object', 'required': ['total', 'count', 'offset', 'has_more', 'account_id', 'creatives'], 'properties': {'count': {'type': 'integer', 'description': 'Number of rows in this response.'}, 'total': {'type': 'integer', 'description': 'Total matching rows across all pages.'}, 'offset': {'type': 'integer', 'description': 'Pagination offset of this page.'}, 'message': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Set when the result page is empty but rows exist — explains what the filters excluded. Relay it rather than concluding the entity is missing.'}, 'has_more': {'type': 'boolean', 'description': 'True when there are more rows past this page.'}, 'creatives': {'type': 'array', 'items': {'type': 'object', 'required': ['id'], 'properties': {'id': {'type': 'string'}, 'body': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Primary ad text. Truncated to 100 chars when browsing; full text when the creative was requested by id via creative_ids.'}, 'name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'title': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Headline.'}, 'status': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'actor_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Page ID.'}, 'link_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'url_tags': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'URL parameters appended to the destination link.'}, 'video_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'image_hash': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'object_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'IMAGE, VIDEO, SHARE, etc.'}, 'call_to_action': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Raw top-level CTA — returned only when creative_ids was supplied.'}, 'ingestion_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'lead_gen_form_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "The Meta Lead Form this creative collects leads into, or null for non-lead ads. Pass it to get_lead_form to see the form's questions."}, 'instagram_user_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Instagram identity used by this creative, if any.'}, 'lead_gen_form_ids': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Every lead form referenced by this creative. Normally one; a carousel can point different cards at different forms.'}, 'object_story_spec': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Raw Meta creative spec — returned only when creative_ids was supplied.'}, 'call_to_action_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'effective_object_story_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'The underlying Page post ({page_id}_{post_id}), for post-backed creatives.'}}, 'description': 'One creative row from list_creatives.', 'additionalProperties': True}}, 'account_id': {'type': 'string'}, 'provenance': {'anyOf': [{'type': 'object', 'required': ['source'], 'properties': {'as_of': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO-8601 timestamp the data is current as of.'}, 'source': {'enum': ['mirror', 'live', 'live_only'], 'type': 'string', 'description': "'mirror' = the synced copy (refreshed every ~30 min); 'live' = read from Meta just now because the account's first sync has not completed; 'live_only' = data Meta alone holds."}, 'stale_reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Set when the synced copy can no longer advance: 'token_invalid' (reconnect Meta) or 'disconnected'. Null when healthy."}}, 'description': 'Where an account-data answer came from and as of when.\n\nEvery read served by the Meta Ads Data Service carries this. The row shape\nnever depends on the source — only this block does. A read the service\ncannot answer for an account whose first sync has not completed (history\nbeyond the live window, too many entities) is answered with\n``{"status": "not_yet_landed", "message", "sync_state", ...}`` instead of\nthe tool\'s normal payload; ``{"status": "live_read_unavailable",\n"retry_after"}`` means Meta could not be read right now — retry later.\nThose two payloads are ``NotYetLandedResponse`` and\n``LiveReadUnavailableResponse`` below, and a tool that can answer with\nthem declares it through ``Refusable[...]``.', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Source and freshness of this answer.'}, 'next_offset': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': "Pass to the next call's offset parameter (null when has_more is false)."}, 'total_unfiltered': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'How many creatives exist in scope before discretionary filtering. Zero means the scope genuinely has none; non-zero with an empty result means the filters excluded them all. Creatives sync every ~30 min — very recent ones may lag.'}}, 'description': 'Response from list_creatives.', 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id'], 'properties': {'status': {'type': 'string', 'const': 'not_yet_landed'}, 'message': {'type': 'string', 'description': 'One sentence to relay: what is available until the sync completes and why this request needs more.'}, 'account_id': {'type': 'string'}, 'sync_state': {'anyOf': [{'type': 'object', 'required': ['landed'], 'properties': {'basis': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "What the verdict rests on: 'run' (a sync record), 'legacy' (the data_synced flag) or 'none'."}, 'phase': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'initial | catchup | landed | failed; null without a sync record.'}, 'landed': {'type': 'boolean', 'description': 'True once every stream of the first sync has completed.'}, 'streams': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Per-stream status: pending | running | completed | failed.'}, 'task_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'failed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'landed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'streams_done': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams completed so far.'}, 'streams_total': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams the first sync covers.'}, 'insights_chunks': {'anyOf': [{'type': 'object', 'additionalProperties': {'anyOf': [{'type': 'integer'}, {'type': 'null'}]}}, {'type': 'null'}], 'default': None, 'description': '{done, total} chunks of the insights backfill.'}, 'catchup_started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': "Progress of an account's first sync — ``SyncState.as_dict()``\n(``app.services.meta_ads_data.landed``) field for field.", 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'How far the first sync is.'}, 'live_window_days': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Days of insights history a Live Read answers (the live window).'}}, 'description': "What a façade-backed tool answers instead of its normal payload when\nthe account's first sync has not completed and the question is outside\nwhat a Live Read may answer — ``NotYetLanded.to_mcp()`` field for field.\nA normal result, never ``isError``: relay ``message`` and, when asked how\nfar along the sync is, ``sync_state``.", 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id', 'retry_after'], 'properties': {'status': {'type': 'string', 'const': 'live_read_unavailable'}, 'message': {'type': 'string'}, 'account_id': {'type': 'string'}, 'retry_after': {'type': 'integer', 'description': 'Seconds to wait before retrying.'}}, 'description': 'Meta could not be read for the account right now (throttled, time\nbudget exceeded) — ``LiveReadUnavailable.to_mcp()`` field for field.\nA normal result, never ``isError``; retry after ``retry_after`` seconds.', 'additionalProperties': True}]}
list_custom_audiences
List Custom Audiences
List custom audiences available for targeting in this ad account. Returns each audience's ID, name, subtype, approximate size, and delivery status. Use the id and name values in create_adset or update_adset_targeting to target or exclude these audiences: 1. list_custom_audiences(account_id, search='lookalike') — find audiences 2. create_adset(..., custom_audiences=[{id: '...', name: '...'}]) 3. Or: update_adset_targeting(..., custom_audiences=[{id: '...', name: '...'}]) Audience types include: - CUSTOM: customer file uploads - LOOKALIKE: lookalike audiences based on seed audiences - WEBSITE: website visitor audiences (pixel-based) - ENGAGEMENT: people who engaged with your content - IG_BUSINESS: Instagram business profile audiences
Read only
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'limit': {'anyOf': [{'type': 'integer', 'maximum': 100, 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Max results to return per page (default 25, max 100).'}, 'offset': {'type': 'integer', 'default': 0, 'minimum': 0, 'description': 'Number of results to skip for pagination. Use next_offset from a prior response to fetch the next page.'}, 'search': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Filter by name (case-insensitive substring match). Omit to return all.'}, 'subtype': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Filter by audience subtype: CUSTOM, LOOKALIKE, WEBSITE, ENGAGEMENT, etc. Omit for all types.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'type': 'object', 'required': ['total', 'count', 'offset', 'has_more', 'account_id', 'audiences'], 'properties': {'count': {'type': 'integer', 'description': 'Number of rows in this response.'}, 'total': {'type': 'integer', 'description': 'Total matching rows across all pages.'}, 'offset': {'type': 'integer', 'description': 'Pagination offset of this page.'}, 'message': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Set when the result page is empty but rows exist — explains what the filters excluded. Relay it rather than concluding the entity is missing.'}, 'has_more': {'type': 'boolean', 'description': 'True when there are more rows past this page.'}, 'audiences': {'type': 'array', 'items': {'type': 'object', 'required': ['id'], 'properties': {'id': {'type': 'string'}, 'name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'subtype': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'CUSTOM, LOOKALIKE, WEBSITE, ENGAGEMENT, IG_BUSINESS, etc.'}, 'description': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'time_updated': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Unix timestamp of last update.'}, 'retention_days': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None}, 'delivery_status': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': '{code, description} — 200 means ready.'}, 'approximate_count_lower_bound': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None}, 'approximate_count_upper_bound': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None}}, 'description': 'One custom audience row from list_custom_audiences.', 'additionalProperties': True}}, 'account_id': {'type': 'string'}, 'provenance': {'anyOf': [{'type': 'object', 'required': ['source'], 'properties': {'as_of': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO-8601 timestamp the data is current as of.'}, 'source': {'enum': ['mirror', 'live', 'live_only'], 'type': 'string', 'description': "'mirror' = the synced copy (refreshed every ~30 min); 'live' = read from Meta just now because the account's first sync has not completed; 'live_only' = data Meta alone holds."}, 'stale_reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Set when the synced copy can no longer advance: 'token_invalid' (reconnect Meta) or 'disconnected'. Null when healthy."}}, 'description': 'Where an account-data answer came from and as of when.\n\nEvery read served by the Meta Ads Data Service carries this. The row shape\nnever depends on the source — only this block does. A read the service\ncannot answer for an account whose first sync has not completed (history\nbeyond the live window, too many entities) is answered with\n``{"status": "not_yet_landed", "message", "sync_state", ...}`` instead of\nthe tool\'s normal payload; ``{"status": "live_read_unavailable",\n"retry_after"}`` means Meta could not be read right now — retry later.\nThose two payloads are ``NotYetLandedResponse`` and\n``LiveReadUnavailableResponse`` below, and a tool that can answer with\nthem declares it through ``Refusable[...]``.', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Source and freshness of this answer.'}, 'next_offset': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': "Pass to the next call's offset parameter (null when has_more is false)."}, 'total_unfiltered': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'How many custom audiences exist in scope before discretionary filtering. Zero means the scope genuinely has none; non-zero with an empty result means the filters excluded them all. Audiences sync every ~30 min — use the id from the create response directly.'}}, 'description': 'Response from list_custom_audiences.', 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id'], 'properties': {'status': {'type': 'string', 'const': 'not_yet_landed'}, 'message': {'type': 'string', 'description': 'One sentence to relay: what is available until the sync completes and why this request needs more.'}, 'account_id': {'type': 'string'}, 'sync_state': {'anyOf': [{'type': 'object', 'required': ['landed'], 'properties': {'basis': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "What the verdict rests on: 'run' (a sync record), 'legacy' (the data_synced flag) or 'none'."}, 'phase': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'initial | catchup | landed | failed; null without a sync record.'}, 'landed': {'type': 'boolean', 'description': 'True once every stream of the first sync has completed.'}, 'streams': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Per-stream status: pending | running | completed | failed.'}, 'task_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'failed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'landed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'streams_done': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams completed so far.'}, 'streams_total': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams the first sync covers.'}, 'insights_chunks': {'anyOf': [{'type': 'object', 'additionalProperties': {'anyOf': [{'type': 'integer'}, {'type': 'null'}]}}, {'type': 'null'}], 'default': None, 'description': '{done, total} chunks of the insights backfill.'}, 'catchup_started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': "Progress of an account's first sync — ``SyncState.as_dict()``\n(``app.services.meta_ads_data.landed``) field for field.", 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'How far the first sync is.'}, 'live_window_days': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Days of insights history a Live Read answers (the live window).'}}, 'description': "What a façade-backed tool answers instead of its normal payload when\nthe account's first sync has not completed and the question is outside\nwhat a Live Read may answer — ``NotYetLanded.to_mcp()`` field for field.\nA normal result, never ``isError``: relay ``message`` and, when asked how\nfar along the sync is, ``sync_state``.", 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id', 'retry_after'], 'properties': {'status': {'type': 'string', 'const': 'live_read_unavailable'}, 'message': {'type': 'string'}, 'account_id': {'type': 'string'}, 'retry_after': {'type': 'integer', 'description': 'Seconds to wait before retrying.'}}, 'description': 'Meta could not be read for the account right now (throttled, time\nbudget exceeded) — ``LiveReadUnavailable.to_mcp()`` field for field.\nA normal result, never ``isError``; retry after ``retry_after`` seconds.', 'additionalProperties': True}]}
list_lead_forms
List Lead Forms
List existing Meta Lead Forms (instant forms) for a Facebook Page. Lead forms are page-scoped (endpoint: /{page_id}/leadgen_forms), so the tool resolves a Page first. If page_id is omitted, the account's default promote_page is used — call list_pages first when the account has multiple Pages and you need a different one. Returns id, name, status, locale, leads_count, follow_up_action_url, question count, and privacy_policy_url for each form. Use the form id as lead_gen_form_id in create_creative to attach to a new ad. Calls Meta directly — lead forms are not ingested into the tenant DB. Common workflow (reuse existing form): 1. list_lead_forms(account_id, status='ACTIVE') — find an active form 2. get_lead_form(account_id, lead_form_id) — check what it asks 3. create_creative(creatives=[{format:'image_link', image_hash:'...', lead_gen_form_id:'<form_id>'}]) 4. create_adset(..., optimization_goal='LEAD_GENERATION', destination_type='ON_AD', promoted_object={'page_id': '<this Page>'}) promoted_object is REQUIRED on an OUTCOME_LEADS ad set. Requires the token to have ads_management + pages_show_list. Read access to submitted leads additionally requires MANAGE_LEADS on the page (use list_pages to verify task grants), and is what list_leads needs.
Read only Open world
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'limit': {'anyOf': [{'type': 'integer', 'maximum': 200, 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Max results to return per page (default 50, max 200).'}, 'offset': {'type': 'integer', 'default': 0, 'minimum': 0, 'description': 'Number of results to skip for pagination. Use next_offset from a prior response to fetch the next page.'}, 'search': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Filter by form name (case-insensitive substring match). Omit to return all.'}, 'status': {'anyOf': [{'enum': ['ACTIVE', 'ARCHIVED', 'DELETED', 'DRAFT'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Filter by form status. Omit for all statuses. Only ACTIVE forms can be attached to new ads — ARCHIVED forms will be rejected at ad creation.'}, 'page_id': {'anyOf': [{'type': 'string', 'minLength': 1}, {'type': 'null'}], 'default': None, 'description': "Facebook Page ID to query. Omit to use the account's default promote_page (the same page create_lead_form picks). Call list_pages if the account has more than one page and you need to pin a specific one."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['total', 'count', 'offset', 'has_more', 'account_id', 'page_id', 'lead_forms'], 'properties': {'count': {'type': 'integer', 'description': 'Number of rows in this response.'}, 'total': {'type': 'integer', 'description': 'Total matching rows across all pages.'}, 'offset': {'type': 'integer', 'description': 'Pagination offset of this page.'}, 'page_id': {'type': 'string', 'description': 'Facebook Page the forms were queried from.'}, 'has_more': {'type': 'boolean', 'description': 'True when there are more rows past this page.'}, 'page_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Display name of the queried Page.'}, 'account_id': {'type': 'string'}, 'lead_forms': {'type': 'array', 'items': {'type': 'object', 'required': ['id'], 'properties': {'id': {'type': 'string', 'description': 'Lead form ID (use as lead_gen_form_id in create_creative).'}, 'name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Form display name.'}, 'locale': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Form locale, e.g. 'en_US'."}, 'status': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ACTIVE, ARCHIVED, DELETED, or DRAFT. Only ACTIVE forms can be used in new ads.'}, 'leads_count': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Total leads collected to date.'}, 'created_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'question_types': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "Question types in display order, e.g. ['FULL_NAME','EMAIL','CUSTOM']. Call get_lead_form for the full labels, options and disclaimers."}, 'questions_count': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Number of questions in the form.'}, 'privacy_policy_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'expired_leads_count': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': "Leads that have aged past Meta's 90-day retrieval window."}, 'organic_leads_count': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Leads collected from non-ad (organic) traffic.'}, 'follow_up_action_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'is_optimized_for_quality': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None}, 'block_display_for_non_targeted_viewer': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': 'True when the form blocks organic / non-targeted submissions.'}}, 'description': 'One lead form row from list_lead_forms.', 'additionalProperties': True}}, 'next_offset': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': "Pass to the next call's offset parameter (null when has_more is false)."}, 'total_unfiltered': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'How many forms the Page has before `search`/`status` filtering. A zero here means the Page genuinely has no forms; a non-zero with an empty result means the filters excluded them all.'}}, 'description': 'Response from list_lead_forms.', 'additionalProperties': True}
list_media_assets
List Media Assets
Browse this business's Creatives Hub — every media asset the platform holds. The Hub is the shared library across surfaces and agents: renders generated by Iris, files uploaded by the user, and assets pulled in by import connectors all land here. An asset lives here whether or not it has ever been used in an ad. Use this when: - You need something to launch and want to see what already exists. - A user names a creative you have no artifact for. Renders made in a direct Iris conversation are never copied into your own store, but they ARE in the Hub — search for the filename here before telling anyone you can't find it. Each asset carries an ``asset_id``. That is the handle you pass to ``register_media_assets`` to get a Meta ``image_hash``/``video_id``. An asset already registered to THIS account returns its hash inline as ``registered_image_hash`` / ``registered_video_id`` — use that directly and skip registration. Typical flow: 1. list_media_assets(account_id, search='hero') — find the asset_id 2. register_media_assets(account_id, asset_ids=['<id>']) — get the image_hash 3. create_creative(account_id, creatives=[{format: 'image_link', image_hash: '<hash>', ...}]) 4. create_ad(account_id, ads=[{adset_id, creative_id, name}])
Read only Idempotent
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'tags': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Return assets carrying ANY of these tags.'}, 'limit': {'type': 'integer', 'default': 25, 'maximum': 100, 'minimum': 1, 'description': 'Max assets per page (default 25).'}, 'offset': {'type': 'integer', 'default': 0, 'minimum': 0, 'description': 'Assets to skip; use next_offset to page.'}, 'search': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Case-insensitive substring match on the asset's display name. Use this when a user names a file you don't have — e.g. 'creative_e9267dd2_v4.png'."}, 'source': {'anyOf': [{'enum': ['ai_generated', 'user_upload', 'shopify_import', 'woocommerce_import', 'wix_import', 'magento_import', 'squarespace_import'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Restrict by origin. 'ai_generated' is what Iris (and any other generating agent) files here; 'user_upload' is what the customer uploaded; the *_import values come from store connectors."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Meta ad account ID'}, 'media_type': {'anyOf': [{'enum': ['image', 'video'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Restrict to images or videos. Omit for both.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['count', 'total', 'account_id', 'assets'], 'properties': {'count': {'type': 'integer'}, 'total': {'type': 'integer', 'description': 'Total matching assets across all pages.'}, 'assets': {'type': 'array', 'items': {'type': 'object', 'required': ['asset_id', 'display_name', 'media_type', 'content_type', 'source'], 'properties': {'tags': {'type': 'array', 'items': {'type': 'string'}}, 'width': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None}, 'height': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None}, 'source': {'type': 'string', 'description': 'Where the asset came from — e.g. an agent render, a user upload, an import connector.'}, 'asset_id': {'type': 'string', 'description': 'Pass to register_media_assets as an asset_id.'}, 'filename': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'created_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'media_type': {'enum': ['image', 'video'], 'type': 'string'}, 'content_type': {'type': 'string'}, 'display_name': {'type': 'string'}, 'registered_video_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'As registered_image_hash, for videos.'}, 'registered_image_hash': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Meta image_hash IF this asset is already registered to the queried ad account — pass it straight to create_creative and skip registration. Null means it has never been pushed to this account.'}}, 'description': 'One Creatives Hub asset as list_media_assets returns it.\n\n``asset_id`` is the Media-Library id — the handle every agent uses to move an asset\naround. It is NOT a Meta identifier: an asset only gains an ``image_hash``/``video_id``\nonce ``register_media_assets`` pushes it to a specific ad account, which is why\n``registered_image_hash`` is null for anything freshly generated.', 'additionalProperties': True}}, 'status': {'type': 'string', 'const': 'success', 'default': 'success'}, 'message': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Set when the result page is empty but rows exist — explains what the filters excluded. Relay it rather than concluding the entity is missing.'}, 'account_id': {'type': 'string'}, 'next_offset': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Pass as offset for the next page; null when exhausted.'}, 'total_unfiltered': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'How many media assets exist in scope before discretionary filtering. Zero means the scope genuinely has none; non-zero with an empty result means the filters excluded them all.'}}, 'description': "Result of list_media_assets — the business's Creatives Hub library.", 'additionalProperties': True}
list_pages
List Pages
List Facebook Pages reachable for advertising from this ad account. Returns each page's id, name, the connecting user's Page-role tasks (e.g. ADVERTISE, MANAGE, ANALYZE), and can_advertise. is_default marks the page create_lead_form uses when page_id is omitted. Pages are the union of the ad account's promote_pages edge and the user's /me/accounts, deduped (the same source the app's page picker uses). Prefer can_advertise=true pages. tasks are joined from /me/accounts because the promote_pages edge does not return tasks. Use this to: - Pin a specific page_id on create_lead_form when the account has more than one page. - Check whether the connecting user has the ADVERTISE task on the page. Note: an empty tasks list is advisory, not a hard block — a system-user or granular-scope grant (pages_manage_ads / leads_retrieval) can still let create_ad / create_lead_form succeed even when tasks is empty. Prefer attempting the real mutation over refusing on an empty tasks list. Calls Meta directly — pages are not ingested into the tenant DB.
Read only Open world
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['account_id', 'count', 'pages'], 'properties': {'count': {'type': 'integer'}, 'error': {'anyOf': [{'type': 'object', 'properties': {'code': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Meta error code.'}, 'reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'permission_denied | page_token_denied | api_error — why the read returned no data.'}, 'message': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Meta's error message."}, 'subcode': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Meta error subcode.'}, 'user_action': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'reconnect_meta | grant_page_access | null — what the human must do to unblock it. Tell the user this; do not retry the tool.'}}, 'description': 'Machine-readable reason a Meta read returned nothing.\n\nPresent on degraded read responses so an empty list is never mistaken for\n"this account genuinely has none". `reason` and `user_action` are the two\nfields the model should branch on; `code`/`subcode`/`message` are Meta\'s\nown values, forwarded verbatim for support triage.', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': "Present when Meta refused one or more of the Page lookups. An empty `pages` list with this set means 'we were not allowed to look', NOT 'this account has no Pages'."}, 'pages': {'type': 'array', 'items': {'type': 'object', 'required': ['page_id', 'is_default'], 'properties': {'name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Page display name.'}, 'tasks': {'type': 'array', 'items': {'type': 'string'}, 'description': "The connecting user's Page-role tasks (from /me/accounts), e.g. ADVERTISE, MANAGE, ANALYZE. Empty means the user holds no Page role on it — advisory only, not a hard block (a granular-scope or system-user grant can still allow ad/form creation)."}, 'page_id': {'type': 'string', 'description': 'Facebook Page ID.'}, 'is_default': {'type': 'boolean', 'description': 'True for the page used by create_lead_form when page_id is omitted.'}, 'can_advertise': {'type': 'boolean', 'default': False, 'description': "True if this Page is advertisable by this ad account — it is in the account's promote_pages OR the user holds the ADVERTISE task on it."}, 'instagram_business_account': {'anyOf': [{'type': 'object', 'required': ['ig_user_id'], 'properties': {'page_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Facebook Page this IG account resolves through (null when linked directly to the ad account).'}, 'username': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'IG handle.'}, 'ig_user_id': {'type': 'string', 'description': 'Instagram business account ID — pass as CreativeSpec.instagram_user_id when using placement asset customization or an Instagram identity.'}, 'profile_pic': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Profile picture URL.'}}, 'description': 'One Instagram business account reachable from an ad account or Page.', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Instagram business account linked to this Page, when one is linked and readable. Its ig_user_id is the value to pass as CreativeSpec.instagram_user_id.'}}, 'description': 'One Page row from list_pages.', 'additionalProperties': True}}, 'warning': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Human-readable summary of a degraded read — relay it to the user.'}, 'account_id': {'type': 'string'}, 'instagram_accounts': {'type': 'array', 'items': {'type': 'object', 'required': ['ig_user_id'], 'properties': {'page_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Facebook Page this IG account resolves through (null when linked directly to the ad account).'}, 'username': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'IG handle.'}, 'ig_user_id': {'type': 'string', 'description': 'Instagram business account ID — pass as CreativeSpec.instagram_user_id when using placement asset customization or an Instagram identity.'}, 'profile_pic': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Profile picture URL.'}}, 'description': 'One Instagram business account reachable from an ad account or Page.', 'additionalProperties': True}, 'description': "Every Instagram business account reachable from this ad account (union of per-Page links and the account's connected_instagram_accounts)."}}, 'description': 'Response from list_pages.', 'additionalProperties': True}
list_promotable_posts
List Promotable Posts
List existing organic posts that can be promoted as ads. Use this to find a Facebook Page post or Instagram post the business already published — promoting one preserves the post's existing likes, comments and shares (social proof) instead of starting a fresh ad at zero. Prefer this over creating a brand-new creative when the user says "boost", "promote this post", or references something they posted. Feed the result into create_creative(format='existing_post'): - facebook: object_story_id='<page_id>_<post_id>' - instagram: source_instagram_media_id + instagram_user_id Facebook posts with is_eligible=false (cover / profile-photo stories) cannot be promoted — skip them. Reads Meta live. Facebook needs a Page access token (pages_manage_ads / pages_read_engagement + a Page role); Instagram needs instagram_basic. An empty items list WITH an `error` set means the read was refused, NOT that there are no posts — relay `warning` to the user instead of saying the Page is empty. reactions/comments/shares are null (not 0) when the counts could not be read.
Read only Open world
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'after': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Pagination cursor — pass next_after from a prior call.'}, 'limit': {'anyOf': [{'type': 'integer', 'maximum': 100, 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Max posts to return (default 24, max 100).'}, 'source': {'enum': ['facebook', 'instagram'], 'type': 'string', 'default': 'facebook', 'description': "'facebook' for the Page's published posts, 'instagram' for the connected IG business account's media."}, 'page_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Facebook Page ID (from list_pages). Omit to use the ad account's default Page."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string, from list_ad_accounts)'}, 'instagram_user_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Instagram business account ID (from list_pages -> instagram_accounts[].ig_user_id). Omit to resolve the account's connected IG account. Instagram source only."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['account_id', 'source', 'count', 'items'], 'properties': {'count': {'type': 'integer'}, 'error': {'anyOf': [{'type': 'object', 'properties': {'code': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Meta error code.'}, 'reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'permission_denied | page_token_denied | api_error — why the read returned no data.'}, 'message': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Meta's error message."}, 'subcode': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Meta error subcode.'}, 'user_action': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'reconnect_meta | grant_page_access | null — what the human must do to unblock it. Tell the user this; do not retry the tool.'}}, 'description': 'Machine-readable reason a Meta read returned nothing.\n\nPresent on degraded read responses so an empty list is never mistaken for\n"this account genuinely has none". `reason` and `user_action` are the two\nfields the model should branch on; `code`/`subcode`/`message` are Meta\'s\nown values, forwarded verbatim for support triage.', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Machine-readable reason the read returned nothing.'}, 'items': {'type': 'array', 'items': {'type': 'object', 'required': ['source'], 'properties': {'shares': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Share count (Facebook only).'}, 'source': {'enum': ['facebook', 'instagram'], 'type': 'string', 'description': 'Which surface the post came from.'}, 'caption': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Post message / caption text.'}, 'comments': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Comment count — null when unreadable, not 0.'}, 'permalink': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Public post URL.'}, 'reactions': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Reaction/like count — null when unreadable, not 0.'}, 'media_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'IMAGE / VIDEO / CAROUSEL_ALBUM (Instagram only).'}, 'is_eligible': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': None, 'description': "Facebook only — Meta's is_eligible_for_promotion. False posts (cover/profile-photo stories) cannot be turned into ads."}, 'created_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO-8601 publish time.'}, 'thumbnail_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Displayable image URL — safe to render.'}, 'object_story_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Facebook posts only — '<page_id>_<post_id>'. Pass straight to create_creative(format='existing_post', object_story_id=...)."}, 'instagram_user_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Instagram posts only — the IG business account that owns the media.'}, 'source_instagram_media_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Instagram posts only — pair with instagram_user_id on an existing_post creative.'}}, 'description': 'One organic post that can be promoted as an ad creative.', 'additionalProperties': True}}, 'source': {'enum': ['facebook', 'instagram'], 'type': 'string'}, 'page_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Facebook Page the posts were read from.'}, 'warning': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Human-readable summary of a degraded read — relay it to the user.'}, 'has_more': {'type': 'boolean', 'default': False, 'description': 'True when more posts remain.'}, 'account_id': {'type': 'string'}, 'next_after': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Cursor for the next page (null when has_more=false).'}, 'instagram_user_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Instagram business account the media was read from.'}}, 'description': 'Response from list_page_posts / list_instagram_media.\n\nAn empty `items` with `error` set means the read was refused — it does NOT\nmean the Page or IG account has no posts.', 'additionalProperties': True}
preview_existing_creatives
Preview Existing Creatives
Preview existing creatives that use the same image or video, with engagement metrics. Call this BEFORE creating a new creative to check if the same asset is already in use. If existing creatives have engagement (reactions, comments, shares), the user can reuse one via create_ad(creative_id=...) to preserve social proof instead of creating a new creative (which starts with zero engagement). Opens a visual preview showing each creative's thumbnail, ad copy, and engagement counts. The user clicks "Use This Creative" to select one. Engagement requires a Page access token. When that can't be minted, the counts come back null with engagement_checked=false and engagement_available=false — that means UNKNOWN, not zero. Never treat it as "no social proof" or use it to justify creating a new creative. Requires either image_hash or video_id (not both).
Read only Open world
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'limit': {'anyOf': [{'type': 'integer', 'maximum': 10, 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Max creatives to return (default 3, max 10).'}, 'video_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Video ID to search for. Provide this OR image_hash.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'image_hash': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Image hash to search for. Provide this OR video_id.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'type': 'object', 'required': ['count', 'total', 'asset_type', 'account_id', 'query_asset', 'creatives'], 'properties': {'count': {'type': 'integer'}, 'error': {'anyOf': [{'type': 'object', 'properties': {'code': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Meta error code.'}, 'reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'permission_denied | page_token_denied | api_error — why the read returned no data.'}, 'message': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Meta's error message."}, 'subcode': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Meta error subcode.'}, 'user_action': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'reconnect_meta | grant_page_access | null — what the human must do to unblock it. Tell the user this; do not retry the tool.'}}, 'description': 'Machine-readable reason a Meta read returned nothing.\n\nPresent on degraded read responses so an empty list is never mistaken for\n"this account genuinely has none". `reason` and `user_action` are the two\nfields the model should branch on; `code`/`subcode`/`message` are Meta\'s\nown values, forwarded verbatim for support triage.', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Present when the Page access token needed for engagement could not be minted. Engagement counts in this response are unknown, not zero.'}, 'total': {'type': 'integer', 'description': 'Total matching creatives across all pages.'}, 'message': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Alternative to `recommendation` when no creatives have measured engagement.'}, 'creatives': {'type': 'array', 'items': {'type': 'object', 'required': ['creative_id', 'name', 'asset_type', 'thumbnail_url', 'engagement'], 'properties': {'name': {'type': 'string'}, 'message': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Body / primary text.'}, 'headline': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Title / headline text.'}, 'link_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'asset_type': {'enum': ['image', 'video'], 'type': 'string'}, 'engagement': {'type': 'object', 'description': 'Counts: {reactions, comments, shares} from the underlying organic post. Values are null (NOT 0) when engagement could not be read — check engagement_checked before treating them as zero social proof.', 'additionalProperties': True}, 'source_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Playable video source URL (videos only).'}, 'creative_id': {'type': 'string'}, 'thumbnail_url': {'type': 'string', 'description': 'High-res thumbnail URL — safe to render in UI.'}, 'engagement_checked': {'type': 'boolean', 'default': False, 'description': "True only when Meta actually returned engagement for this creative's post. False means unknown — never interpret the null counts as zero."}, 'call_to_action_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': 'Creative metadata + post engagement for preview_existing_creatives.', 'additionalProperties': True}}, 'account_id': {'type': 'string'}, 'asset_type': {'enum': ['image', 'video'], 'type': 'string'}, 'provenance': {'anyOf': [{'type': 'object', 'required': ['source'], 'properties': {'as_of': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO-8601 timestamp the data is current as of.'}, 'source': {'enum': ['mirror', 'live', 'live_only'], 'type': 'string', 'description': "'mirror' = the synced copy (refreshed every ~30 min); 'live' = read from Meta just now because the account's first sync has not completed; 'live_only' = data Meta alone holds."}, 'stale_reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Set when the synced copy can no longer advance: 'token_invalid' (reconnect Meta) or 'disconnected'. Null when healthy."}}, 'description': 'Where an account-data answer came from and as of when.\n\nEvery read served by the Meta Ads Data Service carries this. The row shape\nnever depends on the source — only this block does. A read the service\ncannot answer for an account whose first sync has not completed (history\nbeyond the live window, too many entities) is answered with\n``{"status": "not_yet_landed", "message", "sync_state", ...}`` instead of\nthe tool\'s normal payload; ``{"status": "live_read_unavailable",\n"retry_after"}`` means Meta could not be read right now — retry later.\nThose two payloads are ``NotYetLandedResponse`` and\n``LiveReadUnavailableResponse`` below, and a tool that can answer with\nthem declares it through ``Refusable[...]``.', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Source and freshness of this answer.'}, 'query_asset': {'type': 'string', 'description': 'The image_hash or video_id that was queried.'}, 'recommendation': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Reuse hint — present when at least one creative has engagement. Names the top creative + its engagement counts, plus an example create_ad call.'}, 'engagement_available': {'type': 'boolean', 'default': False, 'description': 'True when engagement was readable for at least one creative. When false, social proof is UNKNOWN — do not advise creating a fresh creative on the assumption there is none.'}}, 'description': 'Result of preview_existing_creatives — creatives sharing the same asset.\n\nCreatives with *measured* engagement are sorted DESC and lead the list;\ncreatives whose engagement could not be read (engagement_checked=false) are\nkept last in DB order rather than being ranked as zeros. When at least one\ncreative has measured engagement, a `recommendation` string suggests reusing\nthe top creative; otherwise `message` explains whether engagement was\ngenuinely zero or simply unreadable.', 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id'], 'properties': {'status': {'type': 'string', 'const': 'not_yet_landed'}, 'message': {'type': 'string', 'description': 'One sentence to relay: what is available until the sync completes and why this request needs more.'}, 'account_id': {'type': 'string'}, 'sync_state': {'anyOf': [{'type': 'object', 'required': ['landed'], 'properties': {'basis': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "What the verdict rests on: 'run' (a sync record), 'legacy' (the data_synced flag) or 'none'."}, 'phase': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'initial | catchup | landed | failed; null without a sync record.'}, 'landed': {'type': 'boolean', 'description': 'True once every stream of the first sync has completed.'}, 'streams': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Per-stream status: pending | running | completed | failed.'}, 'task_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'failed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'landed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'streams_done': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams completed so far.'}, 'streams_total': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams the first sync covers.'}, 'insights_chunks': {'anyOf': [{'type': 'object', 'additionalProperties': {'anyOf': [{'type': 'integer'}, {'type': 'null'}]}}, {'type': 'null'}], 'default': None, 'description': '{done, total} chunks of the insights backfill.'}, 'catchup_started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': "Progress of an account's first sync — ``SyncState.as_dict()``\n(``app.services.meta_ads_data.landed``) field for field.", 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'How far the first sync is.'}, 'live_window_days': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Days of insights history a Live Read answers (the live window).'}}, 'description': "What a façade-backed tool answers instead of its normal payload when\nthe account's first sync has not completed and the question is outside\nwhat a Live Read may answer — ``NotYetLanded.to_mcp()`` field for field.\nA normal result, never ``isError``: relay ``message`` and, when asked how\nfar along the sync is, ``sync_state``.", 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id', 'retry_after'], 'properties': {'status': {'type': 'string', 'const': 'live_read_unavailable'}, 'message': {'type': 'string'}, 'account_id': {'type': 'string'}, 'retry_after': {'type': 'integer', 'description': 'Seconds to wait before retrying.'}}, 'description': 'Meta could not be read for the account right now (throttled, time\nbudget exceeded) — ``LiveReadUnavailable.to_mcp()`` field for field.\nA normal result, never ``isError``; retry after ``retry_after`` seconds.', 'additionalProperties': True}]}
register_media_assets
Register Media Assets to Meta
Push Creatives Hub assets to a Meta ad account and get their image hashes back. This is the ONLY way to make a generated or uploaded asset usable in an ad. ``create_creative`` accepts an ``image_hash`` only if it came from ``search_ad_images`` or a registration — an asset that has never been registered cannot be turned into a creative no matter how good it is. Registration is inert: it copies bytes into the account's asset library. It creates no campaign, ad set, ad, or creative; it spends nothing; it changes no delivery. Re-registering the same asset to the same account is safe and returns the existing hash. Partial success is reported honestly. Read ``failed`` before building creatives — an asset that failed has NO usable hash, and substituting another asset's hash is how the wrong creative ends up on the wrong ad. Next: create_creative(account_id, creatives=[{format: 'image_link', image_hash: '<hash from results>', link: '...'}]) then create_ad(...).
Open world Idempotent
Input schema
{'type': 'object', 'required': ['account_id', 'asset_ids'], 'properties': {'asset_ids': {'type': 'array', 'items': {'type': 'string'}, 'maxItems': 25, 'minItems': 1, 'description': 'Creatives Hub asset ids, from list_media_assets. NOT Meta ids and NOT filenames — if you have a filename, look it up with list_media_assets(search=...) first.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Meta ad account ID'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['account_id', 'registered', 'failed', 'results'], 'properties': {'failed': {'type': 'integer'}, 'status': {'enum': ['success', 'partial', 'all_failed'], 'type': 'string', 'default': 'success'}, 'results': {'type': 'array', 'items': {'type': 'object', 'required': ['asset_id', 'status'], 'properties': {'error': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Present only when status is 'failed'."}, 'status': {'enum': ['registered', 'failed'], 'type': 'string'}, 'asset_id': {'type': 'string'}, 'video_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Pass to create_creative as video_id (videos).'}, 'image_hash': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Pass to create_creative as image_hash (images).'}, 'display_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': 'Per-asset outcome of register_media_assets.', 'additionalProperties': True}}, 'account_id': {'type': 'string'}, 'registered': {'type': 'integer'}}, 'description': "Result of register_media_assets — Library assets pushed to a Meta ad account.\n\nPartial success is normal and reported honestly: a bad asset never sinks the batch.\nRead ``failed`` before building creatives — an asset listed there has NO usable hash\nand must not be substituted with another asset's.", 'additionalProperties': True}
search_ad_images
Search Ad Images
Search images in this account's ad library and open a visual picker to select one or more. Queries meta_ad_images by name (case-insensitive) OR by exact hash, fetches fresh thumbnail URLs from Meta API, then opens the asset picker widget. The user can select up to 10 images at once, then clicks Confirm — the chosen image_hashes are sent back to Claude in a single ``updateModelContext`` payload (numbered list when multiple). Each query string is matched as a HASH if it's a 32-character hex string; otherwise it's matched as a case-insensitive substring against the image name. This lets you confirm a hash you got from ``complete_upload`` exists in cache: ``queries=['9f07e04cb9ec2dcefee32ae9eca9cce7']``. Pass several keywords at once with ``queries=[...]`` to search a batch of terms in a single call. Each returned image carries ``matched_queries`` so you can see which term(s) it matched. Read ``unmatched_queries`` in the response. Any term in that list returned ZERO images. Do NOT substitute an unrelated image from ``images`` for an unmatched query — that's how the wrong creative ends up on the wrong ad. If the user just uploaded images via ``init_upload``/``complete_upload`` in this conversation, the ``image_hash`` values from that response are authoritative — pass them directly to ``create_creative`` instead of re-searching by name. Multi-select workflow (recommended for multi-variant creatives): 1. search_ad_images(account_id, queries=['Royal Blue', 'Indigo', 'Navy']) — user picks several hashes in one go 2. create_creative(account_id, creatives=[{format: 'image_link', image_hash: '<h1>', ...}, {format: 'image_link', image_hash: '<h2>', ...}, ...]) ↑ Pass ALL the selected hashes in a single batched call — do NOT call create_creative once per hash. 3. create_ad(account_id, ads=[{adset_id, creative_id, name}, ...]) Single-select workflow (one creative): 1. search_ad_images(account_id, queries=['green watch']) — user picks one image 2. create_creative(account_id, creatives=[{format: 'image_link', image_hash: '<hash>', ...}]) 3. create_ad(account_id, ads=[{adset_id, creative_id, name}]) Omit queries to browse the most recent images (upload discovery mode).
Read only Open world
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'limit': {'anyOf': [{'type': 'integer', 'maximum': 100, 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Max images to return per page (default 20, max 100). When multiple queries are passed, results are deduplicated by hash before paging.'}, 'offset': {'type': 'integer', 'default': 0, 'minimum': 0, 'description': 'Number of (deduped) results to skip for pagination. Use next_offset from a prior response to fetch the next page.'}, 'queries': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}, 'maxItems': 20}, {'type': 'null'}], 'default': None, 'description': "One or more search terms — case-insensitive match on image name. Pass multiple terms to search several keywords at once (e.g. ['Royal Blue', 'Indigo', 'Navy']) instead of calling the tool separately for each. Results are deduped by image hash and each image is tagged with which queries it matched. Omit to browse the most recent images."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'type': 'object', 'required': ['total', 'count', 'offset', 'has_more', 'account_id', 'asset_type', 'queries', 'unmatched_queries', 'images'], 'properties': {'count': {'type': 'integer', 'description': 'Number of rows in this response.'}, 'total': {'type': 'integer', 'description': 'Total matching rows across all pages.'}, 'images': {'type': 'array', 'items': {'type': 'object', 'required': ['hash'], 'properties': {'hash': {'type': 'string', 'description': 'Image hash — pass to create_creative as image_hash.'}, 'name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'width': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None}, 'height': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None}, 'url_128': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': '128-pixel-wide thumbnail URL.'}, 'matched_queries': {'type': 'array', 'items': {'type': 'string'}, 'description': 'Which input queries matched this image (empty in browse mode).'}}, 'description': 'One image row from search_ad_images.', 'additionalProperties': True}}, 'offset': {'type': 'integer', 'description': 'Pagination offset of this page.'}, 'queries': {'type': 'array', 'items': {'type': 'string'}, 'description': 'Normalized input queries (deduped).'}, 'has_more': {'type': 'boolean', 'description': 'True when there are more rows past this page.'}, 'account_id': {'type': 'string'}, 'asset_type': {'type': 'string', 'description': "Always 'image'."}, 'provenance': {'anyOf': [{'type': 'object', 'required': ['source'], 'properties': {'as_of': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO-8601 timestamp the data is current as of.'}, 'source': {'enum': ['mirror', 'live', 'live_only'], 'type': 'string', 'description': "'mirror' = the synced copy (refreshed every ~30 min); 'live' = read from Meta just now because the account's first sync has not completed; 'live_only' = data Meta alone holds."}, 'stale_reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Set when the synced copy can no longer advance: 'token_invalid' (reconnect Meta) or 'disconnected'. Null when healthy."}}, 'description': 'Where an account-data answer came from and as of when.\n\nEvery read served by the Meta Ads Data Service carries this. The row shape\nnever depends on the source — only this block does. A read the service\ncannot answer for an account whose first sync has not completed (history\nbeyond the live window, too many entities) is answered with\n``{"status": "not_yet_landed", "message", "sync_state", ...}`` instead of\nthe tool\'s normal payload; ``{"status": "live_read_unavailable",\n"retry_after"}`` means Meta could not be read right now — retry later.\nThose two payloads are ``NotYetLandedResponse`` and\n``LiveReadUnavailableResponse`` below, and a tool that can answer with\nthem declares it through ``Refusable[...]``.', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Source and freshness of this answer.'}, 'next_offset': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': "Pass to the next call's offset parameter (null when has_more is false)."}, 'unmatched_queries': {'type': 'array', 'items': {'type': 'string'}, 'description': 'Queries that returned zero rows — agents must NOT substitute.'}}, 'description': 'Response from search_ad_images.', 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id'], 'properties': {'status': {'type': 'string', 'const': 'not_yet_landed'}, 'message': {'type': 'string', 'description': 'One sentence to relay: what is available until the sync completes and why this request needs more.'}, 'account_id': {'type': 'string'}, 'sync_state': {'anyOf': [{'type': 'object', 'required': ['landed'], 'properties': {'basis': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "What the verdict rests on: 'run' (a sync record), 'legacy' (the data_synced flag) or 'none'."}, 'phase': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'initial | catchup | landed | failed; null without a sync record.'}, 'landed': {'type': 'boolean', 'description': 'True once every stream of the first sync has completed.'}, 'streams': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Per-stream status: pending | running | completed | failed.'}, 'task_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'failed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'landed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'streams_done': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams completed so far.'}, 'streams_total': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams the first sync covers.'}, 'insights_chunks': {'anyOf': [{'type': 'object', 'additionalProperties': {'anyOf': [{'type': 'integer'}, {'type': 'null'}]}}, {'type': 'null'}], 'default': None, 'description': '{done, total} chunks of the insights backfill.'}, 'catchup_started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': "Progress of an account's first sync — ``SyncState.as_dict()``\n(``app.services.meta_ads_data.landed``) field for field.", 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'How far the first sync is.'}, 'live_window_days': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Days of insights history a Live Read answers (the live window).'}}, 'description': "What a façade-backed tool answers instead of its normal payload when\nthe account's first sync has not completed and the question is outside\nwhat a Live Read may answer — ``NotYetLanded.to_mcp()`` field for field.\nA normal result, never ``isError``: relay ``message`` and, when asked how\nfar along the sync is, ``sync_state``.", 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id', 'retry_after'], 'properties': {'status': {'type': 'string', 'const': 'live_read_unavailable'}, 'message': {'type': 'string'}, 'account_id': {'type': 'string'}, 'retry_after': {'type': 'integer', 'description': 'Seconds to wait before retrying.'}}, 'description': 'Meta could not be read for the account right now (throttled, time\nbudget exceeded) — ``LiveReadUnavailable.to_mcp()`` field for field.\nA normal result, never ``isError``; retry after ``retry_after`` seconds.', 'additionalProperties': True}]}
search_ad_videos
Search Ad Videos
Search videos in this account's ad library and open a visual picker to select one or more. Queries meta_ad_videos by title (case-insensitive), fetches fresh thumbnails from Meta API, then opens the asset picker widget. The user can select up to 10 videos at once, then clicks Confirm — the chosen video_ids are sent back to Claude in a single ``updateModelContext`` payload (numbered list when multiple). Multi-select workflow (recommended for multi-variant creatives): 1. search_ad_videos(account_id, query='product demo') — user picks several video_ids in one go 2. create_creative(account_id, creatives=[{format: 'video', video_id: '<v1>', ...}, {format: 'video', video_id: '<v2>', ...}, ...]) ↑ Pass ALL the selected video_ids in a single batched call — do NOT call create_creative once per video. 3. create_ad(account_id, ads=[{adset_id, creative_id, name}, ...]) Single-select workflow (one creative): 1. search_ad_videos(account_id, query='product demo') — user picks one video 2. create_creative(account_id, creatives=[{format: 'video', video_id: '<id>', ...}]) 3. create_ad(account_id, ads=[{adset_id, creative_id, name}]) Omit query to browse the most recent videos (discovery mode).
Read only Open world
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'limit': {'anyOf': [{'type': 'integer', 'maximum': 100, 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Max videos to return per page (default 20, max 100).'}, 'query': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Search term — case-insensitive match on video title. Omit to browse most recent videos.'}, 'offset': {'type': 'integer', 'default': 0, 'minimum': 0, 'description': 'Number of results to skip for pagination. Use next_offset from a prior response to fetch the next page.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'anyOf': [{'type': 'object', 'required': ['total', 'count', 'offset', 'has_more', 'account_id', 'asset_type', 'videos'], 'properties': {'count': {'type': 'integer', 'description': 'Number of rows in this response.'}, 'query': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'total': {'type': 'integer', 'description': 'Total matching rows across all pages.'}, 'offset': {'type': 'integer', 'description': 'Pagination offset of this page.'}, 'videos': {'type': 'array', 'items': {'type': 'object', 'required': ['video_id'], 'properties': {'title': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'duration': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'default': None, 'description': 'Duration in seconds.'}, 'video_id': {'type': 'string', 'description': 'Video ID — pass to create_creative as video_id.'}, 'source_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Playable source video URL.'}, 'thumbnail_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': 'One video row from search_ad_videos.', 'additionalProperties': True}}, 'has_more': {'type': 'boolean', 'description': 'True when there are more rows past this page.'}, 'account_id': {'type': 'string'}, 'asset_type': {'type': 'string', 'description': "Always 'video'."}, 'provenance': {'anyOf': [{'type': 'object', 'required': ['source'], 'properties': {'as_of': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO-8601 timestamp the data is current as of.'}, 'source': {'enum': ['mirror', 'live', 'live_only'], 'type': 'string', 'description': "'mirror' = the synced copy (refreshed every ~30 min); 'live' = read from Meta just now because the account's first sync has not completed; 'live_only' = data Meta alone holds."}, 'stale_reason': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Set when the synced copy can no longer advance: 'token_invalid' (reconnect Meta) or 'disconnected'. Null when healthy."}}, 'description': 'Where an account-data answer came from and as of when.\n\nEvery read served by the Meta Ads Data Service carries this. The row shape\nnever depends on the source — only this block does. A read the service\ncannot answer for an account whose first sync has not completed (history\nbeyond the live window, too many entities) is answered with\n``{"status": "not_yet_landed", "message", "sync_state", ...}`` instead of\nthe tool\'s normal payload; ``{"status": "live_read_unavailable",\n"retry_after"}`` means Meta could not be read right now — retry later.\nThose two payloads are ``NotYetLandedResponse`` and\n``LiveReadUnavailableResponse`` below, and a tool that can answer with\nthem declares it through ``Refusable[...]``.', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Source and freshness of this answer.'}, 'next_offset': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': "Pass to the next call's offset parameter (null when has_more is false)."}}, 'description': 'Response from search_ad_videos.', 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id'], 'properties': {'status': {'type': 'string', 'const': 'not_yet_landed'}, 'message': {'type': 'string', 'description': 'One sentence to relay: what is available until the sync completes and why this request needs more.'}, 'account_id': {'type': 'string'}, 'sync_state': {'anyOf': [{'type': 'object', 'required': ['landed'], 'properties': {'basis': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "What the verdict rests on: 'run' (a sync record), 'legacy' (the data_synced flag) or 'none'."}, 'phase': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'initial | catchup | landed | failed; null without a sync record.'}, 'landed': {'type': 'boolean', 'description': 'True once every stream of the first sync has completed.'}, 'streams': {'anyOf': [{'type': 'object', 'additionalProperties': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Per-stream status: pending | running | completed | failed.'}, 'task_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'failed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'landed_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'streams_done': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams completed so far.'}, 'streams_total': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Streams the first sync covers.'}, 'insights_chunks': {'anyOf': [{'type': 'object', 'additionalProperties': {'anyOf': [{'type': 'integer'}, {'type': 'null'}]}}, {'type': 'null'}], 'default': None, 'description': '{done, total} chunks of the insights backfill.'}, 'catchup_started_at': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}}, 'description': "Progress of an account's first sync — ``SyncState.as_dict()``\n(``app.services.meta_ads_data.landed``) field for field.", 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'How far the first sync is.'}, 'live_window_days': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None, 'description': 'Days of insights history a Live Read answers (the live window).'}}, 'description': "What a façade-backed tool answers instead of its normal payload when\nthe account's first sync has not completed and the question is outside\nwhat a Live Read may answer — ``NotYetLanded.to_mcp()`` field for field.\nA normal result, never ``isError``: relay ``message`` and, when asked how\nfar along the sync is, ``sync_state``.", 'additionalProperties': True}, {'type': 'object', 'required': ['status', 'message', 'account_id', 'retry_after'], 'properties': {'status': {'type': 'string', 'const': 'live_read_unavailable'}, 'message': {'type': 'string'}, 'account_id': {'type': 'string'}, 'retry_after': {'type': 'integer', 'description': 'Seconds to wait before retrying.'}}, 'description': 'Meta could not be read for the account right now (throttled, time\nbudget exceeded) — ``LiveReadUnavailable.to_mcp()`` field for field.\nA normal result, never ``isError``; retry after ``retry_after`` seconds.', 'additionalProperties': True}]}
search_targeting
Search Targeting Options
Search targeting options — find interests, demographics, locations, behaviors. Use this before create_adset or update_adset_targeting to get valid IDs. Call estimate_audience_size to check reach before committing. Each search_type returns different result fields: - interests: {id, name, audience_size_lower_bound, audience_size_upper_bound} - geolocation: {key, name, type, country_code, region, region_id, ...} - behaviors/income/life_events/industries: {id, name, description, ...} - work_positions/work_employers: {id, name} - locale: {key, name} Always use the exact id/key values from results when building targeting specs.
Read only Open world
Input schema
{'type': 'object', 'required': ['account_id', 'search_type', 'query'], 'properties': {'query': {'type': 'string', 'minLength': 1, 'description': "Search keyword (e.g. 'golf', 'New York', 'accounting'). For interests/geolocation/work_positions/work_employers/locale this searches by keyword. For behaviors/income/life_events/industries this filters the full category list by keyword."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'search_type': {'enum': ['interests', 'geolocation', 'behaviors', 'income', 'life_events', 'industries', 'work_positions', 'work_employers', 'locale'], 'type': 'string', 'description': 'Type of targeting to search for'}, 'location_types': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "Only for geolocation searches. Filter by location type: 'country', 'city', 'region', 'zip', 'geo_market'. Omit to search all types."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['search_type', 'count', 'results'], 'properties': {'count': {'type': 'integer'}, 'query': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'results': {'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}, 'description': "Targeting options. Each has id (pass to create_adset/update_adset_targeting), name, type, audience_size_lower_bound/upper_bound, and a path showing the taxonomy hierarchy (e.g. ['Interests', 'Sports', 'Soccer'])."}, 'search_type': {'type': 'string', 'description': "Taxonomy class searched: 'interests', 'geo', 'behaviors', etc."}}, 'description': 'Results from a Meta targeting-taxonomy search.', 'additionalProperties': True}
set_lead_form_status
Archive / Reactivate Lead Form
Archive or reactivate a Meta Lead Form. Archiving is the ONLY retirement path Meta offers — lead forms cannot be deleted, by us or by anyone. Status is also the only property of a form that can ever be changed after creation; questions, disclaimers and screens are immutable, so a form with the wrong questions must be archived and replaced. Archiving a form that live ads still point at will stop those ads collecting leads. Check first with list_creatives (read lead_gen_form_id) before archiving anything in use. Idempotent — setting a status the form already has is a no-op that returns success.
Destructive Open world Idempotent
Input schema
{'type': 'object', 'required': ['account_id', 'lead_form_id', 'status'], 'properties': {'status': {'enum': ['ACTIVE', 'ARCHIVED'], 'type': 'string', 'description': 'ARCHIVED retires the form: it stops accepting new submissions and cannot be attached to new ads. ACTIVE restores it. Already-collected leads are NOT affected.'}, 'page_id': {'anyOf': [{'type': 'string', 'minLength': 1}, {'type': 'null'}], 'default': None, 'description': "Facebook Page that owns the form. Omit to use the account's default promote_page."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'lead_form_id': {'type': 'string', 'minLength': 1, 'description': 'Lead form ID, from list_lead_forms or create_lead_form.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['status', 'lead_form_id', 'lead_form_status'], 'properties': {'name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'status': {'type': 'string', 'description': "Literal 'success'."}, 'page_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None}, 'update_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'entity_updates audit row id for this change.'}, 'lead_form_id': {'type': 'string'}, 'previous_status': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "The form's status before the change."}, 'lead_form_status': {'type': 'string', 'description': "The form's status after the change (ACTIVE or ARCHIVED)."}}, 'description': 'Result of set_lead_form_status.', 'additionalProperties': True}
update_adset_targeting
Update Ad Set Targeting
Update the targeting for a Meta ad set. Takes effect immediately. Workflow: 1. search_targeting(search_type='interests', query='...') to find interest IDs 2. search_targeting(search_type='geolocation', query='...') to find geo keys 3. update_adset_targeting(adset_id='...', countries=['US'], interests=[{id, name}]) The server assembles a valid Meta targeting spec from the flat inputs, validates it via /reachestimate (auto-fixing deprecated interests), then applies it. Idempotent: applying the same targeting again succeeds. Advantage+ audience: when advantage_audience=True (default) and you pass custom audiences, lookalikes, or detailed targeting, the server marks them as Advantage+ signals (the "relaxed setup") so Meta accepts the edit — they can't be hard controls while Advantage+ is on. Pass advantage_audience=False to use them as strict controls instead. Check current targeting with list_adsets first. All changes are tracked with before/after state snapshots for auditability.
Destructive Open world Idempotent
Input schema
{'type': 'object', 'required': ['account_id', 'adset_id'], 'properties': {'zips': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': "Zip key from search_targeting geolocation results, e.g. 'US:94304'"}}, 'description': "A zip code geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': "Zip code targets from search_targeting(search_type='geolocation')."}, 'cities': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': 'City key from search_targeting geolocation results'}, 'radius': {'anyOf': [{'type': 'integer', 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Radius around city center (10-50 mi / 17-80 km)'}, 'distance_unit': {'anyOf': [{'enum': ['mile', 'kilometer'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Unit for radius'}}, 'description': "A city geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': "City targets from search_targeting(search_type='geolocation')."}, 'income': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Income targets from search_targeting(search_type='income')."}, 'age_max': {'anyOf': [{'type': 'integer', 'maximum': 65, 'minimum': 18}, {'type': 'null'}], 'default': None, 'description': "Maximum age (18-65). Defaults to Meta's default (65). NOTE: Meta rejects age_max<65 when advantage_audience=True (the default). To cap age below 65, also pass advantage_audience=False."}, 'age_min': {'anyOf': [{'type': 'integer', 'maximum': 65, 'minimum': 18}, {'type': 'null'}], 'default': None, 'description': "Minimum age (18-65). Defaults to Meta's default (18)."}, 'genders': {'anyOf': [{'type': 'array', 'items': {'type': 'integer'}}, {'type': 'null'}], 'default': None, 'description': '[1]=male only, [2]=female only, [1,2]=both specified. Omit for all genders (Meta default).'}, 'regions': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': 'Region key from search_targeting geolocation results'}}, 'description': "A region geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': "Region targets from search_targeting(search_type='geolocation')."}, 'adset_id': {'type': 'string', 'minLength': 1, 'description': 'The ad set ID to update (from list_adsets or get_performance)'}, 'behaviors': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Behavior targets from search_targeting(search_type='behaviors')."}, 'countries': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "ISO country codes, e.g. ['US', 'CA']. At least one geo field (countries, cities, regions, or zips) is required."}, 'interests': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Interest targets from search_targeting(search_type='interests')."}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'industries': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Industry targets from search_targeting(search_type='industries')."}, 'life_events': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Life event targets from search_targeting(search_type='life_events')."}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is changing targeting (e.g. 'narrow to high-intent lookalike after soak'). Logged for telemetry."}, 'work_employers': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Work employer targets from search_targeting(search_type='work_employers')."}, 'work_positions': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': "Work position targets from search_targeting(search_type='work_positions')."}, 'custom_audiences': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': 'Custom audiences to target (from list_custom_audiences). Includes lookalikes, website audiences, customer lists.'}, 'device_platforms': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "Device types: mobile, desktop. Omit for both — but note that omitting it also CLEARS an existing restriction, so a call ad set (which requires ['mobile']) must re-supply it."}, 'advantage_audience': {'type': 'boolean', 'default': True, 'description': "Enable Advantage+ audience expansion (Meta's ML broadens targeting beyond your spec). DEFAULTS TO TRUE — recommended for most cases. Set to False when you need a hard demographic cap (age_max<65, narrow gender) or any constraint Meta must respect exactly. Meta rejects advantage_audience=True combined with age_max<65. When True, any custom audiences / lookalikes / detailed targeting are submitted as Advantage+ *signals* (the 'relaxed setup'); they cannot be hard controls while Advantage+ is on (Meta rejects that as subcode 1359202) — set False to use them as controls."}, 'excluded_behaviors': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': 'Behaviors to EXCLUDE from targeting.'}, 'excluded_interests': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': 'Interests to EXCLUDE from targeting.'}, 'facebook_positions': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "Facebook surfaces, e.g. ['feed','story','reels']. Re-supply to keep."}, 'instagram_positions': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "Instagram surfaces, e.g. ['stream','story','reels']. Re-supply to keep."}, 'publisher_platforms': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Platforms the ad set may deliver on: facebook, instagram, audience_network, messenger, threads. IMPORTANT: this tool replaces the entire targeting spec — omit this and any existing placement restriction is cleared, reverting the ad set to Advantage+ placements. Read list_adsets first and re-supply what you want to keep.'}, 'excluded_geo_locations': {'anyOf': [{'type': 'object', 'properties': {'zips': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': "Zip key from search_targeting geolocation results, e.g. 'US:94304'"}}, 'description': "A zip code geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': 'Zip codes to exclude'}, 'cities': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': 'City key from search_targeting geolocation results'}, 'radius': {'anyOf': [{'type': 'integer', 'minimum': 1}, {'type': 'null'}], 'default': None, 'description': 'Radius around city center (10-50 mi / 17-80 km)'}, 'distance_unit': {'anyOf': [{'enum': ['mile', 'kilometer'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Unit for radius'}}, 'description': "A city geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': 'Cities to exclude'}, 'regions': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['key'], 'properties': {'key': {'type': 'string', 'description': 'Region key from search_targeting geolocation results'}}, 'description': "A region geo-target from search_targeting(search_type='geolocation')."}}, {'type': 'null'}], 'default': None, 'description': 'Regions to exclude'}, 'countries': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "ISO country codes to exclude, e.g. ['MX']"}}, 'description': 'Geo locations to exclude from targeting.'}, {'type': 'null'}], 'default': None, 'description': 'Geo locations to EXCLUDE from targeting.'}, 'excluded_custom_audiences': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'required': ['id', 'name'], 'properties': {'id': {'type': 'string', 'description': 'Targeting option ID from search_targeting results'}, 'name': {'type': 'string', 'description': 'Display name from search_targeting results'}}, 'description': 'A targeting option with Meta ID and display name (from search_targeting).'}}, {'type': 'null'}], 'default': None, 'description': 'Custom audiences to EXCLUDE from targeting (from list_custom_audiences).'}, 'audience_network_positions': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': 'Audience Network surfaces. Re-supply to keep.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['next_steps', 'account_id', 'adset_id', 'targeting_spec_applied'], 'properties': {'status': {'type': 'string', 'const': 'success', 'default': 'success'}, 'adset_id': {'type': 'string'}, 'update_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Audit-record ID — the entity_updates row created for this change.'}, 'account_id': {'type': 'string'}, 'next_steps': {'type': 'string', 'description': 'Recommended follow-up tool call.'}, 'entity_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Human-readable name of the entity.'}, 'state_after': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Entity state snapshot after the change (shape varies by entity type).'}, 'state_before': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Entity state snapshot before the change (shape varies by entity type).'}, 'estimated_reach': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Audience-size estimate for the new targeting (Meta returns lower/upper bounds).'}, 'targeting_spec_applied': {'type': 'object', 'description': 'Final targeting_spec sent to Meta (post-validation, post-auto-fix).', 'additionalProperties': True}}, 'description': 'Result of replacing the targeting_spec on an ad set.', 'additionalProperties': True}
update_entity
Update Entity Fields
Patch arbitrary fields on a Meta campaign, ad set, or ad. Idempotent. Long-tail edit tool. Use the dedicated change_entity_status / change_entity_budget tools for the hot paths — they are safer and have richer error messages. Each field of EntityFieldsPatch documents which entity_type(s) it applies to. Live mutation — changes apply immediately on Meta. State before/after is captured for audit. NOT for editing targeting (use update_adset_targeting).
Destructive Open world Idempotent
Input schema
{'type': 'object', 'required': ['account_id', 'entity_type', 'entity_id', 'fields'], 'properties': {'fields': {'type': 'object', 'properties': {'name': {'anyOf': [{'type': 'string', 'minLength': 1}, {'type': 'null'}], 'default': None, 'description': 'Display name. Applies to: campaign, adset, ad.'}, 'status': {'anyOf': [{'enum': ['PAUSED', 'ACTIVE', 'ARCHIVED', 'DELETED'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Lifecycle status. Applies to: campaign, adset, ad. PAUSED, ACTIVE, ARCHIVED, DELETED. For pause/resume prefer change_entity_status.'}, 'adlabels': {'anyOf': [{'type': 'array', 'items': {'type': 'object', 'additionalProperties': True}}, {'type': 'null'}], 'default': None, 'description': "Meta ad labels for cross-entity tagging/analytics, e.g. [{'name': 'BlackFriday2026'}]. Applies to: campaign, ad."}, 'end_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO datetime for ad set end (required with lifetime_budget). Applies to: adset.'}, 'dsa_payor': {'anyOf': [{'type': 'string', 'maxLength': 512}, {'type': 'null'}], 'default': None, 'description': 'DSA payor (entity paying for the ads). Required for ad sets targeting the EU or associated territories per the EU Digital Services Act. Max 512 chars. Applies to: adset.'}, 'spend_cap': {'anyOf': [{'type': 'number', 'minimum': 0}, {'type': 'null'}], 'default': None, 'description': "Lifetime spend ceiling in account currency (e.g. 5000.0 = $5,000 max). Pass 0 to REMOVE an existing spend cap (Meta's documented remove-cap value). Applies to: campaign."}, 'stop_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'ISO datetime for campaign end. Applies to: campaign.'}, 'bid_amount': {'anyOf': [{'type': 'number', 'exclusiveMinimum': 0}, {'type': 'null'}], 'default': None, 'description': 'Bid amount in account currency (cost-per-result for COST_CAP, max bid for LOWEST_COST_WITH_BID_CAP). Applies to: adset.'}, 'start_time': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "ISO datetime, e.g. '2026-01-15T00:00:00-0500'. Applies to: campaign, adset."}, 'bid_strategy': {'anyOf': [{'enum': ['LOWEST_COST_WITHOUT_CAP', 'LOWEST_COST_WITH_BID_CAP', 'COST_CAP', 'LOWEST_COST_WITH_MIN_ROAS'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Bid strategy. Applies to: campaign, adset.'}, 'daily_budget': {'anyOf': [{'type': 'number', 'exclusiveMinimum': 0}, {'type': 'null'}], 'default': None, 'description': 'Daily budget in account currency (e.g. 50.0 = $50/day). Applies to: campaign, adset. Mutually exclusive with lifetime_budget. Prefer change_entity_budget for safer edits.'}, 'billing_event': {'anyOf': [{'enum': ['IMPRESSIONS', 'LINK_CLICKS', 'THRUPLAY'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Billing event (IMPRESSIONS, LINK_CLICKS, THRUPLAY). Applies to: adset.'}, 'daily_spend_cap': {'anyOf': [{'type': 'number', 'minimum': 0}, {'type': 'null'}], 'default': None, 'description': 'CBO only: daily spend ceiling in account currency. Requires the parent campaign to own a daily_budget. Meta enforces a $1 minimum (subcode 1885056) — passing 0 is rejected; to remove an existing cap, clear it in Ads Manager. Applies to: adset.'}, 'dsa_beneficiary': {'anyOf': [{'type': 'string', 'maxLength': 512}, {'type': 'null'}], 'default': None, 'description': 'DSA beneficiary (entity benefiting from the ads). Required for ad sets targeting the EU or associated territories per the EU Digital Services Act. Max 512 chars. Applies to: adset.'}, 'lifetime_budget': {'anyOf': [{'type': 'number', 'exclusiveMinimum': 0}, {'type': 'null'}], 'default': None, 'description': 'Lifetime budget in account currency. Applies to: campaign, adset. Mutually exclusive with daily_budget. Prefer change_entity_budget.'}, 'promoted_object': {'anyOf': [{'type': 'object', 'properties': {'page_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Facebook page ID (for engagement/message objectives)'}, 'pixel_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Meta pixel ID for conversion tracking'}, 'application_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'App ID (for app promotion objectives)'}, 'object_store_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'App Store / Google Play URL (required with application_id for app promotion)'}, 'custom_event_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "Conversion event, e.g. 'PURCHASE', 'LEAD', 'ADD_TO_CART'"}}, 'description': 'Conversion tracking config for an ad set.\n\nAuto-resolved for OUTCOME_SALES campaigns if omitted (picks account pixel + PURCHASE).', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': "What the ad set optimizes toward. For lead gen: {page_id} for instant forms (destination_type=ON_AD) and for calls; {pixel_id, custom_event_type='LEAD'} for website leads. Send the WHOLE object — Meta replaces it rather than merging, so include every key you want to keep. Change it together with destination_type when switching a lead ad set between destinations; changing one without the other leaves a combination Meta rejects. Applies to: adset."}, 'destination_type': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Destination: WEBSITE, APP, MESSENGER, ON_AD, etc. Applies to: adset.'}, 'optimization_goal': {'anyOf': [{'enum': ['OFFSITE_CONVERSIONS', 'LINK_CLICKS', 'IMPRESSIONS', 'REACH', 'LEAD_GENERATION', 'QUALITY_LEAD', 'QUALITY_CALL', 'LANDING_PAGE_VIEWS', 'VALUE', 'CONVERSATIONS'], 'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Optimization goal. Meta rejects this on an ad set whose campaign has already published — duplicate the ad set and change it on the copy. Applies to: adset.'}, 'lifetime_spend_cap': {'anyOf': [{'type': 'number', 'minimum': 0}, {'type': 'null'}], 'default': None, 'description': 'CBO only: lifetime spend ceiling in account currency. Requires the parent campaign to own a lifetime_budget. Meta enforces a minimum (subcode 1885056) — passing 0 is rejected; to remove an existing cap, clear it in Ads Manager. Applies to: adset.'}, 'special_ad_categories': {'anyOf': [{'type': 'array', 'items': {'enum': ['NONE', 'HOUSING', 'EMPLOYMENT', 'FINANCIAL_PRODUCTS_SERVICES', 'ISSUES_ELECTIONS_POLITICS'], 'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "Special ad categories. Allowed: NONE, EMPLOYMENT, HOUSING, FINANCIAL_PRODUCTS_SERVICES (replaces CREDIT from Jan 14 2025), ISSUES_ELECTIONS_POLITICS. Use ['NONE'] for standard ads. Pair with special_ad_category_country whenever a non-NONE value is supplied. Applies to: campaign."}, 'daily_min_spend_target': {'anyOf': [{'type': 'number', 'minimum': 0}, {'type': 'null'}], 'default': None, 'description': 'CBO only: minimum daily spend target in account currency. Requires the parent campaign to own a daily_budget. Pass 0 to clear (e.g. when duplicate_adset deep_copy carried over a floor that pushes the campaign over budget — subcode 1885648). Applies to: adset.'}, 'lifetime_min_spend_target': {'anyOf': [{'type': 'number', 'minimum': 0}, {'type': 'null'}], 'default': None, 'description': 'CBO only: minimum lifetime spend target in account currency. Requires the parent campaign to own a lifetime_budget. Pass 0 to clear. Applies to: adset.'}, 'special_ad_category_country': {'anyOf': [{'type': 'array', 'items': {'type': 'string'}}, {'type': 'null'}], 'default': None, 'description': "ISO country codes scoping special_ad_categories (e.g. ['CA', 'GB']). Required by Meta whenever special_ad_categories contains a non-NONE value. Applies to: campaign."}}, 'description': "Object whose set fields are applied to the entity. Each field in EntityFieldsPatch lists which entity_type(s) it applies to; passing a field that doesn't apply to the chosen entity_type returns a validation error.\nCurrency fields (daily_budget, lifetime_budget, spend_cap, bid_amount) are in account currency — converted server-side.", 'additionalProperties': False}, 'entity_id': {'type': 'string', 'minLength': 1, 'description': 'ID of the entity to patch (from list_campaigns, list_adsets, or list_ads — never invent IDs).'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID. The account where entity_id lives — Meta returns code 100 (parameter error) if entity_id belongs to a different account. Look up via list_campaigns, list_adsets, or list_ads when uncertain.'}, 'entity_type': {'enum': ['campaign', 'adset', 'ad'], 'type': 'string', 'description': 'The type of entity to patch.'}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is making this edit (e.g. 'pausing low-CTR ad sets to reallocate budget'). Logged for telemetry — does not affect behavior."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['entity_type', 'entity_id', 'account_id', 'currency', 'fields_applied'], 'properties': {'status': {'type': 'string', 'const': 'success', 'default': 'success'}, 'currency': {'type': 'string'}, 'entity_id': {'type': 'string'}, 'update_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Audit-record ID — the entity_updates row created for this change.'}, 'account_id': {'type': 'string'}, 'next_steps': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Recommended follow-up tool call(s) for the LLM. Required on every successful mutation per CLAUDE.md; left Optional here because some tools (e.g. update_entity) currently omit it.'}, 'entity_name': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Human-readable name of the entity.'}, 'entity_type': {'enum': ['campaign', 'adset', 'ad'], 'type': 'string'}, 'state_after': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Entity state snapshot after the change (shape varies by entity type).'}, 'state_before': {'anyOf': [{'type': 'object', 'additionalProperties': True}, {'type': 'null'}], 'default': None, 'description': 'Entity state snapshot before the change (shape varies by entity type).'}, 'fields_applied': {'type': 'array', 'items': {'type': 'string'}, 'description': 'Sorted names of the fields actually sent to Meta.'}}, 'description': 'Result of patching arbitrary fields on a Meta entity.', 'additionalProperties': True}
upload_ad_image
Upload Ad Image
Upload a single image to Meta — internal helper for the Upload Creatives widget. Prefer calling upload_creatives, which opens the widget and runs the full upload flow. This tool accepts either image_data (base64 data URL from a file picker) or image_url (public URL to download). Returns image_hash and dimensions on success.
Open world
Input schema
{'type': 'object', 'required': ['account_id', 'file_name'], 'properties': {'file_name': {'type': 'string', 'minLength': 1, 'description': 'File name for the image (e.g. hero.jpg)'}, 'image_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Public URL to download the image from. Used if image_data is not provided.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Meta ad account ID'}, 'image_data': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Image as data URL (data:image/...;base64,...). Provide this or image_url.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['image_hash', 'name', 'account_id'], 'properties': {'name': {'type': 'string'}, 'width': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None}, 'height': {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'default': None}, 'status': {'type': 'string', 'const': 'success', 'default': 'success'}, 'account_id': {'type': 'string'}, 'image_hash': {'type': 'string', 'description': 'Pass to create_creative as image_hash.'}}, 'description': 'Result of upload_ad_image — registers a single image with Meta.', 'additionalProperties': True}
upload_ad_video
Upload Ad Video
Upload a video to Meta — internal helper for the Upload Creatives widget. Prefer calling upload_creatives, which opens the widget and runs the full upload flow. This tool accepts either video_url (Meta fetches directly — must be publicly accessible) or video_data (base64 data URL from a file picker — decoded and uploaded as multipart). Returns video_id and processing status.
Open world
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'title': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Display title for the video.'}, 'video_url': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Public URL to the video file. Provide this or video_data.'}, 'account_id': {'type': 'string', 'minLength': 1, 'description': 'Meta ad account ID'}, 'video_data': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': 'Video as data URL (data:video/...;base64,...). Provide this or video_url.'}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['video_id', 'title', 'account_id'], 'properties': {'title': {'type': 'string'}, 'status': {'type': 'string', 'const': 'success', 'default': 'success'}, 'video_id': {'type': 'string', 'description': 'Pass to create_creative as video_id.'}, 'account_id': {'type': 'string'}}, 'description': 'Result of upload_ad_video — registers a single video with Meta.', 'additionalProperties': True}
upload_creatives
Upload Creatives
Open the upload widget for adding images and videos to a Meta ad account. Opens an interactive widget where the user can: - Drag & drop or browse for image files (JPG, PNG, max 30 MB) and videos (MP4, MOV, max 4 GB) - Paste URLs to images or videos (MP4, MOV) for upload - Upload multiple files at once with per-item progress After upload, the image hashes and video IDs are sent back automatically so they can be used with create_creative and create_ad. This tool validates the account and access token before opening the widget. If the token has expired, ask the user to reconnect their Meta account.
Open world Idempotent
Input schema
{'type': 'object', 'required': ['account_id'], 'properties': {'account_id': {'type': 'string', 'minLength': 1, 'description': 'Ad account ID (numeric string)'}, 'agent_intent': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'default': None, 'description': "OPTIONAL free-text describing why the agent is opening the uploader (e.g. 'upload 3 new hook-driven statics to replace recycled images'). Logged for telemetry."}}, 'additionalProperties': False}
Output schema
{'type': 'object', 'required': ['account_id', 'message'], 'properties': {'status': {'type': 'string', 'const': 'success', 'default': 'success'}, 'message': {'type': 'string', 'description': 'Confirms the widget is open and waiting for input.'}, 'account_id': {'type': 'string'}}, 'description': 'Result of opening the user-facing upload widget.', 'additionalProperties': True}
Added
list_promotable_posts
2026年9月11日0:24
Added
register_media_assets
2026年9月11日0:24
Added
list_media_assets
2026年9月11日0:24
Added
preview_existing_creatives
2026年9月11日0:24
Added
search_ad_videos
2026年9月11日0:24
Added
search_ad_images
2026年9月11日0:24
Added
upload_ad_video
2026年9月11日0:24
Added
upload_ad_image
2026年9月11日0:24
Added
complete_upload
2026年9月11日0:24
Added
init_upload
2026年9月11日0:24
Added
upload_creatives
2026年9月11日0:24
Added
create_lead_form
2026年9月11日0:24
Added
set_lead_form_status
2026年9月11日0:24
Added
create_lead_form_audience
2026年9月11日0:24
Added
check_lead_gen_readiness
2026年9月11日0:24
Added
get_lead_form
2026年9月11日0:24
Added
list_lead_forms
2026年9月11日0:24
Added
create_ad
2026年9月11日0:24
Added
create_creative
2026年9月11日0:24
Added
list_creatives
2026年9月11日0:24
Added
get_pixel_health
2026年9月11日0:24
Added
create_adset
2026年9月11日0:24
Added
create_campaign
2026年9月11日0:24
Added
update_adset_targeting
2026年9月11日0:24
Added
create_lookalike_audience
2026年9月11日0:24
Added
create_website_audience
2026年9月11日0:24
Added
list_custom_audiences
2026年9月11日0:24
Added
estimate_audience_size
2026年9月11日0:24
Added
search_targeting
2026年9月11日0:24
Added
duplicate_ad
2026年9月11日0:24