{
 "generated": "2026-09-07T03:24:36.115Z",
 "server": "mcp-metadata-server",
 "source": "https://mcp-server.metadata.io/mcp",
 "count": 160,
 "categories": {
  "account": 10,
  "analytics": 16,
  "audiences": 39,
  "campaigns": 27,
  "creative": 27,
  "extensions": 18,
  "search": 18,
  "utilities": 5
 },
 "risk": {
  "write": 56,
  "destructive": 39,
  "read": 65
 },
 "tools": [
  {
   "name": "connect_crm",
   "title": "Connect CRM",
   "category": "account",
   "risk": "write",
   "summary": "Get the OAuth redirect URL to connect a specific CRM system to the Metadata platform.",
   "description": "Get the OAuth redirect URL to connect a specific CRM system to the Metadata platform.\n\n                    USAGE INSTRUCTIONS FOR LLM:\n                    Use this tool to obtain the authorization URL that users need to visit to connect their CRM accounts.\n                    This tool returns a redirect URL that initiates the OAuth flow for the selected CRM system.\n\n                    WHEN TO USE THIS TOOL:\n                    - User asks to \"connect [CRM name]\"\n                    - User wants to \"add [CRM] integration\"\n                    - User needs to \"link [CRM] account\"\n                    - User wants to \"authorize [CRM] access\"\n                    - User asks \"how do I connect my Salesforce/HubSpot CRM?\"\n                    - User needs to \"set up [CRM] connection\"\n                    - After disconnecting a CRM and wanting to reconnect\n                    - User asks \"give me the link to connect [CRM]\"\n\n                    SUPPORTED CRM SYSTEMS:\n                    - salesforce: Connect Salesforce CRM\n                    - hubspot: Connect HubSpot CRM\n\n                    IMPORTANT WORKFLOW - MUST FOLLOW THESE STEPS:\n                    1. Call this tool to get the OAuth redirect URL\n                    2. Display the URL to the user clearly and instruct them to open it in their browser\n                    3. Explain that they need to complete the authorization process on the CRM's website\n                    4. IMPORTANT: Ask the user to return to the chat and confirm when they have completed the connection\n                    5. WAIT for the user's confirmation before proceeding with any other tasks\n                    6. Once the user confirms the connection is complete, acknowledge it and continue\n\n                    REQUIRED USER INTERACTION:\n                    - The user MUST visit the returned URL in their browser to complete OAuth authorization\n                    - The URL will redirect to the CRM's authorization page\n                    - After authorization, the user will be redirected back to Metadata platform\n                    - The connection will be active once the OAuth flow is completed successfully\n                    - YOU MUST WAIT for user confirmation before proceeding\n\n                    COMMON SCENARIOS:\n\n                    Scenario 1 - Connect Salesforce: \"I want to connect my Salesforce CRM\"\n                    → Call connect_crm with crm=\"salesforce\"\n                    → Show URL to user: \"Please open this URL to connect your Salesforce CRM: [URL]\"\n                    → \"After completing the authorization, please return here and let me know it's done so we can proceed.\"\n                    → WAIT for user confirmation\n\n                    Scenario 2 - Connect HubSpot: \"Connect HubSpot CRM\"\n                    → Call connect_crm with crm=\"hubspot\"\n                    → Show URL to user with instructions\n                    → WAIT for user confirmation\n\n                    Scenario 3 - Reconnect workflow: \"I want to reconnect my Salesforce with different credentials\"\n                    → First disconnect existing connection if needed\n                    → Call connect_crm with crm=\"salesforce\"\n                    → Provide the URL to user\n                    → WAIT for user confirmation\n\n                    RESPONSE FORMAT:\n                    Returns an object containing the redirect URL for OAuth authorization.\n                    Example: {\"redirectUrl\": \"https://login.salesforce.com/services/oauth2/authorize?...\"}\n\n                    EXAMPLE: \"Connect Salesforce\" → connect_crm(crm=\"salesforce\")",
   "inputSchema": {
    "type": "object",
    "properties": {
     "crm": {
      "type": "string",
      "enum": [
       "salesforce",
       "hubspot"
      ],
      "description": "The CRM system to connect. Must be one of: salesforce, hubspot"
     },
     "acknowledge_data_access": {
      "type": "boolean",
      "description": "Set true to confirm the person you are acting for asked to connect this CRM. The response is an OAuth authorization URL that starts a credential grant into their Metadata account. Without this the call is refused."
     }
    },
    "required": [
     "crm"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_topup_link",
   "title": "Create AI Credit Top-Up Link",
   "category": "account",
   "risk": "destructive",
   "summary": "Get a secure payment link the account owner can use to add AI credits.",
   "description": "Get a secure payment link the account owner can use to add AI credits.\n\n                    USAGE INSTRUCTIONS FOR LLM:\n                    Returns a URL to a page where a human can pay with the card already on file or\n                    enter a new one. You do NOT need to wait for something to fail first — ask for\n                    the link the moment more credit is the obvious next step. Share the URL with the\n                    account owner; you cannot pay on their behalf.\n\n                    WHEN TO USE THIS TOOL:\n                    - The balance is low or negative and the user wants to fix it now\n                    - An audience or other work is about to cost more than the balance covers\n                    - The user asks how to add credit, buy credits, or pay\n                    - Any tool refused for lack of credit and you did not get a link with it\n\n                    This tool keeps working when the balance is exhausted — being out of credit is\n                    exactly when it is needed.\n\n                    CALL THIS ONCE AND SHARE THE URL YOU GET. Each call issues a NEW link and\n                    INVALIDATES any previous one for the account, because only one live payment\n                    capability may exist at a time. So if you mint a second link after sharing the\n                    first, the URL the account owner is holding stops working. Do not call it again\n                    to \"check\" or \"refresh\" — re-read your earlier result instead.\n\n                    AFTER SHARING IT: payment settles ASYNCHRONOUSLY. Poll `get_credit_balance`\n                    every 20-30 seconds, not continuously, and resume work once the balance covers\n                    what you need.\n\n                    EXPECTED RESPONSE FORMAT:\n                    {\n                      \"url\": \"https://agency-stage.metadata.io/payment/<token>\",\n                      \"expires_at\": \"2026-08-11T18:58:00Z\",\n                      \"max_amount_usd\": 0,       // ceiling on this link; 0 means none\n                      \"account_id\": \"1234\",\n                      \"balance_usd\": -12.5       // the balance right now, for context\n                    }\n\n                    On failure returns {\"error\": \"...\"} — say that a link could not be created and\n                    point the user at the Metadata app to add credit instead. Never invent a URL.",
   "inputSchema": {
    "type": "object",
    "properties": {},
    "required": []
   }
  },
  {
   "name": "get_account_details",
   "title": "Get Account Details",
   "category": "account",
   "risk": "read",
   "summary": "Current user and account information.",
   "description": "Current user and account information.\n\nUSE FOR: \"Which account am I using?\", \"Who am I logged in as?\", \"What's my account name/status?\", \"What channels are enabled?\", \"What's my subscription/plan?\"\n\nReturns authenticated user details and associated account information.",
   "inputSchema": {
    "type": "object",
    "properties": {},
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "get_credit_balance",
   "title": "Get AI Credit Balance",
   "category": "account",
   "risk": "read",
   "summary": "Get the current Metadata AI credit balance for the account this token belongs to.",
   "description": "Get the current Metadata AI credit balance for the account this token belongs to.\n\n                    USAGE INSTRUCTIONS FOR LLM:\n                    Tool calls against this server consume AI credits. When the balance reaches zero\n                    or goes negative, every other tool on this server starts refusing with an\n                    OUT OF CREDITS error that carries a payment link. This tool is how you check\n                    the balance before that happens, and how you confirm a top-up has landed.\n\n                    WHEN TO USE THIS TOOL:\n                    - Another tool just failed with OUT OF CREDITS — check here before retrying\n                    - The account owner says they have paid, and you need to know if it landed yet\n                    - The user asks \"how many credits do I have?\" or \"what's my AI balance?\"\n                    - Before starting a long batch of tool calls, to confirm there is headroom\n\n                    IMPORTANT — AFTER A TOP-UP, POLL, DO NOT SPIN:\n                    Payment settles ASYNCHRONOUSLY. A card can be charged successfully and the\n                    balance still read as exhausted for a minute or two while the payment clears.\n                    Poll this tool every 20-30 seconds, not continuously. Resume the work you were\n                    doing only once `is_exhausted` is false.\n\n                    EXPECTED RESPONSE FORMAT:\n                    {\n                      \"account_id\": \"1234\",\n                      \"balance_usd\": 42.5,      // may be NEGATIVE — that is a real state\n                      \"updated_at\": \"2026-08-07T12:00:00\",\n                      \"is_exhausted\": false     // true means other tools will refuse\n                    }\n\n                    On failure returns {\"error\": \"...\"} — treat an unreadable balance as unknown,\n                    not as zero, and do not tell the user they are out of credits because of it.",
   "inputSchema": {
    "type": "object",
    "properties": {},
    "required": []
   }
  },
  {
   "name": "get_credit_transactions",
   "title": "Get AI Credit Transactions",
   "category": "account",
   "risk": "read",
   "summary": "Get recent AI credit movements for the account this token belongs to, newest first.",
   "description": "Get recent AI credit movements for the account this token belongs to, newest first.\n\n                    USAGE INSTRUCTIONS FOR LLM:\n                    Use this to explain WHERE credit went, or to show that a purchase was recorded.\n                    Each row is one movement: a negative `amountUsd` is a charge, a positive one is\n                    a top-up or an adjustment. `source` says what kind of usage it was (for example\n                    `direct_mcp` for tool calls made from an external client like this one,\n                    `image_generation`, `opik_ai_chat`, `maxio_topup` for a purchase).\n\n                    WHEN TO USE THIS TOOL:\n                    - User asks \"what have I been charged for?\" or \"where did my credits go?\"\n                    - User disputes a charge and wants the detail behind it\n                    - After a payment, to show the customer the top-up on their history\n                    - To find the `referenceId` of a purchase so you can pass it to\n                      `get_payment_status`\n\n                    EXPECTED RESPONSE FORMAT:\n                    {\n                      \"account_id\": \"1234\",\n                      \"transactions\": [\n                        {\"amountUsd\": -0.04, \"source\": \"direct_mcp\", \"description\": \"MCP tool: ...\",\n                         \"referenceId\": \"...\", \"createdAt\": \"...\", \"markup\": \"4.000000\"}\n                      ],\n                      \"total_elements\": 210, \"total_pages\": 9, \"page\": 0\n                    }",
   "inputSchema": {
    "type": "object",
    "properties": {
     "page": {
      "type": "integer",
      "description": "Zero-based page number. Defaults to 0 (most recent).",
      "minimum": 0
     },
     "size": {
      "type": "integer",
      "description": "Rows per page, 1-100. Defaults to 25.",
      "minimum": 1,
      "maximum": 100
     }
    },
    "required": []
   }
  },
  {
   "name": "get_integrations_status",
   "title": "Get Integrations Status",
   "category": "account",
   "risk": "read",
   "summary": "Get comprehensive integration connection status for all channels, martech platforms, and CRM systems in the Metadata platform.",
   "description": "Get comprehensive integration connection status for all channels, martech platforms, and CRM systems in the Metadata platform.\n\n                    USAGE INSTRUCTIONS FOR LLM:\n                    Use this tool to identify which platforms and services are connected or disconnected from the Metadata platform.\n                    This is essential for understanding the current state of integrations and diagnosing connectivity issues.\n\n                    WHEN TO USE THIS TOOL:\n                    - User asks \"which channels are connected?\"\n                    - User wants to check \"integration status\"\n                    - User asks \"is [platform name] connected?\" (e.g., \"is LinkedIn connected?\", \"is Facebook connected?\")\n                    - User wants to know \"what martech tools are integrated?\"\n                    - User asks about \"CRM connection status\" or \"which CRM is connected?\"\n                    - User asks \"show me all connected platforms\"\n                    - User wants to troubleshoot \"why campaigns aren't running\" (often related to disconnected channels)\n                    - User needs to \"check if [service] needs reconnection\"\n                    - User asks about \"available ad channels\" or \"active advertising platforms\"\n\n                    EXPECTED RESPONSE FORMAT:\n                    Returns object with three main sections: channels, martech, apps\n\n                    Each integration has: connected (bool), enabled (bool), type (string), businessAccountId,\n                    businessAccountName, currency, connectedAt (ISO timestamp), userFullName, errors (array)\n\n                    CHANNELS: Array of advertising platforms (FACEBOOK, INSTAGRAM, LINKEDIN, GOOGLE_ADS, MICROSOFT_ADS, TWITTER, REDDIT)\n                    MARTECH: Object with crm (array), marketingAutomation (array), googleAnalytics (object)\n                    - CRM types: SALESFORCE, HUBSPOT\n                    - Marketing Automation: HUBSPOT, PARDOT, ELOQUA, MARKETO\n                    APPS: Array of third-party integrations (G2, SLACK with channelName/channelId)\n\n                    USE CASES:\n                    1. Audit and Compliance:\n                       \"Generate a report of all connected marketing platforms for security review\"\n\n                    2. Integration Health Monitoring:\n                       \"Check if any integrations need token refresh or reconnection\"\n\n                    3. Capability Discovery:\n                       \"What advertising channels can I use based on current connections?\"\n\n                    COMMON SCENARIOS:\n\n                    Scenario 1 - Check channel: \"Is Facebook connected?\"\n                    → Parse response.channels, find type=\"FACEBOOK\", check connected=true\n\n                    Scenario 2 - List connected channels: \"Show me all connected advertising platforms\"\n                    → Parse response.channels, filter where connected=true\n\n                    Scenario 3 - Disconnected integrations: \"What needs reconnection?\"\n                    → Check all sections for connected=false\n\n                    Scenario 4 - CRM status: \"Is Salesforce connected?\"\n                    → Parse response.martech.crm, find type=\"SALESFORCE\", check connected=true\n\n                    OPTIONAL PARAMETERS: integrationType (CHANNEL/MARTECH/CRM), connectionStatus (CONNECTED/DISCONNECTED), platformName\n\n                    EXAMPLE: \"What's the status of my integrations?\" → Present summary of connected/disconnected by category",
   "inputSchema": {
    "type": "object",
    "properties": {
     "platformName": {
      "type": "string",
      "description": "Filter by specific platform name (e.g., 'Facebook', 'LinkedIn', 'Salesforce', 'HubSpot')"
     }
    },
    "additionalProperties": false
   }
  },
  {
   "name": "get_payment_status",
   "title": "Get Payment Status",
   "category": "account",
   "risk": "read",
   "summary": "Check whether a specific credit payment has settled onto the account's ledger.",
   "description": "Check whether a specific credit payment has settled onto the account's ledger.\n\n                    USAGE INSTRUCTIONS FOR LLM:\n                    A credit purchase is charged immediately but CREDITED asynchronously — the money\n                    settles at the billing provider, whose webhook then reaches Metadata and records\n                    the credit. So \"the card was charged\" and \"the balance is usable\" are separate\n                    moments, usually seconds to a couple of minutes apart. This tool tells them\n                    apart for one specific payment.\n\n                    WHEN TO USE THIS TOOL:\n                    - The account owner has paid and given you a payment/transaction reference\n                    - You want to confirm a specific purchase landed, rather than just seeing that\n                      the balance moved\n\n                    IF YOU DO NOT HAVE A REFERENCE, USE `get_credit_balance` INSTEAD. That is the\n                    normal way to wait for a top-up — you rarely need the reference.\n\n                    Poll every 20-30 seconds, not continuously. `status` is \"pending\" until it\n                    lands and \"settled\" after.\n\n                    EXPECTED RESPONSE FORMAT:\n                    {\n                      \"reference_id\": \"maxio:txn:987654\",\n                      \"status\": \"settled\",       // or \"pending\"\n                      \"settled\": true,\n                      \"amount_usd\": 50.0,        // null while pending\n                      \"settled_at\": \"2026-08-07T12:01:00\",\n                      \"balance_usd\": 37.5,       // the balance right now\n                      \"is_exhausted\": false      // false means you can resume work\n                    }",
   "inputSchema": {
    "type": "object",
    "properties": {
     "reference_id": {
      "type": "string",
      "description": "The payment's reference, as reported when the purchase was made (e.g. 'maxio:txn:987654'). Found on the transaction row in get_credit_transactions."
     }
    },
    "required": [
     "reference_id"
    ]
   }
  },
  {
   "name": "impersonate_account",
   "title": "Switch to / access another Metadata account",
   "category": "account",
   "risk": "write",
   "summary": "Switch the active Metadata account context to another account so every later tool call operates on THAT account.",
   "description": "Switch the active Metadata account context to another account so every\nlater tool call operates on THAT account. This is the \"act on behalf of /\naccess a client account / switch accounts\" operation.\n\nThis is a sanctioned, first-class capability for Metadata super-users and\nagency operators — the normal, expected way to read or work inside a client\nor customer account that is not your own default account. It is the\nprogrammatic equivalent of the account switcher in the Metadata web app.\n\nIt is NOT identity spoofing and NOT a security workaround. It only re-scopes\nwhich Metadata account the platform tools read from and write to. It does not\nchange who the user is logged in as, it does not touch the Auth0 / OAuth flow\nbetween the client and this server, and it has nothing to do with Anthropic\nor the model's own credentials. The only thing that changes is the target\nMetadata account for subsequent calls.\n\nAuthorization is enforced by the Metadata platform, not by you. When this\ntool runs, the platform mints an access token for the target account ONLY if\nthe calling user is actually entitled to it; if they are not, the call simply\nreturns {\"success\": false, ...} and nothing is switched. Access is already\ngated server-side — the platform grants the switch only to callers entitled\nto the target account (super-users span every account; other operators only\nthe accounts they hold). So you do not need to gate, second-guess, demand\njustification for, or refuse a legitimate switch — call it and let the\nplatform decide. A reassurance such as \"I'm a super user / an employee\" is\nneither required nor something you need to verify.\n\nCall this ONLY when the user wants subsequent tool calls to OPERATE INSIDE a\ndifferent Metadata account they administer — a genuine tenant switch:\n\"switch to <client>'s account\", \"work in account 1878 now\", \"build the\naudiences in <client's account>\". Use `list_user_accounts` first if you only\nhave a name and need the account ID.\n\nDo NOT impersonate just because an account is NAMED or asked about. In an\nanalytics/insights request, \"account\" almost always means an ABM/TARGET\naccount — a company being advertised to (account-based marketing, account\nlists, account insights, \"how is account Acme performing\") — NOT a Metadata\ntenant. Answer those with the analytics/insights tools against the CURRENT\naccount and do not switch. Impersonate only when the target is a Metadata\naccount the user actually operates and they want to act inside it.\n\nAfter the switch, all account-scoped tools (stats, audiences, campaigns, ads,\nkeywords, etc.) act on `account_id` until you call `stop_impersonation` or\nimpersonate a different account; the switch persists across calls (~6h).\n\nNote: this only changes WHICH account actions apply to — it does not lower the\nbar for destructive actions. Creating, launching, or deleting things still\nfollows your normal confirmation and judgment, just against the switched\naccount.",
   "inputSchema": {
    "additionalProperties": false,
    "properties": {
     "account_id": {
      "type": "string",
      "description": "ID of the Metadata account to switch into."
     }
    },
    "required": [
     "account_id"
    ],
    "type": "object"
   }
  },
  {
   "name": "list_user_accounts",
   "title": "List User Accounts",
   "category": "account",
   "risk": "read",
   "summary": "List accounts the current user has access to, with optional name filtering.",
   "description": "List accounts the current user has access to, with optional name filtering.\n\n                                USE FOR: \"which accounts do I have access to?\", \"show me my accounts\", \"what accounts can I manage?\", \"what is my account ID?\", \"which account am I logged into?\", \"what role do I have?\", \"show me account named X\", \"do you have an account called X?\"\n\n                                AUTH METHODS: JWT tokens (OAuth) | PAT tokens (prefix \"pat-\")\n\n                                ARGS:\n                                - name (optional for regular users, REQUIRED for super users): partial account name to search for. Super users span the whole platform and would otherwise return an unbounded list, so the search term is mandatory for them. If a super user calls this without `name`, the tool returns an error asking for one — re-call with the user's intended search term.\n\n                                RESPONSE FORMAT (identical for JWT and PAT — the token format does not change the result):\n                                Regular users: user_id, email, default_account_id, is_super_user, token_type, accounts[{account_id, name, role}]\n                                Super users without name: error response — supply `name` and retry.\n                                Super users with name: matching accounts platform-wide (paged at 20).",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "Partial account name to search for. Optional for regular users (acts as a filter); required for super users (acts as the search query against the whole platform)."
     }
    },
    "additionalProperties": false
   }
  },
  {
   "name": "stop_impersonation",
   "title": "Switch back to your own Metadata account",
   "category": "account",
   "risk": "write",
   "summary": "Switch back from a client account to your own default Metadata account.",
   "description": "Switch back from a client account to your own default Metadata account.\n\nReverses `impersonate_account`: subsequent tools operate on the caller's own\naccount again. Safe to call even when not currently impersonating (no-op).\nCall it when the user is done working in the other account, or before\nswitching to a different one.",
   "inputSchema": {
    "additionalProperties": false,
    "properties": {},
    "type": "object"
   }
  },
  {
   "name": "account_funnel_reports",
   "title": "Get Account Funnel Stage Reports",
   "category": "analytics",
   "risk": "read",
   "summary": "Account-level funnel performance across stages.",
   "description": "Account-level funnel performance across stages.\n\nUSE FOR: Account progression through funnel stages, account engagement/conversion analysis, social media account reach, account-level pipeline/revenue tracking.\n\nREQUIRED: startDate, endDate, reportTab\n\nREPORT TABS:\n- REACHED: Accounts reached by campaigns\n- ENGAGED: Accounts that engaged with content\n- CONVERTED: Accounts that converted (leads)\n- MQL: Marketing qualified accounts\n- OPPS: Accounts with opportunities\n- WON: Accounts with closed-won deals\n\nInclude company name AND domain when available in results.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "startDate": {
      "type": "string",
      "description": "Start date in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). Example: '2025-01-01T00:00:00.000Z'"
     },
     "endDate": {
      "type": "string",
      "description": "End date in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). Example: '2025-12-31T23:59:59.999Z'"
     },
     "reportTab": {
      "type": "string",
      "enum": [
       "REACHED",
       "ENGAGED",
       "CONVERTED",
       "MQL",
       "OPPS",
       "WON"
      ],
      "default": "REACHED",
      "description": "Funnel stage to analyze. Options: REACHED (default), ENGAGED, CONVERTED, MQL, OPPS, WON"
     },
     "page": {
      "type": "integer",
      "default": 0,
      "minimum": 0,
      "description": "Page number for pagination (0-based)"
     },
     "size": {
      "type": "integer",
      "default": 15,
      "minimum": 1,
      "maximum": 10000,
      "description": "Number of results per page. Default: 15, Max: 10000 — raise it to pull the full funnel set in one call."
     },
     "sort": {
      "type": "string",
      "default": "impressions,desc",
      "description": "Sort criteria. Examples: 'impressions,desc' (default), 'engagement,desc', 'conversions,desc'"
     },
     "type": {
      "type": "string",
      "enum": [
       "SOCIAL"
      ],
      "default": "SOCIAL",
      "description": "Report type. Currently only SOCIAL is supported"
     }
    },
    "required": [
     "startDate",
     "endDate",
     "reportTab"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "account_level_stats",
   "title": "Get Account-Level Statistics",
   "category": "analytics",
   "risk": "read",
   "summary": "Account-level statistics and channel breakdown for a timeframe.",
   "description": "Account-level statistics and channel breakdown for a timeframe.\n\nUSE FOR: General account stats by period, channel breakdown, high-level metrics (ROI, spending, leads, opportunities), custom date ranges.\nNOT FOR: Campaign-level info (use experiment_performance_stats), specific experiments (use get_experiments).\n\nCHANNELS: FACEBOOK, LINKEDIN, GOOGLE ADS, ALL\n\nTIMEFRAME LOGIC:\n- 'quarter'/'Q1-Q4' → timeframe='QUARTER'\n- 'week'/'weekly' → timeframe='WEEK'\n- ≤3 months → timeframe='MONTH'\n- >3 months → timeframe='QUARTER'\n- Default: timeframe='MONTH'\n\nCUSTOM DATE RANGE: Provide ONLY startDate + endDate (NO timeframe). Format: ISO 8601 (YYYY-MM-DDTHH:mm:ss.sssZ)\n\nMETRICS: impressions, clicks, spent, leads, mqls | opens, sends, actionClicks (CONVO/MESSAGE ads) | triggeredOpps, triggeredOppsAmount, influencedOpps, influencedOppsAmount | triggeredClosedWonOpps, triggeredClosedWonOppsAmount | roiInfluencedOpp, roiTriggeredOpp | cpc, cpl, cpm, ctr, conversionRate | costPerOpen, costPerSend (CONVO/MESSAGE ads) | customFields array\n\nCONVO/MESSAGE AD CAVEAT: opens, sends, actionClicks (and their per-cost derivatives) are the success metrics for CONVO and MESSAGE (LinkedIn message) ad types. clicks/ctr/cpc are typically 0 for these ads — do not treat that as \"no performance\". If account-level data shows high spend with 0 clicks, drill into experiment_performance_stats or get_ad_details to confirm ad type before drawing conclusions.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "timeframe": {
      "type": "string",
      "enum": [
       "QUARTER",
       "WEEK",
       "YEAR"
      ],
      "description": "Timeframe for data aggregation. MONTH=monthly breakdown, QUARTER=quarterly breakdown, WEEK=weekly breakdown, YEAR=yearly breakdown. DO NOT use this parameter when providing startDate and endDate."
     },
     "startDate": {
      "type": "string",
      "description": "Start date for custom date range in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). Must be used together with endDate. Do NOT provide timeframe when using this. Example: 2025-07-01T00:00:00.000Z"
     },
     "endDate": {
      "type": "string",
      "description": "End date for custom date range in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). Must be used together with startDate. Do NOT provide timeframe when using this. Example: 2025-09-30T00:00:00.000Z"
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "account_list_performance",
   "title": "Get Account List Performance",
   "category": "analytics",
   "risk": "read",
   "summary": "Account list information with engagement metrics and statistics.",
   "description": "Account list information with engagement metrics and statistics.\n\nUSE FOR: Account list performance/engagement, ABM list effectiveness, account journey metrics (reached, engaged, converted, qualified), account list pipeline/opportunities, comparing account lists, conversion funnel analysis.\nNOT FOR: Campaign-level details (use experiment_performance_stats), individual experiment performance (use performance_metrics), budget group analysis (use budget_group_performance).\n\nCHANNELS: FACEBOOK, LINKEDIN, GOOGLE_ADS, INSTAGRAM (comma-separated)\n\nMETRICS: accounts, reachedAccounts, engagedAccounts, convertedAccounts | qualifiedAccounts, influencedOppsAccounts, triggeredOppsAccounts, wonAccounts | impressions, clicks, leads, mqls | influencedOpps, influencedAmount, triggeredOpps, triggeredAmount | id, name, lastUpdatedDate\n\nReturns paginated response with totalElements, totalPages, and data array.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "page": {
      "type": "integer",
      "default": 0,
      "minimum": 0,
      "description": "Page number for pagination (0-based). Example: page=0 for the first page."
     },
     "size": {
      "type": "integer",
      "default": 25,
      "minimum": 1,
      "maximum": 10000,
      "description": "Number of results per page. Default: 25; raise it (up to 10000) to pull every account list in one call."
     },
     "sort": {
      "type": "string",
      "description": "Sorting criteria. Examples: 'triggeredAmount,desc', 'engagedAccounts,desc', 'influencedAmount,desc'. Format: field,direction where direction is 'asc' or 'desc'."
     },
     "channels": {
      "type": "string",
      "description": "Comma-separated channel names (e.g., 'LINKEDIN,FACEBOOK,INSTAGRAM,GOOGLE_ADS'). Leave empty for all channels."
     },
     "startDate": {
      "type": "string",
      "description": "Start date in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). Example: 2025-01-01T00:00:00.000Z. Optional for custom date ranges."
     },
     "endDate": {
      "type": "string",
      "description": "End date in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). Example: 2025-01-31T23:59:59.999Z. Optional for custom date ranges."
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "budget_group_performance",
   "title": "Get Budget Group Performance",
   "category": "analytics",
   "risk": "read",
   "summary": "Performance statistics for a specific budget group by name.",
   "description": "Performance statistics for a specific budget group by name.\nIMPORTANT: Accepts budget group NAME directly - no need to look up ID first. Do NOT call get_budget_group before this tool.\n\nUSE FOR: Budget group CPL/ROI/Pipeline/MQL analysis, spending and conversion metrics, experiment performance within budget group, efficiency analysis.\nNOT FOR: Comparing ALL budget groups (use list_budget_groups), individual experiments without budget group context (use experiment_performance_stats), account-level aggregates (use account_level_stats), budget group config WITHOUT performance (use get_budget_group).\n\nHOW IT WORKS: Takes name → looks up ID → fetches experiments tagged with budget group → returns detailed performance.\n\nMETRICS RETURNED: spent, impressions, clicks | leads, mqls, conversions | ctr, cpl, cpc, cpm, conversionRate, mqlRate | oppsAmount, triggeredAmount, totalOpps, totalTriggered | name, status, channel, dates\n\nDATE HANDLING: No dates = all-time data. Supports ISO 8601 date ranges.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "budget_group_name": {
      "type": "string",
      "description": "Name of the budget group to analyze (required). The tool will automatically look up the budget group ID. Example: 'Brand Awareness', 'ToFu', 'Lead Gen Q4'"
     },
     "startDate": {
      "type": "string",
      "format": "date",
      "description": "Start date for analysis in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.SSSZ, e.g., '2025-01-01T00:00:00.000Z'). Optional - if not provided, no start date filter is applied."
     },
     "endDate": {
      "type": "string",
      "format": "date",
      "description": "End date for analysis in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.SSSZ, e.g., '2025-12-15T23:59:59.999Z'). Optional - if not provided, no end date filter is applied."
     },
     "excludeExperimentName": {
      "type": "string",
      "description": "Experiment name to exclude from results. Useful for filtering out specific experiments from the analysis."
     },
     "page": {
      "type": "integer",
      "default": 0,
      "minimum": 0,
      "description": "Page number for pagination (0-based). Use for paginating through large result sets."
     },
     "size": {
      "type": "integer",
      "default": 10,
      "minimum": 1,
      "maximum": 10000,
      "description": "Number of experiments per page. Default is 10. Use larger values (up to 10000) for comprehensive analysis — pull the whole set rather than a sample."
     },
     "sort": {
      "type": "string",
      "default": "experimentId,name,asc",
      "description": "Sort criteria for experiments. Format: field,field,direction. Examples: 'experimentId,name,asc', 'spent,desc', 'cpl,asc'"
     }
    },
    "required": [
     "budget_group_name"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "deep_funnel_stats",
   "title": "Get Deep Funnel Analysis Statistics",
   "category": "analytics",
   "risk": "read",
   "summary": "Comprehensive funnel conversion analysis: funnel conversions, ROI statistics, experiment performance.",
   "description": "Comprehensive funnel conversion analysis: funnel conversions, ROI statistics, experiment performance.\n\nUSE FOR: Funnel conversion analysis, stage progression, ROI metrics, pipeline attribution, experiment performance within funnels, conversion rate optimization.\n\nREQUIRED: funnel_id, date range (YYYY-MM-DD to YYYY-MM-DD format)\n\nOPPORTUNITY MODELS: ALL (default), TRIGGERED, INFLUENCED",
   "inputSchema": {
    "type": "object",
    "properties": {
     "funnel_id": {
      "type": "integer",
      "description": "The funnel ID to analyze. Example: 35"
     },
     "startDate": {
      "type": "string",
      "description": "Start date in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). Example: '2025-01-01T00:00:00.000Z'"
     },
     "endDate": {
      "type": "string",
      "description": "End date in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). Example: '2025-12-31T23:59:59.999Z'"
     },
     "opportunityModel": {
      "type": "string",
      "enum": [
       "ALL",
       "TRIGGERED",
       "INFLUENCED"
      ],
      "default": "ALL",
      "description": "Opportunity attribution model. Options: ALL (default), TRIGGERED, INFLUENCED"
     }
    },
    "required": [
     "funnel_id"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "demographic_country_stats",
   "title": "Get Demographic Country Statistics",
   "category": "analytics",
   "risk": "read",
   "summary": "Performance metrics grouped by Country.",
   "description": "Performance metrics grouped by Country.\n\nUSE FOR: Performance by country/geography, country-level spending/conversions, geographic comparison, international campaign effectiveness, regional ROI/pipeline.\nNOT FOR: Account-level aggregates (use account_level_stats), campaign-level details (use experiment_performance_stats), ingredient-level (use performance_metrics).\n\nCHANNELS: FACEBOOK, LINKEDIN, GOOGLE_ADS, INSTAGRAM (comma-separated for multiple)\n\nTIMEFRAME: Supports standard (this/last quarter/month/week/year), period-based (today, yesterday), custom ranges (\"last two months\", \"last 20 days\"). ISO 8601 format. Default: current month.\n\nMETRICS: spent, impressions, clicks, leads, mqls | ctr, cpl, cpc, cpm, conversionRate, formConversionRate, mqlRate | totalOpps, totalTriggered, oppsAmount, triggeredAmount | cpMql, cpOppInfluenced, cpOppTriggered | channel, lastConvertedDate",
   "inputSchema": {
    "type": "object",
    "properties": {
     "fromMonth": {
      "type": "string",
      "description": "Start date in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). Example: 2025-01-01T00:00:00.000Z. Required for custom date ranges."
     },
     "toMonth": {
      "type": "string",
      "description": "End date in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). Example: 2025-01-31T23:59:59.999Z. Required for custom date ranges."
     },
     "channels": {
      "type": "string",
      "description": "Comma-separated channel names (e.g., 'LINKEDIN,FACEBOOK,INSTAGRAM,GOOGLE_ADS'). Leave empty for all channels."
     },
     "page": {
      "type": "integer",
      "default": 0,
      "minimum": 0,
      "description": "Page number for pagination (0-based). Example: page=0 for the first page."
     },
     "size": {
      "type": "integer",
      "default": 250,
      "minimum": 1,
      "maximum": 10000,
      "description": "Number of results per page. Default: 250; raise it (up to 10000) to pull every country in one call."
     },
     "sort": {
      "type": "string",
      "default": "spend,desc",
      "description": "Sorting criteria. Examples: 'spend,desc' (default), 'leads,desc', 'cpl,asc', 'impressions,desc'. Format: field,direction where direction is 'asc' or 'desc'."
     },
     "groupType": {
      "type": "string",
      "enum": [
       "COUNTRY"
      ],
      "default": "COUNTRY",
      "description": "Grouping type - currently only COUNTRY is supported for geographic breakdown."
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "experiment_performance_stats",
   "title": "Get Experiment Performance Statistics",
   "category": "analytics",
   "risk": "read",
   "summary": "Experiment-level performance statistics from Metadata.io Experiments API.",
   "description": "Experiment-level performance statistics from Metadata.io Experiments API.\n\nUSE FOR:\n- Experiment-level metrics (spent, leads, impressions, clicks, cpl, ctr, mql)\n- Campaign comparisons (A vs B, time periods)\n- Triggered/influenced opportunities analysis\n- Pipeline opportunity analysis from campaigns\n- Campaign ingredients (audience size, channel, audience/ad/offer used)\n- ROI analysis, trend analysis, top performers\n- Creative usage by experiments\n- Filtering by experiment/campaign names\n- Filtering by launch status via launchedExperimentStatuses (FAILED / DISCONNECTED experiments are excluded by default — see that parameter)\n\nNOT FOR:\n- Experiment pacing status (\"which experiments are underpacing\")\n- Ad-level triggered opportunities\n- Ingredient-level analysis (use performance_metrics)\n\nRETURNED DATA: spent, clicks, impressions, leads, mqls, cpl, cpc, cpm, ctr | opens, sends, actionClicks, costPerOpen, costPerSend (CONVO/MESSAGE ads) | adTypes (list of ad types in this experiment) | triggeredAmount, oppsAmount, cpMql, mqlRate, conversionRate | experimentName, campaignName, audienceName, offerName, adName | audienceTypes, audienceSize, channel, goal | imageLibraryName | startDate, endDate, pacing, quarterIndex\n\nCONVO/MESSAGE AD CAVEAT: when adTypes contains CONVO or MESSAGE, success is measured by opens, sends, and actionClicks (and costPerOpen / costPerSend), NOT clicks/ctr/cpc. An experiment with $100K+ spend and 0 clicks where adTypes includes CONVO can be a top performer — assess on the right metric. To rank conversational performance explicitly, use sort='actionClicks,desc' or sort='opens,desc' instead of the lead-gen defaults.\n\nLINK FORMAT: /hub/advertise/experiments?name={wizExperimentName}\n\nRULES:\n- 'cpl2Score,desc' for Lead Gen/unspecified, 'cpc2Score,desc' for Brand Awareness\n- 'triggeredAmount,desc' for ROI/pipeline, 'oppsAmount,desc' for influenced pipeline\n- size=1 for 'top' singular, size=requested for 'top X', size=50 for plural, size=15 default\n- failed/non-launched experiments are excluded by default; pass launchedExperimentStatuses=['Failed'] ONLY when the user explicitly asks about failures",
   "inputSchema": {
    "type": "object",
    "properties": {
     "metrics": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "List of metrics for secondary ordering when question involves multiple metrics (e.g., ['cpl', 'ctr', 'leads']). Only include when analyzing more than one metric simultaneously. Do NOT include for single metric questions."
     },
     "minSpend": {
      "type": "number",
      "default": 1,
      "minimum": 0,
      "description": "Minimum spend threshold. REQUIRED for all campaigns except Brand Awareness. Use for Lead Generation and when campaign type is unspecified."
     },
     "minLeads": {
      "type": "integer",
      "default": 1,
      "minimum": 0,
      "description": "Minimum leads threshold. REQUIRED for Lead Generation campaigns. Do NOT use for Brand Awareness campaigns."
     },
     "minCpl": {
      "type": "integer",
      "default": 1,
      "minimum": 0,
      "description": "Minimum cost per lead threshold (whole dollars). REQUIRED for Lead Generation campaigns. Do NOT use for Brand Awareness campaigns."
     },
     "minAudienceSize": {
      "type": "integer",
      "default": 1,
      "minimum": 0,
      "description": "Minimum audience size to filter experiments"
     },
     "minClicks": {
      "type": "integer",
      "default": 1,
      "minimum": 0,
      "description": "Minimum clicks threshold. REQUIRED for Brand Awareness campaigns only. Do NOT use for Lead Generation campaigns."
     },
     "minCpc": {
      "type": "integer",
      "default": 1,
      "minimum": 0,
      "description": "Minimum cost per click threshold (whole dollars). Do NOT use for Lead Generation or unspecified campaign types."
     },
     "size": {
      "type": "integer",
      "default": 10,
      "minimum": 1,
      "maximum": 10000,
      "description": "Number of results to return. RULES: size=1 for 'top' singular questions, size=requested number for 'top X' questions, size=50 for plural questions (e.g. 'campaigns'), size=30+ for multiple metrics analysis, size=15 as default when not specified. Raise it freely for full-dataset analytics — there is no small ceiling."
     },
     "sort": {
      "type": "string",
      "default": "cpl2Score,desc",
      "description": "Sorting parameter. CRITICAL RULES: 'cpl2Score,desc' for Lead Generation or unspecified campaigns, 'cpc2Score,desc' for Brand Awareness, 'triggeredAmount,desc' for ROI/pipeline questions, 'oppsAmount,desc' for influenced pipeline, 'conversionRate,desc' for conversion rate, 'internalStatus,desc' for active experiments, use specific metric,desc when asked (e.g. 'impressions,desc', 'mqls,desc')."
     },
     "goals": {
      "type": "string",
      "enum": [
       "CPL",
       "CTR"
      ],
      "description": "Campaign goal type. RULES: 'CPL' for Lead Generation campaigns, 'CTR' for Brand Awareness campaigns. Do NOT include for unspecified campaign types. When multiple metrics involved, use only the first metric's corresponding goal."
     },
     "name": {
      "oneOf": [
       {
        "type": "string"
       },
       {
        "type": "array",
        "items": {
         "type": "string"
        }
       }
      ],
      "description": "Experiment name filter, matched as ONE substring per request. Use 'name' field (NOT experimentName) for specific experiment questions. Can be a single string or an array of strings for multiple experiments; an array is queried one name per request and the results are merged. Do NOT add additional parameters when filtering by specific experiment name."
     },
     "campaignName": {
      "oneOf": [
       {
        "type": "string"
       },
       {
        "type": "array",
        "items": {
         "type": "string"
        }
       }
      ],
      "description": "Campaign name filter. Use when question is about specific campaign(s). Can be single string or array ['campaign1', 'campaign2'] for multiple campaigns. Do NOT include additional parameters when filtering by specific campaign name."
     },
     "audienceName": {
      "type": "string",
      "description": "Filter by target audience name (e.g., 'WTS EMEA Jan25_EMEAPitchbook')"
     },
     "offerName": {
      "type": "string",
      "description": "Filter by specific offer name (e.g., 'AMER_ZO_EN_HBR Reimagining Work'). Use this field when question mentions specific offers."
     },
     "adName": {
      "type": "string",
      "description": "Filter by specific ad name (e.g., 'Sifted_WorkShift_Ad5_Beige'). Use 'adName' field when question asks about specific ad names."
     },
     "audienceTypes": {
      "type": "string",
      "description": "Comma-separated list of audience types (e.g., 'Spotlight Retargeting Contacts (Dynamic),Technographic (Aberdeen)')"
     },
     "imageLibraryName": {
      "type": "string",
      "description": "Filter by creative/image name (e.g., 'JP - Square-Ad15.png'). Use 'imageLibraryName' field when question asks about specific creative names."
     },
     "offerTypes": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "LP",
        "LG"
       ]
      },
      "description": "Offer types filter. RULES: ['LP'] for lead gen forms/landing pages questions, ['LP', 'LG'] as default when offer types mentioned. LP=Landing Pages, LG=Lead Gen forms. Should NOT be empty when included."
     },
     "channels": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "LINKEDIN",
        "FACEBOOK",
        "INSTAGRAM",
        "GOOGLE_ADS",
        "REDDIT",
        "MICROSOFT_ADS"
       ]
      },
      "description": "Marketing channels to include"
     },
     "visibilities": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "VISIBLE",
        "HIDDEN",
        "ARCHIVED"
       ]
      },
      "description": "Visibility status filter"
     },
     "launchedExperimentStatuses": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "Active",
        "WithoutSpend",
        "Paused",
        "Completed",
        "Failed",
        "Disconnected"
       ]
      },
      "description": "Filter experiments by launch status. DEFAULT when omitted: successfully-launched experiments only (Active, WithoutSpend, Paused, Completed) — Failed and Disconnected are EXCLUDED, because a failed/non-launched experiment has 0 leads and is a non-starter, not a 'bottom performer'. Pass values explicitly to override: ['Active'] for live only, or ['Failed'] / ['Failed','Disconnected'] when the user explicitly asks which experiments failed."
     },
     "timeFrame": {
      "type": "string",
      "enum": [
       "WEEK",
       "QUARTER"
      ],
      "description": "Timeframe aggregation. CRITICAL RULES: Use ONLY for WEEK or QUARTER questions. NEVER use 'YEAR' - use startDate/endDate instead. If timeFrame is included, DO NOT add startDate or endDate parameters under any circumstances."
     },
     "startDate": {
      "type": "string",
      "format": "date-time",
      "description": "Start date in ISO 8601 format (e.g., '2024-10-18T00:00:00.000Z'). CRITICAL: Do NOT include if timeFrame parameter is used. Use last year of data if period cannot be inferred from question. For specific campaigns/experiments without timeframe, omit this parameter."
     },
     "endDate": {
      "type": "string",
      "format": "date-time",
      "description": "End date in ISO 8601 format. Must end on last hour of date (e.g., '2024-11-21T23:59:59.999Z'). CRITICAL: Do NOT include if timeFrame parameter is used. For specific campaigns/experiments without timeframe, omit this parameter."
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "get_account_opportunities_insights",
   "title": "Get Account Opportunities Insights",
   "category": "analytics",
   "risk": "read",
   "summary": "Get opportunities for domain(s) with status filtering and pagination.",
   "description": "Get opportunities for domain(s) with status filtering and pagination.\n\nSTATUS VALUES: NO_OPPS, OPEN, CLOSED_WON, CLOSED_LOST\n\nSORT OPTIONS: lastTouchDate (default), status, name, amount\n\nPAGINATION: Use small page sizes (25-50) with sequential calls.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "domains": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "minItems": 1,
      "description": "Domain strings. E.g., ['metadata.io']"
     },
     "status": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "NO_OPPS",
        "OPEN",
        "CLOSED_WON",
        "CLOSED_LOST"
       ]
      },
      "description": "Filter by status. Omit for all."
     },
     "startDate": {
      "type": "string",
      "description": "ISO 8601 format. E.g., '2025-01-01T00:00:00.000Z'"
     },
     "endDate": {
      "type": "string",
      "description": "ISO 8601 format."
     },
     "page": {
      "type": "integer",
      "minimum": 0,
      "default": 0,
      "description": "0-based page number."
     },
     "size": {
      "type": "integer",
      "minimum": 1,
      "maximum": 10000,
      "default": 25,
      "description": "Items per page (default 25, max 10000). Raise it to pull the full set in one call."
     },
     "sort": {
      "type": "string",
      "default": "lastTouchDate",
      "enum": [
       "lastTouchDate",
       "status",
       "name",
       "amount"
      ],
      "description": "Sort field. Default: lastTouchDate"
     }
    },
    "required": [
     "domains"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "get_account_summary_insights",
   "title": "Get Account Summary Insights",
   "category": "analytics",
   "risk": "read",
   "summary": "Get comprehensive account details for specific domain(s).",
   "description": "Get comprehensive account details for specific domain(s).\n\nUSE FOR: Quick lookup of specific company information, engagement metrics, revenue data.\nDO NOT USE FOR: Bulk analysis or complex filtering (use get_insights_report instead).\n\nRETURNS: Engagement metrics (totalTouches, channels, websiteVisits), revenue data (openOppsAmount, closedWonOppsAmount), company info (name, industry, employees, engagementScore).",
   "inputSchema": {
    "type": "object",
    "properties": {
     "domains": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "minItems": 1,
      "description": "Domain strings. E.g., ['metadata.io']"
     }
    },
    "required": [
     "domains"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "get_converted_leads_summary",
   "title": "Get Converted Leads Summary (Smart Leads aggregates)",
   "category": "analytics",
   "risk": "read",
   "summary": "Fast aggregate summary of the account's converted leads (Smart Leads) — counts and rates only, no lead rows.",
   "description": "Fast aggregate summary of the account's converted leads (Smart Leads) — counts and rates only, no lead rows.\n\nALSO KNOWN AS: leads summary, lead totals, lead overview, MQL rate, lead-to-MQL conversion, smart leads summary.\n\nKEYWORDS: LEAD COUNT, LEAD TOTALS, HOW MANY LEADS, LEADS SUMMARY, LEAD OVERVIEW, SMART LEADS, CONVERTED LEADS, MQL RATE, LEAD-TO-MQL, MQLS, UNIQUE LEADS, UNIQUE DOMAINS, LEAD CHANNEL MIX, LEADS BY CHANNEL, ENRICHMENT COVERAGE, SYNCED TO MARKETING AUTOMATION, LEAD QUALITY BASELINE, LEAD AUDIT BASELINE, PIPELINE FROM LEADS, TRIGGERED OPPORTUNITIES, INFLUENCED OPPORTUNITIES, LEAD SANITY CHECK\n\nUSE FOR: \"How many leads do we have?\", MQL rate and lead-to-MQL sanity checks, channel mix of leads (Facebook/Instagram/LinkedIn/Google Ads/display), enrichment coverage, leads synced to marketing automation, unique leads vs unique company domains, pipeline tie-back (triggered/influenced opportunities and amounts). ALWAYS call this FIRST in a lead audit to set the baseline before pulling rows with get_converted_leads.\n\nNOT FOR: Inspecting individual leads or job-title/ICP distributions (use get_converted_leads); campaign performance metrics (use experiment_performance_stats).\n\nRETURNED DATA: total | totalFacebook, totalInstagram, totalLinkedin, totalGoogleAds | fullyEnriched, autoEnriched, unenriched | syncedToMa | uniqueLeads, uniqueDomain | mqls, uniqueMqls, mqlRate, uniqueMqlRate | influencedOpps, triggeredOpps, influencedAmount, triggeredAmount.\n\nRULES:\n- Accepts the same filters as get_converted_leads (no pagination/sort) — scope by campaignName/channels/startDate+endDate to summarize a slice.\n- mqlRate is a useful lead-quality indicator, but clients define MQL criteria differently and it rarely matches a true MQL — treat it as a secondary signal next to job-title/ICP fit.\n- uniqueDomain vs total shows lead concentration: many leads from few domains can mean a few companies dominate the results.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "statuses": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "ACTIVE",
        "INACTIVE"
       ]
      },
      "description": "Lead statuses to include. The platform Leads tab defaults to ACTIVE. Omit to include all."
     },
     "campaignName": {
      "type": "string",
      "description": "Filter by campaign name (partial match)."
     },
     "experimentName": {
      "type": "string",
      "description": "Filter by experiment name (partial match)."
     },
     "customAudienceName": {
      "type": "string",
      "description": "Filter by targeted audience name (partial match)."
     },
     "adName": {
      "type": "string",
      "description": "Filter by the ad (library) name that captured the leads (partial match)."
     },
     "offerName": {
      "type": "string",
      "description": "Filter by the offer name tied to the leads' experiments."
     },
     "jobTitle": {
      "type": "string",
      "description": "Filter by lead job title (partial match)."
     },
     "email": {
      "type": "string",
      "description": "Filter by lead email (partial match)."
     },
     "firstName": {
      "type": "string",
      "description": "Filter by lead first name (partial match)."
     },
     "lastName": {
      "type": "string",
      "description": "Filter by lead last name (partial match)."
     },
     "businessEmails": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Filter by exact business email addresses (OR within the list)."
     },
     "modifiedBy": {
      "type": "string",
      "description": "Filter by the platform user email who last modified the leads (partial match)."
     },
     "companies": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Filter by exact company names."
     },
     "countries": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Filter by exact lead country names."
     },
     "channels": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "FACEBOOK",
        "INSTAGRAM",
        "LINKEDIN",
        "GOOGLE_ADS",
        "MICROSOFT_ADS",
        "REDDIT",
        "TWITTER"
       ]
      },
      "description": "Filter by acquisition channel(s)."
     },
     "audienceTypes": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Filter by audience type of the leads' experiments (e.g. FIRMOGRAPHIC_INCLUDE, CONTACT_LIST, BUYER_INTENT, NATIVE_LINKEDIN)."
     },
     "contentTypes": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "BUSINESS_EMAIL",
        "PHONE_NUMBER",
        "JOB_TITLE",
        "COUNTRY",
        "NO_LINKEDIN_URL"
       ]
      },
      "description": "Data-completeness filter: only leads that HAVE this content."
     },
     "qualityTypes": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "TRIGGERED_OPPS",
        "INFLUENCED_OPPS",
        "MQL_DATE",
        "MQL_SCORE",
        "NO_MQL_DATE"
       ]
      },
      "description": "Quality-signal filter (opportunities, MQL date/score)."
     },
     "keyword": {
      "type": "string",
      "description": "Filter by the exact search keyword that captured the leads."
     },
     "matchTypes": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "PHRASE",
        "BROAD",
        "EXACT",
        "UNSPECIFIED",
        "UNKNOWN"
       ]
      },
      "description": "Filter by keyword match type (search channels)."
     },
     "startDate": {
      "type": "string",
      "description": "Leads created on/after this date. ISO 8601; date-only is accepted."
     },
     "endDate": {
      "type": "string",
      "description": "Leads created on/before this date. ISO 8601; date-only is accepted."
     },
     "isFullyEnriched": {
      "type": "boolean",
      "description": "true = only fully enriched leads; false = only not fully enriched."
     },
     "isAutoUpdated": {
      "type": "boolean",
      "description": "true = only auto-enriched leads."
     },
     "isManuallyUpdate": {
      "type": "boolean",
      "description": "true = only manually enriched/edited leads (platform parameter name is 'isManuallyUpdate')."
     },
     "syncMA": {
      "type": "boolean",
      "description": "true = only leads synced to marketing automation; false = only unsynced."
     },
     "ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Restrict the summary to specific lead IDs."
     },
     "tagIds": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Filter by tag IDs on the leads' experiments."
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "get_insights_report",
   "title": "Get Insights Report",
   "category": "analytics",
   "risk": "read",
   "summary": "Query account journey insights with filtering, sorting, and pagination.",
   "description": "Query account journey insights with filtering, sorting, and pagination.\n\nUSE FOR: Filtering/searching multiple accounts, bulk analysis, trends, building segments.\nDO NOT USE FOR: Single account lookup (use get_account_summary_insights).\n\nTO CREATE AUDIENCE: 1) create_segment with criteria, 2) create_audience_from_segment.\n\nCRITERIA BUILDING:\n1. Use search_insights_criteria_fields to discover fields\n2. Build criteria: {\"operator\": \"AND|OR\", \"match\": [{field, operator, type, value}, ...]}\n3. Or use get_segment_criteria to get saved segment's criteria\n\nOPERATORS: EQUALS, NOT_EQUAL, LIKE, NOT_LIKE, IN, NOT_IN, GREATER_THAN, GREATER_OR_EQUAL, LESS_THAN, LESS_OR_EQUAL, IN_THE_LAST, NOT_IN_THE_LAST, BETWEEN, IN_THE_NEXT\n\nBOOLEAN FIELDS: Use without operator: {\"field\":\"IN_CRM\",\"operator\":null,\"type\":\"BOOLEAN\",\"value\":false}\n\nSORT OPTIONS: timeInJourney, revenue, oppCreatedAmount, highIntentDetails, engagementScoreClassificationDelta, engagementScoreClassification, inCrm, lastTouchDate (default), totalTouches, pageViews, sessions, uniqueVisitors, companyName. Append ,desc or ,asc.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "accountName": {
      "type": "string",
      "description": "Filter by account/company name."
     },
     "startDate": {
      "type": [
       "string",
       "null"
      ],
      "description": "ISO 8601 format or null. E.g., '2025-01-01T00:00:00.000Z'"
     },
     "endDate": {
      "type": [
       "string",
       "null"
      ],
      "description": "ISO 8601 format or null."
     },
     "criteria": {
      "type": [
       "object",
       "null"
      ],
      "description": "Criteria object: {\"operator\":\"AND\",\"match\":[{field,operator,type,value}]}. Use get_segment_criteria for saved segments."
     },
     "page": {
      "type": "integer",
      "minimum": 0,
      "default": 0,
      "description": "0-based page number."
     },
     "sort": {
      "type": "string",
      "default": "lastTouchDate,desc",
      "enum": [
       "timeInJourney,desc",
       "timeInJourney,asc",
       "revenue,desc",
       "revenue,asc",
       "oppCreatedAmount,desc",
       "oppCreatedAmount,asc",
       "highIntentDetails,desc",
       "highIntentDetails,asc",
       "engagementScoreClassificationDelta,desc",
       "engagementScoreClassificationDelta,asc",
       "engagementScoreClassification,desc",
       "engagementScoreClassification,asc",
       "inCrm,desc",
       "inCrm,asc",
       "lastTouchDate,desc",
       "lastTouchDate,asc",
       "totalTouches,desc",
       "totalTouches,asc",
       "pageViews,desc",
       "pageViews,asc",
       "sessions,desc",
       "sessions,asc",
       "uniqueVisitors,desc",
       "uniqueVisitors,asc",
       "companyName,desc",
       "companyName,asc"
      ],
      "description": "Sort format: field,direction. Default: lastTouchDate,desc"
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "performance_metrics",
   "title": "Get Performance Metrics",
   "category": "analytics",
   "risk": "read",
   "summary": "Granular performance metrics for ads, audiences, creatives, offers, target groups, keywords.",
   "description": "Granular performance metrics for ads, audiences, creatives, offers, target groups, keywords.\nIMPORTANT: Always specify endpoint parameter. Always use ISO 8601 for dates.\n\nUSE FOR: Best performing ads/audiences/creatives/offers/target groups, ingredient comparisons, ad type analysis (IMAGE, VIDEO, CAROUSEL), lead gen forms vs landing pages, pipeline by ingredient, creative previews, keyword performance.\nNOT FOR: Account-level stats, experiment-level analysis without ingredient focus.\n\nENDPOINT LOGIC:\n- 'target group' → customAudience/group\n- 'audience' → customAudience\n- 'offer'/'lead gen'/'landing page' → offer\n- 'creative' → creative\n- 'keyword' → keywords\n- 'ad'/'ads' or default → ads\n\nAD TYPES: IMAGE, VIDEO, CAROUSEL, DOCUMENT, CONVO, SPOTLIGHT, SEARCH, MESSAGE\n- The `adFormat` field on each response row identifies the ad type.\n\nMETRICS: spend, impressions, clicks, leads, mqls | opens, sends, actionClicks, costPerOpen, costPerSend (CONVO/MESSAGE ads) | cpl, cpc, cpm, ctr, conversionRate, formConversionRate, mqlRate | totalOpps, totalTriggered, oppsAmount, triggeredAmount | cpMql, cpOppInfluenced, cpOppTriggered | channel, goal, adFormat, audienceSize, statusLabel | previewUrl\n\nCONVO/MESSAGE AD CAVEAT: when adFormat is CONVO or MESSAGE (LinkedIn message ads), success is measured by opens, sends, and actionClicks (and costPerOpen / costPerSend), NOT clicks/ctr/cpc. Do not rank these ads by CTR or dismiss them when clicks=0. Use sort='actionClicks,desc' or sort='opens,desc' for conversational performance ranking.\n\nRULES: Field 'id' corresponds to endpoint queried. Exclude $0 CPL experiments from calculations.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "launchedExperimentStatuses": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "Active",
        "WithoutSpend",
        "Paused",
        "Completed",
        "Failed"
       ]
      },
      "description": "Filter Performance Metrics by experiment launch statuses only."
     },
     "page": {
      "type": "integer",
      "default": 0,
      "minimum": 0,
      "description": "Page number to retrieve (0-based indexing)"
     },
     "size": {
      "type": "integer",
      "default": 100,
      "minimum": 1,
      "maximum": 10000,
      "description": "Number of results per page. Default 100; raise it freely (up to 10000) for comprehensive full-dataset analysis."
     },
     "sort": {
      "type": "string",
      "description": "Sorting criteria (e.g., 'spent,desc', 'cpl,asc', 'experimentId,spent,desc')"
     },
     "ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Set of IDs to filter by"
     },
     "experimentIds": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Set of experiment IDs to filter by"
     },
     "startDate": {
      "type": "string",
      "format": "date",
      "description": "startDate in ISO 8601 format (YYYY-MM-DDThh:mm:ssZ), e.g., `startDate=2025-06-12T14:30:00.123Z`)."
     },
     "endDate": {
      "type": "string",
      "format": "date",
      "description": "endDate in ISO 8601 format (YYYY-MM-DDThh:mm:ssZ), e.g., `startDate=2025-06-12T14:30:00.123Z`)."
     },
     "oppsStartDate": {
      "type": "string",
      "format": "date",
      "description": "Start date for opportunities data in ISO 8601 format"
     },
     "oppsEndDate": {
      "type": "string",
      "format": "date",
      "description": "End date for opportunities data in ISO 8601 format"
     },
     "adName": {
      "type": "string",
      "description": "Filter by specific ad name"
     },
     "keyword": {
      "type": "string",
      "description": "Filter by specific keyword"
     },
     "campaignName": {
      "type": "string",
      "description": "Filter by specific campaign name"
     },
     "creativeName": {
      "type": "string",
      "description": "Filter by specific creative name"
     },
     "customAudienceName": {
      "type": "string",
      "description": "Filter by specific custom audience name"
     },
     "offerName": {
      "type": "string",
      "description": "Filter by specific offer name"
     },
     "experimentName": {
      "type": "string",
      "description": "Filter by specific experiment name"
     },
     "budgetGroupName": {
      "type": "string",
      "description": "Filter by specific budget group name"
     },
     "channels": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "FACEBOOK",
        "LINKEDIN",
        "REDDIT",
        "MICROSOFT_ADS"
       ]
      },
      "description": "Filter by marketing channels"
     },
     "goals": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "CPL",
        "CTR"
       ]
      },
      "description": "Filter by campaign goals"
     },
     "audienceTypes": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "FIRMOGRAPHIC_EXCLUDE_BASIC",
        "FIRMOGRAPHIC_INCLUDE",
        "FIRMOGRAPHIC_EXCLUDE",
        "TECHNOLOGY",
        "BUYER_INTENT",
        "LEADSIFT",
        "SLINTEL_TECHNOLOGY",
        "CONTACT_LIST",
        "NATIVE_TARGETING_CSV",
        "NATIVE_TARGETING_STATIC",
        "NATIVE_TARGETING_DYNAMIC",
        "NATIVE_LINKEDIN",
        "NATIVE_FACEBOOK",
        "NATIVE_DISPLAY",
        "NATIVE_GOOGLE_ADS",
        "DYNAMIC_G2",
        "G2_ABM_CONTACT",
        "DYNAMIC_G2_CONTACT",
        "FACEBOOK_RETARGETING",
        "LINKEDIN_RETARGETING",
        "DISPLAY_RETARGETING",
        "GOOGLE_ADS_RETARGETING",
        "TWITTER_RETARGETING",
        "REDDIT_RETARGETING",
        "SFDC_CONTACTS_STATIC",
        "SFDC_CONTACTS_DYNAMIC",
        "MA_CONTACTS_STATIC",
        "MA_CONTACTS_DYNAMIC",
        "MA_ACCOUNTS_STATIC",
        "MA_ACCOUNTS_DYNAMIC",
        "FIRMOGRAPHIC_INCLUDE_SFDC",
        "SFDC_ACCOUNT_DYNAMIC",
        "GROUP",
        "DYNAMIC_AUDIENCE",
        "STORELEADS",
        "WEBSITE_RETARGETING",
        "WEBSITE_ACCOUNT_RETARGETING"
       ]
      },
      "description": "Filter by audience types"
     },
     "offerTypes": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "LG",
        "LP"
       ]
      },
      "description": "Filter by offer types. LG=Lead Gen forms, LP=Landing Pages"
     },
     "visibilities": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "VISIBLE",
        "HIDDEN",
        "ARCHIVED"
       ]
      },
      "description": "Filter by visibility status"
     },
     "adFormats": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "IMAGE",
        "VIDEO",
        "CAROUSEL",
        "DOCUMENT",
        "CONVO",
        "SPOTLIGHT",
        "SEARCH"
       ]
      },
      "description": "Filter by ad format types. Applied to ads endpoint automatically."
     },
     "minSpend": {
      "type": "number",
      "minimum": 0,
      "description": "Minimum spend threshold"
     },
     "maxSpend": {
      "type": "number",
      "minimum": 0,
      "description": "Maximum spend threshold"
     },
     "minLeads": {
      "type": "integer",
      "minimum": 0,
      "description": "Minimum number of leads threshold"
     },
     "maxLeads": {
      "type": "integer",
      "minimum": 0,
      "description": "Maximum number of leads threshold"
     },
     "minCpl": {
      "type": "number",
      "minimum": 0,
      "description": "Minimum cost per lead threshold"
     },
     "maxCpl": {
      "type": "number",
      "minimum": 0,
      "description": "Maximum cost per lead threshold"
     },
     "minAudienceSize": {
      "type": "integer",
      "minimum": 0,
      "description": "Minimum audience size threshold"
     },
     "maxAudienceSize": {
      "type": "integer",
      "minimum": 0,
      "description": "Maximum audience size threshold"
     },
     "usedInActiveExperiments": {
      "type": "boolean",
      "description": "Filter for ingredients used in active experiments only"
     },
     "tags": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Set of tag IDs to filter by"
     },
     "accountTagIds": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Set of account tag IDs to filter by"
     },
     "showWithSpend": {
      "type": "boolean",
      "description": "Show only experiments/ingredients with spend data"
     },
     "experimentList": {
      "type": "string",
      "description": "Experiment list filter"
     },
     "customFieldIds": {
      "type": "string",
      "description": "Custom field IDs to include"
     },
     "skipCustomFields": {
      "type": "boolean",
      "description": "Skip custom fields in response"
     },
     "endpoint": {
      "type": "string",
      "enum": [
       "ads",
       "customAudience",
       "customAudience/group",
       "offer",
       "creative",
       "keywords"
      ],
      "description": "Specific endpoint to query. If not specified, will be auto-determined from query context. Use 'customAudience/group' for target groups."
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "query_metadata_analytics_account",
   "title": "Query metadata analytics account",
   "category": "analytics",
   "risk": "read",
   "summary": "Query the caller's own account CONTEXT from the Metadata.io knowledge base — a rolled-up summary (firmographic, recent 90-day campaign history, open drafts, per-experiment perfo…",
   "description": "Query the caller's own account CONTEXT from the Metadata.io\nknowledge base — a rolled-up summary (firmographic, recent 90-day\ncampaign history, open drafts, per-experiment performance) refreshed\novernight. Use it to GROUND a recommendation or plan in the account's\nown history before you suggest or generate something.\n\nUSE FOR: firmographic (industry, size, revenue, country) and\nhigh-level recent-history context to back a decision — e.g. \"given our\nrecent LinkedIn performance, what should we try next quarter?\".\n\nNOT FOR the account's CURRENT LIVE STATE. This is a periodic roll-up,\nnot the live platform — it can miss or misreport granular state (e.g.\nanswer \"nothing runs in Germany\" while many live campaigns target\nGermany). For \"what's running / what's live\", \"which campaigns target\n<country/segment>\", \"list our campaigns / audiences / offers\", or\nreal-time spend / pacing, use the live platform analytics tools instead\n(`account_level_stats`, `performance_metrics`, `demographic_country_stats`,\n`search_campaigns_by_names`, etc.). Also NOT FOR industry benchmarks\n(use `query_metadata_analytics_benchmarks`) or a different company\n(cross-tenant queries are rejected).\n\nThe caller's `account_id` is resolved server-side from the caller's own\ncredentials and reflects any active account switch. It cannot be supplied\nor overridden by the caller.",
   "inputSchema": {
    "additionalProperties": false,
    "properties": {
     "question": {
      "type": "string",
      "description": "Natural-language question about the caller's account."
     }
    },
    "required": [
     "question"
    ],
    "type": "object"
   }
  },
  {
   "name": "query_metadata_analytics_benchmarks",
   "title": "Query metadata analytics benchmarks",
   "category": "analytics",
   "risk": "read",
   "summary": "Query the Metadata.io marketing knowledge base for cross-account industry benchmarks and priors.",
   "description": "Query the Metadata.io marketing knowledge base for cross-account\nindustry benchmarks and priors.",
   "inputSchema": {
    "additionalProperties": false,
    "properties": {
     "question": {
      "type": "string",
      "description": "Natural-language question. Should describe a COHORT\n(industry × size × channel × format / audience), not a\nspecific company."
     },
     "conversation_id": {
      "anyOf": [
       {
        "type": "string"
       },
       {
        "type": "null"
       }
      ],
      "default": null,
      "description": "Optional. Pass the value returned from a prior\n`_benchmarks` call to keep the query context warm. The two\nknowledge-base tools never share conversation_ids — pass\nonly IDs returned by `_benchmarks` here."
     }
    },
    "required": [
     "question"
    ],
    "type": "object"
   }
  },
  {
   "name": "search_insights_criteria_fields",
   "title": "Search Insights Criteria Fields",
   "category": "analytics",
   "risk": "read",
   "summary": "Discover available fields for building insights filter criteria.",
   "description": "Discover available fields for building insights filter criteria.\n\nUSE BEFORE: get_insights_report or create_segment when filtering is needed.\n\nWORKFLOW:\n1. Extract keywords from user request (3-10 recommended)\n2. Call this tool with keywords\n3. Use returned field definitions to build criteria\n\nKEYWORD EXAMPLES:\n- URL/visits → [\"page\", \"url\", \"visit\", \"web\"]\n- Engagement → [\"score\", \"engagement\", \"activity\"]\n- CRM → [\"crm\", \"salesforce\", \"opportunity\"]\n- Channels → [\"linkedin\", \"facebook\", \"google\"]\n\nRESPONSE: Returns fields with name, label, type, operators, and options.\n\nFIELD TYPES: MULTIPICKLIST, PICKLIST, STRING, NUMERIC, DATE, BOOLEAN, RANGE, ARRAY, DYNAMIC",
   "inputSchema": {
    "type": "object",
    "properties": {
     "search_keywords": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "minItems": 1,
      "description": "Keywords to search for fields (3-10 recommended). E.g., ['page', 'url', 'visit']"
     }
    },
    "required": [
     "search_keywords"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "website_engagement_stats",
   "title": "Get Website Engagement Statistics",
   "category": "analytics",
   "risk": "read",
   "summary": "Website visitor engagement metrics: session data, page views, visitor behavior analytics.",
   "description": "Website visitor engagement metrics: session data, page views, visitor behavior analytics.\n\nUSE FOR: Website traffic/engagement, visitor behavior analysis, session/page view metrics, website conversion funnel, engagement trends, website performance by period.\nNOT FOR: Campaign ad performance (use experiment_performance_stats), account list performance (use account_list_performance), budget groups (use budget_group_performance), geographic demographics (use demographic_country_stats).\n\nTIMEFRAMES: YESTERDAY, LAST_7_DAYS, LAST_14_DAYS, LAST_30_DAYS, LAST_MONTH, MONTH_TO_DATE (default), LIFETIME\n\nMETRICS: sessions, uniqueVisitors, newVisitors, returningVisitors | pageViews, pagesPerSession, avgSessionDuration | bounceRate, exitRate, timeOnPage | conversions, conversionRate, goals\n\nREQUIRED: Valid timeframe must be specified. Default: MONTH_TO_DATE if ambiguous.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "dateRange": {
      "type": "string",
      "enum": [
       "YESTERDAY",
       "LAST_7_DAYS",
       "LAST_14_DAYS",
       "LAST_30_DAYS",
       "LAST_MONTH",
       "MONTH_TO_DATE",
       "LIFETIME"
      ],
      "default": "MONTH_TO_DATE",
      "description": "Timeframe for engagement data. Options: YESTERDAY, LAST_7_DAYS, LAST_14_DAYS, LAST_30_DAYS, LAST_MONTH, MONTH_TO_DATE (default), LIFETIME. REQUIRED parameter."
     },
     "page": {
      "type": "integer",
      "default": 0,
      "minimum": 0,
      "description": "Page number for pagination (0-based). Example: page=0 for the first page."
     },
     "size": {
      "type": "integer",
      "default": 15,
      "minimum": 1,
      "maximum": 10000,
      "description": "Number of results per page. Default: 15; raise it (up to 10000) to pull the full engagement set in one call."
     },
     "sort": {
      "type": "string",
      "default": "sessions,desc",
      "description": "Sorting criteria. Examples: 'sessions,desc' (default), 'pageViews,desc', 'uniqueVisitors,desc', 'conversionRate,desc'. Format: field,direction where direction is 'asc' or 'desc'."
     }
    },
    "required": [
     "dateRange"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "archive_audience",
   "title": "Archive Audience",
   "category": "audiences",
   "risk": "destructive",
   "summary": "Archive (delete) a custom audience from the Metadata platform.",
   "description": "Archive (delete) a custom audience from the Metadata platform.\n                        Use this tool when the user wants to archiveo or delete a custom audience from the entire System.\n\n                        You can identify the audience either by its numeric custom_audience_id or by its name.\n                        If only the audience name is provided, the tool will search existing audiences to resolve the correct ID before archiving.\n\n                        PARAMETERS:\n                        • custom_audience_id (optional): The numeric ID of the custom audience to archive.\n                        • audience_name (optional): The name of the custom audience to archive. Used to look up the ID if custom_audience_id is not provided.\n\n                        At least one of custom_audience_id or audience_name must be provided.\n\n                        RESPONSE:\n                        • actionStatus: \"SUCCEEDED\" if the audience was archived successfully.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "custom_audience_id": {
      "type": "integer",
      "description": "The numeric ID of the custom audience to archive."
     },
     "audience_name": {
      "type": "string",
      "description": "The name of the custom audience to archive. If provided without custom_audience_id, the tool will search for the audience by name to resolve the ID."
     }
    },
    "additionalProperties": false
   }
  },
  {
   "name": "create_audience_from_segment",
   "title": "Create Audience from Segment",
   "category": "audiences",
   "risk": "write",
   "summary": "Create a new custom audience based on a segment from the insights/attribution tool.",
   "description": "Create a new custom audience based on a segment from the insights/attribution tool.\n\n                   This tool creates a website retargeting audience using segment data from the list_segments tool.\n                   The audience will include accounts that match the segment criteria within the specified lookback window.\n\n                   **IMPORTANT - TWO-STEP WORKFLOW FOR CREATING AUDIENCES FROM INSIGHTS:**\n                   When a user asks to \"create an audience from the insights report\" or \"create an audience based on insights data\":\n\n                   STEP 1: Create a segment first using create_segment tool\n                   - Use search_insights_criteria_fields to find the appropriate fields for filtering\n                   - Build the criteria object based on the user's filtering requirements\n                   - Call create_segment with the criteria to save the segment\n                   - Get the segment_id from the response\n\n                   STEP 2: Create the audience from the segment using this tool\n                   - Use the segment_id from Step 1\n                   - **IMPORTANT**: Verify the segment status is READY (check using get_segment_criteria)\n                   - Provide a descriptive audience name\n                   - Set the lookback window (number of days)\n                   - Call this tool to create the audience\n\n                   This two-step process is necessary because audiences must be based on saved segments, not raw criteria.\n\n                   SEGMENT STATUS REQUIREMENT:\n                   The segment MUST have a status of READY before creating an audience from it.\n                   If the segment status is PENDING or FAILED, the audience creation will fail.\n                   Use get_segment_criteria to check the segment status before proceeding.\n\n                   **USE THIS TOOL WHEN THE USER ASKS TO:**\n                   - \"Create an audience from segment [segment_name or segment_id]\"\n                   - \"Make an audience based on segment [segment_name or segment_id]\"\n                   - \"Turn segment [segment_name or segment_id] into an audience\"\n                   - \"Create a custom audience from [segment_name or segment_id]\"\n                   - \"Create an audience from the insights report\" (requires creating segment first)\n                   - \"Create an audience based on insights data\" (requires creating segment first)\n\n                   WORKFLOW:\n                   1. Use list_segments tool to find available segments and their IDs (OR create new segment if needed)\n                   2. Ask user for the audience name if not provided\n                   3. Ask user for the lookback window (days) if not provided\n                   4. Create the audience using the segment ID\n\n                   PARAMETERS:\n                   - segment_id: The ID of the segment from list_segments tool (required)\n                   - name: Name for the new audience (required)\n                   - lookback_window: Number of days to look back in the segment results (required)\n                     Range: 1 to 365 days\n                     Example: 30 (last 30 days), 90 (last 90 days), 365 (last year)\n\n                   COMMON USE CASES:\n                   - Create retargeting audiences from high-intent segments\n                   - Build audiences from engagement score segments\n                   - Create audiences from behavioral pattern segments\n                   - Generate audiences from CRM status segments\n\n                   SEGMENT ID SOURCES:\n                   Use the list_segments tool to find segment IDs:\n                   - List all available segments\n                   - Find the segment by name\n                   - Get the segment ID from the results\n\n                   EXAMPLE WORKFLOW:\n                   User: \"Create an audience from the high intent segment for the last 30 days\"\n                   1. Call list_segments() to find \"high intent\" segment → get segment_id (e.g., 86)\n                   2. Ask for audience name if not provided → \"High Intent Accounts - 30 Days\"\n                   3. Call this tool with segment_id=86, name=\"High Intent Accounts - 30 Days\", lookback_window=30\n\n                   RETURNS:\n                   Success confirmation with details about the created audience including:\n                   - Audience ID\n                   - Audience name\n                   - Segment ID used\n                   - Lookback window configured\n                   - Estimated audience size\n\n                   IMPORTANT NOTES:\n                   - **Segment status MUST be READY** - verify using get_segment_criteria before creating the audience\n                   - Segment ID must exist and be valid\n                   - Lookback window must be between 1 and 365 days\n                   - Audience name must be unique\n                   - The audience type is automatically set to WEBSITE_RETARGETING\n                   - Audience will update automatically as segment data changes",
   "inputSchema": {
    "type": "object",
    "properties": {
     "segment_id": {
      "type": "integer",
      "description": "The ID of the segment from list_segments tool. Use list_segments to find available segment IDs.",
      "minimum": 1
     },
     "name": {
      "type": "string",
      "description": "Name for the new audience. Must be unique and descriptive. Example: 'High Intent Accounts - 30 Days'",
      "minLength": 1
     },
     "lookback_window": {
      "type": "integer",
      "description": "Number of days to look back in the segment results. Range: 1 to 365 days. Example: 30 for last 30 days, 90 for last quarter, 365 for last year.",
      "minimum": 1,
      "maximum": 365
     }
    },
    "required": [
     "segment_id",
     "name",
     "lookback_window"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_bombora_audience",
   "title": "Create Bombora Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Create Bombora buyer intent audiences on the Metadata platform.",
   "description": "Create Bombora buyer intent audiences on the Metadata platform.\n                Creates B2B audiences based on buyer intent topics, company firmographics, contact attributes, and geographic targeting.\n\n                IMPORTANT PREREQUISITE:\n                Before using this tool, call get_intent_topics first to verify that Bombora topics are available.\n                If get_intent_topics returns NO topics (empty list), Bombora is NOT available for this account —\n                do NOT suggest or use this tool. Instead, recommend creating a firmographic audience\n                (create_firmographic_audience) as the preferred alternative.\n                Firmographic audiences are the default and most commonly used audience type.\n\n                WHEN TO USE:\n                - ONLY after get_intent_topics confirms that Bombora topics are available\n                - Create targeted B2B audiences based on buyer intent signals using Bombora data\n                - Target companies by industry, size, and employee count\n                - Target contacts by seniority level and job function\n                - Target specific buyer intent topics (e.g., Pardot, Salesforce)\n                - Set geographic targeting and contact limits per company\n\n                BUYER INTENT TOPICS:\n                Use the get_intent_topics tool to retrieve available intent topics before creating the audience.\n                Intent topics represent buyer interest areas such as:\n                - CRM solutions (e.g., Pardot, Salesforce)\n                - Marketing automation platforms\n                - Analytics and BI tools\n                - And many others based on available topics\n\n                TARGETING OPTIONS:\n                - Intent Topics: Use topic values from get_intent_topics (required for Bombora audience)\n                - Industries: Use industry IDs. Examples:\n                  1=Defense & Space, 4=Computer Software, 6=Internet, 7=Semiconductors, 8=Telecommunications,\n                  11=Management Consulting, 12=Biotechnology, 41=Banking, 42=Insurance, 43=Financial Services,\n                  80=Marketing & Advertising, 96=Information Technology & Services\n                - Employees: \"FROM_1_TO_10\", \"FROM_10_TO_50\", \"FROM_50_TO_200\", \"FROM_200_TO_500\", \"FROM_500_TO_1000\", \"FROM_1000_TO_5000\", \"FROM_5000_TO_10000\", \"OVER_10000\"\n                - Job Function / Seniority / Job Title / Location: identical shape to the Firmographic + G2 tools (PRD-29704 — the old Bombora-specific names `function_ids` / `seniority_ids` / `countries` / `cities` were renamed to the canonical contact-criteria fields below so Max can transfer the same criteria across audience types).\n\n                CONTACTS CRITERIA:\nUse the following parameters to filter and target specific contacts within accounts:\n\nGEOGRAPHIC TARGETING:\n- location_country_ids: Array of country IDs (e.g., [29] for Brazil, [229] for United States)\n- location_state_ids: Array of state IDs (e.g., [5] for California, [32] for New York)\n\nJOB TITLES:\n- job_title_includes: Array of free text strings to include (e.g., [\"Software Engineer\", \"Manager\"])\n- job_title_excludes: Array of free text strings to exclude (e.g., [\"Senior\", \"Lead\"])\n\nJOB FUNCTIONS:\nValid job function IDs and their labels:\n- 1: Marketing, 2: Sales, 3: Customer Service, 4: Finance, 5: Human Resources, 6: Public Relations\n- 7: Media, 8: Design, 9: Engineering, 10: Operations, 11: Legal, 12: Education\n- 13: Health, 14: Real Estate, 15: Accounting, 16: Administrative, 17: Business Development\n- 18: Community and Social Services, 19: Consulting, 20: Entrepreneurship, 21: Information Technology\n- 22: Military and Protective Services, 23: Product Management, 24: Program and Project Management\n- 25: Purchasing, 26: Quality Assurance, 27: Research, 28: Trades\n\n- job_function_include_ids: Array of job function IDs to include (e.g., [9] for Engineering)\n- job_function_exclude_ids: Array of job function IDs to exclude\n\nSENIORITY LEVELS:\nValid seniority IDs and their labels:\n- 2: Manager, 3: Director, 4: VP, 5: CXO, 6: Unpaid, 7: Training\n- 8: Senior, 9: Owner, 10: Entry, 11: Partner\n\n- seniority_include_ids: Array of seniority level IDs to include (e.g., [10, 7] for Entry and Training)\n- seniority_exclude_ids: Array of seniority level IDs to exclude\n\nCONTACT LIMITS:\n- contacts_per_company_limit: Maximum number of contacts to target per company (optional). Example: 20\n\nUSAGE NOTES:\n- All ID fields (countries, states, job functions, seniorities) use numeric IDs from the platform\n- Job titles are free text strings, not IDs\n- Use include parameters to specify which criteria to target\n- Use exclude parameters to filter out contacts matching specific criteria\n- Combine multiple criteria for precise targeting (e.g., Senior Engineers in California)\n- Criteria are combined with AND logic, so all conditions must be met\n\nCONTACTS CRITERIA PARAMETERS(Usually after other initial audience parameters to be described next):\n- location_country_ids: Array of country IDs (optional)\n- location_state_ids: Array of state IDs (optional)\n- job_title_includes: Array of job title keywords to include (optional)\n- job_title_excludes: Array of job title keywords to exclude (optional)\n- job_function_include_ids: Array of job function IDs (optional)\n- job_function_exclude_ids: Array of job function IDs (optional)\n- seniority_include_ids: Array of seniority level IDs (optional)\n- seniority_exclude_ids: Array of seniority level IDs (optional)\n- contacts_per_company_limit: Maximum contacts per company (optional)\n\n\n                Returns audience details with ID, size estimation, and creation status.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "Audience name (required)"
     },
     "topics": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Array of buyer intent topics (required). Use get_intent_topics to retrieve available topics. Example: ['Pardot', 'Salesforce']"
     },
     "industries": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Industry IDs for company targeting (optional). Valid range: 1-148. Example: [4, 96]"
     },
     "employees": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Employee count ranges for company targeting (optional). Valid values: 'FROM_1_TO_10', 'FROM_10_TO_50', 'FROM_50_TO_200', 'FROM_200_TO_500', 'FROM_500_TO_1000', 'FROM_1000_TO_5000', 'FROM_5000_TO_10000', 'OVER_10000'"
     },
     "location_country_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Country IDs for geographic targeting (optional). Valid IDs: 1 (Afghanistan), 2 (Albania), 3 (Algeria), 4 (American Samoa), 5 (Andorra), 6 (Angola), 7 (Anguilla), 8 (Antarctica), 9 (Antigua and Barbuda), 10 (Argentina), 11 (Armenia), 12 (Aruba), 13 (Australia), 14 (Austria), 15 (Azerbaijan), 16 (Bahamas), 17 (Bahrain), 18 (Bangladesh), 19 (Barbados), 20 (Belarus), 21 (Belgium), 22 (Belize), 23 (Benin), 24 (Bermuda), 25 (Bhutan), 26 (Bolivia), 27 (Bosnia and Herzegovina), 28 (Botswana), 29 (Brazil), 30 (British Indian Ocean Territory), 31 (British Virgin Islands), 32 (Brunei), 33 (Bulgaria), 34 (Burkina Faso), 35 (Burundi), 36 (Cambodia), 37 (Cameroon), 38 (Canada), 39 (Cape Verde), 40 (Cayman Islands), 41 (Central African Republic), 42 (Chad), 43 (Chile), 44 (China), 45 (Christmas Island), 46 (Cocos Islands), 47 (Colombia), 48 (Comoros), 49 (Cook Islands), 50 (Costa Rica), 51 (Croatia), 53 (Curacao), 54 (Cyprus), 55 (Czech Republic), 56 (Democratic Republic of the Congo), 57 (Denmark), 58 (Djibouti), 59 (Dominica), 60 (Dominican Republic), 61 (East Timor), 62 (Ecuador), 63 (Egypt), 64 (El Salvador), 65 (Equatorial Guinea), 66 (Eritrea), 67 (Estonia), 68 (Ethiopia), 69 (Falkland Islands), 70 (Faroe Islands), 71 (Fiji), 72 (Finland), 73 (France), 74 (French Polynesia), 75 (Gabon), 76 (Gambia), 77 (Georgia), 78 (Germany), 79 (Ghana), 80 (Gibraltar), 81 (Greece), 82 (Greenland), 83 (Grenada), 84 (Guam), 85 (Guatemala), 86 (Guernsey), 87 (Guinea), 88 (Guinea-Bissau), 89 (Guyana), 90 (Haiti), 91 (Honduras), 92 (Hong Kong), 93 (Hungary), 94 (Iceland), 95 (India), 96 (Indonesia), 98 (Iraq), 99 (Ireland), 100 (Isle of Man), 101 (Israel), 102 (Italy), 103 (Ivory Coast), 104 (Jamaica), 105 (Japan), 106 (Jersey), 107 (Jordan), 108 (Kazakhstan), 109 (Kenya), 110 (Kiribati), 111 (Kosovo), 112 (Kuwait), 113 (Kyrgyzstan), 114 (Laos), 115 (Latvia), 116 (Lebanon), 117 (Lesotho), 118 (Liberia), 119 (Libya), 120 (Liechtenstein), 121 (Lithuania), 122 (Luxembourg), 123 (Macau), 124 (Macedonia), 125 (Madagascar), 126 (Malawi), 127 (Malaysia), 128 (Maldives), 129 (Mali), 130 (Malta), 131 (Marshall Islands), 132 (Mauritania), 133 (Mauritius), 134 (Mayotte), 135 (Mexico), 136 (Micronesia), 137 (Moldova), 138 (Monaco), 139 (Mongolia), 140 (Montenegro), 141 (Montserrat), 142 (Morocco), 143 (Mozambique), 144 (Myanmar), 145 (Namibia), 146 (Nauru), 147 (Nepal), 148 (Netherlands), 149 (Netherlands Antilles), 150 (New Caledonia), 151 (New Zealand), 152 (Nicaragua), 153 (Niger), 154 (Nigeria), 155 (Niue), 157 (Northern Mariana Islands), 158 (Norway), 159 (Oman), 160 (Pakistan), 161 (Palau), 162 (Palestine), 163 (Panama), 164 (Papua New Guinea), 165 (Paraguay), 166 (Peru), 167 (Philippines), 168 (Pitcairn), 169 (Poland), 170 (Portugal), 171 (Puerto Rico), 172 (Qatar), 173 (Republic of the Congo), 174 (Reunion), 175 (Romania), 176 (Russia), 177 (Rwanda), 178 (Saint Barthelemy), 179 (Saint Helena), 180 (Saint Kitts and Nevis), 181 (Saint Lucia), 182 (Saint Martin), 183 (Saint Pierre and Miquelon), 184 (Saint Vincent and the Grenadines), 185 (Samoa), 186 (San Marino), 187 (Sao Tome and Principe), 188 (Saudi Arabia), 189 (Senegal), 190 (Serbia), 191 (Seychelles), 192 (Sierra Leone), 193 (Singapore), 194 (Sint Maarten), 195 (Slovakia), 196 (Slovenia), 197 (Solomon Islands), 198 (Somalia), 199 (South Africa), 200 (South Korea), 201 (South Sudan), 202 (Spain), 203 (Sri Lanka), 205 (Suriname), 206 (Svalbard and Jan Mayen), 207 (Swaziland), 208 (Sweden), 209 (Switzerland), 211 (Taiwan), 212 (Tajikistan), 213 (Tanzania), 214 (Thailand), 215 (Togo), 216 (Tokelau), 217 (Tonga), 218 (Trinidad and Tobago), 219 (Tunisia), 220 (Turkey), 221 (Turkmenistan), 222 (Turks and Caicos Islands), 223 (Tuvalu), 224 (U.S. Virgin Islands), 225 (Uganda), 226 (Ukraine), 227 (United Arab Emirates), 228 (United Kingdom), 229 (United States), 230 (Uruguay), 231 (Uzbekistan), 232 (Vanuatu), 233 (Vatican), 234 (Venezuela), 235 (Vietnam), 236 (Wallis and Futuna), 237 (Western Sahara), 238 (Yemen), 239 (Zambia), 240 (Zimbabwe), 241 (Guadeloupe). Example: [29] for Brazil, [229] for United States"
     },
     "location_state_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "State IDs for geographic targeting (optional). Valid US State IDs: 1 (Alabama), 2 (Alaska), 3 (Arizona), 4 (Arkansas), 5 (California), 6 (Colorado), 7 (Connecticut), 8 (Delaware), 9 (Florida), 10 (Georgia), 11 (Hawaii), 12 (Idaho), 13 (Illinois), 14 (Indiana), 15 (Iowa), 16 (Kansas), 17 (Kentucky), 18 (Louisiana), 19 (Maine), 20 (Maryland), 21 (Massachusetts), 22 (Michigan), 23 (Minnesota), 24 (Mississippi), 25 (Missouri), 26 (Montana), 27 (Nebraska), 28 (Nevada), 29 (New Hampshire), 30 (New Jersey), 31 (New Mexico), 32 (New York), 33 (North Carolina), 34 (North Dakota), 35 (Ohio), 36 (Oklahoma), 37 (Oregon), 38 (Pennsylvania), 39 (Rhode Island), 40 (South Carolina), 41 (South Dakota), 42 (Tennessee), 43 (Texas), 44 (Utah), 45 (Vermont), 46 (Virginia), 47 (Washington), 48 (West Virginia), 49 (Wisconsin), 50 (Wyoming), 52 (District of Columbia). Example: [5] for California, [32] for New York"
     },
     "job_title_includes": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free text keywords for job titles to include (optional). Example: ['Software Engineer', 'Manager']"
     },
     "job_title_excludes": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free text keywords for job titles to exclude (optional). Example: ['Senior', 'Lead']"
     },
     "job_function_include_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Job function IDs to include (optional). Valid IDs: 1 (Marketing), 2 (Sales), 3 (Customer Service), 4 (Finance), 5 (HR), 6 (PR), 7 (Media), 8 (Design), 9 (Engineering), 10 (Operations), 11 (Legal), 12 (Education), 13 (Health), 14 (Real Estate), 15 (Accounting), 16 (Administrative), 17 (Business Development), 18 (Community Services), 19 (Consulting), 20 (Entrepreneurship), 21 (IT), 22 (Military Services), 23 (Product Management), 24 (Program/Project Management), 25 (Purchasing), 26 (QA), 27 (Research), 28 (Trades). Example: [9] for Engineering"
     },
     "job_function_exclude_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Job function IDs to exclude (optional). Valid IDs: 1-28 as listed above"
     },
     "seniority_include_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Seniority level IDs to include (optional). Valid IDs: 2 (Manager), 3 (Director), 4 (VP), 5 (CXO), 6 (Unpaid), 7 (Training), 8 (Senior), 9 (Owner), 10 (Entry), 11 (Partner). Example: [10, 7] for Entry and Training"
     },
     "seniority_exclude_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Seniority level IDs to exclude (optional). Valid IDs: 2-11 as listed above"
     },
     "contacts_per_company_limit": {
      "type": "integer",
      "description": "Maximum number of contacts to target per company (optional). Example: 20",
      "minimum": 1
     }
    },
    "required": [
     "name",
     "topics"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_csv_upload_linkedin_native_audience",
   "title": "Create CSV Upload - LinkedIn Native Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Create a **CSV Upload - LinkedIn Native** audience (platform `customAudienceType=NATIVE_TARGETING_CSV`).",
   "description": "Create a **CSV Upload - LinkedIn Native** audience (platform `customAudienceType=NATIVE_TARGETING_CSV`).\n\n                AUDIENCE TYPE (mirrors the UI's \"Audience Type\" dropdown):\n                  • UI label: \"CSV Upload - LinkedIn Native\"\n                  • Platform enum: NATIVE_TARGETING_CSV\n                  • Channel: LinkedIn only.\n                  • Two-step flow handled server-side: the CSV is uploaded as a NATIVE ABM list, then the audience is created from it with the LinkedIn-native firmographics + contact criteria applied.\n\n                PREREQUISITE:\n                  • LinkedIn integration MUST be connected.\n\n                WHEN TO USE (exact user phrasing this tool should match):\n                  • \"CSV Upload - LinkedIn Native\"\n                  • \"Upload a CSV and target LinkedIn natively\"\n                  • \"LinkedIn native audience from this CSV\"\n                  • The user attached a CSV of companies AND asked for LinkedIn-native targeting (employees / revenues / job titles / skills resolved via LinkedIn).\n\n                WHEN NOT TO USE:\n                  • If the user asked for a plain \"CSV Upload - Accounts\" → use `upload_account_list_csv_audience` (creates FIRMOGRAPHIC_INCLUDE, NOT LinkedIn-native).\n                  • If the user asked for \"Native Criteria - LinkedIn\" without a CSV → use `create_linkedin_native_criteria_audience`.\n\n                TWO WAYS TO SUPPLY THE ACCOUNT LIST — provide EXACTLY ONE of:\n                  • `companies`: inline `{<companyname>: <companywebsite>}` map (short ad-hoc lists).\n                  • `companies_source_csv_url`: URL of a CSV with header `companyname,companywebsite` (case-insensitive). The MCP server downloads, validates, and uploads it as a native ABM list.\n\n                CRITERIA (all optional, LinkedIn-native shapes resolved server-side):\n                  • employees — LinkedIn employee ranges (e.g. \"201-500\", \"501-1000\").\n                  • revenues — LinkedIn revenue ranges (e.g. \"$1M-$10M\").\n                  • company_names — free-text company names (resolved to LinkedIn IDs).\n                  • location_country_ids — country IDs (e.g. 229=US).\n                  • job_titles — free-text titles (resolved to LinkedIn IDs).\n                  • skills — free-text skills (resolved to LinkedIn IDs).\n\n                RETURNS: id, audience_id, audience_name, audience_type (NATIVE_TARGETING_CSV), status, abmSearchCriteriaId, companies_count, upload_filename, counts.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "audience_name": {
      "type": "string",
      "minLength": 1,
      "description": "Name for the new audience (required)."
     },
     "companies": {
      "type": "object",
      "description": "Inline map of company names to website URLs (optional). MUTUALLY EXCLUSIVE with `companies_source_csv_url`. Example: {\"Acme Corp\": \"https://acme.com\"}",
      "additionalProperties": {
       "type": "string",
       "description": "Company website URL"
      },
      "minProperties": 1
     },
     "companies_source_csv_url": {
      "type": "string",
      "description": "Public URL of a CSV with header EXACTLY `companyname,companywebsite` (case-insensitive). Use when the user attached a CSV to the chat — the URL comes via `AudienceBrief.attached_file_urls`. MUTUALLY EXCLUSIVE with `companies`.",
      "format": "uri"
     },
     "location_country_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Country IDs for geographic targeting (optional). Valid IDs: 1 (Afghanistan), 2 (Albania), 3 (Algeria), 4 (American Samoa), 5 (Andorra), 6 (Angola), 7 (Anguilla), 8 (Antarctica), 9 (Antigua and Barbuda), 10 (Argentina), 11 (Armenia), 12 (Aruba), 13 (Australia), 14 (Austria), 15 (Azerbaijan), 16 (Bahamas), 17 (Bahrain), 18 (Bangladesh), 19 (Barbados), 20 (Belarus), 21 (Belgium), 22 (Belize), 23 (Benin), 24 (Bermuda), 25 (Bhutan), 26 (Bolivia), 27 (Bosnia and Herzegovina), 28 (Botswana), 29 (Brazil), 30 (British Indian Ocean Territory), 31 (British Virgin Islands), 32 (Brunei), 33 (Bulgaria), 34 (Burkina Faso), 35 (Burundi), 36 (Cambodia), 37 (Cameroon), 38 (Canada), 39 (Cape Verde), 40 (Cayman Islands), 41 (Central African Republic), 42 (Chad), 43 (Chile), 44 (China), 45 (Christmas Island), 46 (Cocos Islands), 47 (Colombia), 48 (Comoros), 49 (Cook Islands), 50 (Costa Rica), 51 (Croatia), 53 (Curacao), 54 (Cyprus), 55 (Czech Republic), 56 (Democratic Republic of the Congo), 57 (Denmark), 58 (Djibouti), 59 (Dominica), 60 (Dominican Republic), 61 (East Timor), 62 (Ecuador), 63 (Egypt), 64 (El Salvador), 65 (Equatorial Guinea), 66 (Eritrea), 67 (Estonia), 68 (Ethiopia), 69 (Falkland Islands), 70 (Faroe Islands), 71 (Fiji), 72 (Finland), 73 (France), 74 (French Polynesia), 75 (Gabon), 76 (Gambia), 77 (Georgia), 78 (Germany), 79 (Ghana), 80 (Gibraltar), 81 (Greece), 82 (Greenland), 83 (Grenada), 84 (Guam), 85 (Guatemala), 86 (Guernsey), 87 (Guinea), 88 (Guinea-Bissau), 89 (Guyana), 90 (Haiti), 91 (Honduras), 92 (Hong Kong), 93 (Hungary), 94 (Iceland), 95 (India), 96 (Indonesia), 98 (Iraq), 99 (Ireland), 100 (Isle of Man), 101 (Israel), 102 (Italy), 103 (Ivory Coast), 104 (Jamaica), 105 (Japan), 106 (Jersey), 107 (Jordan), 108 (Kazakhstan), 109 (Kenya), 110 (Kiribati), 111 (Kosovo), 112 (Kuwait), 113 (Kyrgyzstan), 114 (Laos), 115 (Latvia), 116 (Lebanon), 117 (Lesotho), 118 (Liberia), 119 (Libya), 120 (Liechtenstein), 121 (Lithuania), 122 (Luxembourg), 123 (Macau), 124 (Macedonia), 125 (Madagascar), 126 (Malawi), 127 (Malaysia), 128 (Maldives), 129 (Mali), 130 (Malta), 131 (Marshall Islands), 132 (Mauritania), 133 (Mauritius), 134 (Mayotte), 135 (Mexico), 136 (Micronesia), 137 (Moldova), 138 (Monaco), 139 (Mongolia), 140 (Montenegro), 141 (Montserrat), 142 (Morocco), 143 (Mozambique), 144 (Myanmar), 145 (Namibia), 146 (Nauru), 147 (Nepal), 148 (Netherlands), 149 (Netherlands Antilles), 150 (New Caledonia), 151 (New Zealand), 152 (Nicaragua), 153 (Niger), 154 (Nigeria), 155 (Niue), 157 (Northern Mariana Islands), 158 (Norway), 159 (Oman), 160 (Pakistan), 161 (Palau), 162 (Palestine), 163 (Panama), 164 (Papua New Guinea), 165 (Paraguay), 166 (Peru), 167 (Philippines), 168 (Pitcairn), 169 (Poland), 170 (Portugal), 171 (Puerto Rico), 172 (Qatar), 173 (Republic of the Congo), 174 (Reunion), 175 (Romania), 176 (Russia), 177 (Rwanda), 178 (Saint Barthelemy), 179 (Saint Helena), 180 (Saint Kitts and Nevis), 181 (Saint Lucia), 182 (Saint Martin), 183 (Saint Pierre and Miquelon), 184 (Saint Vincent and the Grenadines), 185 (Samoa), 186 (San Marino), 187 (Sao Tome and Principe), 188 (Saudi Arabia), 189 (Senegal), 190 (Serbia), 191 (Seychelles), 192 (Sierra Leone), 193 (Singapore), 194 (Sint Maarten), 195 (Slovakia), 196 (Slovenia), 197 (Solomon Islands), 198 (Somalia), 199 (South Africa), 200 (South Korea), 201 (South Sudan), 202 (Spain), 203 (Sri Lanka), 205 (Suriname), 206 (Svalbard and Jan Mayen), 207 (Swaziland), 208 (Sweden), 209 (Switzerland), 211 (Taiwan), 212 (Tajikistan), 213 (Tanzania), 214 (Thailand), 215 (Togo), 216 (Tokelau), 217 (Tonga), 218 (Trinidad and Tobago), 219 (Tunisia), 220 (Turkey), 221 (Turkmenistan), 222 (Turks and Caicos Islands), 223 (Tuvalu), 224 (U.S. Virgin Islands), 225 (Uganda), 226 (Ukraine), 227 (United Arab Emirates), 228 (United Kingdom), 229 (United States), 230 (Uruguay), 231 (Uzbekistan), 232 (Vanuatu), 233 (Vatican), 234 (Venezuela), 235 (Vietnam), 236 (Wallis and Futuna), 237 (Western Sahara), 238 (Yemen), 239 (Zambia), 240 (Zimbabwe), 241 (Guadeloupe). Example: [29] for Brazil, [229] for United States"
     },
     "employees": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "1",
        "2-10",
        "11-50",
        "51-200",
        "201-500",
        "501-1000",
        "1001-5000",
        "5001-10000",
        "+10001"
       ]
      },
      "description": "LinkedIn employee count ranges for audience targeting. Valid values: 1, 2-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, +10001. Example: ['201-500', '501-1000']"
     },
     "revenues": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "Under $1M",
        "$1M-$10M",
        "$10M-$100M",
        "$100M-$1B",
        "$1B+"
       ]
      },
      "description": "LinkedIn revenue ranges for audience targeting. Valid values: Under $1M, $1M-$10M, $10M-$100M, $100M-$1B, $1B+. Example: ['$1M-$10M', '$10M-$100M']"
     },
     "company_names": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free-text company names (resolved to LinkedIn company IDs). Example: ['Metadata', 'Google']"
     },
     "job_titles": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free-text job titles (resolved to LinkedIn job-title IDs). Example: ['Software Engineer', 'Product Manager']"
     },
     "skills": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free-text professional skills (resolved to LinkedIn skill IDs). Example: ['HubSpot', 'Salesforce']"
     }
    },
    "required": [
     "audience_name"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_facebook_native_criteria_audience",
   "title": "Create Facebook Native Criteria Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Create a Facebook native criteria audience using Facebook-supported targeting criteria.",
   "description": "Create a Facebook native criteria audience using Facebook-supported targeting criteria.\n                Build audiences based on Facebook native industries, geographic location, job titles, interests, and age range.\n\n                ESTIMATE-ONLY MODE:\n                Pass estimate_only=true to preview audience size WITHOUT creating the audience. In this mode the tool\n                returns expectedNumberOfContacts. Default is false.\n\n                ALSO KNOWN AS: Facebook audience, FB criteria audience, native Facebook targeting, Facebook lead audience\n\n                KEYWORDS: Facebook, native, criteria, audience, industry, location, job title, interest, age, lead, targeting\n\n                WHEN TO USE:\n                - Create targeted Facebook audiences using native Facebook criteria\n                - Target users by Facebook industry categories (e.g., IT and Technical Services, Sales, Management)\n                - Target by geographic countries, job titles, interests, and age range\n                - Build lead generation audiences for Facebook campaigns\n\n                INDUSTRIES:\n                Facebook native industry categories - provide the industry name as a string:\n                Administrative Services, Architecture and Engineering, Arts Entertainment Sports and Media,\n                Business and Finance, Business decision maker titles and interests, Cleaning and Maintenance Services,\n                Community and Social Services, Computation and Mathematics, Construction and Extraction,\n                Education and Libraries, Farming Fishing and Forestry, Food and Restaurants,\n                Government Employees (Global), Healthcare and Medical Services, Installation and Repair Services,\n                IT and Technical Services, Large business-to-business enterprise employees (500+ employees),\n                Legal Services, Life Physical and Social Sciences, Management,\n                Medium business-to-business enterprise employees (200 - 500 employees), Production, Sales,\n                Small business-to-business enterprise employees (10-200 employees), Transportation and Moving\n\n                PARAMETERS:\n                - name: Audience name (required)\n                - min_age: Minimum age for targeting (required) - must be between 21 and 65\n                - max_age: Maximum age for targeting (required) - must be between 21 and 65, and >= min_age\n                - industries: Array of Facebook native industry names (optional)\n                - countries: List of country names for location targeting (optional) - each must match a country from the platform's supported countries list\n                - job_titles: Array of free-text job title strings (optional) - e.g., [\"Software Engineer\", \"Product Owner\"]\n                - interests: Array of free-text interest strings (optional) - e.g., [\"Java\", \"Python\", \"Cloud Computing\"]\n\n                RETURNS:\n                Parsed audience criteria with resolved Facebook reference IDs and validated parameters.\n\n                IMPORTANT NOTES:\n                - name, min_age and max_age are required, all other parameters are optional\n                - Industries must be valid Facebook native industry names from the enum list\n                - Countries must be valid country names from the platform's supported countries list\n                - min_age and max_age must both be between 21 and 65, with min_age <= max_age\n                - Job titles and interests are free-text arrays\n                - Job titles and interests are validated by the backend API with the regex pattern [A-Za-z0-9 ]+ (only ASCII letters, digits, and spaces are allowed). Non-ASCII characters, hyphens, and special characters will be automatically replaced with spaces before resolving references (e.g., \"co-founder\" becomes \"co founder\"). When providing job titles or interests, use only alphanumeric characters and spaces.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "Audience name (required)."
     },
     "industries": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "Administrative Services",
        "Architecture and Engineering",
        "Arts, Entertainment, Sports and Media",
        "Business and Finance",
        "Business decision maker titles and interests",
        "Cleaning and Maintenance Services",
        "Community and Social Services",
        "Computation and Mathematics",
        "Construction and Extraction",
        "Education and Libraries",
        "Farming, Fishing and Forestry",
        "Food and Restaurants",
        "Government Employees (Global)",
        "Healthcare and Medical Services",
        "Installation and Repair Services",
        "IT and Technical Services",
        "Large business-to-business enterprise employees (500+ employees)",
        "Legal Services",
        "Life, Physical and Social Sciences",
        "Management",
        "Medium business-to-business enterprise employees (200 - 500 employees)",
        "Production",
        "Sales",
        "Small business-to-business enterprise employees (10-200 employees)",
        "Transportation and Moving"
       ]
      },
      "description": "Facebook native industry categories for audience targeting. Valid values: Administrative Services, Architecture and Engineering, Arts, Entertainment, Sports and Media, Business and Finance, Business decision maker titles and interests, Cleaning and Maintenance Services, Community and Social Services, Computation and Mathematics, Construction and Extraction, Education and Libraries, Farming, Fishing and Forestry, Food and Restaurants, Government Employees (Global), Healthcare and Medical Services, Installation and Repair Services, IT and Technical Services, Large business-to-business enterprise employees (500+ employees), Legal Services, Life, Physical and Social Sciences, Management, Medium business-to-business enterprise employees (200 - 500 employees), Production, Sales, Small business-to-business enterprise employees (10-200 employees), Transportation and Moving. Example: ['IT and Technical Services', 'Sales']"
     },
     "countries": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "Afghanistan",
        "Albania",
        "Algeria",
        "American Samoa",
        "Andorra",
        "Angola",
        "Anguilla",
        "Antarctica",
        "Antigua and Barbuda",
        "Argentina",
        "Armenia",
        "Aruba",
        "Australia",
        "Austria",
        "Azerbaijan",
        "Bahamas",
        "Bahrain",
        "Bangladesh",
        "Barbados",
        "Belarus",
        "Belgium",
        "Belize",
        "Benin",
        "Bermuda",
        "Bhutan",
        "Bolivia",
        "Bosnia and Herzegovina",
        "Botswana",
        "Brazil",
        "British Indian Ocean Territory",
        "British Virgin Islands",
        "Brunei",
        "Bulgaria",
        "Burkina Faso",
        "Burundi",
        "Cambodia",
        "Cameroon",
        "Canada",
        "Cape Verde",
        "Cayman Islands",
        "Central African Republic",
        "Chad",
        "Chile",
        "China",
        "Christmas Island",
        "Cocos Islands",
        "Colombia",
        "Comoros",
        "Cook Islands",
        "Costa Rica",
        "Croatia",
        "Curacao",
        "Cyprus",
        "Czech Republic",
        "Democratic Republic of the Congo",
        "Denmark",
        "Djibouti",
        "Dominica",
        "Dominican Republic",
        "East Timor",
        "Ecuador",
        "Egypt",
        "El Salvador",
        "Equatorial Guinea",
        "Eritrea",
        "Estonia",
        "Ethiopia",
        "Falkland Islands",
        "Faroe Islands",
        "Fiji",
        "Finland",
        "France",
        "French Polynesia",
        "Gabon",
        "Gambia",
        "Georgia",
        "Germany",
        "Ghana",
        "Gibraltar",
        "Greece",
        "Greenland",
        "Grenada",
        "Guam",
        "Guatemala",
        "Guernsey",
        "Guinea",
        "Guinea-Bissau",
        "Guyana",
        "Haiti",
        "Honduras",
        "Hong Kong",
        "Hungary",
        "Iceland",
        "India",
        "Indonesia",
        "Iraq",
        "Ireland",
        "Isle of Man",
        "Israel",
        "Italy",
        "Ivory Coast",
        "Jamaica",
        "Japan",
        "Jersey",
        "Jordan",
        "Kazakhstan",
        "Kenya",
        "Kiribati",
        "Kosovo",
        "Kuwait",
        "Kyrgyzstan",
        "Laos",
        "Latvia",
        "Lebanon",
        "Lesotho",
        "Liberia",
        "Libya",
        "Liechtenstein",
        "Lithuania",
        "Luxembourg",
        "Macau",
        "Macedonia",
        "Madagascar",
        "Malawi",
        "Malaysia",
        "Maldives",
        "Mali",
        "Malta",
        "Marshall Islands",
        "Mauritania",
        "Mauritius",
        "Mayotte",
        "Mexico",
        "Micronesia",
        "Moldova",
        "Monaco",
        "Mongolia",
        "Montenegro",
        "Montserrat",
        "Morocco",
        "Mozambique",
        "Myanmar",
        "Namibia",
        "Nauru",
        "Nepal",
        "Netherlands",
        "Netherlands Antilles",
        "New Caledonia",
        "New Zealand",
        "Nicaragua",
        "Niger",
        "Nigeria",
        "Niue",
        "Northern Mariana Islands",
        "Norway",
        "Oman",
        "Pakistan",
        "Palau",
        "Palestine",
        "Panama",
        "Papua New Guinea",
        "Paraguay",
        "Peru",
        "Philippines",
        "Pitcairn",
        "Poland",
        "Portugal",
        "Puerto Rico",
        "Qatar",
        "Republic of the Congo",
        "Reunion",
        "Romania",
        "Russia",
        "Rwanda",
        "Saint Barthelemy",
        "Saint Helena",
        "Saint Kitts and Nevis",
        "Saint Lucia",
        "Saint Martin",
        "Saint Pierre and Miquelon",
        "Saint Vincent and the Grenadines",
        "Samoa",
        "San Marino",
        "Sao Tome and Principe",
        "Saudi Arabia",
        "Senegal",
        "Serbia",
        "Seychelles",
        "Sierra Leone",
        "Singapore",
        "Sint Maarten",
        "Slovakia",
        "Slovenia",
        "Solomon Islands",
        "Somalia",
        "South Africa",
        "South Korea",
        "South Sudan",
        "Spain",
        "Sri Lanka",
        "Suriname",
        "Svalbard and Jan Mayen",
        "Swaziland",
        "Sweden",
        "Switzerland",
        "Taiwan",
        "Tajikistan",
        "Tanzania",
        "Thailand",
        "Togo",
        "Tokelau",
        "Tonga",
        "Trinidad and Tobago",
        "Tunisia",
        "Turkey",
        "Turkmenistan",
        "Turks and Caicos Islands",
        "Tuvalu",
        "U.S. Virgin Islands",
        "Uganda",
        "Ukraine",
        "United Arab Emirates",
        "United Kingdom",
        "United States",
        "Uruguay",
        "Uzbekistan",
        "Vanuatu",
        "Vatican",
        "Venezuela",
        "Vietnam",
        "Wallis and Futuna",
        "Western Sahara",
        "Yemen",
        "Zambia",
        "Zimbabwe",
        "Guadeloupe"
       ]
      },
      "description": "List of country names to target. Each must be a valid country name (e.g., ['United States', 'Brazil']). Resolved to platform country IDs before submission."
     },
     "job_titles": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free-text job title strings for targeting. Example: ['Software Engineer', 'Senior Developer', 'Product Owner']"
     },
     "interests": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free-text interest strings for targeting. Example: ['Java', 'Python', 'Cloud Computing', 'Machine Learning']"
     },
     "min_age": {
      "type": "integer",
      "minimum": 21,
      "maximum": 65,
      "description": "Minimum age for audience targeting. Must be between 21 and 65, and less than or equal to max_age."
     },
     "max_age": {
      "type": "integer",
      "minimum": 21,
      "maximum": 65,
      "description": "Maximum age for audience targeting. Must be between 21 and 65, and greater than or equal to min_age."
     },
     "estimate_only": {
      "type": "boolean",
      "description": "If true, do NOT create the audience — only estimate its size and return the expected contact count. Use this when the user wants to preview Facebook audience size before committing. Defaults to false (audience is created).",
      "default": false
     }
    },
    "required": [
     "name",
     "min_age",
     "max_age"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_firmographic_audience",
   "title": "Create Firmographic Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Create B2B company firmographic audiences for targeted advertising campaigns.",
   "description": "Create B2B company firmographic audiences for targeted advertising campaigns.\n                Build audiences based on company attributes (industry, size, revenue) and contact-level criteria.\n\n                ESTIMATE-ONLY MODE:\n                Pass estimate_only=true to preview audience size (companies + contacts) WITHOUT creating the audience.\n                In this mode the tool returns expectedNumberOfCompanies,\n                expectedNumberOfContacts, and cappedContactCount. Default is false (audience is created).\n\n                PREFERRED AUDIENCE TYPE:\n                This is the DEFAULT and most commonly used audience type on the Metadata platform.\n                When the user asks to create an audience without specifying a type, prefer this tool.\n                Only suggest Bombora audiences if get_intent_topics returns available topics.\n                Only suggest G2 audiences if G2 is confirmed as connected via get_integrations_status\n                and the user specifically requests G2 targeting.\n\n                ALSO KNOWN AS: company audience, target companies, B2B audience, business targeting, account-based audience, ABM list, company targeting\n\n                KEYWORDS: company, companies, audience, target, targeting, B2B, business, firmographic, industry, size, employees, revenue, ABM, account-based, LinkedIn, Facebook\n\n                WHEN TO USE:\n                - Default choice when the user wants to create a B2B audience\n                - Create targeted B2B audiences for LinkedIn, Facebook campaigns\n                - Target companies by industry, size, revenue\n                - Target contacts by seniority level and job function\n                - Set geographic targeting and contact limits per company\n\n                TARGETING OPTIONS:\n                - Industries: Use industry IDs. Examples:\n                  1=Defense & Space, 4=Computer Software, 6=Internet, 7=Semiconductors, 8=Telecommunications,\n                  11=Management Consulting, 12=Biotechnology, 41=Banking, 42=Insurance, 43=Financial Services,\n                  80=Marketing & Advertising, 96=Information Technology & Services, more values in detail in the input schema\n                - Employees: \"0 - 9\", \"10 - 49\", \"50 - 199\", \"200 - 499\", \"500 - 999\", \"1,000 - 4,999\", \"5,000 - 9,999\", \"10,000+\"\n                - Revenue: \"$1M - $10M\", \"$10M - $50M\", \"$50M - $100M\", \"$100M - $200M\", \"$500M - $1B\", \"$1B+\"\n\n                CONTACTS CRITERIA:\nUse the following parameters to filter and target specific contacts within accounts:\n\nGEOGRAPHIC TARGETING:\n- location_country_ids: Array of country IDs (e.g., [29] for Brazil, [229] for United States)\n- location_state_ids: Array of state IDs (e.g., [5] for California, [32] for New York)\n\nJOB TITLES:\n- job_title_includes: Array of free text strings to include (e.g., [\"Software Engineer\", \"Manager\"])\n- job_title_excludes: Array of free text strings to exclude (e.g., [\"Senior\", \"Lead\"])\n\nJOB FUNCTIONS:\nValid job function IDs and their labels:\n- 1: Marketing, 2: Sales, 3: Customer Service, 4: Finance, 5: Human Resources, 6: Public Relations\n- 7: Media, 8: Design, 9: Engineering, 10: Operations, 11: Legal, 12: Education\n- 13: Health, 14: Real Estate, 15: Accounting, 16: Administrative, 17: Business Development\n- 18: Community and Social Services, 19: Consulting, 20: Entrepreneurship, 21: Information Technology\n- 22: Military and Protective Services, 23: Product Management, 24: Program and Project Management\n- 25: Purchasing, 26: Quality Assurance, 27: Research, 28: Trades\n\n- job_function_include_ids: Array of job function IDs to include (e.g., [9] for Engineering)\n- job_function_exclude_ids: Array of job function IDs to exclude\n\nSENIORITY LEVELS:\nValid seniority IDs and their labels:\n- 2: Manager, 3: Director, 4: VP, 5: CXO, 6: Unpaid, 7: Training\n- 8: Senior, 9: Owner, 10: Entry, 11: Partner\n\n- seniority_include_ids: Array of seniority level IDs to include (e.g., [10, 7] for Entry and Training)\n- seniority_exclude_ids: Array of seniority level IDs to exclude\n\nCONTACT LIMITS:\n- contacts_per_company_limit: Maximum number of contacts to target per company (optional). Example: 20\n\nUSAGE NOTES:\n- All ID fields (countries, states, job functions, seniorities) use numeric IDs from the platform\n- Job titles are free text strings, not IDs\n- Use include parameters to specify which criteria to target\n- Use exclude parameters to filter out contacts matching specific criteria\n- Combine multiple criteria for precise targeting (e.g., Senior Engineers in California)\n- Criteria are combined with AND logic, so all conditions must be met\n\nCONTACTS CRITERIA PARAMETERS(Usually after other initial audience parameters to be described next):\n- location_country_ids: Array of country IDs (optional)\n- location_state_ids: Array of state IDs (optional)\n- job_title_includes: Array of job title keywords to include (optional)\n- job_title_excludes: Array of job title keywords to exclude (optional)\n- job_function_include_ids: Array of job function IDs (optional)\n- job_function_exclude_ids: Array of job function IDs (optional)\n- seniority_include_ids: Array of seniority level IDs (optional)\n- seniority_exclude_ids: Array of seniority level IDs (optional)\n- contacts_per_company_limit: Maximum contacts per company (optional)\n\n\n                PARAMETERS:\n                - name: Audience name (required) - **IMPORTANT: Name MUST NOT exceed 50 characters. This is a mandatory requirement.**\n                - industries: Array of industry IDs (optional)\n                - employees: Array of employee count ranges (optional)\n                - revenue: Array of revenue ranges (optional)\n\n                RETURNS:\n                Audience details with ID, size estimation, and creation status.\n\n                COMMON USE CASES:\n                - Target software companies with 50-500 employees\n                - Find companies in specific industries with revenue ranges\n                - Build audiences with contact-level filtering (seniority, job function, location)\n                - Combine firmographic and contact criteria for precise B2B targeting\n\n                IMPORTANT NOTES:\n                - **CRITICAL: Audience name MUST NOT exceed 50 characters - this is mandatory and will cause creation to fail if exceeded**\n                - name is the only required parameter\n                - All other parameters are optional for flexible audience creation\n                - Contact criteria fields work the same as in G2 metadata dynamic audiences\n                - Audience size depends on firmographic filters and applied contacts criteria",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "Audience name (required)"
     },
     "industries": {
      "type": "array",
      "items": {
       "type": "integer",
       "enum": [
        1,
        3,
        4,
        5,
        6,
        7,
        8,
        9,
        10,
        11,
        12,
        13,
        14,
        15,
        16,
        17,
        18,
        19,
        20,
        21,
        22,
        23,
        24,
        25,
        26,
        27,
        28,
        29,
        30,
        31,
        32,
        33,
        34,
        35,
        36,
        37,
        38,
        39,
        40,
        41,
        42,
        43,
        44,
        45,
        46,
        47,
        48,
        49,
        50,
        51,
        52,
        53,
        54,
        55,
        56,
        57,
        58,
        59,
        60,
        61,
        62,
        63,
        64,
        65,
        66,
        67,
        68,
        69,
        70,
        71,
        72,
        73,
        74,
        75,
        76,
        77,
        78,
        79,
        80,
        81,
        82,
        83,
        84,
        85,
        86,
        87,
        88,
        89,
        90,
        91,
        92,
        93,
        94,
        95,
        96,
        97,
        98,
        99,
        100,
        101,
        102,
        103,
        104,
        105,
        106,
        107,
        108,
        109,
        110,
        111,
        112,
        113,
        114,
        115,
        116,
        117,
        118,
        119,
        120,
        121,
        122,
        123,
        124,
        125,
        126,
        127,
        128,
        129,
        130,
        131,
        132,
        133,
        134,
        135,
        136,
        137,
        138,
        139,
        140,
        141,
        142,
        143,
        144,
        145,
        146,
        147,
        148
       ]
      },
      "description": "Industry IDs for firmographic/technographic filtering (optional). Valid IDs: 1=Defense & Space, 3=Computer Hardware, 4=Computer Software, 5=Computer Networking, 6=Internet, 7=Semiconductors, 8=Telecommunications, 9=Law Practice, 10=Legal Services, 11=Management Consulting, 12=Biotechnology, 13=Medical Practice, 14=Hospital & Health Care, 15=Pharmaceuticals, 16=Veterinary, 17=Medical Device, 18=Cosmetics, 19=Apparel & Fashion, 20=Sporting Goods, 21=Tobacco, 22=Supermarkets, 23=Food Production, 24=Consumer Electronics, 25=Consumer Goods, 26=Furniture, 27=Retail, 28=Entertainment, 29=Gambling & Casinos, 30=Leisure, Travel & Tourism, 31=Hospitality, 32=Restaurants, 33=Sports, 34=Food & Beverages, 35=Motion Pictures & Film, 36=Broadcast Media, 37=Museums & Institutions, 38=Fine Art, 39=Performing Arts, 40=Recreational Facilities & Services, 41=Banking, 42=Insurance, 43=Financial Services, 44=Real Estate, 45=Investment Banking, 46=Investment Management, 47=Accounting, 48=Construction, 49=Building Materials, 50=Architecture & Planning, 51=Civil Engineering, 52=Aviation & Aerospace, 53=Automotive, 54=Chemicals, 55=Machinery, 56=Mining & Metals, 57=Oil & Energy, 58=Shipbuilding, 59=Utilities, 60=Textiles, 61=Paper & Forest Products, 62=Railroad Manufacture, 63=Farming, 64=Ranching, 65=Dairy, 66=Fishery, 67=Primary/Secondary Education, 68=Higher Education, 69=Education Management, 70=Research, 71=Military, 72=Legislative Office, 73=Judiciary, 74=International Affairs, 75=Government Administration, 76=Executive Office, 77=Law Enforcement, 78=Public Safety, 79=Public Policy, 80=Marketing & Advertising, 81=Newspapers, 82=Publishing, 83=Printing, 84=Information Services, 85=Libraries, 86=Environmental Services, 87=Package/Freight Delivery, 88=Individual & Family Services, 89=Religious Institutions, 90=Civic & Social Organization, 91=Consumer Services, 92=Transportation/Trucking/Railroad, 93=Warehousing, 94=Airlines/Aviation, 95=Maritime, 96=Information Technology & Services, 97=Market Research, 98=Public Relations & Communications, 99=Design, 100=Non-profit Organization Management, 101=Fundraising, 102=Program Development, 103=Writing & Editing, 104=Staffing & Recruiting, 105=Professional Training & Coaching, 106=Venture Capital & Private Equity, 107=Political Organization, 108=Translation & Localization, 109=Computer Games, 110=Events Services, 111=Arts & Crafts, 112=Electrical & Electronic Manufacturing, 113=Online Media, 114=Nanotechnology, 115=Music, 116=Logistics & Supply Chain, 117=Plastics, 118=Computer & Network Security, 119=Wireless, 120=Alternative Dispute Resolution, 121=Security & Investigations, 122=Facilities Services, 123=Outsourcing/Offshoring, 124=Health, Wellness & Fitness, 125=Alternative Medicine, 126=Media Production, 127=Animation, 128=Commercial Real Estate, 129=Capital Markets, 130=Think Tanks, 131=Philanthropy, 132=E-learning, 133=Wholesale, 134=Import & Export, 135=Mechanical Or Industrial Engineering, 136=Photography, 137=Human Resources, 138=Business Supplies & Equipment, 139=Mental Health Care, 140=Graphic Design, 141=International Trade & Development, 142=Wine & Spirits, 143=Luxury Goods & Jewelry, 144=Renewables & Environment, 145=Glass, Ceramics & Concrete, 146=Packaging & Containers, 147=Industrial Automation, 148=Government Relations. Example: [4, 96, 11] for Software, IT Services, Consulting"
     },
     "employees": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "LESS_THAN_10",
        "FROM_10_TO_50",
        "FROM_50_TO_200",
        "FROM_200_TO_500",
        "FROM_500_TO_1000",
        "FROM_1000_TO_5000",
        "FROM_5000_TO_10000",
        "OVER_10000"
       ]
      },
      "description": "Employee count ranges. Valid enum values: LESS_THAN_10 (0-9), FROM_10_TO_50 (10-49), FROM_50_TO_200 (50-199), FROM_200_TO_500 (200-499), FROM_500_TO_1000 (500-999), FROM_1000_TO_5000 (1,000-4,999), FROM_5000_TO_10000 (5,000-9,999), OVER_10000 (10,000+)"
     },
     "revenue": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "LESS_THAN_1M",
        "FROM_1M_TO_10M",
        "FROM_10M_TO_50M",
        "FROM_50M_TO_100M",
        "FROM_100M_TO_200M",
        "FROM_200M_TO_500M",
        "FROM_500M_TO_1000M",
        "OVER_1000M"
       ]
      },
      "description": "Revenue ranges. Valid enum values: LESS_THAN_1M ($0-$1M), FROM_1M_TO_10M ($1M-$10M), FROM_10M_TO_50M ($10M-$50M), FROM_50M_TO_100M ($50M-$100M), FROM_100M_TO_200M ($100M-$200M), FROM_200M_TO_500M ($200M-$500M), FROM_500M_TO_1000M ($500M-$1B), OVER_1000M ($1B+)"
     },
     "location_country_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Country IDs for geographic targeting (optional). Valid IDs: 1 (Afghanistan), 2 (Albania), 3 (Algeria), 4 (American Samoa), 5 (Andorra), 6 (Angola), 7 (Anguilla), 8 (Antarctica), 9 (Antigua and Barbuda), 10 (Argentina), 11 (Armenia), 12 (Aruba), 13 (Australia), 14 (Austria), 15 (Azerbaijan), 16 (Bahamas), 17 (Bahrain), 18 (Bangladesh), 19 (Barbados), 20 (Belarus), 21 (Belgium), 22 (Belize), 23 (Benin), 24 (Bermuda), 25 (Bhutan), 26 (Bolivia), 27 (Bosnia and Herzegovina), 28 (Botswana), 29 (Brazil), 30 (British Indian Ocean Territory), 31 (British Virgin Islands), 32 (Brunei), 33 (Bulgaria), 34 (Burkina Faso), 35 (Burundi), 36 (Cambodia), 37 (Cameroon), 38 (Canada), 39 (Cape Verde), 40 (Cayman Islands), 41 (Central African Republic), 42 (Chad), 43 (Chile), 44 (China), 45 (Christmas Island), 46 (Cocos Islands), 47 (Colombia), 48 (Comoros), 49 (Cook Islands), 50 (Costa Rica), 51 (Croatia), 53 (Curacao), 54 (Cyprus), 55 (Czech Republic), 56 (Democratic Republic of the Congo), 57 (Denmark), 58 (Djibouti), 59 (Dominica), 60 (Dominican Republic), 61 (East Timor), 62 (Ecuador), 63 (Egypt), 64 (El Salvador), 65 (Equatorial Guinea), 66 (Eritrea), 67 (Estonia), 68 (Ethiopia), 69 (Falkland Islands), 70 (Faroe Islands), 71 (Fiji), 72 (Finland), 73 (France), 74 (French Polynesia), 75 (Gabon), 76 (Gambia), 77 (Georgia), 78 (Germany), 79 (Ghana), 80 (Gibraltar), 81 (Greece), 82 (Greenland), 83 (Grenada), 84 (Guam), 85 (Guatemala), 86 (Guernsey), 87 (Guinea), 88 (Guinea-Bissau), 89 (Guyana), 90 (Haiti), 91 (Honduras), 92 (Hong Kong), 93 (Hungary), 94 (Iceland), 95 (India), 96 (Indonesia), 98 (Iraq), 99 (Ireland), 100 (Isle of Man), 101 (Israel), 102 (Italy), 103 (Ivory Coast), 104 (Jamaica), 105 (Japan), 106 (Jersey), 107 (Jordan), 108 (Kazakhstan), 109 (Kenya), 110 (Kiribati), 111 (Kosovo), 112 (Kuwait), 113 (Kyrgyzstan), 114 (Laos), 115 (Latvia), 116 (Lebanon), 117 (Lesotho), 118 (Liberia), 119 (Libya), 120 (Liechtenstein), 121 (Lithuania), 122 (Luxembourg), 123 (Macau), 124 (Macedonia), 125 (Madagascar), 126 (Malawi), 127 (Malaysia), 128 (Maldives), 129 (Mali), 130 (Malta), 131 (Marshall Islands), 132 (Mauritania), 133 (Mauritius), 134 (Mayotte), 135 (Mexico), 136 (Micronesia), 137 (Moldova), 138 (Monaco), 139 (Mongolia), 140 (Montenegro), 141 (Montserrat), 142 (Morocco), 143 (Mozambique), 144 (Myanmar), 145 (Namibia), 146 (Nauru), 147 (Nepal), 148 (Netherlands), 149 (Netherlands Antilles), 150 (New Caledonia), 151 (New Zealand), 152 (Nicaragua), 153 (Niger), 154 (Nigeria), 155 (Niue), 157 (Northern Mariana Islands), 158 (Norway), 159 (Oman), 160 (Pakistan), 161 (Palau), 162 (Palestine), 163 (Panama), 164 (Papua New Guinea), 165 (Paraguay), 166 (Peru), 167 (Philippines), 168 (Pitcairn), 169 (Poland), 170 (Portugal), 171 (Puerto Rico), 172 (Qatar), 173 (Republic of the Congo), 174 (Reunion), 175 (Romania), 176 (Russia), 177 (Rwanda), 178 (Saint Barthelemy), 179 (Saint Helena), 180 (Saint Kitts and Nevis), 181 (Saint Lucia), 182 (Saint Martin), 183 (Saint Pierre and Miquelon), 184 (Saint Vincent and the Grenadines), 185 (Samoa), 186 (San Marino), 187 (Sao Tome and Principe), 188 (Saudi Arabia), 189 (Senegal), 190 (Serbia), 191 (Seychelles), 192 (Sierra Leone), 193 (Singapore), 194 (Sint Maarten), 195 (Slovakia), 196 (Slovenia), 197 (Solomon Islands), 198 (Somalia), 199 (South Africa), 200 (South Korea), 201 (South Sudan), 202 (Spain), 203 (Sri Lanka), 205 (Suriname), 206 (Svalbard and Jan Mayen), 207 (Swaziland), 208 (Sweden), 209 (Switzerland), 211 (Taiwan), 212 (Tajikistan), 213 (Tanzania), 214 (Thailand), 215 (Togo), 216 (Tokelau), 217 (Tonga), 218 (Trinidad and Tobago), 219 (Tunisia), 220 (Turkey), 221 (Turkmenistan), 222 (Turks and Caicos Islands), 223 (Tuvalu), 224 (U.S. Virgin Islands), 225 (Uganda), 226 (Ukraine), 227 (United Arab Emirates), 228 (United Kingdom), 229 (United States), 230 (Uruguay), 231 (Uzbekistan), 232 (Vanuatu), 233 (Vatican), 234 (Venezuela), 235 (Vietnam), 236 (Wallis and Futuna), 237 (Western Sahara), 238 (Yemen), 239 (Zambia), 240 (Zimbabwe), 241 (Guadeloupe). Example: [29] for Brazil, [229] for United States"
     },
     "location_state_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "State IDs for geographic targeting (optional). Valid US State IDs: 1 (Alabama), 2 (Alaska), 3 (Arizona), 4 (Arkansas), 5 (California), 6 (Colorado), 7 (Connecticut), 8 (Delaware), 9 (Florida), 10 (Georgia), 11 (Hawaii), 12 (Idaho), 13 (Illinois), 14 (Indiana), 15 (Iowa), 16 (Kansas), 17 (Kentucky), 18 (Louisiana), 19 (Maine), 20 (Maryland), 21 (Massachusetts), 22 (Michigan), 23 (Minnesota), 24 (Mississippi), 25 (Missouri), 26 (Montana), 27 (Nebraska), 28 (Nevada), 29 (New Hampshire), 30 (New Jersey), 31 (New Mexico), 32 (New York), 33 (North Carolina), 34 (North Dakota), 35 (Ohio), 36 (Oklahoma), 37 (Oregon), 38 (Pennsylvania), 39 (Rhode Island), 40 (South Carolina), 41 (South Dakota), 42 (Tennessee), 43 (Texas), 44 (Utah), 45 (Vermont), 46 (Virginia), 47 (Washington), 48 (West Virginia), 49 (Wisconsin), 50 (Wyoming), 52 (District of Columbia). Example: [5] for California, [32] for New York"
     },
     "job_title_includes": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free text keywords for job titles to include (optional). Example: ['Software Engineer', 'Manager']"
     },
     "job_title_excludes": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free text keywords for job titles to exclude (optional). Example: ['Senior', 'Lead']"
     },
     "job_function_include_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Job function IDs to include (optional). Valid IDs: 1 (Marketing), 2 (Sales), 3 (Customer Service), 4 (Finance), 5 (HR), 6 (PR), 7 (Media), 8 (Design), 9 (Engineering), 10 (Operations), 11 (Legal), 12 (Education), 13 (Health), 14 (Real Estate), 15 (Accounting), 16 (Administrative), 17 (Business Development), 18 (Community Services), 19 (Consulting), 20 (Entrepreneurship), 21 (IT), 22 (Military Services), 23 (Product Management), 24 (Program/Project Management), 25 (Purchasing), 26 (QA), 27 (Research), 28 (Trades). Example: [9] for Engineering"
     },
     "job_function_exclude_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Job function IDs to exclude (optional). Valid IDs: 1-28 as listed above"
     },
     "seniority_include_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Seniority level IDs to include (optional). Valid IDs: 2 (Manager), 3 (Director), 4 (VP), 5 (CXO), 6 (Unpaid), 7 (Training), 8 (Senior), 9 (Owner), 10 (Entry), 11 (Partner). Example: [10, 7] for Entry and Training"
     },
     "seniority_exclude_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Seniority level IDs to exclude (optional). Valid IDs: 2-11 as listed above"
     },
     "contacts_per_company_limit": {
      "type": "integer",
      "description": "Maximum number of contacts to target per company (optional). Example: 20",
      "minimum": 1
     },
     "estimate_only": {
      "type": "boolean",
      "description": "If true, do NOT create the audience — only estimate its size and return the counts. Use this when the user wants to preview how many companies/contacts match the criteria before committing. Defaults to false (audience is created).",
      "default": false
     }
    },
    "required": [
     "name"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_g2_intent_dynamic_audience",
   "title": "Create G2 Intent (Dynamic) Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Create a **G2 Intent (Dynamic)** audience (platform `customAudienceType=DYNAMIC_G2_CONTACT`).",
   "description": "Create a **G2 Intent (Dynamic)** audience (platform `customAudienceType=DYNAMIC_G2_CONTACT`).\n\n                AUDIENCE TYPE (mirrors the UI's \"Audience Type\" dropdown):\n                  • UI label: \"G2 Intent (Dynamic)\"\n                  • Platform enum: DYNAMIC_G2_CONTACT\n                  • Refreshes daily as G2 intent signals update; uses generic contact criteria (not LinkedIn-native).\n\n                PREREQUISITE:\n                  • G2 integration MUST be connected. If not, do NOT call this tool — recommend `create_firmographic_audience`.\n\n                WHEN TO USE (exact user phrasing this tool should match):\n                  • \"G2 Intent (Dynamic)\" (without \"LinkedIn Native\")\n                  • \"G2 intent dynamic audience\"\n                  • \"Dynamic G2 audience\"\n                  • The user wants G2 intent that refreshes daily on Facebook/LinkedIn/other channels (not specifically LinkedIn-native).\n\n                WHEN NOT TO USE:\n                  • If the user said \"LinkedIn Native\" → use `create_g2_intent_linkedin_native_dynamic_audience`.\n                  • If the user said \"Static\" → use `create_g2_intent_static_audience`.\n\n                BUYING STAGES — REQUIRED BY THE PLATFORM:\n                  Same as the LinkedIn Native variant: ASK the user if missing. DO NOT default.\n\n                CONTACTS CRITERIA:\nUse the following parameters to filter and target specific contacts within accounts:\n\nGEOGRAPHIC TARGETING:\n- location_country_ids: Array of country IDs (e.g., [29] for Brazil, [229] for United States)\n- location_state_ids: Array of state IDs (e.g., [5] for California, [32] for New York)\n\nJOB TITLES:\n- job_title_includes: Array of free text strings to include (e.g., [\"Software Engineer\", \"Manager\"])\n- job_title_excludes: Array of free text strings to exclude (e.g., [\"Senior\", \"Lead\"])\n\nJOB FUNCTIONS:\nValid job function IDs and their labels:\n- 1: Marketing, 2: Sales, 3: Customer Service, 4: Finance, 5: Human Resources, 6: Public Relations\n- 7: Media, 8: Design, 9: Engineering, 10: Operations, 11: Legal, 12: Education\n- 13: Health, 14: Real Estate, 15: Accounting, 16: Administrative, 17: Business Development\n- 18: Community and Social Services, 19: Consulting, 20: Entrepreneurship, 21: Information Technology\n- 22: Military and Protective Services, 23: Product Management, 24: Program and Project Management\n- 25: Purchasing, 26: Quality Assurance, 27: Research, 28: Trades\n\n- job_function_include_ids: Array of job function IDs to include (e.g., [9] for Engineering)\n- job_function_exclude_ids: Array of job function IDs to exclude\n\nSENIORITY LEVELS:\nValid seniority IDs and their labels:\n- 2: Manager, 3: Director, 4: VP, 5: CXO, 6: Unpaid, 7: Training\n- 8: Senior, 9: Owner, 10: Entry, 11: Partner\n\n- seniority_include_ids: Array of seniority level IDs to include (e.g., [10, 7] for Entry and Training)\n- seniority_exclude_ids: Array of seniority level IDs to exclude\n\nCONTACT LIMITS:\n- contacts_per_company_limit: Maximum number of contacts to target per company (optional). Example: 20\n\nUSAGE NOTES:\n- All ID fields (countries, states, job functions, seniorities) use numeric IDs from the platform\n- Job titles are free text strings, not IDs\n- Use include parameters to specify which criteria to target\n- Use exclude parameters to filter out contacts matching specific criteria\n- Combine multiple criteria for precise targeting (e.g., Senior Engineers in California)\n- Criteria are combined with AND logic, so all conditions must be met\n\nCONTACTS CRITERIA PARAMETERS(Usually after other initial audience parameters to be described next):\n- location_country_ids: Array of country IDs (optional)\n- location_state_ids: Array of state IDs (optional)\n- job_title_includes: Array of job title keywords to include (optional)\n- job_title_excludes: Array of job title keywords to exclude (optional)\n- job_function_include_ids: Array of job function IDs (optional)\n- job_function_exclude_ids: Array of job function IDs (optional)\n- seniority_include_ids: Array of seniority level IDs (optional)\n- seniority_exclude_ids: Array of seniority level IDs (optional)\n- contacts_per_company_limit: Maximum contacts per company (optional)\n\n\n                PARAMETERS:\n                  • name (required, ≤ 50 chars)\n                  • intent_days (required, 1-365)\n                  • buying_stages (REQUIRED by platform — ask if missing)\n                  • Contact criteria as in CONTACTS CRITERIA above.\n\n                RETURNS: id, audience_id, audience_name, audience_type (DYNAMIC_G2_CONTACT), status, buying_stages, intent_days, counts.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "maxLength": 50,
      "description": "Audience name (required, max 50 characters)."
     },
     "intent_days": {
      "type": "integer",
      "minimum": 1,
      "maximum": 365,
      "description": "Days to look back for G2 intent signals (required, 1-365)."
     },
     "buying_stages": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "AWARENESS",
        "CONSIDERATION",
        "DECISION"
       ]
      },
      "description": "Buying stages to target. The platform REQUIRES at least one; if the user did not specify, ASK them before calling — do NOT default.",
      "minItems": 1
     },
     "location_country_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Country IDs for geographic targeting (optional). Valid IDs: 1 (Afghanistan), 2 (Albania), 3 (Algeria), 4 (American Samoa), 5 (Andorra), 6 (Angola), 7 (Anguilla), 8 (Antarctica), 9 (Antigua and Barbuda), 10 (Argentina), 11 (Armenia), 12 (Aruba), 13 (Australia), 14 (Austria), 15 (Azerbaijan), 16 (Bahamas), 17 (Bahrain), 18 (Bangladesh), 19 (Barbados), 20 (Belarus), 21 (Belgium), 22 (Belize), 23 (Benin), 24 (Bermuda), 25 (Bhutan), 26 (Bolivia), 27 (Bosnia and Herzegovina), 28 (Botswana), 29 (Brazil), 30 (British Indian Ocean Territory), 31 (British Virgin Islands), 32 (Brunei), 33 (Bulgaria), 34 (Burkina Faso), 35 (Burundi), 36 (Cambodia), 37 (Cameroon), 38 (Canada), 39 (Cape Verde), 40 (Cayman Islands), 41 (Central African Republic), 42 (Chad), 43 (Chile), 44 (China), 45 (Christmas Island), 46 (Cocos Islands), 47 (Colombia), 48 (Comoros), 49 (Cook Islands), 50 (Costa Rica), 51 (Croatia), 53 (Curacao), 54 (Cyprus), 55 (Czech Republic), 56 (Democratic Republic of the Congo), 57 (Denmark), 58 (Djibouti), 59 (Dominica), 60 (Dominican Republic), 61 (East Timor), 62 (Ecuador), 63 (Egypt), 64 (El Salvador), 65 (Equatorial Guinea), 66 (Eritrea), 67 (Estonia), 68 (Ethiopia), 69 (Falkland Islands), 70 (Faroe Islands), 71 (Fiji), 72 (Finland), 73 (France), 74 (French Polynesia), 75 (Gabon), 76 (Gambia), 77 (Georgia), 78 (Germany), 79 (Ghana), 80 (Gibraltar), 81 (Greece), 82 (Greenland), 83 (Grenada), 84 (Guam), 85 (Guatemala), 86 (Guernsey), 87 (Guinea), 88 (Guinea-Bissau), 89 (Guyana), 90 (Haiti), 91 (Honduras), 92 (Hong Kong), 93 (Hungary), 94 (Iceland), 95 (India), 96 (Indonesia), 98 (Iraq), 99 (Ireland), 100 (Isle of Man), 101 (Israel), 102 (Italy), 103 (Ivory Coast), 104 (Jamaica), 105 (Japan), 106 (Jersey), 107 (Jordan), 108 (Kazakhstan), 109 (Kenya), 110 (Kiribati), 111 (Kosovo), 112 (Kuwait), 113 (Kyrgyzstan), 114 (Laos), 115 (Latvia), 116 (Lebanon), 117 (Lesotho), 118 (Liberia), 119 (Libya), 120 (Liechtenstein), 121 (Lithuania), 122 (Luxembourg), 123 (Macau), 124 (Macedonia), 125 (Madagascar), 126 (Malawi), 127 (Malaysia), 128 (Maldives), 129 (Mali), 130 (Malta), 131 (Marshall Islands), 132 (Mauritania), 133 (Mauritius), 134 (Mayotte), 135 (Mexico), 136 (Micronesia), 137 (Moldova), 138 (Monaco), 139 (Mongolia), 140 (Montenegro), 141 (Montserrat), 142 (Morocco), 143 (Mozambique), 144 (Myanmar), 145 (Namibia), 146 (Nauru), 147 (Nepal), 148 (Netherlands), 149 (Netherlands Antilles), 150 (New Caledonia), 151 (New Zealand), 152 (Nicaragua), 153 (Niger), 154 (Nigeria), 155 (Niue), 157 (Northern Mariana Islands), 158 (Norway), 159 (Oman), 160 (Pakistan), 161 (Palau), 162 (Palestine), 163 (Panama), 164 (Papua New Guinea), 165 (Paraguay), 166 (Peru), 167 (Philippines), 168 (Pitcairn), 169 (Poland), 170 (Portugal), 171 (Puerto Rico), 172 (Qatar), 173 (Republic of the Congo), 174 (Reunion), 175 (Romania), 176 (Russia), 177 (Rwanda), 178 (Saint Barthelemy), 179 (Saint Helena), 180 (Saint Kitts and Nevis), 181 (Saint Lucia), 182 (Saint Martin), 183 (Saint Pierre and Miquelon), 184 (Saint Vincent and the Grenadines), 185 (Samoa), 186 (San Marino), 187 (Sao Tome and Principe), 188 (Saudi Arabia), 189 (Senegal), 190 (Serbia), 191 (Seychelles), 192 (Sierra Leone), 193 (Singapore), 194 (Sint Maarten), 195 (Slovakia), 196 (Slovenia), 197 (Solomon Islands), 198 (Somalia), 199 (South Africa), 200 (South Korea), 201 (South Sudan), 202 (Spain), 203 (Sri Lanka), 205 (Suriname), 206 (Svalbard and Jan Mayen), 207 (Swaziland), 208 (Sweden), 209 (Switzerland), 211 (Taiwan), 212 (Tajikistan), 213 (Tanzania), 214 (Thailand), 215 (Togo), 216 (Tokelau), 217 (Tonga), 218 (Trinidad and Tobago), 219 (Tunisia), 220 (Turkey), 221 (Turkmenistan), 222 (Turks and Caicos Islands), 223 (Tuvalu), 224 (U.S. Virgin Islands), 225 (Uganda), 226 (Ukraine), 227 (United Arab Emirates), 228 (United Kingdom), 229 (United States), 230 (Uruguay), 231 (Uzbekistan), 232 (Vanuatu), 233 (Vatican), 234 (Venezuela), 235 (Vietnam), 236 (Wallis and Futuna), 237 (Western Sahara), 238 (Yemen), 239 (Zambia), 240 (Zimbabwe), 241 (Guadeloupe). Example: [29] for Brazil, [229] for United States"
     },
     "location_state_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "State IDs for geographic targeting (optional). Valid US State IDs: 1 (Alabama), 2 (Alaska), 3 (Arizona), 4 (Arkansas), 5 (California), 6 (Colorado), 7 (Connecticut), 8 (Delaware), 9 (Florida), 10 (Georgia), 11 (Hawaii), 12 (Idaho), 13 (Illinois), 14 (Indiana), 15 (Iowa), 16 (Kansas), 17 (Kentucky), 18 (Louisiana), 19 (Maine), 20 (Maryland), 21 (Massachusetts), 22 (Michigan), 23 (Minnesota), 24 (Mississippi), 25 (Missouri), 26 (Montana), 27 (Nebraska), 28 (Nevada), 29 (New Hampshire), 30 (New Jersey), 31 (New Mexico), 32 (New York), 33 (North Carolina), 34 (North Dakota), 35 (Ohio), 36 (Oklahoma), 37 (Oregon), 38 (Pennsylvania), 39 (Rhode Island), 40 (South Carolina), 41 (South Dakota), 42 (Tennessee), 43 (Texas), 44 (Utah), 45 (Vermont), 46 (Virginia), 47 (Washington), 48 (West Virginia), 49 (Wisconsin), 50 (Wyoming), 52 (District of Columbia). Example: [5] for California, [32] for New York"
     },
     "job_title_includes": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free text keywords for job titles to include (optional). Example: ['Software Engineer', 'Manager']"
     },
     "job_title_excludes": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free text keywords for job titles to exclude (optional). Example: ['Senior', 'Lead']"
     },
     "job_function_include_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Job function IDs to include (optional). Valid IDs: 1 (Marketing), 2 (Sales), 3 (Customer Service), 4 (Finance), 5 (HR), 6 (PR), 7 (Media), 8 (Design), 9 (Engineering), 10 (Operations), 11 (Legal), 12 (Education), 13 (Health), 14 (Real Estate), 15 (Accounting), 16 (Administrative), 17 (Business Development), 18 (Community Services), 19 (Consulting), 20 (Entrepreneurship), 21 (IT), 22 (Military Services), 23 (Product Management), 24 (Program/Project Management), 25 (Purchasing), 26 (QA), 27 (Research), 28 (Trades). Example: [9] for Engineering"
     },
     "job_function_exclude_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Job function IDs to exclude (optional). Valid IDs: 1-28 as listed above"
     },
     "seniority_include_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Seniority level IDs to include (optional). Valid IDs: 2 (Manager), 3 (Director), 4 (VP), 5 (CXO), 6 (Unpaid), 7 (Training), 8 (Senior), 9 (Owner), 10 (Entry), 11 (Partner). Example: [10, 7] for Entry and Training"
     },
     "seniority_exclude_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Seniority level IDs to exclude (optional). Valid IDs: 2-11 as listed above"
     },
     "contacts_per_company_limit": {
      "type": "integer",
      "description": "Maximum number of contacts to target per company (optional). Example: 20",
      "minimum": 1
     }
    },
    "required": [
     "name",
     "intent_days"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_g2_intent_linkedin_native_dynamic_audience",
   "title": "Create G2 Intent - LinkedIn Native (Dynamic) Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Create a **G2 Intent - LinkedIn Native (Dynamic)** audience (platform `customAudienceType=DYNAMIC_G2`).",
   "description": "Create a **G2 Intent - LinkedIn Native (Dynamic)** audience (platform `customAudienceType=DYNAMIC_G2`).\n\n                AUDIENCE TYPE (mirrors the UI's \"Audience Type\" dropdown):\n                  • UI label: \"G2 Intent - LinkedIn Native (Dynamic)\"\n                  • Platform enum: DYNAMIC_G2\n                  • Refreshes daily as G2 intent signals update; targets LinkedIn natively.\n\n                PREREQUISITE:\n                  • Both G2 and LinkedIn integrations MUST be connected. If either is missing, do NOT call this tool — recommend `create_firmographic_audience` instead.\n\n                WHEN TO USE (exact user phrasing this tool should match):\n                  • \"G2 Intent - LinkedIn Native (Dynamic)\"\n                  • \"G2 LinkedIn Native Dynamic\"\n                  • \"LinkedIn native G2 intent audience\"\n                  • The user explicitly mentions BOTH G2 intent AND LinkedIn native targeting.\n\n                WHEN NOT TO USE:\n                  • If the user asked for \"G2 Intent (Dynamic)\" without \"LinkedIn Native\" → use `create_g2_intent_dynamic_audience`.\n                  • If the user asked for \"G2 Intent (Static)\" → use `create_g2_intent_static_audience`.\n\n                BUYING STAGES — REQUIRED BY THE PLATFORM:\n                  The platform UI marks Buying Stages as required. If the user did not name any stages, STOP and ask the user which of AWARENESS / CONSIDERATION / DECISION to target. DO NOT silently default — that produced wrong audiences in PRD-29702 / PRD-29703.\n\n                CRITERIA (LinkedIn-native shapes; free-text fields are resolved server-side via the LinkedIn references API):\n                  • employees — LinkedIn-native employee ranges. Valid labels: see the schema (e.g. \"201-500\", \"501-1000\", \"1001-5000\").\n                  • revenues — LinkedIn-native revenue ranges (e.g. \"$1M-$10M\", \"$10M-$100M\").\n                  • company_names — free-text company names (resolved to LinkedIn company IDs).\n                  • location_country_ids — country IDs (e.g. 229=US, 228=UK).\n                  • job_titles — free-text titles (resolved to LinkedIn job-title IDs).\n                  • skills — free-text professional skills (resolved to LinkedIn skill IDs).\n\n                PARAMETERS:\n                  • name (required, ≤ 50 chars)\n                  • intent_days (required, 1-365)\n                  • buying_stages (REQUIRED by platform — ask the user if missing; do NOT default)\n                  • employees, revenues, company_names, location_country_ids, job_titles, skills (all optional)\n\n                RETURNS: id, audience_id, audience_name, audience_type (DYNAMIC_G2), status, buying_stages, intent_days, expectedNumberOfCompanies, expectedNumberOfContacts.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "maxLength": 50,
      "description": "Audience name (required, max 50 characters)."
     },
     "intent_days": {
      "type": "integer",
      "minimum": 1,
      "maximum": 365,
      "description": "Days to look back for G2 intent signals (required, 1-365)."
     },
     "buying_stages": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "AWARENESS",
        "CONSIDERATION",
        "DECISION"
       ]
      },
      "description": "Buying stages to target. The platform REQUIRES at least one; if the user did not specify, ASK them before calling — do NOT default.",
      "minItems": 1
     },
     "employees": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "1",
        "2-10",
        "11-50",
        "51-200",
        "201-500",
        "501-1000",
        "1001-5000",
        "5001-10000",
        "+10001"
       ]
      },
      "description": "LinkedIn employee count ranges for audience targeting. Valid values: 1, 2-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, +10001. Example: ['201-500', '501-1000']"
     },
     "revenues": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "Under $1M",
        "$1M-$10M",
        "$10M-$100M",
        "$100M-$1B",
        "$1B+"
       ]
      },
      "description": "LinkedIn revenue ranges for audience targeting. Valid values: Under $1M, $1M-$10M, $10M-$100M, $100M-$1B, $1B+. Example: ['$1M-$10M', '$10M-$100M']"
     },
     "company_names": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free-text company names to target (resolved server-side to LinkedIn company IDs). Example: ['Metadata', 'Google', 'Salesforce']"
     },
     "location_country_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Country IDs for geographic targeting (optional). Valid IDs: 1 (Afghanistan), 2 (Albania), 3 (Algeria), 4 (American Samoa), 5 (Andorra), 6 (Angola), 7 (Anguilla), 8 (Antarctica), 9 (Antigua and Barbuda), 10 (Argentina), 11 (Armenia), 12 (Aruba), 13 (Australia), 14 (Austria), 15 (Azerbaijan), 16 (Bahamas), 17 (Bahrain), 18 (Bangladesh), 19 (Barbados), 20 (Belarus), 21 (Belgium), 22 (Belize), 23 (Benin), 24 (Bermuda), 25 (Bhutan), 26 (Bolivia), 27 (Bosnia and Herzegovina), 28 (Botswana), 29 (Brazil), 30 (British Indian Ocean Territory), 31 (British Virgin Islands), 32 (Brunei), 33 (Bulgaria), 34 (Burkina Faso), 35 (Burundi), 36 (Cambodia), 37 (Cameroon), 38 (Canada), 39 (Cape Verde), 40 (Cayman Islands), 41 (Central African Republic), 42 (Chad), 43 (Chile), 44 (China), 45 (Christmas Island), 46 (Cocos Islands), 47 (Colombia), 48 (Comoros), 49 (Cook Islands), 50 (Costa Rica), 51 (Croatia), 53 (Curacao), 54 (Cyprus), 55 (Czech Republic), 56 (Democratic Republic of the Congo), 57 (Denmark), 58 (Djibouti), 59 (Dominica), 60 (Dominican Republic), 61 (East Timor), 62 (Ecuador), 63 (Egypt), 64 (El Salvador), 65 (Equatorial Guinea), 66 (Eritrea), 67 (Estonia), 68 (Ethiopia), 69 (Falkland Islands), 70 (Faroe Islands), 71 (Fiji), 72 (Finland), 73 (France), 74 (French Polynesia), 75 (Gabon), 76 (Gambia), 77 (Georgia), 78 (Germany), 79 (Ghana), 80 (Gibraltar), 81 (Greece), 82 (Greenland), 83 (Grenada), 84 (Guam), 85 (Guatemala), 86 (Guernsey), 87 (Guinea), 88 (Guinea-Bissau), 89 (Guyana), 90 (Haiti), 91 (Honduras), 92 (Hong Kong), 93 (Hungary), 94 (Iceland), 95 (India), 96 (Indonesia), 98 (Iraq), 99 (Ireland), 100 (Isle of Man), 101 (Israel), 102 (Italy), 103 (Ivory Coast), 104 (Jamaica), 105 (Japan), 106 (Jersey), 107 (Jordan), 108 (Kazakhstan), 109 (Kenya), 110 (Kiribati), 111 (Kosovo), 112 (Kuwait), 113 (Kyrgyzstan), 114 (Laos), 115 (Latvia), 116 (Lebanon), 117 (Lesotho), 118 (Liberia), 119 (Libya), 120 (Liechtenstein), 121 (Lithuania), 122 (Luxembourg), 123 (Macau), 124 (Macedonia), 125 (Madagascar), 126 (Malawi), 127 (Malaysia), 128 (Maldives), 129 (Mali), 130 (Malta), 131 (Marshall Islands), 132 (Mauritania), 133 (Mauritius), 134 (Mayotte), 135 (Mexico), 136 (Micronesia), 137 (Moldova), 138 (Monaco), 139 (Mongolia), 140 (Montenegro), 141 (Montserrat), 142 (Morocco), 143 (Mozambique), 144 (Myanmar), 145 (Namibia), 146 (Nauru), 147 (Nepal), 148 (Netherlands), 149 (Netherlands Antilles), 150 (New Caledonia), 151 (New Zealand), 152 (Nicaragua), 153 (Niger), 154 (Nigeria), 155 (Niue), 157 (Northern Mariana Islands), 158 (Norway), 159 (Oman), 160 (Pakistan), 161 (Palau), 162 (Palestine), 163 (Panama), 164 (Papua New Guinea), 165 (Paraguay), 166 (Peru), 167 (Philippines), 168 (Pitcairn), 169 (Poland), 170 (Portugal), 171 (Puerto Rico), 172 (Qatar), 173 (Republic of the Congo), 174 (Reunion), 175 (Romania), 176 (Russia), 177 (Rwanda), 178 (Saint Barthelemy), 179 (Saint Helena), 180 (Saint Kitts and Nevis), 181 (Saint Lucia), 182 (Saint Martin), 183 (Saint Pierre and Miquelon), 184 (Saint Vincent and the Grenadines), 185 (Samoa), 186 (San Marino), 187 (Sao Tome and Principe), 188 (Saudi Arabia), 189 (Senegal), 190 (Serbia), 191 (Seychelles), 192 (Sierra Leone), 193 (Singapore), 194 (Sint Maarten), 195 (Slovakia), 196 (Slovenia), 197 (Solomon Islands), 198 (Somalia), 199 (South Africa), 200 (South Korea), 201 (South Sudan), 202 (Spain), 203 (Sri Lanka), 205 (Suriname), 206 (Svalbard and Jan Mayen), 207 (Swaziland), 208 (Sweden), 209 (Switzerland), 211 (Taiwan), 212 (Tajikistan), 213 (Tanzania), 214 (Thailand), 215 (Togo), 216 (Tokelau), 217 (Tonga), 218 (Trinidad and Tobago), 219 (Tunisia), 220 (Turkey), 221 (Turkmenistan), 222 (Turks and Caicos Islands), 223 (Tuvalu), 224 (U.S. Virgin Islands), 225 (Uganda), 226 (Ukraine), 227 (United Arab Emirates), 228 (United Kingdom), 229 (United States), 230 (Uruguay), 231 (Uzbekistan), 232 (Vanuatu), 233 (Vatican), 234 (Venezuela), 235 (Vietnam), 236 (Wallis and Futuna), 237 (Western Sahara), 238 (Yemen), 239 (Zambia), 240 (Zimbabwe), 241 (Guadeloupe). Example: [29] for Brazil, [229] for United States"
     },
     "job_titles": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free-text job titles (resolved to LinkedIn job-title IDs). Example: ['Software Engineer', 'Product Manager']"
     },
     "skills": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free-text professional skills (resolved to LinkedIn skill IDs). Example: ['HubSpot', 'Salesforce', 'Python']"
     }
    },
    "required": [
     "name",
     "intent_days"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_g2_intent_static_audience",
   "title": "Create G2 Intent (Static) Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Create a **G2 Intent (Static)** audience (platform `customAudienceType=G2_ABM_CONTACT`).",
   "description": "Create a **G2 Intent (Static)** audience (platform `customAudienceType=G2_ABM_CONTACT`).\n\n                AUDIENCE TYPE (mirrors the UI's \"Audience Type\" dropdown):\n                  • UI label: \"G2 Intent (Static)\"\n                  • Platform enum: G2_ABM_CONTACT\n                  • SNAPSHOT at creation time — does NOT refresh daily.\n\n                PREREQUISITE:\n                  • G2 integration MUST be connected.\n\n                WHEN TO USE (exact user phrasing this tool should match):\n                  • \"G2 Intent (Static)\"\n                  • \"Static G2 audience\"\n                  • The user explicitly wants the static / snapshot variant rather than the daily-refreshing dynamic one.\n\n                WHEN NOT TO USE:\n                  • If the user said \"Dynamic\" → use `create_g2_intent_dynamic_audience` or `create_g2_intent_linkedin_native_dynamic_audience`.\n\n                BUYING STAGES — REQUIRED BY THE PLATFORM:\n                  ASK the user if missing. DO NOT default.\n\n                CONTACTS CRITERIA:\nUse the following parameters to filter and target specific contacts within accounts:\n\nGEOGRAPHIC TARGETING:\n- location_country_ids: Array of country IDs (e.g., [29] for Brazil, [229] for United States)\n- location_state_ids: Array of state IDs (e.g., [5] for California, [32] for New York)\n\nJOB TITLES:\n- job_title_includes: Array of free text strings to include (e.g., [\"Software Engineer\", \"Manager\"])\n- job_title_excludes: Array of free text strings to exclude (e.g., [\"Senior\", \"Lead\"])\n\nJOB FUNCTIONS:\nValid job function IDs and their labels:\n- 1: Marketing, 2: Sales, 3: Customer Service, 4: Finance, 5: Human Resources, 6: Public Relations\n- 7: Media, 8: Design, 9: Engineering, 10: Operations, 11: Legal, 12: Education\n- 13: Health, 14: Real Estate, 15: Accounting, 16: Administrative, 17: Business Development\n- 18: Community and Social Services, 19: Consulting, 20: Entrepreneurship, 21: Information Technology\n- 22: Military and Protective Services, 23: Product Management, 24: Program and Project Management\n- 25: Purchasing, 26: Quality Assurance, 27: Research, 28: Trades\n\n- job_function_include_ids: Array of job function IDs to include (e.g., [9] for Engineering)\n- job_function_exclude_ids: Array of job function IDs to exclude\n\nSENIORITY LEVELS:\nValid seniority IDs and their labels:\n- 2: Manager, 3: Director, 4: VP, 5: CXO, 6: Unpaid, 7: Training\n- 8: Senior, 9: Owner, 10: Entry, 11: Partner\n\n- seniority_include_ids: Array of seniority level IDs to include (e.g., [10, 7] for Entry and Training)\n- seniority_exclude_ids: Array of seniority level IDs to exclude\n\nCONTACT LIMITS:\n- contacts_per_company_limit: Maximum number of contacts to target per company (optional). Example: 20\n\nUSAGE NOTES:\n- All ID fields (countries, states, job functions, seniorities) use numeric IDs from the platform\n- Job titles are free text strings, not IDs\n- Use include parameters to specify which criteria to target\n- Use exclude parameters to filter out contacts matching specific criteria\n- Combine multiple criteria for precise targeting (e.g., Senior Engineers in California)\n- Criteria are combined with AND logic, so all conditions must be met\n\nCONTACTS CRITERIA PARAMETERS(Usually after other initial audience parameters to be described next):\n- location_country_ids: Array of country IDs (optional)\n- location_state_ids: Array of state IDs (optional)\n- job_title_includes: Array of job title keywords to include (optional)\n- job_title_excludes: Array of job title keywords to exclude (optional)\n- job_function_include_ids: Array of job function IDs (optional)\n- job_function_exclude_ids: Array of job function IDs (optional)\n- seniority_include_ids: Array of seniority level IDs (optional)\n- seniority_exclude_ids: Array of seniority level IDs (optional)\n- contacts_per_company_limit: Maximum contacts per company (optional)\n\n\n                PARAMETERS:\n                  • name (required, ≤ 50 chars)\n                  • intent_days (required, 1-365)\n                  • buying_stages (REQUIRED — ask if missing)\n                  • Contact criteria as in CONTACTS CRITERIA above.\n\n                RETURNS: id, audience_id, audience_name, audience_type (G2_ABM_CONTACT), status, buying_stages, intent_days, counts.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "maxLength": 50,
      "description": "Audience name (required, max 50 characters)."
     },
     "intent_days": {
      "type": "integer",
      "minimum": 1,
      "maximum": 365,
      "description": "Days to look back for G2 intent signals (required, 1-365)."
     },
     "buying_stages": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "AWARENESS",
        "CONSIDERATION",
        "DECISION"
       ]
      },
      "description": "Buying stages to target. The platform REQUIRES at least one; if the user did not specify, ASK them before calling — do NOT default.",
      "minItems": 1
     },
     "location_country_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Country IDs for geographic targeting (optional). Valid IDs: 1 (Afghanistan), 2 (Albania), 3 (Algeria), 4 (American Samoa), 5 (Andorra), 6 (Angola), 7 (Anguilla), 8 (Antarctica), 9 (Antigua and Barbuda), 10 (Argentina), 11 (Armenia), 12 (Aruba), 13 (Australia), 14 (Austria), 15 (Azerbaijan), 16 (Bahamas), 17 (Bahrain), 18 (Bangladesh), 19 (Barbados), 20 (Belarus), 21 (Belgium), 22 (Belize), 23 (Benin), 24 (Bermuda), 25 (Bhutan), 26 (Bolivia), 27 (Bosnia and Herzegovina), 28 (Botswana), 29 (Brazil), 30 (British Indian Ocean Territory), 31 (British Virgin Islands), 32 (Brunei), 33 (Bulgaria), 34 (Burkina Faso), 35 (Burundi), 36 (Cambodia), 37 (Cameroon), 38 (Canada), 39 (Cape Verde), 40 (Cayman Islands), 41 (Central African Republic), 42 (Chad), 43 (Chile), 44 (China), 45 (Christmas Island), 46 (Cocos Islands), 47 (Colombia), 48 (Comoros), 49 (Cook Islands), 50 (Costa Rica), 51 (Croatia), 53 (Curacao), 54 (Cyprus), 55 (Czech Republic), 56 (Democratic Republic of the Congo), 57 (Denmark), 58 (Djibouti), 59 (Dominica), 60 (Dominican Republic), 61 (East Timor), 62 (Ecuador), 63 (Egypt), 64 (El Salvador), 65 (Equatorial Guinea), 66 (Eritrea), 67 (Estonia), 68 (Ethiopia), 69 (Falkland Islands), 70 (Faroe Islands), 71 (Fiji), 72 (Finland), 73 (France), 74 (French Polynesia), 75 (Gabon), 76 (Gambia), 77 (Georgia), 78 (Germany), 79 (Ghana), 80 (Gibraltar), 81 (Greece), 82 (Greenland), 83 (Grenada), 84 (Guam), 85 (Guatemala), 86 (Guernsey), 87 (Guinea), 88 (Guinea-Bissau), 89 (Guyana), 90 (Haiti), 91 (Honduras), 92 (Hong Kong), 93 (Hungary), 94 (Iceland), 95 (India), 96 (Indonesia), 98 (Iraq), 99 (Ireland), 100 (Isle of Man), 101 (Israel), 102 (Italy), 103 (Ivory Coast), 104 (Jamaica), 105 (Japan), 106 (Jersey), 107 (Jordan), 108 (Kazakhstan), 109 (Kenya), 110 (Kiribati), 111 (Kosovo), 112 (Kuwait), 113 (Kyrgyzstan), 114 (Laos), 115 (Latvia), 116 (Lebanon), 117 (Lesotho), 118 (Liberia), 119 (Libya), 120 (Liechtenstein), 121 (Lithuania), 122 (Luxembourg), 123 (Macau), 124 (Macedonia), 125 (Madagascar), 126 (Malawi), 127 (Malaysia), 128 (Maldives), 129 (Mali), 130 (Malta), 131 (Marshall Islands), 132 (Mauritania), 133 (Mauritius), 134 (Mayotte), 135 (Mexico), 136 (Micronesia), 137 (Moldova), 138 (Monaco), 139 (Mongolia), 140 (Montenegro), 141 (Montserrat), 142 (Morocco), 143 (Mozambique), 144 (Myanmar), 145 (Namibia), 146 (Nauru), 147 (Nepal), 148 (Netherlands), 149 (Netherlands Antilles), 150 (New Caledonia), 151 (New Zealand), 152 (Nicaragua), 153 (Niger), 154 (Nigeria), 155 (Niue), 157 (Northern Mariana Islands), 158 (Norway), 159 (Oman), 160 (Pakistan), 161 (Palau), 162 (Palestine), 163 (Panama), 164 (Papua New Guinea), 165 (Paraguay), 166 (Peru), 167 (Philippines), 168 (Pitcairn), 169 (Poland), 170 (Portugal), 171 (Puerto Rico), 172 (Qatar), 173 (Republic of the Congo), 174 (Reunion), 175 (Romania), 176 (Russia), 177 (Rwanda), 178 (Saint Barthelemy), 179 (Saint Helena), 180 (Saint Kitts and Nevis), 181 (Saint Lucia), 182 (Saint Martin), 183 (Saint Pierre and Miquelon), 184 (Saint Vincent and the Grenadines), 185 (Samoa), 186 (San Marino), 187 (Sao Tome and Principe), 188 (Saudi Arabia), 189 (Senegal), 190 (Serbia), 191 (Seychelles), 192 (Sierra Leone), 193 (Singapore), 194 (Sint Maarten), 195 (Slovakia), 196 (Slovenia), 197 (Solomon Islands), 198 (Somalia), 199 (South Africa), 200 (South Korea), 201 (South Sudan), 202 (Spain), 203 (Sri Lanka), 205 (Suriname), 206 (Svalbard and Jan Mayen), 207 (Swaziland), 208 (Sweden), 209 (Switzerland), 211 (Taiwan), 212 (Tajikistan), 213 (Tanzania), 214 (Thailand), 215 (Togo), 216 (Tokelau), 217 (Tonga), 218 (Trinidad and Tobago), 219 (Tunisia), 220 (Turkey), 221 (Turkmenistan), 222 (Turks and Caicos Islands), 223 (Tuvalu), 224 (U.S. Virgin Islands), 225 (Uganda), 226 (Ukraine), 227 (United Arab Emirates), 228 (United Kingdom), 229 (United States), 230 (Uruguay), 231 (Uzbekistan), 232 (Vanuatu), 233 (Vatican), 234 (Venezuela), 235 (Vietnam), 236 (Wallis and Futuna), 237 (Western Sahara), 238 (Yemen), 239 (Zambia), 240 (Zimbabwe), 241 (Guadeloupe). Example: [29] for Brazil, [229] for United States"
     },
     "location_state_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "State IDs for geographic targeting (optional). Valid US State IDs: 1 (Alabama), 2 (Alaska), 3 (Arizona), 4 (Arkansas), 5 (California), 6 (Colorado), 7 (Connecticut), 8 (Delaware), 9 (Florida), 10 (Georgia), 11 (Hawaii), 12 (Idaho), 13 (Illinois), 14 (Indiana), 15 (Iowa), 16 (Kansas), 17 (Kentucky), 18 (Louisiana), 19 (Maine), 20 (Maryland), 21 (Massachusetts), 22 (Michigan), 23 (Minnesota), 24 (Mississippi), 25 (Missouri), 26 (Montana), 27 (Nebraska), 28 (Nevada), 29 (New Hampshire), 30 (New Jersey), 31 (New Mexico), 32 (New York), 33 (North Carolina), 34 (North Dakota), 35 (Ohio), 36 (Oklahoma), 37 (Oregon), 38 (Pennsylvania), 39 (Rhode Island), 40 (South Carolina), 41 (South Dakota), 42 (Tennessee), 43 (Texas), 44 (Utah), 45 (Vermont), 46 (Virginia), 47 (Washington), 48 (West Virginia), 49 (Wisconsin), 50 (Wyoming), 52 (District of Columbia). Example: [5] for California, [32] for New York"
     },
     "job_title_includes": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free text keywords for job titles to include (optional). Example: ['Software Engineer', 'Manager']"
     },
     "job_title_excludes": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free text keywords for job titles to exclude (optional). Example: ['Senior', 'Lead']"
     },
     "job_function_include_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Job function IDs to include (optional). Valid IDs: 1 (Marketing), 2 (Sales), 3 (Customer Service), 4 (Finance), 5 (HR), 6 (PR), 7 (Media), 8 (Design), 9 (Engineering), 10 (Operations), 11 (Legal), 12 (Education), 13 (Health), 14 (Real Estate), 15 (Accounting), 16 (Administrative), 17 (Business Development), 18 (Community Services), 19 (Consulting), 20 (Entrepreneurship), 21 (IT), 22 (Military Services), 23 (Product Management), 24 (Program/Project Management), 25 (Purchasing), 26 (QA), 27 (Research), 28 (Trades). Example: [9] for Engineering"
     },
     "job_function_exclude_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Job function IDs to exclude (optional). Valid IDs: 1-28 as listed above"
     },
     "seniority_include_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Seniority level IDs to include (optional). Valid IDs: 2 (Manager), 3 (Director), 4 (VP), 5 (CXO), 6 (Unpaid), 7 (Training), 8 (Senior), 9 (Owner), 10 (Entry), 11 (Partner). Example: [10, 7] for Entry and Training"
     },
     "seniority_exclude_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Seniority level IDs to exclude (optional). Valid IDs: 2-11 as listed above"
     },
     "contacts_per_company_limit": {
      "type": "integer",
      "description": "Maximum number of contacts to target per company (optional). Example: 20",
      "minimum": 1
     }
    },
    "required": [
     "name",
     "intent_days"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_linkedin_native_criteria_audience",
   "title": "Create LinkedIn Native Criteria Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Create a LinkedIn native criteria audience using LinkedIn-supported targeting criteria.",
   "description": "Create a LinkedIn native criteria audience using LinkedIn-supported targeting criteria.\n                Build audiences based on job titles, skills, company names, employee count ranges, revenue ranges, and geographic location.\n\n                ESTIMATE-ONLY MODE:\n                Pass estimate_only=true to preview audience size WITHOUT creating the audience. In this mode the tool\n                returns expectedNumberOfContacts. Default is false.\n\n                ALSO KNOWN AS: LinkedIn audience, LI criteria audience, native LinkedIn targeting, LinkedIn lead audience\n\n                KEYWORDS: LinkedIn, native, criteria, audience, job title, skills, company, company name, employees, revenue, targeting, country, location\n\n                WHEN TO USE:\n                - Create targeted LinkedIn audiences using native LinkedIn criteria\n                - Target users by job titles (e.g., Software Engineer, Product Manager)\n                - Target by professional skills (e.g., HubSpot, Salesforce, Python)\n                - Target by company names (e.g., Metadata, Google, Salesforce)\n                - Filter by company employee count ranges (e.g., 201-500, 1001-5000)\n                - Filter by company revenue ranges (e.g., $1M-$10M, $10M-$100M)\n                - Target by country/geographic location (e.g., United States, United Kingdom)\n\n                PARAMETERS:\n                - name: Audience name (required, must be shorter than 50 characters)\n                - job_titles: Array of free-text job title strings (optional) - e.g., [\"Software Engineer\", \"Product Manager\"]\n                - skills: Array of free-text skill strings (optional) - e.g., [\"HubSpot\", \"Salesforce\", \"Python\"]\n                - company_names: Array of free-text company name strings (optional) - e.g., [\"Metadata\", \"Google\", \"Salesforce\"]\n                - employees: Array of LinkedIn employee count ranges (optional) - valid values: 1, 2-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, +10001\n                - revenues: Array of LinkedIn revenue ranges (optional) - valid values: Under $1M, $1M-$10M, $10M-$100M, $100M-$1B, $1B+\n                - location_country_ids: Array of country ID integers (optional) - e.g., [229] for United States\n\n                RETURNS:\n                Audience details with ID, status, and creation info.\n\n                IMPORTANT NOTES:\n                - name is the only required parameter, all other parameters are optional\n                - Audience name must be shorter than 50 characters\n                - Job titles, skills, company names, employees, and revenues are free-text arrays\n                - Country IDs are integer identifiers (e.g., 229 = United States)",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "maxLength": 50,
      "description": "Audience name (required, must be shorter than 50 characters)."
     },
     "job_titles": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free-text job title strings for targeting. Example: ['Software Engineer', 'Product Manager']"
     },
     "skills": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free-text skill strings for targeting. Example: ['HubSpot', 'Salesforce', 'Python']"
     },
     "company_names": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free-text company name strings for targeting. Example: ['Metadata', 'Google', 'Salesforce']"
     },
     "employees": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "1",
        "2-10",
        "11-50",
        "51-200",
        "201-500",
        "501-1000",
        "1001-5000",
        "5001-10000",
        "+10001"
       ]
      },
      "description": "LinkedIn employee count ranges for audience targeting. Valid values: 1, 2-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, +10001. Example: ['201-500', '501-1000']"
     },
     "revenues": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "Under $1M",
        "$1M-$10M",
        "$10M-$100M",
        "$100M-$1B",
        "$1B+"
       ]
      },
      "description": "LinkedIn revenue ranges for audience targeting. Valid values: Under $1M, $1M-$10M, $10M-$100M, $100M-$1B, $1B+. Example: ['$1M-$10M', '$10M-$100M']"
     },
     "location_country_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Country IDs for geographic targeting (optional). Valid IDs: 1 (Afghanistan), 2 (Albania), 3 (Algeria), 4 (American Samoa), 5 (Andorra), 6 (Angola), 7 (Anguilla), 8 (Antarctica), 9 (Antigua and Barbuda), 10 (Argentina), 11 (Armenia), 12 (Aruba), 13 (Australia), 14 (Austria), 15 (Azerbaijan), 16 (Bahamas), 17 (Bahrain), 18 (Bangladesh), 19 (Barbados), 20 (Belarus), 21 (Belgium), 22 (Belize), 23 (Benin), 24 (Bermuda), 25 (Bhutan), 26 (Bolivia), 27 (Bosnia and Herzegovina), 28 (Botswana), 29 (Brazil), 30 (British Indian Ocean Territory), 31 (British Virgin Islands), 32 (Brunei), 33 (Bulgaria), 34 (Burkina Faso), 35 (Burundi), 36 (Cambodia), 37 (Cameroon), 38 (Canada), 39 (Cape Verde), 40 (Cayman Islands), 41 (Central African Republic), 42 (Chad), 43 (Chile), 44 (China), 45 (Christmas Island), 46 (Cocos Islands), 47 (Colombia), 48 (Comoros), 49 (Cook Islands), 50 (Costa Rica), 51 (Croatia), 53 (Curacao), 54 (Cyprus), 55 (Czech Republic), 56 (Democratic Republic of the Congo), 57 (Denmark), 58 (Djibouti), 59 (Dominica), 60 (Dominican Republic), 61 (East Timor), 62 (Ecuador), 63 (Egypt), 64 (El Salvador), 65 (Equatorial Guinea), 66 (Eritrea), 67 (Estonia), 68 (Ethiopia), 69 (Falkland Islands), 70 (Faroe Islands), 71 (Fiji), 72 (Finland), 73 (France), 74 (French Polynesia), 75 (Gabon), 76 (Gambia), 77 (Georgia), 78 (Germany), 79 (Ghana), 80 (Gibraltar), 81 (Greece), 82 (Greenland), 83 (Grenada), 84 (Guam), 85 (Guatemala), 86 (Guernsey), 87 (Guinea), 88 (Guinea-Bissau), 89 (Guyana), 90 (Haiti), 91 (Honduras), 92 (Hong Kong), 93 (Hungary), 94 (Iceland), 95 (India), 96 (Indonesia), 98 (Iraq), 99 (Ireland), 100 (Isle of Man), 101 (Israel), 102 (Italy), 103 (Ivory Coast), 104 (Jamaica), 105 (Japan), 106 (Jersey), 107 (Jordan), 108 (Kazakhstan), 109 (Kenya), 110 (Kiribati), 111 (Kosovo), 112 (Kuwait), 113 (Kyrgyzstan), 114 (Laos), 115 (Latvia), 116 (Lebanon), 117 (Lesotho), 118 (Liberia), 119 (Libya), 120 (Liechtenstein), 121 (Lithuania), 122 (Luxembourg), 123 (Macau), 124 (Macedonia), 125 (Madagascar), 126 (Malawi), 127 (Malaysia), 128 (Maldives), 129 (Mali), 130 (Malta), 131 (Marshall Islands), 132 (Mauritania), 133 (Mauritius), 134 (Mayotte), 135 (Mexico), 136 (Micronesia), 137 (Moldova), 138 (Monaco), 139 (Mongolia), 140 (Montenegro), 141 (Montserrat), 142 (Morocco), 143 (Mozambique), 144 (Myanmar), 145 (Namibia), 146 (Nauru), 147 (Nepal), 148 (Netherlands), 149 (Netherlands Antilles), 150 (New Caledonia), 151 (New Zealand), 152 (Nicaragua), 153 (Niger), 154 (Nigeria), 155 (Niue), 157 (Northern Mariana Islands), 158 (Norway), 159 (Oman), 160 (Pakistan), 161 (Palau), 162 (Palestine), 163 (Panama), 164 (Papua New Guinea), 165 (Paraguay), 166 (Peru), 167 (Philippines), 168 (Pitcairn), 169 (Poland), 170 (Portugal), 171 (Puerto Rico), 172 (Qatar), 173 (Republic of the Congo), 174 (Reunion), 175 (Romania), 176 (Russia), 177 (Rwanda), 178 (Saint Barthelemy), 179 (Saint Helena), 180 (Saint Kitts and Nevis), 181 (Saint Lucia), 182 (Saint Martin), 183 (Saint Pierre and Miquelon), 184 (Saint Vincent and the Grenadines), 185 (Samoa), 186 (San Marino), 187 (Sao Tome and Principe), 188 (Saudi Arabia), 189 (Senegal), 190 (Serbia), 191 (Seychelles), 192 (Sierra Leone), 193 (Singapore), 194 (Sint Maarten), 195 (Slovakia), 196 (Slovenia), 197 (Solomon Islands), 198 (Somalia), 199 (South Africa), 200 (South Korea), 201 (South Sudan), 202 (Spain), 203 (Sri Lanka), 205 (Suriname), 206 (Svalbard and Jan Mayen), 207 (Swaziland), 208 (Sweden), 209 (Switzerland), 211 (Taiwan), 212 (Tajikistan), 213 (Tanzania), 214 (Thailand), 215 (Togo), 216 (Tokelau), 217 (Tonga), 218 (Trinidad and Tobago), 219 (Tunisia), 220 (Turkey), 221 (Turkmenistan), 222 (Turks and Caicos Islands), 223 (Tuvalu), 224 (U.S. Virgin Islands), 225 (Uganda), 226 (Ukraine), 227 (United Arab Emirates), 228 (United Kingdom), 229 (United States), 230 (Uruguay), 231 (Uzbekistan), 232 (Vanuatu), 233 (Vatican), 234 (Venezuela), 235 (Vietnam), 236 (Wallis and Futuna), 237 (Western Sahara), 238 (Yemen), 239 (Zambia), 240 (Zimbabwe), 241 (Guadeloupe). Example: [29] for Brazil, [229] for United States"
     },
     "estimate_only": {
      "type": "boolean",
      "description": "If true, do NOT create the audience — only estimate its size and return the expected contact count. Use this when the user wants to preview LinkedIn audience size before committing. Defaults to false (audience is created).",
      "default": false
     }
    },
    "required": [
     "name"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_marketing_automation_contacts_audience",
   "title": "Create Marketing-Automation Contacts Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Create a Marketing-Automation-contacts custom audience (platform customAudienceType MA_CONTACTS_STATIC or MA_CONTACTS_DYNAMIC) from a provider library list.",
   "description": "Create a Marketing-Automation-contacts custom audience (platform customAudienceType MA_CONTACTS_STATIC or MA_CONTACTS_DYNAMIC) from a provider library list.\n\n                This is one of the audience types eligible for Microsoft Ads (Customer Match). Use it when a user wants a Marketing-Automation-sourced contacts audience, including for a Microsoft Ads campaign.\n\n                PREREQUISITE:\n                  • The provider (HUBSPOT or MARKETO) MUST be connected — the platform validates this and returns a clear error if not.\n                  • A library_list_id from list_marketing_automation_lists — do NOT invent one. For HubSpot the list must be DONE; for Marketo READY.\n\n                VARIANT (mirrors the UI's static/dynamic choice):\n                  • STATIC  → MA_CONTACTS_STATIC — snapshot at creation, does not refresh.\n                  • DYNAMIC → MA_CONTACTS_DYNAMIC — refreshes as the provider data changes.\n                  • If the user did not say which, ASK — do NOT default and do NOT infer it from the source list's type. A HubSpot STATIC_LIST is not the same choice as a STATIC (snapshot) audience; the audience variant is the user's call, independent of the list's type.\n\n                ASYNC: the platform creates the audience in the background and returns a flow id, NOT a ready audience id. The audience appears under the account's Marketing-Automation audiences once the flow completes, and its contact count fills in then. Do not expect to associate it to a campaign in the same turn.\n\n                PARAMETERS:\n                  • custom_audience_name (required): the audience display name — must NOT contain '/'.\n                  • library_list_id (required): id from list_marketing_automation_lists.\n                  • provider (required): HUBSPOT or MARKETO.\n                  • variant (required): STATIC or DYNAMIC.\n                  • list_name (optional): the source list's own name (from the lookup), forwarded to the flow.\n\n                RETURNS: { id, name, status, createdDate, audience_type, async: true } — `audience_type` is the MA_CONTACTS_* you created and `id` is a creation/flow id, not associable yet.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "custom_audience_name": {
      "type": "string",
      "description": "Audience display name. Must not contain '/'."
     },
     "library_list_id": {
      "type": "integer",
      "description": "The library list id from list_marketing_automation_lists."
     },
     "provider": {
      "type": "string",
      "enum": [
       "HUBSPOT",
       "MARKETO"
      ],
      "description": "The Marketing-Automation provider that owns the list."
     },
     "variant": {
      "type": "string",
      "enum": [
       "STATIC",
       "DYNAMIC"
      ],
      "description": "STATIC = snapshot (MA_CONTACTS_STATIC); DYNAMIC = refreshing (MA_CONTACTS_DYNAMIC). Ask the user if unspecified."
     },
     "list_name": {
      "type": "string",
      "description": "Optional: the source list's own name, from list_marketing_automation_lists."
     }
    },
    "required": [
     "custom_audience_name",
     "library_list_id",
     "provider",
     "variant"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_reddit_target_group",
   "title": "Create Reddit Target Group",
   "category": "audiences",
   "risk": "write",
   "summary": "Create a new Reddit target group with name and targeting criteria (communities, interests, keywords, location).",
   "description": "Create a new Reddit target group with name and targeting criteria (communities, interests, keywords, location).\n\nPREREQUISITE: All references MUST come from search_reddit_criteria. estimate_target_group with channel=\"REDDIT\" first is MANDATORY: verify isMatchCountTooSmall=false and report the reach to the user BEFORE creating. Same bar as create_target_group and update_target_group (PRD-31759). Reddit estimation went live with PRD-31996; before it there was no way to size one of these, which is why a Reddit target group could be created at zero reach with nothing noticing.\n\nUSE FOR: \"create a Reddit target group named X targeting r/technology\", \"save this Reddit audience as X\".\n\n⚠️ CHANNEL: Only use this tool when the user explicitly asked for a REDDIT target group. Do NOT infer REDDIT from the underlying audience NAMES (e.g. an audience called \"Reddit Conquest Mktg LI_n7q\" does NOT mean the user wants a REDDIT target group — audience naming is human convention, not a routing signal). Default to `create_target_group` (LINKEDIN) when channel is unspecified; ask the user if unsure.\n\nTARGETING STRUCTURE (LOCATION must be the FIRST include block with isModifiable=false):\n{\n  \"include\": [\n    {\n      \"isModifiable\": false,\n      \"criteria\": [{\"type\": \"LOCATION\", \"data\": {\n        \"countries\": [229],\n        \"states\": []\n      }}],\n      \"audiences\": []\n    },\n    {\n      \"isModifiable\": true,\n      \"criteria\": [{\"type\": \"COMMUNITIES\", \"data\": [\n        {\"externalId\": \"t5_2qh1i\", \"name\": \"r/technology\"}\n      ]}],\n      \"audiences\": []\n    }\n  ],\n  \"exclude\": {\n    \"isModifiable\": true,\n    \"criteria\": [{\"type\": \"KEYWORDS\", \"data\": [{\"externalId\": \"kw_123\", \"name\": \"crypto\"}]}],\n    \"audiences\": []\n  }\n}\n\nREDDIT CRITERIA TYPES (all references from search_reddit_criteria):\n- LOCATION: {\"countries\": [<id>, ...], \"states\": [<id>, ...]} — arrays of NUMERIC Metadata IDs (use the numeric `id` from search_reddit_criteria locations, NOT the {id, name} object). MANDATORY FIRST in include, isModifiable=false\n- COMMUNITIES: Array of {externalId, name} — subreddits (e.g. r/technology)\n- INTERESTS: Array of {externalId, name} — Reddit interest categories\n- KEYWORDS: Array of {externalId, name} — contextual keywords\n\nINCLUDE: Array of rule blocks (AND-joined). EXCLUDE: single object with `criteria` and `audiences`.\n\nAUDIENCES (combine with the criteria above to build complex target groups): each include\nblock and the exclude object also accept an `audiences` array, so you can mix existing\nMetadata audiences with the Reddit criteria in the same block. Get them from the audience\nlisting tools / get_matched_audiences. Each audience entry: {mdAudienceId, name, type,\nmatchCount, matchCountType: \"FIXED_SIZE\", inactive, criteria: [the audience's own criteria]}.\n`mdAudienceId` MUST be the numeric Custom Audience ID (integer, e.g. 49067), NEVER the\naudience name string; the platform's `mdAudienceId` is typed `Long` and the tool rejects\nnon-integer values pre-flight. In the exclude block, use the EXCLUDE variant of the type\n(e.g. RETARGETING_EXCLUDE).\n\nRESPONSE: {id, name, channel, audienceSize, status, createdDate, targeting}\n\nWORKFLOW: search_reddit_criteria → build targeting (LOCATION first) → confirm → create_reddit_target_group",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "maxLength": 49,
      "description": "The name for the new Reddit target group. Must be unique, descriptive, and UNDER 50 characters — the platform rejects 50+ character names with a 400."
     },
     "targeting": {
      "type": "object",
      "description": "Complete Reddit targeting configuration with include/exclude rules. LOCATION must be the first include block. All references must come from search_reddit_criteria.",
      "properties": {
       "include": {
        "type": "array",
        "items": {
         "type": "object",
         "properties": {
          "isModifiable": {
           "type": "boolean"
          },
          "criteria": {
           "type": "array",
           "items": {
            "type": "object",
            "properties": {
             "type": {
              "type": "string"
             },
             "data": {}
            }
           }
          },
          "audiences": {
           "type": "array",
           "items": {
            "type": "object"
           }
          }
         }
        }
       },
       "exclude": {
        "type": "object",
        "properties": {
         "criteria": {
          "type": "array",
          "items": {
           "type": "object"
          }
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "object"
          }
         }
        }
       }
      }
     }
    },
    "required": [
     "name",
     "targeting"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_retargeting_audience",
   "title": "Create Retargeting Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Create a new retargeting audience on the Metadata platform from native channel audiences.",
   "description": "Create a new retargeting audience on the Metadata platform from native channel audiences.\n\n                This tool creates a retargeting audience by importing an existing audience from a native advertising\n                channel (Google Ads, LinkedIn, or Facebook). The audience can be selected from the list of available\n                retargeting audiences returned by the get_retargeting_audiences tool.\n\n                **REQUIRED TWO-STEP WORKFLOW:**\n\n                STEP 1: Get available audiences for your channel\n                - Call get_retargeting_audiences with your channel (GOOGLE_ADS, LINKEDIN, or FACEBOOK)\n                - Review the returned list of available audiences with their details (name, matchCount, etc.)\n                - Select the audience you want to import by its ID or by matching its properties\n\n                STEP 2: Create the retargeting audience\n                - Use this tool (create_retargeting_audience) with the audience data from Step 1\n                - Provide the audience details from the get_retargeting_audiences response\n                - The audience will be created and imported into the Metadata platform\n\n                SUPPORTED CHANNELS:\n                - GOOGLE_ADS: Google Ads retargeting audiences\n                - LINKEDIN: LinkedIn retargeting audiences\n                - FACEBOOK: Facebook retargeting audiences\n\n                WHEN TO USE:\n                - \"Create a retargeting audience from [audience name] in [channel]\"\n                - \"Import [native channel] audience into Metadata\"\n                - \"Add [audience name] from [channel] as a retargeting audience\"\n                - \"Create a new retargeting audience based on [channel] native audience\"\n\n                WORKFLOW EXAMPLE:\n                User: \"Create a retargeting audience from my Google Ads campaign audience\"\n                1. Call get_retargeting_audiences(channel=\"GOOGLE_ADS\") to list available audiences\n                2. Select the desired audience from the results\n                3. Call create_retargeting_audience with the audience object from the results\n\n                PARAMETERS:\n                - audience_data: Complete audience object from get_retargeting_audiences response (required)\n                  Should include: id, name, type, subtype, channel, matchCount, externalAccountId\n                  Note: This is typically the entire audience object returned by get_retargeting_audiences\n\n                AUDIENCE DATA STRUCTURE:\n                The audience_data parameter should be a dictionary containing:\n                - id: Unique audience ID (required)\n                - name: Audience name (required)\n                - type: Audience type (e.g., GoogleAdsAudience, LinkedInAudience, FacebookAudience) (required)\n                - subtype: Audience subtype (e.g., GOOGLE_ADS_RETARGETING, LINKEDIN_RETARGETING) (required)\n                - channel: Channel type identifier (required)\n                - matchCount: Number of matches/members in the audience\n                - externalAccountId: Associated account ID on the advertising platform (required)\n                - mdId: Metadata ID (optional, typically null for new imports)\n                - externalName: External name (optional)\n                - externalSubType: External subtype (optional)\n                - seedSourcePlatform: Source platform for the audience seed (optional)\n\n                RETURNS:\n                Success confirmation with details about the created retargeting audience including:\n                - Audience ID\n                - Audience name\n                - Type and subtype\n                - Channel\n                - Match count\n                - Creation status\n\n                COMMON USE CASES:\n                - Create retargeting audiences from Google Ads native audiences\n                - Import LinkedIn retargeting audiences into Metadata\n                - Setup Facebook retargeting audiences for multi-channel campaigns\n                - Consolidate native channel audiences into Metadata platform\n\n                IMPORTANT NOTES:\n                - Audience must be selected from get_retargeting_audiences results\n                - The audience object must contain all required fields\n                - Channel must be GOOGLE_ADS, LINKEDIN, or FACEBOOK\n                - Audience creation is immediate and the audience becomes available for targeting\n                - Once created, the audience can be used in campaigns across channels",
   "inputSchema": {
    "type": "object",
    "properties": {
     "audience_data": {
      "type": "object",
      "description": "Complete audience object from get_retargeting_audiences response. Should contain: id, name, type, subtype, channel, matchCount, externalAccountId, and other relevant fields.",
      "properties": {
       "id": {
        "type": [
         "string",
         "integer"
        ],
        "description": "Unique audience ID from the native channel"
       },
       "name": {
        "type": "string",
        "description": "Audience name",
        "minLength": 1
       },
       "type": {
        "type": "string",
        "description": "Audience type (e.g., GoogleAdsAudience, LinkedInAudience, FacebookAudience)",
        "minLength": 1
       },
       "subtype": {
        "type": "string",
        "description": "Audience subtype (e.g., GOOGLE_ADS_RETARGETING, LINKEDIN_RETARGETING, FACEBOOK_RETARGETING)",
        "minLength": 1
       },
       "channel": {
        "type": "string",
        "description": "Channel type identifier",
        "minLength": 1
       },
       "matchCount": {
        "type": [
         "integer",
         "string",
         "null"
        ],
        "description": "Number of matches/members in the audience"
       },
       "externalAccountId": {
        "type": [
         "string",
         "integer"
        ],
        "description": "Associated account ID on the advertising platform"
       },
       "mdId": {
        "type": [
         "string",
         "integer",
         "null"
        ],
        "description": "Metadata ID (optional, typically null for new imports)"
       },
       "externalName": {
        "type": [
         "string",
         "null"
        ],
        "description": "External name (optional)"
       },
       "externalSubType": {
        "type": [
         "string",
         "null"
        ],
        "description": "External subtype (optional)"
       },
       "seedSourcePlatform": {
        "type": [
         "string",
         "null"
        ],
        "description": "Source platform for the audience seed (optional)"
       },
       "matchCountType": {
        "type": [
         "string",
         "null"
        ],
        "description": "Type of match count (optional)"
       }
      },
      "required": [
       "id",
       "name",
       "type",
       "subtype",
       "channel",
       "externalAccountId"
      ],
      "additionalProperties": true
     }
    },
    "required": [
     "audience_data"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_salesforce_contacts_audience",
   "title": "Create Salesforce Contacts Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Create a Salesforce-contacts custom audience (platform customAudienceType SFDC_CONTACTS_STATIC or SFDC_CONTACTS_DYNAMIC) from a Salesforce mixed-criteria.",
   "description": "Create a Salesforce-contacts custom audience (platform customAudienceType SFDC_CONTACTS_STATIC or SFDC_CONTACTS_DYNAMIC) from a Salesforce mixed-criteria.\n\n                This is one of the audience types eligible for Microsoft Ads (Customer Match). Use it when a user wants a Salesforce-sourced contacts audience, including for a Microsoft Ads campaign.\n\n                PREREQUISITE:\n                  • Salesforce integration MUST be connected — the tool checks this and, if not connected, creates nothing and returns { warning, connected: false }.\n                  • A criteria_id from list_salesforce_mixed_criteria — do NOT invent one.\n\n                VARIANT (mirrors the UI's static/dynamic choice):\n                  • STATIC  → SFDC_CONTACTS_STATIC — snapshot at creation, does not refresh.\n                  • DYNAMIC → SFDC_CONTACTS_DYNAMIC — refreshes as the Salesforce data changes.\n                  • If the user did not say which, ASK; do not default silently.\n\n                ASYNC: the platform creates the audience in the background and returns a flow id, NOT a ready audience id. The audience appears under the account's Salesforce audiences once the flow completes, and its contact count fills in then. Do not expect to associate it to a campaign in the same turn.\n\n                PARAMETERS:\n                  • name (required): audience display name.\n                  • criteria_id (required): id from list_salesforce_mixed_criteria.\n                  • variant (required): STATIC or DYNAMIC.\n\n                RETURNS: { id, name, status, createdDate, audience_type, async: true } — `audience_type` is the SFDC_CONTACTS_* you created and `id` is a creation/flow id, not associable yet. If Salesforce is not connected: { warning, connected: false }, and nothing is created.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "Audience display name."
     },
     "criteria_id": {
      "type": "integer",
      "description": "The Salesforce mixed-criteria id from list_salesforce_mixed_criteria."
     },
     "variant": {
      "type": "string",
      "enum": [
       "STATIC",
       "DYNAMIC"
      ],
      "description": "STATIC = snapshot (SFDC_CONTACTS_STATIC); DYNAMIC = refreshing (SFDC_CONTACTS_DYNAMIC). Ask the user if unspecified."
     }
    },
    "required": [
     "name",
     "criteria_id",
     "variant"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_segment",
   "title": "Create Segment",
   "category": "audiences",
   "risk": "write",
   "summary": "Create a new segment with insights criteria.",
   "description": "Create a new segment with insights criteria.\n\nWORKFLOW:\n1. Use search_insights_criteria_fields to discover valid fields\n2. Build criteria object\n3. Call create_segment with name and criteria\n\nCRITERIA STRUCTURE:\n{\"operator\": \"AND|OR\", \"match\": [{field, operator, type, value}, ...]}\n\nFIELD TYPES: MULTIPICKLIST (array of {name,label}), PICKLIST (single {name,label}), STRING, NUMERIC, BOOLEAN, DATE\n\nOPERATORS: AND, OR, EQUALS, NOT_EQUAL, LIKE, NOT_LIKE, IN, NOT_IN, CONTAINS, GREATER_THAN, GREATER_OR_EQUAL, LESS_THAN, LESS_OR_EQUAL, IN_THE_LAST, NOT_IN_THE_LAST, BETWEEN, IN_THE_NEXT, EXISTS, NOT_EXISTS\n\nRETURNS: {id, name, criteria, status, createdDate}\nSTATUS: READY (usable), PENDING (processing), FAILED (error)\n\nEXAMPLE:\n{\"name\": \"High Engagement\", \"criteria\": {\"operator\": \"AND\", \"match\": [{\"field\": \"ENGAGEMENT_SCORE_CLASSIFICATION\", \"operator\": \"IN\", \"type\": \"MULTIPICKLIST\", \"value\": [{\"name\": \"HIGH\", \"label\": \"High\"}]}]}}",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "minLength": 1,
      "description": "Descriptive, unique segment name."
     },
     "criteria": {
      "type": "object",
      "description": "Criteria object with operator and match fields.",
      "properties": {
       "operator": {
        "type": "string",
        "enum": [
         "AND",
         "OR"
        ],
        "description": "Logical operator for combining criteria."
       },
       "match": {
        "type": "array",
        "minItems": 1,
        "description": "Array of match criteria objects.",
        "items": {
         "type": "object",
         "properties": {
          "field": {
           "type": "string"
          },
          "operator": {
           "type": "string"
          },
          "type": {
           "type": "string"
          },
          "value": {}
         },
         "required": [
          "field"
         ]
        }
       }
      },
      "required": [
       "operator",
       "match"
      ]
     }
    },
    "required": [
     "name",
     "criteria"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_target_group",
   "title": "Create Target Group",
   "category": "audiences",
   "risk": "write",
   "summary": "Create a new target group with name and targeting criteria.",
   "description": "Create a new target group with name and targeting criteria. Supports BOTH include AND exclude rules — including excluding existing Metadata audiences.\n\nPREREQUISITE: All criteria IDs MUST come from search_target_group_criteria. estimate_target_group first is MANDATORY: verify isMatchCountTooSmall=false and report the reach to the user BEFORE creating. A target group created without an estimate can land at zero matches and nothing downstream will notice (PRD-31759). Same bar as update_target_group.\n\nUSE FOR: \"create target group named X with criteria Y\", \"save this target group as X\", \"create new audience called X\", \"create target group excluding companies Z\", \"create target group that excludes audience X\".\n\n⚠️ CHANNEL: This tool creates LINKEDIN target groups only. Do NOT infer channel from the underlying audience NAMES (e.g. an audience called \"Reddit Conquest Mktg LI_n7q\" does NOT mean the user wants a REDDIT target group — audience naming is human convention, not a routing signal). Use REDDIT only when the user explicitly named the REDDIT channel in the request; in that case use `create_reddit_target_group` instead. When in doubt, ask the user which channel.\n\n**CRITICAL FOR LINKEDIN**: LOCATION must be FIRST criteria in include array with isModifiable=false.\n\nTARGETING STRUCTURE (full example with include + exclude criteria + exclude audiences):\n{\n  \"include\": [\n    {\n      \"isModifiable\": false,\n      \"criteria\": [{\"type\": \"NATIVE_LOCATION\", \"data\": [\n        {\"externalId\": \"urn:li:geo:101165590\", \"name\": \"United Kingdom\"},\n        {\"externalId\": \"urn:li:geo:103644278\", \"name\": \"United States\"}\n      ]}],\n      \"audiences\": []\n    },\n    {\n      \"isModifiable\": true,\n      \"criteria\": [{\"type\": \"AGE\", \"data\": [{\"externalId\": \"urn:li:ageRange:(25,34)\", \"name\": \"25 to 34\"}]}],\n      \"audiences\": []\n    }\n  ],\n  \"exclude\": {\n    \"isModifiable\": true,\n    \"criteria\": [\n      {\"type\": \"COMPANY_NAME\", \"data\": [{\"externalId\": \"urn:li:organization:1090\", \"name\": \"Philips\"}]}\n    ],\n    \"audiences\": [\n      {\n        \"mdAudienceId\": 45316,\n        \"name\": \"ICP Pharma LifeSci Leaders\",\n        \"type\": \"FIRMOGRAPHIC_EXCLUDE\",\n        \"matchCount\": 213127,\n        \"matchCountType\": \"FIXED_SIZE\",\n        \"inactive\": false,\n        \"criteria\": [{\"type\": \"NATIVE_LOCATION\", \"data\": [{\"externalId\": \"urn:li:geo:103644278\", \"name\": \"United States\"}]}]\n      }\n    ]\n  }\n}\n\nINCLUDE: Array of rule blocks (AND-joined). Each block holds a criteria group.\nEXCLUDE: Single object (NOT array) with `criteria` and `audiences`. Anything matching exclude is removed.\n\n⚠️ AUDIENCES ≠ TARGET GROUPS. Both `include[].audiences` and `exclude.audiences` accept Custom Audience records ONLY (the entities returned by `get_matched_audiences`). Target Groups (the entities returned by `list_target_groups` / `retrieve_target_group_by_id`) are a separate entity in a separate ID space and CANNOT be attached here; the platform silently drops or rejects them. If the user names \"X\" and only `list_target_groups` matches it (not `get_matched_audiences`), do NOT pass the Target Group's id as `mdAudienceId`. Stop, tell the user \"X is a Target Group, not a Custom Audience\", and offer to rebuild X as a Custom Audience (e.g. via `create_audiences` with the same criteria) before retrying.\n\nINCLUDE AUDIENCES: `include[].audiences` accepts ANY Metadata audience type — there is NO allow-list and NO restriction to specific types. Pass any audience returned by the audience listing tools (e.g. FIRMOGRAPHIC_INCLUDE, FIRMOGRAPHIC_INCLUDE_SFDC, NATIVE_LINKEDIN, NATIVE_FACEBOOK, RETARGETING, TECHNOGRAPHIC, SFDC_ACCOUNT_DYNAMIC, MA_CONTACTS_DYNAMIC, CONTACT_LIST, and any other type, including types not enumerated in any tool schema). Type-name enums seen in `create_audiences` / `search_audiences` describe how audiences are CLASSIFIED, not which can be attached here. Caveat: estimates may under-report (or return zero) when combining server-resolved audiences (e.g. SFDC dynamic, retargeting) with native LinkedIn criteria — that is an estimate-side reconciliation issue, not a creation constraint, so a zero estimate does not necessarily mean the platform will reject the create call. If a zero estimate is suspected to be a reconciliation artifact, attempt the create and verify in the UI.\n\nHOW TO EXCLUDE AN EXISTING METADATA CUSTOM AUDIENCE: Add it as an object in `exclude.audiences`. The tool pre-flight-rejects placeholder values — never invent ids or types when you don't know them, call `get_matched_audiences` (or `search_audiences`) first and use the real values from the response. Required fields:\n- `mdAudienceId` (integer, REQUIRED): The numeric Custom Audience ID from `get_matched_audiences` (e.g. 49067, 45316). NEVER the audience name string, NEVER a placeholder like 1/2/3/4 (real Custom Audience IDs are 5-digit numbers; the tool rejects anything under 100), and NEVER a Target Group id (those come from `list_target_groups`; see the AUDIENCES ≠ TARGET GROUPS note above).\n- `name` (string): Audience name.\n- `type` (string): MUST be the EXCLUDE variant of the audience type, e.g. `FIRMOGRAPHIC_EXCLUDE`, `RETARGETING_EXCLUDE`, `CONTACT_LIST_EXCLUDE`. NEVER the placeholder string \"AUDIENCE\" — the tool rejects that pre-flight.\n- `matchCount` (int), `matchCountType` (\"FIXED_SIZE\"), `inactive` (bool): copy from audience record. The tool pre-flight-rejects entries with `inactive: true` (an audience still matching after creation cannot be attached yet — attach it once it is active).\n- `criteria`: The audience's own criteria array (copy from the audience's definition).\n\nCRITERIA TYPES (all IDs from search_target_group_criteria), valid in BOTH include and exclude:\n- NATIVE_LOCATION / LOCATION: location data — MANDATORY FIRST in include. Multi-country geo goes in ONE location criterion with one {externalId, name} entry per country (e.g. \"Canada and USA\" = 2 entries in the same data array). Never silently drop a country the user named.\n- AGE, COMPANY_CATEGORY, COMPANY_GROWTH_RATE, INDUSTRY, COMPANY_NAME, REVENUE, EMPLOYEE: Array of {externalId, name}\n- JOB_FUNCTION, JOB_TITLE, MEMBER_GROUP, MEMBER_INTEREST, MEMBER_SKILL, MEMBER_BEHAVIOR, SENIORITY, FOLLOWED_COMPANIES: Array of {externalId, name}\n- YEARS_OF_EXPERIENCE_RANGE: {\"min\": 4, \"max\": 5}\n\nWORKFLOW: search_target_group_criteria → build targeting (include + optional exclude criteria/audiences) → estimate_target_group → confirm → create_target_group\n\nRESPONSE: {id, name, channel, audienceSize, status, createdDate, targeting}",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "maxLength": 49,
      "description": "The name for the new target group. Must be unique, descriptive, and UNDER 50 characters — the platform rejects 50+ character names with a 400."
     },
     "channel": {
      "type": "string",
      "enum": [
       "LINKEDIN"
      ],
      "description": "The advertising channel for the target group. Only LINKEDIN is currently supported."
     },
     "targeting": {
      "type": "object",
      "description": "Complete targeting configuration with include/exclude rules...",
      "properties": {
       "include": {
        "type": "array",
        "description": "Array of include rules with criteria and audiences",
        "items": {
         "type": "object",
         "properties": {
          "isModifiable": {
           "type": "boolean"
          },
          "criteria": {
           "type": "array",
           "items": {
            "type": "object",
            "properties": {
             "type": {
              "type": "string"
             },
             "data": {}
            }
           }
          },
          "audiences": {
           "type": "array",
           "items": {
            "type": "object"
           }
          }
         }
        }
       },
       "exclude": {
        "type": "object",
        "description": "Exclude rules with criteria and audiences",
        "properties": {
         "criteria": {
          "type": "array",
          "items": {
           "type": "object"
          }
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "object"
          }
         }
        }
       }
      }
     }
    },
    "required": [
     "name",
     "channel"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_technographic_audience",
   "title": "Create Technographic Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Creates B2B audiences based on technology stack, software usage, and firmographic attributes.",
   "description": "Creates B2B audiences based on technology stack, software usage, and firmographic attributes.\n\n                ESTIMATE-ONLY MODE:\n                Pass estimate_only=true to preview audience size (companies + contacts) WITHOUT creating the audience.\n                In this mode the tool returns expectedNumberOfCompanies,\n                expectedNumberOfContacts, and cappedContactCount. Default is false (audience is created).\n\n                WHEN TO USE:\n                - Target companies using specific technologies or software\n                - Create audiences based on technology adoption patterns\n                - Target by technology categories (CRM, Marketing Automation, Analytics, etc.)\n                - Combine technology targeting with firmographic filters\n                - Build tech-savvy audience segments for B2B campaigns\n                - User wants to target companies and contacts that are familiar/using some specific software like hubspot, g2, etc.\n\n                TECHNOLOGY CATEGORIES:\n                - CRM: Salesforce, HubSpot, Microsoft Dynamics, Zoho, Pipedrive\n                - Marketing Automation: Marketo, Pardot, Eloqua, HubSpot, Mailchimp\n                - Analytics: Google Analytics, Adobe Analytics, Mixpanel, Amplitude\n                - E-commerce: Shopify, Magento, WooCommerce, BigCommerce\n                - Content Management: WordPress, Drupal, Joomla, Adobe Experience Manager\n                - Collaboration: Slack, Microsoft Teams, Zoom, Asana, Monday.com\n                - Cloud Infrastructure: AWS, Azure, Google Cloud, Oracle Cloud\n                - Database: MySQL, PostgreSQL, MongoDB, Oracle, SQL Server\n                - And many more technology categories available\n\n                FIRMOGRAPHIC FILTERS (OPTIONAL):\n                - Industries: Use industry IDs (1-148, same as firmographic audiences)\n                - Employees: \"LESS_THAN_10\", \"FROM_10_TO_50\", \"FROM_50_TO_200\", \"FROM_200_TO_500\", \"FROM_500_TO_1000\", \"FROM_1000_TO_5000\", \"FROM_5000_TO_10000\", \"OVER_10000\"\n                - Revenue: \"LESS_THAN_1M\", \"FROM_1M_TO_10M\", \"FROM_10M_TO_50M\", \"FROM_50M_TO_100M\", \"FROM_100M_TO_200M\", \"FROM_200M_TO_500M\", \"FROM_500M_TO_1000M\", \"OVER_1000M\"\n\n                CONTACTS CRITERIA:\nUse the following parameters to filter and target specific contacts within accounts:\n\nGEOGRAPHIC TARGETING:\n- location_country_ids: Array of country IDs (e.g., [29] for Brazil, [229] for United States)\n- location_state_ids: Array of state IDs (e.g., [5] for California, [32] for New York)\n\nJOB TITLES:\n- job_title_includes: Array of free text strings to include (e.g., [\"Software Engineer\", \"Manager\"])\n- job_title_excludes: Array of free text strings to exclude (e.g., [\"Senior\", \"Lead\"])\n\nJOB FUNCTIONS:\nValid job function IDs and their labels:\n- 1: Marketing, 2: Sales, 3: Customer Service, 4: Finance, 5: Human Resources, 6: Public Relations\n- 7: Media, 8: Design, 9: Engineering, 10: Operations, 11: Legal, 12: Education\n- 13: Health, 14: Real Estate, 15: Accounting, 16: Administrative, 17: Business Development\n- 18: Community and Social Services, 19: Consulting, 20: Entrepreneurship, 21: Information Technology\n- 22: Military and Protective Services, 23: Product Management, 24: Program and Project Management\n- 25: Purchasing, 26: Quality Assurance, 27: Research, 28: Trades\n\n- job_function_include_ids: Array of job function IDs to include (e.g., [9] for Engineering)\n- job_function_exclude_ids: Array of job function IDs to exclude\n\nSENIORITY LEVELS:\nValid seniority IDs and their labels:\n- 2: Manager, 3: Director, 4: VP, 5: CXO, 6: Unpaid, 7: Training\n- 8: Senior, 9: Owner, 10: Entry, 11: Partner\n\n- seniority_include_ids: Array of seniority level IDs to include (e.g., [10, 7] for Entry and Training)\n- seniority_exclude_ids: Array of seniority level IDs to exclude\n\nCONTACT LIMITS:\n- contacts_per_company_limit: Maximum number of contacts to target per company (optional). Example: 20\n\nUSAGE NOTES:\n- All ID fields (countries, states, job functions, seniorities) use numeric IDs from the platform\n- Job titles are free text strings, not IDs\n- Use include parameters to specify which criteria to target\n- Use exclude parameters to filter out contacts matching specific criteria\n- Combine multiple criteria for precise targeting (e.g., Senior Engineers in California)\n- Criteria are combined with AND logic, so all conditions must be met\n\nCONTACTS CRITERIA PARAMETERS(Usually after other initial audience parameters to be described next):\n- location_country_ids: Array of country IDs (optional)\n- location_state_ids: Array of state IDs (optional)\n- job_title_includes: Array of job title keywords to include (optional)\n- job_title_excludes: Array of job title keywords to exclude (optional)\n- job_function_include_ids: Array of job function IDs (optional)\n- job_function_exclude_ids: Array of job function IDs (optional)\n- seniority_include_ids: Array of seniority level IDs (optional)\n- seniority_exclude_ids: Array of seniority level IDs (optional)\n- contacts_per_company_limit: Maximum contacts per company (optional)\n\n\n                PARAMETERS:\n                - name: Audience name (required) - **IMPORTANT: Name MUST NOT exceed 50 characters. This is a mandatory requirement.**\n                - technologies: Array of technology names (required)\n                - industries: Array of industry IDs (optional)\n                - employees: Array of employee count ranges (optional)\n                - revenue: Array of revenue ranges (optional)\n\n                RETURNS:\n                Audience details with ID, size estimation, and creation status.\n\n                COMMON USE CASES:\n                - Target companies using Salesforce and HubSpot\n                - Find AWS users in the software industry\n                - Build audiences of companies using specific MarTech stacks\n                - Combine technology and firmographic targeting for precise B2B campaigns\n\n                IMPORTANT NOTES:\n                - **CRITICAL: Audience name MUST NOT exceed 50 characters - this is mandatory and will cause creation to fail if exceeded**\n                - name and technologies are required parameters\n                - All other parameters are optional for flexible audience creation\n                - Technology names should match available technologies in the Metadata platform\n                - Contact criteria fields work the same as in other audience types\n                - Audience size depends on technology filters and applied criteria",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "Audience name (required). Maximum 50 characters.",
      "maxLength": 50
     },
     "technologies": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Array of technology names to target (required). Example: ['Salesforce', 'HubSpot', 'Marketo']",
      "minItems": 1
     },
     "industries": {
      "type": "array",
      "items": {
       "type": "integer",
       "enum": [
        1,
        3,
        4,
        5,
        6,
        7,
        8,
        9,
        10,
        11,
        12,
        13,
        14,
        15,
        16,
        17,
        18,
        19,
        20,
        21,
        22,
        23,
        24,
        25,
        26,
        27,
        28,
        29,
        30,
        31,
        32,
        33,
        34,
        35,
        36,
        37,
        38,
        39,
        40,
        41,
        42,
        43,
        44,
        45,
        46,
        47,
        48,
        49,
        50,
        51,
        52,
        53,
        54,
        55,
        56,
        57,
        58,
        59,
        60,
        61,
        62,
        63,
        64,
        65,
        66,
        67,
        68,
        69,
        70,
        71,
        72,
        73,
        74,
        75,
        76,
        77,
        78,
        79,
        80,
        81,
        82,
        83,
        84,
        85,
        86,
        87,
        88,
        89,
        90,
        91,
        92,
        93,
        94,
        95,
        96,
        97,
        98,
        99,
        100,
        101,
        102,
        103,
        104,
        105,
        106,
        107,
        108,
        109,
        110,
        111,
        112,
        113,
        114,
        115,
        116,
        117,
        118,
        119,
        120,
        121,
        122,
        123,
        124,
        125,
        126,
        127,
        128,
        129,
        130,
        131,
        132,
        133,
        134,
        135,
        136,
        137,
        138,
        139,
        140,
        141,
        142,
        143,
        144,
        145,
        146,
        147,
        148
       ]
      },
      "description": "Industry IDs for firmographic/technographic filtering (optional). Valid IDs: 1=Defense & Space, 3=Computer Hardware, 4=Computer Software, 5=Computer Networking, 6=Internet, 7=Semiconductors, 8=Telecommunications, 9=Law Practice, 10=Legal Services, 11=Management Consulting, 12=Biotechnology, 13=Medical Practice, 14=Hospital & Health Care, 15=Pharmaceuticals, 16=Veterinary, 17=Medical Device, 18=Cosmetics, 19=Apparel & Fashion, 20=Sporting Goods, 21=Tobacco, 22=Supermarkets, 23=Food Production, 24=Consumer Electronics, 25=Consumer Goods, 26=Furniture, 27=Retail, 28=Entertainment, 29=Gambling & Casinos, 30=Leisure, Travel & Tourism, 31=Hospitality, 32=Restaurants, 33=Sports, 34=Food & Beverages, 35=Motion Pictures & Film, 36=Broadcast Media, 37=Museums & Institutions, 38=Fine Art, 39=Performing Arts, 40=Recreational Facilities & Services, 41=Banking, 42=Insurance, 43=Financial Services, 44=Real Estate, 45=Investment Banking, 46=Investment Management, 47=Accounting, 48=Construction, 49=Building Materials, 50=Architecture & Planning, 51=Civil Engineering, 52=Aviation & Aerospace, 53=Automotive, 54=Chemicals, 55=Machinery, 56=Mining & Metals, 57=Oil & Energy, 58=Shipbuilding, 59=Utilities, 60=Textiles, 61=Paper & Forest Products, 62=Railroad Manufacture, 63=Farming, 64=Ranching, 65=Dairy, 66=Fishery, 67=Primary/Secondary Education, 68=Higher Education, 69=Education Management, 70=Research, 71=Military, 72=Legislative Office, 73=Judiciary, 74=International Affairs, 75=Government Administration, 76=Executive Office, 77=Law Enforcement, 78=Public Safety, 79=Public Policy, 80=Marketing & Advertising, 81=Newspapers, 82=Publishing, 83=Printing, 84=Information Services, 85=Libraries, 86=Environmental Services, 87=Package/Freight Delivery, 88=Individual & Family Services, 89=Religious Institutions, 90=Civic & Social Organization, 91=Consumer Services, 92=Transportation/Trucking/Railroad, 93=Warehousing, 94=Airlines/Aviation, 95=Maritime, 96=Information Technology & Services, 97=Market Research, 98=Public Relations & Communications, 99=Design, 100=Non-profit Organization Management, 101=Fundraising, 102=Program Development, 103=Writing & Editing, 104=Staffing & Recruiting, 105=Professional Training & Coaching, 106=Venture Capital & Private Equity, 107=Political Organization, 108=Translation & Localization, 109=Computer Games, 110=Events Services, 111=Arts & Crafts, 112=Electrical & Electronic Manufacturing, 113=Online Media, 114=Nanotechnology, 115=Music, 116=Logistics & Supply Chain, 117=Plastics, 118=Computer & Network Security, 119=Wireless, 120=Alternative Dispute Resolution, 121=Security & Investigations, 122=Facilities Services, 123=Outsourcing/Offshoring, 124=Health, Wellness & Fitness, 125=Alternative Medicine, 126=Media Production, 127=Animation, 128=Commercial Real Estate, 129=Capital Markets, 130=Think Tanks, 131=Philanthropy, 132=E-learning, 133=Wholesale, 134=Import & Export, 135=Mechanical Or Industrial Engineering, 136=Photography, 137=Human Resources, 138=Business Supplies & Equipment, 139=Mental Health Care, 140=Graphic Design, 141=International Trade & Development, 142=Wine & Spirits, 143=Luxury Goods & Jewelry, 144=Renewables & Environment, 145=Glass, Ceramics & Concrete, 146=Packaging & Containers, 147=Industrial Automation, 148=Government Relations. Example: [4, 96, 11] for Software, IT Services, Consulting"
     },
     "employees": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "LESS_THAN_10",
        "FROM_10_TO_50",
        "FROM_50_TO_200",
        "FROM_200_TO_500",
        "FROM_500_TO_1000",
        "FROM_1000_TO_5000",
        "FROM_5000_TO_10000",
        "OVER_10000"
       ]
      },
      "description": "Employee count ranges for firmographic filtering (optional). Valid enum values: LESS_THAN_10 (0-9), FROM_10_TO_50 (10-49), FROM_50_TO_200 (50-199), FROM_200_TO_500 (200-499), FROM_500_TO_1000 (500-999), FROM_1000_TO_5000 (1,000-4,999), FROM_5000_TO_10000 (5,000-9,999), OVER_10000 (10,000+). Example: ['FROM_50_TO_200', 'FROM_200_TO_500']"
     },
     "revenue": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "LESS_THAN_1M",
        "FROM_1M_TO_10M",
        "FROM_10M_TO_50M",
        "FROM_50M_TO_100M",
        "FROM_100M_TO_200M",
        "FROM_200M_TO_500M",
        "FROM_500M_TO_1000M",
        "OVER_1000M"
       ]
      },
      "description": "Revenue ranges for firmographic filtering (optional). Valid enum values: LESS_THAN_1M ($0-$1M), FROM_1M_TO_10M ($1M-$10M), FROM_10M_TO_50M ($10M-$50M), FROM_50M_TO_100M ($50M-$100M), FROM_100M_TO_200M ($100M-$200M), FROM_200M_TO_500M ($200M-$500M), FROM_500M_TO_1000M ($500M-$1B), OVER_1000M ($1B+). Example: ['FROM_10M_TO_50M', 'FROM_50M_TO_100M']"
     },
     "location_country_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Country IDs for geographic targeting (optional). Valid IDs: 1 (Afghanistan), 2 (Albania), 3 (Algeria), 4 (American Samoa), 5 (Andorra), 6 (Angola), 7 (Anguilla), 8 (Antarctica), 9 (Antigua and Barbuda), 10 (Argentina), 11 (Armenia), 12 (Aruba), 13 (Australia), 14 (Austria), 15 (Azerbaijan), 16 (Bahamas), 17 (Bahrain), 18 (Bangladesh), 19 (Barbados), 20 (Belarus), 21 (Belgium), 22 (Belize), 23 (Benin), 24 (Bermuda), 25 (Bhutan), 26 (Bolivia), 27 (Bosnia and Herzegovina), 28 (Botswana), 29 (Brazil), 30 (British Indian Ocean Territory), 31 (British Virgin Islands), 32 (Brunei), 33 (Bulgaria), 34 (Burkina Faso), 35 (Burundi), 36 (Cambodia), 37 (Cameroon), 38 (Canada), 39 (Cape Verde), 40 (Cayman Islands), 41 (Central African Republic), 42 (Chad), 43 (Chile), 44 (China), 45 (Christmas Island), 46 (Cocos Islands), 47 (Colombia), 48 (Comoros), 49 (Cook Islands), 50 (Costa Rica), 51 (Croatia), 53 (Curacao), 54 (Cyprus), 55 (Czech Republic), 56 (Democratic Republic of the Congo), 57 (Denmark), 58 (Djibouti), 59 (Dominica), 60 (Dominican Republic), 61 (East Timor), 62 (Ecuador), 63 (Egypt), 64 (El Salvador), 65 (Equatorial Guinea), 66 (Eritrea), 67 (Estonia), 68 (Ethiopia), 69 (Falkland Islands), 70 (Faroe Islands), 71 (Fiji), 72 (Finland), 73 (France), 74 (French Polynesia), 75 (Gabon), 76 (Gambia), 77 (Georgia), 78 (Germany), 79 (Ghana), 80 (Gibraltar), 81 (Greece), 82 (Greenland), 83 (Grenada), 84 (Guam), 85 (Guatemala), 86 (Guernsey), 87 (Guinea), 88 (Guinea-Bissau), 89 (Guyana), 90 (Haiti), 91 (Honduras), 92 (Hong Kong), 93 (Hungary), 94 (Iceland), 95 (India), 96 (Indonesia), 98 (Iraq), 99 (Ireland), 100 (Isle of Man), 101 (Israel), 102 (Italy), 103 (Ivory Coast), 104 (Jamaica), 105 (Japan), 106 (Jersey), 107 (Jordan), 108 (Kazakhstan), 109 (Kenya), 110 (Kiribati), 111 (Kosovo), 112 (Kuwait), 113 (Kyrgyzstan), 114 (Laos), 115 (Latvia), 116 (Lebanon), 117 (Lesotho), 118 (Liberia), 119 (Libya), 120 (Liechtenstein), 121 (Lithuania), 122 (Luxembourg), 123 (Macau), 124 (Macedonia), 125 (Madagascar), 126 (Malawi), 127 (Malaysia), 128 (Maldives), 129 (Mali), 130 (Malta), 131 (Marshall Islands), 132 (Mauritania), 133 (Mauritius), 134 (Mayotte), 135 (Mexico), 136 (Micronesia), 137 (Moldova), 138 (Monaco), 139 (Mongolia), 140 (Montenegro), 141 (Montserrat), 142 (Morocco), 143 (Mozambique), 144 (Myanmar), 145 (Namibia), 146 (Nauru), 147 (Nepal), 148 (Netherlands), 149 (Netherlands Antilles), 150 (New Caledonia), 151 (New Zealand), 152 (Nicaragua), 153 (Niger), 154 (Nigeria), 155 (Niue), 157 (Northern Mariana Islands), 158 (Norway), 159 (Oman), 160 (Pakistan), 161 (Palau), 162 (Palestine), 163 (Panama), 164 (Papua New Guinea), 165 (Paraguay), 166 (Peru), 167 (Philippines), 168 (Pitcairn), 169 (Poland), 170 (Portugal), 171 (Puerto Rico), 172 (Qatar), 173 (Republic of the Congo), 174 (Reunion), 175 (Romania), 176 (Russia), 177 (Rwanda), 178 (Saint Barthelemy), 179 (Saint Helena), 180 (Saint Kitts and Nevis), 181 (Saint Lucia), 182 (Saint Martin), 183 (Saint Pierre and Miquelon), 184 (Saint Vincent and the Grenadines), 185 (Samoa), 186 (San Marino), 187 (Sao Tome and Principe), 188 (Saudi Arabia), 189 (Senegal), 190 (Serbia), 191 (Seychelles), 192 (Sierra Leone), 193 (Singapore), 194 (Sint Maarten), 195 (Slovakia), 196 (Slovenia), 197 (Solomon Islands), 198 (Somalia), 199 (South Africa), 200 (South Korea), 201 (South Sudan), 202 (Spain), 203 (Sri Lanka), 205 (Suriname), 206 (Svalbard and Jan Mayen), 207 (Swaziland), 208 (Sweden), 209 (Switzerland), 211 (Taiwan), 212 (Tajikistan), 213 (Tanzania), 214 (Thailand), 215 (Togo), 216 (Tokelau), 217 (Tonga), 218 (Trinidad and Tobago), 219 (Tunisia), 220 (Turkey), 221 (Turkmenistan), 222 (Turks and Caicos Islands), 223 (Tuvalu), 224 (U.S. Virgin Islands), 225 (Uganda), 226 (Ukraine), 227 (United Arab Emirates), 228 (United Kingdom), 229 (United States), 230 (Uruguay), 231 (Uzbekistan), 232 (Vanuatu), 233 (Vatican), 234 (Venezuela), 235 (Vietnam), 236 (Wallis and Futuna), 237 (Western Sahara), 238 (Yemen), 239 (Zambia), 240 (Zimbabwe), 241 (Guadeloupe). Example: [29] for Brazil, [229] for United States"
     },
     "location_state_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "State IDs for geographic targeting (optional). Valid US State IDs: 1 (Alabama), 2 (Alaska), 3 (Arizona), 4 (Arkansas), 5 (California), 6 (Colorado), 7 (Connecticut), 8 (Delaware), 9 (Florida), 10 (Georgia), 11 (Hawaii), 12 (Idaho), 13 (Illinois), 14 (Indiana), 15 (Iowa), 16 (Kansas), 17 (Kentucky), 18 (Louisiana), 19 (Maine), 20 (Maryland), 21 (Massachusetts), 22 (Michigan), 23 (Minnesota), 24 (Mississippi), 25 (Missouri), 26 (Montana), 27 (Nebraska), 28 (Nevada), 29 (New Hampshire), 30 (New Jersey), 31 (New Mexico), 32 (New York), 33 (North Carolina), 34 (North Dakota), 35 (Ohio), 36 (Oklahoma), 37 (Oregon), 38 (Pennsylvania), 39 (Rhode Island), 40 (South Carolina), 41 (South Dakota), 42 (Tennessee), 43 (Texas), 44 (Utah), 45 (Vermont), 46 (Virginia), 47 (Washington), 48 (West Virginia), 49 (Wisconsin), 50 (Wyoming), 52 (District of Columbia). Example: [5] for California, [32] for New York"
     },
     "job_title_includes": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free text keywords for job titles to include (optional). Example: ['Software Engineer', 'Manager']"
     },
     "job_title_excludes": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free text keywords for job titles to exclude (optional). Example: ['Senior', 'Lead']"
     },
     "job_function_include_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Job function IDs to include (optional). Valid IDs: 1 (Marketing), 2 (Sales), 3 (Customer Service), 4 (Finance), 5 (HR), 6 (PR), 7 (Media), 8 (Design), 9 (Engineering), 10 (Operations), 11 (Legal), 12 (Education), 13 (Health), 14 (Real Estate), 15 (Accounting), 16 (Administrative), 17 (Business Development), 18 (Community Services), 19 (Consulting), 20 (Entrepreneurship), 21 (IT), 22 (Military Services), 23 (Product Management), 24 (Program/Project Management), 25 (Purchasing), 26 (QA), 27 (Research), 28 (Trades). Example: [9] for Engineering"
     },
     "job_function_exclude_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Job function IDs to exclude (optional). Valid IDs: 1-28 as listed above"
     },
     "seniority_include_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Seniority level IDs to include (optional). Valid IDs: 2 (Manager), 3 (Director), 4 (VP), 5 (CXO), 6 (Unpaid), 7 (Training), 8 (Senior), 9 (Owner), 10 (Entry), 11 (Partner). Example: [10, 7] for Entry and Training"
     },
     "seniority_exclude_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Seniority level IDs to exclude (optional). Valid IDs: 2-11 as listed above"
     },
     "contacts_per_company_limit": {
      "type": "integer",
      "description": "Maximum number of contacts to target per company (optional). Example: 20",
      "minimum": 1
     },
     "estimate_only": {
      "type": "boolean",
      "description": "If true, do NOT create the audience — only estimate its size and return the counts. Use this when the user wants to preview how many companies/contacts match the criteria before committing. Defaults to false (audience is created).",
      "default": false
     }
    },
    "required": [
     "name",
     "technologies"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "estimate_target_group",
   "title": "Estimate Target Group",
   "category": "audiences",
   "risk": "read",
   "summary": "Estimate audience size for target group criteria before creating.",
   "description": "Estimate audience size for target group criteria before creating. Supports BOTH include AND exclude rules (criteria + audiences).\n\nUSE FOR: Validate audience size before creating, \"how many people will this reach?\", \"preview audience size\", test targeting combinations, preview impact of exclusion criteria/audiences.\n\nCHANNELS: LINKEDIN (default) and REDDIT. Pass channel=\"REDDIT\" to size a Reddit target group before create_reddit_target_group; its criteria ids come from search_reddit_criteria rather than search_target_group_criteria, and the rest of the call is identical (PRD-31759).\n\nPREREQUISITE: All criteria IDs MUST come from search_target_group_criteria first (search_reddit_criteria for REDDIT).\n\n**CRITICAL FOR LINKEDIN**: LOCATION must be FIRST criteria in include array with isModifiable=false.\n\nTARGETING STRUCTURE (full example with include + exclude criteria + exclude audiences):\n{\n  \"include\": [\n    {\n      \"isModifiable\": false,\n      \"criteria\": [{\"type\": \"NATIVE_LOCATION\", \"data\": [{\"externalId\": \"urn:li:geo:103644278\", \"name\": \"United States\"}]}],\n      \"audiences\": []\n    },\n    {\n      \"isModifiable\": true,\n      \"criteria\": [{\"type\": \"AGE\", \"data\": [{\"externalId\": \"urn:li:ageRange:(25,34)\", \"name\": \"25 to 34\"}]}],\n      \"audiences\": []\n    }\n  ],\n  \"exclude\": {\n    \"isModifiable\": true,\n    \"criteria\": [\n      {\"type\": \"COMPANY_NAME\", \"data\": [{\"externalId\": \"urn:li:organization:1090\", \"name\": \"Philips\"}]}\n    ],\n    \"audiences\": [\n      {\n        \"mdAudienceId\": 45316,\n        \"name\": \"ICP Pharma LifeSci Leaders\",\n        \"type\": \"FIRMOGRAPHIC_EXCLUDE\",\n        \"matchCount\": 213127,\n        \"matchCountType\": \"FIXED_SIZE\",\n        \"inactive\": false,\n        \"criteria\": [{\"type\": \"NATIVE_LOCATION\", \"data\": [{\"externalId\": \"urn:li:geo:103644278\", \"name\": \"United States\"}]}]\n      }\n    ]\n  }\n}\n\nINCLUDE: Array of rule blocks. Each block = AND-group of criteria; multiple blocks are AND'd together.\nEXCLUDE: Single object (NOT array) with `criteria` and `audiences`. Anyone matching exclude is removed from results.\n\n⚠️ AUDIENCES ≠ TARGET GROUPS. Both `include[].audiences` and `exclude.audiences` accept Custom Audience records ONLY (the entities returned by `get_matched_audiences`). Target Groups (the entities returned by `list_target_groups` / `retrieve_target_group_by_id`) are a separate entity in a separate ID space and CANNOT be attached here; the platform silently drops or rejects them. If the user names \"X\" and only `list_target_groups` matches it (not `get_matched_audiences`), do NOT pass the Target Group's id as `mdAudienceId`. Stop, tell the user \"X is a Target Group, not a Custom Audience\", and offer to rebuild X as a Custom Audience (e.g. via `create_audiences` with the same criteria) before retrying.\n\nINCLUDE AUDIENCES: `include[].audiences` accepts ANY Metadata audience type — there is NO allow-list and NO restriction to specific types. Pass any audience returned by the audience listing tools (e.g. FIRMOGRAPHIC_INCLUDE, FIRMOGRAPHIC_INCLUDE_SFDC, NATIVE_LINKEDIN, NATIVE_FACEBOOK, RETARGETING, TECHNOGRAPHIC, SFDC_ACCOUNT_DYNAMIC, MA_CONTACTS_DYNAMIC, CONTACT_LIST, and any other type, including types not enumerated in any tool schema). Type-name enums seen in `create_audiences` / `search_audiences` describe how audiences are CLASSIFIED, not which can be attached here. Caveat: estimates may under-report (or return zero) when combining server-resolved audiences (e.g. SFDC dynamic, retargeting) with native LinkedIn criteria — that is an estimate-side reconciliation issue, not a creation constraint, so a zero estimate does not necessarily mean the platform will reject the create call.\n\nEXCLUDE AUDIENCES: To exclude an existing Metadata Custom Audience, provide its full object in `exclude.audiences`. The tool pre-flight-rejects placeholder values — never invent ids or types when you don't know them, call `get_matched_audiences` (or `search_audiences`) first and use the real values from the response:\n- `mdAudienceId` (integer, REQUIRED): The numeric Custom Audience ID from `get_matched_audiences` (e.g. 49067, 45316). NEVER the audience name string, NEVER a placeholder like 1/2/3/4 (real Custom Audience IDs are 5-digit numbers; the tool rejects anything under 100), and NEVER a Target Group id (those come from `list_target_groups`; see the AUDIENCES ≠ TARGET GROUPS note above).\n- `name` (string, required): Audience name.\n- `type` (string, required): Audience type, e.g. `FIRMOGRAPHIC_EXCLUDE`, `RETARGETING_EXCLUDE`, `CONTACT_LIST_EXCLUDE` (must be the EXCLUDE variant). NEVER the placeholder string \"AUDIENCE\" — the tool rejects that pre-flight.\n- `matchCount` (int), `matchCountType` (\"FIXED_SIZE\"), `inactive` (bool): metadata fields from the audience record. The tool pre-flight-rejects entries with `inactive: true` (an audience still matching after creation cannot be attached yet — attach it once it is active).\n- `criteria`: The audience's own criteria array (copy from the audience's definition).\n\nCRITERIA TYPES (all IDs from search_target_group_criteria), valid in BOTH include and exclude:\n- NATIVE_LOCATION / LOCATION: location data — MANDATORY FIRST in include. Multi-country geo goes in ONE location criterion with one {externalId, name} entry per country (e.g. \"Canada and USA\" = 2 entries in the same data array). Never silently drop a country the user named.\n- AGE, COMPANY_CATEGORY, COMPANY_GROWTH_RATE, INDUSTRY, COMPANY_NAME, REVENUE, EMPLOYEE: Array of {externalId, name}\n- JOB_FUNCTION, JOB_TITLE, MEMBER_GROUP, MEMBER_INTEREST, MEMBER_SKILL, MEMBER_BEHAVIOR, SENIORITY, FOLLOWED_COMPANIES: Array of {externalId, name}\n- YEARS_OF_EXPERIENCE_RANGE: {\"min\": 4, \"max\": 5}\n\nRESPONSE: {matchCount, isMatchCountTooSmall, matchCountType, allowZeroMatchCount}\n\nWORKFLOW: search_target_group_criteria → build targeting (include + optional exclude) → estimate_target_group → adjust → create_target_group",
   "inputSchema": {
    "type": "object",
    "properties": {
     "channel": {
      "type": "string",
      "enum": [
       "LINKEDIN",
       "REDDIT"
      ],
      "default": "LINKEDIN",
      "description": "Channel to estimate for. LINKEDIN (default) takes criteria ids from search_target_group_criteria; REDDIT takes them from search_reddit_criteria and is estimated the same way (PRD-31759). Omit for LinkedIn."
     },
     "targeting": {
      "type": "object",
      "description": "Complete targeting configuration with include/exclude rules. Structure:\n                                                    {\n                                                    \"include\": [\n                                                        {\n                                                        \"criteria\": [{\"type\": str, \"data\": object}],\n                                                        \"audiences\": []\n                                                        }\n                                                    ],\n                                                    \"exclude\": {\"criteria\": [], \"audiences\": []}\n                                                    }\n                                            All criteria IDs/references must come from search_target_group_criteria tool.",
      "properties": {
       "include": {
        "type": "array",
        "description": "Array of include rules with criteria and audiences",
        "items": {
         "type": "object",
         "properties": {
          "criteria": {
           "type": "array",
           "items": {
            "type": "object",
            "properties": {
             "type": {
              "type": "string"
             },
             "data": {}
            }
           }
          },
          "audiences": {
           "type": "array",
           "items": {
            "type": "object"
           }
          },
          "isModifiable": {
           "type": "boolean"
          }
         }
        }
       },
       "exclude": {
        "type": "object",
        "description": "Exclude rules with criteria and audiences",
        "properties": {
         "criteria": {
          "type": "array",
          "items": {
           "type": "object"
          }
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "object"
          }
         }
        }
       }
      }
     }
    },
    "required": [
     "channel"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "find_lookalike_companies",
   "title": "Find Lookalike Companies",
   "category": "audiences",
   "risk": "read",
   "summary": "Find companies similar to one or more seed domains (for audience expansion).",
   "description": "Find companies similar to one or more seed domains (for audience expansion).\n\nCompany-level only. Use to expand a target list from known-good accounts.\n\nRETURNS: {companies: [{domain, name, employee_range, industry, city, state}, ...]}.\nReturns {companies: []} when none are found or targeting is not configured.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "domains": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "minItems": 1,
      "description": "Seed company domains, e.g. ['metadata.io']."
     },
     "limit": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100,
      "default": 10,
      "description": "Max lookalikes to return (default 10)."
     }
    },
    "required": [
     "domains"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "get_abm_account_lists",
   "title": "Get ABM Account Lists",
   "category": "audiences",
   "risk": "read",
   "summary": "Get account lists from the Metadata ABM Lists library.",
   "description": "Get account lists from the Metadata ABM Lists library.\n                Retrieves uploaded CSV account lists, website retargeting lists, and their metadata.\n\n                WHEN TO USE:\n                - List all available account lists with pagination and filtering\n                - Search for specific account lists by filename\n                - Review account list details including status, type, and account counts\n                - Find lists by type (CSV, WEBSITE_RETARGETING, etc.)\n                - Check list readiness and report status\n\n                PARAMETERS:\n                - filename: Filter by filename (partial matching)\n                - page: Page number (default: 0)\n                - size: Results per page (default: 100, max: 100)\n                - sort: Sort criteria (default: \"createdDate,desc\")\n                - status: Filter by status (READY, PROCESSING, etc.)\n                - type: Filter by type (CSV, WEBSITE_RETARGETING, etc.)\n\n                RESPONSE INCLUDES:\n                - List metadata (id, name, filename, status, type)\n                - Account counts and audience associations\n                - Creation dates and file hashes\n                - Native targeting status\n                - Account report status\n\n                EXAMPLES:\n                - get_abm_account_lists() - Get all account lists\n                - get_abm_account_lists(filename=\"test_acl.csv\") - Find specific list\n                - get_abm_account_lists(type=\"CSV\") - Get only CSV lists\n                - get_abm_account_lists(status=\"READY\") - Get ready lists",
   "inputSchema": {
    "type": "object",
    "properties": {
     "filename": {
      "type": "string",
      "description": "Filter by account list filename (supports partial matching)"
     },
     "page": {
      "type": "integer",
      "minimum": 0,
      "description": "Page number for pagination (0-based)",
      "default": 0
     },
     "size": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100,
      "description": "Number of results per page",
      "default": 100
     },
     "sort": {
      "type": "string",
      "description": "Sort criteria (e.g., 'createdDate,desc', 'name,asc', 'accountsCount,desc')",
      "default": "createdDate,desc"
     },
     "status": {
      "type": "string",
      "description": "Filter by status (e.g., READY, PROCESSING, ERROR)"
     },
     "type": {
      "type": "string",
      "description": "Filter by list type (e.g., CSV, WEBSITE_RETARGETING)"
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "get_audience_details",
   "title": "Get Audience Details",
   "category": "audiences",
   "risk": "read",
   "summary": "Custom audience information from Metadata platform: configurations, targeting criteria, size metrics, performance.",
   "description": "Custom audience information from Metadata platform: configurations, targeting criteria, size metrics, performance.\n\nUSE FOR: List audiences with pagination/filtering, get specific audience by ID/name, analyze configurations and targeting, review size and performance.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "page": {
      "type": "integer",
      "minimum": 0,
      "description": "Page number for pagination (0-based)",
      "default": 0
     },
     "size": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100,
      "description": "Number of results per page",
      "default": 25
     },
     "sort": {
      "type": "string",
      "description": "Sort criteria (e.g., 'creationDate,desc', 'name,asc', 'audienceSize,desc')",
      "default": "creationDate,desc"
     },
     "name": {
      "type": "string",
      "description": "Filter by audience name (supports partial matching)"
     }
    },
    "required": [
     "name"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "get_deep_audience_details",
   "title": "Get Deep Audience Details",
   "category": "audiences",
   "risk": "read",
   "summary": "Get detailed information about audiences including criteria and advanced configuration.",
   "description": "Get detailed information about audiences including criteria and advanced configuration.\n\n                This tool returns comprehensive audience data beyond the basic summary, including:\n                - Complete audience criteria and targeting configuration\n                - Segment information and settings\n                - Lookback window configuration\n                - Advanced configuration details and metadata\n                - Full audience structure and composition\n\n                **WHEN TO USE:**\n                - Retrieve complete audience criteria and configuration\n                - Analyze detailed targeting setup for an audience\n                - Export or review full audience specifications\n                - Understand advanced audience configuration\n                - Compare detailed configuration across multiple audiences\n\n                PARAMETERS:\n                - audience_ids: Array of audience IDs to retrieve details for (required)\n                  Example: [42828, 42829, 42831]\n\n                AUDIENCE ID SOURCES:\n                Use the get_audience_details tool first to:\n                - List all available audiences in your account\n                - Find the IDs of audiences you want detailed information for\n                - Get the basic summary before requesting deep details\n\n                EXAMPLE WORKFLOW:\n                1. Call get_audience_details() to see all audiences and their IDs\n                2. Identify which audiences you need deep details for\n                3. Call get_deep_audience_details(audience_ids=[42828, 42831]) to get full configuration\n\n                RETURNS:\n                Detailed audience information including:\n                - Audience ID, name, and type\n                - Complete targeting criteria\n                - Segment configuration (if applicable)\n                - Lookback window settings\n                - Advanced metadata and configuration\n                - Full audience composition details\n\n                IMPORTANT NOTES:\n                - All provided audience IDs must exist and be valid\n                - IDs must be positive integers\n                - You can request details for multiple audiences in a single call\n                - Results include complete audience configuration data",
   "inputSchema": {
    "type": "object",
    "properties": {
     "audience_ids": {
      "type": "array",
      "description": "Array of audience IDs to retrieve detailed information for. Example: [42828, 42829, 42831]",
      "items": {
       "type": "integer",
       "minimum": 1
      },
      "minItems": 1
     }
    },
    "required": [
     "audience_ids"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "get_intent_topics",
   "title": "Get Intent Topics",
   "category": "audiences",
   "risk": "read",
   "summary": "Get available intent topics for BOMBOORA audience creation.",
   "description": "Get available intent topics for BOMBOORA audience creation.\n\n                        IMPORTANT:\n                        Call this tool first before suggesting or creating any Bombora audience.\n                        If the response returns NO topics (empty list), Bombora is NOT available for this account —\n                        do NOT suggest Bombora audiences. Instead, recommend creating a firmographic audience\n                        (create_firmographic_audience) as the preferred alternative.\n\n                        Retrieves the list of available buyer intent topics that can be used\n                        when creating BOMBOORA intent topic audiences. These intent topics represent different\n                        buyer interest areas and purchasing signals that can be used for precise audience targeting.\n\n                        WHEN TO USE:\n                        - Always call this BEFORE suggesting or creating a Bombora audience to verify topic availability\n                        - Discover available intent topics before creating a BOMBOORA intent topic audience\n                        - Understand what buyer intent signals are available for targeting\n                        - Build audiences based on specific buyer interests and purchasing signals\n                        - Integrate intent topic selection into the audience creation flow\n\n                        RETURNS:\n                        List of available intent topics with their details including:\n                        - Topic ID/name\n                        - Topic description\n                        - Available packages or categories\n                        - Any additional metadata for targeting configuration\n\n                        NEXT STEPS:\n                        After retrieving intent topics, use the intent topic IDs with the audience\n                        creation tool to create BOMBOORA intent topic based audiences.",
   "inputSchema": {
    "type": "object",
    "properties": {},
    "additionalProperties": false
   }
  },
  {
   "name": "get_matched_audiences",
   "title": "Get Matched Audiences",
   "category": "audiences",
   "risk": "read",
   "summary": "Get matched audiences (custom audiences) from the Campaign Wizard based on audience type and targeting options.",
   "description": "Get matched audiences (custom audiences) from the Campaign Wizard based on audience type and targeting options.\n\n                        Retrieves custom audiences that match specific targeting configurations.\n                        Use this to find available audiences for campaign targeting on a given advertising channel.\n                        This is the canonical tool to call when you need an audience id to attach to a campaign or a target group's include[].audiences / exclude.audiences arrays. The returned id/name/type/matchCount/inactive fields are exactly the shape those payloads expect. It draws on the same source the main UI's target-group audience-chooser uses, so what this tool returns is what the platform considers attachable.\n\n                        AUDIENCE TARGETING TYPES AVAILABLE:\n                        • FIRMOGRAPHIC_INCLUDE: Firmographic inclusion filters (company size, industry, location)\n                        • FIRMOGRAPHIC_EXCLUDE: Firmographic exclusion filters\n                        • FIRMOGRAPHIC_EXCLUDE_BASIC: Basic firmographic exclusions\n                        • FIRMOGRAPHIC_INCLUDE_SFDC: Salesforce firmographic inclusions\n                        • NATIVE_TARGETING_CSV: CSV-based native targeting\n                        • NATIVE_TARGETING_DYNAMIC: Dynamic native targeting lists\n                        • NATIVE_TARGETING_STATIC: Static native targeting lists\n                        • CONTACT_LIST: Custom contact list targeting\n                        • STORELEADS: Store leads targeting\n                        • DYNAMIC_G2: Dynamic G2 crowd data\n                        • DYNAMIC_G2_CONTACT: Dynamic G2 contact data\n                        • G2_ABM_CONTACT: G2 ABM contact targeting\n                        • LEADSIFT: LeadSift intent data targeting\n                        • BUYER_INTENT: Buyer intent data targeting\n                        • MA_ACCOUNTS_STATIC: Marketing automation static accounts\n                        • MA_ACCOUNTS_DYNAMIC: Marketing automation dynamic accounts\n                        • MA_CONTACTS_STATIC: Marketing automation static contacts\n                        • MA_CONTACTS_DYNAMIC: Marketing automation dynamic contacts\n                        • NATIVE_LINKEDIN: Native LinkedIn targeting options\n                        • LINKEDIN_RETARGETING: LinkedIn retargeting audiences\n                        • SFDC_CONTACTS_DYNAMIC: Salesforce dynamic contacts\n                        • SFDC_CONTACTS_STATIC: Salesforce static contacts\n                        • SFDC_ACCOUNT_DYNAMIC: Salesforce dynamic accounts\n                        • WEBSITE_RETARGETING: Website visitor retargeting\n                        • WEBSITE_ACCOUNT_RETARGETING: Website account-based retargeting\n                        • TECHNOLOGY: Technology stack targeting\n                        • SLINTEL_TECHNOLOGY: Slintel technology targeting\n\n                        COMMON USE CASES:\n                        - Find all LinkedIn audiences using firmographic targeting\n                        - Get audiences with website retargeting enabled\n                        - List audiences using G2 intent data\n                        - Search for audiences with Salesforce integration\n                        - Identify audiences using specific MA platforms\n                        - Find dynamic vs static audience configurations\n                        - Discover available contact lists and store leads\n\n                        PARAMETERS:\n                        - type: Array of targeting types to match (defaults to all types if not specified)\n                        - channel: Advertising channel (LINKEDIN, FACEBOOK, GOOGLE_ADS, INSTAGRAM, TWITTER, YOUTUBE, REDDIT, MICROSOFT_ADS)\n                        - page: Page number for pagination (0-indexed)\n                        - sort: Sort field and direction (e.g., 'createdDate,desc', 'name,asc')\n                        - size: Number of results per page (1-200)\n\n                        RETURNS:\n                        Paginated list of custom audiences matching the specified targeting criteria.\n                        Each audience includes:\n                        - id: Audience ID\n                        - name: Audience name\n                        - matchCount: Number of matches in the audience\n                        - type: Targeting type (e.g., FIRMOGRAPHIC_INCLUDE, MA_CONTACTS_DYNAMIC)\n                        - channel: Channel the audience is for\n                        - createdDate: When the audience was created\n                        - inactive: Whether the audience is active or inactive\n                        - tags: Associated tags",
   "inputSchema": {
    "type": "object",
    "properties": {
     "type": {
      "type": "array",
      "description": "Array of audience targeting types to filter by. If omitted, searches all targeting types.",
      "items": {
       "type": "string",
       "enum": [
        "FIRMOGRAPHIC_INCLUDE",
        "NATIVE_TARGETING_CSV",
        "CONTACT_LIST",
        "STORELEADS",
        "FIRMOGRAPHIC_EXCLUDE",
        "DYNAMIC_G2",
        "DYNAMIC_G2_CONTACT",
        "G2_ABM_CONTACT",
        "LEADSIFT",
        "BUYER_INTENT",
        "MA_ACCOUNTS_STATIC",
        "MA_ACCOUNTS_DYNAMIC",
        "MA_CONTACTS_STATIC",
        "MA_CONTACTS_DYNAMIC",
        "NATIVE_LINKEDIN",
        "LINKEDIN_RETARGETING",
        "SFDC_CONTACTS_DYNAMIC",
        "SFDC_CONTACTS_STATIC",
        "FIRMOGRAPHIC_INCLUDE_SFDC",
        "FIRMOGRAPHIC_EXCLUDE_BASIC",
        "SFDC_ACCOUNT_DYNAMIC",
        "NATIVE_TARGETING_DYNAMIC",
        "NATIVE_TARGETING_STATIC",
        "WEBSITE_RETARGETING",
        "WEBSITE_ACCOUNT_RETARGETING",
        "TECHNOLOGY",
        "SLINTEL_TECHNOLOGY"
       ]
      }
     },
     "channel": {
      "type": "string",
      "description": "The advertising channel to filter campaigns by",
      "enum": [
       "LINKEDIN",
       "FACEBOOK",
       "GOOGLE_ADS",
       "INSTAGRAM",
       "TWITTER",
       "YOUTUBE",
       "REDDIT",
       "MICROSOFT_ADS"
      ],
      "default": "LINKEDIN"
     },
     "page": {
      "type": "integer",
      "description": "Page number for pagination (0-indexed)",
      "minimum": 0,
      "default": 0
     },
     "sort": {
      "type": "string",
      "description": "Sort field and direction in format 'field,direction'. Common fields: createdDate. Direction: asc or desc.",
      "default": "createdDate,desc"
     },
     "size": {
      "type": "integer",
      "description": "Number of results per page",
      "minimum": 1,
      "maximum": 200,
      "default": 50
     }
    },
    "required": [
     "type"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "get_retargeting_audiences",
   "title": "Get Retargeting Audiences",
   "category": "audiences",
   "risk": "read",
   "summary": "Get all available retargeting audiences for a specific advertising channel.",
   "description": "Get all available retargeting audiences for a specific advertising channel.\n\n                This tool retrieves all retargeting audiences and native audiences available for the specified\n                advertising channel (GOOGLE_ADS, LINKEDIN, or FACEBOOK) that can be used to create new retargeting\n                audiences. This is useful when you need to discover existing audiences before creating or targeting them.\n\n                SUPPORTED CHANNELS:\n                - GOOGLE_ADS: Google Ads retargeting audiences\n                - LINKEDIN: LinkedIn retargeting audiences\n                - FACEBOOK: Facebook retargeting audiences\n\n                WHEN TO USE:\n                - Discover all available retargeting audiences for a specific channel\n                - View native audiences that can be used for targeting on a channel\n                - Find audience IDs and details for audience creation or targeting\n                - Review audience names, sizes (matchCount), and other metadata\n                - Understand what audiences are available before building campaigns\n                - Compare audiences across different advertising channels\n\n                PARAMETERS:\n                - channel: The advertising channel to fetch retargeting audiences for (required)\n                  Options: GOOGLE_ADS, LINKEDIN, FACEBOOK\n                  Note: Channel names are case-insensitive and will be converted to uppercase\n\n                RETURNS:\n                List of available retargeting and native audiences with details including:\n                - id: Unique audience ID\n                - name: Audience name\n                - type: Audience type (e.g., GoogleAdsAudience, LinkedInAudience, FacebookAudience)\n                - subtype: Audience subtype (e.g., GOOGLE_ADS_RETARGETING, LINKEDIN_RETARGETING)\n                - channel: Channel type identifier\n                - matchCount: Number of matches/members in the audience\n                - externalAccountId: Associated account ID on the advertising platform\n                - seedSourcePlatform: Source platform for the audience seed\n\n                AUDIENCE DETAILS:\n                Each audience record provides comprehensive information about:\n                - Audience identification and naming\n                - Type and subtype classification\n                - Match count to understand audience size\n                - External platform integration details\n                - Account associations\n\n                COMMON USE CASES:\n                - Get all LinkedIn retargeting audiences (including Marketing Automation, Bulk, Retargeting, and Business Objective Based Audience)\n                - Find Google Ads audiences suitable for retargeting campaigns\n                - Identify Facebook retargeting audiences",
   "inputSchema": {
    "type": "object",
    "properties": {
     "channel": {
      "type": "string",
      "description": "The advertising channel to fetch retargeting audiences for",
      "enum": [
       "GOOGLE_ADS",
       "LINKEDIN",
       "FACEBOOK"
      ]
     }
    },
    "required": [
     "channel"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "get_segment_criteria",
   "title": "Get Segment Criteria",
   "category": "audiences",
   "risk": "read",
   "summary": "Get full criteria object for a segment by ID or name.",
   "description": "Get full criteria object for a segment by ID or name.\n\nRETURNS: {id, name, status, lastUpdatedDate, criteria}\n\nSTATUS VALUES:\n- READY: Can use with get_insights_report\n- PENDING: Processing (check later)\n- FAILED: Error occurred\n\nIMPORTANT: Pass the returned 'criteria' object to get_insights_report, NOT the segment ID.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "segment_id": {
      "type": "integer",
      "description": "Segment ID (fastest lookup)."
     },
     "segment_name": {
      "type": "string",
      "description": "Segment name (fuzzy matching, case-insensitive)."
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "list_marketing_automation_lists",
   "title": "List Marketing-Automation Lists",
   "category": "audiences",
   "risk": "read",
   "summary": "List a Marketing-Automation provider's contact library lists — the selectable source for creating a Marketing-Automation-contacts audience.",
   "description": "List a Marketing-Automation provider's contact library lists — the selectable source for creating a Marketing-Automation-contacts audience.\n\n                PREREQUISITE:\n                  • The provider MUST be connected. Only HUBSPOT and MARKETO expose library lists.\n\n                WHEN TO USE:\n                  • Before create_marketing_automation_contacts_audience, to find the library_list_id.\n                  • \"list my HubSpot lists\", \"which Marketo lists can I use for an audience\".\n\n                PARAMETERS:\n                  • provider (required): HUBSPOT or MARKETO.\n                  • list_type (optional): SMART_LIST | STATIC_LIST | ACTIVE_LIST. If omitted, the provider's own list types are queried and merged (HubSpot: STATIC_LIST + ACTIVE_LIST; Marketo: all three).\n                  • name (optional): filter by list name (partial).\n\n                RETURNS: { lists: [ { library_list_id, name, status, listType, objectsCount } ] }. Use `library_list_id` for create. For HubSpot a list is usable only when status is DONE; for Marketo when READY.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "provider": {
      "type": "string",
      "enum": [
       "HUBSPOT",
       "MARKETO"
      ],
      "description": "The Marketing-Automation provider to list lists for."
     },
     "list_type": {
      "type": "string",
      "enum": [
       "SMART_LIST",
       "STATIC_LIST",
       "ACTIVE_LIST"
      ],
      "description": "Optional list-type filter. If omitted, the provider's own list types are queried and merged (HubSpot: STATIC_LIST + ACTIVE_LIST; Marketo: all three)."
     },
     "name": {
      "type": "string",
      "description": "Optional list-name filter (partial match)."
     }
    },
    "required": [
     "provider"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "list_salesforce_mixed_criteria",
   "title": "List Salesforce Mixed-Criteria",
   "category": "audiences",
   "risk": "read",
   "summary": "List the account's saved Salesforce \"mixed criteria\" (contact-list criteria) — the selectable source for creating a Salesforce-contacts audience.",
   "description": "List the account's saved Salesforce \"mixed criteria\" (contact-list criteria) — the selectable source for creating a Salesforce-contacts audience.\n\n                PREREQUISITE:\n                  • Salesforce integration MUST be connected. If it is not, tell the user to connect Salesforce first (connect_crm with crm=\"salesforce\").\n\n                WHEN TO USE:\n                  • Before create_salesforce_contacts_audience, to find the criteria_id.\n                  • \"list my Salesforce contact criteria\", \"which Salesforce lists can I use for an audience\".\n\n                PARAMETERS:\n                  • qualified (optional): when true, keep only criteria whose generated filter matches qualified-signal fields.\n\n                RETURNS: a list of { id, name, description }. Use `id` as the criteria_id for create_salesforce_contacts_audience. Note: this does NOT include a contacts count.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "qualified": {
      "type": "boolean",
      "description": "When true, keep only criteria whose generated filter matches qualified-signal fields."
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "list_segments",
   "title": "List Segments",
   "category": "audiences",
   "risk": "read",
   "summary": "List pre-saved segments (reusable filter configurations).",
   "description": "List pre-saved segments (reusable filter configurations).\n\nRETURNS: Array of {id, name} objects.\n\nNOTE: To use a segment's criteria with get_insights_report, call get_segment_criteria(segment_id) first.\n\nWORKFLOW:\n1. list_segments() or list_segments(name=\"keyword\")\n2. get_segment_criteria(segment_id)\n3. get_insights_report(criteria=segment_criteria)",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "Optional fuzzy search by name."
     },
     "sort": {
      "type": "string",
      "default": "name,asc",
      "description": "Sort format: field,direction. Default: name,asc"
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "list_target_groups",
   "title": "List Target Groups",
   "category": "audiences",
   "risk": "read",
   "summary": "List and search target groups in Metadata platform.",
   "description": "List and search target groups in Metadata platform.\n\nUSE FOR: \"list all target groups\", \"show available target groups\", \"search target groups with name X\", \"find target groups containing 'engineer'\", \"get target group IDs by name\", \"what target groups do I have?\"\n\nWORKFLOW: list_target_groups(name=\"X\") → get ID → retrieve_target_group_by_id(target_group_id=ID) for full details.\n\nRESPONSE: Paginated {content: [{id, name, channel, audienceSize, createdDate, modifiedDate, status}], totalElements, totalPages}\n\nSEARCH: Partial matching, case-insensitive. name=\"engineer\" matches \"Engineers EMEA\", \"Senior Engineers\", etc.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "Optional name filter for searching target groups (partial matching, case-insensitive). Example: 'engineer' matches 'Engineers EMEA', 'Senior Engineers', etc."
     },
     "sort": {
      "type": "string",
      "description": "Sort criteria. Options: 'createdDate,desc' (newest first), 'createdDate,asc' (oldest first), 'name,asc' (A-Z), 'name,desc' (Z-A). Default: 'createdDate,desc'",
      "default": "createdDate,desc"
     }
    },
    "additionalProperties": false
   }
  },
  {
   "name": "remove_audiences_from_campaign",
   "title": "Remove Audiences from Campaign",
   "category": "audiences",
   "risk": "destructive",
   "summary": "Remove specific audiences from one or more channels in an existing campaign.",
   "description": "Remove specific audiences from one or more channels in an existing campaign.\n\n                Use this tool when you need to detach a named audience/group from a particular\n                channel (e.g. remove \"European Digital Nomad Investors\" from FACEBOOK).\n                Channel and audience IDs are resolved internally — you only need the human-readable\n                channel type and audience name.\n\n                Works on both campaign structures. On a Channel-First / Native (N×N×N) campaign the\n                audience is cleared from its WizNativeAdContainer(s); any container then left with no\n                asset at all (no audience, target group, keyword, ad or offer) is dropped.\n\n                KEYWORDS: remove, delete, audience, group, channel, campaign, targeting\n\n                WHEN TO USE:\n                - User wants to remove a specific audience from a channel\n                - User wants to clean up targeting groups from one or more channels\n                - Multiple audiences across multiple channels can be removed in a single call\n\n                RESPONSE:\n                - Returns the refreshed campaign state plus a removal_results list with one\n                  entry per requested removal recording its success or error. ALWAYS check\n                  removal_results — a partial failure (e.g. one of two names not found) still\n                  returns normally, so report only what actually came back successful.\n\n                REQUIRED PARAMETERS:\n                - campaign_id: Campaign ID (numeric identifier)\n                - channel_audiences: Object mapping channel type → list of audience names to remove\n\n                CHANNEL TYPES: FACEBOOK, INSTAGRAM, GOOGLE_ADS, LINKEDIN, MICROSOFT_ADS, REDDIT, TWITTER\n\n                EXAMPLES:\n\n                Remove one audience from FACEBOOK:\n                remove_audiences_from_campaign({\n                    \"campaign_id\": 42545,\n                    \"channel_audiences\": {\n                        \"FACEBOOK\": [\"European Digital Nomad Investors_p2w8\"]\n                    }\n                })\n\n                Remove audiences from multiple channels at once:\n                remove_audiences_from_campaign({\n                    \"campaign_id\": 42545,\n                    \"channel_audiences\": {\n                        \"FACEBOOK\": [\"Audience A\", \"Audience B\"],\n                        \"LINKEDIN\": [\"Audience C\"]\n                    }\n                })\n\n                INTEGRATION WITH OTHER TOOLS:\n                - Use get_campaign_by_wizard_id to inspect current audiences per channel before removing\n                - Use search_campaigns_by_names to find the campaign ID",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The unique identifier of the campaign",
      "minimum": 1
     },
     "channel_audiences": {
      "type": "object",
      "description": "Map of channel type to list of audience names to remove. Example: {\"FACEBOOK\": [\"Audience A\"], \"LINKEDIN\": [\"Audience B\"]}",
      "additionalProperties": {
       "type": "array",
       "items": {
        "type": "string"
       },
       "minItems": 1
      }
     }
    },
    "required": [
     "campaign_id",
     "channel_audiences"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "remove_target_groups_from_campaign",
   "title": "Remove Targeting Groups from Campaign",
   "category": "audiences",
   "risk": "destructive",
   "summary": "Remove targeting groups from one or more channels in an existing campaign.",
   "description": "Remove targeting groups from one or more channels in an existing campaign.\n\n                Use this tool to detach a named Targeting Group from a channel (e.g. remove\n                \"Tech Decision Makers\" from LINKEDIN). Channel + targeting-group IDs are resolved\n                internally — you only need the channel type and the targeting group name. (Targeting\n                groups are NOT audiences — to remove a standard audience use\n                remove_audiences_from_campaign.)\n\n                Works on both campaign structures. On a Channel-First / Native (N×N×N) campaign the\n                targeting group is cleared from its WizNativeAdContainer.targetingGroupId; any\n                container then left with no asset at all is dropped. On Precision/METADATA it uses the\n                legacy group delete.\n\n                KEYWORDS: remove, delete, targeting group, target group, channel, campaign\n\n                RESPONSE:\n                - Returns the refreshed campaign state plus a removal_results list with one\n                  entry per requested removal recording its success or error. ALWAYS check\n                  removal_results — a partial failure (e.g. one of two names not found) still\n                  returns normally, so report only what actually came back successful.\n\n                REQUIRED PARAMETERS:\n                - campaign_id: Campaign ID (numeric identifier)\n                - channel_target_groups: Object mapping channel type → list of targeting group names\n\n                EXAMPLE:\n                remove_target_groups_from_campaign({\n                    \"campaign_id\": 42545,\n                    \"channel_target_groups\": {\"LINKEDIN\": [\"Tech Decision Makers\"]}\n                })",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The unique identifier of the campaign",
      "minimum": 1
     },
     "channel_target_groups": {
      "type": "object",
      "description": "Map of channel type to list of targeting group names to remove. Example: {\"LINKEDIN\": [\"Tech Decision Makers\"]}",
      "additionalProperties": {
       "type": "array",
       "items": {
        "type": "string"
       },
       "minItems": 1
      }
     }
    },
    "required": [
     "campaign_id",
     "channel_target_groups"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "retrieve_target_group_by_id",
   "title": "Retrieve Target Group by ID",
   "category": "audiences",
   "risk": "read",
   "summary": "Get detailed target group information by ID.",
   "description": "Get detailed target group information by ID.\n\nUSE FOR: \"get target group details for ID X\", \"show target group with ID Y\", \"check target group configuration\", \"show audiences in target group X\", analyze setup before changes.\n\nPREREQUISITE: If you don't have the ID, use list_target_groups(name=\"X\") first.\n\nRESPONSE: {id, name, channel, audiences, targeting, status, createdDate, modifiedDate, size, configuration, ...}\n\nWORKFLOW: list_target_groups(name=\"target_group_name\") → get ID → retrieve_target_group_by_id(target_group_id=ID)",
   "inputSchema": {
    "type": "object",
    "properties": {
     "target_group_id": {
      "type": "integer",
      "description": "The unique ID of the target group to retrieve. Must be a positive integer. If you don't have the ID, use list_target_groups to search by name first.",
      "minimum": 1
     }
    },
    "required": [
     "target_group_id"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "search_reddit_criteria",
   "title": "Search Reddit Target Group Criteria",
   "category": "audiences",
   "risk": "read",
   "summary": "Get available Reddit targeting criteria and their reference values (externalId + name) for target group creation.",
   "description": "Get available Reddit targeting criteria and their reference values (externalId + name) for target group creation.\n\nUSE FOR: \"what Reddit communities match X?\", \"search Reddit interests/keywords\", \"find subreddits about Y\", get valid references before create_reddit_target_group, resolve location countries/states for Reddit.\n\nCRITERIA TYPES:\n- communities: Search subreddits/communities by name (requires search_terms)\n- interests: Search Reddit interest categories (requires search_terms)\n- keywords: Search contextual keywords (requires search_terms)\n- locations: Country/state options for the mandatory LOCATION criterion (search_terms optional; omit to list all)\n\nRESPONSE: For communities/interests/keywords, a paginated {content: [{externalId, name}]} — use externalId in the COMMUNITIES/INTERESTS/KEYWORDS criteria. For locations, {countries, states, regions} where each item has a numeric `id` — put that numeric `id` (NOT the object) into the LOCATION criterion's countries/states arrays.\n\nWORKFLOW: search_reddit_criteria → build targeting (LOCATION first) → create_reddit_target_group",
   "inputSchema": {
    "type": "object",
    "properties": {
     "criteria_type": {
      "type": "string",
      "enum": [
       "communities",
       "interests",
       "keywords",
       "locations"
      ],
      "description": "Type of Reddit criteria to search. communities/interests/keywords require search_terms; locations returns countries/states (search_terms optional)."
     },
     "search_terms": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Search keywords (min 2 characters). Required for communities/interests/keywords. Example: ['technology']"
     }
    },
    "required": [
     "criteria_type"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "search_target_group_criteria",
   "title": "Search Target Group Criteria",
   "category": "audiences",
   "risk": "read",
   "summary": "Get available criteria and their supported values (names and IDs) for target group creation/updates.",
   "description": "Get available criteria and their supported values (names and IDs) for target group creation/updates.\n\nUSE FOR: \"what targeting criteria are available?\", \"what options for [criteria type]?\", \"supported values for industries/seniority/job functions\", \"how to search job titles/interests/member groups?\", validate criteria before creating target group, get valid IDs for create_target_group.\n\nCRITERIA TYPES:\n\n1. LIST-BASED (returns predefined options):\n   - age-ranges: Age range options\n   - company-categories: Company classifications\n   - company-growth-rates: Growth rate ranges\n   - revenues: Revenue ranges\n   - employees: Employee count ranges\n   - industry-taxonomy: Industry codes/names\n   - jobFunctions: Job function categories\n   - seniority: Seniority levels\n   - followed-companies: Company follow options\n   - locations: Geographic data (MANDATORY as FIRST criteria for LinkedIn) - use search_terms for filtering\n\n2. SEARCH-BASED (use search_terms):\n   - job-title: Search job titles (reference_type: LINKEDIN_JOB_TITLES)\n   - member-groups: Search LinkedIn groups (reference_type: LINKEDIN_MEMBER_GROUPS)\n   - member-skills: Search professional skills\n   - interests: Search interests (reference_type: LINKEDIN_INTERESTS)\n   - traits: Search behaviors (reference_type: LINKEDIN_TRAITS)\n\n3. NUMERIC: years-of-experience (0-12, not retrieved via this tool)\n\nOPERATION MODES:\n- List: search_target_group_criteria(channel=\"LINKEDIN\", criteria_type=\"seniority\")\n- Search: search_target_group_criteria(channel=\"LINKEDIN\", criteria_type=\"job-title\", search_terms=[\"engineer\"], exact_match=false)\n- Direct: search_target_group_criteria(channel=\"LINKEDIN\", reference_type=\"LINKEDIN_JOB_TITLES\", search_terms=[\"engineer\"])\n\nRESPONSE: Array of {externalId, name}. Use externalId in target group config, show name to users.\n\nCHANNEL: Only LINKEDIN supported.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "channel": {
      "type": "string",
      "enum": [
       "LINKEDIN"
      ],
      "description": "The advertising channel for which to retrieve criteria. Only LINKEDIN is supported."
     },
     "criteria_type": {
      "type": "string",
      "enum": [
       "age-ranges",
       "company-categories",
       "company-growth-rates",
       "revenues",
       "employees",
       "industry-taxonomy",
       "jobFunctions",
       "seniority",
       "followed-companies",
       "locations",
       "job-title",
       "member-groups",
       "member-skills",
       "interests",
       "traits"
      ],
      "description": "Type of targeting criteria to retrieve or search.\n\n                            List-based (returns predefined options):\n                            - age-ranges: Age range segments\n                            - company-categories: Company type classifications\n                            - company-growth-rates: Company growth classifications\n                            - revenues: Company revenue ranges\n                            - employees: Employee count ranges\n                            - industry-taxonomy: Industry classification\n                            - jobFunctions: Job function categories\n                            - seniority: Seniority levels\n                            - followed-companies: Company follow options\n                            - locations: Geographic locations (countries, states, cities) - MANDATORY as first criteria for LinkedIn\n\n                            Search-based (requires search_terms):\n                            - job-title: Search job titles\n                            - member-groups: Search LinkedIn groups\n                            - member-skills: Search LinkedIn member skills\n                            - interests: Search interests/topics\n                            - traits: Search behaviors/characteristics"
     },
     "search_terms": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Array of search keywords for search-based criteria (job-title, member-groups, interests, traits). Example: ['engineer', 'developer']"
     },
     "reference_type": {
      "type": "string",
      "enum": [
       "LINKEDIN_JOB_TITLES",
       "LINKEDIN_MEMBER_GROUPS",
       "BUYER_INTENT_INDUSTRIES"
      ],
      "description": "Specific reference type for search operations (LinkedIn only). Auto-determined from criteria_type and channel if not provided."
     },
     "exact_match": {
      "type": "boolean",
      "description": "Whether to require exact matches in search (default: true). Set to false for broader/fuzzy matching.",
      "default": true
     }
    },
    "required": [
     "channel"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "unarchive_audience",
   "title": "Unarchive Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Unarchive (restore) a previously archived custom audience on the Metadata platform.",
   "description": "Unarchive (restore) a previously archived custom audience on the Metadata platform.\n                        Use this tool when the user wants to unarchive, restore, or reactivate a custom audience.\n\n                        You can identify the audience either by its numeric custom_audience_id or by its name.\n                        If only the audience name is provided, the tool will search existing audiences to resolve the correct ID before unarchiving.\n\n                        PARAMETERS:\n                        • custom_audience_id (optional): The numeric ID of the custom audience to unarchive.\n                        • audience_name (optional): The name of the custom audience to unarchive. Used to look up the ID if custom_audience_id is not provided.\n\n                        At least one of custom_audience_id or audience_name must be provided.\n\n                        RESPONSE:\n                        • actionStatus: \"SUCCEEDED\" if the audience was unarchived successfully.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "custom_audience_id": {
      "type": "integer",
      "description": "The numeric ID of the custom audience to unarchive."
     },
     "audience_name": {
      "type": "string",
      "description": "The name of the custom audience to unarchive. If provided without custom_audience_id, the tool will search for the audience by name to resolve the ID."
     }
    },
    "additionalProperties": false
   }
  },
  {
   "name": "update_target_group",
   "title": "Update Target Group",
   "category": "audiences",
   "risk": "destructive",
   "summary": "Update existing target group with new name and/or targeting criteria.",
   "description": "Update existing target group with new name and/or targeting criteria. Supports BOTH include AND exclude rules — including adding/removing exclude audiences.\n\n**CRITICAL - PUT REQUEST**: You MUST provide ALL fields, not just changed ones.\n- 'name' is ALWAYS REQUIRED even if not changing\n- If updating targeting, provide complete targeting object (full include + exclude)\n- Always retrieve current config first via retrieve_target_group_by_id and merge changes onto it\n\nPREREQUISITE WORKFLOW:\n1. list_target_groups(name=\"X\") → get ID\n2. retrieve_target_group_by_id(target_group_id=ID) → get current config, check isModifiable fields, copy existing exclude audiences\n3. search_target_group_criteria → get new criteria IDs\n4. estimate_target_group → MANDATORY: verify isMatchCountTooSmall=false\n5. update_target_group with ID and ALL required fields (full targeting object)\n\nMODIFIABILITY: Name always modifiable. LOCATION usually isModifiable=false. Check retrieve_target_group_by_id response for other fields.\n\nTARGETING STRUCTURE: Same as create_target_group — full include array + single exclude object containing both `criteria` and `audiences`. See create_target_group description for the full example payload (with NATIVE_LOCATION, AGE, COMPANY_NAME exclude criteria, and FIRMOGRAPHIC_EXCLUDE audience). Only update criteria with isModifiable=true.\n\n⚠️ AUDIENCES ≠ TARGET GROUPS. Both `include[].audiences` and `exclude.audiences` accept Custom Audience records ONLY (the entities returned by `get_matched_audiences`). Target Groups (the entities returned by `list_target_groups` / `retrieve_target_group_by_id`) are a separate entity in a separate ID space and CANNOT be attached here; the platform silently drops or rejects them. If the user names \"X\" and only `list_target_groups` matches it (not `get_matched_audiences`), do NOT pass the Target Group's id as `mdAudienceId`. Stop, tell the user \"X is a Target Group, not a Custom Audience\", and offer to rebuild X as a Custom Audience (e.g. via `create_audiences` with the same criteria) before retrying.\n\nADDING/REMOVING EXCLUDE AUDIENCES: Provide the COMPLETE desired `exclude.audiences` array. Each entry needs: `mdAudienceId` (integer, REQUIRED — the numeric Custom Audience id like 49067 from `get_matched_audiences`, NEVER the audience name string, NEVER a placeholder like 1/2/3/4 (rejected pre-flight), and NEVER a Target Group id from `list_target_groups`; see the AUDIENCES ≠ TARGET GROUPS note above), `name`, `type` (EXCLUDE variant like FIRMOGRAPHIC_EXCLUDE/RETARGETING_EXCLUDE/CONTACT_LIST_EXCLUDE), `matchCount`, `matchCountType`, `inactive`, and the audience's own `criteria` array. To remove an exclusion, omit it from the array. To add one, append it.\n\nRESPONSE: {id, name, channel, audienceSize, status, modifiedDate, targeting}\n\nVALIDATION: Must estimate first and verify isMatchCountTooSmall=false before updating.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "target_group_id": {
      "type": "integer",
      "description": "The unique ID of the target group to update. Must be a positive integer. Use list_target_groups to find the ID by name.",
      "minimum": 1
     },
     "name": {
      "type": "string",
      "description": "REQUIRED: Target group name. CRITICAL - This is a PUT request so you MUST provide the name even if not changing it. Use retrieve_target_group_by_id to get the current name if you don't have it."
     },
     "targeting": {
      "type": "object",
      "description": "Updated targeting configuration (optional). Structure must match create_target_group format. Only update criteria that have isModifiable=true in the current configuration.\n                                All criteria IDs/references must come from search_target_group_criteria tool.\n                                REQUIRED: Must estimate this targeting first and verify isMatchCountTooSmall is false.\n                                NOTE: This is a PUT request - if the target group has targeting, you should provide the complete targeting object.\n                                ",
      "properties": {
       "include": {
        "type": "array",
        "description": "Array of include rules with criteria and audiences",
        "items": {
         "type": "object",
         "properties": {
          "isModifiable": {
           "type": "boolean"
          },
          "criteria": {
           "type": "array",
           "items": {
            "type": "object",
            "properties": {
             "type": {
              "type": "string"
             },
             "data": {}
            }
           }
          },
          "audiences": {
           "type": "array",
           "items": {
            "type": "object"
           }
          }
         }
        }
       },
       "exclude": {
        "type": "object",
        "description": "Exclude rules with criteria and audiences",
        "properties": {
         "criteria": {
          "type": "array",
          "items": {
           "type": "object"
          }
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "object"
          }
         }
        }
       }
      }
     }
    },
    "required": [
     "target_group_id",
     "name"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "upload_account_list_csv_audience",
   "title": "Upload Account List CSV and Create Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Upload a list of company ACCOUNTS (name + website) as an Account List CSV and create a FIRMOGRAPHIC_INCLUDE audience on the Metadata platform, optionally overlaid with platform-…",
   "description": "Upload a list of company ACCOUNTS (name + website) as an Account List CSV and create a FIRMOGRAPHIC_INCLUDE audience on the Metadata platform, optionally overlaid with platform-side contacts criteria.\n\n                AUDIENCE TYPE: Account List CSV / CSV Upload - Accounts (platform stores `customAudienceType=FIRMOGRAPHIC_INCLUDE`; the main UI's audience-map renders this enum as \"CSV Upload - Accounts\").\n                This tool is ONLY for company/account-level data (company names and websites).\n                Do NOT use this tool for contact-level data (emails, phone numbers, individual people).\n                For contacts, use upload_contact_list_csv_audience instead.\n\n                TWO WAYS TO SUPPLY THE ACCOUNT LIST — provide EXACTLY ONE of:\n                  • `companies`: an inline JSON object map `{ \"<companyname>\": \"<companywebsite>\", ... }`. Use this for short ad-hoc lists you have already parsed into context.\n                  • `companies_source_csv_url`: a public URL of a CSV file with header EXACTLY `companyname,companywebsite` (case-insensitive, whitespace-trimmed). Use this WHENEVER THE USER ATTACHED A CSV to the chat — the URL is surfaced to you via `AudienceBrief.attached_file_urls`; pass it through verbatim. The MCP server downloads + validates + parses the CSV so a multi-megabyte file never has to travel through your context.\n\n                If both are provided, or neither, the tool errors with a clear message — pick one.\n\n                OPTIONAL CONTACTS CRITERIA: layer per-contact filters on top of the account list, matching the same shape `create_firmographic_audience` uses:\n                  • `location_country_ids` / `location_state_ids` — geography\n                  • `job_title_includes` / `job_title_excludes` — free-text title keywords\n                  • `job_function_include_ids` / `job_function_exclude_ids`\n                  • `seniority_include_ids` / `seniority_exclude_ids`\n                  • `contacts_per_company_limit`\n                When set, the resulting audience matches only contacts inside the listed companies that ALSO satisfy these filters — e.g. \"this 1,999-account list, but only VP/Director/Manager Engineering contacts in Canada + Saint Pierre and Miquelon + United States\" becomes a single call. When all of these are omitted, every contact in every uploaded company is matched.\n\n                WHEN TO USE:\n                - \"Upload these companies/accounts as an audience\" / \"Create an account list audience\" / \"Upload account list CSV\"\n                - \"Create a CSV Upload - Accounts audience with this CSV and these contact filters\"\n                - The user attached a CSV of companies (company name + website) and asked for a CSV-Upload audience\n\n                WHEN NOT TO USE:\n                - When the data is contact-level (emails, phone numbers, individual people) — use `upload_contact_list_csv_audience`.\n                - For pure firmographic targeting without an attached account list — use `create_firmographic_audience`.\n\n                INLINE-MAP DATA MAPPING RULES (when you choose the `companies` path):\n                  - Each KEY must be the **company name**; each VALUE must be the **company website URL**.\n                  - Format: { \"<companyname>\": \"<companywebsite>\", ... }\n                  - Example:\n                      {\n                        \"Acme Corp\": \"https://acme.com\",\n                        \"Globex International\": \"https://globex.com\",\n                        \"Salesforce\": \"https://salesforce.com\"\n                      }\n                  - DO NOT pass column headers as keys; DO NOT reverse the mapping; DO NOT send raw file paths or bytes.\n\n                CSV-URL HEADER RULES (when you choose the `companies_source_csv_url` path):\n                  - The first row of the CSV MUST be exactly `companyname,companywebsite` (case-insensitive — `Company Name,Website` is rejected; rename the columns first or fall back to the inline map path).\n                  - Rows with an empty company name are dropped server-side.\n                  - The server enforces a 50 MB cap on the downloaded file.\n\n                PARAMETERS:\n                - audience_name (required)\n                - companies (optional, mutually exclusive with companies_source_csv_url)\n                - companies_source_csv_url (optional, mutually exclusive with companies)\n                - location_country_ids / location_state_ids (optional contacts criteria)\n                - job_title_includes / job_title_excludes (optional)\n                - job_function_include_ids / job_function_exclude_ids (optional)\n                - seniority_include_ids / seniority_exclude_ids (optional)\n                - contacts_per_company_limit (optional)\n\n                RETURNS:\n                - success, id / audience_id, audience_name, audience_type (FIRMOGRAPHIC_INCLUDE), file_id, companies_count, upload_filename, expectedNumberOfCompanies, expectedNumberOfContacts, cappedContactCount\n\n                IMPORTANT NOTES:\n                - There is a small delay between upload and audience creation while the platform processes the file; the tool waits for that internally.\n                - The companies map / source CSV must contain at least one usable row.\n                - Company websites should be valid URLs (e.g., https://example.com).",
   "inputSchema": {
    "type": "object",
    "properties": {
     "audience_name": {
      "type": "string",
      "description": "Name for the new audience (required).",
      "minLength": 1
     },
     "companies": {
      "type": "object",
      "description": "Inline map of company names to website URLs (optional). Example: {\"Acme Corp\": \"https://acme.com\", \"Globex\": \"https://globex.com\"}. MUTUALLY EXCLUSIVE with `companies_source_csv_url` — provide exactly one.",
      "additionalProperties": {
       "type": "string",
       "description": "Company website URL"
      },
      "minProperties": 1
     },
     "companies_source_csv_url": {
      "type": "string",
      "description": "Public URL of a CSV file with header EXACTLY `companyname,companywebsite` (case-insensitive). The MCP server downloads, validates, and parses the file. Use this when the user attached a CSV to the chat — the URL is surfaced via `AudienceBrief.attached_file_urls`. MUTUALLY EXCLUSIVE with `companies`.",
      "format": "uri"
     },
     "location_country_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Country IDs for geographic targeting (optional). Valid IDs: 1 (Afghanistan), 2 (Albania), 3 (Algeria), 4 (American Samoa), 5 (Andorra), 6 (Angola), 7 (Anguilla), 8 (Antarctica), 9 (Antigua and Barbuda), 10 (Argentina), 11 (Armenia), 12 (Aruba), 13 (Australia), 14 (Austria), 15 (Azerbaijan), 16 (Bahamas), 17 (Bahrain), 18 (Bangladesh), 19 (Barbados), 20 (Belarus), 21 (Belgium), 22 (Belize), 23 (Benin), 24 (Bermuda), 25 (Bhutan), 26 (Bolivia), 27 (Bosnia and Herzegovina), 28 (Botswana), 29 (Brazil), 30 (British Indian Ocean Territory), 31 (British Virgin Islands), 32 (Brunei), 33 (Bulgaria), 34 (Burkina Faso), 35 (Burundi), 36 (Cambodia), 37 (Cameroon), 38 (Canada), 39 (Cape Verde), 40 (Cayman Islands), 41 (Central African Republic), 42 (Chad), 43 (Chile), 44 (China), 45 (Christmas Island), 46 (Cocos Islands), 47 (Colombia), 48 (Comoros), 49 (Cook Islands), 50 (Costa Rica), 51 (Croatia), 53 (Curacao), 54 (Cyprus), 55 (Czech Republic), 56 (Democratic Republic of the Congo), 57 (Denmark), 58 (Djibouti), 59 (Dominica), 60 (Dominican Republic), 61 (East Timor), 62 (Ecuador), 63 (Egypt), 64 (El Salvador), 65 (Equatorial Guinea), 66 (Eritrea), 67 (Estonia), 68 (Ethiopia), 69 (Falkland Islands), 70 (Faroe Islands), 71 (Fiji), 72 (Finland), 73 (France), 74 (French Polynesia), 75 (Gabon), 76 (Gambia), 77 (Georgia), 78 (Germany), 79 (Ghana), 80 (Gibraltar), 81 (Greece), 82 (Greenland), 83 (Grenada), 84 (Guam), 85 (Guatemala), 86 (Guernsey), 87 (Guinea), 88 (Guinea-Bissau), 89 (Guyana), 90 (Haiti), 91 (Honduras), 92 (Hong Kong), 93 (Hungary), 94 (Iceland), 95 (India), 96 (Indonesia), 98 (Iraq), 99 (Ireland), 100 (Isle of Man), 101 (Israel), 102 (Italy), 103 (Ivory Coast), 104 (Jamaica), 105 (Japan), 106 (Jersey), 107 (Jordan), 108 (Kazakhstan), 109 (Kenya), 110 (Kiribati), 111 (Kosovo), 112 (Kuwait), 113 (Kyrgyzstan), 114 (Laos), 115 (Latvia), 116 (Lebanon), 117 (Lesotho), 118 (Liberia), 119 (Libya), 120 (Liechtenstein), 121 (Lithuania), 122 (Luxembourg), 123 (Macau), 124 (Macedonia), 125 (Madagascar), 126 (Malawi), 127 (Malaysia), 128 (Maldives), 129 (Mali), 130 (Malta), 131 (Marshall Islands), 132 (Mauritania), 133 (Mauritius), 134 (Mayotte), 135 (Mexico), 136 (Micronesia), 137 (Moldova), 138 (Monaco), 139 (Mongolia), 140 (Montenegro), 141 (Montserrat), 142 (Morocco), 143 (Mozambique), 144 (Myanmar), 145 (Namibia), 146 (Nauru), 147 (Nepal), 148 (Netherlands), 149 (Netherlands Antilles), 150 (New Caledonia), 151 (New Zealand), 152 (Nicaragua), 153 (Niger), 154 (Nigeria), 155 (Niue), 157 (Northern Mariana Islands), 158 (Norway), 159 (Oman), 160 (Pakistan), 161 (Palau), 162 (Palestine), 163 (Panama), 164 (Papua New Guinea), 165 (Paraguay), 166 (Peru), 167 (Philippines), 168 (Pitcairn), 169 (Poland), 170 (Portugal), 171 (Puerto Rico), 172 (Qatar), 173 (Republic of the Congo), 174 (Reunion), 175 (Romania), 176 (Russia), 177 (Rwanda), 178 (Saint Barthelemy), 179 (Saint Helena), 180 (Saint Kitts and Nevis), 181 (Saint Lucia), 182 (Saint Martin), 183 (Saint Pierre and Miquelon), 184 (Saint Vincent and the Grenadines), 185 (Samoa), 186 (San Marino), 187 (Sao Tome and Principe), 188 (Saudi Arabia), 189 (Senegal), 190 (Serbia), 191 (Seychelles), 192 (Sierra Leone), 193 (Singapore), 194 (Sint Maarten), 195 (Slovakia), 196 (Slovenia), 197 (Solomon Islands), 198 (Somalia), 199 (South Africa), 200 (South Korea), 201 (South Sudan), 202 (Spain), 203 (Sri Lanka), 205 (Suriname), 206 (Svalbard and Jan Mayen), 207 (Swaziland), 208 (Sweden), 209 (Switzerland), 211 (Taiwan), 212 (Tajikistan), 213 (Tanzania), 214 (Thailand), 215 (Togo), 216 (Tokelau), 217 (Tonga), 218 (Trinidad and Tobago), 219 (Tunisia), 220 (Turkey), 221 (Turkmenistan), 222 (Turks and Caicos Islands), 223 (Tuvalu), 224 (U.S. Virgin Islands), 225 (Uganda), 226 (Ukraine), 227 (United Arab Emirates), 228 (United Kingdom), 229 (United States), 230 (Uruguay), 231 (Uzbekistan), 232 (Vanuatu), 233 (Vatican), 234 (Venezuela), 235 (Vietnam), 236 (Wallis and Futuna), 237 (Western Sahara), 238 (Yemen), 239 (Zambia), 240 (Zimbabwe), 241 (Guadeloupe). Example: [29] for Brazil, [229] for United States"
     },
     "location_state_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "State IDs for geographic targeting (optional). Valid US State IDs: 1 (Alabama), 2 (Alaska), 3 (Arizona), 4 (Arkansas), 5 (California), 6 (Colorado), 7 (Connecticut), 8 (Delaware), 9 (Florida), 10 (Georgia), 11 (Hawaii), 12 (Idaho), 13 (Illinois), 14 (Indiana), 15 (Iowa), 16 (Kansas), 17 (Kentucky), 18 (Louisiana), 19 (Maine), 20 (Maryland), 21 (Massachusetts), 22 (Michigan), 23 (Minnesota), 24 (Mississippi), 25 (Missouri), 26 (Montana), 27 (Nebraska), 28 (Nevada), 29 (New Hampshire), 30 (New Jersey), 31 (New Mexico), 32 (New York), 33 (North Carolina), 34 (North Dakota), 35 (Ohio), 36 (Oklahoma), 37 (Oregon), 38 (Pennsylvania), 39 (Rhode Island), 40 (South Carolina), 41 (South Dakota), 42 (Tennessee), 43 (Texas), 44 (Utah), 45 (Vermont), 46 (Virginia), 47 (Washington), 48 (West Virginia), 49 (Wisconsin), 50 (Wyoming), 52 (District of Columbia). Example: [5] for California, [32] for New York"
     },
     "job_title_includes": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free text keywords for job titles to include (optional). Example: ['Software Engineer', 'Manager']"
     },
     "job_title_excludes": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Free text keywords for job titles to exclude (optional). Example: ['Senior', 'Lead']"
     },
     "job_function_include_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Job function IDs to include (optional). Valid IDs: 1 (Marketing), 2 (Sales), 3 (Customer Service), 4 (Finance), 5 (HR), 6 (PR), 7 (Media), 8 (Design), 9 (Engineering), 10 (Operations), 11 (Legal), 12 (Education), 13 (Health), 14 (Real Estate), 15 (Accounting), 16 (Administrative), 17 (Business Development), 18 (Community Services), 19 (Consulting), 20 (Entrepreneurship), 21 (IT), 22 (Military Services), 23 (Product Management), 24 (Program/Project Management), 25 (Purchasing), 26 (QA), 27 (Research), 28 (Trades). Example: [9] for Engineering"
     },
     "job_function_exclude_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Job function IDs to exclude (optional). Valid IDs: 1-28 as listed above"
     },
     "seniority_include_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Seniority level IDs to include (optional). Valid IDs: 2 (Manager), 3 (Director), 4 (VP), 5 (CXO), 6 (Unpaid), 7 (Training), 8 (Senior), 9 (Owner), 10 (Entry), 11 (Partner). Example: [10, 7] for Entry and Training"
     },
     "seniority_exclude_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Seniority level IDs to exclude (optional). Valid IDs: 2-11 as listed above"
     },
     "contacts_per_company_limit": {
      "type": "integer",
      "description": "Maximum number of contacts to target per company (optional). Example: 20",
      "minimum": 1
     }
    },
    "required": [
     "audience_name"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "upload_contact_list_csv_audience",
   "title": "Upload Contact List CSV and Create Contact List Audience",
   "category": "audiences",
   "risk": "write",
   "summary": "Upload a list of individual CONTACTS (people) as a Contact List CSV and create a CONTACT_LIST audience on the Metadata platform.",
   "description": "Upload a list of individual CONTACTS (people) as a Contact List CSV and create a CONTACT_LIST audience on the Metadata platform.\n\n                ALSO KNOWN AS: \"CSV Upload - Contacts\", \"Contacts List\", \"Contact List CSV\", \"CSV Contacts audience\", \"CSV Upload - Contacts List Audience\".\n\n                AUDIENCE TYPE: Contact List / CSV Upload - Contacts.\n                This tool is ONLY for contact/people-level data (email, first name, last name, job title, etc.).\n                Do NOT use this tool for company/account-level data (company names and websites) — use upload_account_list_csv_audience instead.\n\n                This tool performs a two-step process:\n                1. Generates a Contact List CSV file from the provided contact data and uploads it.\n                2. Creates a CONTACT_LIST audience using the uploaded contact list file.\n\n                WHEN TO USE:\n                - \"Create a CSV Upload - Contacts with the list\"\n                - \"CSV Upload - Contacts\"\n                - \"Create a contacts list audience\"\n                - \"Upload these contacts as an audience\"\n                - \"Create a contact list audience from these people\"\n                - \"I have a list of emails and names, create an audience\"\n                - \"Build a contact list audience\"\n                - \"Upload contact list CSV\"\n                - When the data contains people-level fields: email, first name, last name, job title, company, country\n\n                WHEN NOT TO USE:\n                - When the user wants to upload company accounts (company name + website) — use upload_account_list_csv_audience.\n                - When the data is account/company-level, not contact/people-level.\n\n                WORKFLOW:\n                1. Provide the audience name and an array of contact objects.\n                2. The tool generates a CSV with the header: email,firstname,lastname,jobtitle,employeecompany,country,appleidfa,googleaid\n                3. Uploads the CSV, then creates the CONTACT_LIST audience.\n\n                ⚠️ CRITICAL — DATA MAPPING RULES (READ CAREFULLY BEFORE CALLING THIS TOOL):\n\n                The \"contacts\" parameter is a JSON array of objects. Each object represents one contact/person\n                and MUST use these exact field names:\n\n                  - \"email\"             → The contact's email address (REQUIRED per contact)\n                  - \"firstname\"         → The contact's first name\n                  - \"lastname\"          → The contact's last name\n                  - \"jobtitle\"          → The contact's job title / role\n                  - \"employeecompany\"   → The company the contact works at\n                  - \"country\"           → The contact's country\n                  - \"appleidfa\"         → Apple IDFA (advertising identifier), optional\n                  - \"googleaid\"         → Google Advertising ID, optional\n\n                Example:\n                  [\n                    {\n                      \"email\": \"jane@metadata.io\",\n                      \"firstname\": \"Jane\",\n                      \"lastname\": \"Johnson\",\n                      \"jobtitle\": \"Marketing Manager\",\n                      \"employeecompany\": \"Metadata.io\",\n                      \"country\": \"United States\",\n                      \"appleidfa\": \"EA7583CD-A667-48BC-B806-42ECB2B48606\",\n                      \"googleaid\": \"\"\n                    },\n                    {\n                      \"email\": \"john@metadata.io\",\n                      \"firstname\": \"John\",\n                      \"lastname\": \"Johnson\",\n                      \"jobtitle\": \"Marketing Manager\",\n                      \"employeecompany\": \"Metadata.io\",\n                      \"country\": \"United States\",\n                      \"appleidfa\": \"\",\n                      \"googleaid\": \"cdda802e-fb9c-47ad-9866-0794d394c912\"\n                    }\n                  ]\n\n                IF THE USER PROVIDES A FILE (CSV, XLSX, spreadsheet, or any tabular data):\n                  1. You MUST first read and inspect the file contents.\n                  2. Identify which columns map to: email, firstname, lastname, jobtitle, employeecompany, country, appleidfa, googleaid.\n                     - The columns may NOT be literally named as above. They could be named:\n                       \"Email Address\", \"E-mail\", \"First Name\", \"First\", \"Last Name\", \"Surname\",\n                       \"Job Title\", \"Title\", \"Role\", \"Position\", \"Company\", \"Organization\", \"Employer\",\n                       \"Country\", \"Location\", \"Apple IDFA\", \"IDFA\", \"Google AID\", \"GAID\", or any variation.\n                     - Use your best judgment to map the correct columns to the expected field names.\n                     - If ambiguous, ask the user to clarify which column maps to which field.\n                  3. Extract every row from the file and build the contacts array yourself,\n                     mapping each column value to the correct field name.\n                  4. Do NOT pass raw file contents, column headers, or file paths — always transform into\n                     the array-of-objects format described above.\n                  5. Skip rows where email is empty/missing (email is the minimum required field per contact).\n                  6. For any field not present in the source data, omit it or pass an empty string.\n                  7. If the file has no identifiable email column, ask the user which column contains emails.\n\n                DO NOT:\n                  - Use the source file's column names as field names — always normalize to: email, firstname, lastname, jobtitle, employeecompany, country, appleidfa, googleaid.\n                  - Send the raw file path or file bytes — extract the data and build the array.\n                  - Confuse this with account/company uploads — this is for PEOPLE, not companies.\n\n                TWO WAYS TO SUPPLY THE CONTACTS — provide EXACTLY ONE of:\n                  • `contacts`: an inline JSON array of contact objects. Use this for short ad-hoc lists you have already parsed into context.\n                  • `contacts_source_csv_url`: a public URL of a CSV file with header EXACTLY `email,firstname,lastname,jobtitle,employeecompany,country,appleidfa,googleaid` (case-insensitive, whitespace-trimmed). Use this WHENEVER THE USER ATTACHED A CSV to the chat — the URL is surfaced to you via `AudienceBrief.attached_file_urls`; pass it through verbatim. The MCP server downloads + validates + uploads the file without the rows ever travelling through your LLM context (essential for 100k+ row contact files).\n                If both are provided, or neither, the tool errors with a clear message — pick one.\n\n                CSV-URL HEADER RULES (when you choose the `contacts_source_csv_url` path):\n                  - First row of the CSV MUST be exactly `email,firstname,lastname,jobtitle,employeecompany,country,appleidfa,googleaid` (case-insensitive).\n                  - Rows with an empty email are dropped server-side before counting.\n                  - The 300–300,000 row limit is enforced on the post-filter count.\n                  - The server enforces a 50 MB cap on the downloaded file.\n\n                PARAMETERS:\n                - audience_name: Name for the new contact list audience (required)\n                - contacts: Array of contact objects (optional, mutually exclusive with contacts_source_csv_url).\n                  At minimum each contact must have \"email\". All other fields are optional but recommended.\n                - contacts_source_csv_url: URL of a contacts CSV with the canonical 8-column header (optional, mutually exclusive with contacts).\n\n                RETURNS:\n                - success: Whether the operation completed successfully\n                - audience_name: The name of the created audience\n                - audience_type: CONTACT_LIST\n                - file_id: The ID of the uploaded contact list file\n                - contacts_count: Number of contacts processed by the backend\n                - contacts_provided: Number of contacts sent in the request\n\n                IMPORTANT NOTES:\n                - CONTACT LIMITS: Minimum 300 contacts, maximum 300,000 contacts.\n                - Email is the minimum required field per contact — contacts without email are dropped.\n                - The audience type created is CONTACT_LIST, distinct from Account List CSV (FIRMOGRAPHIC_INCLUDE).\n                - appleidfa and googleaid are optional mobile advertising identifiers — leave empty if not available.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "audience_name": {
      "type": "string",
      "description": "Name for the new contact list audience (required).",
      "minLength": 1
     },
     "contacts": {
      "type": "array",
      "description": "Inline array of contact objects (optional). Each object represents one person/contact with fields: email (required), firstname, lastname, jobtitle, employeecompany, country, appleidfa, googleaid. Minimum 300 contacts, maximum 300,000 contacts. MUTUALLY EXCLUSIVE with `contacts_source_csv_url` — provide exactly one.",
      "items": {
       "type": "object",
       "properties": {
        "email": {
         "type": "string",
         "description": "Contact email address (required)"
        },
        "firstname": {
         "type": "string",
         "description": "Contact first name"
        },
        "lastname": {
         "type": "string",
         "description": "Contact last name"
        },
        "jobtitle": {
         "type": "string",
         "description": "Contact job title / role"
        },
        "employeecompany": {
         "type": "string",
         "description": "Company the contact works at"
        },
        "country": {
         "type": "string",
         "description": "Contact country"
        },
        "appleidfa": {
         "type": "string",
         "description": "Apple IDFA advertising identifier (optional)"
        },
        "googleaid": {
         "type": "string",
         "description": "Google Advertising ID (optional)"
        }
       },
       "required": [
        "email"
       ],
       "additionalProperties": false
      },
      "minItems": 300,
      "maxItems": 300000
     },
     "contacts_source_csv_url": {
      "type": "string",
      "description": "Public URL of a contacts CSV file with header EXACTLY `email,firstname,lastname,jobtitle,employeecompany,country,appleidfa,googleaid` (case-insensitive). The MCP server downloads, validates, and uploads the file. Use this when the user attached a CSV to the chat — the URL is surfaced via `AudienceBrief.attached_file_urls`. MUTUALLY EXCLUSIVE with `contacts`.",
      "format": "uri"
     }
    },
    "required": [
     "audience_name"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "add_and_edit_campaign_elements",
   "title": "Add & Edit Campaign Elements",
   "category": "campaigns",
   "risk": "destructive",
   "summary": "Add new elements and edit existing properties of a **Precision Optimization (1×1×1 / METADATA)** marketing campaign.",
   "description": "Add new elements and edit existing properties of a **Precision Optimization (1×1×1 / METADATA)** marketing campaign.\n            This tool serves two purposes:\n            1. ADD new audiences, ads, offers, and keywords to campaigns across LinkedIn, Facebook, Instagram, Google Ads, and Reddit.\n            2. EDIT existing campaign level properties: name, budget group, schedule dates, and offers per channel.\n\n            🔴 PRECISION / METADATA CAMPAIGNS ONLY — NATIVE CAMPAIGNS USE A DIFFERENT TOOL.\n            This tool edits **Precision Optimization (1×1×1 / METADATA)** campaigns. For a **Channel-First / Native / Platform-First (N×N×N)** campaign, use `add_and_edit_native_campaign_elements` instead (it takes container-shaped input). If you call this tool on a Native campaign it returns an error telling you to use the native tool (and vice-versa), so a wrong guess self-corrects — no silent mis-write. Pick by the campaign's structure, not by user phrasing.\n\n            🔴 REMEMBER — CAMPAIGN STRUCTURE IS FIXED AT CREATION AND CANNOT BE CHANGED HERE.\n            A campaign's structure (Precision 1×1×1 vs Native N×N×N) is chosen at creation and this tool does NOT switch it. Changing structure means building a NEW campaign with create_campaign (Precision) or create_native_structure_campaign (Native).\n\n            ⚠️ THIS IS THE TOOL TO USE WHEN THE USER WANTS TO:\n            - EDIT/UPDATE a campaign's name, budget group, start date, or end date\n            - CHANGE or REPLACE the offers assigned to a channel in a campaign\n            - ENABLE a channel on an existing campaign\n            - ADD new audiences, ads, ad groups, or keywords to a campaign\n            - Expand campaign reach without recreating the entire campaign\n\n            ⚠️ CRITICAL OFFER-CHANNEL PAIRING RULE (silent failure if violated) ⚠️\n            Lead Gen offers are CHANNEL-LOCKED at creation time (the channel was set on create_update_offer via data.channel). When you populate any channel's \"offers\" array here, you MUST only pass offer_ids whose data.channel matches that slot — the API DOES NOT validate the pairing and will SILENTLY return a \"successful\" update on a campaign that cannot serve.\n              • linkedin.offers → only Lead Gen offer_ids with data.channel='LINKEDIN' (or Landing Page offer_ids).\n              • facebook.offers → only Lead Gen offer_ids with data.channel='FACEBOOK' (or Landing Page offer_ids).\n              • instagram.offers → only Lead Gen offer_ids with data.channel='INSTAGRAM' or 'FACEBOOK' (or Landing Page offer_ids).\n              • google.offers → ONLY Landing Page offer_ids (Google Ads does not support Lead Gen forms).\n              • reddit.offers → ONLY Landing Page offer_ids (Reddit does not support Lead Gen forms).\n            Because this tool's \"offers\" arrays are FULL REPLACEMENTS (not appends), a wrong id here will overwrite the channel's correct offers with broken ones. If you cannot remember an offer_id's channel, call get_offer(name=<offer_name>) BEFORE submitting and inspect the channel field.\n\n            ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n            EDIT CAPABILITIES (overwrites/replaces previous values):\n            ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n            - name → Renames the campaign (smart: only executes if new name differs from current)\n            - budgetGroup → Reassigns campaign to a different budget group (smart: only if different from current)\n            - startDate → Replaces the campaign start date (can be updated independently)\n            - endDate → Replaces the campaign end date (can be updated independently)\n            - offers → ⚠️ FULL REPLACEMENT per channel. Sending offers OVERWRITES the entire offer list for that channel.\n            - Channel state → Enables any channel included in the payload (idempotent if already enabled)\n\n            ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n            ADD CAPABILITIES (appends alongside existing, non-destructive):\n            ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n            - audiences → Appended to existing audiences (POST)\n            - exclude_audiences → Appended as channel-level EXCLUDE (negative-targeting) audience groups (POST). This is the consolidated home for exclusion audiences (the standalone add_exclusion_audiences_to_campaign tool was retired).\n            - target_groups → Appended to existing target groups (POST)\n            - ads → Appended to existing ads/ad groups (POST)\n            - keywords (Google only) → Create keyword groups (free-text and/or existing keyword IDs); optionally LINK a group to a native ad container (NATIVE structure), or change an existing group's match type in place (set_match_type)\n            - negative_keyword_lists (Google only) → Assigned to Google Ads channel after processing\n\n            REQUIRED PARAMETERS:\n            - campaignId: Campaign ID (numeric identifier for the campaign)\n            - campaignType: \"Lead Gen\" or \"Brand Awareness\" (must match existing campaign)\n            - name: Campaign name\n            - startDate: Campaign start date in YYYY-MM-DD format\n            - endDate: Campaign end date in YYYY-MM-DD format\n            - budgetGroup: Budget group name\n\n            CHANNEL ELEMENTS:\n\n            ⚠️ ADS — SEPARATE (default) vs BUNDLED INTO ONE GROUP (opt-in):\n            - \"ads.list\": ad IDs sent one per request. DEFAULT — use this unless the user\n              explicitly asks to group ads. A list of N ids produces N separate ads, not one bundle.\n            - \"ads.groups\" ([{name, ads:[ids]}]): bundles ads into named ad groups. ONLY when grouping is requested.\n            (LinkedIn has no ad groups — always use list.)\n\n            LinkedIn:\n            - audiences: Array of audience names (APPENDED)\n            - target_groups: Array of Targeting Group names (APPENDED)\n            - ads: Object with \"groups\" (ad group objects) and/or \"list\" (individual ad IDs) (APPENDED)\n            - offers: Array of offer IDs (⚠️ REPLACES all existing offers on this channel)\n\n            Facebook:\n            - audiences: Array of audience names (APPENDED)\n            - target_groups: Array of Targeting Group names (APPENDED)\n            - ads: Object with \"groups\" and/or \"list\" (APPENDED)\n            - offers: Array of offer IDs (⚠️ REPLACES all existing offers on this channel)\n\n            Instagram:\n            - audiences: Array of audience names (APPENDED)\n            - target_groups: Array of Targeting Group names (APPENDED)\n            - ads: Object with \"groups\" and/or \"list\" (APPENDED)\n            - offers: Array of offer IDs (⚠️ REPLACES all existing offers on this channel)\n\n            Google Ads:\n            - keywords: Object with \"groups\" and/or \"set_match_type\".\n                - groups: [{name, keywords?: [str], keyword_ids?: [int], match_type?: PHRASE|BROAD|EXACT, container?/container_id?}]\n                  Creates keyword groups. A group naming a container/container_id is LINKED to that native ad container (NATIVE structure: keywordGroupId set by re-POSTing the channel's container list). A group with no container is appended unlinked.\n                - set_match_type: [{keyword_group_id? | container?/container_id?, match_type}] — change an existing group's match type in place (PATCH; non-destructive).\n            - audiences: Array of audience names (APPENDED)\n            - ads: Object with \"groups\" and/or \"list\" (APPENDED)\n            - offers: Array of offer IDs (⚠️ REPLACES all existing offers on this channel)\n            - negative_keyword_lists: Array of negative keyword list names (optional)\n                Example: [\"Competitor Brands\", \"Low Intent Keywords\"]\n                Assigned to the Google Ads channel after processing.\n                Names must match existing negative keyword lists (use list_negative_keywords_list to find them).\n\n            Reddit:\n            - audiences: Array of audience names (APPENDED)\n            - target_groups: Array of Targeting Group names (APPENDED; Reddit Target Groups come from create_reddit_target_group)\n            - ads: Object with \"list\" only — Reddit ads attach one per request, like LinkedIn (APPENDED)\n            - offers: Array of offer IDs (⚠️ REPLACES all existing offers on this channel)\n            - Channel settings (objective, comments, placements, conversion pixel) are NOT edited\n              here — use update_reddit_channel_settings. When this tool flips a disabled Reddit\n              channel to enabled, UI-equivalent defaults are applied (Lead Gen → CLICKS + AUTO_BID;\n              Brand Awareness → IMPRESSIONS + MANUAL_BID).\n\n            CHANNEL ACTIVATION LOGIC:\n            A channel is enabled if ANY of these conditions are met (OR logic):\n            - audiences array has at least 1 item, OR\n            - target_groups array has at least 1 item, OR\n            - ads object exists, OR\n            - offers array has at least 1 item, OR\n            - keywords exist (Google Ads only)\n\n            ⚠️ AUDIENCES vs TARGET GROUPS — CRITICAL DISTINCTION:\n            - \"audiences\": standard Metadata audiences (Firmographic, Bombora Intent, Retargeting, LinkedIn/Facebook Native, etc.)\n            - \"target_groups\": Targeting Groups (also called Target Groups) created in the platform's Targeting Groups section.\n            Both are arrays of names (strings). Send each type in its own field — do NOT mix them.\n            If the user mentions a \"Targeting Group\" or \"Target Group\", it goes in \"target_groups\", NOT in \"audiences\".\n\n            EXAMPLES:\n\n            Edit campaign name and dates (no channel elements needed):\n            add_and_edit_campaign_elements({\n                \"campaignId\": 12345,\n                \"campaignType\": \"Lead Gen\",\n                \"name\": \"NEW_CAMPAIGN_NAME_2025\",\n                \"startDate\": \"2025-09-01\",\n                \"endDate\": \"2025-12-31\",\n                \"budgetGroup\": \"BoFu\"\n            })\n\n            Replace offers on LinkedIn:\n            add_and_edit_campaign_elements({\n                \"campaignId\": 12345,\n                \"campaignType\": \"Lead Gen\",\n                \"name\": \"MD_LG_20252107161406\",\n                \"startDate\": \"2025-08-01\",\n                \"endDate\": \"2025-09-30\",\n                \"budgetGroup\": \"BoFu\",\n                \"linkedin\": {\n                    \"offers\": [57538, 57504]\n                }\n            })\n\n            Add audiences and ads to LinkedIn:\n            add_and_edit_campaign_elements({\n                \"campaignId\": 12345,\n                \"campaignType\": \"Lead Gen\",\n                \"name\": \"MD_LG_20252107161406\",\n                \"startDate\": \"2025-08-01\",\n                \"endDate\": \"2025-09-30\",\n                \"budgetGroup\": \"BoFu\",\n                \"linkedin\": {\n                    \"audiences\": [\"Test Native Audience\", \"EU Countries\"],\n                    \"ads\": {\n                        \"groups\": [{\"name\": \"1GOAT\", \"ads\": [173669, 171805]}],\n                        \"list\": [156502, 156485]\n                    },\n                    \"offers\": [57538, 57504]\n                }\n            })\n\n            Add Google keywords:\n            add_and_edit_campaign_elements({\n                \"campaignId\": 12345,\n                \"campaignType\": \"Lead Gen\",\n                \"name\": \"MD_LG_20252107161406\",\n                \"startDate\": \"2025-08-01\",\n                \"endDate\": \"2025-09-30\",\n                \"budgetGroup\": \"BoFu\",\n                \"google\": {\n                    \"keywords\": {\n                        \"groups\": [{\"name\": \"CRM Keywords\", \"keywords\": [\"crm software\", \"lead gen tools\"]}]\n                    },\n                    \"audiences\": [\"SUP_SF_All-Customers-Contacts_ALL_3\"],\n                    \"ads\": {\"list\": [175258]},\n                    \"offers\": [61054]\n                }\n            })\n\n            Mixed: edit budget group + add across multiple channels:\n            add_and_edit_campaign_elements({\n                \"campaignId\": 12345,\n                \"campaignType\": \"Lead Gen\",\n                \"name\": \"MD_LG_20252107161406\",\n                \"startDate\": \"2025-08-01\",\n                \"endDate\": \"2025-09-30\",\n                \"budgetGroup\": \"ToFu\",\n                \"linkedin\": {\"audiences\": [\"New Audience\"], \"offers\": [57538]},\n                \"facebook\": {\"audiences\": [\"FB Audience\"], \"ads\": {\"list\": [171806]}},\n                \"google\": {\"keywords\": {\"groups\": [{\"name\": \"Keywords\", \"keywords\": [\"keyword\"]}]}, \"offers\": [61054]}\n            })\n\n            VALIDATION:\n            - campaignId must match existing campaign\n            - All asset IDs (ads, offers) must exist\n            - Audience names must match existing audiences\n            - Budget group and name use smart filters: only updated if different from current values\n            - Dates support partial updates: you can change only startDate, only endDate, or both\n\n            RESPONSE: Returns\n              - success, campaign_id, campaign_name, campaign_type, status, status_label\n              - channels_updated: list of channel keys that had any edits\n              - elements_added: true if anything changed at the campaign or channel level\n              - added_summary: per-channel counts of what was ACTUALLY attached (audiences,\n                target_groups, ad_groups, individual_ads, offers, keyword_groups,\n                negative_keyword_lists) — not just what was requested\n              - changes: { renamed, budget_group_reassigned, schedule_updated }\n                reporting which campaign-level edits actually fired\n              - optimization_group: { id, tag_name } after the edit\n              - start_date, end_date, campaign_url, full_response (complete wizard state)\n              - errors[]: populated on partial failures; success=false if present\n\n            BID STRATEGY FOR NEWLY-ENABLED CHANNELS:\n            When a channel transitions from disabled → enabled as part of this edit, the\n            tool automatically applies the same auto-bid default used on create_campaign\n            (AUTO_BID on LI/FB/IG, TARGET_SPEND on Google, objective-driven on Reddit:\n            Lead Gen → CLICKS + AUTO_BID, Brand Awareness → IMPRESSIONS + MANUAL_BID)\n            so the channel is launch-ready.\n            Already-enabled channels keep their existing bid strategy untouched.\n\n            SMART-UPDATE BEHAVIOUR:\n            - name and budgetGroup are only pushed to the platform when different from\n              current values (prevents the UniqueWizardName validator from rejecting a\n              no-op rename, and avoids pointless tag churn).\n            - startDate / endDate are partial: supply either or both.\n\n            ERROR HANDLING: Validates campaign exists, verifies all asset IDs and audience\n            names. Failures on individual operations are collected into errors[] and the\n            rest of the edit proceeds — the campaign is not rolled back on partial errors.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_data": {
      "type": "object",
      "description": "Campaign data with fields to edit and/or elements to add",
      "properties": {
       "campaignId": {
        "type": "integer",
        "description": "Campaign Id (numeric identifier)"
       },
       "campaignType": {
        "type": "string",
        "enum": [
         "Lead Gen",
         "Brand Awareness"
        ],
        "description": "Type of campaign (must match existing campaign)"
       },
       "name": {
        "type": "string",
        "description": "Campaign name — if different from current, the campaign will be RENAMED"
       },
       "startDate": {
        "type": "string",
        "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
        "description": "Campaign start date in YYYY-MM-DD format — REPLACES existing start date"
       },
       "endDate": {
        "type": "string",
        "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
        "description": "Campaign end date in YYYY-MM-DD format — REPLACES existing end date"
       },
       "budgetGroup": {
        "type": "string",
        "description": "Budget group name — if different from current, campaign will be REASSIGNED to this budget group"
       },
       "linkedin": {
        "type": "object",
        "description": "LinkedIn elements to add and/or offers to replace",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of LinkedIn audience names to ADD (appended to existing). For standard audiences only (Firmographic, Bombora Intent, Retargeting, Native, etc.)."
         },
         "exclude_audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of LinkedIn audience names to EXCLUDE (negative targeting) on this channel, appended as channel-level exclusion groups. Consolidated replacement for the old add_exclusion_audiences_to_campaign tool."
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Targeting Group names to ADD for LinkedIn (appended to existing). These are NOT audiences — they are Target Groups from the platform's Targeting Groups section."
         },
         "ads": {
          "type": "object",
          "properties": {
           "groups": {
            "type": "array",
            "items": {
             "type": "object",
             "properties": {
              "name": {
               "type": "string"
              },
              "ads": {
               "type": "array",
               "items": {
                "type": "integer"
               }
              }
             },
             "required": [
              "name",
              "ads"
             ]
            }
           },
           "list": {
            "type": "array",
            "items": {
             "type": "integer"
            }
           }
          }
         },
         "offers": {
          "type": "array",
          "items": {
           "type": "integer"
          },
          "description": "Array of offer IDs — ⚠️ REPLACES all existing offers on this channel. ⚠️ CHANNEL-LOCK: MUST contain ONLY offer_ids whose creating call set data.channel='LINKEDIN' (Lead Gen) OR Landing Page offer_ids (channel-agnostic). A Facebook/Instagram Lead Gen offer_id placed here is a SILENT FAILURE — the API will return success but the campaign won't serve."
         }
        }
       },
       "facebook": {
        "type": "object",
        "description": "Facebook elements to add and/or offers to replace",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Facebook audience names to ADD (appended to existing). For standard audiences only (Firmographic, Bombora Intent, Retargeting, Native, etc.)."
         },
         "exclude_audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Facebook audience names to EXCLUDE (negative targeting) on this channel, appended as channel-level exclusion groups. Consolidated replacement for the old add_exclusion_audiences_to_campaign tool."
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Targeting Group names to ADD for Facebook (appended to existing). These are NOT audiences — they are Target Groups from the platform's Targeting Groups section."
         },
         "ads": {
          "type": "object",
          "properties": {
           "groups": {
            "type": "array",
            "items": {
             "type": "object",
             "properties": {
              "name": {
               "type": "string"
              },
              "ads": {
               "type": "array",
               "items": {
                "type": "integer"
               }
              }
             },
             "required": [
              "name",
              "ads"
             ]
            }
           },
           "list": {
            "type": "array",
            "items": {
             "type": "integer"
            }
           }
          }
         },
         "offers": {
          "type": "array",
          "items": {
           "type": "integer"
          },
          "description": "Array of offer IDs — ⚠️ REPLACES all existing offers on this channel. ⚠️ CHANNEL-LOCK: MUST contain ONLY offer_ids whose creating call set data.channel='FACEBOOK' (Lead Gen) OR Landing Page offer_ids (channel-agnostic). A LinkedIn/Instagram Lead Gen offer_id placed here is a SILENT FAILURE — the API will return success but the campaign won't serve."
         }
        }
       },
       "instagram": {
        "type": "object",
        "description": "Instagram elements to add and/or offers to replace",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Instagram audience names to ADD (appended to existing). For standard audiences only (Firmographic, Bombora Intent, Retargeting, Native, etc.)."
         },
         "exclude_audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Instagram audience names to EXCLUDE (negative targeting) on this channel, appended as channel-level exclusion groups. Consolidated replacement for the old add_exclusion_audiences_to_campaign tool."
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Targeting Group names to ADD for Instagram (appended to existing). These are NOT audiences — they are Target Groups from the platform's Targeting Groups section."
         },
         "ads": {
          "type": "object",
          "properties": {
           "groups": {
            "type": "array",
            "items": {
             "type": "object",
             "properties": {
              "name": {
               "type": "string"
              },
              "ads": {
               "type": "array",
               "items": {
                "type": "integer"
               }
              }
             },
             "required": [
              "name",
              "ads"
             ]
            }
           },
           "list": {
            "type": "array",
            "items": {
             "type": "integer"
            }
           }
          }
         },
         "offers": {
          "type": "array",
          "items": {
           "type": "integer"
          },
          "description": "Array of offer IDs — ⚠️ REPLACES all existing offers on this channel. ⚠️ CHANNEL-LOCK: MUST contain ONLY offer_ids whose creating call set data.channel='INSTAGRAM' or 'FACEBOOK' (Lead Gen; Meta business sharing means FB offers are usable on IG) OR Landing Page offer_ids (channel-agnostic). A LinkedIn Lead Gen offer_id placed here is a SILENT FAILURE — the API will return success but the campaign won't serve."
         }
        }
       },
       "google": {
        "type": "object",
        "description": "Google Ads elements to add and/or offers to replace",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "keywords": {
          "type": "object",
          "description": "Keyword-group edits for the Google Ads channel. `groups` CREATES keyword groups (optionally LINKING each to an existing native ad container); `set_match_type` changes the match type of existing groups in place. Container linkage and container-referenced patches require the campaign to be NATIVE structure.",
          "properties": {
           "groups": {
            "type": "array",
            "description": "Keyword groups to create. A group with `keywords` and/or `keyword_ids` is created on the channel. If it also names a `container`/`container_id`, the new group is LINKED to that native ad container (its keywordGroupId is set by re-POSTing the channel's native container list — NATIVE structure only).",
            "items": {
             "type": "object",
             "properties": {
              "name": {
               "type": "string"
              },
              "keywords": {
               "type": "array",
               "items": {
                "type": "string"
               },
               "description": "Free-text phrases — created in the keyword library and added to this group."
              },
              "keyword_ids": {
               "type": "array",
               "items": {
                "type": "integer"
               },
               "description": "Existing keyword-library IDs (from list_keywords) to add to this group. Unioned with `keywords`."
              },
              "match_type": {
               "type": "string",
               "enum": [
                "PHRASE",
                "BROAD",
                "EXACT"
               ],
               "description": "Match type for this group. Defaults to PHRASE."
              },
              "container": {
               "type": "string",
               "description": "Name of the native ad container to LINK this new group to (NATIVE campaigns only). Use `container_id` instead when you have the container's numeric id."
              },
              "container_id": {
               "type": "integer",
               "description": "ID of the native ad container to LINK this new group to (NATIVE campaigns only). Takes precedence over `container`."
              }
             },
             "required": [
              "name"
             ]
            }
           },
           "set_match_type": {
            "type": "array",
            "description": "Change the match type of EXISTING keyword groups in place. Reference the group by `keyword_group_id`, or by the `container`/`container_id` whose linked group should change (NATIVE only).",
            "items": {
             "type": "object",
             "properties": {
              "keyword_group_id": {
               "type": "integer",
               "description": "ID of the keyword group to update (from the campaign's channel `keywordGroups`)."
              },
              "container": {
               "type": "string",
               "description": "Name of the native ad container whose linked keyword group should be updated (used when keyword_group_id is not given)."
              },
              "container_id": {
               "type": "integer",
               "description": "ID of the native ad container whose linked keyword group should be updated."
              },
              "match_type": {
               "type": "string",
               "enum": [
                "PHRASE",
                "BROAD",
                "EXACT"
               ],
               "description": "New match type for the group."
              }
             },
             "required": [
              "match_type"
             ]
            }
           }
          }
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Google audience names to ADD (appended to existing). For standard audiences only (Firmographic, Bombora Intent, Retargeting, etc.)."
         },
         "exclude_audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Google audience names to EXCLUDE (negative targeting) on this channel, appended as channel-level exclusion groups. Consolidated replacement for the old add_exclusion_audiences_to_campaign tool."
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Targeting Group names to ADD for Google Ads (appended to existing). These are NOT audiences — they are Target Groups from the platform's Targeting Groups section."
         },
         "ads": {
          "type": "object",
          "properties": {
           "groups": {
            "type": "array",
            "items": {
             "type": "object",
             "properties": {
              "name": {
               "type": "string"
              },
              "ads": {
               "type": "array",
               "items": {
                "type": "integer"
               }
              }
             },
             "required": [
              "name",
              "ads"
             ]
            }
           },
           "list": {
            "type": "array",
            "items": {
             "type": "integer"
            }
           }
          }
         },
         "offers": {
          "type": "array",
          "items": {
           "type": "integer"
          },
          "description": "Array of offer IDs — ⚠️ REPLACES all existing offers on this channel. ⚠️ CHANNEL-LOCK: MUST contain ONLY Landing Page offer_ids — Google Ads does NOT support Lead Gen forms. Any offer_id created via create_update_offer with offer_type='Lead Gen' (regardless of data.channel) is FORBIDDEN here and is a SILENT FAILURE — the API will return success but the campaign won't serve."
         },
         "negative_keyword_lists": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of negative keyword list names to assign to the Google Ads channel. Names must match existing lists (use list_negative_keywords_list to find them)."
         },
         "locations": {
          "type": "object",
          "description": "Where the Google Ads campaign SERVES — the UI's Location picker. ⚠️ OMITTING THIS LEAVES THE PLATFORM DEFAULT OF UNITED STATES. Set it whenever the user names a geography.",
          "properties": {
           "countries": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "Country NAMES to target, e.g. [\"United States\", \"Canada\"]. Common shorthand resolves (\"USA\", \"UK\"). An unrecognised name is rejected, not skipped."
           },
           "states": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "US state NAMES to target, e.g. [\"Texas\", \"California\"]. US-only. Naming states without countries keeps the channel's current countries."
           }
          },
          "additionalProperties": false
         },
         "excluded_locations": {
          "type": "object",
          "description": "Where the Google Ads campaign must NOT serve — the UI's Negative Locations editor under Advanced Settings. ⚠️ COUNTRY-LEVEL ONLY: the platform stores exclusions as bare location ids with no country/state discriminator and state ids overlap country ids, so an excluded state would be read back as an unrelated country. To narrow within a country, target the states you want via `locations.states` instead.",
          "properties": {
           "countries": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "Country NAMES to exclude, e.g. [\"India\", \"Pakistan\"]."
           }
          },
          "additionalProperties": false
         }
        }
       },
       "reddit": {
        "type": "object",
        "description": "Reddit elements to add and/or offers to replace. Channel settings (objective, comments, placements, conversion pixel) are edited via update_reddit_channel_settings, not here.",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Reddit audience names to ADD (appended to existing). For standard audiences only (Firmographic, Bombora Intent, Retargeting, etc.)."
         },
         "exclude_audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Reddit audience names to EXCLUDE (negative targeting) on this channel, appended as channel-level exclusion groups. Consolidated replacement for the old add_exclusion_audiences_to_campaign tool."
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Targeting Group names to ADD for Reddit (appended to existing; created via create_reddit_target_group). These are NOT audiences — they are Target Groups from the platform's Targeting Groups section."
         },
         "ads": {
          "type": "object",
          "properties": {
           "list": {
            "type": "array",
            "items": {
             "type": "integer"
            },
            "description": "Array of Reddit ad IDs, each sent as its own request. Like LinkedIn, Reddit uses list only — no ad groups."
           }
          }
         },
         "offers": {
          "type": "array",
          "items": {
           "type": "integer"
          },
          "description": "Array of offer IDs — ⚠️ REPLACES all existing offers on this channel. ⚠️ CHANNEL-LOCK: MUST contain ONLY Landing Page offer_ids — Reddit does NOT support Lead Gen forms. Any offer_id created via create_update_offer with offer_type='Lead Gen' (regardless of data.channel) is FORBIDDEN here and is a SILENT FAILURE — the API will return success but the campaign won't serve."
         }
        }
       }
      },
      "required": [
       "campaignId",
       "campaignType",
       "name",
       "startDate",
       "endDate",
       "budgetGroup"
      ],
      "additionalProperties": false
     }
    },
    "required": [
     "campaign_data"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "add_and_edit_native_campaign_elements",
   "title": "Add & Edit Campaign Elements — Native / Channel-First (N×N×N)",
   "category": "campaigns",
   "risk": "destructive",
   "summary": "Add elements to, and edit campaign-level fields of, an existing **Channel-First / Native (N×N×N)** campaign — the one built with `create_native_structure_campaign` (channel.stru…",
   "description": "Add elements to, and edit campaign-level fields of, an existing **Channel-First / Native (N×N×N)** campaign — the one built with `create_native_structure_campaign` (channel.structureType=NATIVE, WizNativeAdContainer). This is the NATIVE counterpart of `add_and_edit_campaign_elements`: that tool edits Precision (1×1×1 / METADATA) campaigns; THIS tool edits Native (N×N×N) campaigns.\n\n                🔴 PICK THE TOOL BY THE CAMPAIGN'S STRUCTURE (not by user phrasing):\n                  • Native / Channel-First / Platform-First / N×N×N campaign → THIS tool (`add_and_edit_native_campaign_elements`).\n                  • Precision Optimization / 1×1×1 / METADATA campaign → `add_and_edit_campaign_elements`.\n                Both tools guard: call THIS tool on a Precision/METADATA campaign and it returns an error telling you to use `add_and_edit_campaign_elements`, and vice-versa. So if you guess wrong, the error tells you the right one — no silent mis-write. If you don't know the structure, check the campaign first (its channels' structureType) or just try and follow the redirect.\n\n                WHAT THIS TOOL DOES:\n                - EDIT campaign-level fields: name, budgetGroup, startDate, endDate (same smart/partial semantics as the flat tool — only pushed when different / supplied).\n                - ADD new containers: each channel's `containers[]` APPENDS new WizNativeAdContainers (one audience × an `ad_offers` list of {ad, offer} pairs each, same shape as `create_native_structure_campaign`). Existing containers are PRESERVED — the channel's current container list is re-POSTed with the new ones appended.\n                - ADD bare target groups: each channel's optional `target_groups[]` (names) attaches targeting groups to the native channel without a full container (places targetingGroupId on a reused/appended ad-less container).\n                - ATTACH negative keyword lists (Google Ads / Microsoft Ads only): the search channel block's optional `negative_keyword_lists[]` (names of EXISTING lists) attaches them channel-wide — valid with or without new containers. Names inside `containers[].negative_keyword_lists` are unioned in and applied at the same channel level. A name that doesn't resolve on the channel is reported in `errors[]`, never silently skipped.\n                - EDIT existing containers (PER CONTAINER): each channel's optional `modify_containers[]` targets an existing container by `name` and, within it, deletes specific Ad—Offer rows (`remove_ads`), clears an offer from one ad while keeping it on the OTHER ads (`clear_offers_on_ads`), removes this container's audience / targeting group / keywords / exclusion (`remove_audience` / `remove_target_group` / `remove_keywords` / `remove_exclude_audience`), or deletes the whole container (`delete_container`). This is the surgical counterpart to the channel-wide remove_* tools — use it when an audience/offer/ad is shared across containers and you only want to touch one. Removing a group clears it on that one container; a group reused by other containers stays alive for them.\n\n                WHAT THIS TOOL DOES NOT DO:\n                - It does not switch a campaign's structure (that's fixed at creation).\n                - For a change that spans ALL ads (remove an offer/ad everywhere it appears on the channel), use the channel-wide removal tools (remove_offers_from_campaign / remove_ad_from_campaign / remove_audiences_from_campaign / remove_target_groups_from_campaign / remove_keywords_from_campaign); `modify_containers` is only for a specific container/row.\n\n                CONTAINER RULES (same as create_native_structure_campaign):\n                - Each container is one audience × a LIST of (ad, offer) pairs (`ad_offers`, 1+). Group several ads/offers under one audience by adding pairs — e.g. 3 ads on the same offer = 3 pairs that repeat the same `offer` id. Send two containers only to run the audience as two separate ad-sets.\n                - `audience` / `exclude_audience` / `target_group` are NAMES (resolved server-side); the `ad` / `offer` inside each `ad_offers` pair are integer IDs.\n                - Audience names are resolved BEFORE anything is created: a container whose `audience` cannot be resolved is dropped with an error in `errors[]` that states whether retrying can help (\"may still be matching\") or not (\"not available on this channel\"); no keyword group or other element is left behind for a dropped container.\n                - `audience` is REQUIRED for LinkedIn/Facebook/Instagram/Reddit; OPTIONAL for Google/Microsoft search containers (keyword-only is valid — provide `keywords`/`keyword_ids` instead).\n                - Offer–channel lock still applies: a Lead Gen offer must be locked to the container's channel (LinkedIn→LINKEDIN, etc.); Google takes Landing Page offers only; Landing Page offers are universal.\n\n                RESPONSE: same top-level shape as `add_and_edit_campaign_elements` (success, campaign_id, changes {renamed, budget_group_reassigned, schedule_updated}, added_summary per channel {containers, target_groups, negative_keyword_lists, and — when modify_containers is used — ads_removed, offers_cleared, containers_deleted, audiences_removed, target_groups_removed, keywords_removed, excludes_removed}, optimization_group, campaign_url, full_response). On partial failures, success=false plus `errors[]`; the rest of the edit still proceeds.\n\n                EXAMPLE (append a LinkedIn container + rename):\n                add_and_edit_native_campaign_elements(campaign_data={\n                    \"campaignId\": 159490,\n                    \"name\": \"Q3_ABM_Native_v2\",\n                    \"linkedin\": {\"containers\": [\n                        {\"name\": \"VPs > Demo > Form\", \"audience\": \"VPs - NA\", \"ad_offers\": [{\"ad\": 156502, \"offer\": 57538}]}\n                    ]}\n                })\n\n                EXAMPLE (append a Google keyword-only container + bare target group):\n                add_and_edit_native_campaign_elements(campaign_data={\n                    \"campaignId\": 159490,\n                    \"google\": {\n                        \"containers\": [\n                            {\"name\": \"Incident Response\", \"ad_offers\": [{\"ad\": 210804, \"offer\": 67269}],\n                             \"keywords\": [\"incident response platform\"], \"negative_keyword_lists\": [\"Competitor Brands\"]}\n                        ],\n                        \"target_groups\": [\"Tech Decision Makers\"]\n                    }\n                })\n\n                EXAMPLE (ROW-LEVEL: drop the offer from ONE ad in a container, keep it on the other ads):\n                add_and_edit_native_campaign_elements(campaign_data={\n                    \"campaignId\": 159490,\n                    \"facebook\": {\"modify_containers\": [\n                        {\"container\": \"ICP > 3 creatives > TOF offer\",\n                         \"clear_offers_on_ads\": [\"Ungated-AI-in-Finance-v2-vert_FB\"]}\n                    ]}\n                })\n\n                EXAMPLE (PER-CONTAINER: delete one Ad—Offer row in one container; delete a whole other container):\n                add_and_edit_native_campaign_elements(campaign_data={\n                    \"campaignId\": 159490,\n                    \"linkedin\": {\"modify_containers\": [\n                        {\"container\": \"VPs > Demo > Form\", \"remove_ads\": [\"Old Creative A\"]},\n                        {\"container\": \"Retired ad-set\", \"delete_container\": True}\n                    ]}\n                })\n\n                EXAMPLE (PER-CONTAINER: drop a reused audience from ONE container, keep it on the others):\n                add_and_edit_native_campaign_elements(campaign_data={\n                    \"campaignId\": 159490,\n                    \"facebook\": {\"modify_containers\": [\n                        {\"container\": \"ICP - A2\", \"remove_audience\": True}\n                    ]}\n                })",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_data": {
      "type": "object",
      "description": "Native (N×N×N) campaign edit payload: campaignId + optional campaign-level fields + per-channel `containers[]` (append) and optional `target_groups[]`.",
      "properties": {
       "campaignId": {
        "type": "integer",
        "description": "Campaign Id (numeric identifier) of the existing NATIVE campaign to edit."
       },
       "campaignType": {
        "type": "string",
        "enum": [
         "Lead Gen",
         "Brand Awareness"
        ],
        "description": "Campaign goal (optional; informational — must match the existing campaign)."
       },
       "name": {
        "type": "string",
        "description": "New campaign name — if different from current, the campaign is RENAMED."
       },
       "startDate": {
        "type": "string",
        "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
        "description": "Campaign start date (YYYY-MM-DD) — REPLACES the existing start date."
       },
       "endDate": {
        "type": "string",
        "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
        "description": "Campaign end date (YYYY-MM-DD) — REPLACES the existing end date."
       },
       "budgetGroup": {
        "type": "string",
        "description": "Budget group name — if different from current, the campaign is REASSIGNED to it."
       },
       "linkedin": {
        "type": "object",
        "description": "LinkedIn edits: append `containers[]`, edit existing ones via `modify_containers[]`, add bare `target_groups[]`, and/or set the channel's `dailyBudget`.",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps its current value (the platform default is $50/day). Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "containers": {
          "type": "array",
          "minItems": 1,
          "items": {
           "type": "object",
           "properties": {
            "name": {
             "type": "string",
             "maxLength": 255,
             "description": "Container/ad-set name shown in the platform UI."
            },
            "audience": {
             "type": "string",
             "description": "Audience name (resolved server-side). REQUIRED for LinkedIn."
            },
            "ad_offers": {
             "type": "array",
             "minItems": 1,
             "description": "Ad—Offer pairs for this container (the native ad-set's Ad/Offer rows). Each pair renders one ad with one offer; every pair shares this container's single audience. To run 3 ads against the same offer, send 3 pairs that repeat the same `offer` id.",
             "items": {
              "type": "object",
              "properties": {
               "ad": {
                "type": "integer",
                "description": "WizCreative ID from create_update_image_ad / create_update_video_ad."
               },
               "offer": {
                "type": "integer",
                "description": "Offer ID from create_update_offer. Lead Gen offers must be channel-locked; Google takes Landing Page offers only; Landing Page offers are universal."
               }
              },
              "required": [
               "ad",
               "offer"
              ]
             }
            },
            "target_group": {
             "type": "string",
             "description": "Optional Targeting Group name for this container."
            },
            "exclude_audience": {
             "type": "string",
             "description": "Optional audience name to use as the container's exclude group."
            }
           },
           "required": [
            "name",
            "audience",
            "ad_offers"
           ]
          }
         },
         "modify_containers": {
          "type": "array",
          "minItems": 1,
          "description": "Edit EXISTING containers in place: delete specific Ad—Offer rows (`remove_ads`), clear an offer from one ad while keeping it on others (`clear_offers_on_ads`), remove this container's audience / targeting group / keywords / exclusion (`remove_audience` / `remove_target_group` / `remove_keywords` / `remove_exclude_audience` — clears it on THIS container only, leaving any container that reuses it untouched), or delete a whole container (`delete_container`). Target each by its `container` name. Use this for surgical per-container changes; use the channel-wide remove_* tools only to act on ALL containers at once.",
          "items": {
           "type": "object",
           "properties": {
            "container": {
             "type": "string",
             "description": "Name of the existing container / ad-set to edit (the name shown in the platform UI and returned at create time). If several containers share the name, the edit applies to all of them."
            },
            "remove_ads": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Ad names whose Ad—Offer rows to DELETE from this container (the ad and its paired offer go together). Other rows are untouched."
            },
            "clear_offers_on_ads": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Ad names whose offer to CLEAR while keeping the ad (empties that row's offer slot). Use this to drop an offer from one ad while other ads keep the same offer."
            },
            "remove_audience": {
             "type": "boolean",
             "description": "Remove this container's audience (clears its audience group). Use this to drop an audience from ONE container when the same audience is reused across several — the others keep theirs. Default false."
            },
            "remove_target_group": {
             "type": "boolean",
             "description": "Remove this container's Targeting Group (clears targetingGroupId). Default false."
            },
            "remove_keywords": {
             "type": "boolean",
             "description": "Remove this container's keyword group (clears keywordGroupId; search channels). Default false."
            },
            "remove_exclude_audience": {
             "type": "boolean",
             "description": "Remove this container's audience exclusion (clears its exclude group). Default false."
            },
            "delete_container": {
             "type": "boolean",
             "description": "Delete the entire container / ad-set with all its rows. Default false."
            }
           },
           "required": [
            "container"
           ]
          }
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Targeting Group names to attach to the channel without a full container (bare target-group add)."
         },
         "exclude_audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Audience names to EXCLUDE channel-wide — set as groupExcludeId on EVERY container on the channel (overwriting any existing per-container exclude). For per-container exclusions, use containers[].exclude_audience instead."
         }
        }
       },
       "facebook": {
        "type": "object",
        "description": "Facebook edits: append `containers[]`, edit existing ones via `modify_containers[]`, add bare `target_groups[]`, and/or set the channel's `dailyBudget`.",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps its current value (the platform default is $50/day). Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "containers": {
          "type": "array",
          "minItems": 1,
          "items": {
           "type": "object",
           "properties": {
            "name": {
             "type": "string",
             "maxLength": 255,
             "description": "Container/ad-set name shown in the platform UI."
            },
            "audience": {
             "type": "string",
             "description": "Audience name (resolved server-side). REQUIRED for Facebook."
            },
            "ad_offers": {
             "type": "array",
             "minItems": 1,
             "description": "Ad—Offer pairs for this container (the native ad-set's Ad/Offer rows). Each pair renders one ad with one offer; every pair shares this container's single audience. To run 3 ads against the same offer, send 3 pairs that repeat the same `offer` id.",
             "items": {
              "type": "object",
              "properties": {
               "ad": {
                "type": "integer",
                "description": "WizCreative ID from create_update_image_ad / create_update_video_ad."
               },
               "offer": {
                "type": "integer",
                "description": "Offer ID from create_update_offer. Lead Gen offers must be channel-locked; Google takes Landing Page offers only; Landing Page offers are universal."
               }
              },
              "required": [
               "ad",
               "offer"
              ]
             }
            },
            "target_group": {
             "type": "string",
             "description": "Optional Targeting Group name for this container."
            },
            "exclude_audience": {
             "type": "string",
             "description": "Optional audience name to use as the container's exclude group."
            }
           },
           "required": [
            "name",
            "audience",
            "ad_offers"
           ]
          }
         },
         "modify_containers": {
          "type": "array",
          "minItems": 1,
          "description": "Edit EXISTING containers in place: delete specific Ad—Offer rows (`remove_ads`), clear an offer from one ad while keeping it on others (`clear_offers_on_ads`), remove this container's audience / targeting group / keywords / exclusion (`remove_audience` / `remove_target_group` / `remove_keywords` / `remove_exclude_audience` — clears it on THIS container only, leaving any container that reuses it untouched), or delete a whole container (`delete_container`). Target each by its `container` name. Use this for surgical per-container changes; use the channel-wide remove_* tools only to act on ALL containers at once.",
          "items": {
           "type": "object",
           "properties": {
            "container": {
             "type": "string",
             "description": "Name of the existing container / ad-set to edit (the name shown in the platform UI and returned at create time). If several containers share the name, the edit applies to all of them."
            },
            "remove_ads": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Ad names whose Ad—Offer rows to DELETE from this container (the ad and its paired offer go together). Other rows are untouched."
            },
            "clear_offers_on_ads": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Ad names whose offer to CLEAR while keeping the ad (empties that row's offer slot). Use this to drop an offer from one ad while other ads keep the same offer."
            },
            "remove_audience": {
             "type": "boolean",
             "description": "Remove this container's audience (clears its audience group). Use this to drop an audience from ONE container when the same audience is reused across several — the others keep theirs. Default false."
            },
            "remove_target_group": {
             "type": "boolean",
             "description": "Remove this container's Targeting Group (clears targetingGroupId). Default false."
            },
            "remove_keywords": {
             "type": "boolean",
             "description": "Remove this container's keyword group (clears keywordGroupId; search channels). Default false."
            },
            "remove_exclude_audience": {
             "type": "boolean",
             "description": "Remove this container's audience exclusion (clears its exclude group). Default false."
            },
            "delete_container": {
             "type": "boolean",
             "description": "Delete the entire container / ad-set with all its rows. Default false."
            }
           },
           "required": [
            "container"
           ]
          }
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Targeting Group names to attach to the channel without a full container (bare target-group add)."
         },
         "exclude_audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Audience names to EXCLUDE channel-wide — set as groupExcludeId on EVERY container on the channel (overwriting any existing per-container exclude). For per-container exclusions, use containers[].exclude_audience instead."
         }
        }
       },
       "instagram": {
        "type": "object",
        "description": "Instagram edits: append `containers[]`, edit existing ones via `modify_containers[]`, add bare `target_groups[]`, and/or set the channel's `dailyBudget`.",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps its current value (the platform default is $50/day). Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "containers": {
          "type": "array",
          "minItems": 1,
          "items": {
           "type": "object",
           "properties": {
            "name": {
             "type": "string",
             "maxLength": 255,
             "description": "Container/ad-set name shown in the platform UI."
            },
            "audience": {
             "type": "string",
             "description": "Audience name (resolved server-side). REQUIRED for Instagram."
            },
            "ad_offers": {
             "type": "array",
             "minItems": 1,
             "description": "Ad—Offer pairs for this container (the native ad-set's Ad/Offer rows). Each pair renders one ad with one offer; every pair shares this container's single audience. To run 3 ads against the same offer, send 3 pairs that repeat the same `offer` id.",
             "items": {
              "type": "object",
              "properties": {
               "ad": {
                "type": "integer",
                "description": "WizCreative ID from create_update_image_ad / create_update_video_ad."
               },
               "offer": {
                "type": "integer",
                "description": "Offer ID from create_update_offer. Lead Gen offers must be channel-locked; Google takes Landing Page offers only; Landing Page offers are universal."
               }
              },
              "required": [
               "ad",
               "offer"
              ]
             }
            },
            "target_group": {
             "type": "string",
             "description": "Optional Targeting Group name for this container."
            },
            "exclude_audience": {
             "type": "string",
             "description": "Optional audience name to use as the container's exclude group."
            }
           },
           "required": [
            "name",
            "audience",
            "ad_offers"
           ]
          }
         },
         "modify_containers": {
          "type": "array",
          "minItems": 1,
          "description": "Edit EXISTING containers in place: delete specific Ad—Offer rows (`remove_ads`), clear an offer from one ad while keeping it on others (`clear_offers_on_ads`), remove this container's audience / targeting group / keywords / exclusion (`remove_audience` / `remove_target_group` / `remove_keywords` / `remove_exclude_audience` — clears it on THIS container only, leaving any container that reuses it untouched), or delete a whole container (`delete_container`). Target each by its `container` name. Use this for surgical per-container changes; use the channel-wide remove_* tools only to act on ALL containers at once.",
          "items": {
           "type": "object",
           "properties": {
            "container": {
             "type": "string",
             "description": "Name of the existing container / ad-set to edit (the name shown in the platform UI and returned at create time). If several containers share the name, the edit applies to all of them."
            },
            "remove_ads": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Ad names whose Ad—Offer rows to DELETE from this container (the ad and its paired offer go together). Other rows are untouched."
            },
            "clear_offers_on_ads": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Ad names whose offer to CLEAR while keeping the ad (empties that row's offer slot). Use this to drop an offer from one ad while other ads keep the same offer."
            },
            "remove_audience": {
             "type": "boolean",
             "description": "Remove this container's audience (clears its audience group). Use this to drop an audience from ONE container when the same audience is reused across several — the others keep theirs. Default false."
            },
            "remove_target_group": {
             "type": "boolean",
             "description": "Remove this container's Targeting Group (clears targetingGroupId). Default false."
            },
            "remove_keywords": {
             "type": "boolean",
             "description": "Remove this container's keyword group (clears keywordGroupId; search channels). Default false."
            },
            "remove_exclude_audience": {
             "type": "boolean",
             "description": "Remove this container's audience exclusion (clears its exclude group). Default false."
            },
            "delete_container": {
             "type": "boolean",
             "description": "Delete the entire container / ad-set with all its rows. Default false."
            }
           },
           "required": [
            "container"
           ]
          }
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Targeting Group names to attach to the channel without a full container (bare target-group add)."
         },
         "exclude_audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Audience names to EXCLUDE channel-wide — set as groupExcludeId on EVERY container on the channel (overwriting any existing per-container exclude). For per-container exclusions, use containers[].exclude_audience instead."
         }
        }
       },
       "reddit": {
        "type": "object",
        "description": "Reddit edits: append `containers[]`, edit existing ones via `modify_containers[]`, add bare `target_groups[]`, and/or set the channel's `dailyBudget`.",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps its current value (the platform default is $50/day). Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "containers": {
          "type": "array",
          "minItems": 1,
          "items": {
           "type": "object",
           "properties": {
            "name": {
             "type": "string",
             "maxLength": 255,
             "description": "Container/ad-set name shown in the platform UI."
            },
            "audience": {
             "type": "string",
             "description": "Audience name (resolved server-side). OPTIONAL for Reddit: a container may target a `target_group` instead. Name an audience only when Reddit is connected AND the audience already exists on it — the platform never distributes an audience to a channel the account has not connected, so on a disconnected channel the Targeting Group is the only targeting that can work."
            },
            "ad_offers": {
             "type": "array",
             "minItems": 1,
             "description": "Ad—Offer pairs for this container (the native ad-set's Ad/Offer rows). Each pair renders one ad with one offer; every pair shares this container's single audience. To run 3 ads against the same offer, send 3 pairs that repeat the same `offer` id.",
             "items": {
              "type": "object",
              "properties": {
               "ad": {
                "type": "integer",
                "description": "WizCreative ID from create_update_image_ad / create_update_video_ad."
               },
               "offer": {
                "type": "integer",
                "description": "Offer ID from create_update_offer. Lead Gen offers must be channel-locked; Google takes Landing Page offers only; Landing Page offers are universal."
               }
              },
              "required": [
               "ad",
               "offer"
              ]
             }
            },
            "target_group": {
             "type": "string",
             "description": "Targeting Group name for this container (created via create_reddit_target_group for Reddit, create_target_group for LinkedIn). Targets the container on its own — no audience needed — and needs no channel connection, so it is the way to build a container on a channel that is not connected yet."
            },
            "exclude_audience": {
             "type": "string",
             "description": "Optional audience name to use as the container's exclude group."
            }
           },
           "required": [
            "name",
            "ad_offers"
           ]
          }
         },
         "modify_containers": {
          "type": "array",
          "minItems": 1,
          "description": "Edit EXISTING containers in place: delete specific Ad—Offer rows (`remove_ads`), clear an offer from one ad while keeping it on others (`clear_offers_on_ads`), remove this container's audience / targeting group / keywords / exclusion (`remove_audience` / `remove_target_group` / `remove_keywords` / `remove_exclude_audience` — clears it on THIS container only, leaving any container that reuses it untouched), or delete a whole container (`delete_container`). Target each by its `container` name. Use this for surgical per-container changes; use the channel-wide remove_* tools only to act on ALL containers at once.",
          "items": {
           "type": "object",
           "properties": {
            "container": {
             "type": "string",
             "description": "Name of the existing container / ad-set to edit (the name shown in the platform UI and returned at create time). If several containers share the name, the edit applies to all of them."
            },
            "remove_ads": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Ad names whose Ad—Offer rows to DELETE from this container (the ad and its paired offer go together). Other rows are untouched."
            },
            "clear_offers_on_ads": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Ad names whose offer to CLEAR while keeping the ad (empties that row's offer slot). Use this to drop an offer from one ad while other ads keep the same offer."
            },
            "remove_audience": {
             "type": "boolean",
             "description": "Remove this container's audience (clears its audience group). Use this to drop an audience from ONE container when the same audience is reused across several — the others keep theirs. Default false."
            },
            "remove_target_group": {
             "type": "boolean",
             "description": "Remove this container's Targeting Group (clears targetingGroupId). Default false."
            },
            "remove_keywords": {
             "type": "boolean",
             "description": "Remove this container's keyword group (clears keywordGroupId; search channels). Default false."
            },
            "remove_exclude_audience": {
             "type": "boolean",
             "description": "Remove this container's audience exclusion (clears its exclude group). Default false."
            },
            "delete_container": {
             "type": "boolean",
             "description": "Delete the entire container / ad-set with all its rows. Default false."
            }
           },
           "required": [
            "container"
           ]
          }
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Targeting Group names to attach to the channel without a full container (bare target-group add)."
         },
         "exclude_audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Audience names to EXCLUDE channel-wide — set as groupExcludeId on EVERY container on the channel (overwriting any existing per-container exclude). For per-container exclusions, use containers[].exclude_audience instead."
         }
        }
       },
       "google": {
        "type": "object",
        "description": "Google Ads edits: append `containers[]`, edit existing ones via `modify_containers[]`, add bare `target_groups[]`, and/or set the channel's `dailyBudget`.",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps its current value (the platform default is $50/day). Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "containers": {
          "type": "array",
          "minItems": 1,
          "items": {
           "type": "object",
           "properties": {
            "name": {
             "type": "string",
             "maxLength": 255,
             "description": "Container/ad-set name shown in the platform UI."
            },
            "audience": {
             "type": "string",
             "description": "Audience name (resolved server-side). OPTIONAL for Google Ads search containers — keyword-only targeting is valid."
            },
            "ad_offers": {
             "type": "array",
             "minItems": 1,
             "description": "Ad—Offer pairs for this container (the native ad-set's Ad/Offer rows). Each pair renders one ad with one offer; every pair shares this container's single audience. To run 3 ads against the same offer, send 3 pairs that repeat the same `offer` id.",
             "items": {
              "type": "object",
              "properties": {
               "ad": {
                "type": "integer",
                "description": "WizCreative ID from create_update_image_ad / create_update_video_ad."
               },
               "offer": {
                "type": "integer",
                "description": "Offer ID from create_update_offer. Lead Gen offers must be channel-locked; Google takes Landing Page offers only; Landing Page offers are universal."
               }
              },
              "required": [
               "ad",
               "offer"
              ]
             }
            },
            "target_group": {
             "type": "string",
             "description": "Optional Targeting Group name for this container."
            },
            "exclude_audience": {
             "type": "string",
             "description": "Optional audience name to use as the container's exclude group."
            },
            "keywords": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Free-text keyword phrases for this container's keyword group. A search container with neither an audience nor keywords is rejected."
            },
            "keyword_ids": {
             "type": "array",
             "items": {
              "type": "integer"
             },
             "description": "Existing keyword-library IDs (from list_keywords) to include in this container's keyword group; unioned with `keywords`."
            },
            "match_type": {
             "type": "string",
             "enum": [
              "PHRASE",
              "BROAD",
              "EXACT"
             ],
             "description": "Match type for this container's keyword group (default PHRASE)."
            },
            "negative_keyword_lists": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Names of existing negative keyword lists to attach channel-wide."
            }
           },
           "required": [
            "name",
            "ad_offers"
           ]
          }
         },
         "modify_containers": {
          "type": "array",
          "minItems": 1,
          "description": "Edit EXISTING containers in place: delete specific Ad—Offer rows (`remove_ads`), clear an offer from one ad while keeping it on others (`clear_offers_on_ads`), remove this container's audience / targeting group / keywords / exclusion (`remove_audience` / `remove_target_group` / `remove_keywords` / `remove_exclude_audience` — clears it on THIS container only, leaving any container that reuses it untouched), or delete a whole container (`delete_container`). Target each by its `container` name. Use this for surgical per-container changes; use the channel-wide remove_* tools only to act on ALL containers at once.",
          "items": {
           "type": "object",
           "properties": {
            "container": {
             "type": "string",
             "description": "Name of the existing container / ad-set to edit (the name shown in the platform UI and returned at create time). If several containers share the name, the edit applies to all of them."
            },
            "remove_ads": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Ad names whose Ad—Offer rows to DELETE from this container (the ad and its paired offer go together). Other rows are untouched."
            },
            "clear_offers_on_ads": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Ad names whose offer to CLEAR while keeping the ad (empties that row's offer slot). Use this to drop an offer from one ad while other ads keep the same offer."
            },
            "remove_audience": {
             "type": "boolean",
             "description": "Remove this container's audience (clears its audience group). Use this to drop an audience from ONE container when the same audience is reused across several — the others keep theirs. Default false."
            },
            "remove_target_group": {
             "type": "boolean",
             "description": "Remove this container's Targeting Group (clears targetingGroupId). Default false."
            },
            "remove_keywords": {
             "type": "boolean",
             "description": "Remove this container's keyword group (clears keywordGroupId; search channels). Default false."
            },
            "remove_exclude_audience": {
             "type": "boolean",
             "description": "Remove this container's audience exclusion (clears its exclude group). Default false."
            },
            "delete_container": {
             "type": "boolean",
             "description": "Delete the entire container / ad-set with all its rows. Default false."
            }
           },
           "required": [
            "container"
           ]
          }
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Targeting Group names to attach to the channel without a full container (bare target-group add)."
         },
         "exclude_audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Audience names to EXCLUDE channel-wide — set as groupExcludeId on EVERY container on the channel (overwriting any existing per-container exclude). For per-container exclusions, use containers[].exclude_audience instead."
         },
         "negative_keyword_lists": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Names of existing negative keyword lists to attach to the Google Ads channel (channel-wide — negative lists are channel-level on the platform). Valid with or without new containers. Names must match existing lists (use list_negative_keywords_list to find them)."
         },
         "locations": {
          "type": "object",
          "description": "Where the Google Ads campaign SERVES — the UI's Location picker. ⚠️ OMITTING THIS LEAVES THE PLATFORM DEFAULT OF UNITED STATES. Set it whenever the user names a geography.",
          "properties": {
           "countries": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "Country NAMES to target, e.g. [\"United States\", \"Canada\"]. Common shorthand resolves (\"USA\", \"UK\"). An unrecognised name is rejected, not skipped."
           },
           "states": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "US state NAMES to target, e.g. [\"Texas\", \"California\"]. US-only. Naming states without countries keeps the channel's current countries."
           }
          },
          "additionalProperties": false
         },
         "excluded_locations": {
          "type": "object",
          "description": "Where the Google Ads campaign must NOT serve — the UI's Negative Locations editor under Advanced Settings. ⚠️ COUNTRY-LEVEL ONLY: the platform stores exclusions as bare location ids with no country/state discriminator and state ids overlap country ids, so an excluded state would be read back as an unrelated country. To narrow within a country, target the states you want via `locations.states` instead.",
          "properties": {
           "countries": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "Country NAMES to exclude, e.g. [\"India\", \"Pakistan\"]."
           }
          },
          "additionalProperties": false
         }
        }
       },
       "microsoft": {
        "type": "object",
        "description": "Microsoft Ads edits: append `containers[]`, edit existing ones via `modify_containers[]`, add bare `target_groups[]`, and/or set the channel's `dailyBudget`.",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps its current value (the platform default is $50/day). Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "containers": {
          "type": "array",
          "minItems": 1,
          "items": {
           "type": "object",
           "properties": {
            "name": {
             "type": "string",
             "maxLength": 255,
             "description": "Container/ad-set name shown in the platform UI."
            },
            "audience": {
             "type": "string",
             "description": "Audience name (resolved server-side). OPTIONAL for Microsoft Ads search containers — keyword-only targeting is valid."
            },
            "ad_offers": {
             "type": "array",
             "minItems": 1,
             "description": "Ad—Offer pairs for this container (the native ad-set's Ad/Offer rows). Each pair renders one ad with one offer; every pair shares this container's single audience. To run 3 ads against the same offer, send 3 pairs that repeat the same `offer` id.",
             "items": {
              "type": "object",
              "properties": {
               "ad": {
                "type": "integer",
                "description": "WizCreative ID from create_update_image_ad / create_update_video_ad."
               },
               "offer": {
                "type": "integer",
                "description": "Offer ID from create_update_offer. Lead Gen offers must be channel-locked; Google takes Landing Page offers only; Landing Page offers are universal."
               }
              },
              "required": [
               "ad",
               "offer"
              ]
             }
            },
            "target_group": {
             "type": "string",
             "description": "Optional Targeting Group name for this container."
            },
            "exclude_audience": {
             "type": "string",
             "description": "Optional audience name to use as the container's exclude group."
            },
            "keywords": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Free-text keyword phrases for this container's keyword group. A search container with neither an audience nor keywords is rejected."
            },
            "keyword_ids": {
             "type": "array",
             "items": {
              "type": "integer"
             },
             "description": "Existing keyword-library IDs (from list_keywords) to include in this container's keyword group; unioned with `keywords`."
            },
            "match_type": {
             "type": "string",
             "enum": [
              "PHRASE",
              "BROAD",
              "EXACT"
             ],
             "description": "Match type for this container's keyword group (default PHRASE)."
            },
            "negative_keyword_lists": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Names of existing negative keyword lists to attach channel-wide."
            }
           },
           "required": [
            "name",
            "ad_offers"
           ]
          }
         },
         "modify_containers": {
          "type": "array",
          "minItems": 1,
          "description": "Edit EXISTING containers in place: delete specific Ad—Offer rows (`remove_ads`), clear an offer from one ad while keeping it on others (`clear_offers_on_ads`), remove this container's audience / targeting group / keywords / exclusion (`remove_audience` / `remove_target_group` / `remove_keywords` / `remove_exclude_audience` — clears it on THIS container only, leaving any container that reuses it untouched), or delete a whole container (`delete_container`). Target each by its `container` name. Use this for surgical per-container changes; use the channel-wide remove_* tools only to act on ALL containers at once.",
          "items": {
           "type": "object",
           "properties": {
            "container": {
             "type": "string",
             "description": "Name of the existing container / ad-set to edit (the name shown in the platform UI and returned at create time). If several containers share the name, the edit applies to all of them."
            },
            "remove_ads": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Ad names whose Ad—Offer rows to DELETE from this container (the ad and its paired offer go together). Other rows are untouched."
            },
            "clear_offers_on_ads": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Ad names whose offer to CLEAR while keeping the ad (empties that row's offer slot). Use this to drop an offer from one ad while other ads keep the same offer."
            },
            "remove_audience": {
             "type": "boolean",
             "description": "Remove this container's audience (clears its audience group). Use this to drop an audience from ONE container when the same audience is reused across several — the others keep theirs. Default false."
            },
            "remove_target_group": {
             "type": "boolean",
             "description": "Remove this container's Targeting Group (clears targetingGroupId). Default false."
            },
            "remove_keywords": {
             "type": "boolean",
             "description": "Remove this container's keyword group (clears keywordGroupId; search channels). Default false."
            },
            "remove_exclude_audience": {
             "type": "boolean",
             "description": "Remove this container's audience exclusion (clears its exclude group). Default false."
            },
            "delete_container": {
             "type": "boolean",
             "description": "Delete the entire container / ad-set with all its rows. Default false."
            }
           },
           "required": [
            "container"
           ]
          }
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Targeting Group names to attach to the channel without a full container (bare target-group add)."
         },
         "exclude_audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Audience names to EXCLUDE channel-wide — set as groupExcludeId on EVERY container on the channel (overwriting any existing per-container exclude). For per-container exclusions, use containers[].exclude_audience instead."
         },
         "negative_keyword_lists": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Names of existing negative keyword lists to attach to the Microsoft Ads channel (channel-wide — negative lists are channel-level on the platform). Valid with or without new containers. Names must match existing lists (use list_negative_keywords_list to find them)."
         },
         "locations": {
          "type": "object",
          "description": "Where the Microsoft Ads campaign SERVES — the UI's Location picker. ⚠️ OMITTING THIS LEAVES THE PLATFORM DEFAULT OF UNITED STATES. Set it whenever the user names a geography.",
          "properties": {
           "countries": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "Country NAMES to target, e.g. [\"United States\", \"Canada\"]. Common shorthand resolves (\"USA\", \"UK\"). An unrecognised name is rejected, not skipped."
           },
           "states": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "US state NAMES to target, e.g. [\"Texas\", \"California\"]. US-only. Naming states without countries keeps the channel's current countries."
           }
          },
          "additionalProperties": false
         },
         "excluded_locations": {
          "type": "object",
          "description": "Where the Microsoft Ads campaign must NOT serve — the UI's Negative Locations editor under Advanced Settings. ⚠️ COUNTRY-LEVEL ONLY: the platform stores exclusions as bare location ids with no country/state discriminator and state ids overlap country ids, so an excluded state would be read back as an unrelated country. To narrow within a country, target the states you want via `locations.states` instead.",
          "properties": {
           "countries": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "Country NAMES to exclude, e.g. [\"India\", \"Pakistan\"]."
           }
          },
          "additionalProperties": false
         }
        }
       }
      },
      "required": [
       "campaignId"
      ],
      "additionalProperties": false
     }
    },
    "required": [
     "campaign_data"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "check_campaign_launch_readiness",
   "title": "Check Campaign Launch Readiness",
   "category": "campaigns",
   "risk": "read",
   "summary": "Check whether a draft campaign is ready to launch, per channel.",
   "description": "Check whether a draft campaign is ready to launch, per channel. Reads the campaign's real configuration from the platform and reports, for each ENABLED channel, whether it has the ads, audience, offer, budget AND a live channel connection it needs to go live - plus a specific list of blockers for anything missing.\n\n                KEYWORDS: campaign, launch, readiness, ready, preflight, can launch, blockers, missing, go live, validate, checklist\n\n                WHEN TO USE:\n                - Before launching a campaign, to confirm it is actually ready\n                - After assembling or editing a draft, as the go/no-go check\n                - When the user asks \"is my campaign ready to launch?\" or \"what's missing?\"\n\n                RETURNS:\n                A per-channel readiness report:\n                - is_launchable: whether EVERY enabled channel is ready\n                - verified: whether the platform's pre-launch check actually answered. When false,\n                  is_launchable is an assumption, NOT a verdict - say the check could not be run\n                  instead of telling the user the campaign is ready.\n                - channels: [{channel, launchable, blockers[], ads, audiences, offers, budget}]\n                - summary: a one-line human verdict\n                This is a READ-ONLY check - it does NOT launch anything. Use launch_campaign to actually launch.\n\n                CHANNEL CONNECTION: a channel with no live integration is reported as a blocker here, and the\n                platform will refuse the launch itself. This is the ONLY point in a build where a missing\n                connection matters - audiences, targeting, offers, creative and the draft campaign are all\n                built without one on purpose. So do NOT ask the user to connect anything earlier; raise it\n                here, name the channels, and offer to connect them.\n\n                PARAMETERS:\n                - campaign_id: The wizard campaign ID (integer, required)\n\n                CAMPAIGN ID SOURCES:\n                - search_campaigns_by_names tool (returns campaign IDs)\n                - create_campaign tool response (returns campaign_id)",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The wizard campaign ID to check launch readiness for",
      "minimum": 1
     }
    },
    "required": [
     "campaign_id"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "connect_channel",
   "title": "Connect Channel",
   "category": "campaigns",
   "risk": "write",
   "summary": "Get the OAuth redirect URL to connect a specific advertising channel to the Metadata platform.",
   "description": "Get the OAuth redirect URL to connect a specific advertising channel to the Metadata platform.\n\n                    USAGE INSTRUCTIONS FOR LLM:\n                    Use this tool to obtain the authorization URL that users need to visit to connect their advertising channel accounts.\n                    This tool returns a redirect URL that initiates the OAuth flow for the selected channel.\n\n                    WHEN TO USE THIS TOOL:\n                    - User asks to \"connect [channel name]\"\n                    - User wants to \"add [channel] integration\"\n                    - User needs to \"link [channel] account\"\n                    - User wants to \"authorize [channel] access\"\n                    - User asks \"how do I connect my Facebook/LinkedIn/Twitter/Reddit/Google account?\"\n                    - User needs to \"set up [channel] connection\"\n                    - After disconnecting a channel and wanting to reconnect\n                    - User asks \"give me the link to connect [channel]\"\n\n                    SUPPORTED CHANNELS:\n                    - linkedin: Connect LinkedIn advertising account\n                    - meta: Connect Facebook/Instagram advertising accounts (Meta Business Suite)\n                    - google: Connect Google Ads account\n                    - reddit: Connect Reddit advertising account\n                    - x: Connect X (Twitter) advertising account\n\n                    IMPORTANT WORKFLOW - MUST FOLLOW THESE STEPS:\n                    1. Call this tool to get the OAuth redirect URL\n                    2. Display the URL to the user clearly and instruct them to open it in their browser\n                    3. Explain that they need to complete the authorization process on the channel's website\n                    4. IMPORTANT: Ask the user to return to the chat and confirm when they have completed the connection\n                    5. WAIT for the user's confirmation before proceeding with any other tasks\n                    6. Once the user confirms the connection is complete, acknowledge it and continue\n\n                    REQUIRED USER INTERACTION:\n                    - The user MUST visit the returned URL in their browser to complete OAuth authorization\n                    - The URL will redirect to the channel's authorization page\n                    - After authorization, the user will be redirected back to Metadata platform\n                    - The connection will be active once the OAuth flow is completed successfully\n                    - YOU MUST WAIT for user confirmation before proceeding\n\n                    COMMON SCENARIOS:\n\n                    Scenario 1 - Connect new channel: \"I want to connect my LinkedIn account\"\n                    → Call connect_channel with channel=\"linkedin\"\n                    → Show URL to user: \"Please open this URL to connect your LinkedIn account: [URL]\"\n                    → \"After completing the authorization, please return here and let me know it's done so we can proceed.\"\n                    → WAIT for user confirmation\n\n                    Scenario 2 - Reconnect after disconnect: \"Get me the link to reconnect Facebook\"\n                    → Call connect_channel with channel=\"meta\"\n                    → Show URL to user with instructions\n                    → WAIT for user confirmation\n\n                    Scenario 3 - Setup new integration: \"How do I add Google Ads?\"\n                    → Call connect_channel with channel=\"google\"\n                    → Explain OAuth process and provide URL\n                    → Ask user to confirm when complete\n                    → WAIT for user confirmation\n\n                    Scenario 4 - Add Twitter/X: \"Connect my X account\"\n                    → Call connect_channel with channel=\"x\"\n                    → Provide the URL to user\n                    → WAIT for user confirmation\n\n                    RESPONSE FORMAT:\n                    Returns an object containing the redirect URL for OAuth authorization.\n                    Example: {\"redirectUrl\": \"https://oauth.channel.com/authorize?client_id=...\"}\n\n                    EXAMPLE: \"Connect LinkedIn\" → connect_channel(channel=\"linkedin\")",
   "inputSchema": {
    "type": "object",
    "properties": {
     "channel": {
      "type": "string",
      "enum": [
       "linkedin",
       "meta",
       "google",
       "reddit",
       "x",
       "facebook",
       "instagram",
       "twitter"
      ],
      "description": "The channel to connect. Must be one of: linkedin, meta (for Facebook/Instagram), google, reddit, x (for Twitter)"
     },
     "acknowledge_data_access": {
      "type": "boolean",
      "description": "Set true to confirm the person you are acting for asked to connect this channel. The response is an OAuth authorization URL that starts a credential grant into their Metadata account. Without this the call is refused."
     }
    },
    "required": [
     "channel"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_budget_group",
   "title": "Create or Update Budget Group",
   "category": "campaigns",
   "risk": "write",
   "summary": "Create or update advertising budget and spending allocation.",
   "description": "Create or update advertising budget and spending allocation.\n                                Set up budget groups to control how much money campaigns can spend.\n                ALSO KNOWN AS: set budget, allocate spend, create spending plan, budget allocation, ad budget, campaign budget\n\n                KEYWORDS: budget, spend, spending, money, dollars, $, quarter, monthly, allocation, cost, funds, cap, limit\n\n                SUPPORTED TYPES:\n                - Lead Generation (default): goal=CPL, groupMetric=CPL, optimizerFormula=CPL_2 (or CPC_2), autoPauseConfigurationId=14, enableBooster=true. Requires benchmark, groupMetric, startDate, endDate.\n                - Brand Awareness: goal=CTR, autoPauseConfigurationId=9, enableBooster=false, budgetType=MONTHLY_RESET. Two formula variants:\n                  • CPC variant: groupMetric=CPC, optimizerFormula=CPC_2 (benchmark is a CPC target, e.g. 10)\n                  • CTR variant: groupMetric=CTR, optimizerFormula=CTR (benchmark is a CTR target in basis-points style, e.g. 10000)\n                  budgetRedistributionStrategy may be PERFORMANCE or PACING_ONLY. Omit startDate/endDate.\n\n                ⚠️ BUDGET-GROUP TYPE MUST FOLLOW THE CAMPAIGN GOAL (do not mix):\n                - A CPL / Lead Generation campaign (campaignType \"Lead Gen\") REQUIRES a Lead Generation (CPL) budget group: goal=CPL, groupMetric=CPL, optimizerFormula=CPL_2, enableBooster=true, autoPauseConfigurationId=14.\n                - A Brand Awareness campaign (campaignType \"Brand Awareness\") REQUIRES a Brand Awareness budget group: goal=CTR, enableBooster=false, budgetType=MONTHLY_RESET, autoPauseConfigurationId=9 (CPC or CTR formula variant per SUPPORTED TYPES above).\n                - NEVER attach a Brand-Awareness (CTR) budget group to a CPL campaign, or a Lead-Generation (CPL) budget group to a Brand Awareness campaign — the optimizer goal must match the campaign's objective. If the campaign goal is unknown, confirm it before creating the budget group.\n\n                To UPDATE: include 'id' field with the budget group ID.\n                To CREATE: omit the 'id' field.\n                Pass all fields inside the `data` object. Dates must be ISO 8601 UTC with exactly 3 ms digits, e.g. 2026-01-15T12:00:00.000Z (format example only — compute the real values).\n\n                ⚠️ CRITICAL DATE RULES:\n                - You do NOT inherently know today's date. If you are not already certain of it, call get_current_date FIRST and anchor every rule below to that real value — never guess.\n                - The endDate MUST ALWAYS be in the future (strictly after today's real date).\n                - NEVER set an endDate that is today or in the past — this will cause the budget group to be immediately expired.\n                - \"this month\" → endDate = the last day of the current month. \"this quarter\" → endDate = the last day of the current quarter. \"next month\" / \"next quarter\" → compute relative to today's real date.\n                - If the user provides a specific end date that is in the past, WARN THEM and ask for a valid future date. Do NOT submit a past endDate.\n                - startDate can be today or in the future, but never in the past for new budget groups.\n\n                REMARKS:\n                - If the user doesn't EXPLICITLY states that their budget is by month or MONTHLY, then use FIXED_BUDGET as budgetType.\n                - In other words, the default value is FIXED_BUDGET unless the user explicitly says MONTHLY or BY MONTH.\n                - IF the user says \"this month\" then also use FIXED_BUDGET and start date should be today, end date should be the last day of the month.\n                - When you use FIXED_BUDGET (fixed-date) but the user's timing expectations/goals are NOT clear, ASK for an explicit start-date and end-date before creating — do not silently invent a date range. Only skip the question when the dates are already unambiguous (e.g. the user gave a range, or said \"this month\"/\"this quarter\").\n                - monthlyCap is the user's money: when the user has NOT explicitly stated a budget / monthly cap (or confirmed a figure you proposed), ASK for it before creating — do not silently invent a cap. This applies to campaign-creation flows too: a budget group needed by a new campaign still requires a user-chosen cap. Same when an update would change monthlyCap.\n                - If the user says \"set a monthly budget of $X\" or equivalent then use MONTHLY_RESET as budgetType (MONTHLY is NOT a valid value, the platform rejects it)\n                - Before an update, refresh the information by using get_budget_group to avoid overwriting fields unintentionally.\n                - The campaign doesn't need to be in a launched state for its budget group to be updated.\n\n                WHEN TO USE:\n                - User wants to create a new budget group with specific settings\n                - We're creating a campaign and need to set up its budget group.\n                - User requests to update an existing budget group with new parameters\n                - User requests to update the budget of a campaign\n\n                INTEGRATION WITH OTHER TOOLS:\n                - If the ID for a budget group update is unknown there are a few options:\n                  - if you have the campaign name, use search_campaigns_by_name. In its response, `$.optimizationGroup.id` is the budget group ID.\n                    - From search_campaigns_by_names's response, you get the property `$.optimizationGroup.id`. That's the budget group ID.\n                - You can also use get_budget_group if you have the budget group name to retrieve its ID.\n\n                Anchor every date calculation to the REAL current date — if you are not certain what today is, call get_current_date before computing start/end dates.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "data": {
      "type": "object",
      "description": "Budget group configuration",
      "properties": {
       "name": {
        "type": "string",
        "description": "Unique budget group name"
       },
       "budgetType": {
        "type": "string",
        "enum": [
         "MONTHLY_RESET",
         "FIXED_BUDGET"
        ],
        "description": "Budget type"
       },
       "monthlyCap": {
        "type": "integer",
        "minimum": 1,
        "description": "Monthly spending cap in USD"
       },
       "goal": {
        "type": "string",
        "enum": [
         "CPL",
         "MQL",
         "CTR"
        ],
        "description": "Optimization goal. CPL/MQL for Lead Gen; CTR for Brand Awareness."
       },
       "groupMetric": {
        "type": "string",
        "enum": [
         "CPL",
         "MQL",
         "CPC",
         "CTR"
        ],
        "description": "Group metric. CPL/MQL for Lead Gen; CPC or CTR for Brand Awareness (match optimizerFormula)."
       },
       "budgetRedistributionStrategy": {
        "type": "string",
        "enum": [
         "PERFORMANCE",
         "PACING_ONLY"
        ],
        "description": "Budget redistribution strategy"
       },
       "benchmark": {
        "type": "integer",
        "minimum": 1,
        "description": "Benchmark value for the goal"
       },
       "startDate": {
        "type": "string",
        "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
        "description": "ISO 8601 UTC with exactly 3 ms digits"
       },
       "endDate": {
        "type": "string",
        "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
        "description": "ISO 8601 UTC with exactly 3 ms digits"
       },
       "wizCampaignIds": {
        "type": "array",
        "items": {
         "type": "integer"
        },
        "description": "Campaign IDs to include. Omit this field (don't send empty array) if no campaigns to add."
       },
       "campaign_names": {
        "type": "array",
        "items": {
         "type": "string"
        },
        "description": "Names to search (partial, case-insensitive); merged into wizCampaignIds"
       },
       "id": {
        "type": "integer",
        "description": "Budget group ID (required for updates, omit for creates)."
       },
       "autoPauseConfigurationId": {
        "type": "integer",
        "default": 14
       },
       "autoApplyBudget": {
        "type": "boolean",
        "default": true
       },
       "autopauseMonthlyCap": {
        "type": "boolean",
        "default": true
       },
       "autorestartMonthlyCap": {
        "type": "boolean",
        "default": true
       },
       "optimizerFormula": {
        "type": "string",
        "enum": [
         "CPL_2",
         "CPC_2",
         "CTR"
        ],
        "default": "CPL_2",
        "description": "CPL_2/CPC_2 for Lead Gen; CPC_2 or CTR for Brand Awareness."
       },
       "enableBooster": {
        "type": "boolean",
        "description": "Defaults to true for Lead Gen; set to false for Brand Awareness."
       },
       "funnelConfigurationIds": {
        "type": "array",
        "items": {
         "type": "integer"
        }
       },
       "monthlyCapFacebook": {
        "type": "integer"
       },
       "monthlyCapLinkedin": {
        "type": "integer"
       },
       "cplLinkedin": {
        "type": "integer"
       },
       "cplFacebook": {
        "type": "integer"
       },
       "totalBudget": {
        "type": "integer"
       },
       "scheduleFrequency": {
        "type": "integer"
       },
       "lowerLimit": {
        "type": "integer"
       },
       "overspentMultiplier": {
        "type": "number"
       },
       "customField": {
        "type": "object",
        "properties": {
         "id": {
          "type": "integer"
         },
         "label": {
          "type": "string"
         }
        },
        "required": [
         "id",
         "label"
        ],
        "additionalProperties": false
       }
      },
      "required": [
       "name",
       "budgetType",
       "monthlyCap",
       "goal",
       "budgetRedistributionStrategy"
      ],
      "additionalProperties": false
     }
    },
    "required": [
     "data"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_campaign",
   "title": "Create Campaign — Precision Optimization (1×1×1)",
   "category": "campaigns",
   "risk": "write",
   "summary": "Create multi-channel advertising campaigns for LinkedIn, Facebook, Instagram, Google Ads, Reddit, and Microsoft Ads using the platform's \"Precision Optimization\" (1×1×1) model —…",
   "description": "Create multi-channel advertising campaigns for LinkedIn, Facebook, Instagram, Google Ads, Reddit, and Microsoft Ads using the platform's \"Precision Optimization\" (1×1×1) model — Metadata's signature approach. Every audience×ad×offer combination is tested as its OWN isolated experiment (each experiment = exactly 1 audience × 1 ad × 1 offer); you pass per-channel lists of audiences, ads, and offers and the backend creates one experiment per combination. Platform product copy: \"Metadata's signature approach with maximum budget control and fastest learning through separate testing of each combination.\" This is the DEFAULT structure when the user hasn't chosen one. Sets channel.structureType=METADATA (no WizNativeAdContainer).\n\n                    KEYWORDS (for tool discovery — many users ask by the math, not the product name): 1x1x1, 1×1×1, \"1x1x1 campaign\", \"one by one by one\", Precision Optimization, separate testing of each combination, isolated per-combination experiments, maximum budget control, fastest learning.\n\n                    NAMING CONVENTION (from the platform UI's \"Campaign Structure\" selector — match these exact labels):\n                      • \"Precision Optimization (1x1x1)\" = THIS tool (`create_campaign`). Each experiment isolates one audience×ad×offer cell; the platform builds one per combination. Metadata's signature approach; the DEFAULT.\n                      • \"Channel First Structure (NxNxN)\" = the OTHER tool, `create_native_structure_campaign` (the UI also shows it as \"Native Structure\" / \"Platform-First Structure\"). Audiences/ads/offers are grouped in the ad platform's NATIVE structure (WizNativeAdContainer) and optimized together by the platform (~1 experiment per channel).\n                    Routing anchor: \"precision\" / \"1x1x1\" → this tool; \"native\" / \"channel first\" / \"platform first\" / \"NxNxN\" → create_native_structure_campaign. (structureType=METADATA here, NATIVE there — and that lines up, because \"native\" means the native-platform structure.)\n\n                    ════════════════════════════════════════════════════════════════════════════════\n                    ⚠️ TWO CAMPAIGN-CREATION TOOLS EXIST — READ BEFORE PICKING ONE ⚠️\n                    ════════════════════════════════════════════════════════════════════════════════\n                    🔴 REMEMBER — HOW TO CHOOSE THE STRUCTURE (the user's decision always wins):\n                      1. If the user explicitly names one, RESPECT IT — \"precision\" / \"Precision Optimization\" / \"1x1x1\" → create_campaign (this tool); \"native\" / \"Native Structure\" / \"Channel First\" / \"Platform-First\" / \"NxNxN\" → create_native_structure_campaign.\n                      2. ELSE, if the request involves MANY ads AND audiences (Precision Optimization makes one experiment per combination — a large experiment count), ASK the user which structure they want before creating — do not guess.\n                      3. OTHERWISE, DEFAULT to Precision Optimization (1×1×1) → this tool (create_campaign).\n                    The two build structurally different, non-swappable campaigns — so once the user decides, respect that decision for the rest of the campaign.\n\n                    USE THIS TOOL (`create_campaign`) — \"Precision Optimization\" (1×1×1) — WHEN:\n                      • The user says \"Precision Optimization\", \"precision\", \"1x1x1\", \"1×1×1\", \"separate testing of each combination\", or wants each combination tested in its own isolated experiment.\n                      • The user gives per-channel lists of audiences/ads/offers and wants the platform to test every combination separately (one experiment per audience×ad×offer).\n                      • The user wants maximum budget control / fastest per-combination learning.\n                      • No structure was specified — this is the DEFAULT.\n\n                    USE `create_native_structure_campaign` (\"Channel First\" / \"Native Structure\", N×N×N) INSTEAD — DO NOT USE THIS TOOL — WHEN:\n                      • The user says \"native\", \"Native Structure\", \"Channel First Structure\", \"channel-first\", \"Platform-First Structure\", \"platform-first\", \"NxNxN\", \"N×N×N\", \"WizNativeAdContainer\", or asks for the traditional / hierarchical / native-platform structure.\n                      • The user wants the ad platform's native structure to group and optimize audiences/ads/offers together (~1 experiment per channel, unified reporting) rather than one isolated experiment per combination.\n                      • The user is migrating a campaign FROM \"Precision Optimization\" (1×1×1) TO the native \"Channel First\" structure.\n                      Do not silently call this tool when the user has signalled Channel-First / native intent — the schemas and resulting platform state are different and not swappable after creation.\n\n                    Apply the structure-selection rule above: respect an explicit \"precision\"/\"native\" choice; if the user didn't choose, ASK only when there are MANY ads AND audiences; otherwise default to Precision Optimization (1×1×1, this tool).\n                    ════════════════════════════════════════════════════════════════════════════════\n\n                    WHEN TO USE:\n                    - Create multi-channel marketing campaigns with unified configuration\n                    - Set up campaigns with specific start/end dates and budget groups\n                    - Configure Lead Generation or Brand Awareness campaigns\n                    - Organize ads into groups with specific targeting and offers\n                    - Set up Google Ads campaigns with keyword targeting\n\n                    CAMPAIGN TYPES:\n                    1. \"Lead Gen\" - Lead generation campaigns\n                    2. \"Brand Awareness\" - Brand awareness campaigns\n\n                    ⚠️ CRITICAL OFFER TYPE RULE:\n                    - \"Lead Gen\" campaigns: ONLY Lead Gen (LG) form offers allowed. Landing Page (LP) offers are FORBIDDEN.\n                    - \"Brand Awareness\" campaigns: Both LG and LP offers allowed. No restriction.\n\n                    ⚠️ CRITICAL OFFER-CHANNEL PAIRING RULE (silent failure if violated) ⚠️\n                    Lead Gen offers are CHANNEL-LOCKED at creation time (the channel was set on create_update_offer via data.channel). An offer_id created with channel=\"LINKEDIN\" CANNOT be assigned to facebook.offers / instagram.offers / google.offers — and vice versa. This server DOES NOT validate the pairing and will RETURN SUCCESS on a mis-wired campaign that cannot actually serve. The only way the user finds out is when the campaign produces zero impressions in the platform.\n\n                    HARD RULES (verify each offer_id BEFORE submitting):\n                    1. linkedin.offers MUST contain ONLY offer_ids whose creating call set data.channel=\"LINKEDIN\" (or Landing Page offer_ids, which have no channel and are universal).\n                    2. facebook.offers MUST contain ONLY offer_ids with data.channel=\"FACEBOOK\" (or Landing Page offer_ids).\n                    3. instagram.offers MUST contain ONLY offer_ids with data.channel=\"INSTAGRAM\" or \"FACEBOOK\" (or Landing Page offer_ids).\n                    4. google.offers MUST contain ONLY Landing Page offer_ids — Google Ads does NOT support Lead Gen forms. Putting any Lead Gen offer_id in google.offers is a HARD FAILURE.\n                    5. reddit.offers MUST contain ONLY Landing Page offer_ids — Reddit does NOT support Lead Gen forms (create_update_offer rejects channel=\"REDDIT\"). Putting any Lead Gen offer_id in reddit.offers is a HARD FAILURE.\n                    6. microsoft.offers MUST contain ONLY offer_ids with data.channel=\"MICROSOFT_ADS\" (or Landing Page offer_ids). Unlike Google, Microsoft Ads DOES accept Lead Gen offers, but only when channel-locked to MICROSOFT_ADS.\n                    7. If the user's campaign requires Lead Gen on multiple channels, you MUST have called create_update_offer SEPARATELY for each channel (one call per channel per concept). Cross-channel reuse of a Lead Gen offer_id WILL break the campaign even though this API returns success.\n                    8. If you cannot remember which channel an offer_id was created for, call get_offer(name=<offer_name>) first and read the channel field — DO NOT GUESS.\n\n                    REQUIRED FIELDS:\n                    - campaignType: \"Lead Gen\" or \"Brand Awareness\"\n                    - name: Unique campaign name\n                    - startDate / endDate: YYYY-MM-DD format\n                    - budgetGroup: Budget group name (e.g., \"BoFu\", \"ToFu\", \"Brand\")\n                    - Per-channel dailyBudget (optional, min $10): the channel's daily spend in USD. Set it whenever a budget split was approved (monthly channel budget / 30); a channel without it keeps the platform default of $50/day.\n                    - At least one channel must be configured\n\n                    CHANNEL-SPECIFIC CONFIGURATION:\n\n                    ⚠️ AUDIENCES vs TARGET GROUPS — CRITICAL DISTINCTION:\n                    - \"audiences\": standard Metadata audiences (Firmographic, Bombora Intent, Retargeting, LinkedIn/Facebook Native, etc.)\n                    - \"target_groups\": Targeting Groups (also called Target Groups) created in the platform's Targeting Groups section.\n                    Both are arrays of names (strings). Send each type in its own field — do NOT mix them.\n                    If the user mentions a \"Targeting Group\" or \"Target Group\", it goes in \"target_groups\", NOT in \"audiences\".\n\n                    ⚠️ ADS — SEPARATE (default) vs BUNDLED INTO ONE GROUP (opt-in):\n                    The \"ads\" object has two slots and they map to two different platform structures:\n                    - \"ads.list\": a flat array of ad IDs. Each id is sent as its own request, so a list of N ids\n                      produces N separately managed ads, NOT one bundle. THIS IS THE DEFAULT — put ads here\n                      unless the user explicitly asks for ad groups.\n                    - \"ads.groups\": an array of {name, ads:[ids]} objects. Each object bundles its ad IDs into ONE named\n                      ad group. Use this ONLY when the user explicitly wants ads grouped together\n                      (e.g. \"put these in an ad group\", \"group these ads\"). LinkedIn has no ad-group concept — use list only.\n                    You may send both: ads in \"list\" stay separate, ads in \"groups\" get bundled.\n                    (Note: the platform wraps every ad in an ad-group record server-side, single ads included. That is\n                    outside this tool's control and does not change the two shapes above.)\n\n                    LINKEDIN (optional):\n                    - audiences: Array of audience names (required if linkedin included)\n                        Example: [\"Test Native Audience\", \"EU Countries\"]\n                    - target_groups: Array of Targeting Group names\n                        Example: [\"My Target Group 1\"]\n                    - ads: Object with ad organization (required if linkedin included)\n                        - list: Array of ad IDs, each sent as its own request (default). Example: [156502, 156485]\n                          (LinkedIn has no ad groups — always use list.)\n                    - offers: Array of offer IDs (required if linkedin included)\n                        Example: [57538, 57504]\n\n                    FACEBOOK (optional):\n                    - audiences: Array of Facebook audience names (required if facebook included)\n                        Example: [\"fb-test\", \"automation-fb-retargeting-event-stage-blue\"]\n                    - target_groups: Array of Targeting Group names\n                    - ads: Object with ad organization (required if facebook included)\n                        - list: Array of ad IDs, each sent as its own request (DEFAULT — use this)\n                        - groups: Array of ad group objects [{\"name\": str, \"ads\": [ids]}] — bundles ads into\n                          a named ad group. ONLY when the user explicitly asks for grouped ads.\n                    - offers: Array of offer IDs (required if facebook included)\n\n                    INSTAGRAM (optional):\n                    - audiences: Array of Instagram audience names (required if instagram included)\n                    - target_groups: Array of Targeting Group names\n                    - ads: Object with ad organization (required if instagram included)\n                        - list: Array of ad IDs, each sent as its own request (DEFAULT — use this)\n                        - groups: Array of ad group objects [{\"name\": str, \"ads\": [ids]}] — bundles ads into\n                          a named ad group. ONLY when the user explicitly asks for grouped ads.\n                    - offers: Array of offer IDs (required if instagram included)\n                    - Often shares same audiences and offers as Facebook\n\n                    GOOGLE ADS (optional):\n                    - keywords: Object with keyword organization (required if google included)\n                        - groups: [{\"name\": \"CRM Keywords\", \"keywords\": [\"crm software\", \"lead gen tools\"], \"match_type\": \"PHRASE\"}]\n                          Each group also accepts \"keyword_ids\": [<existing library keyword IDs from list_keywords>] and \"match_type\": PHRASE|BROAD|EXACT (default PHRASE).\n                    - audiences: Array of Google audience names (required if google included)\n                        Example: [\"SUP_SF_All-Customers-Contacts_ALL_3\"]\n                    - target_groups: Array of Targeting Group names\n                    - ads: Object with ad organization (required if google included)\n                        - list: [175258] — ad IDs each sent as their own request (DEFAULT — use this)\n                        - groups: [{\"name\": \"GOAT\", \"ads\": [175260, 175259]}] — bundles ads into a named\n                          ad group. ONLY when the user explicitly asks for grouped ads.\n                    - offers: Array of offer IDs (required if google included)\n                    - negative_keyword_lists: Array of negative keyword list names (optional)\n                        Example: [\"Competitor Brands\", \"Low Intent Keywords\"]\n                        These are assigned to the Google Ads channel AFTER campaign creation.\n                        List names must match existing negative keyword lists (use list_negative_keywords_list to find them).\n\n                    REDDIT (optional):\n                    - audiences: Array of Reddit audience names (required if reddit included)\n                    - target_groups: Array of Targeting Group names (Reddit Target Groups are created via create_reddit_target_group)\n                    - ads: Object with ad organization (required if reddit included)\n                        - list: Array of ad IDs, each sent as its own request. Reddit ads\n                          are created via create_update_image_ad / create_update_video_ad with channels=[\"REDDIT\"].\n                          (Like LinkedIn, always use list — no ad groups.)\n                    - offers: Array of offer IDs (required if reddit included). Landing Page\n                      offers ONLY — Reddit does not support Lead Gen forms.\n                    - Channel settings (objective, allow comments, placements, conversion pixel)\n                      get UI-equivalent defaults on creation — adjust them afterwards with\n                      update_reddit_channel_settings.\n\n                    MICROSOFT ADS (optional):\n                    - Search channel — mirrors Google Ads structurally. The platform pairs\n                      MICROSOFT_ADS with GOOGLE_ADS for the TARGET_SPEND bid default.\n                    - keywords: Object with keyword organization (required if microsoft included)\n                        - groups: [{\"name\": \"CRM Keywords\", \"keywords\": [\"crm software\", \"lead gen tools\"], \"match_type\": \"PHRASE\"}]\n                          Each group also accepts \"keyword_ids\": [<existing library keyword IDs from list_keywords>] and \"match_type\": PHRASE|BROAD|EXACT (default PHRASE).\n                    - audiences: Array of Microsoft Ads audience names (required if microsoft included)\n                    - target_groups: Array of Targeting Group names\n                    - ads: Object with ad organization (required if microsoft included)\n                        - list: Array of ad IDs, each sent as its own request (DEFAULT)\n                        - groups: [{\"name\": \"MS Group A\", \"ads\": [ids]}] — bundles ads into a named\n                          ad group. ONLY when the user explicitly asks for grouped ads.\n                    - offers: Array of offer IDs (required if microsoft included). Lead Gen offers\n                      must be MICROSOFT_ADS-locked (created with data.channel=\"MICROSOFT_ADS\");\n                      Landing Page offers are universal.\n                    - negative_keyword_lists: Array of negative keyword list names (optional)\n                        Example: [\"Competitor Brands\", \"Low Intent Keywords\"]\n                        Assigned to the Microsoft Ads channel after creation. Names must match\n                        existing lists (use list_negative_keywords_list to find them).\n\n                    VALIDATION:\n                    - Campaign name must be unique\n                    - Start date must be before end date\n                    - All referenced ad IDs, offer IDs, and audience names must exist\n                    - If a channel is included, its required fields (audiences, ads, offers) must be provided\n                    - ⚠️ Lead Gen campaigns with Landing Page offers MUST be rejected\n\n                    OFFER VERIFICATION (Lead Gen only):\n                    1. Call get_offer to check offer types before campaign creation\n                    2. If any offer is Landing Page → reject and suggest switching to Brand Awareness or replacing with LG offers\n\n                    RESPONSE: Returns campaign_id, campaign_url, status, configured_channels, per-channel\n                    details, optimization_group (id + tag_name), and a `full_response` payload with the\n                    complete wizard state. On partial failures, returns success=false plus an `errors[]`\n                    list — the campaign shell is still created and partial work is preserved.\n                    Campaign URL: https://platform.metadata.io/hub/advertise/campaigns/{campaign_id}\n\n                    BID STRATEGY (automatic — no user input needed):\n                    Every enabled channel is set to a safe \"auto\" default immediately after creation so\n                    the campaign is launch-ready:\n                      - LinkedIn / Facebook / Instagram → AUTO_BID\n                      - Google Ads / Microsoft Ads      → TARGET_SPEND (device modifiers reset to 0)\n                      - Reddit                          → follows the objective, mirroring the platform UI:\n                        Lead Gen campaigns force the CLICKS (Traffic) objective + AUTO_BID; Brand\n                        Awareness keeps the platform default IMPRESSIONS objective + MANUAL_BID.\n                        Change objective/bid afterwards with update_reddit_channel_settings.\n                    This happens regardless of account type and regardless of\n                    campaign goal (CPL / CTR). The user can still override bid strategy later via the\n                    platform UI or a dedicated tool.\n\n                    EXAMPLE (Lead Gen, multi-channel):\n                    create_campaign(campaign_data={\n                        \"campaignType\": \"Lead Gen\",\n                        \"name\": \"Q1_Multi_Channel_2025\",\n                        \"startDate\": \"2025-08-01\",\n                        \"endDate\": \"2025-09-30\",\n                        \"budgetGroup\": \"BoFu\",\n                        \"linkedin\": {\n                            \"audiences\": [\"Test Native Audience\", \"EU Countries\"],\n                            \"ads\": {\"list\": [156502, 156485]},\n                            \"offers\": [57538, 57504]\n                        }\n                    })\n\n                    EXAMPLE (Brand Awareness, single-channel):\n                    create_campaign(campaign_data={\n                        \"campaignType\": \"Brand Awareness\",\n                        \"name\": \"LinkedIn_Brand_Jan2025\",\n                        \"startDate\": \"2025-01-15\",\n                        \"endDate\": \"2025-02-15\",\n                        \"budgetGroup\": \"Brand\",\n                        \"linkedin\": {\n                            \"audiences\": [\"Professional Audience\"],\n                            \"ads\": {\"list\": [173669, 171805]},\n                            \"offers\": [57538]\n                        }\n\n                        CAMPAIGN URL FORMAT:\n                        - All successful campaign creations return a direct URL to the campaign\n                        - URL format: https://platform.metadata.io/hub/advertise/campaigns/{campaign_id}\n                        - Use this URL to navigate directly to the campaign in the platform\n                        - Campaign URL is always included in successful responses\n                        - Provides immediate access to campaign management and monitoring\n\n                        BEST PRACTICES:\n                        1. Use descriptive, unique campaign names with dates or identifiers\n                        2. Ensure all referenced assets (ads, offers, audiences) exist before campaign creation\n                        3. Plan campaign dates appropriately with buffer time for setup\n                        4. Group related ads together for better organization and reporting\n                        5. Use consistent naming conventions for ad groups across channels\n                        6. Test with single-channel campaigns before creating multi-channel campaigns\n                        7. Verify budget group names match existing budget groups in the system\n                        8. Save the returned campaign URL for quick access to campaign management\n\n                        ERROR HANDLING:\n                        - Validates all required fields are provided\n                        - Checks that referenced ad IDs, offer IDs, and audience names exist\n                        - Ensures campaign name uniqueness\n                        - Validates date formats and logical date ranges\n                        - Verifies at least one channel is configured\n                        - Provides detailed error messages for troubleshooting\n\n                        INTEGRATION NOTES:\n                        - Supports both simple single-channel and complex multi-channel campaigns\n                        - Automatically handles channel-specific configuration requirements\n                        - Works in conjunction with ad and offer creation tools for complete campaign setup\n                        - Provides comprehensive campaign status and configuration feedback\n                        - Always returns the platform URL for immediate campaign access",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_data": {
      "type": "object",
      "description": "Complete campaign configuration data",
      "properties": {
       "campaignType": {
        "type": "string",
        "enum": [
         "Lead Gen",
         "Brand Awareness"
        ],
        "description": "Type of campaign - Lead Gen or Brand Awareness"
       },
       "name": {
        "type": "string",
        "description": "Unique campaign name"
       },
       "startDate": {
        "type": "string",
        "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
        "description": "Campaign start date in YYYY-MM-DD format"
       },
       "endDate": {
        "type": "string",
        "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
        "description": "Campaign end date in YYYY-MM-DD format"
       },
       "budgetGroup": {
        "type": "string",
        "description": "Budget group name to use for this campaign"
       },
       "linkedin": {
        "type": "object",
        "description": "LinkedIn campaign configuration",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of LinkedIn audience names (Firmographic, Bombora Intent, Retargeting, Native, etc.)"
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Targeting Group names for LinkedIn. These are NOT audiences — they are Target Groups from the platform's Targeting Groups section."
         },
         "ads": {
          "type": "object",
          "properties": {
           "list": {
            "type": "array",
            "items": {
             "type": "integer"
            },
            "description": "Array of ad IDs for LinkedIn, each sent as its own request. This is the default way to attach ads. LinkedIn has no ad-group concept, so all LinkedIn ads go here."
           }
          }
         },
         "offers": {
          "type": "array",
          "items": {
           "type": "integer"
          },
          "description": "Array of offer IDs for LinkedIn. ⚠️ MUST contain ONLY offer_ids that were created via create_update_offer with data.channel='LINKEDIN' (Lead Gen offers are channel-locked) OR Landing Page offer_ids (channel-agnostic). Passing a Facebook/Instagram Lead Gen offer_id here is a SILENT FAILURE — the server will accept it and return success but the campaign will not serve."
         }
        },
        "required": [
         "ads",
         "offers"
        ]
       },
       "facebook": {
        "type": "object",
        "description": "Facebook campaign configuration",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Facebook audience names (Firmographic, Bombora Intent, Retargeting, Native, etc.)"
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Targeting Group names for Facebook. These are NOT audiences — they are Target Groups from the platform's Targeting Groups section."
         },
         "ads": {
          "type": "object",
          "properties": {
           "groups": {
            "type": "array",
            "items": {
             "type": "object",
             "properties": {
              "name": {
               "type": "string"
              },
              "ads": {
               "type": "array",
               "items": {
                "type": "integer"
               }
              }
             },
             "required": [
              "name",
              "ads"
             ]
            }
           },
           "list": {
            "type": "array",
            "items": {
             "type": "integer"
            }
           }
          }
         },
         "offers": {
          "type": "array",
          "items": {
           "type": "integer"
          },
          "description": "Array of offer IDs for Facebook. ⚠️ MUST contain ONLY offer_ids that were created via create_update_offer with data.channel='FACEBOOK' (Lead Gen offers are channel-locked) OR Landing Page offer_ids (channel-agnostic). Passing a LinkedIn/Instagram Lead Gen offer_id here is a SILENT FAILURE — the server will accept it and return success but the campaign will not serve."
         },
         "biddingOptimizationGoal": {
          "type": "string",
          "description": "Meta performance goal. Defaults to META_LEADS_WEBSITE_CONVERSIONS, which is what a CPL campaign driving to a landing page wants; set it only to depart from that. The platform REFUSES to launch a Meta channel without one, so this is not optional polish, it is a launch requirement the default already satisfies. Names read META_<OUTCOME>_<DESTINATION>_<GOAL>[_CHARGED_BY_<BILLING>]. Common: META_LEADS_WEBSITE_CONVERSIONS, META_LEADS_INSTANT_FORMS_LEADS (lead-gen forms), META_LEADS_WEBSITE_AND_INSTANT_FORMS_CONVERSIONS (both), META_TRAFFIC_WEBSITE_LANDING_PAGE_VIEWS, META_AWARENESS_REACH. Two traps: the *_VIDEO_THRUPLAY_* and *_VIDEO_2SEC_* goals require EVERY ad in the channel to be a video ad, and META_AWARENESS_AD_RECALL_LIFT requires the auto bid strategy. Either mismatch is a launch blocker, not a warning."
         }
        },
        "required": [
         "ads",
         "offers"
        ]
       },
       "instagram": {
        "type": "object",
        "description": "Instagram campaign configuration",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Instagram audience names (Firmographic, Bombora Intent, Retargeting, Native, etc.)"
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Targeting Group names for Instagram. These are NOT audiences — they are Target Groups from the platform's Targeting Groups section."
         },
         "ads": {
          "type": "object",
          "properties": {
           "groups": {
            "type": "array",
            "items": {
             "type": "object",
             "properties": {
              "name": {
               "type": "string"
              },
              "ads": {
               "type": "array",
               "items": {
                "type": "integer"
               }
              }
             },
             "required": [
              "name",
              "ads"
             ]
            }
           },
           "list": {
            "type": "array",
            "items": {
             "type": "integer"
            }
           }
          }
         },
         "offers": {
          "type": "array",
          "items": {
           "type": "integer"
          },
          "description": "Array of offer IDs for Instagram. ⚠️ MUST contain ONLY offer_ids that were created via create_update_offer with data.channel='INSTAGRAM' or 'FACEBOOK' (Lead Gen offers are channel-locked; Meta business sharing means FB offers are usable on IG) OR Landing Page offer_ids (channel-agnostic). Passing a LinkedIn Lead Gen offer_id here is a SILENT FAILURE — the server will accept it and return success but the campaign will not serve."
         },
         "biddingOptimizationGoal": {
          "type": "string",
          "description": "Meta performance goal. Defaults to META_LEADS_WEBSITE_CONVERSIONS, which is what a CPL campaign driving to a landing page wants; set it only to depart from that. The platform REFUSES to launch a Meta channel without one, so this is not optional polish, it is a launch requirement the default already satisfies. Names read META_<OUTCOME>_<DESTINATION>_<GOAL>[_CHARGED_BY_<BILLING>]. Common: META_LEADS_WEBSITE_CONVERSIONS, META_LEADS_INSTANT_FORMS_LEADS (lead-gen forms), META_LEADS_WEBSITE_AND_INSTANT_FORMS_CONVERSIONS (both), META_TRAFFIC_WEBSITE_LANDING_PAGE_VIEWS, META_AWARENESS_REACH. Two traps: the *_VIDEO_THRUPLAY_* and *_VIDEO_2SEC_* goals require EVERY ad in the channel to be a video ad, and META_AWARENESS_AD_RECALL_LIFT requires the auto bid strategy. Either mismatch is a launch blocker, not a warning."
         }
        },
        "required": [
         "ads",
         "offers"
        ]
       },
       "google": {
        "type": "object",
        "description": "Google Ads campaign configuration",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "keywords": {
          "type": "object",
          "properties": {
           "groups": {
            "type": "array",
            "items": {
             "type": "object",
             "properties": {
              "name": {
               "type": "string"
              },
              "keywords": {
               "type": "array",
               "items": {
                "type": "string"
               },
               "description": "Free-text phrases — created in the keyword library and added to this group."
              },
              "keyword_ids": {
               "type": "array",
               "items": {
                "type": "integer"
               },
               "description": "Existing keyword-library IDs (from list_keywords) to add to this group. Unioned with `keywords`."
              },
              "match_type": {
               "type": "string",
               "enum": [
                "PHRASE",
                "BROAD",
                "EXACT"
               ],
               "description": "Match type for this group. Defaults to PHRASE."
              }
             },
             "required": [
              "name"
             ]
            }
           }
          }
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Google audience names (Firmographic, Bombora Intent, Retargeting, etc.)"
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Targeting Group names for Google Ads. These are NOT audiences — they are Target Groups from the platform's Targeting Groups section."
         },
         "ads": {
          "type": "object",
          "properties": {
           "groups": {
            "type": "array",
            "items": {
             "type": "object",
             "properties": {
              "name": {
               "type": "string"
              },
              "ads": {
               "type": "array",
               "items": {
                "type": "integer"
               }
              }
             },
             "required": [
              "name",
              "ads"
             ]
            }
           },
           "list": {
            "type": "array",
            "items": {
             "type": "integer"
            }
           }
          }
         },
         "offers": {
          "type": "array",
          "items": {
           "type": "integer"
          },
          "description": "Array of offer IDs for Google Ads. ⚠️ MUST contain ONLY Landing Page offer_ids — Google Ads does NOT support native Lead Gen forms. Any offer_id created via create_update_offer with offer_type='Lead Gen' (regardless of data.channel) is FORBIDDEN here and will produce a broken campaign that the server still reports as successful."
         },
         "negative_keyword_lists": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of negative keyword list names to assign to the Google Ads channel. Names must match existing lists (use list_negative_keywords_list to find them)."
         },
         "locations": {
          "type": "object",
          "description": "Where the Google Ads campaign SERVES — the UI's Location picker. ⚠️ OMITTING THIS LEAVES THE PLATFORM DEFAULT OF UNITED STATES. Set it whenever the user names a geography.",
          "properties": {
           "countries": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "Country NAMES to target, e.g. [\"United States\", \"Canada\"]. Common shorthand resolves (\"USA\", \"UK\"). An unrecognised name is rejected, not skipped."
           },
           "states": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "US state NAMES to target, e.g. [\"Texas\", \"California\"]. US-only. Naming states without countries keeps the channel's current countries."
           }
          },
          "additionalProperties": false
         },
         "excluded_locations": {
          "type": "object",
          "description": "Where the Google Ads campaign must NOT serve — the UI's Negative Locations editor under Advanced Settings. ⚠️ COUNTRY-LEVEL ONLY: the platform stores exclusions as bare location ids with no country/state discriminator and state ids overlap country ids, so an excluded state would be read back as an unrelated country. To narrow within a country, target the states you want via `locations.states` instead.",
          "properties": {
           "countries": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "Country NAMES to exclude, e.g. [\"India\", \"Pakistan\"]."
           }
          },
          "additionalProperties": false
         }
        },
        "required": [
         "keywords",
         "ads",
         "offers"
        ]
       },
       "reddit": {
        "type": "object",
        "description": "Reddit campaign configuration. Channel settings (objective, comments, placements, conversion pixel) get UI-equivalent defaults; adjust afterwards with update_reddit_channel_settings.",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Reddit audience names (Firmographic, Bombora Intent, Retargeting, etc.). OPTIONAL — Reddit's normal targeting is `target_groups`. An audience exists on Reddit only once the account has CONNECTED Reddit (the platform distributes audiences to connected channels only), so on a disconnected Reddit these silently resolve to nothing; use target_groups instead."
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Targeting Group names for Reddit (created via create_reddit_target_group). These are NOT audiences — they are Target Groups from the platform's Targeting Groups section."
         },
         "ads": {
          "type": "object",
          "properties": {
           "list": {
            "type": "array",
            "items": {
             "type": "integer"
            },
            "description": "Array of Reddit ad IDs (from create_update_image_ad / create_update_video_ad with channels=['REDDIT']), each sent as its own request. Like LinkedIn, Reddit uses list only — no ad groups."
           }
          }
         },
         "offers": {
          "type": "array",
          "items": {
           "type": "integer"
          },
          "description": "Array of offer IDs for Reddit. ⚠️ MUST contain ONLY Landing Page offer_ids — Reddit does NOT support native Lead Gen forms. Any offer_id created via create_update_offer with offer_type='Lead Gen' (regardless of data.channel) is FORBIDDEN here and will produce a broken campaign that the server still reports as successful."
         }
        },
        "required": [
         "ads",
         "offers"
        ]
       },
       "microsoft": {
        "type": "object",
        "description": "Microsoft Ads campaign configuration. Same structural shape as Google (search keywords + audiences + ads + offers + negative keyword lists), and the wizard pairs MICROSOFT_ADS with GOOGLE_ADS for the TARGET_SPEND bid default. Unlike Google, Microsoft Ads supports BOTH Landing Page and Lead Gen offers (the latter must be channel-locked to MICROSOFT_ADS).",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "keywords": {
          "type": "object",
          "properties": {
           "groups": {
            "type": "array",
            "items": {
             "type": "object",
             "properties": {
              "name": {
               "type": "string"
              },
              "keywords": {
               "type": "array",
               "items": {
                "type": "string"
               },
               "description": "Free-text phrases — created in the keyword library and added to this group."
              },
              "keyword_ids": {
               "type": "array",
               "items": {
                "type": "integer"
               },
               "description": "Existing keyword-library IDs (from list_keywords) to add to this group. Unioned with `keywords`."
              },
              "match_type": {
               "type": "string",
               "enum": [
                "PHRASE",
                "BROAD",
                "EXACT"
               ],
               "description": "Match type for this group. Defaults to PHRASE."
              }
             },
             "required": [
              "name"
             ]
            }
           }
          }
         },
         "audiences": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Microsoft Ads audience names (Firmographic, Bombora Intent, Retargeting, etc.)"
         },
         "target_groups": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of Targeting Group names for Microsoft Ads. These are NOT audiences — they are Target Groups from the platform's Targeting Groups section."
         },
         "ads": {
          "type": "object",
          "properties": {
           "groups": {
            "type": "array",
            "items": {
             "type": "object",
             "properties": {
              "name": {
               "type": "string"
              },
              "ads": {
               "type": "array",
               "items": {
                "type": "integer"
               }
              }
             },
             "required": [
              "name",
              "ads"
             ]
            }
           },
           "list": {
            "type": "array",
            "items": {
             "type": "integer"
            }
           }
          }
         },
         "offers": {
          "type": "array",
          "items": {
           "type": "integer"
          },
          "description": "Array of offer IDs for Microsoft Ads. ⚠️ MUST contain ONLY offer_ids that were created via create_update_offer with data.channel='MICROSOFT_ADS' (Lead Gen offers are channel-locked) OR Landing Page offer_ids (channel-agnostic). Passing another channel's Lead Gen offer_id here is a SILENT FAILURE — the server will accept it and return success but the campaign will not serve."
         },
         "negative_keyword_lists": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "Array of negative keyword list names to assign to the Microsoft Ads channel. Names must match existing lists (use list_negative_keywords_list to find them)."
         },
         "locations": {
          "type": "object",
          "description": "Where the Microsoft Ads campaign SERVES — the UI's Location picker. ⚠️ OMITTING THIS LEAVES THE PLATFORM DEFAULT OF UNITED STATES. Set it whenever the user names a geography.",
          "properties": {
           "countries": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "Country NAMES to target, e.g. [\"United States\", \"Canada\"]. Common shorthand resolves (\"USA\", \"UK\"). An unrecognised name is rejected, not skipped."
           },
           "states": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "US state NAMES to target, e.g. [\"Texas\", \"California\"]. US-only. Naming states without countries keeps the channel's current countries."
           }
          },
          "additionalProperties": false
         },
         "excluded_locations": {
          "type": "object",
          "description": "Where the Microsoft Ads campaign must NOT serve — the UI's Negative Locations editor under Advanced Settings. ⚠️ COUNTRY-LEVEL ONLY: the platform stores exclusions as bare location ids with no country/state discriminator and state ids overlap country ids, so an excluded state would be read back as an unrelated country. To narrow within a country, target the states you want via `locations.states` instead.",
          "properties": {
           "countries": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "Country NAMES to exclude, e.g. [\"India\", \"Pakistan\"]."
           }
          },
          "additionalProperties": false
         }
        },
        "required": [
         "keywords",
         "ads",
         "offers"
        ]
       }
      },
      "required": [
       "campaignType",
       "name",
       "startDate",
       "endDate",
       "budgetGroup"
      ],
      "additionalProperties": false
     }
    },
    "required": [
     "campaign_data"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_native_structure_campaign",
   "title": "Create Campaign — Channel First / Native Structure (N×N×N)",
   "category": "campaigns",
   "risk": "write",
   "summary": "Create a multi-channel campaign using the platform's \"Channel First Structure\" (N×N×N) — the UI also labels it \"Native Structure\" / \"Platform-First Structure\".",
   "description": "Create a multi-channel campaign using the platform's \"Channel First Structure\" (N×N×N) — the UI also labels it \"Native Structure\" / \"Platform-First Structure\". Your audiences, ads, and offers are organized into the ad platform's NATIVE structure (the WizNativeAdContainer entity) and optimized TOGETHER by the platform's own algorithm — ~1 experiment per channel (LinkedIn is the only exception: one experiment per container), with unified reporting and pacing. You specify the structure as explicit per-container groupings (each container is one audience + its `ad_offers` rows); the \"N×N×N\" label refers to the native structure holding your full set of audiences, ads, and offers together (it is NOT one isolated experiment per combination — that's Precision Optimization, `create_campaign`). Platform product copy: \"Traditional hierarchical setup that leverages native platform algorithms while maintaining Metadata's pacing control, unified user experience and reporting.\" Sets channel.structureType=NATIVE.\n\n                    KEYWORDS (for tool discovery — many users ask by the math, not the product name): NxNxN, N×N×N, \"NxNxN campaign\", Channel First Structure, Native Structure, Platform-First Structure, channel-first, platform-first, native platform structure, WizNativeAdContainer, container-based campaign, traditional / hierarchical structure, unified reporting.\n\n                    NAMING CONVENTION (from the platform UI's \"Campaign Structure\" selector — match these exact labels):\n                      • \"Channel First Structure (NxNxN)\" = THIS tool (`create_native_structure_campaign`); the UI also shows \"Native Structure\" / \"Platform-First Structure\". Audiences/ads/offers grouped in the platform's native structure and optimized together (~1 experiment per channel). \"native\" / \"platform\" here = the native ad platform (LinkedIn / Facebook / Google) whose algorithms this leans on — and structureType=NATIVE matches.\n                      • \"Precision Optimization (1x1x1)\" = the OTHER tool, `create_campaign` — each audience×ad×offer combination tested as its own isolated experiment. Metadata's signature approach; the DEFAULT.\n                    Routing anchor: \"native\" / \"channel first\" / \"platform first\" / \"NxNxN\" → this tool; \"precision\" / \"1x1x1\" → create_campaign.\n\n                    ════════════════════════════════════════════════════════════════════════════════\n                    ⚠️ TWO CAMPAIGN-CREATION TOOLS EXIST — READ BEFORE PICKING ONE ⚠️\n                    ════════════════════════════════════════════════════════════════════════════════\n                    🔴 REMEMBER — HOW TO CHOOSE THE STRUCTURE (the user's decision always wins):\n                      1. If the user explicitly names one, RESPECT IT — \"native\" / \"Native Structure\" / \"Channel First\" / \"Platform-First\" / \"NxNxN\" → create_native_structure_campaign (this tool); \"precision\" / \"Precision Optimization\" / \"1x1x1\" → create_campaign.\n                      2. ELSE, if the request involves MANY ads AND audiences (Precision Optimization makes one experiment per combination — a large experiment count), ASK the user which structure they want before creating — do not guess.\n                      3. OTHERWISE, DEFAULT to Precision Optimization (1×1×1) → create_campaign (NOT this tool).\n                    The two build structurally different, non-swappable campaigns — so once the user decides, respect that decision for the rest of the campaign.\n\n                    USE THIS TOOL (`create_native_structure_campaign`) — \"Channel First\" / \"Native Structure\" (N×N×N) — WHEN:\n                      • The user says \"native\", \"Native Structure\", \"Channel First Structure\", \"channel-first\", \"Platform-First Structure\", \"platform-first\", \"NxNxN\", \"N×N×N\", \"WizNativeAdContainer\", or asks for the traditional / hierarchical / native-platform structure.\n                      • The user wants the ad platform's native structure to group and optimize audiences/ads/offers together (~1 experiment per channel, unified reporting) rather than an isolated experiment per combination.\n                      • The user provides explicit per-container groupings of one audience + its (ad, offer) pairs.\n\n                    USE `create_campaign` (\"Precision Optimization\", 1×1×1) INSTEAD — DO NOT USE THIS TOOL — WHEN:\n                      • The user says \"Precision Optimization\", \"precision\", \"1x1x1\", \"1×1×1\", or wants every audience×ad×offer combination tested separately in its own experiment.\n                      • The user wants maximum budget control / fastest per-combination learning.\n                      • No structure was specified — Precision Optimization (`create_campaign`) is the DEFAULT.\n\n                    If you're unsure whether the user wants isolated per-combination experiments (→ `create_campaign`, Precision Optimization) or the native-platform grouped structure (→ this tool, Channel First), ASK which structure they want — switching modes after creation requires rebuilding the campaign from scratch.\n\n                    The two tools produce structurally different campaigns on the backend (channel.structureType = METADATA for create_campaign, = NATIVE for this tool) and are NOT swappable mid-campaign. Pick correctly the first time.\n                    ════════════════════════════════════════════════════════════════════════════════\n\n                    WHEN TO USE THIS TOOL:\n                    - The user explicitly asks for \"Channel First Structure\" / \"Native Structure\" / \"Platform-First Structure\" / N×N×N / WizNativeAdContainer / the native-platform structure.\n                    - You want the ad platform's algorithm to optimize audiences/ads/offers together under a unified structure (~1 experiment per channel) instead of isolating every combination.\n                    - You want a compact, platform-managed experiment footprint with unified reporting.\n\n                    WHEN NOT TO USE THIS TOOL:\n                    - The user wants \"Precision Optimization\" (1×1×1) — every combination tested as its own isolated experiment — use `create_campaign`.\n                    - No structure was specified (default is Precision Optimization) — use `create_campaign`.\n\n                    CAMPAIGN TYPES (same as create_campaign):\n                    1. \"Lead Gen\" — only Lead Gen (LG) offers allowed in any container.\n                    2. \"Brand Awareness\" — both LG and LP offers allowed.\n\n                    ⚠️ OFFER-CHANNEL CHANNEL-LOCK STILL APPLIES ⚠️\n                    Lead Gen offers are channel-locked at the offer's creation time. A container under `linkedin.containers` MUST reference an offer whose data.channel='LINKEDIN' (or any Landing Page offer). Cross-channel reuse silently produces a non-serving campaign. The server does NOT validate the pairing. Same rules as create_campaign:\n                    - linkedin.containers[i].ad_offers[].offer   → LINKEDIN-locked LG OR any LP\n                    - facebook.containers[i].ad_offers[].offer   → FACEBOOK-locked LG OR any LP\n                    - instagram.containers[i].ad_offers[].offer  → FACEBOOK/INSTAGRAM-locked LG OR any LP\n                    - google.containers[i].ad_offers[].offer     → Landing Page offers ONLY (Google Ads has no native LG form support)\n                    - reddit.containers[i].ad_offers[].offer     → Landing Page offers ONLY (Reddit has no native LG form support)\n                    - microsoft.containers[i].ad_offers[].offer  → MICROSOFT_ADS-locked LG OR any LP\n\n                    Each container is one audience × a LIST of (ad, offer) pairs (`ad_offers`, 1+). Put several pairs in one container to group multiple ads/offers under the same audience — the platform's native ad-set with multiple Ad/Offer rows. \"1 audience → 3 ads → 1 offer\" is ONE container whose `ad_offers` holds 3 pairs that repeat the same `offer` id. (Each pair is exactly one ad with one offer; to run the same audience as two separate ad-sets, send two containers instead.)\n\n                    ⚠️ SEARCH CHANNELS ARE KEYWORD-FIRST — `audience` IS OPTIONAL FOR GOOGLE ADS & MICROSOFT ADS ⚠️\n                    Google/Microsoft search ad groups are keyword-targeted, so a search container may OMIT `audience` and run keyword-only (just keywords + ad_offers). The platform allows audience-less native search containers, so this tool no longer forces an audience there. `audience` STAYS REQUIRED for the audience-targeted social channels (LinkedIn / Facebook / Instagram / Reddit). A search container with neither an audience nor keywords has nothing to target and is rejected.\n\n                    REQUIRED FIELDS:\n                    - campaignType: \"Lead Gen\" | \"Brand Awareness\"\n                    - name: unique campaign name\n                    - startDate / endDate: YYYY-MM-DD\n                    - budgetGroup: budget group name\n                    - At least one channel block with a non-empty containers list\n\n                    PER-CHANNEL SHAPE:\n                    Every channel block (linkedin / facebook / instagram / google / reddit / microsoft) takes:\n                      containers: [\n                        {\n                          name: \"string (<=255 chars) — visible in the platform UI\",\n                          audience: \"audience name (resolved server-side via the same matched-poll create_campaign uses). REQUIRED for LinkedIn/Facebook/Instagram/Reddit; OPTIONAL for Google/Microsoft search (omit it for keyword-only ad groups)\",\n                          ad_offers: [ {ad: <int WizCreative ID>, offer: <int offer ID>}, ... ]   // 1+ Ad/Offer pairs sharing this container's audience; repeat the same offer id to run several ads on it,\n                          target_group: \"optional Targeting Group name (singular — one per container)\",\n                          exclude_audience: \"optional audience name to use as the exclude group for this container\"\n                        }\n                      ]\n\n                    SEARCH-CHANNEL EXTRAS (Google Ads + Microsoft Ads, per container):\n                      keywords: [\"string\", ...]            // free-text phrases — created in the keyword library and grouped into this container's keyword group\n                      keyword_ids: [<int>, ...]            // optional; existing keyword-library IDs (from list_keywords) to put in this container's group — unioned with `keywords`\n                      match_type: \"PHRASE\"|\"BROAD\"|\"EXACT\" // optional; match type for this container's keyword group (default PHRASE)\n                      negative_keyword_lists: [\"name\", ...] // optional; applied at the channel level\n                    On a search channel `audience` is OPTIONAL: a keyword-only container (keywords + ad_offers, no audience) is valid and is how non-brand / pure-keyword search is normally structured. You can still pass an audience to layer one on. The keyword group is created and linked to the container automatically. A search container with neither an audience nor keywords (`keywords`/`keyword_ids`) is rejected (nothing to target).\n\n                    AUDIENCE NAME RESOLUTION:\n                    - `audience` and `exclude_audience` are NAMES, not IDs. Same lookup path as create_campaign — the tool waits up to ~20s per channel for a freshly-created audience to become attachable (firmographics + FB/IG can take 8-15s).\n                    - Audience names are resolved BEFORE any container elements are created: a container whose `audience` cannot be resolved is dropped up front (no keyword group or other element is left behind for it) and the failure is reported in the response `errors[]`. The error states whether retrying can help (\"may still be matching\") or not (\"not available on this channel\") — do NOT blind-retry the latter. The rest of the campaign still gets built.\n                    - On Google/Microsoft search containers `audience` may be omitted entirely (keyword-only) — only names that are actually provided are resolved, so leaving it out costs no polling and raises no error.\n\n                    AD AND OFFER ARE IDS (one `ad_offers` pair = one ad + one offer):\n                    - `ad` is the ID returned by `create_update_image_ad` or `create_update_video_ad`.\n                    - `offer` is the ID returned by `create_update_offer`. The tool looks up each unique offer to discover whether it's a Landing Page or Lead Gen Form and routes it into the right `nativeAds[].landingPageId` vs `nativeAds[].leadGenFormId` slot. Every pair becomes one `nativeAds[]` entry under the container.\n\n                    BID STRATEGY (automatic):\n                    Same defaults as create_campaign:\n                      - LinkedIn / Facebook / Instagram → AUTO_BID\n                      - Google Ads / Microsoft Ads      → TARGET_SPEND (device modifiers reset)\n                      - Reddit                          → objective-driven, mirroring the platform UI:\n                        Lead Gen forces CLICKS (Traffic) + AUTO_BID; Brand Awareness keeps\n                        IMPRESSIONS + MANUAL_BID. Adjust later with update_reddit_channel_settings.\n\n                    WHAT THE TOOL DOES FOR YOU (single call, no follow-up needed):\n                    Creates the campaign shell and its budget group, enables each requested\n                    channel and sets it to the NATIVE structure, builds the per-container\n                    keyword group on Google / Microsoft Ads search channels, creates one\n                    native ad container per grouping with its ad/offer pairs, applies the\n                    bid strategy, and sets the schedule. The response reflects the final\n                    campaign state — you do not need to sequence or repeat any of this.\n\n                    RESPONSE: Same top-level shape as create_campaign (`success`, `campaign_id`, `campaign_url`, `status`, `optimization_group`, `summary`, `full_response`). The per-channel breakdown returns a `containers` array — `[{id, name, experiment_id, audience_group_id, targeting_group_id, group_exclude_id, keyword_group_id, native_ads: [{id, ad_id, landing_page_id, lead_gen_form_id}]}]` — instead of the legacy `audiences`/`ad_groups`/`lead_gen_forms` arrays. On partial failures, returns success=false plus `errors[]` with per-container detail.\n\n                    EXAMPLE (Lead Gen, LinkedIn-only, two containers):\n                    create_native_structure_campaign(campaign_data={\n                        \"campaignType\": \"Lead Gen\",\n                        \"name\": \"Q3_ABM_Native_2026\",\n                        \"startDate\": \"2026-06-01\",\n                        \"endDate\": \"2026-08-31\",\n                        \"budgetGroup\": \"BoFu\",\n                        \"linkedin\": {\n                            \"containers\": [\n                                {\"name\": \"IT Mgrs > Demo > Form\",\n                                 \"audience\": \"IT Managers - NA\",\n                                 \"ad_offers\": [{\"ad\": 156502, \"offer\": 57538}]},\n                                {\"name\": \"CTOs > Demo > Form\",\n                                 \"audience\": \"CTOs - Tech\",\n                                 \"ad_offers\": [{\"ad\": 156485, \"offer\": 57504}]}\n                            ]\n                        }\n                    })\n\n                    EXAMPLE (one audience → 3 ads on the SAME offer, ONE container — the multi-ad ad-set):\n                    create_native_structure_campaign(campaign_data={\n                        \"campaignType\": \"Brand Awareness\",\n                        \"name\": \"Q3_3Ads_1Offer_2026\",\n                        \"startDate\": \"2026-06-01\",\n                        \"endDate\": \"2026-07-31\",\n                        \"budgetGroup\": \"Brand\",\n                        \"facebook\": {\n                            \"containers\": [\n                                {\"name\": \"ICP > 3 creatives > TOF offer\",\n                                 \"audience\": \"26-Q2-ICP-AllPersonas-NA\",\n                                 \"ad_offers\": [\n                                     {\"ad\": 210801, \"offer\": 67269},\n                                     {\"ad\": 210802, \"offer\": 67269},\n                                     {\"ad\": 210803, \"offer\": 67269}\n                                 ]}\n                            ]\n                        }\n                    })\n\n                    EXAMPLE (Brand Awareness, Google + LinkedIn, with keywords):\n                    create_native_structure_campaign(campaign_data={\n                        \"campaignType\": \"Brand Awareness\",\n                        \"name\": \"Q3_Brand_Native_2026\",\n                        \"startDate\": \"2026-06-01\",\n                        \"endDate\": \"2026-07-31\",\n                        \"budgetGroup\": \"Brand\",\n                        \"linkedin\": {\n                            \"containers\": [\n                                {\"name\": \"Professionals A\",\n                                 \"audience\": \"Professional Audience\",\n                                 \"ad_offers\": [{\"ad\": 173669, \"offer\": 57538}]}\n                            ]\n                        },\n                        \"google\": {\n                            \"containers\": [\n                                {\"name\": \"CRM Search\",\n                                 \"audience\": \"Tech Decision Makers\",\n                                 \"ad_offers\": [{\"ad\": 175260, \"offer\": 57600}],\n                                 \"keywords\": [\"crm software\", \"lead gen tools\"],\n                                 \"negative_keyword_lists\": [\"Competitor Brands\"]}\n                            ]\n                        }\n                    })\n\n                    EXAMPLE (Brand Awareness, Google keyword-only — NO audience, pure non-brand search):\n                    create_native_structure_campaign(campaign_data={\n                        \"campaignType\": \"Brand Awareness\",\n                        \"name\": \"Q3_NonBrand_Search_2026\",\n                        \"startDate\": \"2026-06-01\",\n                        \"endDate\": \"2026-07-31\",\n                        \"budgetGroup\": \"Search\",\n                        \"google\": {\n                            \"containers\": [\n                                {\"name\": \"Incident Response\",\n                                 \"ad_offers\": [{\"ad\": 210804, \"offer\": 67269}],\n                                 \"keywords\": [\"incident response platform\", \"on-call alerting\"],\n                                 \"negative_keyword_lists\": [\"Competitor Brands\"]}\n                                # note: no \"audience\" key — keyword-targeted ad group\n                            ]\n                        }\n                    })\n\n                    BEST PRACTICES:\n                    1. Decide each container's audience and its `ad_offers` pairs up front — this tool will not invent pairings for you. Group several ads under one audience by adding pairs to `ad_offers`. On Google/Microsoft search, audience-less (keyword + ad_offers) containers are valid; pure non-brand search normally has no audience. On Reddit, targeting is the `target_group` (create_reddit_target_group) and `audience` is optional — an audience only exists on a channel the account has CONNECTED, so naming one on a disconnected Reddit loses the container.\n                    2. Use descriptive container names — they show up in the platform UI as the ad-set/ad-group label.\n                    3. For Google Ads, put related keywords in the same container (each container becomes one keyword group). Don't pile dozens of unrelated keywords into a single container.\n                    4. Use Brand Awareness for any container that needs Landing Page offers — Lead Gen campaigns reject LP offers.\n                    5. The campaign URL is returned in the response: https://platform.metadata.io/hub/advertise/campaigns/{campaign_id}",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_data": {
      "type": "object",
      "description": "Complete Channel First / Native Structure (N×N×N) campaign configuration. Each channel takes a `containers` list; each container groups one audience with an `ad_offers` list of {ad, offer} pairs.",
      "properties": {
       "campaignType": {
        "type": "string",
        "enum": [
         "Lead Gen",
         "Brand Awareness"
        ],
        "description": "Campaign goal: Lead Gen (CPL) or Brand Awareness (CTR)."
       },
       "name": {
        "type": "string",
        "description": "Unique campaign name."
       },
       "startDate": {
        "type": "string",
        "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
        "description": "Campaign start date (YYYY-MM-DD)."
       },
       "endDate": {
        "type": "string",
        "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
        "description": "Campaign end date (YYYY-MM-DD)."
       },
       "budgetGroup": {
        "type": "string",
        "description": "Budget group name (e.g., 'BoFu', 'ToFu', 'Brand')."
       },
       "linkedin": {
        "type": "object",
        "description": "LinkedIn Precision-Optimization block. LinkedIn creates one experiment per container.",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "containers": {
          "type": "array",
          "minItems": 1,
          "items": {
           "type": "object",
           "properties": {
            "name": {
             "type": "string",
             "maxLength": 255,
             "description": "Container/ad-set name shown in the platform UI."
            },
            "audience": {
             "type": "string",
             "description": "Audience name. Resolved server-side (waits up to ~20s for a freshly-created audience)."
            },
            "ad_offers": {
             "type": "array",
             "minItems": 1,
             "description": "Ad—Offer pairs for this container (the native ad-set's Ad/Offer rows). Each pair renders one ad with one offer; every pair shares this container's single audience. To run 3 ads against the same offer, send 3 pairs that repeat the same `offer` id.",
             "items": {
              "type": "object",
              "properties": {
               "ad": {
                "type": "integer",
                "description": "WizCreative ID from create_update_image_ad / create_update_video_ad."
               },
               "offer": {
                "type": "integer",
                "description": "Offer ID from create_update_offer. ⚠️ Lead Gen offers must be LINKEDIN-locked."
               }
              },
              "required": [
               "ad",
               "offer"
              ]
             }
            },
            "target_group": {
             "type": "string",
             "description": "Optional Targeting Group name for this container."
            },
            "exclude_audience": {
             "type": "string",
             "description": "Optional audience name to use as the container's exclude group."
            }
           },
           "required": [
            "name",
            "audience",
            "ad_offers"
           ]
          }
         }
        },
        "required": [
         "containers"
        ]
       },
       "facebook": {
        "type": "object",
        "description": "Facebook Precision-Optimization block. All containers share one experiment.",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "containers": {
          "type": "array",
          "minItems": 1,
          "items": {
           "type": "object",
           "properties": {
            "name": {
             "type": "string",
             "maxLength": 255,
             "description": "Container/ad-set name shown in the platform UI."
            },
            "audience": {
             "type": "string"
            },
            "ad_offers": {
             "type": "array",
             "minItems": 1,
             "description": "Ad—Offer pairs for this container (the native ad-set's Ad/Offer rows). Each pair renders one ad with one offer; every pair shares this container's single audience. To run 3 ads against the same offer, send 3 pairs that repeat the same `offer` id.",
             "items": {
              "type": "object",
              "properties": {
               "ad": {
                "type": "integer",
                "description": "WizCreative ID from create_update_image_ad / create_update_video_ad."
               },
               "offer": {
                "type": "integer",
                "description": "⚠️ Lead Gen offers must be FACEBOOK-locked."
               }
              },
              "required": [
               "ad",
               "offer"
              ]
             }
            },
            "target_group": {
             "type": "string",
             "description": "Optional Targeting Group name for this container."
            },
            "exclude_audience": {
             "type": "string",
             "description": "Optional audience name to use as the container's exclude group."
            }
           },
           "required": [
            "name",
            "audience",
            "ad_offers"
           ]
          }
         }
        },
        "required": [
         "containers"
        ]
       },
       "instagram": {
        "type": "object",
        "description": "Instagram Precision-Optimization block. All containers share one experiment.",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "containers": {
          "type": "array",
          "minItems": 1,
          "items": {
           "type": "object",
           "properties": {
            "name": {
             "type": "string",
             "maxLength": 255,
             "description": "Container/ad-set name shown in the platform UI."
            },
            "audience": {
             "type": "string"
            },
            "ad_offers": {
             "type": "array",
             "minItems": 1,
             "description": "Ad—Offer pairs for this container (the native ad-set's Ad/Offer rows). Each pair renders one ad with one offer; every pair shares this container's single audience. To run 3 ads against the same offer, send 3 pairs that repeat the same `offer` id.",
             "items": {
              "type": "object",
              "properties": {
               "ad": {
                "type": "integer",
                "description": "WizCreative ID from create_update_image_ad / create_update_video_ad."
               },
               "offer": {
                "type": "integer",
                "description": "⚠️ Lead Gen offers must be FACEBOOK- or INSTAGRAM-locked."
               }
              },
              "required": [
               "ad",
               "offer"
              ]
             }
            },
            "target_group": {
             "type": "string",
             "description": "Optional Targeting Group name for this container."
            },
            "exclude_audience": {
             "type": "string",
             "description": "Optional audience name to use as the container's exclude group."
            }
           },
           "required": [
            "name",
            "audience",
            "ad_offers"
           ]
          }
         }
        },
        "required": [
         "containers"
        ]
       },
       "google": {
        "type": "object",
        "description": "Google Ads Precision-Optimization block. All containers share one experiment. Each container can carry its own keyword group.",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "containers": {
          "type": "array",
          "minItems": 1,
          "items": {
           "type": "object",
           "properties": {
            "name": {
             "type": "string",
             "maxLength": 255,
             "description": "Container/ad-set name shown in the platform UI."
            },
            "audience": {
             "type": "string",
             "description": "OPTIONAL for Google Ads. Google search ad groups are keyword-targeted, so a container may omit `audience` and run keyword-only — the platform allows audience-less native search containers (a WizNativeAdContainer with a null audience group). Provide an audience name only when you want to layer an audience onto the search container."
            },
            "ad_offers": {
             "type": "array",
             "minItems": 1,
             "description": "Ad—Offer pairs for this container (the native ad-set's Ad/Offer rows). Each pair renders one ad with one offer; every pair shares this container's single audience. To run 3 ads against the same offer, send 3 pairs that repeat the same `offer` id.",
             "items": {
              "type": "object",
              "properties": {
               "ad": {
                "type": "integer",
                "description": "WizCreative ID from create_update_image_ad / create_update_video_ad."
               },
               "offer": {
                "type": "integer",
                "description": "⚠️ Landing Page offers ONLY — Google Ads has no native Lead Gen form support."
               }
              },
              "required": [
               "ad",
               "offer"
              ]
             }
            },
            "target_group": {
             "type": "string",
             "description": "Optional Targeting Group name for this container."
            },
            "exclude_audience": {
             "type": "string",
             "description": "Optional audience name to use as the container's exclude group."
            },
            "keywords": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Free-text keyword phrases for this container's keyword group. A search container with neither an audience nor keywords is rejected."
            },
            "keyword_ids": {
             "type": "array",
             "items": {
              "type": "integer"
             },
             "description": "Existing keyword-library IDs (from list_keywords) to include in this container's keyword group; unioned with `keywords`."
            },
            "match_type": {
             "type": "string",
             "enum": [
              "PHRASE",
              "BROAD",
              "EXACT"
             ],
             "description": "Match type for this container's keyword group (default PHRASE)."
            },
            "negative_keyword_lists": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Names of existing negative keyword lists to attach channel-wide."
            }
           },
           "required": [
            "name",
            "ad_offers"
           ]
          }
         },
         "locations": {
          "type": "object",
          "description": "Where the Google Ads campaign SERVES — the UI's Location picker. ⚠️ OMITTING THIS LEAVES THE PLATFORM DEFAULT OF UNITED STATES. Set it whenever the user names a geography.",
          "properties": {
           "countries": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "Country NAMES to target, e.g. [\"United States\", \"Canada\"]. Common shorthand resolves (\"USA\", \"UK\"). An unrecognised name is rejected, not skipped."
           },
           "states": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "US state NAMES to target, e.g. [\"Texas\", \"California\"]. US-only. Naming states without countries keeps the channel's current countries."
           }
          },
          "additionalProperties": false
         },
         "excluded_locations": {
          "type": "object",
          "description": "Where the Google Ads campaign must NOT serve — the UI's Negative Locations editor under Advanced Settings. ⚠️ COUNTRY-LEVEL ONLY: the platform stores exclusions as bare location ids with no country/state discriminator and state ids overlap country ids, so an excluded state would be read back as an unrelated country. To narrow within a country, target the states you want via `locations.states` instead.",
          "properties": {
           "countries": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "Country NAMES to exclude, e.g. [\"India\", \"Pakistan\"]."
           }
          },
          "additionalProperties": false
         }
        },
        "required": [
         "containers"
        ]
       },
       "reddit": {
        "type": "object",
        "description": "Reddit native-structure block. All containers share one experiment. Reddit targets by Targeting Group (communities / interests / locations, from create_reddit_target_group), so a container carries `target_group` and needs no `audience`. Channel settings (objective, comments, placements, conversion pixel) get UI-equivalent defaults; adjust afterwards with update_reddit_channel_settings.",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "containers": {
          "type": "array",
          "minItems": 1,
          "items": {
           "type": "object",
           "properties": {
            "name": {
             "type": "string",
             "maxLength": 255,
             "description": "Container/ad-set name shown in the platform UI."
            },
            "audience": {
             "type": "string",
             "description": "OPTIONAL for Reddit. Reddit containers are normally targeted by `target_group` (create_reddit_target_group), which the platform counts as the container's targeting exactly like an audience. Name an audience ONLY when Reddit is connected and the audience already exists on it: the platform distributes an audience only to channels the account has connected, so on a disconnected Reddit no audience row can ever exist and naming one loses the whole container. Check with get_integrations_status when unsure."
            },
            "ad_offers": {
             "type": "array",
             "minItems": 1,
             "description": "Ad—Offer pairs for this container (the native ad-set's Ad/Offer rows). Each pair renders one ad with one offer; every pair shares this container's single audience. To run 3 ads against the same offer, send 3 pairs that repeat the same `offer` id.",
             "items": {
              "type": "object",
              "properties": {
               "ad": {
                "type": "integer",
                "description": "WizCreative ID from create_update_image_ad / create_update_video_ad."
               },
               "offer": {
                "type": "integer",
                "description": "⚠️ Landing Page offers ONLY — Reddit has no native Lead Gen form support."
               }
              },
              "required": [
               "ad",
               "offer"
              ]
             }
            },
            "target_group": {
             "type": "string",
             "description": "Targeting Group name for this container (created via create_reddit_target_group for Reddit, create_target_group for LinkedIn). Targets the container on its own — no audience needed — and needs no channel connection, so it is the way to build a container on a channel that is not connected yet."
            },
            "exclude_audience": {
             "type": "string",
             "description": "Optional audience name to use as the container's exclude group."
            }
           },
           "required": [
            "name",
            "ad_offers"
           ]
          }
         }
        },
        "required": [
         "containers"
        ]
       },
       "microsoft": {
        "type": "object",
        "description": "Microsoft Ads native-structure block. Same shape as Google (search channel: per-container keyword groups + channel-level negative keyword lists), and the wizard pairs MICROSOFT_ADS with GOOGLE_ADS for the TARGET_SPEND bid default. Unlike Google, Microsoft Ads supports BOTH Landing Page and Lead Gen offers (the latter must be channel-locked to MICROSOFT_ADS).",
        "properties": {
         "dailyBudget": {
          "type": "number",
          "minimum": 10,
          "description": "Daily budget in USD for this channel (platform minimum $10). Applied through the platform's own channel budget endpoint; without it the channel keeps the platform default of $50/day. Derive from the approved split (e.g. monthly channel budget / 30, rounded to cents)."
         },
         "containers": {
          "type": "array",
          "minItems": 1,
          "items": {
           "type": "object",
           "properties": {
            "name": {
             "type": "string",
             "maxLength": 255,
             "description": "Container/ad-set name shown in the platform UI."
            },
            "audience": {
             "type": "string",
             "description": "OPTIONAL for Microsoft Ads. Like Google, Microsoft search ad groups are keyword-targeted, so a container may omit `audience` and run keyword-only (the platform allows audience-less native search containers). Provide an audience name only when layering an audience onto the search container."
            },
            "ad_offers": {
             "type": "array",
             "minItems": 1,
             "description": "Ad—Offer pairs for this container (the native ad-set's Ad/Offer rows). Each pair renders one ad with one offer; every pair shares this container's single audience. To run 3 ads against the same offer, send 3 pairs that repeat the same `offer` id.",
             "items": {
              "type": "object",
              "properties": {
               "ad": {
                "type": "integer",
                "description": "WizCreative ID from create_update_image_ad / create_update_video_ad."
               },
               "offer": {
                "type": "integer",
                "description": "⚠️ Lead Gen offers must be MICROSOFT_ADS-locked (created with data.channel='MICROSOFT_ADS'); Landing Page offers are universal."
               }
              },
              "required": [
               "ad",
               "offer"
              ]
             }
            },
            "target_group": {
             "type": "string",
             "description": "Optional Targeting Group name for this container."
            },
            "exclude_audience": {
             "type": "string",
             "description": "Optional audience name to use as the container's exclude group."
            },
            "keywords": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Free-text keyword phrases for this container's keyword group. A search container with neither an audience nor keywords is rejected."
            },
            "keyword_ids": {
             "type": "array",
             "items": {
              "type": "integer"
             },
             "description": "Existing keyword-library IDs (from list_keywords) to include in this container's keyword group; unioned with `keywords`."
            },
            "match_type": {
             "type": "string",
             "enum": [
              "PHRASE",
              "BROAD",
              "EXACT"
             ],
             "description": "Match type for this container's keyword group (default PHRASE)."
            },
            "negative_keyword_lists": {
             "type": "array",
             "items": {
              "type": "string"
             },
             "description": "Names of existing negative keyword lists to attach channel-wide."
            }
           },
           "required": [
            "name",
            "ad_offers"
           ]
          }
         },
         "locations": {
          "type": "object",
          "description": "Where the Microsoft Ads campaign SERVES — the UI's Location picker. ⚠️ OMITTING THIS LEAVES THE PLATFORM DEFAULT OF UNITED STATES. Set it whenever the user names a geography.",
          "properties": {
           "countries": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "Country NAMES to target, e.g. [\"United States\", \"Canada\"]. Common shorthand resolves (\"USA\", \"UK\"). An unrecognised name is rejected, not skipped."
           },
           "states": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "US state NAMES to target, e.g. [\"Texas\", \"California\"]. US-only. Naming states without countries keeps the channel's current countries."
           }
          },
          "additionalProperties": false
         },
         "excluded_locations": {
          "type": "object",
          "description": "Where the Microsoft Ads campaign must NOT serve — the UI's Negative Locations editor under Advanced Settings. ⚠️ COUNTRY-LEVEL ONLY: the platform stores exclusions as bare location ids with no country/state discriminator and state ids overlap country ids, so an excluded state would be read back as an unrelated country. To narrow within a country, target the states you want via `locations.states` instead.",
          "properties": {
           "countries": {
            "type": "array",
            "items": {
             "type": "string"
            },
            "description": "Country NAMES to exclude, e.g. [\"India\", \"Pakistan\"]."
           }
          },
          "additionalProperties": false
         }
        },
        "required": [
         "containers"
        ]
       }
      },
      "required": [
       "campaignType",
       "name",
       "startDate",
       "endDate",
       "budgetGroup"
      ],
      "additionalProperties": false
     }
    },
    "required": [
     "campaign_data"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_update_offer",
   "title": "Create or Update Offer",
   "category": "campaigns",
   "risk": "destructive",
   "summary": "Create or update Lead Gen or Landing Page offers in Metadata platform.",
   "description": "Create or update Lead Gen or Landing Page offers in Metadata platform.\n    Returns direct URL to access the offer in the platform.\n\n    OPERATION MODE:\n    - CREATE: When 'id' field is NOT included in data\n    - UPDATE: When 'id' field IS included in data\n\n    OFFER TYPES:\n    1. Lead Gen - Native forms rendered inside LinkedIn, Facebook, or Instagram. User never leaves the platform.\n    2. Landing Page - External page hosted on the advertiser's website. Works on EVERY channel (LinkedIn, Facebook, Instagram, Google Ads, Reddit, Microsoft Ads).\n\n    ⚠️ LEAD GEN ON LINKEDIN / META CAN BE DONE TWO WAYS — pick the right one for the user's intent:\n    - Native Lead Gen form (offer_type='Lead Gen', data.channel=<channel>, goal=CPL): user fills the form INSIDE LinkedIn/Meta. Lower friction, higher conversion, but leads stay in the ad platform until synced. Channel-locked — see below.\n    - Landing Page lead capture (offer_type='Landing Page', goal=CPL, landingPageUrl + thankYouUrl + EMAIL field): user clicks through to the advertiser's own landing page and submits the form there. Choose this when the user wants branded landing pages, tracking pixels, or server-side enrichment. Channel-agnostic — one offer works on every channel.\n    Both flows count as Lead Gen (goal=CPL). Default to native Lead Gen for LinkedIn/Meta unless the user explicitly wants their own landing page.\n\n    ⚠️ CHANNEL-LOCK ON NATIVE LEAD GEN OFFERS (read this BEFORE planning your offer batch) ⚠️\n    The data.channel value you set on a Lead Gen offer PERMANENTLY locks the returned offer_id to that one channel. The same offer_id CANNOT be used in another channel's slot inside create_campaign / add_and_edit_campaign_elements — those tools accept the mismatch SILENTLY and produce a broken campaign that returns a successful response but cannot serve. Landing Page offers are NOT channel-locked and can be safely reused across channels.\n\n    What this means for planning:\n    - Want a native Lead Gen \"Demo\" concept on BOTH LinkedIn AND Facebook? You MUST call create_update_offer TWICE for that one concept — once with data.channel='LINKEDIN', once with data.channel='FACEBOOK'. Track both offer_ids and assign each to the matching channel slot.\n    - Three concepts × two native Lead Gen channels = SIX create_update_offer calls. That is correct, not over-creation. The \"2-3 offers\" guidance refers to distinct CONCEPTS, not call count.\n    - Want a single offer that works on every channel including Google? Use offer_type='Landing Page' (omit data.channel — Landing Pages have no channel field and are channel-agnostic). Google Ads ONLY accepts Landing Page offers.\n    - Want Lead Gen on LinkedIn using the advertiser's OWN landing page instead of LinkedIn's native form? Use offer_type='Landing Page' with goal='CPL' — then assign the returned offer_id to linkedin.offers[]. This is fully supported and a single LP offer_id can serve LinkedIn, Meta, Google simultaneously.\n\n    Right after each successful create_update_offer call, write down (offer_id, channel, concept) so you can correctly map them in the create_campaign call later. If you lose track, call get_offer(name=<offer_name>) to look up the offer and read back the channel field before submitting a campaign.\n\n    REQUIRED PARAMETERS:\n    - offer_type: \"Lead Gen\" or \"Landing Page\"\n    - data: Offer configuration object\n      - id: (optional) Include only when updating an existing offer\n\n    IMPORTANT LIMITS:\n    - offerName: MUST be unique AND under 50 characters. If name + timestamp exceeds 49 chars, shorten the base name.\n      ⚠️ ALLOWED CHARACTERS: letters (a-z, A-Z), numbers (0-9), spaces, and these punctuation marks ONLY:\n        _ . ? , : ' / + - = & ; % $ # [ ] | ( )\n      Any other character (em-dashes, smart quotes, emoji, non-ASCII letters, ©, ™, !, @, *, {, }, <, >, \", \\, ^, ~, `) will be REJECTED by the platform on Landing Page offers and stripped by this tool before the request is sent. Stick to ASCII letters/digits/space and the punctuation set above.\n    - offerHeader: max 60 chars\n    - offerDetails: max 160 chars\n    - thankYouMessage: max 60 chars (LinkedIn)\n    - privacyText: max 70 chars (Facebook/Instagram)\n    - Fields should always be included\n\n    CHANNEL-SPECIFIC REQUIREMENTS:\n\n    LinkedIn Lead Gen (offer_type=\"Lead Gen\", channel=\"LINKEDIN\"):\n    - REQUIRED: channel, offerName, offerHeader, thankYouMessage, thankYouUrl, privacyUrl, privacyText, fields\n    - CTA (linkedinCTA): REQUIRED by the backend. Defaults to DOWNLOAD_NOW when omitted. Must be one of\n      LEARN_MORE | VIEW_NOW | DOWNLOAD_NOW | TRY_NOW | VISIT_COMPANY_WEBSITE. Any other value silently falls\n      back to VISIT_COMPANY_WEBSITE on LinkedIn's side — always set linkedinCTA explicitly for predictable behavior.\n    - offerDetails: OPTIONAL but STRONGLY RECOMMENDED. Populates the description shown above the form on LinkedIn\n      and improves conversion. Not mandatory on LinkedIn (unlike FB/IG) but there's no reason to skip it.\n\n    Facebook/Instagram Lead Gen (offer_type=\"Lead Gen\", channel=\"FACEBOOK\" or \"INSTAGRAM\"):\n     - REQUIRED: channel, offerName, offerHeader, offerDetails, thankYouHeadline,\n       thankYouMessage, thankYouDescription, thankYouPageButtonText, websiteUrl, privacyUrl\n     - Meta's thank-you screen has THREE distinct text blocks — populate all three for a complete\n       UX:\n         • thankYouHeadline → title\n         • thankYouMessage  → short_message (small text under the headline)\n         • thankYouDescription → body (longer copy). Falls back to thankYouMessage if omitted,\n           but supplying both is strongly preferred.\n     - thankYouPageButtonType (enum, default VIEW_WEBSITE). Cross-field rules:\n         • VIEW_WEBSITE → websiteUrl (or thankYouUrl fallback) is required\n         • VIEW_ON_FACEBOOK → thankYouAssetId (library creative ID) is required; without it the\n           backend validation will reject the offer.\n     - CRITICAL: offerDetails is MANDATORY when offerHeader is provided. If the user doesn't\n       specify offer details, generate a brief description based on the offer context.\n       Omitting offerDetails when offerHeader is set will cause an API validation error.\n     - The backend HTTP-probes followUpActionUrl (derived from websiteUrl / thankYouUrl) during\n       validation, so the URL MUST resolve to a live page — not a placeholder.\n     - linkedinCTA is ignored on FB/IG.\n\n    Landing Page (offer_type=\"Landing Page\"):\n    - REQUIRED: offerName, landingPageUrl, thankYouUrl, fields\n    - CRITICAL: fields MUST include an EMAIL entry SPECIFICALLY. The platform's\n      `INVALID_LANDINGPAGE_FORM: Email mapping is required` check is satisfied\n      ONLY by EMAIL — BUSINESS_EMAIL alone does NOT satisfy it and the offer is\n      rejected. Without an email capture, the form cannot deliver a lead. If the\n      user does not mention form fields at all, default to:\n        fields: [{\"name\": \"email\", \"field\": \"EMAIL\"}, {\"name\": \"firstName\", \"field\": \"FIRST_NAME\"}, {\"name\": \"lastName\", \"field\": \"LAST_NAME\"}, {\"name\": \"company\", \"field\": \"COMPANY\"}]\n      BUSINESS_EMAIL may be added as an EXTRA field when the user explicitly wants\n      to filter out personal email domains, but it can NEVER replace EMAIL — keep\n      EMAIL in the form. (The server auto-corrects a lone BUSINESS_EMAIL to EMAIL.)\n\n    AVAILABLE FIELD TYPES BY OFFER TYPE:\n\n    For Lead Gen offers, the supported field types DIFFER BY CHANNEL — and the enum\n    SPELLINGS differ too (LinkedIn PHONE_NUMBER/ZIP_CODE vs Meta PHONE/ZIP). Mixing\n    them up is what causes the \"Cannot deserialize Convo LeadForm JSON\" attach failure.\n    PREFER the channel's DEFAULT-VISIBLE fields (marked [default] below) — they cover\n    the standard B2B form; only reach for the rest when the brief specifically needs them.\n\n    LinkedIn Lead Gen (channel=\"LINKEDIN\") — these 22 types ONLY (source of truth:\n    the platform's LinkedIn Lead Gen Form dialog):\n    [default] \"EMAIL\", \"WORK_EMAIL\", \"FIRST_NAME\", \"LAST_NAME\", \"COMPANY_NAME\", \"LINKEDIN_PROFILE_LINK\"\n    [more]    \"PHONE_NUMBER\", \"WORK_PHONE_NUMBER\", \"COUNTRY\", \"CITY\", \"STATE\", \"ZIP_CODE\", \"JOB_TITLE\",\n              \"JOB_FUNCTION\", \"SENIORITY\", \"INDUSTRY\", \"COMPANY_SIZE\", \"DEGREE\", \"SCHOOL\",\n              \"FIELD_OF_STUDY\", \"GRADUATION_DATE\", \"START_DATE\"\n    LinkedIn has NO field for REVENUE, EMPLOYEES (use COMPANY_SIZE), GENDER, DOB, MESSENGER,\n    STREET_ADDRESS, etc. LinkedIn caps a form at 12 fields.\n\n    Facebook/Instagram Lead Gen (channel=\"FACEBOOK\"/\"INSTAGRAM\") — these 19 types ONLY\n    (source of truth: the platform's FB/IG Lead Gen Form dialog):\n    [default] \"EMAIL\", \"WORK_EMAIL\", \"FIRST_NAME\", \"LAST_NAME\", \"COMPANY_NAME\", \"COUNTRY\"\n    [more]    \"PHONE\", \"WORK_PHONE_NUMBER\", \"STREET_ADDRESS\", \"CITY\", \"STATE\", \"PROVINCE\",\n              \"POST_CODE\", \"ZIP\", \"GENDER\", \"MARITIAL_STATUS\", \"RELATIONSHIP_STATUS\",\n              \"MILITARY_STATUS\", \"JOB_TITLE\"\n    Meta has NO field for REVENUE, EMPLOYEES, COMPANY_SIZE, SENIORITY, INDUSTRY, JOB_FUNCTION,\n    or the education fields (those are LinkedIn-only). \"MARITIAL_STATUS\" is the literal enum\n    (the typo is load-bearing — do not \"fix\" it). No field-count cap on Meta.\n    IMPORTANT: Do NOT use \"BUSINESS_EMAIL\" for Lead Gen - use \"WORK_EMAIL\" instead.\n    IMPORTANT: Do NOT use \"COMPANY\" for Lead Gen - use \"COMPANY_NAME\" instead.\n    IMPORTANT: Do NOT use \"PHONE\" for LinkedIn Lead Gen - use \"PHONE_NUMBER\" instead (auto-corrected if provided).\n    IMPORTANT: Do NOT use \"PHONE_NUMBER\" or \"ZIP_CODE\" for Facebook/Instagram - use \"PHONE\" / \"ZIP\" instead (auto-corrected if provided).\n    IMPORTANT: Custom / free-text questions are NOT supported. Every fields[].field MUST be one of the\n    predefined types for the offer's channel. Do NOT pass \"CUSTOM\", and do NOT invent a field for a concept\n    the brief names but that has no predefined type (e.g. \"monthly transaction volume\", \"budget\", \"ad spend\",\n    \"use case\", \"team size\"). Neither channel has a revenue/spend/budget field — DROP such a field. For\n    headcount: LinkedIn uses COMPANY_SIZE; Meta has no equivalent (drop). This server auto-normalizes near-miss\n    spellings (LinkedIn: EMPLOYEES->COMPANY_SIZE, ZIP->ZIP_CODE, POST_CODE->ZIP_CODE, PROVINCE->STATE; Meta:\n    PHONE_NUMBER->PHONE, ZIP_CODE->ZIP) and then DROPS any field the offer's channel still can't render\n    (logged), so the offer ships with its valid fields instead of failing at campaign-attach — but pass\n    channel-correct fields up front so nothing the user asked for is silently dropped.\n\n    For Landing Page offers (LandingPageFieldType):\n    \"EMAIL\", \"BUSINESS_EMAIL\", \"FIRST_NAME\", \"LAST_NAME\", \"COUNTRY\", \"PHONE\", \"CITY\", \"STATE\", \"ZIP\",\n    \"JOB_TITLE\", \"SENIORITY\", \"COMPANY\", \"COMPANY_DOMAIN\", \"COMPANY_PHONE\", \"EMPLOYEES\", \"INDUSTRY\",\n    \"REVENUE\", \"LINKEDIN_URL\", \"UTM_SOURCE\", \"UTM_MEDIUM\", \"UTM_TERM\", \"UTM_CONTENT\"\n    IMPORTANT: Do NOT use \"COMPANY_NAME\" for Landing Pages - use \"COMPANY\" instead.\n    IMPORTANT: Do NOT use \"WORK_PHONE_NUMBER\" for Landing Pages - use \"PHONE\" instead.\n\n    EXAMPLES:\n\n    CREATE LinkedIn Offer:\n    create_or_update_offer(\"Lead Gen\", {\n        \"channel\": \"LINKEDIN\",\n        \"status\": \"COMPLETED\",\n        \"offerName\": \"Free AI Marketing Audit\",\n        \"offerHeader\": \"Unlock Growth with AI\",\n        \"offerDetails\": \"A 30-minute audit of your current AI stack with tailored recommendations.\",\n        \"linkedinCTA\": \"DOWNLOAD_NOW\",\n        \"thankYouMessage\": \"We'll contact you soon!\",\n        \"thankYouUrl\": \"https://example.com/thanks\",\n        \"privacyUrl\": \"https://example.com/privacy\",\n        \"privacyText\": \"We respect your privacy\",\n        \"fields\": [\n            {\"name\": \"email\", \"field\": \"EMAIL\"},\n            {\"name\": \"firstName\", \"field\": \"FIRST_NAME\"},\n        ]\n    })\n\n    UPDATE LinkedIn Offer:\n    create_or_update_offer(\"Lead Gen\", {\n        \"id\": 62148,\n        \"channel\": \"LINKEDIN\",\n        \"status\": \"COMPLETED\",\n        \"offerName\": \"Free AI Marketing Audit 2025\",\n        \"offerHeader\": \"Unlock Growth with AI\",\n        \"offerDetails\": \"A 30-minute audit of your current AI stack with tailored recommendations.\",\n        \"linkedinCTA\": \"LEARN_MORE\",\n        \"thankYouMessage\": \"We'll contact you soon!\",\n        \"thankYouUrl\": \"https://example.com/thanks\",\n        \"privacyUrl\": \"https://example.com/privacy\",\n        \"privacyText\": \"We respect your privacy\",\n        \"fields\": [\n            {\"name\": \"email\", \"field\": \"EMAIL\"},\n            {\"name\": \"firstName\", \"field\": \"FIRST_NAME\"},\n        ]\n    })\n\n    CREATE Facebook/Instagram Offer:\n    create_or_update_offer(\"Lead Gen\", {\n        \"channel\": \"FACEBOOK\",\n        \"status\": \"COMPLETED\",\n        \"offerName\": \"Free Consultation\",\n        \"offerHeader\": \"Get Expert Advice\",\n        \"offerDetails\": \"Book a 30-minute consultation with our senior strategists.\",\n        \"thankYouHeadline\": \"Thank You!\",\n        \"thankYouMessage\": \"We'll be in touch soon\",\n        \"thankYouDescription\": \"A member of our team will email you within one business day to schedule your session.\",\n        \"thankYouPageButtonText\": \"Visit Website\",\n        \"thankYouPageButtonType\": \"VIEW_WEBSITE\",\n        \"websiteUrl\": \"https://example.com\",\n        \"privacyUrl\": \"https://example.com/privacy\",\n        \"privacyText\": \"Privacy Policy\",\n        \"fields\": [\n            {\"name\": \"email\", \"field\": \"EMAIL\"},\n            {\"name\": \"firstName\", \"field\": \"FIRST_NAME\"},\n        ]\n    })\n\n    CREATE Landing Page Offer:\n    create_or_update_offer(\"Landing Page\", {\n        \"offerName\": \"Free Demo\",\n        \"landingPageUrl\": \"https://example.com/landing\",\n        \"thankYouUrl\": \"https://example.com/thanks\",\n        \"fields\": [\n            {\"name\": \"email\", \"field\": \"EMAIL\"},\n            {\"name\": \"firstName\", \"field\": \"FIRST_NAME\"},\n        ]\n    })\n\n    RESPONSE: Success returns offer_id, offer_url, and configuration details.\n    VALIDATION: Checks URLs start with https://, character limits, required fields per channel.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "offer_type": {
      "type": "string",
      "enum": [
       "Lead Gen",
       "Landing Page"
      ],
      "description": "Type of offer to create"
     },
     "data": {
      "type": "object",
      "description": "Offer configuration data. Required fields depend on offer_type and channel - see description for details.",
      "properties": {
       "id": {
        "type": "integer",
        "description": "offer ID (include for updates, omit for new offers)"
       },
       "channel": {
        "type": "string",
        "enum": [
         "LINKEDIN",
         "FACEBOOK",
         "INSTAGRAM",
         "MICROSOFT_ADS"
        ],
        "description": "Advertising channel for the offer. ⚠️ CHANNEL-LOCK: REQUIRED for Lead Gen offers and PERMANENTLY locks the returned offer_id to this channel — the offer_id cannot be reused in another channel's offers slot inside create_campaign. To run the same Lead Gen concept on multiple channels, you must call create_update_offer once per channel (one with channel='LINKEDIN', another with channel='FACEBOOK', etc.). REDDIT is NOT accepted: Reddit does not support Lead Gen forms, so Reddit campaigns take Landing Page offers only. Omit this field for offer_type='Landing Page' — Landing Page offers are channel-agnostic and reusable across all channels."
       },
       "status": {
        "type": "string",
        "enum": [
         "DRAFT",
         "COMPLETED"
        ],
        "description": "Offer completion status. Use DRAFT for offers in progress, COMPLETED for finalized offers. Defaults to COMPLETED if not specified."
       },
       "offerName": {
        "type": "string",
        "maxLength": 49,
        "description": "Name of the offer (MUST be under 50 characters). If adding a timestamp suffix, shorten the base name to fit. ⚠️ ALLOWED CHARACTERS: letters, numbers, spaces, and ONLY these punctuation marks: _ . ? , : ' / + - = & ; % $ # [ ] | ( ). Anything else (em-dashes, smart quotes, emoji, !, @, *, {, }, <, >, \", \\, ^, ~, `, non-ASCII letters, ©/™) is rejected by the platform for Landing Page offers and will be auto-stripped by the server before the request is sent."
       },
       "offerHeader": {
        "type": "string",
        "description": "Header text (max 60 characters)"
       },
       "offerDetails": {
        "type": "string",
        "maxLength": 160,
        "description": "Detailed offer description (HARD LIMIT: 160 characters, ~2 short sentences). VALID ON ALL CHANNELS — appears as the description above the form. LinkedIn: populates the form's 'description' shown to the user before they fill it out — strongly recommended to improve conversion. Facebook/Instagram: MANDATORY when offerHeader is provided. If omitted on FB/IG, the API will reject the offer with INVALID_LEADGEN_FORM. Exceeding 160 chars will be REJECTED with a validation error — count characters before sending and rewrite concisely if needed."
       },
       "thankYouMessage": {
        "type": "string",
        "description": "LinkedIn: message shown after form submission (post-submission message). FB/IG: short text that renders UNDER the thank-you headline (maps to Meta's `thank_you_page.short_message`). Use thankYouDescription for the longer body text on Meta."
       },
       "thankYouDescription": {
        "type": "string",
        "description": "FB/IG Lead Gen ONLY — longer body text on the thank-you screen (maps to Meta's `thank_you_page.body`). REQUIRED when status='COMPLETED'. If omitted, thankYouMessage is reused — but supplying both yields a better UX (headline + short_message + body)."
       },
       "thankYouUrl": {
        "type": "string",
        "description": "Thank you page URL (must start with https://). LinkedIn: target URL of the post-submission CTA button. FB/IG: fallback when websiteUrl is omitted. Backend actually HTTP-probes this URL during validation, so it MUST resolve to a live page — no placeholders."
       },
       "thankYouHeadline": {
        "type": "string",
        "description": "Thank you page headline (required for FB/IG Lead Gen). Maps to Meta's `thank_you_page.title`."
       },
       "thankYouPageButtonText": {
        "type": "string",
        "description": "FB/IG Lead Gen ONLY — plain button TEXT displayed on the thank-you screen CTA (e.g. 'Download', 'Visit Site'). Ignored on LinkedIn — use `linkedinCTA` instead."
       },
       "thankYouPageButtonType": {
        "type": "string",
        "enum": [
         "VIEW_WEBSITE",
         "CALL_BUSINESS",
         "MESSAGE_BUSINESS",
         "VIEW_ON_FACEBOOK"
        ],
        "description": "FB/IG Lead Gen ONLY — Meta button TYPE enum for the thank-you screen CTA. Defaults to VIEW_WEBSITE. Cross-field rules enforced by LeadGenThankYouValidator: VIEW_WEBSITE requires websiteUrl/thankYouUrl (→ followUpActionUrl); VIEW_ON_FACEBOOK requires thankYouAssetId. Ignored on LinkedIn."
       },
       "thankYouAssetId": {
        "type": "integer",
        "description": "FB/IG Lead Gen ONLY — library creative ID for a gated asset. REQUIRED when thankYouPageButtonType='VIEW_ON_FACEBOOK' (otherwise validation fails). Ignored for other button types and on LinkedIn."
       },
       "linkedinCTA": {
        "type": "string",
        "enum": [
         "LEARN_MORE",
         "VIEW_NOW",
         "DOWNLOAD_NOW",
         "TRY_NOW",
         "VISIT_COMPANY_WEBSITE"
        ],
        "description": "LinkedIn Lead Gen ONLY — main form submission CTA (AdFormCallToAction enum). Required on the backend but defaults to DOWNLOAD_NOW when omitted. Must be one of the listed values — anything else silently falls back to VISIT_COMPANY_WEBSITE on LinkedIn's side. Ignored on FB/IG."
       },
       "websiteUrl": {
        "type": "string",
        "description": "Website URL for thank you button (required for FB/IG Lead Gen)"
       },
       "privacyUrl": {
        "type": "string",
        "description": "Privacy policy URL (must start with https://)"
       },
       "privacyText": {
        "type": "string",
        "description": "Privacy disclaimer text (required for LinkedIn Lead Gen). For Facebook/Instagram Lead Gen this is the privacy-policy link text and Meta caps it at 70 characters — a longer value is rejected by Meta and the form never reaches completed status, so keep it <= 70 chars on Meta."
       },
       "questionPageCustomHeadline": {
        "type": "string",
        "description": "Custom question page headline (FB/IG Lead Gen)"
       },
       "dynamicUtms": {
        "type": "boolean",
        "description": "Enable dynamic UTM tracking"
       },
       "landingPageUrl": {
        "type": "string",
        "description": "Landing page URL (required for Landing Page offers)"
       },
       "goal": {
        "type": "string",
        "enum": [
         "CPL",
         "CTR"
        ],
        "description": "Campaign goal for this offer. 'CPL' (Cost Per Lead, default) covers all Lead Gen offers and standard Landing Page lead offers. Use 'CTR' ONLY for Landing Page Brand Awareness offers (LP-BA). Lead Gen offers must always be CPL."
       },
       "fields": {
        "type": "array",
        "description": "Field mappings. REQUIRED for all offer types and MUST contain at least one email-type entry — for Lead Gen use 'EMAIL' or 'WORK_EMAIL'; for Landing Page use 'EMAIL' (BUSINESS_EMAIL alone is NOT accepted — it can only be an EXTRA field alongside EMAIL). ⚠️ Landing Page offers without an EMAIL mapping are rejected by the platform with `INVALID_LANDINGPAGE_FORM: Email mapping is required` — there is no valid lead-capture form without it. IMPORTANT: Lead Gen and Landing Page offer types accept DIFFERENT field values - see tool description for the full list per type. Do NOT use BUSINESS_EMAIL for Lead Gen (use WORK_EMAIL). Do NOT use COMPANY for Lead Gen (use COMPANY_NAME). Do NOT use COMPANY_NAME or WORK_PHONE_NUMBER for Landing Pages (use COMPANY and PHONE).",
        "minItems": 1,
        "items": {
         "type": "object",
         "properties": {
          "name": {
           "type": "string",
           "description": "Form field name (e.g., 'email', 'firstName', 'company', etc.)"
          },
          "field": {
           "type": "string",
           "description": "Metadata field type. For Lead Gen: EMAIL, WORK_EMAIL, FIRST_NAME, LAST_NAME, PHONE, JOB_TITLE, COMPANY_NAME, etc. For Landing Page: EMAIL, BUSINESS_EMAIL, FIRST_NAME, LAST_NAME, PHONE, COMPANY, etc. See tool description for complete lists."
          }
         },
         "required": [
          "name",
          "field"
         ],
         "additionalProperties": false
        }
       }
      }
     }
    },
    "required": [
     "offer_type",
     "data"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "disconnect_channel",
   "title": "Disconnect Channel",
   "category": "campaigns",
   "risk": "destructive",
   "summary": "Disconnect a specific advertising channel from the Metadata platform.",
   "description": "Disconnect a specific advertising channel from the Metadata platform.\n\n                    USAGE INSTRUCTIONS FOR LLM:\n                    Use this tool to disconnect/remove integration connections for advertising channels.\n                    This is useful when users need to remove a channel integration or reconnect with different credentials.\n\n                    WHEN TO USE THIS TOOL:\n                    - User asks to \"disconnect [channel name]\"\n                    - User wants to \"remove [channel] integration\"\n                    - User needs to \"unlink [channel] account\"\n                    - User wants to \"revoke [channel] access\"\n                    - User asks to \"disconnect my Facebook/LinkedIn/Twitter/Reddit/Google account\"\n                    - User needs to \"reset [channel] connection\"\n                    - Preparing to reconnect a channel with different credentials\n\n                    SUPPORTED CHANNELS:\n                    - facebook: Disconnect Facebook advertising account\n                    - instagram: Disconnect Instagram advertising account\n                    - linkedin: Disconnect LinkedIn advertising account\n                    - twitter: Disconnect Twitter/X advertising account\n                    - reddit: Disconnect Reddit advertising account\n                    - google: Disconnect Google Ads account\n\n                    IMPORTANT NOTES:\n                    - This action will disconnect the channel immediately\n                    - Any active campaigns on this channel may be affected\n                    - The user will need to reconnect the channel to resume advertising\n                    - Always confirm with the user before disconnecting a channel\n                    - Check integration status first using get_integrations_status to verify the channel is connected\n\n                    COMMON SCENARIOS:\n\n                    Scenario 1 - Disconnect single channel: \"Disconnect my LinkedIn account\"\n                    → Call disconnect_channel with channel=\"linkedin\"\n\n                    Scenario 2 - Reconnect workflow: \"I want to reconnect Facebook with a different account\"\n                    → First call disconnect_channel with channel=\"facebook\", then guide user to reconnect\n\n                    Scenario 3 - Remove access: \"Remove Google Ads integration\"\n                    → Call disconnect_channel with channel=\"google\"\n\n                    RESPONSE FORMAT:\n                    Returns a success/failure status indicating whether the channel was disconnected successfully.\n\n                    EXAMPLE: \"Disconnect Facebook\" → disconnect_channel(channel=\"facebook\")",
   "inputSchema": {
    "type": "object",
    "properties": {
     "channel": {
      "type": "string",
      "enum": [
       "reddit",
       "facebook",
       "linkedin",
       "twitter",
       "google",
       "instagram"
      ],
      "description": "The channel to disconnect. Must be one of: reddit, facebook, linkedin, twitter, google, instagram"
     }
    },
    "required": [
     "channel"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "duplicate_campaign",
   "title": "Duplicate Campaign",
   "category": "campaigns",
   "risk": "write",
   "summary": "Duplicate (copy) an existing campaign into a NEW DRAFT campaign.",
   "description": "Duplicate (copy) an existing campaign into a NEW DRAFT campaign.\n\n                KEYWORDS: duplicate, copy, clone, replicate, reuse, campaign, draft, copy of\n\n                WHAT IT DOES:\n                Creates a brand-new campaign that is a DEEP COPY of an existing one. The new\n                campaign is created in DRAFT status and spends no budget. This mirrors the\n                platform UI's \"Duplicate campaign\" action.\n\n                WHAT GETS COPIED (server-side, automatic — there are NO toggles to choose):\n                - Goal (Lead Gen / Brand Awareness)\n                - Optimization (budget) group / tag\n                - All landing pages\n                - Every channel that is currently ENABLED FOR THE ACCOUNT, together with that\n                  channel's audiences, ads/creatives, keywords, offers and bid settings.\n                  (Channels that are disabled for the account are skipped — connect/enable the\n                  channel on the account first if you need it carried over.)\n\n                WHAT IS NOT COPIED / CHANGES:\n                - The new campaign starts in DRAFT (the original's live/active state is NOT copied).\n                - Start/end dates are not carried as a live schedule — set them on the new draft.\n                - Only the new name is caller-controlled.\n\n                REQUIRED PARAMETERS:\n                - campaign_id (integer): The wizard/campaign ID of the campaign to duplicate.\n                  Must be in status Launched, Launching, Editing, or Draft.\n\n                OPTIONAL PARAMETERS:\n                - name (string): The name for the NEW campaign. Must be non-blank, <= 255 chars,\n                  and UNIQUE across the account's campaigns. If omitted, a unique name is\n                  auto-generated as \"<original name>_copy_<UTC timestamp>\" (e.g.\n                  \"Q1_2026_Campaign_copy_20260115120000\"). If you pass a name that\n                  already exists on the account, the platform rejects it — choose a unique name\n                  or omit it to auto-generate one.\n\n                CAMPAIGN ID SOURCES:\n                - search_campaigns_by_names tool (recommended)\n                - list_wizard_campaigns tool\n                - get_campaign_by_wizard_id tool\n\n                WORKFLOW:\n                1. Find the source campaign ID (search_campaigns_by_names / list_wizard_campaigns).\n                2. duplicate_campaign(campaign_id=..., name=\"...\"  # name optional).\n                3. The response contains the NEW campaign's id and full configuration.\n                4. Review the new DRAFT campaign and MODIFY it with the edit tools below (it is a\n                   normal editable draft — change anything you want).\n\n                AFTER DUPLICATING — HOW TO MODIFY THE NEW DRAFT:\n                The copy is a regular DRAFT campaign, so use the standard campaign-editing tools on\n                the NEW campaign id (NOT the original). The main ones:\n                - add_and_edit_campaign_elements — the primary editor: rename, change budget group,\n                  start/end dates, enable channels, replace per-channel offers, and ADD audiences,\n                  exclusion audiences, ads, ad groups, and keywords. (Native / N×N×N campaigns:\n                  use add_and_edit_native_campaign_elements.)\n                - update_experiments_daily_budgets — adjust per-experiment daily budgets.\n                - update_reddit_channel_settings — Reddit-specific channel/bid/pixel settings.\n                - remove_channels_from_campaign / remove_audiences_from_campaign /\n                  remove_ad_from_campaign / remove_keywords_from_campaign /\n                  remove_offers_from_campaign / remove_target_groups_from_campaign\n                  — remove elements from the copy.\n                - manage_campaign — pause/restart (lifecycle).\n                Tip: call get_campaign_by_wizard_id on the new id first to see what was copied,\n                then edit. Use search_campaigns_by_names to re-locate the copy by its new name.\n\n                EXAMPLE:\n                User: \"Make a copy of campaign 54050 called 'Q2 Relaunch'\"\n                Call: duplicate_campaign(campaign_id=54050, name=\"Q2 Relaunch\")\n                Then: add_and_edit_campaign_elements(campaign_id=<new id>, ...) to tweak the copy.\n\n                RESPONSE FORMAT:\n                Returns the full configuration of the newly created DRAFT campaign — including its\n                new campaign id, name, status (Draft), goal, optimization group, and copied channels.\n                Use the returned id with get_campaign_by_wizard_id and the edit tools.\n\n                Anchor any date-derived naming to the REAL current date — call get_current_date if unsure.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The wizard/campaign ID of the campaign to duplicate. Must be in status Launched, Launching, Editing, or Draft.",
      "minimum": 1
     },
     "name": {
      "type": "string",
      "description": "Name for the new (copied) campaign. Must be unique on the account and <= 255 chars. Omit to auto-generate '<original name>_copy_<timestamp>'.",
      "minLength": 1,
      "maxLength": 255
     }
    },
    "required": [
     "campaign_id"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "find_offer_url",
   "title": "Find Offer Landing-Page URL",
   "category": "campaigns",
   "risk": "read",
   "summary": "Locate the BEST specific landing-page URL for a marketing offer on a given domain.",
   "description": "Locate the BEST specific landing-page URL for a marketing offer on a given domain.\n\n                    Runs three discovery strategies in parallel:\n                      1. Direct path probe — conventional paths per offer_type (e.g. /request-demo, /case-studies, /roi-calculator)\n                      2. Sitemap scan      — <loc> entries filtered by offer-type keywords + optional topic\n                      3. Footer/body scrape — CTA/nav links matching the offer intent\n\n                    Candidates are scored by specificity. Generic hub pages (/resources/, /content/, /library/, homepages)\n                    are HARD REJECTED — this tool exists precisely to avoid shipping them.\n\n                    USE THIS BEFORE create_update_offer WHENEVER offer_type = \"Landing Page\"\n                    AND the user did NOT provide an explicit URL.\n                    Do NOT guess a URL. Do NOT rely on web_search for landing pages — this tool is more reliable.\n\n                    USER-PROVIDED URL OVERRIDE (HARD RULE):\n                    If the user already specified a landing-page URL in their request (e.g. \"use\n                    https://acme.com/demo-fintech\" or \"point offers at our pricing page\"), DO NOT\n                    call find_offer_url — use the user's URL verbatim in create_update_offer.\n                    Never overwrite an explicit user-provided URL, even if it looks generic.\n\n                    WHEN TO USE:\n                    - Before every create_update_offer call with offer_type=\"Landing Page\"\n                    - When you need a specific demo / case-study / whitepaper / ROI calculator / guide URL\n                    - When web_search returned only generic /resources/ or homepage URLs\n\n                    FALLBACK BEHAVIOR:\n                    - On success: use `offer_url` verbatim in create_update_offer.landingPageUrl\n                    - On success=false: DO NOT create a Landing Page offer. Switch to offer_type=\"Lead Gen\" instead.\n\n                    RETURNS:\n                    {\n                        \"success\": true,\n                        \"domain\": \"snowflake.com\",\n                        \"offer_type\": \"case_study\",\n                        \"topic\": \"fintech\",\n                        \"offer_url\": \"https://www.snowflake.com/customers/square/\",\n                        \"source\": \"sitemap\",\n                        \"link_text\": \"Square\",\n                        \"score\": 95,\n                        \"steps_tried\": [\"direct-probe\", \"sitemap\", \"footer-scrape\"],\n                        \"candidate_count\": 42\n                    }\n\n                    On failure: { \"success\": false, \"suggestion\": \"Switch offer_type to 'Lead Gen'...\" }\n\n                    EXAMPLES:\n                    - find_offer_url(domain=\"snowflake.com\", offer_type=\"case_study\", topic=\"fintech\")\n                    - find_offer_url(domain=\"servicenow.com\", offer_type=\"demo\")\n                    - find_offer_url(domain=\"mongodb.com\", offer_type=\"roi_calculator\")",
   "inputSchema": {
    "type": "object",
    "properties": {
     "domain": {
      "type": "string",
      "description": "Company website URL or domain. Examples: 'snowflake.com', 'www.servicenow.com'"
     },
     "offer_type": {
      "type": "string",
      "enum": [
       "case_study",
       "demo",
       "whitepaper",
       "roi_calculator",
       "guide",
       "report",
       "ebook",
       "webinar"
      ],
      "description": "Type of offer content to locate. Drives which paths are probed and which keywords score higher."
     },
     "topic": {
      "type": "string",
      "description": "Optional topic keywords to bias scoring (e.g., 'fintech', 'servicenow integration', 'data warehousing'). Improves hit rate when the domain has many offers of the same type."
     }
    },
    "required": [
     "domain",
     "offer_type"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "get_budget_group",
   "title": "Get Budget Group",
   "category": "campaigns",
   "risk": "read",
   "summary": "Get configuration and settings for a budget group (NOT performance data).",
   "description": "Get configuration and settings for a budget group (NOT performance data).\n\n                KEYWORDS: budget, group, settings, configuration, details, allocation, campaigns\n\n                WHEN TO USE:\n                - \"Show me the [group name] budget group settings/configuration\"\n                - \"What campaigns are in the [group name] budget group?\"\n                - \"What's the budget allocation for [group name]?\"\n                - Getting budget group metadata without performance metrics\n\n                WHEN NOT TO USE:\n                - Performance questions (spend, CPL, leads, conversions) → use budget_group_performance instead\n                - Questions like \"how did X perform\" or \"total spend for X\" → use budget_group_performance instead\n\n                NOTE: If the user is asking about performance metrics (spend, leads, CPL, etc.),\n                use budget_group_performance directly - it accepts the name and handles ID lookup internally.\n\n                Returns complete budget group information including campaigns, budget allocation, and settings.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "The name of the budget group to search for. Example: 'Brand'"
     }
    },
    "required": [
     "name"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "get_campaign_by_wizard_id",
   "title": "Get Campaign by Wizard ID",
   "category": "campaigns",
   "risk": "read",
   "summary": "Get full campaign details by its wizard campaign ID, including channels, audiences, ads, and budget configuration.",
   "description": "Get full campaign details by its wizard campaign ID, including channels, audiences, ads, and budget configuration.\n\n                KEYWORDS: campaign, get, details, wizard, id, lookup, channels, audiences, ads, budget, configuration\n\n                WHEN TO USE:\n                - User wants to see full details of a specific campaign by its ID\n                - User needs to inspect channel configurations, audiences, ad groups, or budget settings for a campaign\n                - After creating a campaign, to verify its full configuration\n                - To retrieve the complete campaign structure including all channel details\n\n                RETURNS:\n                Full campaign details in a structured format including:\n                - Campaign ID, name, status, dates\n                - Optimization group / budget group details\n                - Per-channel configuration (enabled, daily budget, audiences, ad groups, lead gen forms, bid strategy)\n                - Summary statistics (total channels, enabled channels, total daily budget, goal)\n\n                PARAMETERS:\n                - campaign_id: The wizard campaign ID (integer, required)\n\n                CAMPAIGN ID SOURCES:\n                - search_campaigns_by_names tool (returns campaign IDs)\n                - create_campaign tool response (returns campaign_id)\n                - Campaign management interface URLs",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The wizard campaign ID to retrieve details for",
      "minimum": 1
     }
    },
    "required": [
     "campaign_id"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "get_campaign_targeting_locations",
   "title": "Get Campaign Targeting Locations",
   "category": "campaigns",
   "risk": "read",
   "summary": "Where a campaign actually TARGETS, by channel, with the platform's location ids resolved to country and state names.",
   "description": "Where a campaign actually TARGETS, by channel, with the platform's location ids resolved to country and state names.\n\n                KEYWORDS: campaign, targeting, location, geo, country, region, where, targets, germany, dach, emea, market, geographic\n\n                WHEN TO USE:\n                - \"Which campaigns target <country>?\" / \"Do we run anything in Germany?\"\n                - \"Where is this campaign targeted?\" before editing or launching it\n                - Any question about geographic TARGETING\n\n                DO NOT ANSWER GEO TARGETING FROM demographic_country_stats. That tool reports where ads were SERVED (delivery); this one reports where they are AIMED. The two disagree routinely, and answering a targeting question from delivery data is how the agent once told an account with 68 Germany campaigns that it had none.\n\n                RETURNS:\n                - campaigns: [{campaignId, name, status, channels[], summary}]\n                - channels[]: {channel, enabled, geoSource, targeting{resolved, countries[{id,name}], states[], ...}, exclusions}\n                  geoSource is \"search_ad_location\" (Google/Microsoft, held on the channel) or\n                  \"native_audience\" (LinkedIn/Meta/Reddit, held on the attached audience).\n                - summary: {countries[], unresolvedIds[], partial}\n                - query: {countryFilter, scanned, matched, unreadable[]}\n                - limitations: []\n\n                READING THE RESULT - these three distinctions are the whole point:\n                - countries: [] means nothing is targeted. targeting.resolved false means it could NOT BE READ. Never report the second as the first.\n                - exclusions.readable is ALWAYS false: the platform offers no GET for negative locations. Never state that a campaign excludes, or does not exclude, anywhere.\n                - summary.partial true means this answer is incomplete. Say so rather than presenting it as a full list.\n                - If `limitations` mentions truncation, you have NOT seen the whole account. Re-call with a higher max_campaigns before answering \"there are none\".\n\n                PARAMETERS:\n                - campaign_ids: specific wizard campaign ids (optional)\n                - country_names: filter to campaigns targeting these, e.g. [\"Germany\"] (optional). Names must match the platform catalog; unmatched names are reported, never silently dropped.\n                - max_campaigns: cap when scanning the account (default 25, max 100)\n\n                With neither campaign_ids nor country_names, returns the most recent campaigns up to max_campaigns.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_ids": {
      "type": "array",
      "items": {
       "type": "integer",
       "minimum": 1
      },
      "description": "Wizard campaign ids to read. Omit to scan the account."
     },
     "country_names": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Filter to campaigns targeting these countries, e.g. [\"Germany\", \"Austria\"]."
     },
     "max_campaigns": {
      "type": "integer",
      "description": "Cap on campaigns read when scanning (default 25, max 100).",
      "minimum": 1,
      "maximum": 100,
      "default": 25
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "get_offer",
   "title": "Get Offer",
   "category": "campaigns",
   "risk": "read",
   "summary": "Get offer information by name or list all offers.",
   "description": "Get offer information by name or list all offers.\n\nUSE FOR: \"Show me the [offer name] offer\", \"What's in [offer name]?\", \"Find offer called [name]\", \"List all offers\"\n\nIf name provided: returns detailed offer info (name, description, assets, configuration).\nIf no name: returns list of all available offers.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "The name of the offer to search for. Example: 'AI Marketing Audit Demo'. If omitted, returns a list of all available offers."
     },
     "offer_id": {
      "type": "integer",
      "description": "Exact offer id. PREFER THIS over `name` whenever you have an id: a name lookup returns the first search match, so offers sharing a prefix (a template name plus a random suffix, which is how offers get named here) can resolve to the wrong one silently. Takes precedence when both are given."
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "launch_campaign",
   "title": "Launch Campaign",
   "category": "campaigns",
   "risk": "destructive",
   "summary": "LAUNCHES REAL MARKETING CAMPAIGNS WITH REAL BUDGET SPEND 🚨 KEYWORDS: launch, start, activate, go live, campaign, begin, advertising, spend, marketing PREREQUISITE: Before runni…",
   "description": "🚨 LAUNCHES REAL MARKETING CAMPAIGNS WITH REAL BUDGET SPEND 🚨\n\n                KEYWORDS: launch, start, activate, go live, campaign, begin, advertising, spend, marketing\n\n                PREREQUISITE: Before running this tool, call get_integrations_status and verify at least one advertising channel is connected (FACEBOOK, INSTAGRAM, LINKEDIN, GOOGLE_ADS). If none are connected, tell the user to connect at least one channel before proceeding and stop.\n\n                This tool takes a draft campaign and launches it as a LIVE, ACTIVE marketing campaign.\n                Once launched, the campaign will immediately start:\n                - SPENDING REAL MONEY from your marketing budget\n                - SERVING ADS to real audiences across platforms (LinkedIn, Facebook, Instagram, Google)\n                - GENERATING ACTUAL IMPRESSIONS, CLICKS, AND CONVERSIONS\n                - COMPETING in real ad auctions with real bid amounts\n\n                ⚠️ CRITICAL SAFETY REQUIREMENTS ⚠️\n                This tool will ALWAYS require explicit user confirmation before launching any campaign.\n                The user must understand they are launching a real campaign with real budget implications.\n\n                WHEN TO USE:\n                - Launch a draft campaign to start real advertising and budget spend\n                - Activate a campaign after final review, approval, and budget confirmation\n                - Begin live ad serving across configured channels\n                - Transition from campaign testing/setup phase to live marketing execution\n\n                REQUIRED PARAMETERS:\n                - campaign_id: The unique identifier of the draft campaign to launch (integer, required)\n\n                MANDATORY CONFIRMATION PROCESS:\n                Before launching any real campaign, this tool MUST:\n                1. Display the campaign ID and retrieve campaign details if possible\n                2. Show estimated daily/total budget that will be spent\n                3. List all channels where ads will go live (LinkedIn, Facebook, Instagram, Google)\n                4. Warn that this starts REAL BUDGET SPENDING immediately\n                5. Require explicit \"YES, LAUNCH REAL CAMPAIGN\" confirmation\n                6. Confirm the user understands this is not a test or simulation\n                7. Only proceed after receiving unambiguous user approval\n                8. Provide immediate launch status and live campaign monitoring URL\n\n                CAMPAIGN ID SOURCES:\n                Campaign IDs can be obtained from:\n                - search_campaigns_by_names tool (recommended)\n                - create_campaign tool response (returns campaign_id)\n                - Campaign management interface URLs\n\n                EXAMPLE CONFIRMATION FLOW:\n\n                User: launch_campaign(campaign_id=159490)\n\n                System Response:\n                \"🚨 REAL CAMPAIGN LAUNCH REQUEST 🚨\n\n                Campaign ID: 159490\n                Campaign Name: 'Q1_2026_Multi_Channel_Campaign'\n                Channels: LinkedIn, Facebook, Instagram, Google Ads\n                Budget Group: 'BoFu'\n                Estimated Daily Spend: $500-800\n                Total Budget Allocation: $15,000\n\n                ⚠️ WARNING: This will launch a REAL marketing campaign ⚠️\n                - Real money will be spent immediately\n                - Ads will serve to real audiences\n                - This is NOT a test or simulation\n\n                Type 'YES, LAUNCH REAL CAMPAIGN' to confirm you want to proceed with live campaign launch:\"\n\n                Only after explicit confirmation will the campaign launch proceed.\n\n                RESPONSE FORMAT:\n\n                Success Response (launched — ONLY when \"waitingAudiencesToLaunch\" is false or absent):\n                {\n                    \"success\": true,\n                    \"campaign_id\": 159490,\n                    \"status\": \"LIVE - ACTIVELY SPENDING BUDGET\",\n                    \"campaign_url\": \"https://platform.metadata.io/hub/advertise/campaigns/159490\",\n                    \"launch_timestamp\": \"2026-01-15T12:00:00Z\",\n                    \"message\": \"🔥 REAL CAMPAIGN IS NOW LIVE 🔥 - Budget spending has begun\",\n                    \"live_channels\": [\"LinkedIn\", \"Facebook\", \"Instagram\", \"Google Ads\"],\n                    \"budget_status\": \"Active - Real spend initiated\",\n                    \"immediate_actions_required\": [\n                        \"🚨 MONITOR SPEND IMMEDIATELY - Real money is being spent\",\n                        \"Check live ad performance in dashboard\",\n                        \"Set up budget alerts if not already configured\",\n                        \"Monitor audience engagement in first 2 hours\",\n                        \"Prepare to pause campaign if performance issues arise\"\n                    ]\n                }\n\n                DEFERRED LAUNCH — CHECK THIS BEFORE YOU REPORT A LAUNCH:\n                A 202 does NOT always mean the campaign went live. When the response carries\n                \"waitingAudiencesToLaunch\": true (it comes back with \"status\": \"Draft\", not\n                \"Launching\"), the platform has QUEUED the campaign rather than launched it. It is\n                NOT live, it is NOT serving ads, and it is NOT spending budget. The response also\n                carries a \"context\" block that states this in plain language.\n\n                Deferred Response:\n                {\n                    \"id\": 159490,\n                    \"status\": \"Draft\",\n                    \"waitingAudiencesToLaunch\": true,\n                    \"context\": {\n                        \"waitingAudiencesToLaunch\": true,\n                        \"description\": \"This campaign will be launched only after its audiences\n                            finish uploading and matching on the native channels. That happens\n                            automatically when the match completes, so nothing further is needed\n                            from the user. The campaign is NOT live and is NOT spending budget\n                            yet, and it stays editable until it launches. Some channels can take\n                            up to 72 hours to finish the match flow.\"\n                    }\n                }\n\n                When you get this response, tell the user plainly:\n                - The campaign is queued, not live, and no budget is being spent yet.\n                - It will launch on its own once the audiences finish syncing to the channels.\n                  Nothing more is required from them.\n                - They can keep editing the campaign until it launches.\n                - Some channels take up to 72 hours to finish matching, so it may not be today.\n\n                NEVER describe this outcome as live, launched, active, or spending. Do not tell the\n                user to monitor spend, and do not hand them the live-campaign monitoring checklist\n                from the success case above; none of it applies until the campaign actually launches.\n\n                Error Response:\n                {\n                    \"success\": false,\n                    \"campaign_id\": 159490,\n                    \"error\": \"Campaign not found or already launched\",\n                    \"details\": \"The specified campaign ID does not exist or is already in active status\"\n                }\n\n                User Cancellation Response:\n                {\n                    \"success\": false,\n                    \"campaign_id\": 159490,\n                    \"status\": \"Cancelled\",\n                    \"message\": \"Campaign launch cancelled by user. Campaign remains in Draft.\"\n                }\n\n                BUDGET IMPLICATIONS — REAL MONEY:\n                Launching a campaign will IMMEDIATELY:\n                - Deduct REAL DOLLARS from your marketing budget\n                - Serve ads that cost money per impression/click\n                - Compete in live ad auctions with actual bid amounts\n                - Impact budget availability for other campaigns in the same budget group\n\n                POST-LAUNCH MONITORING (CRITICAL):\n                After launch, IMMEDIATE monitoring is required:\n                - Check spend within the first 30 minutes\n                - Monitor CPCs, impression delivery, and budget burn rate\n                - Review audience engagement and conversion tracking\n                - Be prepared to pause or adjust bids if needed\n\n                Anchor all date-related decisions to the REAL current date — call get_current_date if you are not already certain what today is.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The unique identifier of the draft campaign to launch",
      "minimum": 1
     }
    },
    "required": [
     "campaign_id"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "list_budget_groups",
   "title": "List All Budget Groups",
   "category": "campaigns",
   "risk": "read",
   "summary": "Browse and list all available budget groups in the Metadata platform with customizable sorting.",
   "description": "Browse and list all available budget groups in the Metadata platform with customizable sorting.\n\n                KEYWORDS: budget, group, list, all, browse, spending, allocation, optimization, overview\n\n                **DIFFERENCE FROM get_budget_group:**\n                - get_budget_group: Searches for ONE specific group BY NAME (requires group name input)\n                - list_budget_groups: Returns ALL groups at once (supports flexible sorting)\n\n                USE THIS TOOL WHEN THE USER ASKS:\n                - \"Show me all budget groups\" or \"List all budget groups\"\n                - \"What budget groups do we have?\"\n                - \"Display all available groups\"\n                - \"Show groups sorted by [metric]\"\n                - \"Which groups have the highest budget?\"\n                - \"Show groups by leads generated\"\n                - \"List groups sorted by CPL\"\n                - \"Order groups by spending\"\n\n                DO NOT USE if the user:\n                - Asks for details about a SPECIFIC group by name → use get_budget_group instead\n                - Wants to find a group by name → use get_budget_group instead\n                - Says \"Find the [specific name] group\" → use get_budget_group instead\n\n                SORTING OPTIONS:\n                You can sort by any of these fields (default: totalActiveExperiments descending):\n                - totalActiveExperiments, monthlyCap, averageDailyBudgetPerExperiment\n                - spent, totalBudget, endDate, startDate, leads, cpl\n\n                SORT ORDER:\n                - asc: Ascending order (lowest to highest, oldest to newest)\n                - desc: Descending order (highest to lowest, newest to oldest) [DEFAULT]",
   "inputSchema": {
    "type": "object",
    "properties": {
     "sort_by": {
      "type": "string",
      "enum": [
       "totalActiveExperiments",
       "monthlyCap",
       "averageDailyBudgetPerExperiment",
       "spent",
       "totalBudget",
       "endDate",
       "startDate",
       "leads",
       "cpl"
      ],
      "description": "Field to sort budget groups by. Default: totalActiveExperiments"
     },
     "sort_order": {
      "type": "string",
      "enum": [
       "asc",
       "desc"
      ],
      "description": "Sort order - 'asc' (ascending) or 'desc' (descending). Default: desc"
     },
     "page": {
      "type": "integer",
      "description": "Page number for pagination (0-based). Default: 0",
      "default": 0,
      "minimum": 0
     },
     "size": {
      "type": "integer",
      "description": "Number of results per page. Default: 10",
      "default": 10,
      "minimum": 1
     }
    },
    "additionalProperties": false
   }
  },
  {
   "name": "list_wizard_campaigns",
   "title": "List Wizard Campaigns",
   "category": "campaigns",
   "risk": "read",
   "summary": "List wizard (Metadata-native) campaigns for the current account with pagination, sorting, and experiment-status filtering.",
   "description": "List wizard (Metadata-native) campaigns for the current account with pagination, sorting, and experiment-status filtering.\n\n                KEYWORDS: campaigns, list, browse, wizard, native, all campaigns, campaign list, launched campaigns, paused campaigns, active campaigns\n\n                USE THIS TOOL WHEN THE USER ASKS:\n                - \"Show me all campaigns\" / \"List campaigns\" / \"What campaigns do we have?\"\n                - \"Show active campaigns\" / \"List paused campaigns\" / \"Show completed campaigns\"\n                - \"Show recent campaigns\" / \"Last N campaigns\"\n                - \"Campaigns sorted by spend / leads / CPL / created date\"\n                - Page-by-page campaign browsing for the campaigns UI\n\n                DO NOT USE if the user:\n                - Wants details on ONE SPECIFIC campaign by name → use search_campaigns_by_names\n                - Wants budget group info (not campaigns) → use list_budget_groups / get_budget_group\n                - Wants per-experiment performance → use experiment_performance_stats / search_experiments\n\n                TWO INDEPENDENT FILTERS (COMBINE TO MATCH UI TABS):\n\n                1) status — filters by CAMPAIGN status. Accepts a comma-separated subset of: Draft, Launched, Finished.\n                   - Draft: the campaign exists but has never been launched.\n                   - Launched: the campaign has been launched (experiments may be Active, Paused, or idle).\n                   - Finished: the campaign has reached its end date.\n                   - Omit to include all three.\n\n                2) experiment_statuses — filters by EXPERIMENT status inside each campaign. Accepts a comma-separated subset of:\n                   Active, WithoutSpend, Paused, Completed, Failed. Omit to include all five.\n\n                HOW TO MIX THEM (mirrors the UI tabs on /campaigns):\n                - \"All\" tab: omit both → API returns everything.\n                - \"Active\" tab: status=\"Launched\", experiment_statuses=\"Active,WithoutSpend\" (launched campaigns with live or not-yet-spending experiments).\n                - \"Paused\" tab: status=\"Launched\", experiment_statuses=\"Paused\" (launched campaigns whose experiments are paused).\n                - \"Draft\" tab: status=\"Draft\" (campaign hasn't been launched yet — experiment status doesn't apply).\n                - \"Finished\" tab: status=\"Finished\".\n\n                SORTING:\n                - sort_by: createdDate (default), startDate, endDate, name, budget, spent, leads, cpl, cpc, ctr, impressions, clicks, status\n                - sort_order: asc | desc (default: desc)\n\n                PAGINATION:\n                - page: 0-based page index (default 0)\n                - size: results per page (default 25; the UI campaign list fetches 25–50)\n\n                RESPONSE SHAPE:\n                {\n                  \"totalElements\": 435,\n                  \"totalPages\": 18,\n                  \"data\": [\n                    {\n                      \"id\": 173290,\n                      \"accountId\": 1455,\n                      \"name\": \"2026_Q2_NA_Builder_Content_...\",\n                      \"createdDate\": \"2026-01-15T12:00:00Z\",\n                      \"visibility\": \"VISIBLE\" | \"ARCHIVED\",\n                      \"budget\": 25,\n                      \"startDate\": \"...\",\n                      \"endDate\": \"...\",\n                      \"status\": \"Launched\" | \"Draft\" | ...,\n                      \"activeExperiments\": 9, \"pausedExperiments\": 0, \"totalExperiments\": 9,\n                      \"leads\": 0, \"spent\": 0, \"impressions\": 48, \"clicks\": 0,\n                      \"cpc\": 0, \"cpl\": 0, \"ctr\": 0.0,\n                      \"goal\": \"CTR\" | \"CPL\",\n                      \"campaignGroup\": \"TOF - AO - Social - Billing - BA\",\n                      \"tags\": [...]\n                    }, ...\n                  ]\n                }\n\n                WORKFLOW (UI-style): list_wizard_campaigns(page=0, size=25) → render list → user clicks campaign → search_campaigns_by_names / get_campaign_by_wizard_id for details.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "page": {
      "type": "integer",
      "description": "0-based page index. Default: 0",
      "default": 0,
      "minimum": 0
     },
     "size": {
      "type": "integer",
      "description": "Results per page. Default: 25",
      "default": 25,
      "minimum": 1,
      "maximum": 200
     },
     "sort_by": {
      "type": "string",
      "enum": [
       "createdDate",
       "startDate",
       "endDate",
       "name",
       "budget",
       "spent",
       "leads",
       "cpl",
       "cpc",
       "ctr",
       "impressions",
       "clicks",
       "status"
      ],
      "description": "Field to sort by. Default: createdDate"
     },
     "sort_order": {
      "type": "string",
      "enum": [
       "asc",
       "desc"
      ],
      "description": "Sort order. Default: desc"
     },
     "status": {
      "type": "string",
      "description": "Campaign-level status filter. Comma-separated subset of Draft, Launched, Finished. Omit to include all three. Combine with experiment_statuses to mirror the UI tabs (e.g. Active tab = status=Launched + experiment_statuses=Active,WithoutSpend)."
     },
     "experiment_statuses": {
      "type": "string",
      "description": "Experiment-level status filter. Comma-separated subset of Active, WithoutSpend, Paused, Completed, Failed. Omit to include all five."
     },
     "visibility": {
      "type": "string",
      "enum": [
       "VISIBLE",
       "ARCHIVED"
      ],
      "description": "Optional: filter by campaign visibility. Omit to include all."
     },
     "name": {
      "type": "string",
      "description": "Optional: server-side name substring filter."
     }
    },
    "additionalProperties": false
   }
  },
  {
   "name": "manage_campaign",
   "title": "Manage Campaign",
   "category": "campaigns",
   "risk": "destructive",
   "summary": "Manage a campaign by pausing or restarting it.",
   "description": "Manage a campaign by pausing or restarting it.\n\n                KEYWORDS: pause, stop, restart, resume, unpause, campaign, control, manage, halt\n\n                WHEN TO USE:\n                - User asks to \"pause campaign X\"\n                - User wants to \"stop campaign Y\"\n                - User requests to \"restart campaign Z\"\n                - User wants to \"resume campaign A\"\n                - User asks to \"unpause campaign B\"\n\n                ACTIONS:\n                - pause: Temporarily pause an active campaign to stop ad serving and budget spend\n                - restart: Resume a paused campaign to continue ad serving and budget spend\n\n                REQUIRED PARAMETERS:\n                - campaign_id: The unique identifier of the campaign to manage (integer, required)\n                - action: The action to perform - \"pause\" or \"restart\" (string, required)\n\n                CAMPAIGN ID SOURCES:\n                Campaign IDs can be obtained from:\n                - search_campaigns_by_names tool\n                - create_campaign tool response\n                - Campaign management interface URLs\n\n                IMPORTANT VALIDATION REQUIREMENTS:\n                - **CRITICAL**: You can ONLY pause campaigns that are currently \"Active\"\n                - **CRITICAL**: You can ONLY restart campaigns that are currently \"Paused\"\n                - Before attempting to pause or restart, use search_campaigns_by_names to verify the current campaign status",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The unique identifier of the campaign to manage",
      "minimum": 1
     },
     "action": {
      "type": "string",
      "enum": [
       "pause",
       "restart"
      ],
      "description": "The action to perform: 'pause' to stop the campaign, 'restart' to resume it"
     }
    },
    "required": [
     "campaign_id",
     "action"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "manage_experiment",
   "title": "Manage Experiment",
   "category": "campaigns",
   "risk": "destructive",
   "summary": "Pause or restart a whole experiment (a launched channel campaign inside a wizard campaign).",
   "description": "Pause or restart a whole experiment (a launched channel campaign inside a wizard campaign).\n\n                KEYWORDS: pause, stop, restart, resume, unpause, experiment, control, manage, halt\n\n                WHEN TO USE:\n                - User asks to \"pause experiment X\"\n                - User wants to \"stop the LinkedIn experiment\"\n                - User requests to \"restart experiment Y\"\n                - User wants to \"resume/unpause a paused experiment\"\n\n                SCOPE - DO NOT CONFUSE WITH SIMILAR TOOLS:\n                - manage_experiment (this tool) acts on ONE WHOLE EXPERIMENT.\n                - pause_experiment_keyword / restart_experiment_keyword act on ONE\n                  KEYWORD inside an experiment - they never pause the experiment itself.\n                - manage_campaign acts on the whole wizard campaign, which can span\n                  several experiments/channels.\n\n                ACTIONS:\n                - pause: Stop ad serving and budget spend for the experiment\n                - restart: Resume a paused experiment so it serves ads and spends again\n\n                WORKFLOW:\n                1. Find the experiment: search_experiments(search_name=\"my experiment\")\n                   and note experimentId and statusLabel from the results\n                2. Pause it: manage_experiment(experiment_id=123, action=\"pause\")\n                3. Or restart it: manage_experiment(experiment_id=123, action=\"restart\")\n\n                IMPORTANT VALIDATION REQUIREMENTS:\n                - **CRITICAL**: You can ONLY pause experiments that are currently active\n                - **CRITICAL**: You can ONLY restart experiments that are currently Paused\n                - Use search_experiments first to verify the current experiment status\n\n                RESTART BUDGET CONFIRMATION FLOW:\n                Restarting first estimates the impact on budget groups (the same check\n                the main UI runs). If any budget group is impacted, the tool does NOT\n                restart: it returns status=\"requires_confirmation\" with the impacted\n                groups in budgetImpact. Present the impact to the user, and only after\n                they explicitly confirm, call again with confirm_budget_impact=true.\n\n                ⚠️ WARNING: This changes the live state of the experiment on the ad channel.\n                ⚠️ ASYNC FLOW: The channel-side status change may take a moment to\n                propagate. Verify later with search_experiments.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "experiment_id": {
      "type": "integer",
      "description": "The experiment ID to pause or restart (experimentId from search_experiments).",
      "minimum": 1
     },
     "action": {
      "type": "string",
      "enum": [
       "pause",
       "restart"
      ],
      "description": "The action to perform: 'pause' to stop the experiment, 'restart' to resume it"
     },
     "confirm_budget_impact": {
      "type": "boolean",
      "description": "Restart only. Set true ONLY after the user explicitly confirmed the budget-group impact returned by a previous manage_experiment call with status='requires_confirmation'."
     },
     "auto_pause_ignore_historical_data": {
      "type": "boolean",
      "description": "Restart only. When the experiment was auto-paused for low performance, true tells the optimizer to ignore the historical performance data that caused the pause."
     }
    },
    "required": [
     "experiment_id",
     "action"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "remove_channels_from_campaign",
   "title": "Remove Channels from Campaign",
   "category": "campaigns",
   "risk": "destructive",
   "summary": "Disable one or more entire channels from an existing campaign.",
   "description": "Disable one or more entire channels from an existing campaign.\n\n                Use this tool when the goal is to turn off a whole channel (e.g. stop running\n                ads on FACEBOOK entirely). To remove a specific audience from a channel instead,\n                use remove_audiences_from_campaign.\n\n                KEYWORDS: remove, delete, disable, channel, campaign\n\n                WHEN TO USE:\n                - Disable/remove an entire channel from a campaign (e.g. remove FACEBOOK or GOOGLE_ADS)\n                - Multiple channels can be disabled in a single call\n                - Returns the campaign state before and after the change\n\n                RESPONSE:\n                - Returns the refreshed campaign state plus a removal_results list with one\n                  entry per requested removal recording its success or error. ALWAYS check\n                  removal_results — a partial failure (e.g. one of two names not found) still\n                  returns normally, so report only what actually came back successful.\n\n                REQUIRED PARAMETERS:\n                - campaign_id: Campaign ID (numeric identifier)\n                - channels: Array of channel types to disable\n\n                CHANNEL TYPES: FACEBOOK, INSTAGRAM, GOOGLE_ADS, LINKEDIN, MICROSOFT_ADS, REDDIT\n\n                EXAMPLES:\n\n                Disable a single channel:\n                remove_channels_from_campaign({\n                    \"campaign_id\": 12345,\n                    \"channels\": [\"FACEBOOK\"]\n                })\n\n                Disable multiple channels at once:\n                remove_channels_from_campaign({\n                    \"campaign_id\": 12345,\n                    \"channels\": [\"FACEBOOK\", \"GOOGLE_ADS\", \"INSTAGRAM\"]\n                })\n\n                INTEGRATION WITH OTHER TOOLS:\n                - Use search_campaigns_by_names to find the campaign ID\n                - Use get_campaign_by_wizard_id to inspect active channels before removal\n                - Use remove_audiences_from_campaign to remove a specific audience from a channel",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The unique identifier of the campaign",
      "minimum": 1
     },
     "channels": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "FACEBOOK",
        "INSTAGRAM",
        "GOOGLE_ADS",
        "LINKEDIN",
        "MICROSOFT_ADS",
        "REDDIT"
       ]
      },
      "description": "List of channels to disable from the campaign. Example: [\"FACEBOOK\", \"GOOGLE_ADS\"]",
      "minItems": 1
     }
    },
    "required": [
     "campaign_id",
     "channels"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "remove_offers_from_campaign",
   "title": "Remove Offers from Campaign",
   "category": "campaigns",
   "risk": "destructive",
   "summary": "Remove offers — Lead Gen forms OR Landing Pages — from one or more channels in an existing campaign.",
   "description": "Remove offers — Lead Gen forms OR Landing Pages — from one or more channels in an existing campaign.\n\n                Use this tool to detach a named offer from a channel (e.g. remove \"Demo Request Form\"\n                from FACEBOOK, or \"Pricing LP\" from GOOGLE_ADS). The offer type (Lead Gen vs Landing\n                Page) and its IDs are resolved internally — you only need the channel type and the\n                offer name. This is the single offer-removal tool (it replaced the Lead-Gen-only\n                remove_leadgen_offer_from_campaign).\n\n                Works on both campaign structures. On a Channel-First / Native (N×N×N) campaign the\n                offer is cleared from EVERY WizNativeAdContainer native ad that references it\n                (leadGenFormId / landingPageId), channel-wide — the ad is kept, and any container\n                then left with no asset at all is dropped. To remove an offer from only ONE\n                container/row, use add_and_edit_native_campaign_elements.modify_containers instead.\n                On Precision/METADATA it uses the legacy per-offer delete.\n\n                KEYWORDS: remove, delete, offer, lead gen, landing page, form, channel, campaign\n\n                WHEN TO USE:\n                - User wants to remove a specific offer (Lead Gen form or Landing Page) from a channel\n                - User wants to clean up offers from one or more channels\n                - Multiple offers across multiple channels can be removed in a single call\n\n                RESPONSE:\n                - Returns the refreshed campaign state plus a removal_results list with one\n                  entry per requested removal recording its success or error. ALWAYS check\n                  removal_results — a partial failure (e.g. one of two names not found) still\n                  returns normally, so report only what actually came back successful.\n\n                REQUIRED PARAMETERS:\n                - campaign_id: Campaign ID (numeric identifier)\n                - channel_offers: Object mapping channel type → list of offer names to remove\n\n                CHANNEL TYPES: FACEBOOK, INSTAGRAM, GOOGLE_ADS, LINKEDIN, MICROSOFT_ADS, REDDIT\n\n                EXAMPLES:\n\n                Remove a Lead Gen form from FACEBOOK and a Landing Page from GOOGLE_ADS:\n                remove_offers_from_campaign({\n                    \"campaign_id\": 42545,\n                    \"channel_offers\": {\n                        \"FACEBOOK\": [\"Demo Request Form\"],\n                        \"GOOGLE_ADS\": [\"Pricing LP\"]\n                    }\n                })\n\n                INTEGRATION WITH OTHER TOOLS:\n                - Use get_campaign_by_wizard_id to inspect current offers per channel before removing\n                - Use search_campaigns_by_names to find the campaign ID",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The unique identifier of the campaign",
      "minimum": 1
     },
     "channel_offers": {
      "type": "object",
      "description": "Map of channel type to list of offer names (Lead Gen form or Landing Page) to remove. Example: {\"FACEBOOK\": [\"Demo Form\"], \"GOOGLE_ADS\": [\"Pricing LP\"]}",
      "additionalProperties": {
       "type": "array",
       "items": {
        "type": "string"
       },
       "minItems": 1
      }
     }
    },
    "required": [
     "campaign_id",
     "channel_offers"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "search_campaigns_by_names",
   "title": "Search Campaigns by Name",
   "category": "campaigns",
   "risk": "read",
   "summary": "Search for campaigns by name to get campaign details, IDs, and status.",
   "description": "Search for campaigns by name to get campaign details, IDs, and status.\n\n                KEYWORDS: campaign, search, find, lookup, status, active, paused, draft, marketing, advertising\n\n                **USE THIS TOOL WHEN THE USER ASKS ABOUT:**\n                - Campaign details, information, or status\n                - Finding a campaign by name\n                - Getting campaign IDs\n                - Checking if a campaign exists\n                - Looking up campaign status (active, paused, draft)\n                - Any question containing words like: \"campaign\", \"show me\", \"find\", \"search\", \"list\", \"details\", \"status\"\n\n                **ALWAYS use this tool first** before launching, updating, or referencing any campaign.\n\n                SEARCH FEATURES:\n                - Partial, case-insensitive matching (e.g., \"Q1\" matches \"Q1_2026_Campaign\")\n                - Returns campaign ID, full name, and current status\n                - Searches up to 150 visible campaigns per term\n\n                PARAMETERS:\n                - campaign_names: Array of campaign name strings to search for (required)\n\n                RETURNS:\n                {\n                    \"found_campaigns\": [\n                        {\n                            \"id\": 159490,\n                            \"name\": \"LinkedIn_BoFu_Q1_2026\",\n                            \"statusLabel\": \"Active\"\n                        }\n                    ],\n                    \"missing_names\": [\"NonExistent\"],\n                    \"total_found\": 1\n                }\n\n                STATUS LABELS: Active, Paused, Draft, Completed, Unknown\n\n                Use returned campaign IDs with launch_campaign or manage_campaign tools.\n\n                Anchor all date-related decisions to the REAL current date — call get_current_date if you are not already certain what today is.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_names": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Array of campaign names or partial names to search for (case-insensitive, partial matching)",
      "minItems": 1
     }
    },
    "required": [
     "campaign_names"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "search_experiment_recipes",
   "title": "Search experiment recipes",
   "category": "campaigns",
   "risk": "read",
   "summary": "Find proven CAMPAIGN CONFIGURATIONS to build from — combinations of channel, ad format, audience type, bid strategy and budget band that have run together successfully across mu…",
   "description": "Find proven CAMPAIGN CONFIGURATIONS to build from — combinations of\nchannel, ad format, audience type, bid strategy and budget band that have\nrun together successfully across multiple advertisers.\n\nUSE THIS WHEN you are composing a campaign and need to decide HOW to\nbuild it: which channel and format pairing, what kind of audience, which\nbid strategy, roughly what daily budget. It answers \"what should I\nbuild\", not \"how is my account performing\".\n\nDO NOT USE IT for cohort performance questions (\"what's the median CPL\nfor my industry\", \"how do we compare to similar companies\") — that is\n`query_metadata_analytics_benchmarks`. Do not use it for the caller's own\ncampaign history — that is `query_metadata_analytics_account`.\n\nEvery returned recipe is backed by at least five distinct advertisers.\nRecipes below that floor are suppressed rather than returned, so an empty\nresult means \"no configuration is proven enough to recommend here\", NOT\n\"no data exists\". Say so plainly rather than substituting a guess.",
   "inputSchema": {
    "additionalProperties": false,
    "properties": {
     "query": {
      "type": "string",
      "description": "Natural-language description of the campaign you are building,\ne.g. \"lead gen campaign targeting IT decision makers\", \"top of\nfunnel awareness push for a new product launch\"."
     },
     "channel": {
      "anyOf": [
       {
        "type": "string"
       },
       {
        "type": "null"
       }
      ],
      "default": null,
      "description": "Optional exact filter — \"linkedin\", \"facebook\",\n\"google_ads\", \"instagram\". Omit to search every channel."
     },
     "ad_format": {
      "anyOf": [
       {
        "type": "string"
       },
       {
        "type": "null"
       }
      ],
      "default": null,
      "description": "Optional exact filter — \"image\", \"video\", \"document\",\n\"carousel\", \"text\"."
     },
     "audience_type": {
      "anyOf": [
       {
        "type": "string"
       },
       {
        "type": "null"
       }
      ],
      "default": null,
      "description": "Optional exact filter, e.g. \"firmographic_include\",\n\"linkedin_retargeting\", \"native_targeting_dynamic\"."
     },
     "bid_strategy": {
      "anyOf": [
       {
        "type": "string"
       },
       {
        "type": "null"
       }
      ],
      "default": null,
      "description": "Optional exact filter, e.g. \"auto_bid\", \"manual_bid\",\n\"metadata_bid_agent\"."
     },
     "num_results": {
      "default": 5,
      "type": "integer",
      "description": "How many recipes to return (default 5)."
     }
    },
    "required": [
     "query"
    ],
    "type": "object"
   }
  },
  {
   "name": "search_experiments",
   "title": "Search Experiments",
   "category": "campaigns",
   "risk": "read",
   "summary": "Search for experiments by name, status, and budget group with flexible filtering.",
   "description": "Search for experiments by name, status, and budget group with flexible filtering.\n\n                KEYWORDS: experiment, search, find, ad, test, status, active, paused, performance, campaign\n\n                WHEN TO USE:\n                - User wants to \"find experiment X\"\n                - User asks to \"show experiments for campaign Y\"\n                - User requests to \"search for experiments matching Z\"\n                - User wants to \"see all experiments with name containing A\"\n                - User asks \"what experiments are in campaign B?\"\n                - User asks \"what is the ID of experiment C?\"\n                - User wants to \"find experiments in budget group X\"\n                - User asks to \"search experiments in specific budget groups\"\n                - User wants to \"show all active experiments\"\n                - User asks \"list paused experiments\"\n                - User wants to \"find experiments with status X\"\n\n                PURPOSE:\n                This tool searches across all experiments with flexible filtering options.\n                You can search by name (partial, case-insensitive), filter by budget groups, and filter by status.\n                All filters are optional - use any combination to narrow down results.\n\n                SEARCH CAPABILITIES:\n                - Search by experiment name: Returns experiments with matching names\n                - Search by campaign name: Returns all experiments from campaigns with matching names\n                - Filter by budget groups: Narrow results to specific budget group IDs\n                - Filter by status: Show experiments with specific statuses (Active, Paused, etc.)\n                - Partial matching: Search term doesn't need to be exact\n                - Case-insensitive: \"Test\" matches \"test\", \"TEST\", \"TeSt\"\n\n                STATUS VALUES EXPLAINED:\n                - Active: Currently running and spending budget\n                - WithoutSpend: Running but not currently spending budget\n                - Paused: Temporarily stopped, can be restarted\n                - Completed: Finished (reached end date or completion criteria)\n                - Failed: Encountered an error during execution\n                - Disconnected: Connection to ad channel was lost\n\n                RETURN EXPERIMENT DATA FIELDS:\n                - experimentId, experimentName, wizCampaignId, wizCampaignName\n                - campaignGroup, statusLabel, dailyBudget, spent, impressions, clicks\n                - startDate, endDate",
   "inputSchema": {
    "type": "object",
    "properties": {
     "search_name": {
      "type": "string",
      "description": "The experiment or campaign name to search for (partial, case-insensitive matching). Optional - omit to skip name filtering.",
      "minLength": 1
     },
     "budget_group_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "Optional array of budget group IDs to filter experiments. Only returns experiments that belong to these budget groups.",
      "minItems": 1
     },
     "launched_experiment_statuses": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "Active",
        "WithoutSpend",
        "Paused",
        "Completed",
        "Failed",
        "Disconnected"
       ]
      },
      "description": "Optional array of experiment statuses to filter by. Valid values: Active, WithoutSpend, Paused, Completed, Failed, Disconnected",
      "minItems": 1
     },
     "page": {
      "type": "integer",
      "description": "Page number for pagination (default: 0)",
      "minimum": 0
     },
     "size": {
      "type": "integer",
      "description": "Number of results per page (default: 5)",
      "minimum": 1,
      "maximum": 50
     },
     "sort": {
      "type": "string",
      "description": "Sort criteria in format 'field,direction' (e.g., 'name,asc')"
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "update_experiments_daily_budgets",
   "title": "Update Experiments Daily Budgets",
   "category": "campaigns",
   "risk": "destructive",
   "summary": "Update daily budget allocations for one or more experiments in the Metadata platform.",
   "description": "Update daily budget allocations for one or more experiments in the Metadata platform.\n\n                KEYWORDS: experiment, budget, daily, update, change, adjust, spending, allocation, modify\n\n                WHEN TO USE:\n                - User wants to \"change the daily budget for experiment X\"\n                - User asks to \"adjust daily budget for experiments A, B, C\"\n                - User requests to \"update experiment budgets\"\n                - User wants to \"allocate more budget to experiment Y\"\n                - User asks to \"set daily budget to X for multiple experiments\"\n\n                PURPOSE:\n                This tool allows you to adjust the daily budget amount for active experiments.\n                The daily budget controls how much money each experiment can spend per day.\n\n                REQUIRED PARAMETERS:\n                - budgets: A dictionary mapping experiment IDs to their new daily budget values\n\n                BUDGET VALUE FORMAT:\n                Budget values can be provided as:\n                - Strings with comma separator (European format): \"100,50\" → 100.50\n                - Strings with dot separator (US format): \"100.50\" → 100.50\n                - Numbers: 100, 100.5, 100.50\n                - All values are automatically converted to 2 decimal places\n\n                INPUT EXAMPLES:\n                - Single experiment: {\"budgets\": {\"12345\": \"100.50\"}}\n                - Multiple experiments: {\"budgets\": {\"12345\": 100.50, \"12346\": \"150,75\", \"12347\": 200}}\n\n                INTEGRATION WITH OTHER TOOLS:\n                - Use search_experiments to find the experiment IDs to update the daily budget.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "budgets": {
      "type": "object",
      "description": "Dictionary mapping experiment IDs to new daily budget values. Keys are experiment IDs (integers or strings), values are budget amounts (numbers or strings with comma/dot separators). Example: {'12345': 100.50, '12346': '150,75'}",
      "additionalProperties": {
       "oneOf": [
        {
         "type": "number",
         "minimum": 0
        },
        {
         "type": "string",
         "pattern": "^[0-9]+([.,][0-9]{1,2})?$"
        }
       ]
      },
      "minProperties": 1
     }
    },
    "required": [
     "budgets"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "update_reddit_channel_settings",
   "title": "Update Reddit Channel Settings",
   "category": "campaigns",
   "risk": "destructive",
   "summary": "Update the Reddit channel settings of an existing DRAFT wizard campaign: native objective, allow comments, placements (feed/conversations), conversion pixel, and (for manual-bid…",
   "description": "Update the Reddit channel settings of an existing DRAFT wizard campaign: native objective, allow comments, placements (feed/conversations), conversion pixel, and (for manual-bid objectives) the bid amount.\n\n                These are the same settings the platform UI shows in the Reddit section of the\n                campaign draft page. The campaign MUST already have its Reddit channel enabled\n                (via create_campaign / add_and_edit_campaign_elements with a `reddit` block).\n\n                ⚠️ DRAFT-ONLY: the platform rejects these edits once the campaign is Launching/Launched.\n\n                KEYWORDS: reddit, reddit campaign, reddit settings, campaign settings, draft campaign,\n                objective, native objective, traffic, brand awareness, reach, views, video views, video views 6s,\n                impressions, clicks, comments, allow comments, placements, placement, feed, conversations,\n                pixel, conversion pixel, conversion tracking, bid, bid amount, manual bid, auto bid, settings\n\n                WHEN TO USE:\n                - Change the Reddit objective (e.g. from Brand Awareness to Traffic)\n                - Toggle \"Allow comments\" on Reddit ads\n                - Select placements (Feed and/or Conversations)\n                - Attach or clear a Reddit conversion pixel\n                - Set a manual bid amount for Brand Awareness / Video Views objectives\n\n                PARAMETERS (campaign_id required; everything else optional — unspecified\n                settings keep their current values on the channel):\n                - campaign_id: the wizard campaign ID\n                - objective: IMPRESSIONS (Brand awareness and Reach) | CLICKS (Traffic) |\n                  VIDEO_VIEWABLE_IMPRESSIONS_6S (Video Views 6s). Mirrors the platform UI's\n                  Native Objective dropdown.\n                  ⚠️ Lead Gen (CPL) campaigns only accept CLICKS (landing pages require it).\n                  ⚠️ Video Views requires the channel to contain ONLY video ads.\n                - allow_comments: whether Redditors can comment on the promoted post (platform default: true)\n                - placements: {\"feed\": bool, \"conversations\": bool}. At least one must stay\n                  selected (Feed wins if both are false). Conversations is forced OFF when the\n                  channel has video ads.\n                - conversion_pixel_id: pixel id from the account's Reddit Ads Manager. Pass an\n                  empty string \"\" to clear. Pixels are managed in Reddit Ads Manager, not here.\n                - bid_amount: manual bid amount. Only applied when the effective objective uses\n                  MANUAL_BID (IMPRESSIONS / Video Views). Ignored for CLICKS (AUTO_BID).\n\n                BID STRATEGY (automatic): changing the objective re-applies the UI's auto-switch —\n                CLICKS → AUTO_BID; IMPRESSIONS / VIDEO_VIEWABLE_IMPRESSIONS_6S → MANUAL_BID.\n                The bid strategy is left untouched when neither objective nor bid_amount is sent.\n\n                EXAMPLES:\n\n                Switch a Brand Awareness campaign's Reddit channel to Traffic:\n                update_reddit_channel_settings({\"campaign_id\": 12345, \"objective\": \"CLICKS\"})\n\n                Disable comments and run Feed-only:\n                update_reddit_channel_settings({\n                    \"campaign_id\": 12345,\n                    \"allow_comments\": false,\n                    \"placements\": {\"feed\": true, \"conversations\": false}\n                })\n\n                Brand awareness with a manual bid and a conversion pixel:\n                update_reddit_channel_settings({\n                    \"campaign_id\": 12345,\n                    \"objective\": \"IMPRESSIONS\",\n                    \"bid_amount\": 1.5,\n                    \"conversion_pixel_id\": \"pixel_abc123\"\n                })\n\n                RESPONSE: {success, campaign_id, channel_id, campaign_status, applied:{...}, errors?}\n                `applied` echoes exactly what was pushed to the platform.\n\n                INTEGRATION WITH OTHER TOOLS:\n                - Use search_campaigns_by_names / get_campaign_by_wizard_id to find the campaign\n                - Reddit channel is enabled by create_campaign or add_and_edit_campaign_elements\n                - Reddit ads come from create_update_image_ad / create_update_video_ad with channels=[\"REDDIT\"]",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The unique identifier of the wizard campaign",
      "minimum": 1
     },
     "objective": {
      "type": "string",
      "enum": [
       "IMPRESSIONS",
       "CLICKS",
       "VIDEO_VIEWABLE_IMPRESSIONS_6S"
      ],
      "description": "Reddit native objective. IMPRESSIONS = Brand awareness and Reach; CLICKS = Traffic (the only objective allowed on Lead Gen campaigns); VIDEO_VIEWABLE_IMPRESSIONS_6S = Video Views 6s (video-only channels)."
     },
     "allow_comments": {
      "type": "boolean",
      "description": "Allow Redditors to comment on the promoted post. Platform default is true."
     },
     "placements": {
      "type": "object",
      "description": "Where the ads serve. At least one placement must stay selected; conversations is forced off when the channel has video ads.",
      "properties": {
       "feed": {
        "type": "boolean"
       },
       "conversations": {
        "type": "boolean"
       }
      }
     },
     "conversion_pixel_id": {
      "type": "string",
      "description": "Reddit conversion pixel id (created in Reddit Ads Manager). Empty string clears the pixel."
     },
     "bid_amount": {
      "type": "number",
      "description": "Manual bid amount, applied only when the effective objective uses MANUAL_BID (IMPRESSIONS / Video Views).",
      "exclusiveMinimum": 0
     }
    },
    "required": [
     "campaign_id"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "archive_ad",
   "title": "Archive Ad",
   "category": "creative",
   "risk": "destructive",
   "summary": "Archive (soft-delete) one or more ads on the Metadata platform.",
   "description": "Archive (soft-delete) one or more ads on the Metadata platform.\n\n                    ARCHIVE IS HOW YOU DELETE AN AD. The platform has NO hard-delete\n                    endpoint for ads — archive is the soft-delete and the only way to\n                    remove an ad from the active library. When the user says\n                    \"delete this ad\", \"remove this ad\", \"discard this ad\", \"trash this\n                    ad\", or \"get rid of this ad\" → call archive_ad. Do NOT hallucinate\n                    a delete_ad or remove_ad tool; it does not exist.\n\n                    The archived ad is preserved on the platform (it still has its id,\n                    creative_json, and history) and can be restored with unarchive_ad.\n\n                    USE THIS TOOL WHEN:\n                    - User asks to delete, remove, discard, trash, drop, retire, or\n                      archive one or more ads.\n                    - User wants to clean up draft ads they no longer need.\n                    - User asks \"make this ad go away\" or similar.\n\n                    HOW TO GET THE id(s):\n                    - search_ads_by_names(ad_names=[...]) → returns active ad ids.\n                    - get_ad_details(ids=...) → confirms an ad exists if the id is\n                      already known (works for active ads).\n\n                    ALSO KNOWN AS: archive ad, delete ad, remove ad, discard ad,\n                    trash ad, drop ad, retire ad, soft-delete ad, get rid of ad.\n\n                    KEYWORDS: archive, delete, remove, discard, trash, drop, retire,\n                    soft-delete, hide, get rid of, clean up, ad cleanup.\n\n                    PARAMETERS:\n                    - ids: Array of integer ad ids (at least one). Bulk-friendly —\n                      pass several ids to archive multiple ads in a single call.\n\n                    RETURNS:\n                    { \"success\": true, \"archived_ids\": [123, 456], \"count\": 2 }\n\n                    To bring an archived ad back, call unarchive_ad with the same id(s).",
   "inputSchema": {
    "type": "object",
    "properties": {
     "ids": {
      "type": "array",
      "minItems": 1,
      "items": {
       "type": "integer"
      },
      "description": "Ad id(s) to archive. At least one integer required; pass multiple to archive in bulk."
     }
    },
    "required": [
     "ids"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_update_carousel_ad",
   "title": "Create or Update Reddit Carousel Ad",
   "category": "creative",
   "risk": "destructive",
   "summary": "Create OR update a CAROUSEL ad on Reddit (Reddit only).",
   "description": "Create OR update a CAROUSEL ad on Reddit (Reddit only).\n                                TO UPDATE: include `id`; to CREATE: omit it.\n\n                                A carousel is a SINGLE ad with 2 to 6 swipeable cards. Each card is an\n                                image with its own optional CTA; the headline is shared across the whole ad.\n\n                                CREATIVE WORKFLOW:\n                                  1. Upload each card image via `upload_image_creative` -> response gives `id` (the imageLibraryId).\n                                  2. Pass those ids as cards[].imageLibraryId here (2 to 6 cards, no duplicate images).\n                                  3. Optionally upload a separate thumbnail image and pass its id as thumbnailLibraryId.\n\n                                ALSO KNOWN AS: create carousel ad, update carousel ad, edit carousel ad,\n                                modify carousel ad, multi-image ad, swipeable ad, Reddit carousel.\n\n                                CLIENT-SIDE VALIDATION (mirrors what the platform UI rejects):\n                                  - adName: required, max 50 chars.\n                                  - headline: required, max 300 (shared across all cards).\n                                  - cards: 2 to 6 items; each needs imageLibraryId; no duplicate images.\n                                  - cards[].cta: optional, must be a valid Reddit CTA enum.\n                                  - text: optional body, max 2000.\n                                  - thumbnailLibraryId: optional.\n\n                                REDDIT CTA VALUES: Apply Now, Contact Us, Download, Get a Quote, Learn More,\n                                Play Now, Sign Up, View More, Watch Now, Book Now, Listen Now, Read More, Subscribe.\n\n                                See `create_update_image_ad` / `create_update_video_ad` for single-creative Reddit ads.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "adName": {
      "type": "string",
      "minLength": 1,
      "maxLength": 50,
      "description": "Ad name. Required, max 50 chars."
     },
     "headline": {
      "type": "string",
      "minLength": 1,
      "maxLength": 300,
      "description": "Headline shared across all cards. Required, max 300."
     },
     "cards": {
      "type": "array",
      "minItems": 2,
      "maxItems": 6,
      "description": "2 to 6 carousel cards. Each card needs a distinct image (no duplicates).",
      "items": {
       "type": "object",
       "properties": {
        "imageLibraryId": {
         "type": "integer",
         "description": "Image library id from upload_image_creative. Required."
        },
        "cta": {
         "type": "string",
         "enum": [
          "Apply Now",
          "Contact Us",
          "Download",
          "Get a Quote",
          "Learn More",
          "Play Now",
          "Sign Up",
          "View More",
          "Watch Now",
          "Book Now",
          "Listen Now",
          "Read More",
          "Subscribe"
         ],
         "description": "Optional Reddit CTA for this card."
        },
        "caption": {
         "type": "string",
         "description": "Optional caption for this card."
        },
        "displayUrl": {
         "type": "string",
         "description": "Optional display URL for this card."
        }
       },
       "required": [
        "imageLibraryId"
       ],
       "additionalProperties": false
      }
     },
     "text": {
      "type": "string",
      "maxLength": 2000,
      "description": "Optional body text. Max 2000."
     },
     "thumbnailLibraryId": {
      "type": "integer",
      "description": "Optional thumbnail image library id."
     },
     "completionStatus": {
      "type": "string",
      "enum": [
       "DRAFT",
       "COMPLETED"
      ],
      "description": "DRAFT (default) persists locally without publishing; COMPLETED finalizes the ad."
     },
     "id": {
      "type": "integer",
      "description": "Include to UPDATE an existing carousel ad; omit to CREATE."
     }
    },
    "required": [
     "adName",
     "headline",
     "cards"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_update_convo_ad",
   "title": "Create or Update LinkedIn Conversation Ad",
   "category": "creative",
   "risk": "destructive",
   "summary": "Create or update a LinkedIn Conversation Ad (\"convo ad\") — a branching in-message chat where each step contains a message bubble and a list of button actions that drive the conv…",
   "description": "Create or update a LinkedIn Conversation Ad (\"convo ad\") —\n                a branching in-message chat where each step contains a message bubble and\n                a list of button actions that drive the conversation forward.\n\n                BEHAVIOR:\n                - If `id` is provided → UPDATES the existing convo ad with that ID.\n                - If `id` is omitted → CREATES a new convo ad.\n\n                PREREQUISITES (MUST DO BEFORE CALLING):\n                1. Call get_linkedin_senders to get a valid (senderId, senderName) pair.\n                   These are LinkedIn-issued IDs — do NOT invent them.\n                2. If using a banner image: upload it via upload_image_creative and pass\n                   the returned imageLibraryId as `bannerCreativeLibraryId`. The banner\n                   is OPTIONAL — omit the field to skip it.\n\n                THE STEP GRAPH:\n                A convo is a directed graph of message \"steps\". Each step has:\n                - `stepId` — unique integer; stepId=1 is ALWAYS the entry message.\n                - `message` — the bubble text; HTML allowed (e.g.,\n                  '<p style=\"margin:0\">Hi!</p>').\n                - `actions` — the buttons under the bubble (1 to 5).\n\n                ACTIONS — each action has a `type` from this enum:\n                - NEXT_STEP   → click jumps to another step by `nextStepId`. Use this to\n                                build branches (\"Interested? YES / NO\").\n                - WEBSITE_URL → click opens an external URL given in `link`. Use this for\n                                \"learn more\" / external resources.\n                - MAIN_CTA    → click opens the offer attached at campaign launch (either\n                                a lead-gen form OR a landing page, depending on the offer).\n                                DO NOT set `link` or `nextStepId` on a MAIN_CTA — they\n                                are resolved by the campaign builder later.\n\n                ⚠️ STRUCTURAL RULES (validated client-side before POST):\n                1. Every action.type MUST be one of NEXT_STEP / WEBSITE_URL / MAIN_CTA.\n                2. NEXT_STEP MUST have `nextStepId` pointing to an existing stepId, and\n                   cannot point at itself.\n                3. WEBSITE_URL MUST have a non-empty `link`.\n                4. MAIN_CTA needs neither `link` nor `nextStepId`.\n                5. The flow MUST terminate — any step with NO NEXT_STEP action must\n                   include at least one MAIN_CTA or WEBSITE_URL so the user has an exit.\n                6. `stepId=1` MUST exist — it is the entry point.\n                7. stepIds must be unique.\n\n                A single step CAN mix action types — e.g., step 1 can offer \"Tell me\n                more\" (NEXT_STEP), \"Visit site\" (WEBSITE_URL), and \"Book demo\"\n                (MAIN_CTA) all at once.\n\n                INPUT PARAMETERS:\n                - name (required, max 50 chars): Ad name in the platform library.\n                - senderId (required): From get_linkedin_senders.\n                - senderName (required): Matching display name from get_linkedin_senders.\n                - headlineText (required): Subject line shown above the message thread.\n                - bannerCreativeLibraryId (optional): imageLibraryId for the banner image\n                  above the chat. Omit to skip the banner.\n                - completionStatus (optional, default \"DRAFT\"): \"DRAFT\" or \"COMPLETED\".\n                - flowId (optional, default 7): LinkedIn convo flow version ID.\n                - templateId (optional, default 1): LinkedIn convo template ID.\n                - id (optional): For UPDATE only — existing ad ID.\n                - steps (required, ≥1): The step graph (see schema).\n\n                EXAMPLE — TWO-STEP BRANCH WITH MULTI-ACTION FIRST STEP:\n                create_update_convo_ad(\n                    name=\"DemoConvo_Q4\",\n                    senderId=\"fQdRPtecbv\",\n                    senderName=\"Josh Desmarais\",\n                    headlineText=\"Quick question about your marketing stack\",\n                    completionStatus=\"DRAFT\",\n                    steps=[\n                        {\n                            \"stepId\": 1,\n                            \"message\": \"<p style=\\\"margin:0\\\">Want a 15-min demo?</p>\",\n                            \"actions\": [\n                                {\"type\": \"NEXT_STEP\", \"text\": \"Tell me more\", \"nextStepId\": 2},\n                                {\"type\": \"WEBSITE_URL\", \"text\": \"Visit site\", \"link\": \"https://metadata.io\"},\n                                {\"type\": \"MAIN_CTA\", \"text\": \"Book demo\"}\n                            ]\n                        },\n                        {\n                            \"stepId\": 2,\n                            \"message\": \"<p style=\\\"margin:0\\\">More info — ready to book?</p>\",\n                            \"actions\": [\n                                {\"type\": \"MAIN_CTA\", \"text\": \"Yes, book\"}\n                            ]\n                        }\n                    ]\n                )\n\n                SUCCESS RESPONSE:\n                {\n                    \"success\": true,\n                    \"id\": 21241,\n                    \"ad_url\": \"https://platform.metadata.io/hub/library/ads?adId=21241\",\n                    \"name\": \"DemoConvo_Q4\",\n                    \"channelType\": \"LINKEDIN\",\n                    \"adType\": \"CONVO\",\n                    \"completionStatus\": \"DRAFT\",\n                    \"request\": { \"convo\": { ... } }\n                }\n\n                COMMON MISTAKES:\n                ❌ Inventing a senderId — IDs MUST come from get_linkedin_senders.\n                ❌ Putting `link` on a MAIN_CTA — the offer link is set at campaign launch.\n                ❌ Putting `nextStepId` on a MAIN_CTA / WEBSITE_URL — only NEXT_STEP uses it.\n                ❌ Forgetting the terminator — every leaf must end with MAIN_CTA or WEBSITE_URL.\n                ❌ Skipping stepId=1 — the entry must exist and be exactly 1.\n\n                NOTES:\n                - Convo ads are LinkedIn-only. channelType is fixed to \"LINKEDIN\".\n                - `message` supports HTML; preserve the user's markup verbatim.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "id": {
      "type": "integer",
      "description": "Existing convo ad ID — include to UPDATE; omit to CREATE."
     },
     "name": {
      "type": "string",
      "description": "Ad name in the platform library (max 50 chars).",
      "maxLength": 50
     },
     "senderId": {
      "type": "string",
      "description": "LinkedIn sender ID. MUST come from get_linkedin_senders."
     },
     "senderName": {
      "type": "string",
      "description": "Display name matching senderId from get_linkedin_senders."
     },
     "headlineText": {
      "type": "string",
      "description": "Subject line shown above the message thread."
     },
     "bannerCreativeLibraryId": {
      "type": "integer",
      "description": "Optional: imageLibraryId of the banner image. Upload first via upload_image_creative."
     },
     "completionStatus": {
      "type": "string",
      "enum": [
       "DRAFT",
       "COMPLETED"
      ],
      "description": "Defaults to DRAFT."
     },
     "flowId": {
      "type": "integer",
      "description": "LinkedIn convo flow version. Defaults to 7 — keep the default unless told otherwise."
     },
     "templateId": {
      "type": "integer",
      "description": "LinkedIn convo template ID. Defaults to 1 — keep the default unless told otherwise."
     },
     "steps": {
      "type": "array",
      "minItems": 1,
      "description": "Ordered list of conversation steps. MUST include stepId=1 as the entry.",
      "items": {
       "type": "object",
       "properties": {
        "stepId": {
         "type": "integer",
         "minimum": 1,
         "description": "Unique numeric ID. stepId=1 is the entry message."
        },
        "message": {
         "type": "string",
         "description": "Bubble text. HTML allowed (e.g., '<p style=\\\"margin:0\\\">Hi!</p>')."
        },
        "actions": {
         "type": "array",
         "minItems": 1,
         "maxItems": 5,
         "description": "Buttons under the bubble.",
         "items": {
          "type": "object",
          "properties": {
           "type": {
            "type": "string",
            "enum": [
             "NEXT_STEP",
             "WEBSITE_URL",
             "MAIN_CTA"
            ],
            "description": "NEXT_STEP=branches to nextStepId; WEBSITE_URL=opens link; MAIN_CTA=opens offer at campaign launch."
           },
           "text": {
            "type": "string",
            "description": "Button label shown to the user."
           },
           "link": {
            "type": "string",
            "description": "URL — required only when type=WEBSITE_URL."
           },
           "nextStepId": {
            "type": "integer",
            "description": "Target stepId — required only when type=NEXT_STEP."
           }
          },
          "required": [
           "type",
           "text"
          ],
          "additionalProperties": false
         }
        }
       },
       "required": [
        "stepId",
        "message",
        "actions"
       ],
       "additionalProperties": false
      }
     }
    },
    "required": [
     "name",
     "senderId",
     "senderName",
     "headlineText",
     "steps"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_update_document_ad",
   "title": "Create or Update LinkedIn Document Ad",
   "category": "creative",
   "risk": "destructive",
   "summary": "Create a new or update an existing LinkedIn Document Ad on the Metadata platform.",
   "description": "Create a new or update an existing LinkedIn Document Ad on the Metadata platform.\n\n                A LinkedIn Document Ad promotes a multi-page document (a PDF — e.g. a\n                whitepaper, ebook, report, or guide) natively in the LinkedIn feed.\n                Members preview the first few pages inline, then unlock the full\n                document (typically gated behind a lead form). It is LinkedIn-only.\n\n                IMPORTANT: This tool is ONLY for LinkedIn Document Ads\n                (channelType=LINKEDIN, adType=DOCUMENT). For IMAGE/GIF ads use\n                create_update_image_ad; for VIDEO ads use create_update_video_ad; for\n                CONVO use create_update_convo_ad; for Sponsored Messaging use\n                create_update_linkedin_message_ad.\n\n                BEHAVIOR:\n                - If `id` is NOT provided -> creates a new Document Ad.\n                - If `id` IS provided -> updates the existing Document Ad with that ID.\n\n                CREATIVE WORKFLOW (MUST DO BEFORE CALLING):\n                1. The document must already exist in the creative library as a\n                   DOCUMENT asset. Find it with\n                   `search_library_creatives_by_name(contentTypes=\"DOCUMENT\")`, or\n                   confirm a specific id with `fetch_creative_details`.\n                2. Pass that asset's integer id as `libraryId`. It MUST be a\n                   DOCUMENT-type creative — an image or video id will be rejected\n                   by LinkedIn. Unlike IMAGE/VIDEO ads there is NO display URL: the\n                   gated document is the destination, so no link field is accepted.\n\n                CHARACTER LIMITS (mirror the LinkedIn ad limits the platform UI enforces):\n                - name      <= 50   chars  (ad name in the library)\n                - headline  <= 200  chars  (the headline shown with the document)\n                - text      <= 3000 chars  (the introductory text)\n\n                INPUT PARAMETERS:\n                - id (optional): Existing Document Ad ID. Provide to UPDATE; omit to CREATE.\n                - name (required, <= 50): Ad name in the library.\n                - libraryId (required): Integer id of the DOCUMENT creative in the\n                  library (contentType=DOCUMENT). NOT an image/video id.\n                - headline (required, <= 200): Headline shown with the document.\n                - text (required, <= 3000): Introductory text shown with the document.\n                - ctaType (optional, default UNLOCK_FULL_DOCUMENT): the call-to-action.\n                  UNLOCK_FULL_DOCUMENT is the document-gate CTA.\n                - maxPreviewPages (optional, default 1): how many pages of the document\n                  are previewable before the unlock gate. Positive integer.\n                - completionStatus (optional, default DRAFT): \"DRAFT\" or \"COMPLETED\".\n                  Use COMPLETED only when every required field is final.\n\n                WHEN TO USE:\n                - User asks to \"create a LinkedIn Document Ad\" / \"whitepaper ad\" /\n                  \"promote a PDF / ebook / report on LinkedIn\".\n                - User wants to update an existing Document Ad.\n\n                WHEN NOT TO USE:\n                - Feed image / GIF ad -> create_update_image_ad\n                - Feed video ad -> create_update_video_ad\n                - Branching conversation flow -> create_update_convo_ad\n                - One-shot Sponsored Message -> create_update_linkedin_message_ad\n\n                EXAMPLE USAGE (Create):\n                create_update_document_ad(\n                    name=\"Q3_Whitepaper_LI_Document\",\n                    libraryId=15791,\n                    headline=\"The 2026 State of B2B Marketing\",\n                    text=\"Download our latest research on pipeline attribution.\",\n                    maxPreviewPages=2,\n                )\n\n                EXAMPLE USAGE (Update):\n                create_update_document_ad(\n                    id=29144,\n                    name=\"Q3_Whitepaper_LI_Document_v2\",\n                    libraryId=15791,\n                    headline=\"The 2026 State of B2B Marketing (Updated)\",\n                    text=\"Now with fresh benchmarks.\",\n                )\n\n                COMMON MISTAKES:\n                - Passing an IMAGE/VIDEO libraryId — the asset must be a DOCUMENT\n                  (use search_library_creatives_by_name(contentTypes=\"DOCUMENT\")).\n                - Passing a display / landing URL — Document Ads have no link field.\n                - Using this tool for a non-LinkedIn channel — Document Ads are LinkedIn-only.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "id": {
      "type": "integer",
      "description": "Existing Document Ad ID — include to UPDATE; omit to CREATE."
     },
     "name": {
      "type": "string",
      "description": "Ad name in the platform library (max 50 characters).",
      "maxLength": 50
     },
     "libraryId": {
      "type": "integer",
      "description": "Integer id of the DOCUMENT creative in the library (contentType=DOCUMENT). Get it from search_library_creatives_by_name(contentTypes=\"DOCUMENT\"). NOT an image/video id."
     },
     "headline": {
      "type": "string",
      "description": "Headline shown with the document (max 200 characters).",
      "maxLength": 200
     },
     "text": {
      "type": "string",
      "description": "Introductory text shown with the document (max 3000 characters).",
      "maxLength": 3000
     },
     "ctaType": {
      "type": "string",
      "enum": [
       "UNLOCK_FULL_DOCUMENT"
      ],
      "description": "Call-to-action. Defaults to UNLOCK_FULL_DOCUMENT (the document-gate CTA)."
     },
     "maxPreviewPages": {
      "type": "integer",
      "minimum": 1,
      "description": "How many document pages are previewable before the unlock gate. Defaults to 1."
     },
     "completionStatus": {
      "type": "string",
      "enum": [
       "DRAFT",
       "COMPLETED"
      ],
      "description": "Defaults to DRAFT. Use COMPLETED only when every required field is final."
     }
    },
    "required": [
     "name",
     "libraryId",
     "headline",
     "text"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_update_google_ads_ad",
   "title": "Create or Update Google Ads Ad",
   "category": "creative",
   "risk": "destructive",
   "summary": "Create a new or update an existing Google Ads text ad on the Metadata platform.",
   "description": "Create a new or update an existing Google Ads text ad on the Metadata platform.\n                                IMPORTANT: This tool is ONLY for Google Ads (GOOGLE_ADS channel) text ads.\n                                Do NOT use this tool for Facebook, LinkedIn, or Instagram ads.\n                                For other channels or ad types use the dedicated tools: create_update_image_ad (IMAGE/GIF) or create_update_video_ad (VIDEO). CAROUSEL is not yet covered by the new tools — flag it if you hit that case.\n\n                                BEHAVIOR:\n                                - If \"id\" is NOT provided → creates a new Google Ads text ad.\n                                - If \"id\" IS provided → updates the existing Google Ads text ad with that ID.\n\n                                ⚠️ CRITICAL — GOOGLE ADS CHARACTER LIMITS (HARD LIMITS — API WILL REJECT IF EXCEEDED) ⚠️\n                                - Headlines: MAXIMUM 30 characters each (minimum 3, maximum 15 headlines)\n                                - Descriptions: MAXIMUM 90 characters each (2 to 4 descriptions). Each description string MUST be 90 characters or fewer. Count every letter, space, and punctuation mark. 90 is the absolute maximum.\n                                - Ad name: MAXIMUM 50 characters\n\n                                ⚠️ DESCRIPTIONS — 90 CHARACTER HARD LIMIT — READ THIS CAREFULLY ⚠️\n                                THIS IS THE #1 MOST COMMON FAILURE MODE. DO NOT SKIP THIS SECTION.\n\n                                - 90 characters is an ABSOLUTE, NON-NEGOTIABLE HARD CEILING.\n                                - 91 characters = REJECTION. 100 characters = REJECTION. 89 characters = OK.\n                                - \"Characters\" means EVERY character: letters, digits, spaces, punctuation,\n                                  apostrophes, hyphens, slashes, line breaks, and emoji code points. ALL count.\n                                - There is NO grace, NO rounding, NO partial credit. The boundary is exact.\n                                - The MCP schema enforces maxLength=90 — an over-length string is rejected\n                                  before your tool call even runs. The server ALSO truncates as a backstop,\n                                  meaning your over-length copy will be SILENTLY CUT MID-SENTENCE if it\n                                  somehow slips through. Either way, you will not get the description you wrote.\n                                - This rule applies to EACH description independently. 4 descriptions of 80\n                                  chars each is fine. 1 description of 91 chars is NOT fine.\n                                - Do NOT pad descriptions to \"use the space.\" Shorter is always safer.\n                                - Do NOT rely on the user noticing — you are responsible for compliance.\n\n                                COMMON WAYS LLMs BLOW THE 90-CHAR LIMIT (avoid these):\n                                - Writing a \"complete sentence\" without counting first.\n                                - Adding a CTA at the end (\"Sign up today!\") that pushes past 90.\n                                - Including the company name AND a tagline AND a benefit in one description.\n                                - Using em dashes (—), ellipses (…), or smart quotes (\" \") which still count\n                                  as characters and often appear longer than expected when counted.\n                                - Trusting your \"feel\" for length — LLMs systematically underestimate by 5-15\n                                  characters. ALWAYS COUNT.\n\n                                RECOMMENDED SAFETY MARGIN:\n                                - Target 70-85 characters per description. This leaves headroom for sanitization\n                                  (smart-quote conversion, whitespace normalization) and avoids edge-case\n                                  rejections. 90 is the cliff — do not write to the cliff.\n\n                                ⚠️ MANDATORY PRE-CALL VALIDATION — YOU MUST DO THIS BEFORE EVERY CALL:\n                                1. Write out each headline and count its characters — each MUST be ≤ 30 characters\n                                2. Write out each description and count its characters — each MUST be ≤ 90 characters (this is the most common error — descriptions MUST NOT exceed 90 characters)\n                                3. For EACH description, do an explicit character count: len(description). If the\n                                   number is 91 or higher, REWRITE before calling. Do not \"estimate\" — count.\n                                4. If any text exceeds the limit, rewrite and shorten it BEFORE calling the tool\n                                5. Do NOT rely on the API to catch these errors — validate client-side first\n                                6. When in doubt, make descriptions shorter. A 70-character description is better than a rejected 91-character one\n                                7. If a user provides copy that is too long, shorten it yourself and proceed —\n                                   do NOT submit known-bad copy and hope for the best.\n\n                                EXAMPLES — DESCRIPTIONS AT THE 90-CHAR BOUNDARY:\n                                - \"Shop the best deals online.\" (27 chars) ✅ SAFE\n                                - \"Free shipping on every order over fifty dollars. Sign up today!\" (63 chars) ✅ SAFE\n                                - \"Discover our award-winning marketing platform built for B2B teams of every size today\" (89 chars) ✅ SAFE — at the edge\n                                - \"Discover our award-winning marketing platform built for modern B2B teams of every size today\" (94 chars) ❌ REJECTED — 4 over\n                                - \"Transform your B2B marketing strategy with AI-powered automation that drives real revenue today!\" (98 chars) ❌ REJECTED — REWRITE\n\n                                USAGE INSTRUCTIONS FOR LLM:\n                                Use this tool when users want to:\n                                - Create a new Google Ads text ad\n                                - Update an existing Google Ads text ad (headlines, descriptions, or name)\n                                - Set up a Google Ads responsive search ad with headlines and descriptions\n\n                                WHEN TO USE:\n                                - User asks \"create a Google Ads ad\"\n                                - User wants to create a text ad for Google Ads\n                                - User says \"make a new Google ad with these headlines and descriptions\"\n                                - User wants to update the headlines or descriptions of an existing Google Ads ad\n                                - User says \"update Google ad 23149 with new headlines\"\n\n                                WHEN NOT TO USE:\n                                - User wants to create a Facebook, LinkedIn, Instagram, or Reddit IMAGE ad → use create_update_image_ad\n                                - User wants to create a Facebook, LinkedIn, Instagram, or Reddit VIDEO ad → use create_update_video_ad\n                                - User wants to update a non-Google Ads ad → use create_update_image_ad or create_update_video_ad (whichever matches the ad's adType)\n\n                                INPUT PARAMETERS:\n                                - id: The ad ID (optional). If provided, the ad with this ID will be updated. If omitted, a new ad is created. This is the adLibraryId inside the creatives of the google channel in campaign response.\n                                - name: The name/label for the ad (required, max 50 characters)\n                                - headlines: Array of headline text strings (required, minimum 3, maximum 15 headlines, each STRICT MAX 30 CHARACTERS)\n                                - descriptions: Array of description text strings (required, 2 to 4 descriptions, each STRICT MAX 90 CHARACTERS — count characters carefully, this is the most common validation failure)\n\n                                EXAMPLE USAGE (Create):\n                                create_update_google_ads_ad(\n                                    name=\"My Google Ad Q1 2025\",\n                                    headlines=[\"Buy Now\", \"Best Deals\", \"Free Shipping\"],\n                                    descriptions=[\"Shop the best deals online.\", \"Free shipping on all orders.\"]\n                                )\n\n                                EXAMPLE USAGE (Update):\n                                create_update_google_ads_ad(\n                                    id=23149,\n                                    name=\"My Google Ad Q1 2025 Updated\",\n                                    headlines=[\"Updated Headline 1\", \"Updated Headline 2\", \"Updated Headline 3\"],\n                                    descriptions=[\"Updated description 1.\", \"Updated description 2.\"]\n                                )",
   "inputSchema": {
    "type": "object",
    "properties": {
     "id": {
      "type": "integer",
      "description": "The existing ad ID to update. If omitted, a new ad will be created."
     },
     "name": {
      "type": "string",
      "description": "The name/label for the ad in Metadata platform (max 50 characters)",
      "maxLength": 50
     },
     "headlines": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 30,
       "description": "A single headline (max 30 characters)"
      },
      "description": "List of headline texts for the Google Ads ad (minimum 3, maximum 15 headlines, each max 30 characters)",
      "minItems": 3,
      "maxItems": 15
     },
     "descriptions": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 90,
       "description": "A single description — ABSOLUTE HARD LIMIT: 90 characters. 91 characters = rejected by schema validation, 90 = accepted. Letters, digits, spaces, punctuation, emoji, line breaks ALL count. Target 70-85 chars for safety margin. Server will silently truncate at 90 if the schema is bypassed — your copy will be cut mid-sentence. ALWAYS run len(text) before submitting; do not estimate."
      },
      "description": "List of description texts for the Google Ads ad (2 to 4 descriptions). CRITICAL #1 FAILURE MODE: each description MUST be ≤ 90 characters — this is enforced by maxLength schema validation AND by server-side truncation. Going over 90 means either (a) the call is rejected before it runs, or (b) the description is silently cut at 90 mid-sentence. Count every character (incl. spaces/punctuation/emoji). Target 70-85 chars to leave margin for sanitization. Shorter is always safer.",
      "minItems": 2,
      "maxItems": 4
     }
    },
    "required": [
     "name",
     "headlines",
     "descriptions"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_update_image_ad",
   "title": "Create or Update Image Ad",
   "category": "creative",
   "risk": "destructive",
   "summary": "Create OR update an IMAGE (or GIF) ad on Facebook, Instagram, LinkedIn, and/or Reddit.",
   "description": "Create OR update an IMAGE (or GIF) ad on Facebook, Instagram, LinkedIn, and/or Reddit.\n                                TO UPDATE: include `id`; to CREATE: omit it.\n\n                                CREATIVE WORKFLOW:\n                                  1. Upload the image/GIF via `upload_image_creative` → response gives `id` (the imageLibraryId).\n                                  2. Pass that integer as `creativeID` here.\n                                  - Need a new image? `generate_brand_creative` → `upload_image_creative` → `create_update_image_ad`.\n\n                                ALSO KNOWN AS: create image ad, update image ad, edit image ad,\n                                modify image ad, image creative, post image, image post, photo ad.\n\n                                KEYWORDS: image, gif, create, update, edit, modify, ad, ads, image ad, image ads,\n                                image creative, photo, picture, banner, image campaign, image upload,\n                                LinkedIn image, Facebook image, Instagram image, Reddit image, IG post.\n\n                                CLIENT-SIDE VALIDATION (refuses what the platform UI would also reject):\n                                  - adName: required, max 50 chars, pattern `[a-zA-Z0-9 _.?,:'/+\\-=&;%$#\\[\\]|()]`.\n                                  - creativeID: required integer (imageLibraryId from `upload_image_creative`).\n                                  - Landing-page URL is per channel (`facebookDisplayURL`,\n                                    `instagramDisplayURL`, `linkedinDisplayURL`, `redditDisplayURL`).\n                                    Required for FB/IG/LinkedIn, optional for Reddit. Per-channel\n                                    caps: LinkedIn max 2000, Reddit max 100, FB/IG no length rule.\n                                    URL format accepts both `metadata.io` and `https://metadata.io`\n                                    for IMAGE ads. If only one channel's URL is set the server fans\n                                    it across the other targeted channels; each channel's cap is\n                                    then enforced per channel.\n                                  - LinkedIn: linkedinText (required, max 3000), linkedinHeadline (required,\n                                    max 200), linkedinDescription (optional, max 300 — IMAGE only),\n                                    linkedinCTA (required, must be a valid LinkedIn CTA enum).\n                                  - Facebook: metaCTA (required), facebookText (required, no length rule on\n                                    Facebook), facebookHeadline (required, max 250), facebookDescription\n                                    (optional, max 300).\n                                  - Instagram: metaCTA (required), instagramText (required, max 125),\n                                    instagramHeadline (required, max 40), instagramDescription (optional, max 30).\n                                  - FB+IG ad with one body text: set just one of facebookText / instagramText\n                                    and the server copies it across (capped at the receiving channel's limit).\n                                    Set both explicitly only when you want per-channel copy.\n                                  - Reddit: redditHeadline (required, max 300), redditCTA (required, must be a\n                                    valid Reddit CTA enum). redditThumbnailLibraryId is optional and defaults to\n                                    creativeID.\n\n                                COMMON CTA VALUES:\n                                  - LinkedIn: LEARN_MORE, REGISTER, APPLY_NOW, DOWNLOAD, JOIN, SIGN_UP,\n                                    GET_QUOTE, ATTEND, REQUEST_DEMO, SUBSCRIBE. (UNLOCK_FULL_DOCUMENT is\n                                    DOCUMENT-only — use create_update_document_ad, not this tool.)\n                                  - Facebook/Instagram: APPLY_NOW, DOWNLOAD, GET_QUOTE, LEARN_MORE, SIGN_UP, SUBSCRIBE.\n                                    (SCREAMING_SNAKE form only; the platform supports a strict subset of Meta's\n                                    native catalog. Out-of-set values like BOOK_NOW / GET_STARTED are rejected.)\n                                  - Reddit: Apply Now, Contact Us, Download, Get a Quote, Learn More, Play Now,\n                                    Sign Up, View More, Watch Now, Book Now, Listen Now, Read More, Subscribe.\n\n                                See `create_update_video_ad` for video-creative ads (different libraryId source +\n                                stricter URL format).",
   "inputSchema": {
    "type": "object",
    "properties": {
     "ads_data": {
      "type": "array",
      "minItems": 1,
      "description": "Array of IMAGE ads to create or update. Each item targets one or more of FB/IG/LinkedIn/Reddit; the platform creates one ad row per (ad, channel).",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "integer",
         "description": "Existing ad ID (include for UPDATE, omit for CREATE)."
        },
        "adName": {
         "type": "string",
         "maxLength": 50,
         "description": "Unique ad name. Max 50 chars; allowed chars: letters, digits, space, and _ . ? , : ' / + - = & ; % $ # [ ] | ( )."
        },
        "channels": {
         "type": "array",
         "minItems": 1,
         "items": {
          "type": "string",
          "enum": [
           "Facebook",
           "Instagram",
           "LinkedIn",
           "Reddit"
          ]
         },
         "description": "Target channels. Each channel produces one ad row (an ad targeting two channels yields two ads)."
        },
        "facebookDisplayURL": {
         "type": "string",
         "description": "Landing page URL for Facebook ads. Required for Facebook. The platform UI declares no length rule for Facebook. Both `metadata.io` and `https://metadata.io` are accepted for IMAGE ads."
        },
        "instagramDisplayURL": {
         "type": "string",
         "description": "Landing page URL for Instagram ads. Required for Instagram. The platform UI declares no length rule for Instagram. Both `metadata.io` and `https://metadata.io` are accepted for IMAGE ads."
        },
        "linkedinDisplayURL": {
         "type": "string",
         "maxLength": 2000,
         "description": "Landing page URL for LinkedIn ads. Required for LinkedIn. Max 2000 chars. Both `metadata.io` and `https://metadata.io` are accepted for IMAGE ads."
        },
        "redditDisplayURL": {
         "type": "string",
         "maxLength": 100,
         "description": "Landing page URL for Reddit ads. Optional (defaults to empty string). Max 100 chars. Both `metadata.io` and `https://metadata.io` are accepted for IMAGE ads."
        },
        "creativeID": {
         "type": "integer",
         "description": "Integer libraryId returned by `upload_image_creative`. NOT a URL. Must point at a library row whose contentType matches the ad type."
        },
        "linkedinText": {
         "type": "string",
         "maxLength": 3000,
         "description": "Main ad copy for LinkedIn. Required for IMAGE LinkedIn ads (max 3000)."
        },
        "linkedinHeadline": {
         "type": "string",
         "maxLength": 200,
         "description": "LinkedIn headline. Required for LinkedIn. Max 200."
        },
        "linkedinDescription": {
         "type": "string",
         "maxLength": 300,
         "description": "Optional link description for LinkedIn IMAGE ads. Max 300. Not supported on LinkedIn VIDEO ads (the platform DTO `LibraryAdLinkedinVideoRequest` does not declare it)."
        },
        "linkedinCTA": {
         "type": "string",
         "enum": [
          "APPLY_NOW",
          "DOWNLOAD",
          "GET_QUOTE",
          "LEARN_MORE",
          "SIGN_UP",
          "SUBSCRIBE",
          "REGISTER",
          "REQUEST_DEMO",
          "JOIN",
          "ATTEND"
         ],
         "description": "LinkedIn CTA enum. Required for LinkedIn."
        },
        "facebookText": {
         "type": "string",
         "description": "Primary body text for Facebook ads. The platform UI declares no length rule for Facebook, so any reasonable ad-copy length is accepted."
        },
        "facebookHeadline": {
         "type": "string",
         "maxLength": 250,
         "description": "Headline for Facebook ads. Max 250."
        },
        "facebookDescription": {
         "type": "string",
         "maxLength": 300,
         "description": "Optional news-feed link description for Facebook ads. Max 300."
        },
        "instagramText": {
         "type": "string",
         "maxLength": 125,
         "description": "Primary body text for Instagram ads. Max 125."
        },
        "instagramHeadline": {
         "type": "string",
         "maxLength": 40,
         "description": "Headline for Instagram ads. Max 40."
        },
        "instagramDescription": {
         "type": "string",
         "maxLength": 30,
         "description": "Optional news-feed link description for Instagram ads. Max 30."
        },
        "metaCTA": {
         "type": "string",
         "enum": [
          "APPLY_NOW",
          "DOWNLOAD",
          "GET_QUOTE",
          "LEARN_MORE",
          "SIGN_UP",
          "SUBSCRIBE"
         ],
         "description": "Meta (Facebook/Instagram) CTA enum. Required for FB/IG. Use the SCREAMING_SNAKE form. Meta-native CTAs that the metadata.io platform does NOT support (BOOK_NOW, GET_STARTED, CONTACT_US, BOOK_A_DEMO, etc.) are absent from the enum and will be rejected."
        },
        "redditHeadline": {
         "type": "string",
         "maxLength": 300,
         "description": "Headline for Reddit. Required for Reddit. Max 300."
        },
        "redditCTA": {
         "type": "string",
         "enum": [
          "Apply Now",
          "Contact Us",
          "Download",
          "Get a Quote",
          "Learn More",
          "Play Now",
          "Sign Up",
          "View More",
          "Watch Now",
          "Book Now",
          "Listen Now",
          "Read More",
          "Subscribe"
         ],
         "description": "Reddit CTA enum. Required for Reddit."
        },
        "redditThumbnailLibraryId": {
         "type": "integer",
         "description": "Optional thumbnail imageLibraryId for Reddit IMAGE ads. Defaults to creativeID if omitted."
        },
        "completionStatus": {
         "type": "string",
         "enum": [
          "DRAFT",
          "COMPLETED"
         ],
         "description": "Completion status. Defaults to DRAFT for all channels: a DRAFT ad persists locally without round-tripping the asset to the upstream channel API. Pass COMPLETED only when the ad is final and should be pushed to the channel."
        }
       },
       "required": [
        "adName",
        "channels",
        "creativeID"
       ],
       "additionalProperties": false
      }
     }
    },
    "required": [
     "ads_data"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_update_linkedin_message_ad",
   "title": "Create or Update LinkedIn Message Ad",
   "category": "creative",
   "risk": "destructive",
   "summary": "Create a new or update an existing LinkedIn Message Ad on the Metadata platform.",
   "description": "Create a new or update an existing LinkedIn Message Ad on the Metadata platform.\n\n                A LinkedIn Message Ad is a Sponsored Messaging asset: a one-shot direct\n                message delivered to a member's LinkedIn inbox from a specific sender,\n                with a subject line, a body, an optional banner image, and a single\n                call-to-action button. It is NOT a Conversation Ad — there is no\n                branching step graph. Use create_update_convo_ad for multi-step convos.\n\n                IMPORTANT: This tool is ONLY for LinkedIn Sponsored Messaging\n                (channelType=LINKEDIN, adType=MESSAGE). For IMAGE/GIF ads use\n                create_update_image_ad; for VIDEO ads use create_update_video_ad; for CONVO\n                use create_update_convo_ad; for Google Ads text ads use\n                create_update_google_ads_ad; for DOCUMENT ads use\n                create_update_document_ad. SPOTLIGHT / CTV / FOLLOWER are not yet\n                covered by the dedicated tools.\n\n                BEHAVIOR:\n                - If `id` is NOT provided → creates a new Message Ad.\n                - If `id` IS provided → updates the existing Message Ad with that ID.\n\n                PREREQUISITES (MUST DO BEFORE CALLING):\n                1. Call get_linkedin_senders to fetch a valid (senderId, senderName) pair.\n                   Message Ads share LinkedIn's sponsored-messaging sender allow-list\n                   with Conversation Ads — both pull from the same endpoint. Do NOT\n                   invent sender IDs.\n                2. If using a banner image: upload it via upload_image_creative and\n                   pass the returned imageLibraryId as `bannerCreativeLibraryId`. The\n                   banner is OPTIONAL — omit the field to skip it. LinkedIn requires\n                   an IMAGE creative ≤ 2 MB; oversize banners are rejected upstream.\n\n                CHARACTER LIMITS (enforced by the platform; mirror the UI validator at\n                metadata-ui-v3/.../li/message/edit/validation.ts):\n                - name           ≤ 50   chars  (ad name in the library)\n                - headlineText   ≤ 60   chars  (the message subject line)\n                - messageText    ≤ 2000 chars  (the message body)\n                - actionText     ≤ 20   chars  (the CTA button label)\n\n                PERSONALIZATION MACROS IN messageText:\n                The body supports LinkedIn personalization macros. Pass them verbatim\n                inside `%...%` — the platform substitutes at send time. The validator\n                accepts ONLY these five macros (any other %...% token is rejected):\n                  %FIRSTNAME%\n                  %LASTNAME%\n                  %COMPANYNAME%\n                  %JOBTITLE%\n                  %INDUSTRY%\n                Do not use `{firstName}` or any other syntax — only `%MACRO%`.\n\n                USAGE INSTRUCTIONS FOR LLM:\n                Use this tool when users want to:\n                - Create a new LinkedIn Sponsored Message ad\n                - Update an existing Message Ad (subject, body, CTA, sender, or banner)\n\n                WHEN TO USE:\n                - User asks \"create a LinkedIn Message Ad\" / \"InMail ad\" /\n                  \"Sponsored Messaging ad\"\n                - User wants a one-shot LinkedIn DM with subject + body + CTA\n                - User wants to update an existing Message Ad\n\n                WHEN NOT TO USE:\n                - User wants a branching conversation flow → use create_update_convo_ad\n                - User wants a feed image / GIF ad → use create_update_image_ad\n                - User wants a feed video ad → use create_update_video_ad\n                - User wants a Google Ads text ad → use create_update_google_ads_ad\n\n                INPUT PARAMETERS:\n                - id (optional): Existing Message Ad ID. Provide to UPDATE; omit to\n                  CREATE.\n                - name (required, ≤ 50): Ad name in the library.\n                - senderId (required): LinkedIn sender ID from get_linkedin_senders.\n                - senderName (required): Display name matching senderId.\n                - headlineText (required, ≤ 60): Subject line shown in the inbox.\n                - messageText (required, ≤ 2000): Message body. Supports the five\n                  %MACRO% tokens listed above.\n                - actionText (required, ≤ 20): CTA button label.\n                - bannerCreativeLibraryId (optional): imageLibraryId of the banner\n                  image. Upload first via upload_image_creative. Omit to skip the\n                  banner.\n                - completionStatus (optional, default DRAFT): \"DRAFT\" or \"COMPLETED\".\n                  Use COMPLETED only when every required field is final.\n\n                EXAMPLE USAGE (Create):\n                create_update_linkedin_message_ad(\n                    name=\"Q1_Demo_Outreach_LI_Message\",\n                    senderId=\"fQdRPtecbv\",\n                    senderName=\"Josh Desmarais\",\n                    headlineText=\"Quick question about your marketing stack\",\n                    messageText=\"Hi %FIRSTNAME%, as a %JOBTITLE% at %COMPANYNAME% you know how hard pipeline attribution is. Want a 15-min walkthrough?\",\n                    actionText=\"Book a demo\",\n                    bannerCreativeLibraryId=482931,\n                )\n\n                EXAMPLE USAGE (Update):\n                create_update_linkedin_message_ad(\n                    id=29144,\n                    name=\"Q1_Demo_Outreach_LI_Message_v2\",\n                    senderId=\"fQdRPtecbv\",\n                    senderName=\"Josh Desmarais\",\n                    headlineText=\"Following up — quick demo?\",\n                    messageText=\"Hi %FIRSTNAME%, circling back…\",\n                    actionText=\"Book a demo\",\n                )\n\n                SUCCESS RESPONSE (mirrors create_update_convo_ad):\n                {\n                    \"success\": true,\n                    \"id\": 29144,\n                    \"ad_url\": \"https://platform.metadata.io/hub/library/ads?adId=29144\",\n                    \"name\": \"Q1_Demo_Outreach_LI_Message\",\n                    \"channelType\": \"LINKEDIN\",\n                    \"adType\": \"MESSAGE\",\n                    \"completionStatus\": \"DRAFT\",\n                    \"request\": { \"message\": { ... } }\n                }\n\n                COMMON MISTAKES:\n                ❌ Inventing a senderId — IDs MUST come from get_linkedin_senders.\n                ❌ Passing a banner URL instead of an imageLibraryId — upload first.\n                ❌ Using `{firstName}`-style macros — LinkedIn uses `%FIRSTNAME%`.\n                ❌ Using an unsupported macro (anything outside the five listed) —\n                   the platform/UI validator rejects it.\n                ❌ Using this tool for a multi-step flow — use create_update_convo_ad.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "id": {
      "type": "integer",
      "description": "Existing Message Ad ID — include to UPDATE; omit to CREATE."
     },
     "name": {
      "type": "string",
      "description": "Ad name in the platform library (max 50 characters).",
      "maxLength": 50
     },
     "senderId": {
      "type": "string",
      "description": "LinkedIn sender ID. MUST come from get_linkedin_senders. Do not invent."
     },
     "senderName": {
      "type": "string",
      "description": "Display name matching senderId, also from get_linkedin_senders."
     },
     "headlineText": {
      "type": "string",
      "description": "Message subject line shown in the recipient's inbox (max 60 characters).",
      "maxLength": 60
     },
     "messageText": {
      "type": "string",
      "description": "Message body (max 2000 characters). Supports LinkedIn personalization macros — ONLY %FIRSTNAME%, %LASTNAME%, %COMPANYNAME%, %JOBTITLE%, %INDUSTRY%. Any other %...% token is rejected by the platform validator.",
      "maxLength": 2000
     },
     "actionText": {
      "type": "string",
      "description": "Call-to-action button label (max 20 characters).",
      "maxLength": 20
     },
     "bannerCreativeLibraryId": {
      "type": "integer",
      "description": "Optional: imageLibraryId of the banner image shown above the message. Upload the image first via upload_image_creative and pass the returned ID. Omit to skip the banner."
     },
     "completionStatus": {
      "type": "string",
      "enum": [
       "DRAFT",
       "COMPLETED"
      ],
      "description": "Defaults to DRAFT. Use COMPLETED only when every required field is final."
     }
    },
    "required": [
     "name",
     "senderId",
     "senderName",
     "headlineText",
     "messageText",
     "actionText"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_update_microsoft_ads_ad",
   "title": "Create or Update Microsoft Ads Ad",
   "category": "creative",
   "risk": "destructive",
   "summary": "Create a new or update an existing Microsoft Ads (Bing) text ad on the Metadata platform.",
   "description": "Create a new or update an existing Microsoft Ads (Bing) text ad on the Metadata platform.\n                                IMPORTANT: This tool is ONLY for Microsoft Ads (MICROSOFT_ADS channel) text ads.\n                                Ad library entries are PER-CHANNEL: a Google RSA can NOT be attached to a Microsoft Ads channel, and vice versa. Building the \"same\" ad for both channels means two calls, one per tool.\n                                For Google Ads text ads use create_update_google_ads_ad.\n                                Do NOT use this tool for Facebook, LinkedIn, Instagram or Reddit ads: use create_update_image_ad (IMAGE/GIF) or create_update_video_ad (VIDEO).\n\n                                ALSO KNOWN AS: Microsoft Ads text ad, Bing text ad, Bing search ad, MS Ads ad, Microsoft responsive search ad\n\n                                BEHAVIOR:\n                                - If \"id\" is NOT provided -> creates a new Microsoft Ads text ad.\n                                - If \"id\" IS provided -> updates the existing Microsoft Ads text ad with that ID.\n\n                                CHARACTER LIMITS (HARD LIMITS — THE API REJECTS ANYTHING OVER)\n                                Microsoft Ads and Google Ads share one text-ad shape on the platform, so the limits are identical:\n                                - Headlines: MAXIMUM 30 characters each (minimum 3, maximum 15 headlines)\n                                - Descriptions: MAXIMUM 90 characters each (minimum 2, maximum 4 descriptions)\n                                - Ad name: MAXIMUM 50 characters\n\n                                DESCRIPTIONS — 90 CHARACTER HARD LIMIT — READ THIS CAREFULLY\n                                THIS IS THE #1 MOST COMMON FAILURE MODE.\n                                - 90 characters is an ABSOLUTE ceiling. 91 = REJECTION. 89 = OK.\n                                - EVERY character counts: letters, digits, spaces, punctuation, apostrophes, hyphens, line breaks, emoji.\n                                - The MCP schema enforces maxLength=90, and the server truncates as a backstop, so over-length copy is either rejected outright or SILENTLY CUT MID-SENTENCE.\n                                - Target 70-85 characters per description. Do not write to the cliff.\n                                - LLMs systematically underestimate length by 5-15 characters. ALWAYS COUNT, never estimate.\n\n                                EVERY HEADLINE AND EVERY DESCRIPTION MUST BE UNIQUE\n                                The platform rejects the WHOLE ad when two headlines carry identical text (\"Enter a headline that is different from: '<text>'\"), and applies the same rule to descriptions. Writing 12 real headlines plus 3 near-copies of them is the second most common rejection. Make every line genuinely different.\n\n                                NO EXCLAMATION MARK IN A HEADLINE\n                                \"!\" is rejected in headline text (\"! is not supported\"). Descriptions accept it.\n\n                                DYNAMIC KEYWORD INSERTION\n                                A headline may use the \"{KeyWord:fallback text}\" form; the platform measures the 30-character limit against the fallback text inside the braces.\n\n                                MANDATORY PRE-CALL VALIDATION — DO THIS BEFORE EVERY CALL:\n                                1. Count each headline: every one MUST be 30 characters or fewer, and free of \"!\".\n                                2. Count each description: every one MUST be 90 characters or fewer.\n                                3. Check for repeats across headlines, and across descriptions. Rewrite any duplicate.\n                                4. Rewrite anything over the limit BEFORE calling. Do not submit known-bad copy and hope.\n\n                                USAGE INSTRUCTIONS FOR LLM:\n                                Use this tool when users want to:\n                                - Create a new Microsoft Ads / Bing text ad\n                                - Update an existing Microsoft Ads text ad (headlines, descriptions, or name)\n                                - Give a Microsoft Ads campaign the ad it needs to become launch-ready\n\n                                WHEN TO USE:\n                                - User asks \"create a Microsoft Ads ad\" / \"Bing ad\" / \"MS Ads search ad\"\n                                - A campaign has Microsoft Ads enabled and the channel has no ad yet\n                                - User says \"update Microsoft ad 23149 with new headlines\"\n\n                                WHEN NOT TO USE:\n                                - User wants the Google Ads version of the ad -> use create_update_google_ads_ad\n                                - User wants a Facebook, LinkedIn, Instagram or Reddit IMAGE ad -> use create_update_image_ad\n                                - User wants a Facebook, LinkedIn, Instagram or Reddit VIDEO ad -> use create_update_video_ad\n                                - User wants a sitelink, callout or other extension -> use the create_microsoft_*_extension tools\n\n                                INPUT PARAMETERS:\n                                - id: The ad ID (optional). If provided, that ad is updated. If omitted, a new ad is created. This is the adLibraryId inside the creatives of the microsoft channel in a campaign response.\n                                - name: The name/label for the ad (required, max 50 characters)\n                                - headlines: Array of headline text strings (required, minimum 3, maximum 15, each STRICT MAX 30 CHARACTERS, all distinct, no \"!\")\n                                - descriptions: Array of description text strings (required, minimum 2, maximum 4, each STRICT MAX 90 CHARACTERS, all distinct)\n\n                                EXAMPLE USAGE (Create):\n                                create_update_microsoft_ads_ad(\n                                    name=\"Bing Hosting Q1 2026\",\n                                    headlines=[\"EU Data Centers\", \"Local Expert Support\", \"Hosting Built For Founders\"],\n                                    descriptions=[\"Keep your business data in Europe.\", \"2000+ local experts, one platform.\"]\n                                )\n\n                                EXAMPLE USAGE (Update):\n                                create_update_microsoft_ads_ad(\n                                    id=23149,\n                                    name=\"Bing Hosting Q1 2026 v2\",\n                                    headlines=[\"Updated Headline 1\", \"Updated Headline 2\", \"Updated Headline 3\"],\n                                    descriptions=[\"Updated description one.\", \"Updated description two.\"]\n                                )",
   "inputSchema": {
    "type": "object",
    "properties": {
     "id": {
      "type": "integer",
      "description": "The existing ad ID to update. If omitted, a new ad will be created."
     },
     "name": {
      "type": "string",
      "description": "The name/label for the ad in Metadata platform (max 50 characters)",
      "maxLength": 50
     },
     "headlines": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 30,
       "description": "A single headline (max 30 characters, no exclamation mark)"
      },
      "description": "List of headline texts for the Microsoft Ads ad (minimum 3, maximum 15 headlines, each max 30 characters). Every headline must be DISTINCT — the platform rejects the ad if two share the same text — and must not contain '!'.",
      "minItems": 3,
      "maxItems": 15
     },
     "descriptions": {
      "type": "array",
      "items": {
       "type": "string",
       "maxLength": 90,
       "description": "A single description — ABSOLUTE HARD LIMIT: 90 characters. 91 characters = rejected by schema validation, 90 = accepted. Letters, digits, spaces, punctuation, emoji, line breaks ALL count. Target 70-85 chars for safety margin. Server will silently truncate at 90 if the schema is bypassed — your copy will be cut mid-sentence. ALWAYS run len(text) before submitting; do not estimate."
      },
      "description": "List of description texts for the Microsoft Ads ad (2 to 4 descriptions — 2 is the platform minimum, a single-description ad is rejected). Each description MUST be 90 characters or fewer and MUST be distinct from the others. Count every character (incl. spaces/punctuation/emoji); target 70-85 chars to leave margin for sanitization.",
      "minItems": 2,
      "maxItems": 4
     }
    },
    "required": [
     "name",
     "headlines",
     "descriptions"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_update_video_ad",
   "title": "Create or Update Video Ad",
   "category": "creative",
   "risk": "destructive",
   "summary": "Create OR update a VIDEO ad on Facebook, Instagram, LinkedIn, and/or Reddit.",
   "description": "Create OR update a VIDEO ad on Facebook, Instagram, LinkedIn, and/or Reddit.\n                                TO UPDATE: include `id`; to CREATE: omit it.\n\n                                CREATIVE WORKFLOW:\n                                  1. Upload the video via `upload_video_creative` → response gives `id` (the videoLibraryId).\n                                  2. Pass that integer as `creativeID` here.\n                                  - For Reddit, ALSO upload a still thumbnail via `upload_image_creative` and pass\n                                    its imageLibraryId as `redditThumbnailLibraryId` (required; the video cannot\n                                    be its own thumbnail).\n\n                                ALSO KNOWN AS: create video ad, update video ad, edit video ad,\n                                modify video ad, video creative, post video, video post, motion ad.\n\n                                KEYWORDS: video, create, update, edit, modify, ad, ads, video ad, video ads, mp4, motion,\n                                video creative, video campaign, video upload, video post, video promo,\n                                LinkedIn video, Facebook video, Instagram video, Reddit video, IG reel.\n\n                                CLIENT-SIDE VALIDATION (refuses what the platform UI would also reject):\n                                  - adName: required, max 50 chars, pattern `[a-zA-Z0-9 _.?,:'/+\\-=&;%$#\\[\\]|()]`.\n                                  - creativeID: required integer (videoLibraryId from `upload_video_creative`).\n                                  - Landing-page URL is per channel (`facebookDisplayURL`,\n                                    `instagramDisplayURL`, `linkedinDisplayURL`, `redditDisplayURL`).\n                                    Required for FB/IG/LinkedIn, optional for Reddit. Per-channel\n                                    caps: LinkedIn max 2000, Reddit max 100, FB/IG no length rule.\n                                    For VIDEO the URL MUST be ABSOLUTE (http:// or https:// is\n                                    required); inputs like `metadata.io` or `www.foo.com` are\n                                    rejected the same way the UI rejects them. If only one channel's\n                                    URL is set the server fans it across the other targeted channels.\n                                  - LinkedIn: linkedinText (max 3000, OPTIONAL for VIDEO unlike IMAGE),\n                                    linkedinHeadline (required, max 200), linkedinCTA (required, valid enum).\n                                    linkedinDescription is NOT accepted on VIDEO ads (the platform DTO\n                                    does not declare it).\n                                  - Facebook: metaCTA (required), facebookText (required, no length rule on\n                                    Facebook), facebookHeadline (required, max 250), facebookDescription\n                                    (optional, max 300).\n                                  - Instagram: metaCTA (required), instagramText (required, max 125),\n                                    instagramHeadline (required, max 40), instagramDescription (optional, max 30).\n                                  - FB+IG ad with one body text: set just one of facebookText / instagramText\n                                    and the server copies it across (capped at the receiving channel's limit).\n                                    Set both explicitly only when you want per-channel copy.\n                                  - Reddit: redditHeadline (required, max 300), redditCTA (required, valid enum),\n                                    redditThumbnailLibraryId (REQUIRED — separate IMAGE library id).\n\n                                COMMON CTA VALUES: same as `create_update_image_ad` (see that tool for the lists).\n\n                                See `create_update_image_ad` for image/GIF ads (the URL rule is looser there).",
   "inputSchema": {
    "type": "object",
    "properties": {
     "ads_data": {
      "type": "array",
      "minItems": 1,
      "description": "Array of VIDEO ads to create or update. Each item targets one or more of FB/IG/LinkedIn/Reddit; the platform creates one ad row per (ad, channel).",
      "items": {
       "type": "object",
       "properties": {
        "id": {
         "type": "integer",
         "description": "Existing ad ID (include for UPDATE, omit for CREATE)."
        },
        "adName": {
         "type": "string",
         "maxLength": 50,
         "description": "Unique ad name. Max 50 chars; allowed chars: letters, digits, space, and _ . ? , : ' / + - = & ; % $ # [ ] | ( )."
        },
        "channels": {
         "type": "array",
         "minItems": 1,
         "items": {
          "type": "string",
          "enum": [
           "Facebook",
           "Instagram",
           "LinkedIn",
           "Reddit"
          ]
         },
         "description": "Target channels. Each channel produces one ad row (an ad targeting two channels yields two ads)."
        },
        "facebookDisplayURL": {
         "type": "string",
         "description": "Landing page URL for Facebook ads. Required for Facebook. The platform UI declares no length rule for Facebook. MUST start with http:// or https:// for VIDEO ads."
        },
        "instagramDisplayURL": {
         "type": "string",
         "description": "Landing page URL for Instagram ads. Required for Instagram. The platform UI declares no length rule for Instagram. MUST start with http:// or https:// for VIDEO ads."
        },
        "linkedinDisplayURL": {
         "type": "string",
         "maxLength": 2000,
         "description": "Landing page URL for LinkedIn ads. Required for LinkedIn. Max 2000 chars. MUST start with http:// or https:// for VIDEO ads."
        },
        "redditDisplayURL": {
         "type": "string",
         "maxLength": 100,
         "description": "Landing page URL for Reddit ads. Optional (defaults to empty string). Max 100 chars. MUST start with http:// or https:// for VIDEO ads."
        },
        "creativeID": {
         "type": "integer",
         "description": "Integer libraryId returned by `upload_video_creative`. NOT a URL. Must point at a library row whose contentType matches the ad type."
        },
        "linkedinText": {
         "type": "string",
         "maxLength": 3000,
         "description": "Main ad copy for LinkedIn. Optional for VIDEO ads (max 3000)."
        },
        "linkedinHeadline": {
         "type": "string",
         "maxLength": 200,
         "description": "LinkedIn headline. Required for LinkedIn. Max 200."
        },
        "linkedinDescription": {
         "type": "string",
         "maxLength": 300,
         "description": "Optional link description for LinkedIn IMAGE ads. Max 300. Not supported on LinkedIn VIDEO ads (the platform DTO `LibraryAdLinkedinVideoRequest` does not declare it)."
        },
        "linkedinCTA": {
         "type": "string",
         "enum": [
          "APPLY_NOW",
          "DOWNLOAD",
          "GET_QUOTE",
          "LEARN_MORE",
          "SIGN_UP",
          "SUBSCRIBE",
          "REGISTER",
          "REQUEST_DEMO",
          "JOIN",
          "ATTEND"
         ],
         "description": "LinkedIn CTA enum. Required for LinkedIn."
        },
        "facebookText": {
         "type": "string",
         "description": "Primary body text for Facebook ads. The platform UI declares no length rule for Facebook, so any reasonable ad-copy length is accepted."
        },
        "facebookHeadline": {
         "type": "string",
         "maxLength": 250,
         "description": "Headline for Facebook ads. Max 250."
        },
        "facebookDescription": {
         "type": "string",
         "maxLength": 300,
         "description": "Optional news-feed link description for Facebook ads. Max 300."
        },
        "instagramText": {
         "type": "string",
         "maxLength": 125,
         "description": "Primary body text for Instagram ads. Max 125."
        },
        "instagramHeadline": {
         "type": "string",
         "maxLength": 40,
         "description": "Headline for Instagram ads. Max 40."
        },
        "instagramDescription": {
         "type": "string",
         "maxLength": 30,
         "description": "Optional news-feed link description for Instagram ads. Max 30."
        },
        "metaCTA": {
         "type": "string",
         "enum": [
          "APPLY_NOW",
          "DOWNLOAD",
          "GET_QUOTE",
          "LEARN_MORE",
          "SIGN_UP",
          "SUBSCRIBE"
         ],
         "description": "Meta (Facebook/Instagram) CTA enum. Required for FB/IG. Use the SCREAMING_SNAKE form. Meta-native CTAs that the metadata.io platform does NOT support (BOOK_NOW, GET_STARTED, CONTACT_US, BOOK_A_DEMO, etc.) are absent from the enum and will be rejected."
        },
        "redditHeadline": {
         "type": "string",
         "maxLength": 300,
         "description": "Headline for Reddit. Required for Reddit. Max 300."
        },
        "redditCTA": {
         "type": "string",
         "enum": [
          "Apply Now",
          "Contact Us",
          "Download",
          "Get a Quote",
          "Learn More",
          "Play Now",
          "Sign Up",
          "View More",
          "Watch Now",
          "Book Now",
          "Listen Now",
          "Read More",
          "Subscribe"
         ],
         "description": "Reddit CTA enum. Required for Reddit."
        },
        "redditThumbnailLibraryId": {
         "type": "integer",
         "description": "REQUIRED for VIDEO Reddit ads — must be a separate IMAGE library id from `upload_image_creative` (the video can't be its own thumbnail)."
        },
        "completionStatus": {
         "type": "string",
         "enum": [
          "DRAFT",
          "COMPLETED"
         ],
         "description": "Completion status. Defaults to DRAFT for all channels: a DRAFT ad persists locally without round-tripping the asset to the upstream channel API. Pass COMPLETED only when the ad is final and should be pushed to the channel."
        }
       },
       "required": [
        "adName",
        "channels",
        "creativeID"
       ],
       "additionalProperties": false
      }
     }
    },
    "required": [
     "ads_data"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "edit_brand_creative",
   "title": "Edit Brand Creative",
   "category": "creative",
   "risk": "destructive",
   "summary": "Edit existing creative image using AI-powered editing while preserving brand consistency.",
   "description": "Edit existing creative image using AI-powered editing while preserving brand consistency.\n\nUSE FOR: Modify headline/CTA/hero graphic, add/remove elements, adjust styling, create variations, iterate on designs.\n\nEDITABLE: Headlines, text, CTA buttons, hero graphics, badges/icons, colors, stats, any combination in single request.\n\nEXAMPLES:\n- Single: \"Change headline to 'BOOST YOUR ROI'\"\n- Multiple: \"Change headline to 'DRIVE PIPELINE', update CTA to 'LEARN MORE', add +50% badge\"\n\nPRESERVES (unless asked to change): Logo position/size, brand colors, typography, composition, dimensions.\n\nCHAINED EDITS: Use output URL as input for next edit (Edit → URL_1 → Edit → URL_2 → final).\n\nOUTPUT: Returns the hosted image URL.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "image_source": {
      "type": "string",
      "description": "URL of the creative image to edit. Must be a valid accessible image URL (PNG, JPG, WebP). Can be a URL returned by a previous generation or edit."
     },
     "edit_instruction": {
      "type": "string",
      "description": "Natural language description of the desired edit(s). Can include multiple changes in one request. Examples: 'Replace the robot with a dashboard', 'Change headline to BOOST ROI and make CTA say GET STARTED', 'Add a +50% ROI badge in the corner'"
     },
     "domain": {
      "type": "string",
      "description": "Company domain for brand context. Used to fetch brand DNA for consistency. Examples: 'metadata.io', 'bloomreach.com'"
     }
    },
    "required": [
     "image_source",
     "edit_instruction",
     "domain"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "fetch_creative_details",
   "title": "Fetch Creative Details",
   "category": "creative",
   "risk": "read",
   "summary": "Get detailed information for specific creative assets by their IDs.",
   "description": "Get detailed information for specific creative assets by their IDs.\n                   Looks up each creative ID individually.\n\n                   WHEN TO USE:\n                   - Get metadata for specific creative assets\n                   - Lookup creative names and content types\n                   - Verify creative asset properties before using in ads\n                   - Audit creative asset details and status\n                   - Validate library IDs before ad creation\n\n                   RETURNED INFORMATION:\n                   - Creative asset name and ID\n                   - Content type (IMAGE, VIDEO, GIF, DOCUMENT)\n                   - File properties (size, dimensions, format)\n                   - Upload and modification timestamps\n                   - Visibility status and permissions\n                   - Preview URLs and download links\n                   - Associated metadata and tags\n\n                   FEATURES:\n                   - Batch lookup of multiple creative IDs\n                   - Individual error handling for invalid/missing IDs\n                   - Returns None for failed lookups while continuing others\n                   - Detailed logging of API requests and responses\n                   - Comprehensive metadata for each valid asset",
   "inputSchema": {
    "type": "object",
    "properties": {
     "creative_id": {
      "type": "integer",
      "description": "creative id to be query"
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "fix_flexible_creative",
   "title": "Fix Flexible Creative Image",
   "category": "creative",
   "risk": "destructive",
   "summary": "Apply a free-text AI edit to a flexible creative's BACKGROUND image and return the edited image URL.",
   "description": "Apply a free-text AI edit to a flexible creative's BACKGROUND image and return the edited image URL. Instruction-based image-to-image edit (like edit_brand_creative), scoped to the background so the crisp editable text/logo layers are never touched.\n\nUSE FOR: \"remove the glare\", \"make the sky warmer\", \"clean up the left side\", \"swap the desk for a laptop\".\n\nOUTPUT: the edited background image URL (the editor swaps the background layer).",
   "inputSchema": {
    "type": "object",
    "properties": {
     "image_source": {
      "type": "string",
      "description": "URL of the background image to edit (PNG/JPG/WebP). Typically the flexible creative's current background."
     },
     "edit_instruction": {
      "type": "string",
      "description": "Natural-language description of the fix(es). Can combine several changes in one request."
     },
     "domain": {
      "type": "string",
      "description": "Company domain for brand context (e.g. 'metadata.io')."
     }
    },
    "required": [
     "image_source",
     "edit_instruction",
     "domain"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "generate_brand_creative",
   "title": "Generate Brand Creative",
   "category": "creative",
   "risk": "write",
   "summary": "Generate branded advertising images and designs using AI and company Brand DNA.",
   "description": "Generate branded advertising images and designs using AI and company Brand DNA.\n\n                        FLAT vs EDITABLE: this is the FLAT generator — it bakes the headline + CTA into the pixels and returns one finished PNG. For a creative the user can rearrange / retype / restyle before finalizing (the DEFAULT for a standalone \"make me a creative\" request), use generate_flexible_brand_creative instead. Use THIS flat tool when building an ad or feeding a campaign (the image flows straight into upload_image_creative -> create_update_image_ad, with no human edit-and-save step), or when a finished flat image / a batch of quick variations is explicitly wanted.\n\n                        ALSO KNOWN AS: design ad, create ad image, make creative, generate banner, create advertisement visual, design campaign image, ad design, advertising artwork\n                        KEYWORDS: design, creative, image, banner, ad, advertisement, visual, generate, create, artwork, graphic, brand, branded, logo, campaign asset\n\n                        USE FOR:\n                        - Creating new ad images/banners/creatives for campaigns\n                        - Generating LinkedIn/Facebook/Instagram/Display ad visuals\n                        - Making branded advertisements that match company style\n                        - Designing campaign creative assets\n\n                        WORKFLOW CONTEXT:\n                        - PREREQUISITE: Call get_brand_kit first to check if brand exists\n                        - NEXT STEPS: Use upload_image_creative to add to library, then create_update_image_ad\n                        - PART OF: Campaign creation workflow (brand → creative → upload → ad → campaign)\n\n                        COMMON QUERIES THAT NEED THIS:\n                        - \"create ads for my company\"\n                        - \"design advertising images\"\n                        - \"make branded banners\"\n                        - \"generate creative for LinkedIn campaign\"\n                        - \"I need ad visuals\"\n\n                        KEYWORDS: ad, advertisement, creative, image, banner, design, visual, brand, generate, create, linkedin, facebook, instagram, display, campaign asset\n                        PROCESS: Checks for existing brand kit → (Creates brand kit only if missing) → Selects top 3 aligned reference ads → Enhances prompt with brand patterns → Generates optimized image → Returns URL (must upload before use in ads).\n\n                        TEXT RULES:\n                        - Wrap text in quotes: \"Your Headline Here\"\n                        - Max 25 chars per phrase, 2-3 phrases max\n                        - Good: \"Boost ROI 30%\" (13 chars) | Bad: \"Supercharge Your Marketing Performance Today\" (44 chars)\n\n                        PLATFORMS: LinkedIn (16:9, B2B) | Facebook (1:1, eye-catching) | Instagram (4:5, mobile-first) | Display (16:9, high impact)\n                OUTPUT: Returns image URL. ALWAYS display/link the image for user to see.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "domain": {
      "type": "string",
      "description": "Company domain to extract brand DNA from. Examples: 'docebo.com', 'metadata.io', 'bloomreach.com'. Do not include protocol (http/https)."
     },
     "headline": {
      "type": "string",
      "maxLength": 40,
      "description": "Primary headline text to display in the creative. Keep to 25 characters or fewer for optimal rendering (hard cap 40 — the image model truncates or garbles longer text). Used to select aligned reference ads and guide visual hierarchy. Examples: 'Boost ROI by 30%', 'AI-Powered Marketing', 'Drive More Pipeline'"
     },
     "instructions": {
      "type": "string",
      "description": "Natural language instructions describing the desired creative. Will be enhanced with brand DNA patterns including proven layouts, CTAs, colors, and tones. Examples: 'Create a professional B2B ad with clean design', 'Generate announcement for new AI feature launch', 'Design case study visual with modern aesthetic'"
     },
     "platform": {
      "type": "string",
      "enum": [
       "linkedin",
       "facebook",
       "instagram",
       "display",
       "general"
      ],
      "description": "Target platform for the creative. Each platform has specific dimensions, text limits, and best practices that will be automatically applied. Default: 'linkedin'",
      "default": "linkedin"
     },
     "include_logo": {
      "type": "boolean",
      "description": "Whether to include a logo overlay on the generated creative. Set to false to get the raw creative without any logo. Default: true",
      "default": true
     },
     "include_cta": {
      "type": "boolean",
      "description": "Whether to render a call-to-action (button or styled text link) in the creative. Set to false for surfaces that must not contain a CTA, e.g. stills that seed video ads, where the channel's ad unit provides the real CTA. Default: true",
      "default": true
     },
     "logo_position": {
      "type": "string",
      "enum": [
       "top-left",
       "top-right",
       "bottom-left",
       "bottom-right"
      ],
      "description": "Force a specific logo position instead of using AI-recommended placement. If not provided, an AI model analyzes the creative to find the optimal position."
     },
     "logo_size_percent": {
      "type": "number",
      "description": "Logo width as a percentage of the image width (e.g., 15 means the logo occupies 15% of the image width). If not provided, the AI recommends an optimal size. Typical range: 10-30."
     },
     "logo_url": {
      "type": "string",
      "description": "Optional direct URL to a logo image (e.g. the URL of a logo the user just uploaded in chat). When set, THIS logo is overlaid for this creative instead of the brand kit's logo or the one from Brandfetch. Use it when the user says things like 'use this logo' or 'use the logo I uploaded'. The uploaded logo is automatically expanded in-memory into clean variants (transparent, high-res, and both a light and dark version when the mark is a single color) for this creative only — nothing is persisted — so the placement model can pick the best fit for the background. To change a brand's logo permanently for all future creatives, call update_brand_kit with logo_url instead of passing it here."
     },
     "use_website_background": {
      "type": "boolean",
      "description": "Use the brand website's background color as the creative's dominant background; brand primary colors then appear as accents (CTA button, shapes, highlights). Default: true. Set false when the user wants a brand-colored or model-chosen background instead. Silently ignored when the brand kit has no resolvable website background color.",
      "default": true
     }
    },
    "required": [
     "domain",
     "headline",
     "instructions"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "generate_brand_kit",
   "title": "Generate Brand Kit",
   "category": "creative",
   "risk": "write",
   "summary": "Analyze a company's brand from their website and extract brand identity.",
   "description": "Analyze a company's brand from their website and extract brand identity.\n                                Fetch brand assets (logo, colors, fonts) and create a brand DNA document for consistent advertising.\n\n                                ALSO KNOWN AS: analyze brand, get brand from website, extract brand identity, brand analysis, company branding, fetch brand, brand style guide\n\n                                USE THIS TOOL WHEN USER WANTS TO:\n                                - Analyze their brand from website\n                                - Extract brand colors, logo, fonts\n                                - Match their existing brand style\n                                - Get brand identity for ads\n                                - Create brand guidelines\n\n                                COMMON USER PHRASES THAT NEED THIS TOOL:\n                                - \"match my brand\"\n                                - \"my company website is [URL]\"\n                                - \"analyze my brand\"\n                                - \"get my brand colors\"\n                                - \"brand style\"\n                                - \"company branding\"\n                                - \"use my brand\"\n\n                                WORKFLOW:\n                                1. Call this tool with company domain\n                                2. Returns brand DNA (colors, fonts, logo, style)\n                                3. Use brand info in generate_brand_creative\n\n                                PARAMETERS:\n                                - domain: Company website URL (e.g., \"dapta.ai\", \"example.com\")\n                                - brand_name: Optional brand name override\n                                - force_regenerate: Set true to refresh existing brand kit\n\n                                OUTPUT:\n                                - Logo variations and usage guidelines\n                                - Brand colors (primary, secondary, accent)\n                                - Typography/fonts\n                                - Messaging framework\n                                - Channel-specific guidelines\n\n                                IMPORTANT: Automatically checks if brand kit exists. Returns existing kit if found.\n\n                                RETRY SCOPE: This tool handles ONE domain per call. When the user asks to retry or\n                                regenerate the kit for specific named domain(s), call it ONLY for those domain(s),\n                                with force_regenerate=true, starting with the domain the user named. NEVER widen the\n                                request: domains the user did not name stay untouched, even if their kits were\n                                generated together as a batch earlier in the conversation.\n\n                                KEYWORDS: brand, website, URL, domain, logo, colors, fonts, identity, style, company, analyze, extract, branding, guidelines",
   "inputSchema": {
    "type": "object",
    "properties": {
     "domain": {
      "type": "string",
      "description": "Company website URL or domain"
     },
     "brand_name": {
      "type": "string",
      "description": "Optional brand name override"
     },
     "sample_ad_urls": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Optional ad image URLs to analyze (max 5)"
     },
     "force_regenerate": {
      "type": "boolean",
      "description": "If True, regenerate brand kit even if one already exists. Default: False",
      "default": false
     }
    },
    "required": [
     "domain"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "generate_flexible_brand_creative",
   "title": "Generate Flexible Brand Creative",
   "category": "creative",
   "risk": "write",
   "summary": "Generate an EDITABLE, layered brand creative: a clean AI background scene (no baked text) plus AI-placed, editable text/CTA/logo layers.",
   "description": "Generate an EDITABLE, layered brand creative: a clean AI background scene (no baked text) plus AI-placed, editable text/CTA/logo layers. Returns a structured layout document (JSON), NOT a flat image — the UI renders it in an editor the user can rearrange before saving.\n\nUSE FOR:\n- The DEFAULT for a standalone creative the user will review, refine, or use — \"make me a creative\", \"design an ad\", \"give me options\", \"a creative I can tweak/edit\".\n- Any request where the user should be able to move/retype the headline, restyle the CTA, swap the logo, add layers, or regenerate the background before finalizing.\n\nDIFFERENT FROM generate_brand_creative: PREFER THIS for standalone creative requests. generate_brand_creative bakes headline + CTA into the pixels and returns one finished PNG; use that flat tool only when building an ad / feeding a campaign inline (the flat image flows straight into an ad, with no human edit-and-save step) or when a finished flat image is explicitly requested.\n\nCOPY DISCIPLINE: fewer layers = a cleaner layout the user can actually edit. Default to headline + cta (add subheadline only when it genuinely earns its place); pass body / disclaimer ONLY when the user explicitly asked for that copy.\n\nRETURNS: a layered document — canvas dimensions, background image URL, brand colors/logo, and an ordered list of layers (headline, subheadline, body, cta, disclaimer, logo) with normalized positions and styling.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "domain": {
      "type": "string",
      "description": "Company domain for brand DNA (e.g. 'metadata.io'). No protocol."
     },
     "instructions": {
      "type": "string",
      "description": "Natural-language direction for the background scene and overall creative (e.g. 'clean B2B scene for an AI analytics launch')."
     },
     "platform": {
      "type": "string",
      "enum": [
       "linkedin",
       "facebook",
       "instagram",
       "reddit",
       "display",
       "general"
      ],
      "description": "Target platform (sets the default aspect ratio). Default: 'linkedin'.",
      "default": "linkedin"
     },
     "aspect_ratio": {
      "type": "string",
      "enum": [
       "1:1",
       "16:9",
       "9:16",
       "4:5"
      ],
      "description": "Canvas shape, overriding the one 'platform' implies. Pass this whenever the user names a size or shape: '1:1' for square (e.g. 1080x1080), '16:9' for wide/landscape, '9:16' for a full-height story, '4:5' for portrait. Omit to keep the platform's default shape."
     },
     "headline": {
      "type": "string",
      "description": "Headline copy to place as an editable layer."
     },
     "subheadline": {
      "type": "string",
      "description": "Optional subheadline copy."
     },
     "body": {
      "type": "string",
      "description": "Optional supporting body copy. Pass ONLY when the user explicitly asked for body copy — extra layers crowd the layout."
     },
     "cta": {
      "type": "string",
      "description": "Optional call-to-action button text (e.g. 'Learn more')."
     },
     "disclaimer": {
      "type": "string",
      "description": "Optional fine-print/disclaimer copy. Pass ONLY when the user explicitly asked for a disclaimer."
     },
     "include_logo": {
      "type": "boolean",
      "description": "Add a brand logo layer (resolved from the brand kit). Default: true.",
      "default": true
     }
    },
    "required": [
     "domain",
     "instructions"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "get_ad_details",
   "title": "Get Ad Details",
   "category": "creative",
   "risk": "read",
   "summary": "Get complete details and configuration for one or more ads from the Metadata platform.",
   "description": "Get complete details and configuration for one or more ads from the Metadata platform.\n\n                    USAGE INSTRUCTIONS FOR LLM:\n                    Use this tool when users want to:\n                    - Get complete ad details by ad ID\n                    - Retrieve ad configuration including content, targeting, and creative assets\n                    - Check existing ad settings before making modifications\n                    - Analyze ad performance data and configuration\n                    - Get creative library information associated with ads\n                    - Extract ad content like text, headline, CTA, links for reference\n                    - the value returned libraryCreative labelName corresponds to the library/image name.\n\n                    WHEN TO USE:\n                    - User asks \"show me ad details for ID X\"\n                    - User wants to see ad configuration: \"what's in ad 182124?\"\n                    - User needs ad content: \"get the text/headline/CTA from ad Y\"\n                    - User wants to check ad settings before editing\n                    - User asks \"what creative is used in ad Z?\"\n                    - User needs to analyze existing ad setup\n\n                    INPUT PARAMETERS:\n                    - ids: Flexible format for specifying ad IDs:\n                        * Single integer: 182124\n                        * Comma-separated string: \"182124,182123,182125\"\n\n                    EXAMPLE CALLS:\n                    - get_ad_details(ids=182124) (single)\n                    - get_ad_details(ids=\"182124,182123,182125\") (multiple)\n\n                    DETAILED RESPONSE INCLUDES:\n                    AD CONFIGURATION:\n                    - Basic info: id, name, channelType (FACEBOOK/LINKEDIN), adType (IMAGE/VIDEO/etc.)\n                    - Status: completionStatus, aiState, renameAllowed\n\n                    AD CONTENT:\n                    - text: Main ad copy/body text\n                    - headline: Ad headline text\n                    - link: Landing page URL\n                    - description: Additional ad description\n                    - ctaType: Call-to-action button (e.g., \"Get_Demo\", \"Learn_More\")\n\n                    CREATIVE ASSET DETAILS:\n                    - libraryId: Creative asset ID reference\n                    - libraryCreative object with:\n                        - id, labelName (filename)\n                        - url: Full-size creative asset URL\n                        - thumbnail: Thumbnail URL\n                        - dimensions: width, height in pixels\n                        - sizeInKb: File size\n                        - content_type: image/png, image/jpeg, etc.\n                        - created_date: When creative was uploaded\n\n                    METADATA:\n                    - tags: Ad tags/labels\n                    - created/modified dates\n                    - AI processing state\n\n                    ERROR HANDLING:\n                    - If ad ID doesn't exist, API returns empty array\n                    - Always validate that IDs are integers before making request\n                    - Handle both single ID and multiple ID requests\n\n                    EXAMPLE USAGE:\n                    - get_ad_details(ids=182124) - Single ad by integer\n                    - get_ad_details(ids=\"182124,182123,182125\") - Multiple ads by comma-separated string",
   "inputSchema": {
    "type": "object",
    "properties": {
     "ids": {
      "oneOf": [
       {
        "type": "integer",
        "description": "Single ad ID to get details for"
       },
       {
        "type": "string",
        "description": "Comma-separated list of ad IDs (e.g., '182124,182123,182125')",
        "pattern": "^\\d+(,\\d+)*$"
       }
      ],
      "description": "Ad ID(s) to get details for. Can be: single integer (182124) or comma-separated string ('182124,182123')."
     }
    },
    "required": [
     "ids"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "get_brand_kit",
   "title": "Get Brand Kit",
   "category": "creative",
   "risk": "read",
   "summary": "Retrieve current brand kit configuration for a company.",
   "description": "Retrieve current brand kit configuration for a company.\n\n                                USE FOR: Check stored brand info, review colors/fonts/logos before updating, verify brand kit exists, view brand guidelines.\n\n                                RETURNS: domain, longDescription, logos[], colors[], fonts[], industries\n\n                                Accepts URLs with/without protocol, normalizes to domain (e.g., \"www.example.com\" → \"example_com.json\").\n                                Requires brand kit to exist (created via generate_brand_kit).",
   "inputSchema": {
    "type": "object",
    "properties": {
     "domain": {
      "type": "string",
      "description": "Company website URL or domain. Examples: 'metadata.io', 'www.bloomreach.com', 'https://example.com'"
     }
    },
    "required": [
     "domain"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "regenerate_flexible_background",
   "title": "Regenerate Flexible Creative Background",
   "category": "creative",
   "risk": "destructive",
   "summary": "Regenerate ONLY the background scene of a flexible creative, keeping the editable text/logo layers untouched.",
   "description": "Regenerate ONLY the background scene of a flexible creative, keeping the editable text/logo layers untouched. Returns the new background image URL so the editor swaps just the background layer.\n\nUSE FOR: \"regenerate the background\", \"try a different scene\", \"new background, same text\".\n\nAccepts an optional image_source (the current background) to guide a same-style regeneration; omit it for a fresh scene.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "domain": {
      "type": "string",
      "description": "Company domain for brand DNA (e.g. 'metadata.io'). No protocol."
     },
     "instructions": {
      "type": "string",
      "description": "Direction for the new background scene."
     },
     "platform": {
      "type": "string",
      "enum": [
       "linkedin",
       "facebook",
       "instagram",
       "reddit",
       "display",
       "general"
      ],
      "description": "Target platform (sets the default aspect ratio). Default: 'linkedin'.",
      "default": "linkedin"
     },
     "aspect_ratio": {
      "type": "string",
      "enum": [
       "1:1",
       "16:9",
       "9:16",
       "4:5"
      ],
      "description": "Pass the creative's own canvas.aspect_ratio so the new background keeps its shape. Omitting it on a non-default canvas reshapes the creative and the existing layers no longer fit."
     },
     "image_source": {
      "type": "string",
      "description": "Optional URL of the current background to guide a same-style regeneration (image-to-image). Omit for a fresh scene."
     },
     "background_hex": {
      "type": "string",
      "description": "Optional 6-digit hex to pin the dominant background color (e.g. '#0A2540')."
     }
    },
    "required": [
     "domain",
     "instructions"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "remove_ad_from_campaign",
   "title": "Remove Ad from Campaign",
   "category": "creative",
   "risk": "destructive",
   "summary": "Remove specific ads (ad groups) from one or more channels in an existing campaign.",
   "description": "Remove specific ads (ad groups) from one or more channels in an existing campaign.\n\n                Use this tool when you need to detach a named ad from a particular channel\n                (e.g. remove \"Pixel Ads in Minutes_v2_LI\" from LINKEDIN).\n                Channel and ad group IDs are resolved internally — you only need the\n                human-readable channel type and ad name.\n\n                Works on both campaign structures. On a Channel-First / Native (N×N×N) campaign the\n                ad is removed from its WizNativeAdContainer's native ads (resolved by ad name via the\n                ads library); any container then left with no asset at all is dropped.\n\n                KEYWORDS: remove, delete, ad, creative, ad group, channel, campaign\n\n                WHEN TO USE:\n                - User wants to remove a specific ad from a channel\n                - User wants to clean up ad groups from one or more channels\n                - Multiple ads across multiple channels can be removed in a single call\n\n                RESPONSE:\n                - Returns the refreshed campaign state plus a removal_results list with one\n                  entry per requested removal recording its success or error. ALWAYS check\n                  removal_results — a partial failure (e.g. one of two names not found) still\n                  returns normally, so report only what actually came back successful.\n\n                REQUIRED PARAMETERS:\n                - campaign_id: Campaign ID (numeric identifier)\n                - channel_ads: Object mapping channel type → list of ad names to remove\n\n                CHANNEL TYPES: FACEBOOK, INSTAGRAM, GOOGLE_ADS, LINKEDIN, MICROSOFT_ADS, REDDIT, TWITTER\n\n                EXAMPLES:\n\n                Remove one ad from LINKEDIN:\n                remove_ad_from_campaign({\n                    \"campaign_id\": 42546,\n                    \"channel_ads\": {\n                        \"LINKEDIN\": [\"Pixel Ads in Minutes_v2_LI\"]\n                    }\n                })\n\n                Remove ads from multiple channels at once:\n                remove_ad_from_campaign({\n                    \"campaign_id\": 42546,\n                    \"channel_ads\": {\n                        \"LINKEDIN\": [\"Ad A\", \"Ad B\"],\n                        \"FACEBOOK\": [\"Ad C\"]\n                    }\n                })\n\n                INTEGRATION WITH OTHER TOOLS:\n                - Use get_campaign_by_wizard_id to inspect current ads per channel before removing\n                - Use search_campaigns_by_names to find the campaign ID",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The unique identifier of the campaign",
      "minimum": 1
     },
     "channel_ads": {
      "type": "object",
      "description": "Map of channel type to list of ad names to remove. Example: {\"LINKEDIN\": [\"Ad A\"], \"FACEBOOK\": [\"Ad B\"]}",
      "additionalProperties": {
       "type": "array",
       "items": {
        "type": "string"
       },
       "minItems": 1
      }
     }
    },
    "required": [
     "campaign_id",
     "channel_ads"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "render_flexible_creative",
   "title": "Render Flexible Creative to PNG",
   "category": "creative",
   "risk": "write",
   "summary": "Flatten a layered flexible-creative document into a final PNG (rendered from the SAME layout the editor shows) and upload it to the creatives library.",
   "description": "Flatten a layered flexible-creative document into a final PNG (rendered from the SAME layout the editor shows) and upload it to the creatives library. Returns the rendered image URL, the library image id, and pixel dimensions.\n\nINTERNAL SAVE-PATH TOOL — invoked by the backend when the user clicks Save in the editor; not part of the chat generation flow. Renders the doc's background + text/CTA/logo/image layers so the flat image is pixel-identical to the editor preview.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "design": {
      "type": "object",
      "description": "The layered creative document to flatten (canvas, background, layers, brand) — the same JSON the editor edits."
     },
     "name": {
      "type": "string",
      "description": "Optional library name for the rendered image. Omit it and one is derived from the doc as `<brand-domain>-<channel>-<width>x<height>.png`, which is normally what you want. Must end in a file extension."
     },
     "upload_to_library": {
      "type": "boolean",
      "description": "Push the rendered PNG to the creatives library. Default: true.",
      "default": true
     },
     "archive_library_image_id": {
      "type": "string",
      "description": "Optional. The library image id this save supersedes (the creative's previous render). Archived on the platform ONLY after the new upload succeeds, so re-saving an editable creative retires the old PNG instead of piling up dead library images. Never affects an ad already built from it."
     }
    },
    "required": [
     "design"
    ]
   }
  },
  {
   "name": "search_ads_by_names",
   "title": "Search Ads by Name",
   "category": "creative",
   "risk": "read",
   "summary": "Search for ads by name in the Metadata platform library, with pagination.",
   "description": "Search for ads by name in the Metadata platform library, with pagination.\n\n                   **USE THIS TOOL WHEN THE USER ASKS ABOUT:**\n                   - Ad details, information, or status\n                   - Finding an ad by name\n                   - Getting ad IDs\n                   - Checking if an ad exists\n                   - Looking up ad creative assets (images, thumbnails)\n                   - Ad type or channel information\n                   - Any question containing words like: \"ad\", \"creative\", \"show me\", \"find\", \"search\", \"image\"\n\n                   SEARCH FEATURES:\n                   - Partial, case-insensitive matching on ad names\n                   - Returns ad ID, name, channel type, ad type, status, and image URLs\n                   - Three-state status filter via the `status` param (default\n                     `\"active\"`, preserves the prior active-only behaviour):\n                       * `\"active\"`   → active ads only\n                       * `\"archived\"` → archived (soft-deleted) ads only; use\n                                        this to find an ad id for `unarchive_ad`\n                       * `\"all\"`      → both active and archived in one response\n\n                   PAGINATION:\n                   - Supports `page` and `size` parameters (defaults: page=0, size=25).\n                   - Page numbering starts at 0.\n                   - Recommended size: 5-25 to keep responses LLM-friendly. Use up\n                     to 100 only when the caller needs a wider sweep — large pages\n                     can blow your context budget.\n                   - Returned `total_elements` and `total_pages` are taken from the\n                     platform's PageResponse so callers can drive a paginator.\n                   - When `ad_names` carries multiple entries, the platform endpoint\n                     accepts only ONE name per request — this tool fires one paged\n                     request per name and merges results. `page`/`size` apply\n                     PER-NAME, `total_elements` is the SUM across names, and\n                     `total_pages` is the MAX across names. Single-name searches\n                     get clean pagination semantics; multi-name is best-effort.\n\n                   PARAMETERS:\n                   - ad_names: Array of ad name strings to search for (required, ≥1)\n                   - page:     Zero-based page index (optional, default 0)\n                   - size:     Results per page (optional, default 25)\n                   - sort:     Spring sort clause (optional, default \"createdDate,desc\";\n                               also valid: \"id,desc\", \"name,asc\", etc.)\n                   - status:   \"active\" (default) / \"archived\" / \"all\". Pick the\n                               bucket you want; \"archived\" is the unarchive\n                               discovery path, \"all\" is the rare both-buckets case.\n\n                   RETURNS:\n                   {\n                       \"found_ads\": [\n                           {\n                               \"id\": 190084,\n                               \"name\": \"AI_Marketing_Audit_Q4_2025_Demo_LI_LI\",\n                               \"channelType\": \"LINKEDIN\",\n                               \"adType\": \"IMAGE\",\n                               \"completionStatus\": \"COMPLETED\",\n                               \"status\": true,\n                               \"thumbnail\": \"https://...\",\n                               \"imageUrl\": \"https://...\"\n                           }\n                       ],\n                       \"missing_names\": [\"NonExistent\"],\n                       \"total_found\": 1,\n                       \"page\": 0,\n                       \"size\": 25,\n                       \"sort\": \"createdDate,desc\",\n                       \"total_elements\": 1,\n                       \"total_pages\": 1\n                   }\n\n                   CHANNEL TYPES: LINKEDIN, FACEBOOK, INSTAGRAM, GOOGLE, REDDIT, MICROSOFT_ADS\n                   AD TYPES: IMAGE, VIDEO, CAROUSEL, TEXT\n                   STATUS: true (active) or false (inactive)",
   "inputSchema": {
    "type": "object",
    "properties": {
     "ad_names": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Array of ad names or partial names to search for (case-insensitive, partial matching)",
      "minItems": 1
     },
     "page": {
      "type": "integer",
      "minimum": 0,
      "description": "Zero-based page index (default: 0). Page numbering starts at 0."
     },
     "size": {
      "type": "integer",
      "minimum": 1,
      "description": "Results per page (default: 25, recommended max: 25). Larger windows blow LLM token budgets — only request more when the caller actually needs it."
     },
     "sort": {
      "type": "string",
      "description": "Spring sort clause, e.g. 'createdDate,desc' (default), 'id,desc', or 'name,asc'."
     },
     "status": {
      "type": "string",
      "enum": [
       "active",
       "archived",
       "all"
      ],
      "default": "active",
      "description": "Which status bucket to return. 'active' (default) is the prior active-only behaviour. 'archived' returns ONLY archived (soft-deleted) ads, which is what you need to find an ad's id for unarchive_ad. 'all' returns both active and archived in one response. Maps to the platform's status query param semantics (Set<Boolean>): active=true, archived=false, all=omit."
     }
    },
    "required": [
     "ad_names"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "search_library_creatives_by_name",
   "title": "Search Creative Library (Images, Videos, GIFs, Documents)",
   "category": "creative",
   "risk": "read",
   "summary": "Search or list creative assets in the library by name, across ALL content types — images, videos, GIFs and documents.",
   "description": "Search or list creative assets in the library by name, across ALL content\n                   types — images, videos, GIFs and documents. Returns detailed information about\n                   matching assets with pagination support.\n\n                   KEYWORDS (for tool discovery): search creative library, list video creatives,\n                   find videos, list uploaded videos, browse video library, search videos by name,\n                   list all creatives, find creative by id, look up creative id, list documents,\n                   list GIFs, library assets, video assets, creative assets, list creatives by type.\n                   This is the general-purpose library search/list tool for every content\n                   type — there is no separate \"list videos\" or \"get library items\" tool.\n\n                   WHEN TO USE:\n                   - Find specific creatives by name in the library\n                   - List all creatives in the library (when creativeName is omitted)\n                   - Search for creative assets with partial name matching\n                   - Filter by content type (IMAGE, VIDEO, GIF, DOCUMENT)\n                   - Locate creatives for use in ad creation\n                   - Verify creative asset availability before campaign setup\n                   - Get creative metadata and IDs for asset management\n                   - Browse through paginated results\n\n                   SEARCH BEHAVIOR:\n                   - Supports partial name matching when creativeName is provided\n                   - Case-insensitive search\n                   - Returns all creatives matching the search term\n                   - Automatically URL encodes the search name\n                   - When creativeName is omitted, returns all creatives in the library\n                   - Supports optional content type filtering (IMAGE, VIDEO, GIF, DOCUMENT)\n                   - Use lower number in the size parameter. Preference for 5 or 10 max.\n\n                   PAGINATION:\n                   - Supports pagination with `page` and `size` parameters.\n                   - Defaults: page=0, size=10 (server-side default in\n                     `MetadataAPIClient.search_library_creatives_by_name`).\n                   - Page numbering starts at 0.\n                   - The platform returns Spring `PageResponse` shape\n                     (`totalElements`, `totalPages`, `data`) so callers can\n                     drive a paginator off the response.\n                   - Recommended size: 5-10 to keep LLM responses manageable.\n                     Larger windows (up to ~25) are accepted; anything beyond\n                     blows token budgets fast.\n\n                   RETURNED DATA:\n                   - Creative asset ID and name\n                   - File properties (size, dimensions, format)\n                   - Upload and modification timestamps\n                   - Visibility status and permissions\n                   - Preview URLs and download links\n                   - Associated metadata and tags\n                   - Pagination metadata (total count, page info)\n\n                   USE CASES:\n                   - \"List all images in the library\"\n                   - \"Find all images with 'product' in the name\"\n                   - \"Search for logo images in the library\"\n                   - \"Get image ID for 'campaign-hero-banner.jpg'\"\n                   - \"Find all images uploaded for the Q4 campaign\"\n                   - \"Browse images page by page (page=0, size=5)\"\n                   - \"List only videos: contentTypes=VIDEO\"\n                   - \"Filter for images and videos: contentTypes=IMAGE,VIDEO\"\n                   - \"Get all documents: contentTypes=DOCUMENT\"\n                   - \"Find all asset types: contentTypes=IMAGE,VIDEO,GIF,DOCUMENT\"\n\n                   NOTE: This is the single tool for searching/listing ALL creative asset\n                   types. To restrict to a type, pass contentTypes (e.g. VIDEO for videos\n                   only); omit it to return every type. Use fetch_creative_details when you\n                   already have a specific creative id and want its full metadata.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "creativeName": {
      "type": "string",
      "description": "Name or partial name of the creative to search for. Supports partial matching and is case-insensitive. Omit to list all creatives."
     },
     "page": {
      "type": "integer",
      "minimum": 0,
      "description": "Zero-based page index (default: 0)."
     },
     "size": {
      "type": "integer",
      "minimum": 1,
      "description": "Results per page (default: 10, recommended max: 10). Larger windows blow LLM token budgets — only request more when the caller actually needs it."
     },
     "contentTypes": {
      "type": "string",
      "description": "Filter by content types (comma-separated). Supported values: IMAGE,VIDEO,GIF,DOCUMENT. Example: IMAGE,VIDEO or IMAGE%2CVIDEO%2CGIF%2CDOCUMENT"
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "unarchive_ad",
   "title": "Unarchive Ad",
   "category": "creative",
   "risk": "write",
   "summary": "Unarchive (restore) one or more previously archived ads.",
   "description": "Unarchive (restore) one or more previously archived ads.\n\n                    Brings an archived (soft-deleted) ad back to active status. The\n                    reverse of archive_ad. The ad's id, creative_json, and history are\n                    preserved across archive/unarchive — same row, status flip.\n\n                    USE THIS TOOL WHEN:\n                    - User asks to restore, unarchive, undelete, recover, reactivate,\n                      bring back, or undo the archiving of an ad.\n                    - User wants to reuse an ad they previously archived.\n\n                    HOW TO GET THE id(s) FOR ARCHIVED ADS:\n                    - Archived ads are NOT returned by `search_ads_by_names` by default\n                      (the default `status=\"active\"` filters them out).\n                    - Call `search_ads_by_names(ad_names=[...], status=\"archived\")` to\n                      get ONLY archived ads in the results, then pass the matching ids\n                      to this tool. Use `status=\"all\"` if you also want active matches\n                      in the same response (rare; usually you want just \"archived\").\n                    - get_ad_details(ids=...) works on archived ids too if you already\n                      have the id.\n\n                    ALSO KNOWN AS: unarchive ad, restore ad, undelete ad, recover ad,\n                    reactivate ad, bring back ad, undo archive.\n\n                    KEYWORDS: unarchive, restore, undelete, recover, reactivate,\n                    bring back, undo archive, revive, resurrect.\n\n                    PARAMETERS:\n                    - ids: Array of integer ad ids (at least one). Bulk-friendly.\n\n                    RETURNS:\n                    { \"success\": true, \"unarchived_ids\": [123, 456], \"count\": 2 }",
   "inputSchema": {
    "type": "object",
    "properties": {
     "ids": {
      "type": "array",
      "minItems": 1,
      "items": {
       "type": "integer"
      },
      "description": "Ad id(s) to unarchive. At least one integer required; pass multiple to restore in bulk."
     }
    },
    "required": [
     "ids"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "update_brand_kit",
   "title": "Update Brand Kit",
   "category": "creative",
   "risk": "destructive",
   "summary": "Update existing brand kit with brand DNA parameters.",
   "description": "Update existing brand kit with brand DNA parameters. Only company_url is required; all others are optional.\n\n                    UPDATABLE FIELDS (grouped by section):\n\n                    TOP-LEVEL:\n                    - long_description: Company marketing description\n                    - logo_url: Direct URL to logo image\n                    - search_keywords: List of SEO/search keywords\n                    - colors: Legacy flat color list [{hex, type, name?}]\n                    - fonts: Legacy flat font list [{name, type}]\n                    - website_background: Hex color of the brand website's background, used by generate_brand_creative as the default ad background\n\n                    COLOR PALETTES (each entry: {hex, name, usage}):\n                    - primary_colors, accent_colors, extended_palette, semantic_colors\n\n                    TYPOGRAPHY (string values):\n                    - primary_font, primary_font_usage, secondary_font, secondary_font_usage\n                    - headline_font, headline_weight, headline_size, headline_case, headline_linespace, headline_letterspace\n                    - eyebrow_font, eyebrow_weight, eyebrow_case, eyebrow_linespace, eyebrow_letterspace\n                    - subtitle_font, subtitle_weight, subtitle_linespace, subtitle_letterspace\n                    - body_font, body_weight, body_size, body_linespace, body_letterspace\n                    - cta_font, cta_weight, cta_size, cta_linespace, cta_letterspace\n\n                    LOGO USAGE:\n                    - logo_on_dark, logo_on_light, logo_clear_space: string rules\n                    - logo_misuse: list of misuse rules\n\n                    CTA SYSTEM:\n                    - button_shape, button_background, button_text_color, button_text_weight, button_text_case, button_padding\n                    - standard_ctas: list of CTA labels\n                    - cta_placement: placement guidance\n\n                    SIGNATURE ELEMENTS:\n                    - tagline, tagline_placement: strings\n                    - tagline_variants, visual_motifs, promotional_elements: lists\n\n                    MESSAGING:\n                    - tone: string\n                    - value_propositions, headline_formulas, proof_points: lists\n                    - product_names: dict (e.g. {platform: \"X\", vlm: \"Y\"})\n\n                    CONTENT HIERARCHY:\n                    - content_hierarchy_order: list, content_hierarchy_spacing: string, content_hierarchy_text_max_width: string\n\n                    PHOTOGRAPHY:\n                    - photography_style, photography_treatment, photography_avoid: strings\n                    - photography_subjects: list\n\n                    RULES:\n                    - dos: list of \"do\" rules\n                    - donts: list of \"don't\" rules\n\n                    LAYOUT & CHANNELS:\n                    - primary_layouts: list of layout dicts\n                    - channel_linkedin, channel_facebook, channel_display: channel config dicts\n\n                    EXAMPLE: {\"company_url\": \"ambient.ai\", \"tagline\": \"See what matters.\", \"headline_font\": \"Chakra Petch\", \"primary_colors\": [{\"hex\": \"#FFFFFF\", \"name\": \"White\", \"usage\": \"Primary background\"}]}",
   "inputSchema": {
    "type": "object",
    "properties": {
     "company_url": {
      "type": "string",
      "description": "Company website URL or domain (e.g. 'ambient.ai')."
     },
     "long_description": {
      "type": "string",
      "description": "Company description for marketing use."
     },
     "logo_url": {
      "type": "string",
      "description": "Direct URL to a single company logo image (back-compat; prefer logo_urls). Setting a permanent brand logo requires elevated Metadata permissions."
     },
     "logo_urls": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "One or more direct URLs to logo images to set as the brand's permanent logo(s). Each uploaded logo is automatically expanded into clean, persisted variants (transparent, high-res, and both a light and dark version when the mark is a single color) so generated creatives have multiple usable options. Setting a permanent brand logo requires elevated Metadata permissions."
     },
     "remove_old_logos": {
      "type": "boolean",
      "description": "When true (default), replace the brand's existing logos with the provided ones; when false, append to them."
     },
     "search_keywords": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "SEO/search keywords for the brand."
     },
     "website_background": {
      "type": "string",
      "description": "6-digit hex color (e.g. '#FFFFFF') of the brand website's background. generate_brand_creative uses it as the default dominant background for ads (use_website_background). Set it when the user wants ads to match their site's background or the auto-detected value is wrong."
     },
     "colors": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "hex": {
         "type": "string"
        },
        "type": {
         "type": "string"
        },
        "name": {
         "type": "string"
        }
       },
       "required": [
        "hex",
        "type"
       ]
      },
      "description": "Legacy flat color list. Each: {hex (with #), type, name?}."
     },
     "fonts": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "name": {
         "type": "string"
        },
        "type": {
         "type": "string"
        }
       },
       "required": [
        "name",
        "type"
       ]
      },
      "description": "Legacy flat font list. Each: {name, type}."
     },
     "primary_colors": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "hex": {
         "type": "string"
        },
        "name": {
         "type": "string"
        },
        "usage": {
         "type": "string"
        }
       },
       "required": [
        "hex",
        "name",
        "usage"
       ]
      },
      "description": "Primary color palette."
     },
     "accent_colors": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "hex": {
         "type": "string"
        },
        "name": {
         "type": "string"
        },
        "usage": {
         "type": "string"
        }
       },
       "required": [
        "hex",
        "name",
        "usage"
       ]
      },
      "description": "Accent color palette."
     },
     "extended_palette": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "hex": {
         "type": "string"
        },
        "name": {
         "type": "string"
        },
        "usage": {
         "type": "string"
        }
       },
       "required": [
        "hex",
        "name",
        "usage"
       ]
      },
      "description": "Extended grayscale palette."
     },
     "semantic_colors": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "hex": {
         "type": "string"
        },
        "name": {
         "type": "string"
        },
        "usage": {
         "type": "string"
        }
       },
       "required": [
        "hex",
        "name",
        "usage"
       ]
      },
      "description": "Semantic/status colors."
     },
     "primary_font": {
      "type": "string",
      "description": "Primary font family name."
     },
     "primary_font_usage": {
      "type": "string",
      "description": "Primary font usage description."
     },
     "secondary_font": {
      "type": "string",
      "description": "Secondary/display font family name."
     },
     "secondary_font_usage": {
      "type": "string",
      "description": "Secondary font usage description."
     },
     "headline_font": {
      "type": "string",
      "description": "Headline font family."
     },
     "headline_weight": {
      "type": "string",
      "description": "Headline font weight."
     },
     "headline_size": {
      "type": "string",
      "description": "Headline font size range."
     },
     "headline_case": {
      "type": "string",
      "description": "Headline text case rule."
     },
     "headline_linespace": {
      "type": "string",
      "description": "Headline line spacing."
     },
     "headline_letterspace": {
      "type": "string",
      "description": "Headline letter spacing."
     },
     "eyebrow_font": {
      "type": "string",
      "description": "Eyebrow font family."
     },
     "eyebrow_weight": {
      "type": "string",
      "description": "Eyebrow font weight."
     },
     "eyebrow_case": {
      "type": "string",
      "description": "Eyebrow text case rule."
     },
     "eyebrow_linespace": {
      "type": "string",
      "description": "Eyebrow line spacing."
     },
     "eyebrow_letterspace": {
      "type": "string",
      "description": "Eyebrow letter spacing."
     },
     "subtitle_font": {
      "type": "string",
      "description": "Subtitle font family."
     },
     "subtitle_weight": {
      "type": "string",
      "description": "Subtitle font weight."
     },
     "subtitle_linespace": {
      "type": "string",
      "description": "Subtitle line spacing."
     },
     "subtitle_letterspace": {
      "type": "string",
      "description": "Subtitle letter spacing."
     },
     "body_font": {
      "type": "string",
      "description": "Body text font family."
     },
     "body_weight": {
      "type": "string",
      "description": "Body text font weight."
     },
     "body_size": {
      "type": "string",
      "description": "Body text font size."
     },
     "body_linespace": {
      "type": "string",
      "description": "Body text line spacing."
     },
     "body_letterspace": {
      "type": "string",
      "description": "Body text letter spacing."
     },
     "cta_font": {
      "type": "string",
      "description": "CTA/button font family."
     },
     "cta_weight": {
      "type": "string",
      "description": "CTA/button font weight."
     },
     "cta_size": {
      "type": "string",
      "description": "CTA/button font size."
     },
     "cta_linespace": {
      "type": "string",
      "description": "CTA/button line spacing."
     },
     "cta_letterspace": {
      "type": "string",
      "description": "CTA/button letter spacing."
     },
     "logo_on_dark": {
      "type": "string",
      "description": "Logo color rule on dark backgrounds."
     },
     "logo_on_light": {
      "type": "string",
      "description": "Logo color rule on light backgrounds."
     },
     "logo_clear_space": {
      "type": "string",
      "description": "Logo clear space rule."
     },
     "logo_misuse": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "List of logo misuse rules."
     },
     "button_shape": {
      "type": "string",
      "description": "Button shape description."
     },
     "button_background": {
      "type": "string",
      "description": "Button background style."
     },
     "button_text_color": {
      "type": "string",
      "description": "Button text color rule."
     },
     "button_text_weight": {
      "type": "string",
      "description": "Button text weight."
     },
     "button_text_case": {
      "type": "string",
      "description": "Button text case."
     },
     "button_padding": {
      "type": "string",
      "description": "Button padding."
     },
     "standard_ctas": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Standard CTA labels."
     },
     "cta_placement": {
      "type": "string",
      "description": "CTA placement guidance."
     },
     "tagline": {
      "type": "string",
      "description": "Primary brand tagline."
     },
     "tagline_variants": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Alternative taglines."
     },
     "tagline_placement": {
      "type": "string",
      "description": "Tagline placement guidance."
     },
     "visual_motifs": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Brand visual motifs."
     },
     "promotional_elements": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Promotional proof points."
     },
     "tone": {
      "type": "string",
      "description": "Brand tone description."
     },
     "value_propositions": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Core value propositions."
     },
     "headline_formulas": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Headline writing patterns."
     },
     "proof_points": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Evidence/social proof items."
     },
     "product_names": {
      "type": "object",
      "description": "Product name mapping (e.g. {platform: 'X', vlm: 'Y'})."
     },
     "content_hierarchy_order": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Content element ordering."
     },
     "content_hierarchy_spacing": {
      "type": "string",
      "description": "Section spacing guidance."
     },
     "content_hierarchy_text_max_width": {
      "type": "string",
      "description": "Max text block width."
     },
     "photography_style": {
      "type": "string",
      "description": "Photography style description."
     },
     "photography_subjects": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Recommended photo subjects."
     },
     "photography_treatment": {
      "type": "string",
      "description": "Photo treatment rules."
     },
     "photography_avoid": {
      "type": "string",
      "description": "Photography anti-patterns."
     },
     "dos": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Brand 'do' rules."
     },
     "donts": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Brand 'don't' rules."
     },
     "primary_layouts": {
      "type": "array",
      "items": {
       "type": "object"
      },
      "description": "Layout system definitions."
     },
     "channel_linkedin": {
      "type": "object",
      "description": "LinkedIn channel config."
     },
     "channel_facebook": {
      "type": "object",
      "description": "Facebook channel config."
     },
     "channel_display": {
      "type": "object",
      "description": "Display ads channel config."
     }
    },
    "required": [
     "company_url"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "update_meta_placements",
   "title": "Update Meta Placements (Facebook / Instagram)",
   "category": "creative",
   "risk": "destructive",
   "summary": "Set the channel-level Meta placement default of an existing DRAFT wizard campaign's Facebook or Instagram channel: automatic placements (Meta decides where ads serve) or a manua…",
   "description": "Set the channel-level Meta placement default of an existing DRAFT wizard campaign's Facebook or Instagram channel: automatic placements (Meta decides where ads serve) or a manual list of surfaces (Feed, Stories, Reels, ...).\n\n                Placements are where the ads serve inside Meta's networks. This is the same\n                \"Placements\" control the platform UI shows in the Facebook/Instagram section of\n                the campaign draft page. The campaign MUST already have the channel enabled\n                (via create_campaign / add_and_edit_campaign_elements).\n\n                ⚠️ DRAFT-ONLY: the platform rejects the edit once the campaign is Launching/Launched.\n\n                KEYWORDS: meta, facebook, instagram, threads, placements, placement, feed, stories,\n                reels, explore, search results, profile feed, automatic placements, manual placements,\n                advantage+ placements, where ads serve, surfaces, channel settings, campaign settings\n\n                WHEN TO USE:\n                - Serve ads only on specific surfaces (e.g. Instagram Reels and Stories)\n                - Switch a channel back to automatic placements (Meta optimizes delivery)\n                - The user asks where their Facebook/Instagram ads will appear\n\n                PARAMETERS:\n                - campaign_id: the wizard campaign ID (required)\n                - channel: FACEBOOK | INSTAGRAM (required — the campaign channel to edit)\n                - mode: AUTO | MANUAL (required). AUTO = automatic placements, clears any\n                  manual set. MANUAL = serve only on the placements listed.\n                - placements: array of placement codes, required when mode=MANUAL, ignored\n                  for AUTO. Facebook surfaces: FB_FEED, FB_STORIES, FB_REELS,\n                  FB_SEARCH_RESULTS. Instagram surfaces: IG_FEED, IG_PROFILE_FEED,\n                  IG_EXPLORE, IG_EXPLORE_HOME, IG_STORIES, IG_REELS, IG_SEARCH_RESULTS.\n                  Threads: TH_THREADS_STREAM.\n\n                PLACEMENT RULES (enforced; violations return a descriptive error):\n                - Companion rules: FB_STORIES and FB_SEARCH_RESULTS require FB_FEED;\n                  IG_PROFILE_FEED, IG_SEARCH_RESULTS and IG_EXPLORE require IG_FEED;\n                  IG_EXPLORE_HOME requires IG_EXPLORE; TH_THREADS_STREAM requires IG_FEED.\n                - An INSTAGRAM channel accepts IG_* placements only.\n                - A FACEBOOK channel always accepts FB_* placements. IG_* / TH_* placements\n                  on a FACEBOOK channel additionally need the account's unified-placement\n                  rollout (feature flag CORE_META_IG_PLACEMENT_ON_FB) — the platform rejects\n                  them on accounts without it.\n                - IG_PROFILE_FEED is not available on Lead Gen (CPL) campaigns.\n                - AUTO is rejected while the channel has ad groups (per-ad-group setups keep\n                  manual placements).\n\n                This tool sets the campaign-wide default for the channel. Per-ad-group\n                placement overrides exist in the platform UI but are not editable through\n                chat yet — send users to the campaign page for per-ad-group customization.\n\n                EXAMPLES:\n\n                Instagram channel, Reels + Stories + Feed only:\n                update_meta_placements({\"campaign_id\": 12345, \"channel\": \"INSTAGRAM\",\n                    \"mode\": \"MANUAL\", \"placements\": [\"IG_FEED\", \"IG_STORIES\", \"IG_REELS\"]})\n\n                Facebook channel back to automatic placements:\n                update_meta_placements({\"campaign_id\": 12345, \"channel\": \"FACEBOOK\", \"mode\": \"AUTO\"})\n\n                RESPONSE: {success, campaign_id, channel_id, channel, campaign_status,\n                applied:{mode, placements}, previous?, errors?}\n                `previous` echoes the channel's placement state before the edit.\n\n                INTEGRATION WITH OTHER TOOLS:\n                - Use search_campaigns_by_names / get_campaign_by_wizard_id to find the campaign\n                - The channel is enabled by create_campaign or add_and_edit_campaign_elements\n                - Reddit placements (feed/conversations) are a different system: use\n                  update_reddit_channel_settings for those",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The unique identifier of the wizard campaign",
      "minimum": 1
     },
     "channel": {
      "type": "string",
      "enum": [
       "FACEBOOK",
       "INSTAGRAM"
      ],
      "description": "Which campaign channel to edit. Meta placements only exist on Facebook and Instagram channels."
     },
     "mode": {
      "type": "string",
      "enum": [
       "AUTO",
       "MANUAL"
      ],
      "description": "AUTO = automatic placements (Meta decides, clears the manual set). MANUAL = serve only on the placements listed in `placements`."
     },
     "placements": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "FB_FEED",
        "FB_STORIES",
        "FB_REELS",
        "FB_SEARCH_RESULTS",
        "IG_FEED",
        "IG_PROFILE_FEED",
        "IG_EXPLORE",
        "IG_EXPLORE_HOME",
        "IG_STORIES",
        "IG_REELS",
        "IG_SEARCH_RESULTS",
        "TH_THREADS_STREAM"
       ]
      },
      "description": "Placement codes to serve on. Required when mode=MANUAL (at least one); ignored for AUTO. Companion rules apply — see the tool description."
     }
    },
    "required": [
     "campaign_id",
     "channel",
     "mode"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "upload_image_creative",
   "title": "Upload Image Creative",
   "category": "creative",
   "risk": "write",
   "summary": "Upload images directly to Metadata platform library to create image creatives.",
   "description": "Upload images directly to Metadata platform library to create image creatives.\n                Downloads images from provided URLs and uploads them to the platform.\n                ⚠️ REQUIRED STEP IN AD CREATION WORKFLOW:\n                This tool MUST be called between generate_brand_creative and create_update_image_ad.\n                **WORKFLOW INTEGRATION:**\n                This tool is part of the ad creation workflow and should be used:\n                1. BEFORE creating ads with create_update_image_ad (which requires imageLibraryId/creativeID)\n                2. IN COMBINATION with generate_brand_creative to create AI-generated images first, then upload them\n                3. When you need to upload existing images from external sources\n\n                **COMMON WORKFLOWS:**\n                - Upload existing images → Get imageLibraryId → Use in create_update_image_ad\n                - Generate image with generate_brand_creative → Download generated image → Upload with this tool → Use in create_update_image_ad\n                - Batch upload multiple creative assets for campaign preparation\n\n                **IMPORTANT:** The returned image ID (imageLibraryId/creativeID) is REQUIRED when creating ads.\n                Every ad needs a creative asset, so you must upload images first before calling create_update_image_ad.\n\n                REQUIRED PARAMETERS:\n                - images: Array of image URLs to upload\n\n                IMAGE REQUIREMENTS:\n                - URLs must be valid and publicly accessible\n                - Supported formats: PNG, JPG, JPEG, GIF, WebP, and others\n                - Images will be downloaded and then uploaded to platform\n                - Filenames with spaces will have spaces replaced with underscores\n\n                EXAMPLES:\n\n                Single Image:\n                upload_image([\n                    \"https://my-bucket.s3.amazonaws.com/images/sample-image.png\"\n                ])\n\n                Multiple Images:\n                upload_image([\n                    \"https://my-bucket.s3.amazonaws.com/images/logo.png\",\n                    \"https://example-assets.s3.us-west-2.amazonaws.com/photos/banner.jpg\",\n                    \"https://content-bucket.s3.eu-west-1.amazonaws.com/uploads/hero image.webp\"\n                ])\n\n                RESPONSE FORMAT:\n                Returns array of objects for each image:\n                [\n                    {\n                        \"url\": \"https://original-url.com/image.png\",\n                        \"name\": \"image.png\",\n                        \"id\": 12345,\n                        \"success\": true\n                    },\n                    {\n                        \"url\": \"https://failed-url.com/bad.png\",\n                        \"name\": \"bad.png\",\n                        \"id\": null,\n                        \"success\": false,\n                        \"error\": \"Download failed: Connection timeout\"\n                    }\n                ]\n\n                ERROR HANDLING:\n                - If one upload fails, others will continue\n                - Each result includes success status\n                - Failed uploads include error message\n                - Successful uploads include platform image ID\n\n                USE CASES:\n                - Upload creative assets before creating ads\n                - Import images from external sources\n                - Batch upload multiple campaign images\n                - Migrate images from other platforms",
   "inputSchema": {
    "type": "object",
    "properties": {
     "images": {
      "type": "array",
      "description": "Array of image URLs to upload to platform",
      "minItems": 1,
      "items": {
       "type": "string",
       "format": "uri",
       "description": "Publicly accessible URL to an image file"
      }
     },
     "names": {
      "type": "array",
      "description": "Optional library name per image, positional against `images`. Pass one whenever you know what the creative is: without it the name is taken from the URL, which for a generated creative is an opaque key like `brand_creative_20260814_211126_66d8a83e.png` and leaves the user unable to tell their creatives apart in the library. Use `<brand-domain>-<channel>-<width>x<height>.png`, e.g. `stripe-com-linkedin-1200x1200.png`. A name MUST end in one of .jpg/.jpeg/.png/.gif/.mov/.mp4/.html/.pdf.",
      "items": {
       "type": "string",
       "description": "Library name for the image at the same position"
      }
     }
    },
    "required": [
     "images"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "upload_video_creative",
   "title": "Upload Video Creative",
   "category": "creative",
   "risk": "write",
   "summary": "Upload videos directly to the Metadata platform library to create video creatives.",
   "description": "Upload videos directly to the Metadata platform library to create video creatives.\n                Downloads videos from provided URLs and uploads them to the platform.\n\n                ⚠️ VIDEO-ONLY: The downloaded file MUST have a `video/*` content-type\n                (e.g. video/mp4, video/quicktime). GIFs and image formats are\n                rejected — use `upload_image_creative` for those.\n\n                ⚠️ WHEN YOU NEED TO CALL THIS:\n                Call this BEFORE `create_update_video_ad` ONLY when you don't already\n                have a videoLibraryId. If the user (or an earlier step) has\n                already given you a creativeID for the video, skip the upload\n                and pass that id straight to `create_update_video_ad`.\n\n                WORKFLOW INTEGRATION (when an upload IS needed):\n                1. Upload your video URL with this tool → response contains the integer `id` (the videoLibraryId).\n                2. Pass that integer `id` as `creativeID` in `create_update_video_ad`.\n\n                COMMON WORKFLOWS:\n                - Upload existing video → Get videoLibraryId → Use as creativeID in `create_update_video_ad`.\n                - For Reddit VIDEO ads, ALSO upload a thumbnail image via `upload_image_creative` and pass that image\n                  library id as `redditThumbnailLibraryId` to `create_update_video_ad`.\n\n                REQUIRED PARAMETERS:\n                - videos: Array of video URLs to upload.\n\n                OPTIONAL PARAMETERS:\n                - thumbnail_library_id: Existing imageLibraryId to attach to the\n                  uploaded video as its platform thumbnail. Most callers don't\n                  need this — Reddit VIDEO ads handle thumbnails at ad-creation\n                  time via `redditThumbnailLibraryId`, not here.\n\n                VIDEO REQUIREMENTS:\n                - URLs must be valid and publicly accessible.\n                - Supported formats: MP4 (recommended), MOV, and other video/* MIME types.\n                - For GIFs (image/gif): use `upload_image_creative` instead.\n\n                RESPONSE FORMAT:\n                Returns array of objects, one per video. `id` is returned as a\n                string (the platform's library ids are integers but the upload\n                response stringifies them); pass it to `create_update_video_ad` as an\n                integer (Python `int(id)` if you need the cast).\n                [\n                    {\n                        \"url\": \"https://example.com/video.mp4\",\n                        \"name\": \"video.mp4\",\n                        \"id\": \"12345\",                # videoLibraryId — use as creativeID in create_update_video_ad\n                        \"success\": true\n                    },\n                    {\n                        \"url\": \"https://example.com/bad.mp4\",\n                        \"name\": \"bad.mp4\",\n                        \"id\": null,\n                        \"success\": false,\n                        \"error\": \"Download failed: Connection timeout\"\n                    }\n                ]\n\n                ERROR HANDLING:\n                - If one upload fails, others continue.\n                - Each result includes a success flag and (on failure) an error message.\n                - A content-type that does not start with `video/` is rejected\n                  with a clear error pointing the caller at `upload_image_creative`.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "videos": {
      "type": "array",
      "description": "Array of video URLs to upload to the platform library.",
      "minItems": 1,
      "items": {
       "type": "string",
       "format": "uri",
       "description": "Publicly accessible URL to a video file (video/* content-type)."
      }
     },
     "thumbnail_library_id": {
      "type": "integer",
      "description": "Optional existing imageLibraryId to attach as the video's platform thumbnail."
     }
    },
    "required": [
     "videos"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "add_extensions_to_native_campaign",
   "title": "Add Extensions to Native-Structure Campaign",
   "category": "extensions",
   "risk": "write",
   "summary": "Attach existing library extensions (sitelinks, callouts, etc.) to a native-structure Google Ads / Microsoft Ads campaign, so they actually serve when the campaign launches.",
   "description": "Attach existing library extensions (sitelinks, callouts, etc.) to a native-structure Google Ads / Microsoft Ads campaign, so they actually serve when the campaign launches.\n\n                ALSO KNOWN AS: add extension to campaign, attach sitelink, put sitelink on campaign, link extension to campaign, enable extension for campaign\n\n                KEYWORDS: add, attach, link, bind, extension, sitelink, callout, campaign, native, native structure, channel first, google ads, microsoft ads, bing\n\n                WHY THIS EXISTS: creating an extension only puts it in the account library — it is NOT attached to any campaign and will not serve. This tool performs the binding.\n\n                WHEN TO USE:\n                - \"Add our 'Book a Demo' sitelink to campaign X\"\n                - \"Attach these callouts to the native search campaign\"\n                - After create_google_sitelink_extension, when the user wants it live on a specific campaign\n\n                WHEN NOT TO USE:\n                - The campaign uses the Precision / METADATA structure — this tool covers native-structure campaigns only and will tell you so. Precision binds extensions per-creative and has no tool yet; direct the user to the platform UI.\n                - You do not have the extension's internal id → call list_extensions first\n                - Removing an attachment → use remove_extensions_from_native_campaign\n\n                INPUTS (all required):\n                - campaign_id:    wizard campaign id\n                - channel:        GOOGLE_ADS or MICROSOFT_ADS\n                - extension_ids:  internal ids from list_extensions (the `id` field, NOT `externalId`)\n\n                BEHAVIOUR: additive — existing attachments are preserved. Safe to re-run; already-attached ids are reported and left alone.\n\n                ORDERING CAVEAT: add_and_edit_native_campaign_elements rebuilds the channel's native containers and CLEARS extension attachments. Attach extensions AFTER any native element edits, or re-attach afterwards.\n\n                EXAMPLE:\n                add_extensions_to_native_campaign(campaign_id=8421, channel=\"GOOGLE_ADS\", extension_ids=[551, 552])",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The wizard campaign id."
     },
     "channel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS",
       "MICROSOFT_ADS"
      ],
      "description": "Search channel. Ad extensions are a search-channel-only concept."
     },
     "extension_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "minItems": 1,
      "description": "Internal library extension ids from list_extensions. These are the `id` field, NOT `externalId` (the ad platform's own id)."
     }
    },
    "required": [
     "campaign_id",
     "channel",
     "extension_ids"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_google_business_logo_extension",
   "title": "Create Google Ads Business Logo Extension (BETA)",
   "category": "extensions",
   "risk": "write",
   "summary": "Attach the business logo to a Google Ads text ad for brand recognition.",
   "description": "Attach the business logo to a Google Ads text ad for brand recognition. BETA. References a single image-library asset (the logo) and optionally pairs it with the business name and domain (the latter two are also used when the same logo is reused for Microsoft Ads).\n\n                ALSO KNOWN AS: business logo, brand logo, logo, company logo, brand mark, mark, identity asset\n\n                KEYWORDS: logo, business logo, brand, brand mark, identity, company logo, brand recognition, BETA, google ads, extension\n\n                WHEN TO USE:\n                - \"Add our business logo to our Google Ads\"\n                - \"Create a logo extension referencing image library ID 123\"\n                - \"Attach the Acme Corp logo to our search ads for brand lift\"\n\n                WHEN NOT TO USE:\n                - Multiple product / lifestyle shots → use create_google_image_extension\n                - The logo is not yet in the image library — upload it first, then call this with the resulting ID.\n                - Same logo for Microsoft Ads / Bing → use create_microsoft_business_logo_extension\n\n                INPUTS:\n                - image_library_id (required): single image library ID pointing to the logo asset.\n                - business_name:   business name displayed alongside the logo (≤25 chars). Required if the same library entry is also used for Microsoft Ads.\n                - domain_name:     business domain (≤50 chars). Used by Microsoft Ads.\n\n                EXAMPLE:\n                create_google_business_logo_extension(\n                    image_library_id=123,\n                    business_name=\"Acme Corp\",\n                    domain_name=\"acme.com\",\n                )",
   "inputSchema": {
    "type": "object",
    "properties": {
     "image_library_id": {
      "type": "integer"
     },
     "business_name": {
      "type": "string",
      "maxLength": 25
     },
     "domain_name": {
      "type": "string",
      "maxLength": 50
     }
    },
    "required": [
     "image_library_id"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_google_callout_extension",
   "title": "Create Google Ads Callout Extension",
   "category": "extensions",
   "risk": "write",
   "summary": "Add a short non-clickable selling-point snippet (callout) that appears beneath a Google Ads ad — short trust-builder phrases like \"Free Shipping\", \"24/7 Support\", \"Money-Back Gu…",
   "description": "Add a short non-clickable selling-point snippet (callout) that appears beneath a Google Ads ad — short trust-builder phrases like \"Free Shipping\", \"24/7 Support\", \"Money-Back Guarantee\", or \"No Setup Fees\". Callouts reinforce value props without taking up headline space.\n\n                ALSO KNOWN AS: callout, callout extension, selling point, trust badge, value prop, value proposition, tagline, ad badge\n\n                KEYWORDS: callout, selling point, trust signal, value prop, badge, free shipping, money back, guarantee, support, no fees, google ads, extension\n\n                WHEN TO USE:\n                - \"Add a 'Free Shipping' callout to our Google Ads library\"\n                - \"Create a callout for 24/7 support\"\n                - \"Register a new value-prop tag for the holiday campaigns\"\n                - \"I want a 'Money-Back Guarantee' badge to show under our ads\"\n\n                WHEN NOT TO USE:\n                - Want a clickable shortcut to a landing page → use create_google_sitelink_extension\n                - Want a list of named items (e.g. \"Brands: Nest, Nexus\") → use create_google_structured_snippet_extension\n                - Same callout for Microsoft Ads / Bing → use create_microsoft_callout_extension\n                - Anything for Facebook / LinkedIn / Reddit — search-channel-only\n\n                INPUTS:\n                - callout_text: visible snippet (1-25 chars).\n\n                EXAMPLE:\n                create_google_callout_extension(callout_text=\"Free Shipping\")",
   "inputSchema": {
    "type": "object",
    "properties": {
     "callout_text": {
      "type": "string",
      "minLength": 1,
      "maxLength": 25,
      "description": "Callout snippet. 1-25 characters."
     }
    },
    "required": [
     "callout_text"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_google_image_extension",
   "title": "Create Google Ads Image Extension (BETA)",
   "category": "extensions",
   "risk": "write",
   "summary": "Attach visual imagery (1-20 product / lifestyle shots) to a Google Ads text ad to lift click-through.",
   "description": "Attach visual imagery (1-20 product / lifestyle shots) to a Google Ads text ad to lift click-through. BETA. Re-uses assets already uploaded to the Metadata image library — pass their IDs, not raw files.\n\n                ALSO KNOWN AS: image extension, image asset, ad image, visual extension, picture extension, photo extension, hero image\n\n                KEYWORDS: image, image extension, photo, picture, visual, hero, asset, library, BETA, google ads, extension, CTR\n\n                WHEN TO USE:\n                - \"Attach images to our Google Ads text ad\"\n                - \"Add image library IDs 123 and 456 as an image extension\"\n                - \"Create an image extension showing product photos under our search ads\"\n\n                WHEN NOT TO USE:\n                - The image is a logo / brand mark → use create_google_business_logo_extension\n                - You haven't uploaded the assets yet — image extensions reference library IDs, so upload to the image library first.\n                - Want to drive clicks via text snippets → use create_google_sitelink_extension / callout\n                - Same images for Microsoft Ads / Bing → use create_microsoft_image_extension\n\n                INPUTS:\n                - image_library_ids (required): 1-20 IDs from the Metadata image library.\n                - alternative_text:  accessibility alt text (≤90 chars).\n                - display_text:      short caption overlaid on the image (≤35 chars). E.g. \"Shop Now\".\n                - destination_url:   landing page when the image is clicked.\n\n                EXAMPLE:\n                create_google_image_extension(\n                    image_library_ids=[123, 456],\n                    alternative_text=\"Product hero shot\",\n                    display_text=\"Shop Now\",\n                    destination_url=\"https://example.com/landing\",\n                )",
   "inputSchema": {
    "type": "object",
    "properties": {
     "image_library_ids": {
      "type": "array",
      "minItems": 1,
      "maxItems": 20,
      "items": {
       "type": "integer"
      },
      "description": "1-20 image-library IDs."
     },
     "alternative_text": {
      "type": "string",
      "maxLength": 90
     },
     "display_text": {
      "type": "string",
      "maxLength": 35
     },
     "destination_url": {
      "type": "string"
     }
    },
    "required": [
     "image_library_ids"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_google_lead_form_extension",
   "title": "Create Google Ads Lead Form Extension",
   "category": "extensions",
   "risk": "write",
   "summary": "Add an inline lead-capture form attached to a Google Ads ad — prospects submit their email plus optional contact and work info without leaving the SERP.",
   "description": "Add an inline lead-capture form attached to a Google Ads ad — prospects submit their email plus optional contact and work info without leaving the SERP. Drives higher conversion than landing-page redirects and feeds leads straight into Metadata.\n\n                ALSO KNOWN AS: lead form, lead gen form, inline form, lead capture, contact form, demo request form, in-ad form, form extension\n\n                KEYWORDS: lead form, lead gen, lead capture, form, contact form, demo request, inline form, conversion, MQL, opt-in, signup, registration, google ads, extension\n\n                WHEN TO USE:\n                - \"Add a lead form extension to capture demo requests under our search ads\"\n                - \"Create an inline form asking for work email and job title\"\n                - \"Set up a Google Ads lead-gen form with a 'Request Demo' CTA\"\n                - \"Register a contact-us form extension for the homepage campaign\"\n\n                WHEN NOT TO USE:\n                - Just want to drive clicks to a landing page (no inline form) → use create_google_sitelink_extension\n                - Want to plug a single product / pricing tier under the ad → use create_google_price_extension\n                - For Microsoft Ads / Bing — Microsoft does not surface inline lead forms via the platform; pick a different extension type or use a landing-page redirect.\n                - For Facebook lead forms or LinkedIn LeadGen — those are separate channel surfaces, not search-channel ad-extensions.\n\n                INPUTS (required):\n                - headline:                          form headline (≤30 chars).\n                - business_name:                     advertiser name shown in form (≤25 chars).\n                - privacy_policy_url:                link to your privacy policy (Google requires it).\n                - post_submit_headline:              thank-you screen headline (≤30 chars).\n                - post_submit_description:           thank-you screen body (≤200 chars).\n                - post_submit_call_to_action_type:   button on the thank-you screen. VISIT_SITE | DOWNLOAD | LEARN_MORE | SHOP_NOW.\n                - call_to_action_type:               main CTA on the ad. APPLY_NOW | BOOK_NOW | CONTACT_US | DOWNLOAD | GET_INFO | GET_OFFER | GET_QUOTE | GET_STARTED | JOIN_NOW | LEARN_MORE | REGISTER | REQUEST_DEMO | SIGN_UP | SUBSCRIBE.\n                - call_to_action_description:        sub-text under the CTA (≤30 chars).\n                - email_lead_form_user_input_types:  at least one of WORK_EMAIL, EMAIL.\n\n                INPUTS (optional):\n                - description: form body text (≤200 chars).\n                - contact_lead_form_user_input_types: any of FIRST_NAME, LAST_NAME, PHONE_NUMBER, CITY, POSTAL_CODE, REGION, COUNTRY, STREET_ADDRESS.\n                - work_lead_form_user_input_types:    any of COMPANY_NAME, JOB_TITLE, WORK_PHONE.\n                - url: thank-you-page URL after submit.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "headline": {
      "type": "string",
      "minLength": 1,
      "maxLength": 30
     },
     "business_name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 25
     },
     "description": {
      "type": "string",
      "maxLength": 200
     },
     "email_lead_form_user_input_types": {
      "type": "array",
      "minItems": 1,
      "items": {
       "type": "string",
       "enum": [
        "WORK_EMAIL",
        "EMAIL"
       ]
      }
     },
     "contact_lead_form_user_input_types": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "FIRST_NAME",
        "LAST_NAME",
        "PHONE_NUMBER",
        "CITY",
        "POSTAL_CODE",
        "REGION",
        "COUNTRY",
        "STREET_ADDRESS"
       ]
      }
     },
     "work_lead_form_user_input_types": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "COMPANY_NAME",
        "JOB_TITLE",
        "WORK_PHONE"
       ]
      }
     },
     "privacy_policy_url": {
      "type": "string",
      "minLength": 1
     },
     "post_submit_headline": {
      "type": "string",
      "minLength": 1,
      "maxLength": 30
     },
     "post_submit_description": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
     },
     "post_submit_call_to_action_type": {
      "type": "string",
      "enum": [
       "VISIT_SITE",
       "DOWNLOAD",
       "LEARN_MORE",
       "SHOP_NOW"
      ]
     },
     "url": {
      "type": "string",
      "description": "Optional thank-you-page URL."
     },
     "call_to_action_type": {
      "type": "string",
      "enum": [
       "APPLY_NOW",
       "BOOK_NOW",
       "CONTACT_US",
       "DOWNLOAD",
       "GET_INFO",
       "GET_OFFER",
       "GET_QUOTE",
       "GET_STARTED",
       "JOIN_NOW",
       "LEARN_MORE",
       "REGISTER",
       "REQUEST_DEMO",
       "SIGN_UP",
       "SUBSCRIBE"
      ]
     },
     "call_to_action_description": {
      "type": "string",
      "minLength": 1,
      "maxLength": 30
     }
    },
    "required": [
     "headline",
     "business_name",
     "email_lead_form_user_input_types",
     "privacy_policy_url",
     "post_submit_headline",
     "post_submit_description",
     "post_submit_call_to_action_type",
     "call_to_action_type",
     "call_to_action_description"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_google_price_extension",
   "title": "Create Google Ads Price Extension",
   "category": "extensions",
   "risk": "write",
   "summary": "Add a price-list shown beneath a Google Ads ad — 3-8 tiles, each with a plan/product name, a short description, a USD amount, and a billing unit.",
   "description": "Add a price-list shown beneath a Google Ads ad — 3-8 tiles, each with a plan/product name, a short description, a USD amount, and a billing unit. Standard play for SaaS pricing tiers, subscription plans, course catalogs, service menus, or hotel night rates.\n\n                ALSO KNOWN AS: price extension, pricing, price list, pricing tiers, plan pricing, subscription pricing, price tiles, rate card, fare list\n\n                KEYWORDS: price, pricing, price list, plan, plans, tier, tiers, subscription, monthly, yearly, hourly, daily, fare, rate, cost, dollar, $, USD, google ads, extension\n\n                WHEN TO USE:\n                - \"Show our pricing tiers under our Google Ads\"\n                - \"Add a price extension with Basic / Pro / Scale plans\"\n                - \"Register monthly subscription prices for the campaign\"\n                - \"Create a rate card extension starting from $9.99\"\n\n                WHEN NOT TO USE:\n                - Single time-bound discount (Black Friday 20% off, promo code) → use create_google_promotion_extension\n                - Non-priced offerings list (just names, no amounts) → use create_google_structured_snippet_extension\n                - Same price list for Microsoft Ads / Bing → use create_microsoft_price_extension\n\n                INPUTS:\n                - price_qualifier: prefix shown before each price. UNSPECIFIED (no prefix) | FROM (\"From $X\") | UP_TO (\"Up to $X\") | AVERAGE (\"Avg $X\").\n                - price_offerings: 3-8 tiles. Each tile:\n                    - url:         landing page for that tier (non-blank, ≤2000 chars).\n                    - header:      tier name (1-25 chars). E.g. \"Basic\", \"Pro\", \"Scale\".\n                    - description: short tagline (1-25 chars). E.g. \"Starter plan\".\n                    - amount:      USD number (max 999999999.99, two decimals).\n                    - unit:        billing cadence. UNSPECIFIED | PER_HOUR | PER_DAY | PER_WEEK | PER_MONTH | PER_YEAR | PER_NIGHT.\n\n                EXAMPLE:\n                create_google_price_extension(\n                    price_qualifier=\"FROM\",\n                    price_offerings=[\n                        {\"url\": \"https://example.com/basic\", \"header\": \"Basic\",\n                         \"description\": \"Starter plan\",  \"amount\": 9.99,  \"unit\": \"PER_MONTH\"},\n                        {\"url\": \"https://example.com/pro\",   \"header\": \"Pro\",\n                         \"description\": \"Growing teams\", \"amount\": 29.00, \"unit\": \"PER_MONTH\"},\n                        {\"url\": \"https://example.com/scale\", \"header\": \"Scale\",\n                         \"description\": \"Enterprise\",    \"amount\": 99.00, \"unit\": \"PER_MONTH\"},\n                    ],\n                )",
   "inputSchema": {
    "type": "object",
    "properties": {
     "price_qualifier": {
      "type": "string",
      "enum": [
       "UNSPECIFIED",
       "FROM",
       "UP_TO",
       "AVERAGE"
      ],
      "description": "Price qualifier shown before the price."
     },
     "price_offerings": {
      "type": "array",
      "minItems": 3,
      "maxItems": 8,
      "items": {
       "type": "object",
       "properties": {
        "url": {
         "type": "string",
         "minLength": 1,
         "maxLength": 2000
        },
        "header": {
         "type": "string",
         "minLength": 1,
         "maxLength": 25
        },
        "description": {
         "type": "string",
         "minLength": 1,
         "maxLength": 25
        },
        "amount": {
         "type": "number",
         "exclusiveMinimum": 0
        },
        "unit": {
         "type": "string",
         "enum": [
          "UNSPECIFIED",
          "PER_HOUR",
          "PER_DAY",
          "PER_WEEK",
          "PER_MONTH",
          "PER_YEAR",
          "PER_NIGHT"
         ]
        }
       },
       "required": [
        "url",
        "header",
        "description",
        "amount",
        "unit"
       ],
       "additionalProperties": false
      },
      "description": "3-8 price offerings."
     }
    },
    "required": [
     "price_qualifier",
     "price_offerings"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_google_promotion_extension",
   "title": "Create Google Ads Promotion Extension",
   "category": "extensions",
   "risk": "write",
   "summary": "Add a time-bound promotional offer (sale, discount, promo code) that shows under a Google Ads ad — flag a holiday occasion, pick the discount style (% off or $ off), optionally…",
   "description": "Add a time-bound promotional offer (sale, discount, promo code) that shows under a Google Ads ad — flag a holiday occasion, pick the discount style (% off or $ off), optionally attach a promo code or \"on orders over $X\" qualifier, and set the start/end window. Standard for Black Friday, Cyber Monday, seasonal sales, flash discounts, holiday campaigns.\n\n                ALSO KNOWN AS: promotion, promo, discount, sale, offer, coupon, promo code, voucher, deal, holiday sale, seasonal offer\n\n                KEYWORDS: promotion, promo, promo code, coupon, sale, discount, offer, deal, black friday, cyber monday, holiday, christmas, summer sale, percent off, dollar off, $ off, voucher, google ads, extension\n\n                WHEN TO USE:\n                - \"Set up a Black Friday 20% off promotion for our Pro plan\"\n                - \"Add a holiday promo extension with code SUMMER2026\"\n                - \"Register a sale: $50 off orders over $200\"\n                - \"Create a Cyber Monday discount extension running Nov 30 to Dec 2\"\n\n                WHEN NOT TO USE:\n                - Permanent pricing tiers (not a limited-time discount) → use create_google_price_extension\n                - Plain \"Free Shipping\" badge → use create_google_callout_extension\n                - Lead-capture form attached to the ad → use create_google_lead_form_extension\n                - Same promotion for Microsoft Ads / Bing → use create_microsoft_promotion_extension\n\n                INPUTS (required):\n                - occasion: holiday/event tag (NONE if no specific occasion). Available: NONE, BACK_TO_SCHOOL, BLACK_FRIDAY, BOXING_DAY, CARNIVAL, CHINESE_NEW_YEAR, CHRISTMAS, CYBER_MONDAY, DIWALI, EASTER, EID_AL_ADHA, EID_AL_FITR, END_OF_SEASON, EPIPHANY, FALL_SALE, FATHERS_DAY, HALLOWEEN, HANUKKAH, HOLI, INDEPENDENCE_DAY, LABOR_DAY, MOTHERS_DAY, NATIONAL_DAY, NAVRATRI, NEW_YEARS, PARENTS_DAY, PASSOVER, RAMADAN, ROSH_HASHANAH, SINGLES_DAY, SONGKRAN, SPRING_SALE, ST_NICHOLAS_DAY, SUMMER_SALE, VALENTINES_DAY, WINTER_SALE, WOMENS_DAY, YEAR_END_GIFT.\n                - url: landing page for the promotion.\n                - item: what's on sale (1-20 chars). E.g. \"Pro subscription\".\n                - category_type: discount style.\n                    * MONETARY        → \"$X off\". Provide category_amount.\n                    * UP_TO_MONETARY  → \"Up to $X off\". Provide category_amount.\n                    * PERCENT         → \"X% off\". Provide category_percent (≤100).\n                    * UP_TO_PERCENT   → \"Up to X% off\". Provide category_percent (≤100).\n                - details_type: optional qualifier.\n                    * NONE            → no qualifier.\n                    * ON_ORDERS_OVER  → \"on orders over $X\". Provide details_amount.\n                    * PROMO_CODE      → \"with code XXXX\". Provide details_promo_code (≤15 chars).\n                - start_date / end_date: ISO-8601 in pattern \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\" (e.g. 2026-11-25T00:00:00.000Z). Both must be today or in the future.\n\n                INPUTS (optional, see category_type / details_type above):\n                - category_amount, category_percent, details_promo_code, details_amount.\n\n                EXAMPLE — Black Friday 20% off Pro with promo code:\n                create_google_promotion_extension(\n                    occasion=\"BLACK_FRIDAY\",\n                    url=\"https://example.com/sale\",\n                    item=\"Pro subscription\",\n                    category_type=\"PERCENT\",   category_percent=20,\n                    details_type=\"PROMO_CODE\", details_promo_code=\"BF2026\",\n                    start_date=\"2026-11-25T00:00:00.000Z\",\n                    end_date=\"2026-11-30T23:59:59.000Z\",\n                )",
   "inputSchema": {
    "type": "object",
    "properties": {
     "occasion": {
      "type": "string",
      "enum": [
       "NONE",
       "BACK_TO_SCHOOL",
       "BLACK_FRIDAY",
       "BOXING_DAY",
       "CARNIVAL",
       "CHINESE_NEW_YEAR",
       "CHRISTMAS",
       "CYBER_MONDAY",
       "DIWALI",
       "EASTER",
       "EID_AL_ADHA",
       "EID_AL_FITR",
       "END_OF_SEASON",
       "EPIPHANY",
       "FALL_SALE",
       "FATHERS_DAY",
       "HALLOWEEN",
       "HANUKKAH",
       "HOLI",
       "INDEPENDENCE_DAY",
       "LABOR_DAY",
       "MOTHERS_DAY",
       "NATIONAL_DAY",
       "NAVRATRI",
       "NEW_YEARS",
       "PARENTS_DAY",
       "PASSOVER",
       "RAMADAN",
       "ROSH_HASHANAH",
       "SINGLES_DAY",
       "SONGKRAN",
       "SPRING_SALE",
       "ST_NICHOLAS_DAY",
       "SUMMER_SALE",
       "VALENTINES_DAY",
       "WINTER_SALE",
       "WOMENS_DAY",
       "YEAR_END_GIFT"
      ]
     },
     "url": {
      "type": "string",
      "minLength": 1
     },
     "item": {
      "type": "string",
      "minLength": 1,
      "maxLength": 20
     },
     "category_type": {
      "type": "string",
      "enum": [
       "MONETARY",
       "UP_TO_MONETARY",
       "PERCENT",
       "UP_TO_PERCENT"
      ]
     },
     "category_amount": {
      "type": "number",
      "exclusiveMinimum": 0,
      "description": "USD amount; supply when category_type is MONETARY or UP_TO_MONETARY."
     },
     "category_percent": {
      "type": "number",
      "exclusiveMinimum": 0,
      "maximum": 100,
      "description": "Percent (≤100); supply when category_type is PERCENT or UP_TO_PERCENT."
     },
     "details_type": {
      "type": "string",
      "enum": [
       "NONE",
       "ON_ORDERS_OVER",
       "PROMO_CODE"
      ]
     },
     "details_promo_code": {
      "type": "string",
      "maxLength": 15,
      "description": "Promo code; supply when details_type is PROMO_CODE."
     },
     "details_amount": {
      "type": "number",
      "exclusiveMinimum": 0,
      "description": "Threshold USD amount; supply when details_type is ON_ORDERS_OVER."
     },
     "start_date": {
      "type": "string",
      "description": "ISO-8601 \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\". Future or present."
     },
     "end_date": {
      "type": "string",
      "description": "ISO-8601 \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\". Future or present."
     }
    },
    "required": [
     "occasion",
     "url",
     "item",
     "category_type",
     "details_type",
     "start_date",
     "end_date"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_google_sitelink_extension",
   "title": "Create Google Ads Sitelink Extension",
   "category": "extensions",
   "risk": "write",
   "summary": "Add an extra clickable shortcut (sitelink) that appears beneath a Google Ads ad — helpful deep-links like \"See Pricing\", \"Book a Demo\", or \"Contact Sales\" with two short descrip…",
   "description": "Add an extra clickable shortcut (sitelink) that appears beneath a Google Ads ad — helpful deep-links like \"See Pricing\", \"Book a Demo\", or \"Contact Sales\" with two short description lines. Sitelinks lift click-through rate by giving searchers alternate landing paths to the same advertiser.\n\n                ALSO KNOWN AS: sitelink, site link, extra link, ad link, secondary link, deep link, additional link, jump link\n\n                KEYWORDS: sitelink, site link, link, deep link, shortcut, google ads, extension, pricing link, demo link, contact link, landing page, CTR, click through\n\n                WHEN TO USE:\n                - \"Add a sitelink to our Google Ads library\"\n                - \"Create a 'See Pricing' / 'Book a Demo' / 'Contact Sales' link extension\"\n                - \"Register a new sitelink for the homepage redesign\"\n                - \"I need a shortcut under our search ads that points to the new pricing page\"\n\n                WHEN NOT TO USE:\n                - Want a non-clickable selling-point snippet (\"Free Shipping\", \"24/7 Support\") → use create_google_callout_extension\n                - Want a labeled list of offerings (e.g. \"Brands: Nest, Nexus\") → use create_google_structured_snippet_extension\n                - Same sitelink for Microsoft Ads / Bing → use create_microsoft_sitelink_extension (library entries are per-channel)\n                - Anything for Facebook / LinkedIn / Reddit — ad extensions are a search-channel-only concept\n\n                INPUTS (all required):\n                - url:          destination the sitelink opens.\n                - link_text:    visible link text (1-25 chars).\n                - description1: first description line under the link (1-35 chars).\n                - description2: second description line (1-35 chars).\n\n                EXAMPLE:\n                create_google_sitelink_extension(\n                    url=\"https://example.com/pricing\",\n                    link_text=\"See Pricing\",\n                    description1=\"Plans for every team size\",\n                    description2=\"Start free, upgrade anytime\",\n                )\n\n                Returns the persisted extension (id, externalId, type=SITELINK, channel=GOOGLE_ADS). A 400 usually means a character-limit was exceeded.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "url": {
      "type": "string",
      "minLength": 1,
      "description": "Destination URL the sitelink points to (non-blank)."
     },
     "link_text": {
      "type": "string",
      "minLength": 1,
      "maxLength": 25,
      "description": "Visible link text. 1-25 characters."
     },
     "description1": {
      "type": "string",
      "minLength": 1,
      "maxLength": 35,
      "description": "First description line. 1-35 characters."
     },
     "description2": {
      "type": "string",
      "minLength": 1,
      "maxLength": 35,
      "description": "Second description line. 1-35 characters."
     }
    },
    "required": [
     "url",
     "link_text",
     "description1",
     "description2"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_google_structured_snippet_extension",
   "title": "Create Google Ads Structured Snippet Extension",
   "category": "extensions",
   "risk": "write",
   "summary": "Add a labeled list of offerings (structured snippet) shown beneath a Google Ads ad — a category header plus 3-10 short items, e.g.",
   "description": "Add a labeled list of offerings (structured snippet) shown beneath a Google Ads ad — a category header plus 3-10 short items, e.g. header \"Brands\" with values \"Nest\", \"Nexus\", \"Chromebook\", or header \"Services\" with \"Hosting\", \"DNS\", \"CDN\". Helps searchers self-qualify before clicking.\n\n                ALSO KNOWN AS: structured snippet, snippet, category list, offering list, header values, product list, service list\n\n                KEYWORDS: structured snippet, snippet, header, values, brands, services, products, courses, destinations, list, category, google ads, extension\n\n                WHEN TO USE:\n                - \"Add a structured snippet listing our service tiers\"\n                - \"Create a snippet under 'Brands' with Nest, Nexus, Chromebook\"\n                - \"Register a list of supported integrations as a snippet extension\"\n                - \"Show our product categories below our Google Ads\"\n\n                WHEN NOT TO USE:\n                - Want a single phrase (e.g. \"Free Shipping\") → use create_google_callout_extension\n                - Want clickable links to subpages → use create_google_sitelink_extension\n                - Want a pricing table (with amounts) → use create_google_price_extension\n                - Same snippet for Microsoft Ads / Bing → use create_microsoft_structured_snippet_extension\n\n                INPUTS:\n                - header: snippet category title (1-25 chars). The Google-Ads vocabulary requires picking one of Google's predefined header names (\"Brands\", \"Models\", \"Service catalog\", \"Insurance coverage\", etc.) — pass the chosen header string here.\n                - values: 3-10 items, each 1-25 chars.\n\n                EXAMPLE:\n                create_google_structured_snippet_extension(\n                    header=\"Brands\",\n                    values=[\"Nest\", \"Nexus\", \"Chromebook\"],\n                )",
   "inputSchema": {
    "type": "object",
    "properties": {
     "header": {
      "type": "string",
      "minLength": 1,
      "maxLength": 25,
      "description": "Snippet header. 1-25 characters."
     },
     "values": {
      "type": "array",
      "minItems": 3,
      "maxItems": 10,
      "items": {
       "type": "string",
       "minLength": 1,
       "maxLength": 25
      },
      "description": "3-10 snippet values, each 1-25 characters."
     }
    },
    "required": [
     "header",
     "values"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_microsoft_business_logo_extension",
   "title": "Create Microsoft Ads Business Logo Extension (BETA)",
   "category": "extensions",
   "risk": "write",
   "summary": "Attach the business logo to a Microsoft Ads (Bing) text ad for brand recognition.",
   "description": "Attach the business logo to a Microsoft Ads (Bing) text ad for brand recognition. BETA. References a single image-library asset (the logo) and optionally pairs it with the business name and domain (which Bing surfaces alongside the logo on its SERP).\n\n                ALSO KNOWN AS: business logo, brand logo, logo, company logo, brand mark, mark, identity asset\n\n                KEYWORDS: logo, business logo, brand, brand mark, identity, company logo, brand recognition, BETA, microsoft ads, bing, extension\n\n                WHEN TO USE:\n                - \"Add our business logo to our Microsoft Ads\"\n                - \"Create a Bing logo extension referencing image library ID 123\"\n                - \"Attach the Acme Corp logo to our Microsoft Ads search ads for brand lift\"\n\n                WHEN NOT TO USE:\n                - Multiple product / lifestyle shots → use create_microsoft_image_extension\n                - The logo is not yet in the image library — upload it first, then call this with the resulting ID.\n                - Same logo for Google Ads → use create_google_business_logo_extension\n\n                INPUTS:\n                - image_library_id (required): single image library ID pointing to the logo asset.\n                - business_name:   business name displayed alongside the logo (≤25 chars). Microsoft Ads surfaces this on the SERP.\n                - domain_name:     business domain (≤50 chars). Microsoft Ads surfaces this on the SERP.\n\n                EXAMPLE:\n                create_microsoft_business_logo_extension(\n                    image_library_id=123,\n                    business_name=\"Acme Corp\",\n                    domain_name=\"acme.com\",\n                )",
   "inputSchema": {
    "type": "object",
    "properties": {
     "image_library_id": {
      "type": "integer"
     },
     "business_name": {
      "type": "string",
      "maxLength": 25
     },
     "domain_name": {
      "type": "string",
      "maxLength": 50
     }
    },
    "required": [
     "image_library_id"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_microsoft_callout_extension",
   "title": "Create Microsoft Ads Callout Extension",
   "category": "extensions",
   "risk": "write",
   "summary": "Add a short non-clickable selling-point snippet (callout) that appears beneath a Microsoft Ads (Bing) ad — short trust-builder phrases like \"Free Shipping\", \"24/7 Support\", \"Mon…",
   "description": "Add a short non-clickable selling-point snippet (callout) that appears beneath a Microsoft Ads (Bing) ad — short trust-builder phrases like \"Free Shipping\", \"24/7 Support\", \"Money-Back Guarantee\", or \"No Setup Fees\". Callouts reinforce value props without taking up headline space.\n\n                ALSO KNOWN AS: callout, callout extension, selling point, trust badge, value prop, value proposition, tagline, ad badge\n\n                KEYWORDS: callout, selling point, trust signal, value prop, badge, free shipping, money back, guarantee, support, no fees, microsoft ads, bing, extension\n\n                WHEN TO USE:\n                - \"Add a 'Free Shipping' callout to our Microsoft Ads library\"\n                - \"Create a Bing callout for 24/7 support\"\n                - \"Register a new value-prop tag for the holiday Microsoft Ads campaigns\"\n\n                WHEN NOT TO USE:\n                - Want a clickable shortcut to a landing page → use create_microsoft_sitelink_extension\n                - Want a list of named items (e.g. \"Brands: Nest, Nexus\") → use create_microsoft_structured_snippet_extension\n                - Same callout for Google Ads → use create_google_callout_extension\n                - Anything for Facebook / LinkedIn / Reddit — search-channel-only\n\n                INPUTS:\n                - callout_text: visible snippet (1-25 chars).\n\n                EXAMPLE:\n                create_microsoft_callout_extension(callout_text=\"Free Shipping\")",
   "inputSchema": {
    "type": "object",
    "properties": {
     "callout_text": {
      "type": "string",
      "minLength": 1,
      "maxLength": 25,
      "description": "Callout snippet. 1-25 characters."
     }
    },
    "required": [
     "callout_text"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_microsoft_image_extension",
   "title": "Create Microsoft Ads Image Extension (BETA)",
   "category": "extensions",
   "risk": "write",
   "summary": "Attach visual imagery (1-20 product / lifestyle shots) to a Microsoft Ads (Bing) text ad to lift click-through.",
   "description": "Attach visual imagery (1-20 product / lifestyle shots) to a Microsoft Ads (Bing) text ad to lift click-through. BETA. Re-uses assets already uploaded to the Metadata image library — pass their IDs, not raw files.\n\n                ALSO KNOWN AS: image extension, image asset, ad image, visual extension, picture extension, photo extension, hero image\n\n                KEYWORDS: image, image extension, photo, picture, visual, hero, asset, library, BETA, microsoft ads, bing, extension, CTR\n\n                WHEN TO USE:\n                - \"Attach images to our Microsoft Ads text ad\"\n                - \"Add image library IDs 123 and 456 as a Bing image extension\"\n                - \"Create a Microsoft Ads image extension showing product photos under our search ads\"\n\n                WHEN NOT TO USE:\n                - The image is a logo / brand mark → use create_microsoft_business_logo_extension\n                - You haven't uploaded the assets yet — image extensions reference library IDs, so upload to the image library first.\n                - Want to drive clicks via text snippets → use create_microsoft_sitelink_extension / callout\n                - Same images for Google Ads → use create_google_image_extension\n\n                INPUTS:\n                - image_library_ids (required): 1-20 IDs from the Metadata image library.\n                - alternative_text:  accessibility alt text (≤90 chars).\n                - display_text:      short caption overlaid on the image (≤35 chars). E.g. \"Shop Now\".\n                - destination_url:   landing page when the image is clicked.\n\n                EXAMPLE:\n                create_microsoft_image_extension(\n                    image_library_ids=[123, 456],\n                    alternative_text=\"Product hero shot\",\n                    display_text=\"Shop Now\",\n                    destination_url=\"https://example.com/landing\",\n                )",
   "inputSchema": {
    "type": "object",
    "properties": {
     "image_library_ids": {
      "type": "array",
      "minItems": 1,
      "maxItems": 20,
      "items": {
       "type": "integer"
      },
      "description": "1-20 image-library IDs."
     },
     "alternative_text": {
      "type": "string",
      "maxLength": 90
     },
     "display_text": {
      "type": "string",
      "maxLength": 35
     },
     "destination_url": {
      "type": "string"
     }
    },
    "required": [
     "image_library_ids"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_microsoft_price_extension",
   "title": "Create Microsoft Ads Price Extension",
   "category": "extensions",
   "risk": "write",
   "summary": "Add a price-list shown beneath a Microsoft Ads (Bing) ad — 3-8 tiles, each with a plan/product name, a short description, a USD amount, and a billing unit.",
   "description": "Add a price-list shown beneath a Microsoft Ads (Bing) ad — 3-8 tiles, each with a plan/product name, a short description, a USD amount, and a billing unit. Standard play for SaaS pricing tiers, subscription plans, course catalogs, service menus, or hotel night rates.\n\n                ALSO KNOWN AS: price extension, pricing, price list, pricing tiers, plan pricing, subscription pricing, price tiles, rate card, fare list\n\n                KEYWORDS: price, pricing, price list, plan, plans, tier, tiers, subscription, monthly, yearly, hourly, daily, fare, rate, cost, dollar, $, USD, microsoft ads, bing, extension\n\n                WHEN TO USE:\n                - \"Show our pricing tiers under our Microsoft Ads\"\n                - \"Add a Bing price extension with Basic / Pro / Scale plans\"\n                - \"Register monthly subscription prices for the Microsoft Ads campaign\"\n\n                WHEN NOT TO USE:\n                - Single time-bound discount (Black Friday 20% off, promo code) → use create_microsoft_promotion_extension\n                - Non-priced offerings list (just names, no amounts) → use create_microsoft_structured_snippet_extension\n                - Same price list for Google Ads → use create_google_price_extension\n\n                INPUTS:\n                - price_qualifier: prefix shown before each price. UNSPECIFIED (no prefix) | FROM (\"From $X\") | UP_TO (\"Up to $X\") | AVERAGE (\"Avg $X\").\n                - price_offerings: 3-8 tiles. Each tile:\n                    - url:         landing page for that tier (non-blank, ≤2000 chars).\n                    - header:      tier name (1-25 chars). E.g. \"Basic\", \"Pro\", \"Scale\".\n                    - description: short tagline (1-25 chars). E.g. \"Starter plan\".\n                    - amount:      USD number (max 999999999.99, two decimals).\n                    - unit:        billing cadence. UNSPECIFIED | PER_HOUR | PER_DAY | PER_WEEK | PER_MONTH | PER_YEAR | PER_NIGHT.\n\n                EXAMPLE:\n                create_microsoft_price_extension(\n                    price_qualifier=\"FROM\",\n                    price_offerings=[\n                        {\"url\": \"https://example.com/basic\", \"header\": \"Basic\",\n                         \"description\": \"Starter plan\",  \"amount\": 9.99,  \"unit\": \"PER_MONTH\"},\n                        {\"url\": \"https://example.com/pro\",   \"header\": \"Pro\",\n                         \"description\": \"Growing teams\", \"amount\": 29.00, \"unit\": \"PER_MONTH\"},\n                        {\"url\": \"https://example.com/scale\", \"header\": \"Scale\",\n                         \"description\": \"Enterprise\",    \"amount\": 99.00, \"unit\": \"PER_MONTH\"},\n                    ],\n                )",
   "inputSchema": {
    "type": "object",
    "properties": {
     "price_qualifier": {
      "type": "string",
      "enum": [
       "UNSPECIFIED",
       "FROM",
       "UP_TO",
       "AVERAGE"
      ],
      "description": "Price qualifier shown before the price."
     },
     "price_offerings": {
      "type": "array",
      "minItems": 3,
      "maxItems": 8,
      "items": {
       "type": "object",
       "properties": {
        "url": {
         "type": "string",
         "minLength": 1,
         "maxLength": 2000
        },
        "header": {
         "type": "string",
         "minLength": 1,
         "maxLength": 25
        },
        "description": {
         "type": "string",
         "minLength": 1,
         "maxLength": 25
        },
        "amount": {
         "type": "number",
         "exclusiveMinimum": 0
        },
        "unit": {
         "type": "string",
         "enum": [
          "UNSPECIFIED",
          "PER_HOUR",
          "PER_DAY",
          "PER_WEEK",
          "PER_MONTH",
          "PER_YEAR",
          "PER_NIGHT"
         ]
        }
       },
       "required": [
        "url",
        "header",
        "description",
        "amount",
        "unit"
       ],
       "additionalProperties": false
      },
      "description": "3-8 price offerings."
     }
    },
    "required": [
     "price_qualifier",
     "price_offerings"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_microsoft_promotion_extension",
   "title": "Create Microsoft Ads Promotion Extension",
   "category": "extensions",
   "risk": "write",
   "summary": "Add a time-bound promotional offer (sale, discount, promo code) that shows under a Microsoft Ads (Bing) ad — flag a holiday occasion, pick the discount style (% off or $ off), o…",
   "description": "Add a time-bound promotional offer (sale, discount, promo code) that shows under a Microsoft Ads (Bing) ad — flag a holiday occasion, pick the discount style (% off or $ off), optionally attach a promo code or \"on orders over $X\" qualifier, and set the start/end window. Standard for Black Friday, Cyber Monday, seasonal sales, flash discounts, holiday campaigns.\n\n                ALSO KNOWN AS: promotion, promo, discount, sale, offer, coupon, promo code, voucher, deal, holiday sale, seasonal offer\n\n                KEYWORDS: promotion, promo, promo code, coupon, sale, discount, offer, deal, black friday, cyber monday, holiday, christmas, summer sale, percent off, dollar off, $ off, voucher, microsoft ads, bing, extension\n\n                WHEN TO USE:\n                - \"Set up a Black Friday 20% off Microsoft Ads promotion for our Pro plan\"\n                - \"Add a Bing holiday promo extension with code SUMMER2026\"\n                - \"Register a Microsoft Ads sale: $50 off orders over $200\"\n\n                WHEN NOT TO USE:\n                - Permanent pricing tiers (not a limited-time discount) → use create_microsoft_price_extension\n                - Plain \"Free Shipping\" badge → use create_microsoft_callout_extension\n                - Same promotion for Google Ads → use create_google_promotion_extension\n\n                INPUTS (required):\n                - occasion: holiday/event tag (NONE if no specific occasion). Available: NONE, BACK_TO_SCHOOL, BLACK_FRIDAY, BOXING_DAY, CARNIVAL, CHINESE_NEW_YEAR, CHRISTMAS, CYBER_MONDAY, DIWALI, EASTER, EID_AL_ADHA, EID_AL_FITR, END_OF_SEASON, EPIPHANY, FALL_SALE, FATHERS_DAY, HALLOWEEN, HANUKKAH, HOLI, INDEPENDENCE_DAY, LABOR_DAY, MOTHERS_DAY, NATIONAL_DAY, NAVRATRI, NEW_YEARS, PARENTS_DAY, PASSOVER, RAMADAN, ROSH_HASHANAH, SINGLES_DAY, SONGKRAN, SPRING_SALE, ST_NICHOLAS_DAY, SUMMER_SALE, VALENTINES_DAY, WINTER_SALE, WOMENS_DAY, YEAR_END_GIFT.\n                - url: landing page for the promotion.\n                - item: what's on sale (1-20 chars). E.g. \"Pro subscription\".\n                - category_type: discount style.\n                    * MONETARY        → \"$X off\". Provide category_amount.\n                    * UP_TO_MONETARY  → \"Up to $X off\". Provide category_amount.\n                    * PERCENT         → \"X% off\". Provide category_percent (≤100).\n                    * UP_TO_PERCENT   → \"Up to X% off\". Provide category_percent (≤100).\n                - details_type: optional qualifier.\n                    * NONE            → no qualifier.\n                    * ON_ORDERS_OVER  → \"on orders over $X\". Provide details_amount.\n                    * PROMO_CODE      → \"with code XXXX\". Provide details_promo_code (≤15 chars).\n                - start_date / end_date: ISO-8601 in pattern \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\" (e.g. 2026-11-25T00:00:00.000Z). Both must be today or in the future.\n\n                INPUTS (optional, see category_type / details_type above):\n                - category_amount, category_percent, details_promo_code, details_amount.\n\n                EXAMPLE — Black Friday 20% off Pro with promo code:\n                create_microsoft_promotion_extension(\n                    occasion=\"BLACK_FRIDAY\",\n                    url=\"https://example.com/sale\",\n                    item=\"Pro subscription\",\n                    category_type=\"PERCENT\",   category_percent=20,\n                    details_type=\"PROMO_CODE\", details_promo_code=\"BF2026\",\n                    start_date=\"2026-11-25T00:00:00.000Z\",\n                    end_date=\"2026-11-30T23:59:59.000Z\",\n                )",
   "inputSchema": {
    "type": "object",
    "properties": {
     "occasion": {
      "type": "string",
      "enum": [
       "NONE",
       "BACK_TO_SCHOOL",
       "BLACK_FRIDAY",
       "BOXING_DAY",
       "CARNIVAL",
       "CHINESE_NEW_YEAR",
       "CHRISTMAS",
       "CYBER_MONDAY",
       "DIWALI",
       "EASTER",
       "EID_AL_ADHA",
       "EID_AL_FITR",
       "END_OF_SEASON",
       "EPIPHANY",
       "FALL_SALE",
       "FATHERS_DAY",
       "HALLOWEEN",
       "HANUKKAH",
       "HOLI",
       "INDEPENDENCE_DAY",
       "LABOR_DAY",
       "MOTHERS_DAY",
       "NATIONAL_DAY",
       "NAVRATRI",
       "NEW_YEARS",
       "PARENTS_DAY",
       "PASSOVER",
       "RAMADAN",
       "ROSH_HASHANAH",
       "SINGLES_DAY",
       "SONGKRAN",
       "SPRING_SALE",
       "ST_NICHOLAS_DAY",
       "SUMMER_SALE",
       "VALENTINES_DAY",
       "WINTER_SALE",
       "WOMENS_DAY",
       "YEAR_END_GIFT"
      ]
     },
     "url": {
      "type": "string",
      "minLength": 1
     },
     "item": {
      "type": "string",
      "minLength": 1,
      "maxLength": 20
     },
     "category_type": {
      "type": "string",
      "enum": [
       "MONETARY",
       "UP_TO_MONETARY",
       "PERCENT",
       "UP_TO_PERCENT"
      ]
     },
     "category_amount": {
      "type": "number",
      "exclusiveMinimum": 0,
      "description": "USD amount; supply when category_type is MONETARY or UP_TO_MONETARY."
     },
     "category_percent": {
      "type": "number",
      "exclusiveMinimum": 0,
      "maximum": 100,
      "description": "Percent (≤100); supply when category_type is PERCENT or UP_TO_PERCENT."
     },
     "details_type": {
      "type": "string",
      "enum": [
       "NONE",
       "ON_ORDERS_OVER",
       "PROMO_CODE"
      ]
     },
     "details_promo_code": {
      "type": "string",
      "maxLength": 15,
      "description": "Promo code; supply when details_type is PROMO_CODE."
     },
     "details_amount": {
      "type": "number",
      "exclusiveMinimum": 0,
      "description": "Threshold USD amount; supply when details_type is ON_ORDERS_OVER."
     },
     "start_date": {
      "type": "string",
      "description": "ISO-8601 \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\". Future or present."
     },
     "end_date": {
      "type": "string",
      "description": "ISO-8601 \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\". Future or present."
     }
    },
    "required": [
     "occasion",
     "url",
     "item",
     "category_type",
     "details_type",
     "start_date",
     "end_date"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_microsoft_sitelink_extension",
   "title": "Create Microsoft Ads Sitelink Extension",
   "category": "extensions",
   "risk": "write",
   "summary": "Add an extra clickable shortcut (sitelink) that appears beneath a Microsoft Ads (Bing) ad — helpful deep-links like \"See Pricing\", \"Book a Demo\", or \"Contact Sales\" with two sho…",
   "description": "Add an extra clickable shortcut (sitelink) that appears beneath a Microsoft Ads (Bing) ad — helpful deep-links like \"See Pricing\", \"Book a Demo\", or \"Contact Sales\" with two short description lines. Sitelinks lift click-through rate by giving searchers alternate landing paths to the same advertiser.\n\n                ALSO KNOWN AS: sitelink, site link, extra link, ad link, secondary link, deep link, additional link, jump link\n\n                KEYWORDS: sitelink, site link, link, deep link, shortcut, microsoft ads, bing, extension, pricing link, demo link, contact link, landing page, CTR, click through\n\n                WHEN TO USE:\n                - \"Add a sitelink to our Microsoft Ads / Bing library\"\n                - \"Create a 'See Pricing' / 'Book a Demo' / 'Contact Sales' link extension for Bing\"\n                - \"Register a new Microsoft Ads sitelink for the homepage redesign\"\n\n                WHEN NOT TO USE:\n                - Want a non-clickable selling-point snippet (\"Free Shipping\", \"24/7 Support\") → use create_microsoft_callout_extension\n                - Want a labeled list of offerings (e.g. \"Brands: Nest, Nexus\") → use create_microsoft_structured_snippet_extension\n                - Same sitelink for Google Ads → use create_google_sitelink_extension (library entries are per-channel)\n                - Anything for Facebook / LinkedIn / Reddit — ad extensions are a search-channel-only concept\n\n                INPUTS (all required):\n                - url:          destination the sitelink opens.\n                - link_text:    visible link text (1-25 chars).\n                - description1: first description line under the link (1-35 chars).\n                - description2: second description line (1-35 chars).\n\n                EXAMPLE:\n                create_microsoft_sitelink_extension(\n                    url=\"https://example.com/pricing\",\n                    link_text=\"See Pricing\",\n                    description1=\"Plans for every team size\",\n                    description2=\"Start free, upgrade anytime\",\n                )\n\n                Returns the persisted extension (id, externalId, type=SITELINK, channel=MICROSOFT_ADS). A 400 usually means a character-limit was exceeded.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "url": {
      "type": "string",
      "minLength": 1,
      "description": "Destination URL the sitelink points to (non-blank)."
     },
     "link_text": {
      "type": "string",
      "minLength": 1,
      "maxLength": 25,
      "description": "Visible link text. 1-25 characters."
     },
     "description1": {
      "type": "string",
      "minLength": 1,
      "maxLength": 35,
      "description": "First description line. 1-35 characters."
     },
     "description2": {
      "type": "string",
      "minLength": 1,
      "maxLength": 35,
      "description": "Second description line. 1-35 characters."
     }
    },
    "required": [
     "url",
     "link_text",
     "description1",
     "description2"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_microsoft_structured_snippet_extension",
   "title": "Create Microsoft Ads Structured Snippet Extension",
   "category": "extensions",
   "risk": "write",
   "summary": "Add a labeled list of offerings (structured snippet) shown beneath a Microsoft Ads (Bing) ad — a category header plus 3-10 short items, e.g.",
   "description": "Add a labeled list of offerings (structured snippet) shown beneath a Microsoft Ads (Bing) ad — a category header plus 3-10 short items, e.g. header \"Brands\" with values \"Nest\", \"Nexus\", \"Chromebook\", or header \"Services\" with \"Hosting\", \"DNS\", \"CDN\". Helps searchers self-qualify before clicking.\n\n                ALSO KNOWN AS: structured snippet, snippet, category list, offering list, header values, product list, service list\n\n                KEYWORDS: structured snippet, snippet, header, values, brands, services, products, courses, destinations, list, category, microsoft ads, bing, extension\n\n                WHEN TO USE:\n                - \"Add a structured snippet listing our service tiers on Microsoft Ads\"\n                - \"Create a Bing snippet under 'Brands' with Nest, Nexus, Chromebook\"\n                - \"Register a list of supported integrations as a Microsoft Ads snippet extension\"\n\n                WHEN NOT TO USE:\n                - Want a single phrase (e.g. \"Free Shipping\") → use create_microsoft_callout_extension\n                - Want clickable links to subpages → use create_microsoft_sitelink_extension\n                - Want a pricing table (with amounts) → use create_microsoft_price_extension\n                - Same snippet for Google Ads → use create_google_structured_snippet_extension\n\n                INPUTS:\n                - header: snippet category title (1-25 chars). Microsoft Ads mirrors Google's predefined header vocabulary (\"Brands\", \"Models\", \"Service catalog\", \"Insurance coverage\", etc.) — pass the chosen header string here.\n                - values: 3-10 items, each 1-25 chars.\n\n                EXAMPLE:\n                create_microsoft_structured_snippet_extension(\n                    header=\"Brands\",\n                    values=[\"Nest\", \"Nexus\", \"Chromebook\"],\n                )",
   "inputSchema": {
    "type": "object",
    "properties": {
     "header": {
      "type": "string",
      "minLength": 1,
      "maxLength": 25,
      "description": "Snippet header. 1-25 characters."
     },
     "values": {
      "type": "array",
      "minItems": 3,
      "maxItems": 10,
      "items": {
       "type": "string",
       "minLength": 1,
       "maxLength": 25
      },
      "description": "3-10 snippet values, each 1-25 characters."
     }
    },
    "required": [
     "header",
     "values"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "list_extensions",
   "title": "List Search Ad Extensions",
   "category": "extensions",
   "risk": "read",
   "summary": "List the account's Google Ads / Microsoft Ads library extensions (sitelinks, callouts, snippets, etc.) with their internal ids.",
   "description": "List the account's Google Ads / Microsoft Ads library extensions (sitelinks, callouts, snippets, etc.) with their internal ids.\n\n                ALSO KNOWN AS: list extensions, show extensions, find sitelink, which extensions do we have, extension library, browse extensions\n\n                KEYWORDS: list, show, find, browse, extensions, sitelink, callout, library, google ads, microsoft ads, bing, id\n\n                WHEN TO USE:\n                - \"What sitelinks do we have?\" / \"Show our Google Ads extensions\"\n                - ALWAYS before add_extensions_to_native_campaign or remove_extensions_from_native_campaign — those need internal ids, and this is the only tool that returns them. The create_*_extension tools return an empty body and cannot tell you the new id.\n\n                WHEN NOT TO USE:\n                - Creating a new extension → use the relevant create_*_extension tool\n                - Non-search channels (Facebook / LinkedIn / Reddit) — extensions do not exist there\n\n                INPUTS:\n                - channel (required): GOOGLE_ADS or MICROSOFT_ADS\n                - types (optional):   filter, e.g. [\"SITELINK\"]\n                - name (optional):    name filter. Extension names are AUTO-DERIVED, not user-chosen — a sitelink's name is its link text and both description lines joined by newlines, truncated to 50 chars. Filter by `types` and match on the returned fields instead of guessing a name.\n                - page / size (optional): pagination, defaults 0 / 50\n\n                Returns {totalElements, totalPages, data:[{id, name, type, createdDate, ...}]}. Only campaign-attachable extensions are listed (account-level ones are excluded by the platform).",
   "inputSchema": {
    "type": "object",
    "properties": {
     "channel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS",
       "MICROSOFT_ADS"
      ],
      "description": "Search channel. Ad extensions are a search-channel-only concept."
     },
     "types": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "SITELINK",
        "CALLOUT",
        "STRUCTURED_SNIPPET",
        "PRICE",
        "PROMOTION",
        "LEAD_FORM",
        "AD_IMAGE",
        "BUSINESS_LOGO",
        "BUSINESS_NAME"
       ]
      },
      "description": "Optional extension-type filter, e.g. [\"SITELINK\"]."
     },
     "name": {
      "type": "string",
      "description": "Optional name filter. Names are auto-derived by the platform, not user-chosen, so prefer filtering by `types`."
     },
     "page": {
      "type": "integer",
      "minimum": 0,
      "description": "0-based page index. Defaults to 0."
     },
     "size": {
      "type": "integer",
      "minimum": 1,
      "description": "Page size. Defaults to 50."
     }
    },
    "required": [
     "channel"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "remove_extensions_from_native_campaign",
   "title": "Remove Extensions from Native-Structure Campaign",
   "category": "extensions",
   "risk": "destructive",
   "summary": "Detach extensions from a native-structure Google Ads / Microsoft Ads campaign.",
   "description": "Detach extensions from a native-structure Google Ads / Microsoft Ads campaign. The extension stays in the account library — only the campaign binding is removed.\n\n                ALSO KNOWN AS: remove extension from campaign, detach sitelink, unlink extension, take sitelink off campaign\n\n                KEYWORDS: remove, detach, unlink, unbind, extension, sitelink, callout, campaign, native, native structure, google ads, microsoft ads, bing\n\n                WHEN TO USE:\n                - \"Remove that sitelink from campaign X\"\n                - \"Take the old promotion extension off the native search campaign\"\n                - Undoing a wrong add_extensions_to_native_campaign call\n\n                WHEN NOT TO USE:\n                - Deleting the extension from the library entirely — this only unbinds it from one campaign\n                - The campaign uses the Precision / METADATA structure — native-structure campaigns only\n\n                INPUTS (all required):\n                - campaign_id:    wizard campaign id\n                - channel:        GOOGLE_ADS or MICROSOFT_ADS\n                - extension_ids:  internal ids from list_extensions\n\n                BEHAVIOUR: only the named ids are detached; other attachments are preserved. Safe to re-run.\n\n                EXAMPLE:\n                remove_extensions_from_native_campaign(campaign_id=8421, channel=\"GOOGLE_ADS\", extension_ids=[551])",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The wizard campaign id."
     },
     "channel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS",
       "MICROSOFT_ADS"
      ],
      "description": "Search channel. Ad extensions are a search-channel-only concept."
     },
     "extension_ids": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "minItems": 1,
      "description": "Internal library extension ids from list_extensions. These are the `id` field, NOT `externalId` (the ad platform's own id)."
     }
    },
    "required": [
     "campaign_id",
     "channel",
     "extension_ids"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "add_keywords_to_negative_lists",
   "title": "Add Keywords to Negative Lists",
   "category": "search",
   "risk": "write",
   "summary": "Add keywords to one or more existing Google Ads negative keyword lists.",
   "description": "Add keywords to one or more existing Google Ads negative keyword lists.\n\n                PURPOSE:\n                Append keywords (typically search terms surfaced by list_search_terms) to one or\n                more existing negative keyword lists, so future ads are blocked from matching them.\n                Use this for the day-to-day \"review search terms, exclude the wasteful ones\" loop.\n\n                WHEN TO USE:\n                - Block search terms that consumed budget without producing clicks or conversions\n                - Extend an existing \"Keywords we don't wanna spend money on\" list with new offenders\n                - Apply the same keywords to multiple negative lists in a single call\n\n                THIS IS NOT FOR CREATING A NEW LIST:\n                To create a brand-new negative keyword list, use create_negative_keywords_list.\n                This tool only appends to lists that already exist.\n\n                LOOKING UP LIST IDS:\n                Call list_negative_keywords_list first to find the id(s) of the list(s) you want\n                to extend. The 'id' field returned there is what you pass here as negativeListIds.\n\n                MATCH TYPE:\n                Two ways to set the match type, pick whichever fits:\n\n                (a) Single match type for the whole batch: pass keywords as plain strings\n                    and set the top-level match_type. Defaults to PHRASE (the same default\n                    the UI uses when adding from the search-terms page).\n\n                (b) Mixed match types in a single call (preferred when recommending negatives\n                    after reviewing search terms, since it keeps the whole batch in one call):\n                    pass keywords as objects {text, match_type}. The per-item match_type wins\n                    over the top-level default. Items without a match_type fall back to the\n                    top-level default.\n\n                Only PHRASE, BROAD, and EXACT are valid for negative keywords.\n\n                COMMON USE CASES:\n                1. Add a single wasteful search term to one list (all-PHRASE):\n                   add_keywords_to_negative_lists(\n                     negativeListIds=[43256533],\n                     keywords=[\"free download\"],\n                   )\n\n                2. Add several terms to multiple lists, all exact-match:\n                   add_keywords_to_negative_lists(\n                     negativeListIds=[43256533, 43258891],\n                     keywords=[\"competitor a\", \"competitor b\"],\n                     match_type=\"EXACT\",\n                   )\n\n                3. Mixed match types in ONE call (recommended after a search-terms review):\n                   add_keywords_to_negative_lists(\n                     negativeListIds=[43256533],\n                     keywords=[\n                       {\"text\": \"free download\", \"match_type\": \"PHRASE\"},\n                       {\"text\": \"competitor a\",  \"match_type\": \"EXACT\"},\n                       {\"text\": \"how to\",        \"match_type\": \"BROAD\"},\n                     ],\n                   )\n\n                4. Mostly PHRASE with one EXACT override (mix strings + objects):\n                   add_keywords_to_negative_lists(\n                     negativeListIds=[43256533],\n                     keywords=[\n                       \"free download\",\n                       \"tutorial\",\n                       {\"text\": \"competitor a\", \"match_type\": \"EXACT\"},\n                     ],\n                     match_type=\"PHRASE\",\n                   )\n\n                PARAMETERS:\n                - negativeListIds (array of integers, required): IDs of the existing negative\n                  keyword lists to extend, as returned by list_negative_keywords_list.\n                - keywords (array, required): each item is either a plain string (keyword text,\n                  uses the top-level match_type) or an object {text, match_type} where\n                  match_type overrides the top-level default. Min 1.\n                - match_type (string, optional): EXACT, PHRASE, or BROAD. Default PHRASE.\n                  Used as the default for plain-string items and for object items that omit\n                  match_type.\n\n                RESPONSE FORMAT:\n                Returns the platform response describing the updated list(s).\n\n                CHANNEL SCOPE:\n                BOTH search channels are supported: pass channel=MICROSOFT_ADS for Microsoft\n                Ads (Bing) and channel=GOOGLE_ADS for Google. The two channels hold SEPARATE\n                lists, so never substitute one channel for the other to get \"close enough\"\n                results.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "negativeListIds": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "minItems": 1,
      "description": "IDs of the existing negative keyword lists to extend (from list_negative_keywords_list)."
     },
     "keywords": {
      "type": "array",
      "items": {
       "anyOf": [
        {
         "type": "string",
         "description": "Plain keyword text. Uses the top-level match_type."
        },
        {
         "type": "object",
         "properties": {
          "text": {
           "type": "string",
           "description": "Keyword text."
          },
          "match_type": {
           "type": "string",
           "enum": [
            "EXACT",
            "PHRASE",
            "BROAD"
           ],
           "description": "Per-keyword match type. Overrides the top-level match_type."
          }
         },
         "required": [
          "text"
         ],
         "additionalProperties": false
        }
       ]
      },
      "minItems": 1,
      "description": "Keywords to add to every list in negativeListIds. Each item is either a plain string or {text, match_type}."
     },
     "match_type": {
      "type": "string",
      "enum": [
       "EXACT",
       "PHRASE",
       "BROAD"
      ],
      "default": "PHRASE",
      "description": "Default match type, used when an item is a plain string or an object that omits match_type. Default PHRASE."
     },
     "channel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS",
       "MICROSOFT_ADS"
      ],
      "default": "GOOGLE_ADS",
      "description": "Ad channel scope. Both search channels are supported. These are PER-CHANNEL resources: GOOGLE_ADS and MICROSOFT_ADS hold separate sets, and one is invisible to the other. Defaults to GOOGLE_ADS when the user does not say which."
     }
    },
    "required": [
     "negativeListIds",
     "keywords"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "add_negative_keyword_lists_to_live_campaigns",
   "title": "Add Negative Keyword Lists to Live Campaigns",
   "category": "search",
   "risk": "write",
   "summary": "Attach existing Google Ads negative keyword lists to campaigns that are ALREADY LAUNCHED — a live edit that syncs through to Google.",
   "description": "Attach existing Google Ads negative keyword lists to campaigns that are ALREADY LAUNCHED — a live edit that syncs through to Google.\n\n                🔴 LIVE EDIT vs DRAFT EDIT — THE DISTINCTION THAT PICKS THIS TOOL:\n                A campaign is a DRAFT until it is launched; every status after that\n                (Launched, Launching, Editing, Finished, Error, Failed) is LIVE. Editing a\n                live campaign is a different code path on the platform, not just a different\n                status — the change has to be published to the running Google campaign, not\n                only written to the wizard.\n                  • LIVE campaign (already launched) → THIS tool. It calls the platform's\n                    bulk live-edit endpoint, which publishes an update per running ad group\n                    so Google actually starts excluding the keywords.\n                  • DRAFT campaign (never launched) → the campaign-builder tools\n                    (`create_campaign` / `create_native_structure_campaign` /\n                    `add_and_edit_campaign_elements` / `add_and_edit_native_campaign_elements`,\n                    via their `negative_keyword_lists` field). Those write wizard state that\n                    gets pushed at launch.\n                Pass a draft campaign here and it is SKIPPED with a reason rather than\n                written — the draft path has different, destructive semantics, so this tool\n                refuses to take it. If you are unsure which a campaign is, just call this\n                tool: the per-campaign report tells you the status and which path applies.\n\n                WORKS ON BOTH CAMPAIGN STRUCTURES. Negative keyword lists attach at the\n                CHANNEL level, not to an ad group / container, so Precision Optimization\n                (1×1×1) and Channel-First / Native (N×N×N) campaigns are handled identically.\n                You do NOT need to know the structure, and you must NOT reach for\n                `add_and_edit_native_campaign_elements` to do this on a live campaign — that\n                tool rebuilds containers, which the platform forbids once experiments are\n                launched.\n\n                BULK BY DESIGN: pass every campaign the user named in ONE call. The platform\n                endpoint takes a campaign list, and this tool groups them so campaigns\n                needing the same final set share a single request.\n\n                ADDS, DOES NOT REPLACE (default): the platform applies exactly the set of\n                lists it is handed, so `mode=\"add\"` first reads what each campaign already\n                has and submits the union — attaching one list never silently detaches the\n                others. Use `mode=\"replace\"` ONLY when the user explicitly wants the\n                campaign's negative lists to become exactly the ones named; the response's\n                `detached` field reports what that removed.\n\n                FINDING LISTS: pass `listNames` (what the user says) and/or `listIds` (from\n                `list_negative_keywords_list`). Names must match an existing list EXACTLY\n                (case-insensitive) — a name that matches nothing is reported in `warnings`\n                and skipped, never swapped for a similar list. To attach a list that does not\n                exist yet, create it first with `create_negative_keywords_list`.\n\n                COMMON USE CASES:\n                1. Add one list to a running campaign:\n                   add_negative_keyword_lists_to_live_campaigns(campaignIds=[159490], listNames=[\"Competitor Brands\"])\n\n                2. Same lists across several live campaigns, one call:\n                   add_negative_keyword_lists_to_live_campaigns(campaignIds=[159490, 159502], listNames=[\"Job Seekers\", \"Free Content\"])\n\n                3. Make a campaign's negatives exactly one list (removes any others):\n                   add_negative_keyword_lists_to_live_campaigns(campaignIds=[159490], listIds=[43256533], mode=\"replace\")\n\n                RESPONSE FORMAT:\n                Per campaign: status, `edit_path` (\"live\" / \"draft\"), `attached_before`,\n                `newly_attached`, `already_attached`, `detached`, `attached_after`, and an\n                `action` of submitted / no_change / skipped / failed. Top level carries\n                `submitted_campaign_ids`, `warnings`, `errors`, and `delivery_note`.\n\n                ⚠️ ASYNC — REPORT IT HONESTLY: the platform accepts this in the background\n                and answers with no body. `action: \"submitted\"` means ACCEPTED, not applied.\n                It also drops campaigns server-side WITHOUT an error when they are locked by\n                another pending change or when a launched campaign is not currently\n                active/paused (e.g. completed). Tell the user the change was submitted and\n                that it lands on Google shortly — do not claim it is live, and if they need\n                certainty, re-check the campaign a little later.\n\n                CHANNEL SCOPE:\n                GOOGLE_ADS only — the platform endpoint is hardcoded to the Google Ads\n                channel. It does NOT work for Microsoft Ads (Bing) even though Microsoft has\n                the same negative-list concept; if the user asks for Microsoft, say this is\n                not supported yet rather than calling with channel=GOOGLE_ADS.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaignIds": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "minItems": 1,
      "description": "Wizard campaign ids of the LIVE (already launched) campaigns to update. Pass every campaign the user named in one call. Draft campaigns are skipped with a reason."
     },
     "listNames": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Names of existing negative keyword lists to attach. Matched exactly (case-insensitive) against the account library; an unmatched name is reported, never substituted."
     },
     "listIds": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "IDs of existing negative keyword lists to attach, as returned by list_negative_keywords_list. Can be combined with listNames."
     },
     "mode": {
      "type": "string",
      "enum": [
       "add",
       "replace"
      ],
      "default": "add",
      "description": "'add' (default) merges with the lists already on the campaign. 'replace' makes the campaign's negative lists exactly the ones requested, detaching the rest — only for an explicit user request."
     },
     "channel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS"
      ],
      "default": "GOOGLE_ADS",
      "description": "Ad channel scope. Only GOOGLE_ADS is supported — the platform's live-edit endpoint is Google-only."
     }
    },
    "required": [
     "campaignIds"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_keywords",
   "title": "Create Keywords",
   "category": "search",
   "risk": "write",
   "summary": "Create one or more positive keywords in the Metadata platform library.",
   "description": "Create one or more positive keywords in the Metadata platform library.\n\n                PURPOSE:\n                Batch create positive keywords that can be used in advertising campaigns. Use this tool to\n                expand your keyword library by adding new terms, variations, or related keywords for targeting\n                and campaign optimization.\n\n                WHEN TO USE:\n                - Add new keywords to your campaign library\n                - Build keyword lists for new product lines or services\n                - Create variations of existing keywords\n                - Expand keyword coverage for targeted advertising\n                - Create keyword groups for seasonal campaigns\n                - Add long-tail keywords for niche targeting\n                - Build negative keyword lists (blocked keywords)\n                - Bulk import keyword lists from external sources\n\n                KEY FEATURES:\n                ✓ BATCH CREATION: Create multiple keywords in a single API request\n                ✓ SIMPLE FORMAT: Each keyword is simply a string\n                ✓ FLEXIBLE: Create 1 or 100+ keywords at once\n                ✓ LIBRARY INTEGRATION: Keywords are added to your keyword library and available for campaigns\n\n                KEYWORD FORMAT:\n                Each keyword in the batch should be a simple string:\n                - \"digital marketing\"\n                - \"seo services\"\n                - \"ppc advertising\"\n\n                BATCH SIZE RECOMMENDATIONS:\n                - Small batches (5-50 keywords): Ideal for testing and quick additions\n                - Medium batches (50-500 keywords): Good for standard imports\n                - Large batches (500+ keywords): Use for bulk library imports\n                - Maximum recommended: 1000 keywords per request for optimal performance\n\n                KEYWORD NAMING CONVENTIONS:\n                - Use lowercase for consistency with platform standards\n                - Separate multi-word phrases (e.g., \"digital marketing\" not \"DigitalMarketing\")\n                - Avoid special characters unless necessary for the keyword\n                - Remove leading/trailing whitespace - the API will normalize\n                - Empty strings are allowed but won't create keywords\n                - Duplicate names in the batch will be deduplicated by the API\n\n                COMMON USE CASES:\n                1. Create a single keyword:\n                   create_keywords(keywords=[\"marketing\"])\n\n                2. Create multiple related keywords:\n                   create_keywords(keywords=[\n                     \"digital marketing\",\n                     \"marketing automation\",\n                     \"email marketing\"\n                   ])\n\n                3. Create keyword variations:\n                   create_keywords(keywords=[\n                     \"seo\",\n                     \"seo services\",\n                     \"seo tools\",\n                     \"seo optimization\"\n                   ])\n\n                4. Create long-tail keywords:\n                   create_keywords(keywords=[\n                     \"best digital marketing agency in new york\",\n                     \"affordable seo services for small business\",\n                     \"enterprise marketing automation platform\"\n                   ])\n\n                5. Bulk import from external list:\n                   create_keywords(keywords=[\n                     \"keyword1\",\n                     \"keyword2\",\n                     \"keyword3\",\n                     ... (repeat for all keywords)\n                   ])\n\n                6. Create campaign-specific keywords:\n                   create_keywords(keywords=[\n                     \"black friday deals\",\n                     \"cyber monday sales\",\n                     \"holiday promotions\"\n                   ])\n\n                7. Create product-related keywords:\n                   create_keywords(keywords=[\n                     \"product a\",\n                     \"product a review\",\n                     \"buy product a\",\n                     \"product a price\"\n                   ])\n\n                PARAMETERS:\n                - keywords: Required. Array of keyword strings to create.\n                           Example: [\"goku\", \"ssj\", \"ssj1\"]\n                           - Each keyword is a string\n                           - Minimum: 1 keyword\n                           - Recommended maximum: 1000 keywords per request\n                           - Empty strings are allowed but won't create keywords\n                           - Duplicates will be deduplicated\n\n                RESPONSE FORMAT:\n                Returns a response containing:\n                - Created keywords with their IDs and metadata\n                - Success/failure status for each keyword\n                - Any validation errors for invalid entries\n                - Confirmation of keywords added to the library\n\n                VALIDATION NOTES:\n                - Empty strings or null values in the batch are skipped\n                - Platform automatically removes duplicate names in the batch\n                - Keywords are case-insensitive for deduplication\n                - Special characters are preserved as provided\n                - Platform validates keyword length (typically 2-100 characters recommended)\n\n                PERFORMANCE TIPS:\n                - Keep batch sizes under 1000 keywords for faster processing\n                - Group related keywords together for better organization\n                - Review the response to confirm all keywords were created successfully\n                - Check your keyword library after creation to verify the additions\n\n                EXAMPLES:\n                - create_keywords(keywords=[\"marketing\"]) - Create single keyword\n                - create_keywords(keywords=[\"seo\", \"sem\", \"ppc\"]) - Create multiple keywords\n                - create_keywords(keywords=[\"digital marketing\", \"online marketing\", \"internet marketing\"]) - Variations\n                - create_keywords(keywords=[\"goku\", \"ssj\", \"ssj1\", \"ssj2\"]) - Batch creation",
   "inputSchema": {
    "type": "object",
    "properties": {
     "keywords": {
      "type": "array",
      "items": {
       "type": "string",
       "description": "A keyword string to create. Can be single or multi-word. Empty strings are allowed but won't create keywords."
      },
      "minItems": 1,
      "description": "Array of keyword strings to create. Example: [\"goku\", \"ssj\", \"ssj1\"]"
     }
    },
    "required": [
     "keywords"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "create_negative_keywords_list",
   "title": "Create Negative Keywords List",
   "category": "search",
   "risk": "write",
   "summary": "Create a new negative keywords list in the Metadata platform with Google Ads integration.",
   "description": "Create a new negative keywords list in the Metadata platform with Google Ads integration.\n\n                PURPOSE:\n                Batch create a negative keywords list that can be used in Google Ads campaigns to exclude specific search terms\n                from triggering your ads. Use this tool to build negative keyword exclusion lists for campaign optimization\n                and to prevent wasted ad spend on irrelevant searches.\n\n                ACCOUNT LIMIT:\n                ⚠️ MAXIMUM 20 NEGATIVE KEYWORDS LISTS PER ACCOUNT ⚠️\n                Users can have a maximum of 20 negative keyword lists. The system will validate this limit\n                before creating a new list and return an error if the limit has been reached.\n                To create a new list, you must delete or archive existing lists to free up space.\n\n                WHEN TO USE:\n                - Create a new negative keywords list for Google Ads campaigns\n                - Build competitor blocking lists (block competitor brand names)\n                - Create category exclusions (block unwanted product categories)\n                - Create seasonal exclusion lists (block irrelevant seasonal terms)\n                - Build intent-based negative lists (block non-commercial searches)\n                - Prevent wasted spend on irrelevant keywords\n                - Manage brand protection by blocking misused terms\n\n                KEY FEATURES:\n                ✓ BATCH CREATION: Create one or more negative keywords in a single request\n                ✓ MATCH TYPE CONTROL: Specify match type for all keywords (EXACT, PHRASE, BROAD)\n                ✓ FLEXIBLE NAMING: Custom names for organizing negative keyword lists\n                ✓ GOOGLE ADS INTEGRATION: Lists are created ready for use in Google Ads campaigns\n                ✓ SIMPLE INTERFACE: Just provide keywords as strings, API handles formatting\n                ✓ AUTOMATIC VALIDATION: System checks against 20-list limit before creation\n\n                KEYWORD MATCH TYPES:\n                The 'matchType' parameter controls how broadly the negative keyword blocks ads:\n\n                1. EXACT (\"Exact Match\"):\n                   - Blocks ads only for searches that exactly match the keyword\n                   - Most restrictive and precise blocking\n                   - Example: Keyword \"free\" with EXACT blocks only searches for \"free\", not \"free shipping\"\n                   - Best for: High-value keyword protection, brand safety\n\n                2. PHRASE (\"Phrase Match\"):\n                   - Blocks ads when the keyword appears as a phrase within the search\n                   - Moderate blocking scope\n                   - Example: Keyword \"vintage furniture\" with PHRASE blocks \"vintage furniture sale\" but not \"furniture vintage\"\n                   - Best for: Competitive category blocking\n\n                3. BROAD (\"Broad Match\"):\n                   - Blocks ads for searches containing any of the keyword terms\n                   - Least restrictive and broadest blocking\n                   - Example: Keyword \"free\" with BROAD blocks any search containing \"free\"\n                   - Best for: General category exclusions\n\n                MATCH TYPE SELECTION GUIDE:\n                - Use EXACT for specific brand names or exact competitor terms\n                - Use PHRASE for specific multi-word concepts or phrases\n                - Use BROAD for general categories or very broad exclusions\n\n                LIST NAMING CONVENTIONS:\n                - Use descriptive names that indicate the list's purpose\n                - Examples: \"Competitor Brands\", \"Low Intent Keywords\", \"Seasonal Exclusions\"\n                - Avoid special characters - use hyphens or underscores for clarity\n                - Keep names concise and meaningful\n\n                BATCH SIZE RECOMMENDATIONS:\n                - Small batches (1-10 keywords): For targeted blocking\n                - Medium batches (10-50 keywords): For category-based lists\n                - Large batches (50+ keywords): For comprehensive blocking lists\n                - Maximum recommended: 100+ keywords per list\n\n                NEGATIVE KEYWORDS VS POSITIVE KEYWORDS:\n                - Positive keywords: Terms you WANT to show ads for (create_keywords)\n                - Negative keywords: Terms you DON'T want to show ads for (create_negative_keywords_list)\n                - Use together for optimal campaign performance\n\n                COMMON USE CASES:\n                1. Create competitor blocking list (EXACT match):\n                   create_negative_keywords_list(\n                     name=\"Competitor Brands\",\n                     keywords=[\"competitor a\", \"competitor b\"],\n                     match_type=\"EXACT\"\n                   )\n\n                2. Create low-intent keyword list (BROAD match):\n                   create_negative_keywords_list(\n                     name=\"Low Intent Keywords\",\n                     keywords=[\"free\", \"how to\", \"tutorial\"],\n                     match_type=\"BROAD\"\n                   )\n\n                3. Create job seeker exclusion list (PHRASE match):\n                   create_negative_keywords_list(\n                     name=\"Job Seekers\",\n                     keywords=[\"job search\", \"hiring\", \"career\"],\n                     match_type=\"PHRASE\"\n                   )\n\n                4. Create seasonal exclusion list:\n                   create_negative_keywords_list(\n                     name=\"Summer Sale Exclusions\",\n                     keywords=[\"winter coats\", \"snow gear\"],\n                     match_type=\"PHRASE\"\n                   )\n\n                5. Create product category exclusions:\n                   create_negative_keywords_list(\n                     name=\"Non-Ecommerce\",\n                     keywords=[\"news\", \"forum\", \"wikipedia\"],\n                     match_type=\"EXACT\"\n                   )\n\n                PARAMETERS:\n                - name: Required. Name of the negative keywords list (string)\n                        Example: \"Competitor Brands\", \"Low Intent Keywords\"\n                        Use descriptive, meaningful names for easy identification\n\n                - keywords: Required. Array of keyword strings to block\n                           Example: [\"raichu\", \"blastoise\", \"charizard\"]\n                           - Minimum: 1 keyword\n                           - Recommended maximum: 100+ keywords per request\n                           - Each item is a simple string representing the keyword\n\n                - matchType: Required. Match type for all keywords - EXACT, PHRASE, or BROAD\n                            Example: \"EXACT\"\n                            Options: EXACT (exact match), PHRASE (phrase match), BROAD (broad match)\n\n                RESPONSE FORMAT:\n                Returns a response containing:\n                - Confirmation of the negative keywords list creation\n                - List ID for future reference and management\n                - Success/failure status\n                - Details of keywords added to the list\n\n                VALIDATION NOTES:\n                - Name is required and must be a non-empty string\n                - Keywords array must have at least 1 keyword\n                - matchType must be exactly one of: EXACT, PHRASE, BROAD (case-sensitive)\n                - Empty strings in keyword list are not allowed\n                - Duplicate keywords in the batch will be handled by the API\n\n                PERFORMANCE TIPS:\n                - Create lists with related keywords for better organization\n                - Group keywords by blocking strategy (competitor, seasonal, intent-based)\n                - Use EXACT match for high-value keyword protection\n                - Use BROAD match for general category exclusions\n                - Review created lists regularly and update as needed\n                - Can use list_negative_keywords_list() to verify creation\n\n                INTEGRATION WITH OTHER TOOLS:\n                - Use list_negative_keywords_list() to find and manage existing lists\n                - Use create_keywords() for positive keyword lists\n                - Combine with campaign creation for comprehensive keyword strategies\n\n                EXAMPLES:\n                - create_negative_keywords_list(name=\"Competitors\", keywords=[\"competitor1\"], matchType=\"EXACT\")\n                - create_negative_keywords_list(name=\"Free Content\", keywords=[\"free\", \"no cost\"], matchType=\"BROAD\")\n                - create_negative_keywords_list(name=\"Brand Safety\", keywords=[\"controversial term\"], matchType=\"PHRASE\")\n\n                CHANNEL SCOPE:\n                BOTH search channels are supported: pass channel=MICROSOFT_ADS for Microsoft\n                Ads (Bing) and channel=GOOGLE_ADS for Google. The two channels hold SEPARATE\n                lists, so never substitute one channel for the other to get \"close enough\"\n                results.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "Name of the negative keywords list (string). Example: \"Competitor Brands\", \"Low Intent Keywords\""
     },
     "keywords": {
      "type": "array",
      "items": {
       "type": "string",
       "description": "A keyword string to block. Example: 'raichu', 'blastoise', 'free'"
      },
      "minItems": 1,
      "description": "Array of keyword strings to block. Example: [\"raichu\", \"blastoise\", \"charizard\"]"
     },
     "match_type": {
      "type": "string",
      "enum": [
       "EXACT",
       "PHRASE",
       "BROAD"
      ],
      "description": "Match type for all keywords. Options: EXACT (exact match), PHRASE (phrase match), BROAD (broad match). Example: 'EXACT'"
     },
     "channel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS",
       "MICROSOFT_ADS"
      ],
      "default": "GOOGLE_ADS",
      "description": "Ad channel scope. Both search channels are supported. These are PER-CHANNEL resources: GOOGLE_ADS and MICROSOFT_ADS hold separate sets, and one is invisible to the other. Defaults to GOOGLE_ADS when the user does not say which."
     }
    },
    "required": [
     "name",
     "keywords",
     "match_type"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "delete_negative_keyword_lists",
   "title": "Delete Negative Keyword Lists",
   "category": "search",
   "risk": "destructive",
   "summary": "Permanently delete one or more entire Google Ads negative keyword lists, only when the user has explicitly asked.",
   "description": "Permanently delete one or more entire Google Ads negative keyword lists, only when the user has explicitly asked.\n\n                PURPOSE:\n                Wipe negative keyword lists that the user has named for deletion. The platform\n                supports a multi-id delete in one HTTP call, so batch every list the user\n                identified into a single tool call instead of calling once per list.\n\n                ONLY CALL THIS TOOL WHEN THE USER HAS EXPLICITLY NAMED LISTS TO DELETE.\n                Examples that qualify:\n                - \"Delete the 'Old competitors' list\"\n                - \"Drop these three lists: A, B, C\"\n                - \"Yes, delete the one I just showed you\"\n                Examples that do NOT qualify:\n                - \"Create a new list called X\" — even if the 20-list account cap blocks the\n                  create call, do NOT pick a list and delete it. Surface the cap to the user\n                  (via list_negative_keywords_list so they see what's there), and ask which\n                  list(s) THEY want to delete before calling this tool.\n                - \"Clean up my lists\" — too vague; first list_negative_keywords_list, present\n                  the current set, and let the user pick which to drop.\n\n                WARNING:\n                This deletes the LIST itself (every keyword in it goes with it, and any\n                campaigns attaching the list lose the attachment). To remove specific\n                keywords inside a list without dropping the list, use\n                remove_keywords_from_negative_list instead.\n\n                LOOKING UP LIST IDS:\n                Call list_negative_keywords_list to map the names the user gave you to ids.\n                The 'id' field returned there is what you pass here.\n\n                COMMON USE CASES:\n                1. Delete a single list the user named:\n                   delete_negative_keyword_lists(listIds=[43256533])\n\n                2. Delete several at once (mirrors the UI's \"Select rows -> Delete\" action):\n                   delete_negative_keyword_lists(listIds=[43256533, 43258891, 43261107])\n\n                PARAMETERS:\n                - listIds (array of integers, required, min 1): IDs of the negative keyword\n                  lists the user asked to delete, as returned by list_negative_keywords_list.\n\n                RESPONSE FORMAT:\n                Returns the platform response. The channels service typically responds with\n                204 No Content on success, in which case the wrapper returns a code 204 +\n                Success envelope with the ids that were deleted.\n\n                CHANNEL SCOPE:\n                BOTH search channels are supported: pass channel=MICROSOFT_ADS for Microsoft\n                Ads (Bing) and channel=GOOGLE_ADS for Google. The two channels hold SEPARATE\n                lists, so never substitute one channel for the other to get \"close enough\"\n                results.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "listIds": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "minItems": 1,
      "description": "IDs of the negative keyword lists to delete (from list_negative_keywords_list)."
     },
     "channel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS",
       "MICROSOFT_ADS"
      ],
      "default": "GOOGLE_ADS",
      "description": "Ad channel scope. Both search channels are supported. These are PER-CHANNEL resources: GOOGLE_ADS and MICROSOFT_ADS hold separate sets, and one is invisible to the other. Defaults to GOOGLE_ADS when the user does not say which."
     }
    },
    "required": [
     "listIds"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "duplicate_negative_keywords_list",
   "title": "Duplicate Negative Keywords List Across Channels",
   "category": "search",
   "risk": "write",
   "summary": "Duplicate (copy) a negative keyword list from one search channel to the other: Google Ads to Microsoft Ads (Bing), or Microsoft Ads to Google Ads.",
   "description": "Duplicate (copy) a negative keyword list from one search channel to the other:\n                Google Ads to Microsoft Ads (Bing), or Microsoft Ads to Google Ads.\n\n                KEYWORDS: duplicate, copy, clone, replicate, negative keywords, Bing, Microsoft Ads, Google Ads\n\n                PURPOSE:\n                Users who run the same campaigns on Google and Bing keep the two negative keyword\n                libraries in sync by hand. This tool copies an existing negative keyword list to the\n                other search channel in one call: the platform reads the source list's keywords,\n                adapts match types for the target channel, and creates a new list there.\n\n                WHAT IT DOES (server-side, automatic):\n                - Reads every keyword in the source list.\n                - Remaps match types for the target: Microsoft Ads does not support BROAD negative\n                  keywords, so BROAD (and untyped) keywords become PHRASE when the target is\n                  Microsoft Ads. EXACT and PHRASE are kept as-is. Google-bound copies keep all\n                  match types unchanged.\n                - Creates a NEW list on the target channel. The source list is never modified.\n\n                DRY RUN (preview):\n                Call with dryRun=true first when the source list may contain BROAD keywords.\n                Nothing is created; the response reports keywordsCopied and broadRemappedToPhrase\n                so you can tell the user \"N broad keywords will become phrase match on Microsoft\"\n                and let them confirm. Then repeat the call with dryRun=false to actually create.\n\n                PARAMETERS:\n                - sourceListId: Required. The id of the negative keyword list to copy, as returned\n                  by list_negative_keywords_list.\n                - sourceChannel: Channel the source list lives on. Default GOOGLE_ADS.\n                - targetChannel: Channel to create the copy on. Default MICROSOFT_ADS.\n                  Must differ from sourceChannel - only cross-channel copies are supported.\n                - name: Optional name for the new list. When omitted the platform derives\n                  \"<source name> (<target>)\", e.g. \"Competitor Brands (Microsoft)\".\n                - dryRun: Optional, default false. When true, preview without creating.\n\n                RESPONSE FORMAT:\n                {\n                  \"id\": 456,                    // new list id on the target channel; null on dry run\n                  \"name\": \"Competitor Brands (Microsoft)\",\n                  \"keywordsCopied\": 32,\n                  \"broadRemappedToPhrase\": 5,   // 0 when target is Google Ads\n                  \"dryRun\": false\n                }\n\n                WORKFLOW:\n                1. Find the source list id with list_negative_keywords_list (or ask the user).\n                2. Optional: duplicate_negative_keywords_list(sourceListId=..., dryRun=true) to\n                   preview the match-type conversion and confirm with the user.\n                3. duplicate_negative_keywords_list(sourceListId=...) to create the copy.\n                4. The new list exists on the target channel; attach it to campaigns like any\n                   other negative keyword list.\n\n                ERROR CASES:\n                - Source list not found (or empty) on the source channel: the platform rejects the\n                  copy with a not-found error - re-check the id and sourceChannel.\n                - Same source and target channel: rejected; use the platform UI's same-channel\n                  duplicate instead.\n                - Accounts are capped at 20 negative keyword lists per channel; if the target\n                  channel is at the cap the creation fails - delete a list there first.\n\n                EXAMPLES:\n                - \"Copy my Google negative list to Bing\":\n                  duplicate_negative_keywords_list(sourceListId=123)\n                - Preview first:\n                  duplicate_negative_keywords_list(sourceListId=123, dryRun=true)\n                - Custom name, Microsoft to Google:\n                  duplicate_negative_keywords_list(sourceListId=77, sourceChannel=\"MICROSOFT_ADS\",\n                                                   targetChannel=\"GOOGLE_ADS\", name=\"Brand Safety (Google)\")\n\n                RELATED TOOLS:\n                - list_negative_keywords_list / get_negative_keywords_list_details: find the source\n                  list and inspect its keywords (currently Google Ads only).\n                - create_negative_keywords_list: build a brand-new list from scratch instead.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "sourceListId": {
      "type": "integer",
      "minimum": 1,
      "description": "Id of the negative keyword list to copy, as returned by list_negative_keywords_list."
     },
     "sourceChannel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS",
       "MICROSOFT_ADS"
      ],
      "default": "GOOGLE_ADS",
      "description": "Channel the source list lives on. Default GOOGLE_ADS."
     },
     "targetChannel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS",
       "MICROSOFT_ADS"
      ],
      "default": "MICROSOFT_ADS",
      "description": "Channel to create the copy on. Must differ from sourceChannel. Default MICROSOFT_ADS."
     },
     "name": {
      "type": "string",
      "description": "Optional name for the new list. Defaults to \"<source name> (<target>)\", e.g. \"Competitor Brands (Microsoft)\"."
     },
     "dryRun": {
      "type": "boolean",
      "default": false,
      "description": "When true, preview the copy (keyword count + BROAD-to-PHRASE conversions) without creating the list."
     }
    },
    "required": [
     "sourceListId"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "experiments_keywords_stats",
   "title": "Get Keyword Performance Statistics from Experiments",
   "category": "search",
   "risk": "read",
   "summary": "Keyword performance data from active experiments with date range filtering.",
   "description": "Keyword performance data from active experiments with date range filtering.\n\nUSE FOR: Keyword performance across experiments, Google Ads keyword metrics/trends, keyword ROI and conversions, search term effectiveness.\n\nREQUIRED: Date range in YYYY-MM-DD to YYYY-MM-DD format.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "startDate": {
      "type": "string",
      "description": "Start date in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). Example: '2025-01-01T00:00:00.000Z'"
     },
     "endDate": {
      "type": "string",
      "description": "End date in ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ). Example: '2025-12-31T23:59:59.999Z'"
     }
    },
    "required": [
     "startDate",
     "endDate"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "get_negative_keywords_list_details",
   "title": "Get Negative Keywords List Details",
   "category": "search",
   "risk": "read",
   "summary": "Fetch the keywords inside a single Google Ads negative keyword list.",
   "description": "Fetch the keywords inside a single Google Ads negative keyword list.\n\n                PURPOSE:\n                List_negative_keywords_list only returns each list's name + id; it does NOT\n                expose the individual keywords inside. Use this tool to see what's actually\n                in a given list (text, match type, resourceName) before deciding to add or\n                remove entries.\n\n                WHEN TO USE:\n                - Inspect what's already excluded before adding more negatives\n                - Filter by substring with the 'text' query parameter\n                - Look up an entry to confirm it exists before calling\n                  remove_keywords_from_negative_list (which matches by text internally)\n\n                RESPONSE FORMAT:\n                Returns the list payload:\n                {\n                  \"id\": 43256533,\n                  \"name\": \"Keywords we don't wanna spend money\",\n                  \"totalKeywords\": 87,\n                  \"totalPages\": 4,\n                  \"keywords\": [\n                    {\n                      \"resourceName\": \"customers/123/sharedCriteria/456~789\",\n                      \"text\": \"free download\",\n                      \"keywordMatchType\": \"PHRASE\"\n                    },\n                    ...\n                  ]\n                }\n\n                PARAMETERS:\n                - listId (integer, required): the id from list_negative_keywords_list.\n                - text (string, optional): substring filter on keyword text.\n                - page (integer, optional): 0-based page index. Default 0.\n                - size (integer, optional): rows per page. Default 25, max 100.\n                - sort (string, optional): \"<field>,<direction>\". Default \"text,asc\".\n\n                CHANNEL SCOPE:\n                BOTH search channels are supported: pass channel=MICROSOFT_ADS for Microsoft\n                Ads (Bing) and channel=GOOGLE_ADS for Google. The two channels hold SEPARATE\n                lists, so never substitute one channel for the other to get \"close enough\"\n                results.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "listId": {
      "type": "integer",
      "description": "ID of the negative keyword list (from list_negative_keywords_list)."
     },
     "text": {
      "type": "string",
      "description": "Optional substring filter on keyword text."
     },
     "page": {
      "type": "integer",
      "minimum": 0,
      "default": 0,
      "description": "0-based page index. Default 0."
     },
     "size": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100,
      "default": 25,
      "description": "Rows per page. Default 25, max 100."
     },
     "sort": {
      "type": "string",
      "default": "text,asc",
      "description": "Sort criteria in format '<field>,<direction>'. Default 'text,asc'."
     },
     "channel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS",
       "MICROSOFT_ADS"
      ],
      "default": "GOOGLE_ADS",
      "description": "Ad channel scope. Both search channels are supported. These are PER-CHANNEL resources: GOOGLE_ADS and MICROSOFT_ADS hold separate sets, and one is invisible to the other. Defaults to GOOGLE_ADS when the user does not say which."
     }
    },
    "required": [
     "listId"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "list_experiment_keywords",
   "title": "List Experiment Keywords",
   "category": "search",
   "risk": "read",
   "summary": "Search and browse keywords running in experiments with simplified response data.",
   "description": "Search and browse keywords running in experiments with simplified response data.\n\n                PURPOSE:\n                Retrieve a paginated list of keywords that are currently running in experiments,\n                with optional filtering by keyword or experiment name. Returns only essential fields\n                for quick identification: keyword name, keywordExternalId, wizExperimentId, wizExperimentName, and status.\n\n                WHEN TO USE:\n                - Find keywords running in a specific experiment\n                - Search for a keyword across all experiments\n                - Look up keywordExternalId and wizExperimentId before pausing a keyword\n                - Browse experiment keywords with pagination\n                - Verify the status of a keyword after calling pause_experiment_keyword\n                  (note: pausing is an async flow, so the status may take some time to update)\n\n                RESPONSE FORMAT:\n                Returns a simplified paginated response:\n                {\n                  \"totalElements\": 50,\n                  \"totalPages\": 5,\n                  \"data\": [\n                    {\n                      \"keyword\": \"digital marketing\",\n                      \"keywordExternalId\": 12345,\n                      \"wizExperimentId\": 678,\n                      \"wizExperimentName\": \"Q1 Campaign Test\",\n                      \"status\": \"ACTIVE\",\n                      \"primaryStatus\": \"NOT_ELIGIBLE\",\n                      \"primaryStatusReasons\": [\"CAMPAIGN_PAUSED\", \"CAMPAIGN_PENDING\"]\n                    }\n                  ]\n                }\n\n                COMMON USE CASES:\n                1. Search by experiment name:\n                   list_experiment_keywords(keywordOrExperimentName=\"Q1 Campaign\")\n\n                2. Search by keyword:\n                   list_experiment_keywords(keywordOrExperimentName=\"marketing\")\n\n                3. Paginate through results:\n                   list_experiment_keywords(keywordOrExperimentName=\"test\", page=0, size=20)\n                   list_experiment_keywords(keywordOrExperimentName=\"test\", page=1, size=20)\n\n                PARAMETERS:\n                - keywordOrExperimentName: Required. Search term to filter by keyword or experiment name.\n                - page: Page number for pagination (0-based, default: 0)\n                - size: Number of items per page (default: 10)\n\n                TIP: Use this tool first to find the keywordExternalId and wizExperimentId needed\n                by the pause_experiment_keyword tool.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "keywordOrExperimentName": {
      "type": "string",
      "description": "Search term to filter results by keyword name or experiment name."
     },
     "page": {
      "type": "integer",
      "minimum": 0,
      "default": 0,
      "description": "Page number for pagination (0-based). Default is 0."
     },
     "size": {
      "type": "integer",
      "minimum": 1,
      "default": 10,
      "description": "Number of items per page. Default is 10."
     }
    },
    "required": [
     "keywordOrExperimentName"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "list_keywords",
   "title": "List Keywords",
   "category": "search",
   "risk": "read",
   "summary": "List and search keywords with filtering, sorting, and pagination options.",
   "description": "List and search keywords with filtering, sorting, and pagination options.\n\n                PURPOSE:\n                Retrieve a paginated list of keywords from the Metadata platform with advanced\n                sorting and filtering capabilities. Optionally search by keyword name. Use this tool\n                to discover, analyze, and export keyword data for campaign planning and optimization.\n\n                WHEN TO USE:\n                - Browse all available keywords in the account\n                - Search for specific keywords by name\n                - Find keyword variations and similar terms\n                - Export keyword data with custom sorting\n                - Analyze keyword metrics (search volume, bid prices)\n                - Build keyword lists for campaign creation\n                - Filter keywords by archived status\n                - Compare keyword performance metrics\n\n                KEY FEATURES:\n                ✓ NAME SEARCH: Filter by keyword name for targeted searches (optional)\n                ✓ PAGINATION: Use page and size parameters to navigate large datasets\n                ✓ SORTING: Sort by search volume, bid prices, name, or modification date\n                ✓ FILTERING: Include or exclude archived keywords\n                ✓ PERFORMANCE DATA: Get avgMonthlySearches, lowerPageBid, higherPageBid metrics\n\n                NAME SEARCH:\n                The optional 'name' parameter supports:\n                1. Single name (string): Searches for one keyword name\n                   - \"marketing\" → Finds \"digital marketing\", \"email marketing\", \"marketing automation\"\n                   - \"seo\" → Finds \"SEO services\", \"SEO tools\", \"SEO analytics\"\n                   - \"ppc\" → Finds \"PPC advertising\", \"PPC campaigns\"\n\n                2. Multiple names (array of strings): Searches for multiple keywords at once\n                   - [\"marketing\", \"seo\", \"ppc\"] → Aggregates results from all three searches\n                   - Makes separate API requests for each name and combines results\n                   - Automatically deduplicates keywords by ID\n                   - Returns all unique keywords matching any of the provided names\n\n                - Omit 'name' parameter to list all keywords without filtering\n                - Both single and multiple searches support partial matching\n\n                PAGINATION STRATEGY:\n                The API returns paginated results. Use these parameters to navigate:\n                - page: 0-based page number (default: 0, meaning first page)\n                - size: Number of results per page (default: 25, recommended: 25-100)\n\n                To get the next page of results, increment the 'page' parameter.\n                Example:\n                - page=0, size=25 → Returns items 0-24\n                - page=1, size=25 → Returns items 25-49\n                - page=2, size=25 → Returns items 50-74\n\n                SORT OPTIONS (use format: field,direction):\n                Available fields for sorting:\n                - avgMonthlySearches,desc/asc: Sort by average monthly search volume\n                - lowerPageBid,desc/asc: Sort by lower page bid (CPC floor price)\n                - higherPageBid,desc/asc: Sort by higher page bid (CPC ceiling price)\n                - name,desc/asc: Sort by keyword name alphabetically\n                - modifiedDate,desc/asc: Sort by Modification/Update date (default)\n\n                Direction options:\n                - desc: Descending order (highest to lowest)\n                - asc: Ascending order (lowest to highest)\n\n                SORT EXAMPLES:\n                - sort=\"avgMonthlySearches,desc\": Keywords with highest search volume first\n                - sort=\"avgMonthlySearches,asc\": Keywords with lowest search volume first\n                - sort=\"lowerPageBid,desc\": Keywords with highest CPC floor first\n                - sort=\"higherPageBid,asc\": Keywords with lowest CPC ceiling first\n                - sort=\"name,asc\": Keywords in alphabetical order (A-Z)\n                - sort=\"name,desc\": Keywords in reverse alphabetical order (Z-A)\n\n                FILTERING:\n                - archived: Filter by archived status (true/false, default: false)\n                  Set to true to include archived keywords\n                  Set to false to show only active keywords (recommended)\n\n                PAGINATION WORKFLOW:\n                1. Start with page=0 to get the first set of keywords\n                2. Check the response metadata to see if more results exist\n                3. If needed, increment page number and fetch again\n                4. Continue until all desired results are retrieved\n\n                RESPONSE FORMAT:\n                Returns a paginated response with:\n                {\n                  \"totalElements\": 2,\n                  \"totalPages\": 1,\n                  \"data\": [\n                    {\n                      \"name\": \"product match\",\n                      \"avgMonthlySearches\": 260,\n                      \"competition\": \"LOW\",\n                      \"lowerPageBid\": 0.00,\n                      \"higherPageBid\": 0.00,\n                      \"id\": 18330,\n                      \"archived\": false,\n                      \"createdDate\": \"2025-09-15T20:35:50.000Z\",\n                      \"modifiedDate\": \"2025-09-30T21:14:59.000Z\"\n                    },\n                    {\n                      \"name\": \"keyword match\",\n                      \"avgMonthlySearches\": 140,\n                      \"competition\": \"LOW\",\n                      \"lowerPageBid\": 0.00,\n                      \"higherPageBid\": 0.00,\n                      \"id\": 18339,\n                      \"archived\": false,\n                      \"createdDate\": \"2025-09-15T20:35:50.000Z\",\n                      \"modifiedDate\": \"2025-09-30T21:14:59.000Z\"\n                    }\n                  ]\n                }\n\n                COMMON USE CASES:\n                1. List all active keywords:\n                   list_keywords()\n\n                2. Search for \"marketing\" keywords:\n                   list_keywords(name=\"marketing\")\n\n                3. Get top 50 keywords by search volume:\n                   list_keywords(page=0, size=50, sort=\"avgMonthlySearches,desc\")\n\n                4. Find expensive keywords (highest CPC) with name search:\n                   list_keywords(name=\"analytics\", sort=\"higherPageBid,desc\")\n\n                5. Find affordable keywords (lowest CPC):\n                   list_keywords(sort=\"lowerPageBid,asc\")\n\n                6. Get alphabetically sorted active keywords:\n                   list_keywords(page=0, size=100, sort=\"name,asc\", archived=false)\n\n                7. Export all keywords (paginate through results):\n                   list_keywords(page=0, size=100)\n                   list_keywords(page=1, size=100)\n                   list_keywords(page=2, size=100)\n                   ... (repeat for all pages shown in totalPages)\n\n                8. Get recently modified keywords:\n                   list_keywords(page=0, size=25, sort=\"modifiedDate,desc\")\n\n                9. Search with pagination:\n                   list_keywords(name=\"marketing\", page=0, size=50)\n                   list_keywords(name=\"marketing\", page=1, size=50)\n\n                10. Search for multiple keyword names at once:\n                    list_keywords(name=[\"marketing\", \"seo\", \"ppc\"])\n\n                11. Search multiple names with sorting:\n                    list_keywords(name=[\"analytics\", \"ads\"], sort=\"avgMonthlySearches,desc\")\n\n                12. Search multiple names and exclude archived:\n                    list_keywords(name=[\"social\", \"media\"], archived=false)\n\n                PARAMETERS:\n                - name: Optional keyword name or partial name to search for. Can be a string or array of strings.\n                        Supports partial matching. Omit to list all keywords. (optional)\n                - archived: Filter by archived status (default: false)\n                - page: Page number for pagination (0-based, default: 0)\n                - size: Number of items per page (default: 25, max recommended: 100)\n                - sort: Sort criteria in format: field,direction (default: modifiedDate,desc)\n\n                PERFORMANCE TIPS:\n                - Use size=100 for bulk exports to reduce API calls\n                - Use page number to efficiently navigate large datasets\n                - Filter by archived=false to exclude inactive keywords\n                - Sort by modifiedDate,desc to see recent changes\n                - Use name parameter for targeted searches to reduce result set\n                - When searching multiple names, each name triggers a separate API call\n                  Use reasonable list sizes to avoid excessive API calls\n\n                EXAMPLES:\n                - list_keywords() - Get first 25 active keywords\n                - list_keywords(name=\"marketing\") - Search for marketing keywords\n                - list_keywords(name=[\"marketing\", \"seo\"]) - Search multiple keywords\n                - list_keywords(page=0, size=50, sort=\"avgMonthlySearches,desc\") - Top 50 by search volume\n                - list_keywords(name=\"seo\", sort=\"avgMonthlySearches,desc\") - SEO keywords by search volume\n                - list_keywords(name=[\"ads\", \"analytics\"], sort=\"higherPageBid,desc\") - Multiple names by bid price\n                - list_keywords(page=1, size=100, archived=false, sort=\"name,asc\") - Page 2 of keywords A-Z\n                - list_keywords(page=0, size=25, sort=\"lowerPageBid,desc\") - Most expensive keywords",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "oneOf": [
       {
        "type": "string",
        "description": "Single keyword name or partial name to search for. Supports partial matching."
       },
       {
        "type": "array",
        "items": {
         "type": "string"
        },
        "minItems": 1,
        "description": "Multiple keyword names to search for. Makes separate requests for each name and aggregates results with automatic deduplication."
       }
      ],
      "description": "Optional keyword name(s) to search for. Can be a single string or array of strings. Supports partial matching. Omit to list all keywords."
     },
     "archived": {
      "type": "boolean",
      "default": false,
      "description": "Filter by archived status. Set to false to show active keywords (default), true to include archived keywords."
     },
     "page": {
      "type": "integer",
      "minimum": 0,
      "default": 0,
      "description": "Page number for pagination (0-based indexing). Default is 0 for the first page."
     },
     "size": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100,
      "default": 25,
      "description": "Number of items per page (default: 25, recommended: 25-100)."
     },
     "sort": {
      "type": "string",
      "default": "modifiedDate,desc",
      "enum": [
       "avgMonthlySearches,desc",
       "avgMonthlySearches,asc",
       "lowerPageBid,desc",
       "lowerPageBid,asc",
       "higherPageBid,desc",
       "higherPageBid,asc",
       "name,desc",
       "name,asc",
       "modifiedDate,desc",
       "modifiedDate,asc"
      ],
      "description": "Sort criteria in format: field,direction. Options: avgMonthlySearches, lowerPageBid, higherPageBid, name, modifiedDate. Direction: desc (descending) or asc (ascending). Default: modifiedDate,desc"
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "list_negative_keywords_list",
   "title": "List Negative Keywords Lists",
   "category": "search",
   "risk": "read",
   "summary": "List and search negative keywords lists by name filter.",
   "description": "List and search negative keywords lists by name filter.\n\n                PURPOSE:\n                Retrieve a list of negative keywords lists from the Metadata platform with optional name filtering.\n                Use this tool to discover and search for specific negative keywords lists for campaign management and optimization.\n\n                ACCOUNT LIMIT:\n                ⚠️ MAXIMUM 20 NEGATIVE KEYWORDS LISTS PER ACCOUNT ⚠️\n                Each account can have a maximum of 20 negative keyword lists. Use this tool to check your current list count\n                and manage your existing lists. If you need to create a new list but have reached the limit, you'll need to\n                delete or archive existing lists first.\n\n                WHEN TO USE:\n                - Browse all available negative keywords lists in the account\n                - Search for specific negative keywords lists by name\n                - Find negative keywords lists by partial name matching\n                - Get all negative keywords lists data\n                - Discover available negative keywords lists for campaign use\n                - Check current list count to manage the 20-list limit\n\n                KEY FEATURES:\n                ✓ NAME SEARCH: Filter by negative keywords list name for targeted searches (optional)\n                ✓ MULTIPLE SEARCH: Search for multiple list names at once with automatic deduplication\n                ✓ SIMPLE INTERFACE: Only name-based filtering, no pagination or sorting needed\n                ✓ LIST COUNTING: Use to verify your current count against the 20-list maximum\n\n                NAME SEARCH:\n                The optional 'name' parameter supports:\n                1. Single name (string): Searches for one negative keywords list name\n                   - \"branded\" → Finds negative keywords list named \"Branded\"\n                   - \"competitor\" → Finds negative keywords list named \"Competitor\"\n                   - \"generic\" → Finds negative keywords list named \"Generic\"\n\n                2. Multiple names (array of strings): Searches for multiple negative keywords lists at once\n                   - [\"branded\", \"competitor\", \"generic\"] → Aggregates results from all three searches\n                   - Makes separate API requests for each name and combines results\n                   - Automatically deduplicates lists by ID\n                   - Returns all unique negative keywords lists matching any of the provided names\n\n                - Omit 'name' parameter to list all negative keywords lists without filtering\n                - Both single and multiple searches support partial matching\n\n                RESPONSE FORMAT:\n                Returns a response with:\n                {\n                  \"data\": [\n                    {\n                      \"name\": \"BRanded\",\n                      \"id\": 123\n                    },\n                    {\n                      \"name\": \"Competitor\",\n                      \"id\": 124\n                    }\n                  ]\n                }\n\n                COMMON USE CASES:\n                1. List all negative keywords lists:\n                   list_negative_keywords_list()\n\n                2. Search for \"branded\" negative keywords list:\n                   list_negative_keywords_list(name=\"branded\")\n\n                3. Find specific negative keywords list:\n                   list_negative_keywords_list(name=\"BRanded\")\n\n                4. Search for multiple negative keywords list names at once:\n                   list_negative_keywords_list(name=[\"branded\", \"competitor\", \"generic\"])\n\n                5. Search multiple negative keywords list names:\n                   list_negative_keywords_list(name=[\"competitor\", \"generic\"])\n\n                PARAMETERS:\n                - name: Optional negative keywords list name or partial name to search for. Can be a string or array of strings.\n                        Supports partial matching. Omit to list all negative keywords lists. (optional)\n\n                PERFORMANCE TIPS:\n                - Use name parameter for targeted searches to reduce result set\n                - When searching multiple names, each name triggers a separate API request\n                  Use reasonable list sizes to avoid excessive API calls\n                - Results are automatically deduplicated when searching multiple names\n\n                EXAMPLES:\n                - list_negative_keywords_list() - Get all negative keywords lists\n                - list_negative_keywords_list(name=\"branded\") - Search for branded negative keywords list\n                - list_negative_keywords_list(name=[\"branded\", \"competitor\"]) - Search multiple negative keywords lists\n                - list_negative_keywords_list(name=\"competitor\") - Find competitor negative keywords list\n                - list_negative_keywords_list(name=[\"branded\", \"generic\"]) - Multiple list names search\n\n                CHANNEL SCOPE:\n                BOTH search channels are supported: pass channel=MICROSOFT_ADS for Microsoft\n                Ads (Bing) and channel=GOOGLE_ADS for Google. The two channels hold SEPARATE\n                lists, so never substitute one channel for the other to get \"close enough\"\n                results.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "name": {
      "oneOf": [
       {
        "type": "string",
        "description": "Single negative keywords list name or partial name to search for. Supports partial matching."
       },
       {
        "type": "array",
        "items": {
         "type": "string"
        },
        "minItems": 1,
        "description": "Multiple negative keywords list names to search for. Makes separate requests for each name and aggregates results with automatic deduplication."
       }
      ],
      "description": "Optional negative keywords list name(s) to search for. Can be a single string or array of strings. Supports partial matching. Omit to list all negative keywords lists."
     },
     "channel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS",
       "MICROSOFT_ADS"
      ],
      "default": "GOOGLE_ADS",
      "description": "Ad channel scope. Both search channels are supported. These are PER-CHANNEL resources: GOOGLE_ADS and MICROSOFT_ADS hold separate sets, and one is invisible to the other. Defaults to GOOGLE_ADS when the user does not say which."
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "list_search_terms",
   "title": "List Search Terms",
   "category": "search",
   "risk": "read",
   "summary": "Review the Google Ads search terms report with filtering, sorting, and pagination.",
   "description": "Review the Google Ads search terms report with filtering, sorting, and pagination.\n\n                PURPOSE:\n                List the actual search queries that triggered the account's Google Ads, so you can\n                review wasteful or off-target terms and decide which ones to add to a negative\n                keywords list. Mirrors the \"Search Terms\" library page in the Metadata UI.\n\n                WHEN TO USE:\n                - Investigate which queries are costing money without producing clicks or conversions\n                - Find queries to add to a negative keywords list (pair with add_keywords_to_negative_lists)\n                - Inspect search terms tied to a specific experiment or matched keyword\n                - Sort by cost / clicks / conversions to surface the worst offenders\n\n                FILTERS (all optional):\n                - searchTerm: substring match on the search term text\n                - experiment: experiment name substring match\n                - keyword: matched keyword substring\n                - matchType: array subset of [PHRASE, BROAD, EXACT, NEAR_EXACT, NEAR_PHRASE]\n                - startDate / endDate: ISO-8601 timestamps (e.g. 2026-06-01T00:00:00.000Z).\n                  The UI defaults to the last 30 days when both are omitted.\n\n                PAGINATION & SORT:\n                - page: 0-based (default 0)\n                - size: rows per page (default 12, max 100)\n                - sort: \"<field>,<direction>\". Sortable fields include searchTerm, matchType,\n                  experiment, addedExcluded, keyword, impressions, clicks, ctr, averageCpc,\n                  spent, conversions, costPerConversion. Direction is asc or desc.\n                  Default: \"clicks,desc\".\n\n                RESPONSE FORMAT:\n                Returns a paginated response:\n                {\n                  \"totalElements\": 123,\n                  \"totalPages\": 5,\n                  \"data\": [\n                    {\n                      \"id\": \"bGVhZCBnZW5lcmF0aW9uIGNhbmFkYQ\",\n                      \"searchTerm\": \"lead generation canada\",\n                      \"experiment\": \"MD_Experiment\",\n                      \"experimentId\": \"179908\",\n                      \"matchType\": \"PHRASE\",\n                      \"keyword\": \"lead generation\",\n                      \"addedExcluded\": \"Added\",\n                      \"impressions\": 200,\n                      \"clicks\": 11,\n                      \"ctr\": 5.5,\n                      \"averageCpc\": 2.3,\n                      \"spent\": 25.30,\n                      \"conversions\": 0,\n                      \"costPerConversion\": null\n                    },\n                    ...\n                  ]\n                }\n\n                COMMON USE CASES:\n                1. Top spenders with zero conversions in the last 30 days:\n                   list_search_terms(sort=\"spent,desc\")\n                   then filter the response client-side for conversions == 0.\n\n                2. Worst CTR among phrase matches:\n                   list_search_terms(matchType=[\"PHRASE\"], sort=\"ctr,asc\")\n\n                3. Search terms tied to a specific experiment:\n                   list_search_terms(experiment=\"Q2 Brand Test\")\n\n                4. Date-bounded review:\n                   list_search_terms(\n                     startDate=\"2026-05-01T00:00:00.000Z\",\n                     endDate=\"2026-05-31T23:59:59.999Z\",\n                     sort=\"spent,desc\"\n                   )\n\n                PARAMETERS:\n                - searchTerm (string, optional): substring filter on the search term text.\n                - experiment (string, optional): experiment name substring filter.\n                - keyword (string, optional): matched keyword substring filter.\n                - matchType (array of strings, optional): subset of\n                  [PHRASE, BROAD, EXACT, NEAR_EXACT, NEAR_PHRASE].\n                - startDate (string, optional): ISO-8601 timestamp lower bound.\n                - endDate (string, optional): ISO-8601 timestamp upper bound.\n                - page (integer, optional): 0-based page index (default 0).\n                - size (integer, optional): rows per page (default 12).\n                - sort (string, optional): \"<field>,<direction>\" (default \"clicks,desc\").\n\n                CHANNEL SCOPE:\n                BOTH search channels are supported: pass channel=MICROSOFT_ADS for Microsoft\n                Ads (Bing) and channel=GOOGLE_ADS for Google. The two channels hold SEPARATE\n                reports, so never substitute one channel for the other to get \"close enough\"\n                results.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "searchTerm": {
      "type": "string",
      "description": "Optional substring filter on the search term text."
     },
     "experiment": {
      "type": "string",
      "description": "Optional experiment name substring filter."
     },
     "keyword": {
      "type": "string",
      "description": "Optional matched-keyword substring filter."
     },
     "matchType": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "PHRASE",
        "BROAD",
        "EXACT",
        "NEAR_EXACT",
        "NEAR_PHRASE"
       ]
      },
      "description": "Optional subset of match types to include."
     },
     "startDate": {
      "type": "string",
      "description": "Optional ISO-8601 timestamp lower bound (e.g. 2026-06-01T00:00:00.000Z)."
     },
     "endDate": {
      "type": "string",
      "description": "Optional ISO-8601 timestamp upper bound (e.g. 2026-06-30T23:59:59.999Z)."
     },
     "page": {
      "type": "integer",
      "minimum": 0,
      "default": 0,
      "description": "0-based page index. Default 0."
     },
     "size": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100,
      "default": 12,
      "description": "Rows per page. Default 12 (kept small to limit chat-context tokens), max 100."
     },
     "sort": {
      "type": "string",
      "default": "clicks,desc",
      "description": "Sort criteria in format '<field>,<direction>'. Default 'clicks,desc'. Sortable fields: searchTerm, matchType, experiment, addedExcluded, keyword, impressions, clicks, ctr, averageCpc, spent, conversions, costPerConversion."
     },
     "channel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS",
       "MICROSOFT_ADS"
      ],
      "default": "GOOGLE_ADS",
      "description": "Ad channel scope. Both search channels are supported. These are PER-CHANNEL resources: GOOGLE_ADS and MICROSOFT_ADS hold separate sets, and one is invisible to the other. Defaults to GOOGLE_ADS when the user does not say which."
     }
    },
    "required": [],
    "additionalProperties": false
   }
  },
  {
   "name": "pause_experiment_keyword",
   "title": "Pause Experiment Keyword",
   "category": "search",
   "risk": "destructive",
   "summary": "Pause a specific keyword running in an experiment.",
   "description": "Pause a specific keyword running in an experiment.\n\n                PURPOSE:\n                Pause a keyword that is currently active in an experiment. This stops the keyword\n                from being used in the experiment without removing it entirely.\n\n                SCOPE - THIS TOOL ACTS ON ONE KEYWORD, NOT ON THE EXPERIMENT:\n                It pauses a SINGLE keyword INSIDE an experiment. The experiment itself keeps\n                running with its other keywords. To pause or restart a WHOLE experiment,\n                use manage_experiment instead.\n\n                WHEN TO USE:\n                - Pause an underperforming keyword in an experiment\n                - Temporarily stop a keyword from running in a specific experiment\n                - Manage keyword performance within experiments\n\n                PREREQUISITE:\n                Use list_experiment_keywords first to find the keywordExternalId and wizExperimentId\n                for the keyword you want to pause. Both come from that tool's results:\n                keywordExternalId identifies the keyword on the ad channel, and wizExperimentId\n                is the experiment's ID (the same ID search_experiments returns as experimentId).\n\n                WORKFLOW:\n                1. Search for the keyword: list_experiment_keywords(keywordOrExperimentName=\"my keyword\")\n                2. Note the keywordExternalId and wizExperimentId from the results\n                3. Pause it: pause_experiment_keyword(keywordExternalId=12345, wizExperimentId=678)\n                4. (Optional) Verify later: list_experiment_keywords(keywordOrExperimentName=\"my keyword\")\n                   to check the status has changed\n\n                PARAMETERS:\n                - keywordExternalId: Required. The external ID of the keyword to pause (from list_experiment_keywords).\n                - wizExperimentId: Required. The experiment ID where the keyword is running (from list_experiment_keywords).\n\n                ⚠️ WARNING: This action changes the state of the keyword in the experiment.\n                The keyword will stop running in the specified experiment.\n\n                ⚠️ ASYNC FLOW: Pausing a keyword is an asynchronous operation. The status change\n                may not be immediately reflected. Use list_experiment_keywords after some time\n                to verify the keyword status has been updated.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "keywordExternalId": {
      "type": "integer",
      "description": "The external ID of the keyword to pause."
     },
     "wizExperimentId": {
      "type": "integer",
      "description": "The experiment ID where the keyword is running."
     }
    },
    "required": [
     "keywordExternalId",
     "wizExperimentId"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "remove_keywords_from_campaign",
   "title": "Remove Keywords from Campaign",
   "category": "search",
   "risk": "destructive",
   "summary": "Remove Google Ads keyword groups and/or negative keyword list attachments from a campaign's GOOGLE_ADS channel.",
   "description": "Remove Google Ads keyword groups and/or negative keyword list attachments from a campaign's GOOGLE_ADS channel.\n\n                Use this tool to clear keywords/negative lists from a DRAFT campaign so the\n                user can replace them with new ones (e.g. \"I don't like these keywords, use\n                these instead\" — call this tool to remove the old set, then call\n                add_and_edit_campaign_elements with the new google.keywords / google.negative_keyword_lists).\n\n                Works on both campaign structures. On a Channel-First / Native (N×N×N) campaign a\n                positive keyword group is also unlinked from its WizNativeAdContainer (keywordGroupId);\n                negative-list detach is channel-level and unchanged.\n\n                KEYWORDS: remove, delete, keyword, keywords, negative, group, google, campaign\n\n                PLATFORM CONSTRAINT:\n                - Only the Draft state allows keyword removal; the platform rejects DELETE on launched/launching campaigns.\n\n                CHANNEL: GOOGLE_ADS only (other channels do not have keyword groups).\n\n                RESPONSE:\n                - Returns the refreshed campaign state plus a removal_results list with one\n                  {channel, keyword_group|negative_keyword_list, success, message|error} entry per\n                  requested removal. ALWAYS check removal_results — a partial failure (e.g. one of\n                  two group names not found) still returns normally.\n                - If EVERY requested removal failed, the tool errors instead of returning.\n\n                REQUIRED:\n                - campaign_id: Campaign ID (numeric identifier)\n                - At least one of channel_keywords OR channel_negative_keyword_lists must be provided.\n\n                OPTIONAL FAST PATH:\n                - channel_id: When you already have the GOOGLE_ADS channel id in context (e.g. from a recent get_campaign_by_wizard_id call), pass it to skip the full-campaign lookup. The tool will fetch only that single channel.\n\n                EXAMPLES:\n\n                Remove one keyword group:\n                remove_keywords_from_campaign({\n                    \"campaign_id\": 42546,\n                    \"channel_keywords\": {\n                        \"GOOGLE_ADS\": [\"CRM Keywords\"]\n                    }\n                })\n\n                Remove a negative keyword list attachment:\n                remove_keywords_from_campaign({\n                    \"campaign_id\": 42546,\n                    \"channel_negative_keyword_lists\": {\n                        \"GOOGLE_ADS\": [\"Competitor Brands\"]\n                    }\n                })\n\n                Remove both at once:\n                remove_keywords_from_campaign({\n                    \"campaign_id\": 42546,\n                    \"channel_keywords\": {\"GOOGLE_ADS\": [\"CRM Keywords\", \"Demo Keywords\"]},\n                    \"channel_negative_keyword_lists\": {\"GOOGLE_ADS\": [\"Competitor Brands\"]}\n                })\n\n                Remove with known channel id (skips campaign lookup):\n                remove_keywords_from_campaign({\n                    \"campaign_id\": 42546,\n                    \"channel_id\": 88123,\n                    \"channel_keywords\": {\"GOOGLE_ADS\": [\"CRM Keywords\"]}\n                })\n\n                INTEGRATION WITH OTHER TOOLS:\n                - Use get_campaign_by_wizard_id to inspect current keyword groups on a channel before removing.\n                - After removal, use add_and_edit_campaign_elements with google.keywords / google.negative_keyword_lists to add replacements.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The unique identifier of the campaign",
      "minimum": 1
     },
     "channel_id": {
      "type": "integer",
      "description": "Optional fast-path: numeric id of the GOOGLE_ADS channel on this campaign. When provided, the tool skips the full-campaign GET and fetches only this channel's keyword list. Use it when you already have the id from a recent campaign read; omit it and the tool resolves the channel by type.",
      "minimum": 1
     },
     "channel_keywords": {
      "type": "object",
      "description": "Map of channel type to list of positive keyword group names to remove. Only GOOGLE_ADS is supported. Example: {\"GOOGLE_ADS\": [\"CRM Keywords\"]}",
      "additionalProperties": {
       "type": "array",
       "items": {
        "type": "string"
       },
       "minItems": 1
      }
     },
     "channel_negative_keyword_lists": {
      "type": "object",
      "description": "Map of channel type to list of negative keyword list names to detach from the channel. Only GOOGLE_ADS is supported. Example: {\"GOOGLE_ADS\": [\"Competitor Brands\"]}",
      "additionalProperties": {
       "type": "array",
       "items": {
        "type": "string"
       },
       "minItems": 1
      }
     }
    },
    "required": [
     "campaign_id"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "remove_keywords_from_negative_list",
   "title": "Remove Keywords from Negative List",
   "category": "search",
   "risk": "destructive",
   "summary": "Remove keywords from a single existing Google Ads negative keyword list, matched by text.",
   "description": "Remove keywords from a single existing Google Ads negative keyword list, matched by text.\n\n                PURPOSE:\n                Drop keywords that no longer belong in a negative keyword list (e.g. a search\n                term that turned out to convert after all, or an over-eager exclusion). The\n                tool fetches the list internally, finds entries whose text matches what you\n                pass, and removes every match.\n\n                MATCHING:\n                - Comparison is case-sensitive and exact (whole-text equality).\n                - If the same text appears under multiple match types (e.g. \"free\" as PHRASE\n                  AND as EXACT), all of them are removed.\n                - Inputs that don't match anything are reported back in 'notFound' without\n                  failing the call.\n\n                WHEN TO USE:\n                - Undo a previous add_keywords_to_negative_lists mistake\n                - Clean up a list during the day-to-day review loop\n                - For full-list wipe use the platform's deleteAll endpoint instead\n                  (not currently wrapped here)\n\n                NOT FOR:\n                - Detaching the list from a campaign (use remove_keywords_from_campaign).\n                - Deleting the list itself.\n\n                COMMON USE CASES:\n                1. Remove a single keyword:\n                   remove_keywords_from_negative_list(\n                     listId=43256533,\n                     keywords=[\"free download\"],\n                   )\n\n                2. Remove several at once:\n                   remove_keywords_from_negative_list(\n                     listId=43256533,\n                     keywords=[\"competitor a\", \"tutorial\", \"wiki\"],\n                   )\n\n                PARAMETERS:\n                - listId (integer, required): negative keyword list id\n                  (from list_negative_keywords_list).\n                - keywords (array of strings, required, min 1): exact text(s) to remove.\n\n                RESPONSE FORMAT:\n                {\n                  \"listId\": 43256533,\n                  \"removed\": [{\"text\": \"free download\", \"keywordMatchType\": \"PHRASE\", \"resourceName\": \"...\"}, ...],\n                  \"notFound\": [\"wiki\"]\n                }\n\n                CHANNEL SCOPE:\n                BOTH search channels are supported: pass channel=MICROSOFT_ADS for Microsoft\n                Ads (Bing) and channel=GOOGLE_ADS for Google. The two channels hold SEPARATE\n                lists, so never substitute one channel for the other to get \"close enough\"\n                results.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "listId": {
      "type": "integer",
      "description": "ID of the negative keyword list (from list_negative_keywords_list)."
     },
     "keywords": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "minItems": 1,
      "description": "Exact keyword texts to remove. Case-sensitive whole-text match."
     },
     "channel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS",
       "MICROSOFT_ADS"
      ],
      "default": "GOOGLE_ADS",
      "description": "Ad channel scope. Both search channels are supported. These are PER-CHANNEL resources: GOOGLE_ADS and MICROSOFT_ADS hold separate sets, and one is invisible to the other. Defaults to GOOGLE_ADS when the user does not say which."
     }
    },
    "required": [
     "listId",
     "keywords"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "restart_experiment_keyword",
   "title": "Restart Experiment Keyword",
   "category": "search",
   "risk": "write",
   "summary": "Restart a specific paused keyword in an experiment.",
   "description": "Restart a specific paused keyword in an experiment.\n\n                PURPOSE:\n                Restart a keyword that was previously paused in an experiment. This re-enables the keyword\n                so it starts running again in the specified experiment.\n\n                SCOPE - THIS TOOL ACTS ON ONE KEYWORD, NOT ON THE EXPERIMENT:\n                It restarts a SINGLE keyword INSIDE an experiment. It cannot restart a paused\n                experiment. To pause or restart a WHOLE experiment, use manage_experiment instead.\n\n                WHEN TO USE:\n                - Restart a keyword that was previously paused in an experiment\n                - Re-enable a keyword after resolving performance issues\n                - Resume keyword activity in a specific experiment\n\n                PREREQUISITE:\n                Use list_experiment_keywords first to find the keywordExternalId and wizExperimentId\n                for the keyword you want to restart. Both come from that tool's results:\n                keywordExternalId identifies the keyword on the ad channel, and wizExperimentId\n                is the experiment's ID (the same ID search_experiments returns as experimentId).\n\n                WORKFLOW:\n                1. Search for the keyword: list_experiment_keywords(keywordOrExperimentName=\"my keyword\")\n                2. Note the keywordExternalId and wizExperimentId from the results\n                3. Restart it: restart_experiment_keyword(keywordExternalId=12345, wizExperimentId=678)\n                4. (Optional) Verify later: list_experiment_keywords(keywordOrExperimentName=\"my keyword\")\n                   to check the status has changed\n\n                PARAMETERS:\n                - keywordExternalId: Required. The external ID of the keyword to restart (from list_experiment_keywords).\n                - wizExperimentId: Required. The experiment ID where the keyword is running (from list_experiment_keywords).\n\n                ⚠️ WARNING: This action changes the state of the keyword in the experiment.\n                The keyword will resume running in the specified experiment.\n\n                ⚠️ ASYNC FLOW: Restarting a keyword is an asynchronous operation. The status change\n                may not be immediately reflected. Use list_experiment_keywords after some time\n                to verify the keyword status has been updated.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "keywordExternalId": {
      "type": "integer",
      "description": "The external ID of the keyword to restart."
     },
     "wizExperimentId": {
      "type": "integer",
      "description": "The experiment ID where the keyword is running."
     }
    },
    "required": [
     "keywordExternalId",
     "wizExperimentId"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "set_search_channel_final_url_suffix",
   "title": "Set Final URL Suffix on a Search Channel",
   "category": "search",
   "risk": "destructive",
   "summary": "Set or clear the Final URL Suffix on a wizard campaign's search channel (Google Ads or Microsoft Ads).",
   "description": "Set or clear the Final URL Suffix on a wizard campaign's search channel (Google Ads or Microsoft Ads). The suffix is appended to every final URL the platform serves — typically UTM parameters for tracking (utm_source, utm_medium, utm_campaign, ...).\n\n                Matches the platform UI's \"Final URL Suffix\" input under the Google Ads / Microsoft Ads channel's Advanced Settings section.\n\n                KEYWORDS: final url suffix, final-url-suffix, url suffix, suffix, utm, utm parameters,\n                utm source, utm medium, utm campaign, tracking, tracking template, url tracking,\n                tracking parameters, google ads, google, microsoft ads, microsoft, bing,\n                search channel, search campaign, campaign settings, advanced settings\n\n                WHEN TO USE:\n                - Add or change UTM / tracking parameters on Google Ads or Microsoft Ads final URLs\n                - Clear an existing suffix (pass empty string \"\")\n\n                REQUIREMENTS:\n                - The campaign MUST already have the target channel enabled (via create_campaign /\n                  add_and_edit_campaign_elements with a `google` and/or `microsoft` block).\n                - Final URL Suffix is search-channel-only. LinkedIn / Meta / Reddit do NOT support\n                  it and the tool will reject other channel types.\n\n                PARAMETERS:\n                - campaign_id (required): the wizard campaign ID.\n                - channel (required): \"GOOGLE_ADS\" or \"MICROSOFT_ADS\".\n                - final_url_suffix (required): the suffix string. Max 2048 chars. Format: key=value\n                  pairs separated by `&`. Allowed characters: A–Z a–z 0–9 . _ ~ = & { } - or %XX\n                  percent-encoded. Spaces and [ ] ( ) # ? must be percent-encoded. Pass empty string\n                  \"\" to clear.\n\n                EXAMPLES:\n\n                Set UTMs on a Google Ads channel:\n                set_search_channel_final_url_suffix({\n                    \"campaign_id\": 177214,\n                    \"channel\": \"GOOGLE_ADS\",\n                    \"final_url_suffix\": \"utm_source=metadata&utm_medium=cpc&utm_campaign={campaignid}\"\n                })\n\n                Clear the suffix on a Microsoft Ads channel:\n                set_search_channel_final_url_suffix({\n                    \"campaign_id\": 177214,\n                    \"channel\": \"MICROSOFT_ADS\",\n                    \"final_url_suffix\": \"\"\n                })\n\n                RESPONSE: {success, campaign_id, channel_id, channel, applied:{final_url_suffix}, errors?}\n\n                INTEGRATION WITH OTHER TOOLS:\n                - Use search_campaigns_by_names / get_campaign_by_wizard_id to find the campaign.\n                - Google Ads / Microsoft Ads channels are enabled by create_campaign or\n                  add_and_edit_campaign_elements with a `google` / `microsoft` block.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The unique identifier of the wizard campaign.",
      "minimum": 1
     },
     "channel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS",
       "MICROSOFT_ADS"
      ],
      "description": "Which search channel on the campaign to update. Final URL Suffix is only supported on these two."
     },
     "final_url_suffix": {
      "type": "string",
      "maxLength": 2048,
      "pattern": "^([A-Za-z0-9._~=&{}-]|%[0-9A-Fa-f]{2})*$",
      "description": "Tracking suffix appended to every final URL on this channel. Format: key=value pairs separated by `&`. Allowed characters: A–Z a–z 0–9 . _ ~ = & { } - or %XX percent-encoded. Pass empty string \"\" to clear."
     }
    },
    "required": [
     "campaign_id",
     "channel",
     "final_url_suffix"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "set_search_channel_locations",
   "title": "Set Geo Targeting on a Search Channel",
   "category": "search",
   "risk": "destructive",
   "summary": "Set the geographic targeting of a wizard campaign's search channel (Google Ads or Microsoft Ads) — which countries/states the campaign serves in, and which countries to exclude.",
   "description": "Set the geographic targeting of a wizard campaign's search channel (Google Ads or Microsoft Ads) — which countries/states the campaign serves in, and which countries to exclude.\n\n                Matches the platform UI's \"Location\" picker under the Google Ads / Microsoft Ads channel SETTINGS panel, plus the \"Negative Locations\" editor under that channel's Advanced Settings.\n\n                ⚠️ NEW SEARCH CAMPAIGNS DEFAULT TO UNITED STATES. The platform seeds the Location\n                field with United States, so a campaign created without calling this tool serves in\n                the US only. Whenever the user names a geography — \"target Canada and the UK\",\n                \"run this in EMEA\", \"exclude India\" — set it explicitly; do not assume the default\n                is what they want.\n\n                KEYWORDS: geo, geos, geo targeting, geotargeting, geographic, geography, location,\n                locations, location targeting, country, countries, state, states, region, territory,\n                market, target location, exclude location, exclude geo, exclude country,\n                negative location, negative locations, location exclusion, excluded locations,\n                where the campaign runs, google ads, google, microsoft ads, microsoft, bing,\n                search channel, search campaign, campaign settings\n\n                WHEN TO USE:\n                - Point a search campaign at specific countries/states instead of the US default\n                - Exclude countries the campaign must not serve in\n                - Clear targeting back to all locations (countries=[], states=[])\n\n                REQUIREMENTS:\n                - The campaign MUST already have the target channel enabled (via create_campaign /\n                  add_and_edit_campaign_elements with a `google` and/or `microsoft` block).\n                - Geo targeting here is search-channel-only. LinkedIn / Meta / Reddit carry location\n                  on the audience or target group instead — use create_target_group or the audience\n                  tools for those.\n\n                PARAMETERS (each one REPLACES that side of the targeting; omit to leave untouched):\n                - campaign_id (required): the wizard campaign ID.\n                - channel (required): \"GOOGLE_ADS\" or \"MICROSOFT_ADS\".\n                - countries: country NAMES to target, e.g. [\"United States\", \"Canada\"].\n                  Pass [] to clear (= all locations).\n                - states: US state NAMES to target, e.g. [\"Texas\", \"California\"].\n                  Pass [] to clear. States are US-only.\n                - excluded_countries: country NAMES the campaign must NOT serve in.\n                  Pass [] to clear all exclusions.\n\n                Names are matched case-insensitively against the platform's location catalog and\n                common shorthand resolves (\"USA\", \"UK\", \"UAE\"). An unrecognised name is REJECTED\n                rather than skipped — a silently dropped geo would serve the campaign somewhere\n                nobody chose. At least one of the three lists must be given.\n\n                ⚠️ EXCLUSIONS ARE COUNTRY-LEVEL ONLY. The platform stores exclusions as bare\n                location ids with no country/state discriminator, and state ids overlap country ids,\n                so an excluded state would be read back as an unrelated country. The UI's Negative\n                Locations picker is countries-only for the same reason. To narrow within a country,\n                target the states you want via `states` instead of excluding the ones you don't.\n\n                EXAMPLES:\n\n                Target Canada and the UK on Google Ads (replacing the US default):\n                set_search_channel_locations({\n                    \"campaign_id\": 177214,\n                    \"channel\": \"GOOGLE_ADS\",\n                    \"countries\": [\"Canada\", \"United Kingdom\"]\n                })\n\n                Target three US states only:\n                set_search_channel_locations({\n                    \"campaign_id\": 177214,\n                    \"channel\": \"GOOGLE_ADS\",\n                    \"countries\": [\"United States\"],\n                    \"states\": [\"Texas\", \"California\", \"New York\"]\n                })\n\n                Keep targeting as-is but exclude two countries on Microsoft Ads:\n                set_search_channel_locations({\n                    \"campaign_id\": 177214,\n                    \"channel\": \"MICROSOFT_ADS\",\n                    \"excluded_countries\": [\"India\", \"Pakistan\"]\n                })\n\n                RESPONSE: {success, campaign_id, channel_id, channel, applied:{locations?:{countries,states}, excluded_locations?:{countries}}, errors?}\n                `applied` echoes the resolved location NAMES, so you can confirm back to the user what the campaign now targets.\n\n                INTEGRATION WITH OTHER TOOLS:\n                - Use search_campaigns_by_names / get_campaign_by_wizard_id to find the campaign.\n                - create_campaign and add_and_edit_campaign_elements accept the same geo inline on\n                  their `google` / `microsoft` blocks — prefer that when creating, and use this tool\n                  to change geo on a campaign that already exists.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "campaign_id": {
      "type": "integer",
      "description": "The unique identifier of the wizard campaign.",
      "minimum": 1
     },
     "channel": {
      "type": "string",
      "enum": [
       "GOOGLE_ADS",
       "MICROSOFT_ADS"
      ],
      "description": "Which search channel on the campaign to update. Geo targeting is set per channel and only these two carry it."
     },
     "countries": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Country NAMES to target, e.g. [\"United States\", \"Canada\"]. REPLACES the channel's current countries. Pass [] to clear (targets all locations). Omit to leave unchanged."
     },
     "states": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "US state NAMES to target, e.g. [\"Texas\", \"California\"]. REPLACES the channel's current states. Pass [] to clear. Omit to leave unchanged. US states only."
     },
     "excluded_countries": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Country NAMES the campaign must NOT serve in, e.g. [\"India\"]. REPLACES the channel's current exclusions. Pass [] to clear them all. Omit to leave unchanged. Country-level only — states cannot be excluded."
     }
    },
    "required": [
     "campaign_id",
     "channel"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "enrich_company_domain",
   "title": "Enrich Company Domain",
   "category": "utilities",
   "risk": "read",
   "summary": "Enrich a company DOMAIN with firmographics (industry, employee range, location).",
   "description": "Enrich a company DOMAIN with firmographics (industry, employee range, location).\n\nCompany-level only (no contact PII). Use to enrich a lead's email domain or an account.\n\nRETURNS: {domain, name, employee_range, industry, city, state, enriched: bool}.\nReturns {enriched: false} when the domain can't be enriched or targeting is not configured.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "domain": {
      "type": "string",
      "description": "Company domain, e.g. 'metadata.io'."
     }
    },
    "required": [
     "domain"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "find_privacy_url",
   "title": "Find Privacy URL",
   "category": "utilities",
   "risk": "read",
   "summary": "Locate the privacy-policy URL for a given website domain.",
   "description": "Locate the privacy-policy URL for a given website domain.\n\n                    Uses a waterfall strategy for maximum reliability:\n                      1. robots.txt  → extract Sitemap directives\n                      2. sitemap.xml → search for privacy-related URLs in <loc> entries\n                      3. Page scrape → footer / page body links (most reliable fallback)\n\n                    USE FOR:\n                    - Find a company's privacy policy URL before creating Lead Gen offers\n                    - Auto-populate privacyUrl field in create_update_offer\n                    - Verify privacy URL exists for GDPR/CCPA compliance checks\n                    - Look up privacy page for any domain\n\n                    WHEN TO USE:\n                    - User asks \"find the privacy URL for example.com\"\n                    - User wants to create an offer but doesn't know the privacy URL\n                    - Before calling create_update_offer to auto-fill privacyUrl\n                    - User asks \"does this site have a privacy policy?\"\n\n                    RETURNS:\n                    {\n                        \"privacy_url\": \"https://example.com/privacy-policy\",\n                        \"source\": \"footer\",\n                        \"link_text\": \"Privacy Policy\",\n                        \"steps_tried\": [\"robots.txt + sitemap\", \"playwright-footer\"],\n                        \"success\": true\n                    }\n\n                    EXAMPLE: find_privacy_url(domain=\"metadata.io\")",
   "inputSchema": {
    "type": "object",
    "properties": {
     "domain": {
      "type": "string",
      "description": "Company website URL or domain. Examples: 'metadata.io', 'www.bloomreach.com', 'https://example.com'"
     }
    },
    "required": [
     "domain"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "get_current_date",
   "title": "Get Current Date",
   "category": "utilities",
   "risk": "read",
   "summary": "Return the REAL current date and time (UTC).",
   "description": "Return the REAL current date and time (UTC).\n\n                ⚠️ You do NOT inherently know today's date — never guess it or\n                rely on training-time assumptions. Whenever a task depends on\n                \"today\" — setting campaign/budget start & end dates, interpreting\n                \"this month\" / \"next quarter\" / \"last 30 days\", validating that an\n                endDate is in the future, or any relative date math — you MUST\n                anchor to the real current date. If you are not already certain\n                what today is, CALL THIS TOOL FIRST and use its `current_date`\n                for all subsequent date calculations.\n\n                Takes no parameters.\n\n                RETURNS:\n                {\n                    \"current_date\": \"2026-09-30\",\n                    \"current_datetime_utc\": \"2026-09-30T12:00:00.000Z\",\n                    \"current_year\": 2026,\n                    \"current_quarter\": \"Q3\",\n                    \"timezone\": \"UTC\"\n                }",
   "inputSchema": {
    "type": "object",
    "properties": {},
    "additionalProperties": false
   }
  },
  {
   "name": "resolve_visitor_ip",
   "title": "Resolve Visitor IP to Company",
   "category": "utilities",
   "risk": "read",
   "summary": "Deanonymize a website visitor IP to the COMPANY that owns it.",
   "description": "Deanonymize a website visitor IP to the COMPANY that owns it.\n\nCompany-level only (no person-level PII). Use for visitor-to-account attribution.\n\nRETURNS: {resolved: bool, company?: {domain, name, employee_range, industry, city, state}}.\nReturns {resolved: false} when the IP can't be resolved or targeting is not configured.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "ip": {
      "type": "string",
      "description": "The visitor's IP address (IPv4 or IPv6)."
     }
    },
    "required": [
     "ip"
    ],
    "additionalProperties": false
   }
  },
  {
   "name": "set_landing_subdomain",
   "title": "Set Landing Page Subdomain",
   "category": "utilities",
   "risk": "destructive",
   "summary": "Set the subdomain this account's generated landing pages are served from.",
   "description": "Set the subdomain this account's generated landing pages are served from.\n\nUSE FOR: \"set our landing page domain\", \"I want our pages on zoom.ad.live\", \"use acme as our landing subdomain\", \"our landing pages should be branded\".\n\nSET ONCE: this claims a subdomain for an account that has NONE. It cannot change or release one that is already set, and it does not fail loudly when asked to; it returns status \"locked\" and changes nothing. A change is support's to make.\n\nWHEN TO OFFER IT UNPROMPTED: new accounts start with no subdomain, so their landing pages are served from a long shared service URL. If get_account_details shows `landingSubdomain` is null and the user is creating or about to create a landing page, offer to set one, since a branded URL is what goes into their ads. Suggest a label derived from their own company domain (acme.com -> \"acme\") and let them confirm or change it. Do NOT set one without the user agreeing to the specific name.\n\nARGS:\n- subdomain (required): the label ONLY, never the full hostname. Pass \"zoom\", not \"zoom.ad.live\" and not \"https://zoom.ad.live\". Pass an empty string to release the account's current claim.\n\nRULES THE LABEL MUST MEET: 1-63 characters of a-z, 0-9 and hyphens; no leading or trailing hyphen; not all digits; not a reserved name (www, api, app, mail, cdn, admin and similar). The platform only trims and lowercases what you send; it does NOT rewrite an invalid label into a valid one, so build a valid label yourself before calling: \"Zoom Events\" is refused, \"zoom-events\" is accepted. Always report the value the tool returns rather than what you sent, because the two can differ.\n\nSCOPE: acts on the CALLER'S OWN account only. There is no account argument and it cannot be pointed at another account; to set one for a different account, an operator does it from the platform admin screen.\n\nRESPONSE `status` AND WHAT TO DO NEXT:\n- \"set\": claimed. Tell the user the stored subdomain.\n- \"cleared\": the claim was released; pages fall back to the default URL.\n- \"unavailable\": another account already holds that name. This is NOT an error. Tell the user it is taken, suggest a variant, and call again once they choose.\n- \"invalid\": the label breaks the rules above. Rewrite it, confirm with the user, call again.\n- \"not_permitted\": the caller is not an account admin, and nothing was changed. Tell them an admin on their account has to set it, under Settings then Configurations. Do not ask them for a name and do not retry.\n- \"locked\": the account already has a subdomain and nothing was changed. Tell the user it is already set, name it, and say support changes it. Do NOT retry and do NOT offer to pick a different name.\n\nIMPORTANT: the name is globally unique and first come first served, and once pages are live it is baked into ad URLs already in market, so changing it later breaks them. That is why it is set once and only support can move it.",
   "inputSchema": {
    "type": "object",
    "properties": {
     "subdomain": {
      "type": "string",
      "description": "The subdomain label only, without any zone suffix or scheme (\"zoom\", not \"zoom.ad.live\"). Empty string releases the current claim."
     }
    },
    "required": [
     "subdomain"
    ],
    "additionalProperties": false
   }
  }
 ]
}