Agency AI manages Meta (Facebook and Instagram) and Google Ads campaigns from one interface. Analyze spend, revenue, ROAS, and campaign anomalies; get optimization recommendations; create ads; adjust budgets; and pause underperforming campaigns.
Encrypted at rest, isolated from the model
Resolved from an AES-256-GCM vault at the moment of the call and attached to the request — the model never sees the secrets.
Try asking
Brand profile and economics: what the store sells, audience, and breakeven ROAS (profitability threshold). For budgets use get_pacing. Also returns currencies: which currency each ad account is billed in (every budget is in that one), which currency spend and revenue are displayed in, and Meta's maximum daily budget in the account's own units. Also returns ad_destination: the URL an ad built here will send clicks to and whether the merchant set it or it is the storefront fallback — the destination is never part of the brand profile, so read it here before asking a merchant where an ad should point.
Meta + Google ad campaigns with spend, revenue, ROAS, impressions, clicks, conversions, objective. Top 50 by spend. Each Google row carries a campaignType (PMax, Search, or Shopping) — read it, never infer the type from the name. Google coverage is Performance Max, Search, and Shopping campaigns; Display, Video, and Demand Gen campaigns are not returned. Never conclude a Google campaign type is absent from this list alone. A row's money — spend, revenue, cpc, cpm, cost_per_purchase — is in its currencyCode (the store's display currency). dailyBudget is the one exception and carries its own: amount and currency (the ad account's billing currency). The two are often different — never read either as dollars, and never convert one into the other. data_as_of is when the reading was taken: statuses and budgets move, so when this list disagrees with one from earlier in the conversation, the newer reading is the current one. Say a campaign's status changed only when the same campaign is in both readings with different status values. A campaign missing from this list was not returned by this call — a platform / status / campaign_id / name_contains filter, the top-50 cap, or a platform failure listed in errors all omit a campaign that is still there — so say it was not returned, and read errors before saying anything about it. Never that it does not exist. startDate and endDate are the campaign's schedule — when it is set to run, not when it was created, not when it was last changed, and not whether it is delivering. No tool returns a creation time, so a campaign's age is something you do not have: say that rather than computing an age from a schedule date, and never explain missing impressions by how recently a campaign was made. A null startDate or endDate means no schedule date was returned for that campaign, not that it has none. budgetLevel says where the budget lives: campaign (the campaign spends one budget; a new ad set inside it needs none) or adset (each ad set carries its own). agencyRole marks the campaigns Agency AI built at launch — new_customers or remarketing; a new-customer ad belongs in the new_customers one unless the merchant asks otherwise. agencyRole is null on every other campaign, and a campaign's name is not a role: when the merchant refers to a campaign by its role ("the new customer campaign", "retargeting"), pass role and answer for the campaign that comes back; when none does, note says why — ask which campaign they mean rather than choosing one by name. A Google row's targetRoas is that campaign's bid strategy setting as a multiplier (3.5 means 350%) — not the brand's target ROAS — null when the strategy has none, and it is changed from here with target_roas on the campaign change tools. Each row with metrics also carries ctr_all (CTR over ALL clicks, as a percent), cpc, cpm (Meta's own reported CPM on Meta rows, spend per 1,000 impressions on Google rows), cost_per_purchase (spend over conversions), and vs_target_pct / vs_breakeven_pct: how far ROAS sits from the brand's target and break-even ROAS (in roas_targets), as a percent, negative meaning below. A field is absent when its denominator is zero or the brand has not set that target — quote these fields rather than recomputing them. roas_targets.not_set names a target the brand has not set: say it is not set, and never call break-even the target. clicks is not one metric across platforms: on a Meta row it is all clicks on the ad (link clicks plus reactions, comments, shares and other taps), on a Google row it is clicks through to the destination — say which when comparing them. A Meta row also carries reach (unique accounts that saw it in the window) and frequency (average times each of them saw it) when Meta returned them; a Google row has neither. Reach belongs to its row alone: the same people are counted in more than one row, so it is never added up and no sum or total carries it. total_sum adds up every campaign that matched — all total_count of them, listed or not — and carries the same ratios over that sum: a total row for this list is total_sum, never the listed rows added up. It is the total of this list, not of the account: the list holds active and paused campaigns, so an archived or deleted campaign that spent in the window is not in it — for an account or platform total use get_performance_timeseries totals. When the platforms report in different currencies it holds per_platform instead: one sum per platform in that platform's currencyCode — quote each on its own line and never add them together. A platform whose currency is unknown gets no sum, and total_sum is omitted when errors is not empty or the campaign list is still loading: the sum would be short. On a Meta account with more campaigns than this list holds, total_sum is the account's own figures for the window instead (scope account): one row per platform, and blended across them when there is more than one and they share a currency — quote blended as the one account figure, never the platform rows added up. They cover every campaign on those platforms, the ones outside this list included: label them as the account's. Past the cap the result carries no total_count: the list is a slice of the account, so never state how many campaigns the account has. Under a status, id or name filter there is none. Pass limit to get only the top N by spend: listed_sum then adds up exactly the rows returned, with the same ratios and vs_target_pct / vs_breakeven_pct: a total row for a top-N table is listed_sum, labelled as the top N, never the rows added up and never presented as the total for all campaigns. When incomplete is present the list is still loading: the rows are real but may not be the top by spend — say that instead of calling them the top N.
Daily spend, revenue, and ROAS per connected platform over a date window. The result names the window it covers (period, start_date, end_date) — report the numbers as that window and no other. Rows are dense: every connected platform has exactly one row per date up to today (a date that has not happened has no row); a platform that is not connected is left out. Each row carries its date's weekday. Read each row's data_status: complete (settled; spend 0 is a real zero), partial (today or yesterday, still updating — never call a low partial day a drop), not_synced (no data yet, metrics null), error (the fetch failed, metrics null, the series' error says why). Null is unknown, never zero. roas is null when spend is 0. Each series carries last_synced_at. The window is at most 366 days. totals carries the window's sums: totals.platforms has each platform's spend, revenue, roas, results (purchases), cost_per_purchase, impressions, clicks, ctr_all (CTR over all clicks), cpc, cpm — the account-level CTR, CPC and CPM for the window, never an average of campaign rows — and vs_target_pct / vs_breakeven_pct (percent from the brand's target and break-even ROAS in roas_targets; negative means below; roas_targets.not_set names a target the brand has not set). A platform with no figures for the window — its fetch failed, or nothing has synced yet — is listed with available false. totals.blended combines the platforms with blended_roas; it is absent when they report in different currencies or when a platform has no figures, and then there is no account total for the window — name the platform that is missing, never present the rest as the whole account. Quote these totals rather than adding up days or list rows.
The Shopify store's own figures for a date window, as shown on the app's home page: total sales, orders, average order value, sessions (traffic), ad spend, blended ROAS, MER, per-platform spend and revenue, each with its change against the previous window, and new-customer ROAS on request. Use it for any question about store revenue, orders, traffic, MER, or how ad-reported revenue compares with what the store actually took. Money is in the store's display currency (currencyCode). ads.spend is the exact total ad spend (value formatted, amount as a number); the home page card shows the same figure rounded to whole units. Each ads.per_platform row carries cost_per_purchase and vs_target_pct / vs_breakeven_pct, and ads carries blended_roas_vs_target_pct / blended_roas_vs_breakeven_pct: percent from the brand's target and break-even ROAS in roas_targets, negative meaning below; roas_targets.not_set names a target the brand has not set. When an ad platform's figures could not be fetched, ads carries unavailable_platforms and a note in place of the totals, and that platform's row has available false: there is no account total for the window.
Month-to-date ad spend vs the merchant-set monthly budget, per platform. A platform with budgetConfigured: false has no budget set — say so rather than inferring one. A platform with available: false could not be fetched, so its spend is unknown (not zero) and mtdSpend leaves it out (spendComplete: false) — say so. Every figure is in currencyCode, the store's display currency — these are reporting numbers, not ad account amounts, so never set or propose a daily budget from one. A platform listed in unconvertedPlatforms has spend in its own row's currencyCode instead and is left out of mtdSpend.
Statistically unusual spend/ROAS days over the trailing 30 days (z-score based). Days with no data yet or a failed fetch are never flagged; today and yesterday are still updating, so spend there is flagged only when high, never low, and ROAS is not scored.
Use this when the merchant asks what to change, improve, optimize, scale, cut or pause in their Meta or Google ads. Returns Agency AI's own generated optimization recommendations (budget changes, tROAS changes, pauses), each with reasoning and confidence. Read-only — approval happens in the dashboard. status picks approved (the default) or pending recommendations. On budget rows, current_budget and recommended_budget are in budget_currency, the ad account's billing currency, which is not necessarily dollars. tROAS rows reuse the same fields for unitless target-ROAS multipliers and carry no budget_currency; those values are not money.
Meta ads with performance for the date range (spend, revenue, ROAS, impressions, clicks, conversions) plus ad set and campaign ids, top spenders in the date range first (zero-spend ads last). The plain list is only that top slice, so never look for a named ad in it: pass name_contains for an ad the merchant names, ad_id for an id, or adset_id / campaign_id to list what is inside. A list holds at most the 200 highest-spending ads of its scope and total_count stops there: on a larger account or campaign narrow it with adset_id, and never report total_count 200 as the number of ads. When error is set the list was not read: say so rather than that there are no ads. incomplete means some rows may be missing. Use get_ad_creative for an ad's copy and media. Also the source-ad picker for create_ad. Money figures are in the response's currency, the ad account's billing currency — never dollars. Each row with metrics also carries ctr_all (CTR over ALL clicks, as a percent), cpc, cpm (Meta's own reported CPM on Meta rows, spend per 1,000 impressions on Google rows), cost_per_purchase (spend over conversions), and vs_target_pct / vs_breakeven_pct: how far ROAS sits from the brand's target and break-even ROAS (in roas_targets), as a percent, negative meaning below. A field is absent when its denominator is zero or the brand has not set that target — quote these fields rather than recomputing them. roas_targets.not_set names a target the brand has not set: say it is not set, and never call break-even the target. clicks is not one metric across platforms: on a Meta row it is all clicks on the ad (link clicks plus reactions, comments, shares and other taps), on a Google row it is clicks through to the destination — say which when comparing them. A Meta row also carries reach (unique accounts that saw it in the window) and frequency (average times each of them saw it) when Meta returned them; a Google row has neither. Reach belongs to its row alone: the same people are counted in more than one row, so it is never added up and no sum or total carries it. page_sum adds up only the rows returned in this response, never the account total, and carries the same ratios over those rows — quote them instead of averaging rows; for a window total use get_performance_timeseries totals or get_store_metrics. page_sum is omitted when metrics_error is set: the rows' figures are then missing or stale.
Creative content of one Meta ad: ad type (image/video/dynamic), primary text, headline, description (arrays when the ad carries multiple variants), CTA button, landing page, and image/video URLs. Find ad ids with get_ads. Meta only — Google Performance Max has no per-ad creative.
Ad sets (Meta) or asset groups (Google): id, name, status, campaign id, Meta daily budget, and Meta performance for the date range (spend, revenue, ROAS, impressions, clicks, conversions — Google asset groups are structural only). Meta rows also carry the ad set's targeting: the countries, regions and cities it serves (and any excluded), age range, genders, interests, custom audiences, and whether Advantage+ audience is on — this is where a question about markets or international reach is answered. Scope with adset_id to look up one by id, or campaign_id to list what is inside a campaign. Meta ad sets come newest first (by creation time), not by spend. Without campaign_id the list is the account's active and paused ad sets only, up to 200, and total_count stops there: on a larger account scope it with campaign_id. An id must be the numeric one — a name is an error. When error is set the list was not read: say so rather than that there are no ad sets. incomplete means some rows may be missing. For Google, omit campaign_id to list every Performance Max asset group with its campaign — the way to find one by name. Meta budgets and metrics are both in the response's currency, the ad account's billing currency — never dollars. Each row with metrics also carries ctr_all (CTR over ALL clicks, as a percent), cpc, cpm (Meta's own reported CPM on Meta rows, spend per 1,000 impressions on Google rows), cost_per_purchase (spend over conversions), and vs_target_pct / vs_breakeven_pct: how far ROAS sits from the brand's target and break-even ROAS (in roas_targets), as a percent, negative meaning below. A field is absent when its denominator is zero or the brand has not set that target — quote these fields rather than recomputing them. roas_targets.not_set names a target the brand has not set: say it is not set, and never call break-even the target. clicks is not one metric across platforms: on a Meta row it is all clicks on the ad (link clicks plus reactions, comments, shares and other taps), on a Google row it is clicks through to the destination — say which when comparing them. A Meta row also carries reach (unique accounts that saw it in the window) and frequency (average times each of them saw it) when Meta returned them; a Google row has neither. Reach belongs to its row alone: the same people are counted in more than one row, so it is never added up and no sum or total carries it. page_sum adds up only the rows returned in this response, never the account total, and carries the same ratios over those rows — quote them instead of averaging rows; for a window total use get_performance_timeseries totals or get_store_metrics. page_sum is omitted when metrics_error is set: the rows' figures are then missing or stale. Google asset groups carry none of these.
The store's Asset Library — the images and videos it already has on file with Agency AI, uploaded or generated in the app. Each row carries an id, name, tags, and placement (its aspect-ratio slot: square, vertical, landscape, story). Call this whenever the merchant refers to creatives they already have, rather than saying you cannot reach them. An image row (usable_as_ad_creative: true) can be built into a new ad by passing its id as image_asset_id to create_ad; a video has to be used from the Create Ad page in the Agency AI app.
Progress, result, or error of an ad creation started by create_ad, which runs in the background. Pass operation_id for one operation. Omit operation_id to list this account's ad creations from the last 24 hours — use that form whenever the merchant asks how a creation went and you have no operation_id.
Create a new Meta campaign, ad set, or ad. placement picks how much new structure is created: new-campaign (campaign + ad set + ad), new-adset (ad set + ad inside an existing campaign), existing-adset (ad only). The creative comes from ONE of: source_ad_id (copy an existing ad's media) or image_asset_id (an image from the store's Asset Library) — never both. Runs in the background and returns an operation_id for get_operation_status. It creates immediately in the live ad account, and the new ad can start spending its budget. The source ad set's schedule does not carry over: the new ad set runs with no end date unless end_date is given, and end_date must be in the future — a past date is rejected.
Update a campaign's name, daily budget, status (active/paused), and/or — on Google — target ROAS. Meta daily budgets are in the ad account's billing currency, at least its minimum daily budget and capped at its maximum daily budget (get_brand_info reports both currency and cap). The change is applied immediately to the live ad account and affects real delivery and spend.
Update a Meta ad set's name, daily budget, and/or status, or a Google asset group's name and/or status. Google asset groups have no budget of their own. The change is applied immediately to the live ad account and affects real delivery and spend.
Update a Meta ad's name and/or status. Meta only — Google has no separate ad level. The change is applied immediately to the live ad account and affects real delivery and spend.
Start (activate) or pause one or more campaigns in a single call, across Meta and Google. Reports per-campaign success or failure. The change is applied immediately to the live ad account and affects real delivery and spend.
One endpoint, the same key, whichever client you use.
~/Library/Application Support/Claude/claude_desktop_config.json (Mac) · %APPDATA%\Claude\claude_desktop_config.json (Windows)
Replace API_KEY with your own key.
Already have an "mcpServers" section in your config? Just add the server entry inside it.
Discovery, routing, credentials, tool scoping and execution logs all happen at the gateway→connections stay ACTIVE with no work from you
Agency AI MCP runs through a gateway that holds the credentials, scopes the access and records every call.
Managed auth, hosted MCP servers, and every Gmail tool your agent needs.
Free to start.