> Generated by scripts/generate_mcp_contract_docs.py from the server's profile, catalog, OAuth-scope, policy, and result-budget registries. Guide version: 2026-07-30.1.
Public web guide: https://adsagent.md/docs/mcp-onboarding
0. Connection And Compatibility
- Default:
https://adsagent.md/mcp/v2(v2, stateless JSON HTTP,49152-byte response budget). - Legacy fallback:
https://adsagent.md/mcp(legacy_v1, stateful HTTP, dotted aliases,262144-byte response budget). - Test endpoints:
https://t.adsagent.md/mcp/v2withhttps://t.adsagent.md/mcpas the legacy fallback. - Header:
Authorization: Bearer <mcp_token>. /mcp/v2and/mcpselect AdsAgent Meta product profiles; they are not MCP protocol-version identifiers.- Let the client negotiate the protocol. MCP
2026-07-28uses statelessserver/discover; supported legacy revisions (2024-11-05,2025-03-26,2025-06-18,2025-11-25) continue to useinitialize. - Prefer the v2 product profile for new Claude, Codex, and other MCP connections. Use
/mcponly when the client cannot use that product profile. Existing/mcpclients remain compatible. - Keep the same endpoint and bearer across a protocol upgrade. MCP protocol changes alone never require re-registration or token replacement.
- Never synthesize
MCP-Protocol-VersionorMcp-Session-Id; session recovery applies only after a legacy protocol was negotiated. - After a server update, close and reopen the transport, re-list tools, read
adsagent://guide/brief, and rerunsetup_get_status.
Reading order: adsagent://guide/brief -> adsagent://guide/catalog -> one bounded adsagent://guide/catalog/<domain> topic. adsagent://guide/tools is a large fallback reference and must not be read end-to-end during onboarding.
1. OAuth Safe Mode Capability Contract
New OAuth grants default to the least-privilege mcp scope. Existing legacy grants retain their callable surface. The server selects policy; the agent cannot self-elevate.
| OAuth scope | Effective capabilities | Added to baseline | | --- | --- | --- | | mcp | mcp.read, mcp.templates.write, mcp.launch.prepare, mcp.launch.confirm, mcp.assets.refresh | Baseline | | mcp.assets.refresh | mcp.read, mcp.templates.write, mcp.launch.prepare, mcp.launch.confirm, mcp.assets.refresh | Baseline | | mcp.insights.pull | mcp.read, mcp.templates.write, mcp.launch.prepare, mcp.launch.confirm, mcp.assets.refresh, mcp.insights.pull | mcp.insights.pull | | mcp.optimize.write | mcp.read, mcp.templates.write, mcp.launch.prepare, mcp.launch.confirm, mcp.assets.refresh, mcp.optimize.write | mcp.optimize.write | | mcp.routing.write | mcp.read, mcp.templates.write, mcp.launch.prepare, mcp.launch.confirm, mcp.assets.refresh, mcp.routing.write | mcp.routing.write | | mcp.operator | mcp.read, mcp.templates.write, mcp.launch.prepare, mcp.launch.confirm, mcp.assets.refresh, mcp.operator | mcp.operator |
mcp.operator is an explicit override for bounded operator-only tool families, but OAuth responses still hide internal IDs and never expose insights_query_rows. Public handles are product_ref, task_ref, owner_ref, external ad_account_id, exact template names, and creative selection keys.
Internal task_id is operator-only. OAuth workflows always use the returned task_ref.
Before any Meta status, budget, or bid change, inspect setup_get_status.capabilities.delivery_mutations. If allowed=false, stop: for permission_action=enable_in_dashboard, direct the user to /dashboard/settings#mcp-access; for permission_action=reauthorize_with_scope, ask the user to reconnect with OAuth scope mcp.optimize.write. Never change customer permissions automatically. After the user changes access, reconnect the MCP transport and re-list tools. For ABO use overview_update_adset_budget then overview_update_confirm; for CBO use overview_update_campaign_budget then overview_update_campaign_budget_confirm. Never substitute one budget level for the other. For one known campaign, ad set, or ad, do not scan historical Insights to discover its current status, budget, or bid. Call the matching prepare tool directly: prepare reads the live current value and does not mutate Meta. If an explicit preflight read is needed, call overview_get_live_configs with exactly one typed entity and no mutation_ref. management=true or product/date Insights is not live configuration evidence.
2. First Run
- Call
setup_get_status.
Inspect top-level client_skill_pack as notify-only policy. If the installed version is unknown, continue silently; never auto-update.
- Call
products_list; it is already sorted by recent spend. - Tell the user the product-card count and ask for one product/account plus
date_fromanddate_to. - If
setup_get_status.capabilities.agent_method_profile.profile_id=adsagent_agent_methods_v1, callinsights_query_consistentonce with rootquery_contract_version=1and onescopeor up to 20scopes. - Poll every distinct returned
task_refserially withtasks_get_status(response_mode=compact). When terminal, consume its boundedresultdirectly only if taskstatus=completed,result.status=complete, andresult.meta.complete=true; do not rerun page 1 merely to continue. Stop on a missing/incomplete result orpartial_completed,failed, orcancelled. For a next page, preserve every filter and increment onlypage. Setmin_as_ofto the taskresult.meta.source_observed_at, or for an immediate complete response use the corresponding scope overview'squery_contract.coverage.source_observed_at. Locate a single-scope overview atdata, and each ordered multi-scope overview atdata.items[].result; with multiple scopes use the earliest first-page source anchor. Preserve each overview'smeta.inventory_anchorand passinventory_anchorson page 2+ as one ordered opaque value per original scope. Ifcontinuation_valid=falseor an anchor is rejected, discard all partial rows and restart page 1 serially with the original query. - If
agent_method_profileis absent, preserve native behavior: one scope ->insights_query_overview; multiple scopes -> oneinsights_query_batch_overview. Passmetadata_contract_version=1; use native consistency only when advertised. - Report server totals only when completeness is proven. A missing scope is unknown, never zero.
3. Facebook, Google Ads, And TikTok Connect
- Meta: use
setup_begin_facebook_connect, let the human open the returned URL, then pollsetup_check_facebook_connect. - Google Ads and TikTok: use
connections_create_intent, let the human complete the single-use browser flow, then pollconnections_check_intent. - Never collect platform passwords, cookies, OAuth codes, or bearer tokens in chat.
4. Insights And Output Stability
- Never fan out one overview call per product/account. On
mcp_fanout_detected, stop the loop and followdata.recommended_toolplusdata.recommended_modefor all current and pending scopes. - Profile mode is opt-in only through
query_contract_version=1at the request root. Keepscopeandscopesmutually exclusive, preserve input order, and stop onverification_pending,rate_limited,data_not_fresh,incomplete_coverage,failed, orcomplete=false. - Use
require_freshonly whensetup_get_status.capabilities.insights_consistencyadvertises it. Otherwise use onecachedtenant-ledger query; explicit cumulative cached windows may cover up to 365 days. Never turn one long read into repeated 31-day refresh calls. An advertised source refresh must obeycapabilities.insights_refresh.max_date_range_days. - Candidate filtering stays server-side: keep
page_size<=50, use allowlistedfilterscombined with AND, and retry a corrected request once whenadsagent_query_invalidnames a public invalid field. Text hierarchy fields supportcontains/prefix/eq; metrics, budgets, and bids supportgt/gte/lt/lte/eq; status and delivery enums supporteq/in. Legacysearchandspend_gtremain compatible, but do not usededupe_byin new workflows. Useeffective_statusfor Meta delivery outcomes andconfigured_statusfor configured ACTIVE/PAUSED state. Withgroup_by=ad, use returnedad_account_id,ad_account_name,campaign_id,campaign_name,adset_id,adset_name,ad_id, andad_name; do not prefetch or fan out parents. When the user explicitly asks for every matching Ad, preserve eachad_idand advancepageserially whiledata.meta.has_more=true; keep all filters identical and pinmin_as_ofto the first-page source anchor described above. Deduplicate Ad names, classify language, and perform business grouping in the client. Never parallelize pages or enlarge the page. For an explicitly requested large full table, call groupedinsights_export_csvwith the same filters and consume the artifact instead of putting thousands of rows in chat. Onscope_unavailable, do not infer another workspace/token or Meta permission, a specific missing account, a disabled-account gap, missing date coverage, stale ledger data, or zero spend from that signal alone. Runsetup_get_statusand the matching discovery tool (products_listoraccounts_list_linked_accounts) once. If the same scope remains listed, retry the identical bounded read once; if it persists, stop and preservesupport_reffor operator review. Never broaden scope or change permissions. - Read
setup_get_status.support_reportingbefore reporting. Submit automatically only whenautomatic_allowed=true; in manual mode submit only after explicit user request or approval; when off or unavailable, preserve thesupport_refand stop. For an eligible unresolved internal/contract/dependency/verification error, callsupport_report_erroronce using therequestwrapper, a stableidempotency_key, bounded classifications, and client versions. Never submit prompts, raw arguments, tokens, cookies, authorization, provider responses, or confirmation material. Reporting is observational only and never retries, confirms, resumes, or replays a write. Never report an error returned by either support tool; stop and retain itssupport_ref. Preserve expected validation/auth/permission/business-rule failures without auto-reporting unless repeated. Pollsupport_get_report_statusno earlier thanpoll_after_secondsor when the user asks. - Do not borrow profile behavior from another channel and do not synthesize
query_contract, completeness, timezone, or freshness in the client. Absence ofagent_method_profilemeans use the channel's native fallback. freshness_kind=age_onlymeans only that a pull is recent; it is not mutation coverage.source_watermark,metrics_observed_after_mutation, andconfig_verified_liveare distinct proofs.- Do not make a decision when the response is
verification_pending/data_not_fresh, the launch date is unknown, orcomplete=false. - Reference campaigns by ID, not name. Report effective Meta statuses verbatim.
- Use
creatives_list(created_from=<inclusive>, created_to=<exclusive>, limit<=50)with timezone-aware ISO 8601 bounds to discover one recent upload window. Follownext_cursorserially with unchanged filters whilemeta.has_more=true. Omit both bounds for the whole library; never infer upload completion from cache age alone. - Follow
adsagent://guide/metadata-contract: configured ACTIVE/PAUSED isconfigured_status; provider delivery, review, and rejection outcomes areeffective_status; additivedelivery_statusplusdelivery_issuesexpose normalized inherited blockers without rewriting child state. For campaign/adset/ad entity-existence totals, requiremeta.inventory_coverage=completeandzero_insights_entities_included=true;partial,insights_rows_only, andunavailablekeep absence unknown. Inventory freshness is independent from Insights freshness. Legacy readstatusmirrorseffective_status. Money values are account-currency major units,budget_leveliscampaign|adset, andbid_strategy/optimization_goalare canonical lower-case values. - Return metric summaries as horizontal Markdown tables. Do not sum visible rows or infer totals from page length.
- Use
insights_export_csvonly when explicitly requested. If queued, polltasks_get_statuswith the returnedtask_refuntilterminal=true. Compact and standard terminal responses includeresult.artifact; HTTP GET its opaquedownload_urlbyte-for-byte and never redact, rebuild, decode, or truncate it. Return artifact metadata/link and never raw CSV. - Oversized normal results are bounded previews. Full tables and files use artifact metadata instead of consuming the agent's context window.
Meta structured filter fields
| group_by | Text fields | Numeric fields | Enum fields | | --- | --- | --- | --- | | account | ad_account_id, ad_account_name | clicks, conversion_value, conversions, cpa, cpc, cpm, ctr, impressions, reach, roas, spend | account_blocker_code, currency, delivery_issue_codes, delivery_status | | campaign | ad_account_id, ad_account_name, campaign_id, campaign_name | bid_amount, clicks, conversion_value, conversions, cpa, cpc, cpm, ctr, daily_budget, impressions, lifetime_budget, reach, roas, spend | account_blocker_code, bid_strategy, budget_level, configured_status, currency, delivery_issue_codes, delivery_status, effective_status, objective, product_type, status | | adset | ad_account_id, ad_account_name, adset_id, adset_name, app_id, campaign_id, campaign_name, pixel_id | bid_amount, clicks, conversion_value, conversions, cpa, cpc, cpm, ctr, daily_budget, impressions, lifetime_budget, reach, roas, spend | account_blocker_code, bid_strategy, billing_event, budget_level, configured_status, conversion_event, currency, delivery_issue_codes, delivery_status, effective_status, objective, optimization_goal, product_type, status | | ad | ad_account_id, ad_account_name, ad_id, ad_name, adset_id, adset_name, app_id, campaign_id, campaign_name, pixel_id | bid_amount, clicks, conversion_value, conversions, cpa, cpc, cpm, ctr, daily_budget, impressions, lifetime_budget, reach, roas, spend | account_blocker_code, bid_strategy, billing_event, budget_level, configured_status, conversion_event, currency, delivery_issue_codes, delivery_status, effective_status, objective, optimization_goal, product_type, status |
All conditions are AND. Text operators: contains, prefix, eq; numeric operators: gt, gte, lt, lte, eq; enum operators: eq, in. At most 12 conditions and 20 values per in condition.
effective_status is Meta's actual delivery/review outcome, including DISAPPROVED and PENDING_REVIEW. configured_status is the configured ACTIVE/PAUSED state. Legacy status remains an alias of effective_status.
Meta metadata contract
{
"canonical_values": {
"bid_strategy": "platform_agnostic_lowercase",
"billing_event": "meta_native_uppercase",
"budget_level": [
"campaign",
"adset"
],
"conversion_event": "lowercase_separate_from_optimization_goal",
"currency": "uppercase_three_letter_account_currency",
"delivery_status": {
"configured": "configured_status",
"effective": "effective_status",
"write_target": "target_configured_status"
},
"money_unit": "major",
"normalized_delivery_block_state": {
"field": "delivery_status",
"values": [
"BLOCKED",
"PENDING",
"UNKNOWN"
]
},
"objective": "meta_native_uppercase",
"optimization_goal": "platform_agnostic_lowercase"
},
"public_metadata_contract_version": 1,
"resource": "adsagent://guide/metadata-contract",
"rules": {
"activation": "Pass metadata_contract_version=1 to native Insights/live-config tools, or query_contract_version=1 to insights_query_consistent.",
"conflicts": "Canonical and legacy write values that differ are rejected.",
"delivery_block_boundary": "delivery_status is an additive normalized block state. It never replaces configured_status, effective_status, or write targets.",
"inventory_boundary": "For campaign, adset, and ad reads, inventory_coverage=complete means the current entity inventory was left-joined before filtering and pagination, including zero-Insights entities. partial, insights_rows_only, and unavailable never prove entity absence or delivery health. Read inventory_freshness and inventory_source_observed_at separately from Insights freshness.",
"legacy_scope": "Aliases are accepted only at the exact listed scope.",
"new_clients": "Use canonical fields and canonical values only.",
"no_global_aliases": true,
"product_scope_boundary": "A product scope includes current cached asset bindings as well as accounts with Insights rows in the requested window. An account with no window Insights remains in coverage; missing metrics are unknown and must make a required-complete read incomplete rather than silently shrinking the product scope.",
"read_write_boundary": "effective_status is an observed delivery outcome; target_configured_status is a requested configured state.",
"source_boundary": "Insights ledger metadata and live Meta configuration are distinct evidence sources and must remain labeled."
}
}Queued response shape:
{
"status": "queued",
"task_ref": "<opaque public ref>",
"poll": {
"tool": "tasks_get_status",
"arguments": {"task_ref": "<opaque public ref>"}
}
}poll = resp["poll"]
while True:
task = call_tool(poll["tool"], poll["arguments"])
if task["terminal"]:
break5. Consequential Actions
For a task error meta_write_verification_pending or meta_write_verification_ambiguous, call operations_get_context(task_ref=...) and never repeat the write. Follow each returned failure's retry flags; only manual_new_task_allowed=true permits preparing a new task with fresh approval and a new confirm token.
Launches and delivery changes use prepare -> sanitized approval_request.summary -> explicit user approval -> confirm. Status mutations use target_configured_status=ACTIVE|PAUSED; never pass an effective_status outcome as a write target. Confirm tokens are exact, single-use values with a 15-minute TTL. Never reconstruct, retry, or automatically confirm them. If a token expires, prepare again, show the new summary, and obtain fresh approval. Use deny to discard.
After templates_create or templates_update, call templates_get with the exact template_name. For reverse-engineered imports, pass source_adset_id and optional source_account_id without configuration maps; the server re-reads and persists the source snapshot. If a legacy client supplies all three complete configuration maps, those maps are preserved and reverse-engineered source provenance is cleared. A manual complete-map template may retain source_account_id without source_adset_id; this is an account binding, not proof of a Meta source snapshot. Partial source-plus-map requests are rejected. Treat templates_list as bounded screening only: when its migration_state.persisted=false, perform the exact-name detail read and repair or re-save if detail is not launch-ready. Continue to QuickCreate prepare only when template_snapshot.persisted=true, template_snapshot.normalization.rejected_paths_complete=true, template_snapshot.configuration_coverage.complete=true, and template_snapshot.launch_readiness.status=ready_for_prepare. template_snapshot.launch_critical_coverage describes saved coverage; runtime fields may still be supplied by the QuickCreate request and are validated after overrides by prepare. legacy_projection and quarantined templates remain readable but block prepare until repaired or re-saved. Preserve template_snapshot_ref and template_snapshot_revision from the approval summary. If confirm returns template_snapshot_changed, do not reuse the old confirm token: read the template again, prepare again, show the new summary, and obtain fresh explicit approval. Never infer snapshot identity from updated_at.
Notification channels and Meta Ads webhooks use notifications_integrations_list -> notifications_integration_prepare -> sanitized summary -> explicit user approval -> notifications_integration_confirm. Credential inputs are encrypted in the approval draft and never returned by list or confirm. test_channel sends exactly one external message. Meta registration/subscription actions never change FB user permissions. If Meta returns an uncertain subscription outcome, re-list the integration state and never replay the consumed confirm token.
Notification monitoring contract
notifications_integrations_list.monitoring_capabilities is the machine-readable source of truth.
Meta Ads webhook monitoring is event-driven and requires one subscription for each ad account:
| Meta field | Notification type | Severity | | --- | --- | --- | | ad_recommendations | meta_ad_recommendation | info | | creative_fatigue | meta_creative_fatigue | warning | | effective_status | meta_effective_status | status_dependent | | in_process_ad_objects | meta_ad_object_processed | info | | subscriptions | meta_ads_subscription | info | | with_issues_ad_objects | meta_ad_object_issue | critical |
Cached asset-health monitoring runs after an asset refresh or notifications_scan; that scan does not call Meta directly:
| Notification type | Condition | Severity | | --- | --- | --- | | ad_account_status | account_status_not_active | warning_or_critical | | ad_account_recharge | spend_cap_remaining_below_threshold | warning_or_critical | | page_unpublished | page_is_not_published | critical | | page_ads_restricted | page_is_not_promotion_eligible | critical | | page_no_advertise_access | no_connected_fb_user_has_advertise_access | warning | | fb_user_abnormal | fb_user_connection_not_active | warning_or_critical | | fb_user_disabled | create_pull_and_decision_are_all_disabled | warning | | fb_user_token_expiring | user_token_expires_within_threshold | warning |
Default thresholds: spend-cap remaining <= 50.0 account-currency major units or <= 10%; USER-token expiry <= 7 days; duplicate alert cooldown 3600 seconds. Product ownership is included when a saved product maps to the affected asset.
Delivery channels are Email, Feishu, and Telegram, filterable by exact notification_type (or all) and minimum severity. Webhooks do not stream spend/balance metrics and do not replace Insights pulls. Ad-account balance, Page restrictions, and FB User health come from the cached asset-health scan. effective_status events are live-read before AdsAgent emits the verified notification.
When a prepare returns category=launch_request_incomplete, code=adsagent_request_incomplete, and public invalid_fields, correct only those advertised fields and rerun the prepare tool once. Prepare creates no Meta object. If that one correction fails, the rule is redacted/unknown, or operator_review_required is returned, stop and preserve support_ref for operator handoff. Never reuse or automatically retry a confirm token or consequential write.
When confirm first returns code=mcp_meta_quota_deferred, recover only if request_sent=false, safe_to_retry=true, and operator_review_required=false. The server has already exhausted its bounded pre-send admission retries and did not send the Graph POST. Treat plan_recovery.action=stop_remaining_mutations as a circuit breaker: stop the entire mutation loop immediately and call no later confirm during the blocked window. Preserve completed_mutations, receipts, and support_refs, place only the current item in not_sent_mutations, leave later items in remaining_mutations, and set safe_resume_from to the current item. Honor the largest applicable retry_after_seconds plus jitter. After the window, re-prepare only unchanged not-sent and remaining entity/value pairs, show one fresh consolidated approval summary, and resume serially. Never repeat a completed mutation or reuse an old confirm token. Any sent or uncertain write must use operation recovery and must not be replayed.
QuickCreate prepare checks the final Page, Pixel, FB User status, Create switches, and Token before issuing approval, and the worker repeats the check before execution. On no_create_permission, direct the user to /dashboard/assets/fb-users to enable Create on an active connection, then prepare again. Never change customer permissions automatically.
QuickCreate modes stay server-owned. Use creative_source.media_mode=multi for a frozen 2-10 item media window, creative_source.mode=carousel for ordered 2-10 image cards per group, interest_pack_mode=random for prepare-time per-ad-set Audience Pack assignment, and append mode only with its matching target id. Never fan out or re-randomize these choices client-side, and never replace an append target after prepare.
QuickCreate requires destination.type=web|app. Web requires web_url and forbids app_id, store_url, and deep_link. App requires at least one of app_id, store_url, or deep_link and forbids web_url. Follow the machine-readable creation contract; do not infer destination type from a template or URL.
QuickCreate Append supports exactly append_mode=append-campaign with target_campaign_id, or append_mode=append-adset with target_adset_id. append-adset requires execution.campaign_count=1 and execution.adset_count=1 and inherits the existing parent budget. Never send append_mode=existing, existing_campaign_id, existing_adset_id, or product_ref. On adsagent_request_incomplete with public invalid_fields, correct only those fields and rerun prepare once; show the new approval summary and obtain fresh explicit approval before confirm. Never auto-confirm, change permissions, rotate accounts, or replay a confirm token.
After a delivery confirm, call the returned next_action exactly. The expected tool is overview_get_live_configs with typed entities plus mutation_ref; retry only while verification is pending. Verification is pinned to the FB user that performed the write; if that subject is unavailable, keep pending/not_available and never substitute another token or stored Insights. Use operations_get(mutation_ref) for a delivery-mutation receipt. For a task error meta_write_verification_pending or meta_write_verification_ambiguous, use operations_get_context(task_ref=...) and never repeat the write. Follow each returned failure's retry flags; only manual_new_task_allowed=true permits preparing a new task with fresh approval and a new confirm token. Use insights_query_consistent(consistency=require_fresh, after_mutation_ref=mutation_ref) only for post-write metrics: metrics_observed_after_mutation does not verify delivery configuration.
For a terminal create/copy task, inspect result.create_reconciliation before result.failures. Require reconciled=true before claiming every requested object is accounted for. Use creative_results to map each ad_name and available selection_key/selection_keys to its created ad_id. Treat configuration.source=approved_task_payload and live_verified=false as approved execution input, not live Meta configuration. A failed auxiliary adimages receipt with workflow_status=recovered_by_url_fallback has no final-Ad impact and never permits retry or a new task. operations_get_context(response_mode=compact) returns receipt totals, reconciliation, and anomalous receipts; request standard only when every receipt is needed. Report each bounded result.failures.items[] entry with its public ad_name, code, message, and next_action; raw Meta errors remain private. Never retry the unchanged write. If operator_review_required=true or unclassified_count>0, report the known items, stop, and preserve the task/support reference for operator review. A corrected manual retry must be a newly prepared task with fresh user approval.
Bulk Ad writes may be split into configurable, sequential AdsAgent chunks. This is a defensive reliability policy, not proof of a fixed Meta batch limit. Preserve acknowledged objects and receipts across chunks. Only an item with manual_new_task_allowed=true may enter a newly prepared task with fresh user approval; pending or ambiguous items must remain in operation recovery.
Copy routing:
- One source ad ->
copy_ad_quick_copy. - Campaign or ad set ->
copy_ad_clone_structure. - Repeat a previous task ->
campaigns_recreate_from_task. - Ask deep-post reuse versus fresh creative upload; never guess.
- Partnership/boosted-post sources require deep post reuse. Fresh copy is rejected during prepare before approval; show the bounded warning and do not retry with fresh.
- Copy contract v3 sets
creation_contract_version=3and explicitrequest_mode=single|grouped. Single mode usessource_ad_id,source_ad_account_id,target_ad_account_id,campaign_count,adset_count,ads_per_adset, andtarget_campaign_name. Grouped mode usesgrouped_plan.campaigns[].campaign_name,campaigns[].adsets[].adset_name, andcampaigns[].adsets[].ads[].source_ad_id; optional destination Ad names useads[].ad_name. Bare nestednameremains compatible only at the exact object path and conflicting values are rejected. Readadsagent://guide/name-contract; flattened Insights names, source snapshots, naming patterns, selectors, and destination names are distinct roles. The grouped tree supports 1-1-N, 1-N-1, custom layouts, deep/fresh, and same/cross-account copies as one paused-by-default resumable task. The first ad of the first ad set seeds campaign settings, and the first ad of every ad set seeds that ad set's settings; order them deliberately and verify eachsettings_source_ad_idin the approval summary. Show the complete summary, obtain one explicit approval, then pass itscgb_confirm_*token unchanged tocopy_ad_quick_copy_confirm. - Use
countries_overridefor explicit included countries. Useworldwide_override=trueplusexcluded_countries_overridefor worldwide-minus-country targeting, and read the frozengeo_targeting_overrideback before approval.
Machine-readable creation model and read-to-write mapping:
{
"adapter_mappings": {
"destination.app_id": "meta.promoted_object.application_id",
"destination.deep_link": "meta.creative.app_link",
"destination.store_url": "meta.promoted_object.object_store_url",
"destination.web_url": "meta.creative.link_url"
},
"creation_contract_version": 3,
"grouped_copy_canonical_example": {
"creation_contract_version": 3,
"grouped_plan": {
"ad_status": "PAUSED",
"adset_status": "PAUSED",
"campaign_status": "PAUSED",
"campaigns": [
{
"adsets": [
{
"ads": [
{
"ad_name": "<optional target ad name>",
"source_ad_id": "<source ad id>"
}
],
"adset_name": "<optional ad set name>"
}
],
"campaign_name": "<campaign name>"
}
],
"copy_mode": "deep",
"source_ad_account_id": "<source ad account id>",
"target_ad_account_id": "<target ad account id>"
},
"request_mode": "grouped"
},
"grouped_plan_canonical_example": {
"ad_status": "PAUSED",
"adset_status": "PAUSED",
"campaign_status": "PAUSED",
"campaigns": [
{
"adsets": [
{
"ads": [
{
"ad_name": "<optional target ad name>",
"source_ad_id": "<source ad id>"
}
],
"adset_name": "<optional ad set name>"
}
],
"campaign_name": "<campaign name>"
}
],
"copy_mode": "deep",
"source_ad_account_id": "<source ad account id>",
"target_ad_account_id": "<target ad account id>"
},
"path_scoped_fields": [
{
"accepted_legacy_aliases": [],
"canonical_field": "request_mode",
"conflict_policy": "must_match_single_or_grouped_payload_shape",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "copy_ad",
"sensitive_classification": "non_sensitive",
"value_type": "enum(single,grouped)"
},
{
"accepted_legacy_aliases": [
"ad_id"
],
"canonical_field": "source_ad_id",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "copy_ad.single",
"sensitive_classification": "business_identifier",
"value_type": "meta_ad_identifier"
},
{
"accepted_legacy_aliases": [
"name"
],
"canonical_field": "campaign_name",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 3,
"scoped_json_path": "grouped_plan.campaigns[]",
"sensitive_classification": "business_text",
"value_type": "non_blank_display_name"
},
{
"accepted_legacy_aliases": [
"name"
],
"canonical_field": "adset_name",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 3,
"scoped_json_path": "grouped_plan.campaigns[].adsets[]",
"sensitive_classification": "business_text",
"value_type": "optional_display_name"
},
{
"accepted_legacy_aliases": [
"name"
],
"canonical_field": "ad_name",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 3,
"scoped_json_path": "grouped_plan.campaigns[].adsets[].ads[]",
"sensitive_classification": "business_text",
"value_type": "optional_display_name"
},
{
"accepted_legacy_aliases": [
"ad_id"
],
"canonical_field": "source_ad_id",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "grouped_plan.campaigns[].adsets[].ads[]",
"sensitive_classification": "business_identifier",
"value_type": "meta_ad_identifier"
},
{
"accepted_legacy_aliases": [
"camp_num",
"num_campaigns"
],
"canonical_field": "campaign_count",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "copy_ad.single",
"sensitive_classification": "non_sensitive",
"value_type": "integer_count"
},
{
"accepted_legacy_aliases": [
"adgroup_count",
"adset_num",
"num_adsets"
],
"canonical_field": "adset_count",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "copy_ad.single",
"sensitive_classification": "non_sensitive",
"value_type": "integer_count"
},
{
"accepted_legacy_aliases": [
"ad_num",
"num_ads_per_adset"
],
"canonical_field": "ads_per_adset",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "copy_ad.single",
"sensitive_classification": "non_sensitive",
"value_type": "integer_count_per_parent"
},
{
"accepted_legacy_aliases": [
"ad_account_id"
],
"canonical_field": "source_ad_account_id",
"conflict_policy": "reject_if_values_differ_after_act_prefix_normalization",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "copy_ad",
"sensitive_classification": "business_identifier",
"value_type": "meta_ad_account_identifier"
},
{
"accepted_legacy_aliases": [
"target_account_id"
],
"canonical_field": "target_ad_account_id",
"conflict_policy": "reject_if_values_differ_after_act_prefix_normalization",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "copy_ad",
"sensitive_classification": "business_identifier",
"value_type": "meta_ad_account_identifier"
},
{
"accepted_legacy_aliases": [
"campaign_name_override",
"campaign_name"
],
"canonical_field": "target_campaign_name",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "copy_ad.single",
"sensitive_classification": "business_text",
"value_type": "optional_display_name"
},
{
"accepted_legacy_aliases": [
"country_override"
],
"canonical_field": "countries_override",
"conflict_policy": "canonical_array_wins_only_when_legacy_is_absent_or_equal",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "copy_ad.single",
"sensitive_classification": "business_targeting",
"value_type": "iso_3166_1_alpha_2_array"
},
{
"accepted_legacy_aliases": [
"adgroup_count"
],
"canonical_field": "adset_count",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create.execution",
"sensitive_classification": "non_sensitive",
"value_type": "integer_count"
},
{
"accepted_legacy_aliases": [
"budget_level"
],
"canonical_field": "budget.level",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create.execution",
"sensitive_classification": "non_sensitive",
"value_type": "enum(campaign,adset)"
},
{
"accepted_legacy_aliases": [
"budget_type"
],
"canonical_field": "budget.type",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create.execution",
"sensitive_classification": "non_sensitive",
"value_type": "enum(daily,lifetime)"
},
{
"accepted_legacy_aliases": [
"budget_amount"
],
"canonical_field": "budget.amount",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create.execution",
"sensitive_classification": "business_value",
"value_type": "positive_money_major_units"
},
{
"accepted_legacy_aliases": [
"bid_strategy"
],
"canonical_field": "bid.strategy",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create.execution",
"sensitive_classification": "non_sensitive",
"value_type": "bid_strategy"
},
{
"accepted_legacy_aliases": [
"bid_cap"
],
"canonical_field": "bid.cap",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create.execution",
"sensitive_classification": "business_value",
"value_type": "optional_positive_money_major_units"
},
{
"accepted_legacy_aliases": [
"campaign_status"
],
"canonical_field": "statuses.campaign",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create.execution",
"sensitive_classification": "non_sensitive",
"value_type": "enum(ACTIVE,PAUSED)"
},
{
"accepted_legacy_aliases": [
"adset_status"
],
"canonical_field": "statuses.adset",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create.execution",
"sensitive_classification": "non_sensitive",
"value_type": "enum(ACTIVE,PAUSED)"
},
{
"accepted_legacy_aliases": [
"ad_status"
],
"canonical_field": "statuses.ad",
"conflict_policy": "reject_if_values_differ",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create.execution",
"sensitive_classification": "non_sensitive",
"value_type": "enum(ACTIVE,PAUSED)"
},
{
"accepted_legacy_aliases": [],
"canonical_field": "destination.type",
"conflict_policy": "web_app_fields",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create",
"sensitive_classification": "non_sensitive",
"value_type": "enum(web,app)"
},
{
"accepted_legacy_aliases": [],
"canonical_field": "destination.web_url",
"conflict_policy": "distinct_from_store_url_and_deep_link",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create",
"sensitive_classification": "business_url",
"value_type": "http_url"
},
{
"accepted_legacy_aliases": [
"overrides.store_url"
],
"canonical_field": "destination.store_url",
"conflict_policy": "distinct_from_web_url_and_deep_link",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create",
"sensitive_classification": "business_url",
"value_type": "http_url"
},
{
"accepted_legacy_aliases": [
"overrides.deeplink"
],
"canonical_field": "destination.deep_link",
"conflict_policy": "distinct_from_web_url_and_store_url",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create",
"sensitive_classification": "business_url",
"value_type": "application_deep_link"
},
{
"accepted_legacy_aliases": [
"creative_names"
],
"canonical_field": "creative_source.creative_names",
"conflict_policy": "reject_multiple_source_modes",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create",
"sensitive_classification": "business_text",
"value_type": "creative_display_name_array"
},
{
"accepted_legacy_aliases": [
"partnership_rows"
],
"canonical_field": "creative_source.partnership_rows",
"conflict_policy": "reject_multiple_source_modes",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create",
"sensitive_classification": "business_payload",
"value_type": "partnership_row_array"
},
{
"accepted_legacy_aliases": [
"carousel_groups"
],
"canonical_field": "creative_source.carousel_groups",
"conflict_policy": "reject_multiple_source_modes",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create",
"sensitive_classification": "business_payload",
"value_type": "carousel_group_array"
},
{
"accepted_legacy_aliases": [
"name"
],
"canonical_field": "ad_name",
"conflict_policy": "reject_if_both_present",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create.creative_source.carousel_groups[]",
"sensitive_classification": "business_text",
"value_type": "non_blank_display_name"
},
{
"accepted_legacy_aliases": [],
"canonical_field": "app_id",
"conflict_policy": "adapter_maps_to_meta_application_id",
"deprecation_version": null,
"first_supported_version": 2,
"scoped_json_path": "campaigns_quick_create.destination",
"sensitive_classification": "business_identifier",
"value_type": "meta_application_identifier"
}
],
"quick_create_canonical_example": {
"ad_account_id": "<target ad account id>",
"creation_contract_version": 3,
"creative_source": {
"creative_names": [
"<creative name>"
],
"mode": "creative"
},
"destination": {
"app_id": null,
"deep_link": null,
"store_url": null,
"type": "web",
"web_url": "https://example.com"
},
"execution": {
"adset_count": 1,
"bid": {
"cap": null,
"strategy": "lowest_cost"
},
"budget": {
"amount": 50,
"level": "adset",
"type": "daily"
},
"campaign_count": 1,
"statuses": {
"ad": "PAUSED",
"adset": "PAUSED",
"campaign": "PAUSED"
}
},
"template_name": "<template name>"
},
"read_to_write_mappings": {
"reuse_source_ad_name_as_target_default": {
"read_field": "insights.ad_name",
"relation": "explicit_copy_default",
"write_field": "grouped_plan.campaigns[].adsets[].ads[].ad_name"
},
"reuse_source_adset_name_as_target_default": {
"read_field": "insights.adset_name",
"relation": "explicit_copy_default",
"write_field": "grouped_plan.campaigns[].adsets[].adset_name"
},
"reuse_source_campaign_name_as_target_default": {
"read_field": "insights.campaign_name",
"relation": "explicit_copy_default",
"write_field": "grouped_plan.campaigns[].campaign_name"
},
"select_insights_ad_as_copy_source": {
"read_field": "insights.ad_id",
"relation": "selected_source_reference",
"write_field": "grouped_plan.campaigns[].adsets[].ads[].source_ad_id"
}
},
"single_copy_canonical_example": {
"ads_per_adset": 1,
"adset_count": 1,
"campaign_count": 1,
"copy_mode": "deep",
"creation_contract_version": 3,
"request_mode": "single",
"source_ad_account_id": "<optional source ad account id>",
"source_ad_id": "<source ad id>",
"target_ad_account_id": "<optional target ad account id>",
"target_campaign_name": "<optional target campaign name>"
}
}6. MMP Reads
Use product-scoped MMP tools and pass channel_pid. Long supported windows are split by the server. For channel_pid=all, status=partial and complete=false mean at least one channel failed; report failed channels instead of silently returning a smaller total.
7B. B2B Agent Output Contract
Answer
Lead with the direct answer and server-computed totals. Use cleaned / aggregated results.
Scope
State product/account, date range, grouping, attribution, and completeness.
Results
Use a compact horizontal Markdown table. Do not use raw-row reads for normal business questions, and do not return only item counts when totals or summaries are available. Preserve effective statuses and public IDs.
Notes
Mention partial/missing scopes, artifact links, or retry state. Do not volunteer optimization advice unless requested.
7C. Optional Client Enhancements
AdsAgent Tri-Channel Skill Pack (Claude Code, Codex CLI, and other Agent-Skills clients)
This optional skill pack does not replace MCP. Do not pause or block hosted MCP setup for installation. After the MCP servers are configured and setup_get_status succeeds, present it as a post-setup reminder for Claude Code users, not a prerequisite. Do not ask the user for permission to install the skill pack before completing MCP setup. Skip this section for Grok, Notion, Cursor, and clients without plugin support. If uncertain whether plugins are supported, complete MCP setup first.
Claude commands:
claude plugin marketplace add adsagents/adsagent-ai-skills
claude plugin install adsagent-ai-skills@adsagent-ai-skills
claude plugin update --scope user adsagent-ai-skills@adsagent-ai-skills
claude plugin uninstall --scope local adsagent-ai-skills@adsagent-ai-skillsVersions below v0.7.0 are stale. adsagent-meta-ai-skills remains a compatibility alias. Install at both local and user scope only when the client explicitly supports those scopes.
Codex compatibility path:
git clone [email protected]:adsagents/adsagent-ai-skills.git ~/.codex/skills/adsagent-ai-skillsThe pack contains /adsagent-router, /meta-insights, /meta-copy, /adsagent-notifications, /google-ads-insights, and /tiktok-insights. Suggested first test in a fresh Claude Code session: list connected Meta products, Google Ads customers, and TikTok advertisers, then ask which scope's today data to inspect. Never use a hardcoded example account, product, or advertiser name.
9. MCP Client Reliability Contract (REQUIRED for onboarding pass)
Passing the MCP onboarding check requires a client-side retry/backoff implementation:
- The client MUST bound per-token concurrency to 4-6; it MUST NOT launch unbounded parallel calls.
- The client MUST use batch tools for multiple scopes.
- The client MUST honor
Retry-Afterplus jitter and retry serially. - Cancellation MUST release local leases and stop retries.
- The client MUST retry read-only/idempotent calls only; it MUST never automatically retry a confirm token or consequential write. The strict
mcp_meta_quota_deferredpre-send contract is a plan-level circuit breaker: stop before later confirms, wait, and re-prepare only the unchanged remainder under one fresh consolidated approval, never the old confirm. - The client MUST poll queued tasks with
task_refuntilterminal=true. - The client MUST treat profile top-level
complete=false, nativemeta.complete=false, missing scopes, partial channels,verification_pending, anddata_not_freshas incomplete data. - The client MUST NOT treat age-only freshness or an immediate write response as mutation verification.
9.2 Onboarding acceptance criteria
- Launch 20 parallel requests on the same token and prove local concurrency stays bounded while the server returns structured 429 for excess work.
- Simulate an MCP restart on legacy
/mcp, discard the stale session, call initialize again, and retry one read. - Prove
Retry-Afteris honored before retry. - Cache initialize/tools-list once per transport and refresh after a guide-version bump.
9.3 Reference implementation
MAX_CONCURRENCY = 6 # recommended range 4-6
def recover(response, pending_scopes):
data = response.get("data") or (response.get("error") or {}).get("data") or {}
subcode = data.get("subcode")
if subcode == "mcp_session_not_found":
assert data.get("action") == "discard_session_and_initialize"
return "reinitialize"
if subcode == "mcp_concurrency_limited":
return f"wait {response.headers['Retry-After']} + jitter"
if subcode == "mcp_fanout_detected":
return call_tool("insights_query_batch_overview", pending_scopes)
if response.get("error") == "dependency_unavailable":
return f"wait {response.headers['Retry-After']} + jitter"
return "do_not_retry"9.4 Why this is required
The 2026-05-12 slim incident combined unbounded client concurrency, repeated initialize/tools-list calls, and immediate retries. The result was avoidable saturation and 503s. This contract prevents that failure mode as customer volume grows.
10. Client recovery contracts (404 / 429 / 503)
10.1 Session-not-found (404)
For legacy /mcp, branch on structured fields, never message text:
{"error": {"data": {"subcode": "mcp_session_not_found", "action": "discard_session_and_initialize"}}}The action requires a transport-level reconnect, fresh initialize, fresh session ID, tool re-list, and one retry of the original read. Do not reuse the stale session ID. V2 is stateless and avoids this recovery path.
10.2 Concurrency caps (429) - two-layer
"mcp_concurrency_limited" is a transient admission signal. The per-token trigger is more than 8 simultaneous calls; the per-customer trigger is more than 20 simultaneous calls across tokens. Response header: Retry-After: 5. Wait plus jitter and retry one read at a time. More than one outstanding retry refills the same slots and is non-compliant.
10.2A Fan-out guard (429)
mcp_fanout_detected is a query-planning correction, not a transient retry. Stop the single-scope loop and follow data.recommended_tool plus data.recommended_mode for all pending scopes. The server recommends canonical insights_query_consistent scopes when the Agent Method Profile is live and native insights_query_batch_overview otherwise.
10.3 Full status-code branching table
| Status | Signal | Action | | --- | --- | --- | | 400/422 | invalid or missing input | Ask for the listed fields; never guess. | | 401/403 | "invalid_token" or scope failure | Read /.well-known/oauth-protected-resource, reconnect, or request the named scope; do not retry-loop. | | 404 | "mcp_session_not_found" on legacy | Run discard_session_and_initialize and retry one read once. | | 409/410 | stale/used confirm token | Re-prepare only after the user still wants the action. | | tool error | mcp_meta_quota_deferred + request_sent=false + safe_to_retry=true | Stop the entire plan before later confirms; preserve completed/current/later partitions; honor the largest Retry-After plus jitter; re-prepare only the unchanged remainder under one fresh consolidated approval. | | 429 | "mcp_concurrency_limited" / "mcp_fanout_detected" | Honor backoff, or follow the server-recommended tool/mode for fan-out. | | 503 | "dependency_unavailable" / server_overloaded | Honor backoff and retry one idempotent read. |
10.8 Client retry/backoff reference implementation
The parser handles both top-level data and JSON-RPC error.data:
def retry_after_seconds(payload, headers=None):
headers = {str(k).lower(): v for k, v in (headers or {}).items()}
candidates = [headers.get("retry-after")]
if isinstance(payload, dict):
candidates += [payload.get("retry_after"), payload.get("retry_after_seconds")]
for node in (payload.get("data"), (payload.get("error") or {}).get("data")):
if isinstance(node, dict):
candidates += [node.get("retry_after"), node.get("retry_after_seconds")]
for value in candidates:
try:
seconds = float(value)
except (TypeError, ValueError):
continue
if seconds > 0:
return seconds
return NoneNever print or persist bearer tokens, cookies, authorization headers, raw request bodies, or internal IDs in retry/audit logs.
MCP token identity & central-auth (Google Ads / TikTok)
The same central AdsAgent identity can route to Meta, Google Ads, and TikTok hosted MCP servers. Each server keeps its own tool catalog and platform authorization; a valid central token does not prove the linked ad-platform account is connected.