Skip to content

logly/mureo

v0.10.30 Breaking

This release includes 1 breaking change for platform teams planning a safe upgrade.

✓ No known CVEs patched
Read the diff → Tool health → What is this tool? →

✓ No known CVEs patched in this version

Topics

advertising agentic-ai ai-agents claude-code cli codex
+11 more
cursor facebook-ads gemini-cli google-ads marketing marketing-automation mcp meta-ads model-context-protocol python search-console

Affected surfaces

auth

Summary

AI summary

Updates Key fixes, Highlights, and None across a mixed release.

Full changelog

Highlights

  • Meta bidding controls. bid_strategy, bid_constraints
    (roas_average_floor), promoted_object, ad-set-level bid_amount on
    update, and is_adset_budget_sharing_enabled are now exposed on the
    Meta campaign / ad-set write tools — cost-cap, bid-cap and min-ROAS
    strategies are reachable from mureo for the first time.
  • Meta targeting discovery. Two new read-only tools,
    meta_ads_targeting_search (interest catalogue by keyword) and
    meta_ads_targeting_categories (behavior / demographic / life-event
    catalogues), resolve the internal targeting IDs an ad-set targeting
    spec needs.
  • System-user token auth for Live Meta apps. The configure UI now
    opens with an explicit either/or connection-method chooser and offers a
    first-class "Paste a system-user token" path, closing the dead end
    where a Live-mode Meta app can never complete browser OAuth against the
    localhost redirect. The token is validated (granted vs required scopes,
    reachable ad accounts) before it is saved, and is stored without
    app_id / app_secret so it never enters the 53-day refresh path.
  • New tools: meta_ads_pixels_create and meta_ads_pages_list.
    Create a Meta Pixel on an ad account, and list the Facebook Pages the
    current token can manage (personal + business-owned).
  • Bid-cap guardrails. StrategyPolicyGate gains
    max_bid_amount_per_ad_set and max_cpc_bid_per_ad_group, and plugin
    tools can now declare where they carry their proposed bid so those caps
    reach a custom argument vocabulary too.

⚠️ Breaking change

Every builtin MCP tool (all 203) now declares additionalProperties: false
on its top-level inputSchema. An unknown or misspelled parameter used to
pass validation and then get silently dropped by the handler's whitelist —
a typo like budgett became a no-op. Such a call now fails validation
with a clear error naming the offending key.
If you have callers that
relied on typo-tolerance or on passing extra keys, fix them before
upgrading. Nested object properties (e.g. Meta's targeting blob) are
intentionally left open so they stay forward-compatible with the platform
Graph / GAQL sub-object surfaces.

Key fixes

  • meta_ads_images_upload_file returned 400 for every file. The
    /adimages multipart upload was a dead end; the uploader now uses the
    base64 bytes form body, and Meta's error_subcode / fbtrace_id are
    surfaced instead of httpx's generic 400.
  • Ad-account selection in the Meta token card is genuinely optional.
    The picker silently pre-selected the first account, and the route then
    rejected an explicit null as account_not_accessible (str(None) is
    the truthy string "None"), so the optional path was unreachable.

Added

  • Meta system-user token in the configure UI. The Meta Ads authentication
    step now offers a first-class "Paste a system-user token" option next to
    Login with Facebook, closing the gap where Live-mode Meta apps cannot
    complete OAuth from the localhost configure page (Facebook rejects the
    localhost redirect on its own consent screen) and dev-mode apps cannot create
    ad creatives (Marketing API subcode 1885183). A new probe
    (validate_meta_access_token) reports the token's granted vs required scopes
    and the ad accounts it can reach; the operator validates, picks an account,
    and saves via POST /api/credentials/meta/token. A never-expiring system-user
    token is stored without app_id / app_secret so it never enters the
    53-day auto-refresh path. The Facebook-login timeout branch now points Meta
    users at this option instead of silently resetting (#458).

  • Plugin bid declarations. A plugin tool can now declare where it carries
    its proposed bid in standard MCP metadata
    (_meta={"mureo": {"bid": {"cpc_bid": "<key>", "unit": "micros"}}}), so the
    built-in StrategyPolicyGate enforces max_bid_amount_per_ad_set /
    max_cpc_bid_per_ad_group on a plugin whose argument vocabulary differs from
    the built-in Meta/Google keys — closing the bid twin of the silent-
    underenforcement gap BudgetDeclaration closed for budgets (#414). The
    declaration names one or both channels (bid_amount, minor units, direct;
    cpc_bid, currency units, ÷1e6 when unit: micros), replaces the built-in
    bid scan for that tool, and feeds the same _bid_inputs fail-closed choke
    point — a present-but-unreadable declared key (inf/nan/bool/non-numeric/
    nested) denies rather than sails through. Purely additive and opt-in:
    undeclared tools keep today's behavior byte-identical.

  • Bid-cap guardrails in StrategyPolicyGate. The built-in gate now
    enforces two new ## Guardrails rules alongside the budget caps:
    max_bid_amount_per_ad_set refuses a meta_ads_ad_sets_create /
    meta_ads_ad_sets_update whose bid_amount (account-currency minor units,
    identical to Meta's field) exceeds the cap, and max_cpc_bid_per_ad_group
    refuses a google_ads_ad_groups_create / google_ads_ad_groups_update
    whose cpc_bid_micros (converted from micros to currency units) exceeds the
    cap. Bids are per-auction ceilings with distinct semantics from spend
    budgets, so they get their own caps. The bid_constraints.roas_average_floor
    (a min-ROAS floor) and Google bid_modifier (a bid-adjustment multiplier)
    are not spend amounts and are deliberately not constrained. Both fields are
    optional and default to no check (backward compatible); an oversized bid
    saturates to infinity and is refused rather than silently abstained.

  • Meta Ads targeting-discovery tools (meta_ads_targeting_search,
    meta_ads_targeting_categories).
    Two read-only tools that resolve
    Meta's internal targeting IDs needed to build an ad-set targeting
    spec. meta_ads_targeting_search searches the interest catalogue by
    keyword (query, optional limit / locale) and returns interest
    ids with audience-size bounds and path, for use in
    targeting.flexible_spec / interests. meta_ads_targeting_categories
    lists a full category catalogue for one category_class (behaviors,
    demographics, life_events, industries, income, family_statuses,
    user_device, user_os — mapped to Graph's class param) for behavior /
    demographic targeting where keyword search is unavailable. Both hit the
    API-root /search endpoint (not account-scoped) via a new
    TargetingMixin; the class name is validated at the boundary. Meta Ads
    tool count 84 → 86 (aggregate 201 → 203).

  • Meta Ads bidding controls on campaign / ad-set write tools.
    meta_ads_campaigns_create and meta_ads_campaigns_update gain
    bid_strategy (LOWEST_COST_WITHOUT_CAP, LOWEST_COST_WITH_BID_CAP,
    COST_CAP, LOWEST_COST_WITH_MIN_ROAS); campaigns_create also gains
    is_adset_budget_sharing_enabled (required by Meta when creating a
    campaign without campaign budget optimization). meta_ads_ad_sets_create
    and meta_ads_ad_sets_update gain bid_strategy, bid_constraints
    (carrying roas_average_floor for the min-ROAS strategy), and
    promoted_object (the pixel/event conversion target); ad_sets_update
    additionally exposes bid_amount (previously create-only, needed when
    switching to a capped strategy). Combinations are passed through to Graph
    rather than pre-validated, with the tool descriptions documenting the
    cross-field requirements. Read paths now surface
    is_adset_budget_sharing_enabled (campaigns) and promoted_object
    (ad sets).

  • meta_ads_pages_list MCP tool. Read-only listing of the Facebook
    Pages the current token can manage, aggregating personal
    (/me/accounts) and business-owned (/me/businessesowned_pages)
    Pages. Backed by a new MetaAdsApiClient.list_pages method that shares
    the two-step page-discovery logic with get_page_access_token.

  • meta_ads_pixels_create MCP tool. Creates a Meta Pixel on an ad
    account via Graph API POST /act_{ad_account_id}/adspixels with a required
    name. Returns the created pixel id. Mutating and not automatically
    reversible — pixels cannot be deleted through the Graph API — so the tool
    description recommends calling meta_ads_pixels_list first (ad accounts
    have a pixel limit) and recording the change with
    mureo_state_action_log_append. Backed by a new PixelsMixin.create_ad_pixel
    client method (previously the mixin was read-only).

Changed

  • The Meta authentication step now opens with an explicit connection-method
    chooser.
    The step used to render the prominent "Login with Facebook"
    button with the system-user token card collapsed underneath, so operators
    running a Live Meta app — for whom browser OAuth can never succeed,
    Facebook rejects the localhost redirect — clicked the loud button anyway and
    dead-ended. The two paths are mutually exclusive alternatives, so the step
    now leads with "Choose a connection method" / "Pick one — you do not need
    both."
    and two selectable option cards: Paste a system-user token
    (listed first, badged Recommended: for Live apps and automation, never
    expires, no browser login) and Log in with Facebook (for
    Development-mode apps, renews about every 60 days). Nothing is preselected —
    until the operator picks, neither the OAuth button nor the token form is
    on screen — and choosing one reveals only that flow, with the token card
    already expanded. The chooser is a framework-free radiogroup (role="radio"

    • aria-checked, roving tabindex, Space/Enter, arrows and Home/End),
      switching is idempotent and preserves typed-but-unsaved token text, and the
      OAuth timeout guidance now points at the token option above. The card keeps
      its
      <details class="meta-token-card"> shape and every existing
      wizard.auth.meta_token_* key, so downstream markup hooks are unaffected.
  • The ad account in the Meta system-user token card is now explicitly
    optional.
    After Validate, the account <select> was populated with the
    probed accounts only — no placeholder — so the browser silently pre-selected
    the first account and Save persisted it even if the operator never
    opened the dropdown. The picker now opens on a "Select an ad account
    (optional)"
    placeholder and carries a helper line explaining that the
    selection can be left unset when the ad account is assigned elsewhere. Saving with the placeholder selected
    posts account_id: null and writes a meta_ads block with no account_id
    key (a previously saved selection is still carried forward). Also fixes the
    route rejecting that null as account_not_accessiblestr(None) is the
    truthy string "None", so the optional path was unreachable (#459).

  • Internal refactor: split the plugin declaration machinery out of
    mureo.policy.strategy_gate.
    BudgetDeclaration, BidDeclaration, their
    registries, and the register / lookup / reset helpers now live in the new
    mureo.policy.declarations module, keeping strategy_gate.py within the
    project file-size budget. No API change: every name is re-exported from
    mureo.policy.strategy_gate, so all existing import paths remain stable.

  • Strict input schemas across the entire builtin MCP tool registry.
    Every builtin tool (all 203 across Google Ads, Meta Ads, Search Console,
    rollback, analysis, mureo-context, analytics-registry, learning, and
    creative-studio) now declares additionalProperties: false on its
    top-level inputSchema. Previously an unknown or misspelled parameter
    passed server-side validation and was then silently discarded by the
    handler's whitelist — a typo like budgett became a no-op. Such a call
    now fails validation with a clear error naming the offending key.
    Breaking for any caller that relied on typo-tolerance / passing extra
    keys.
    Nested object properties (e.g. Meta's targeting blob) are
    intentionally left open so they stay forward-compatible with the
    platform Graph/GAQL sub-object surfaces, which evolve independently of
    mureo releases. A new enforcement test
    (tests/test_mcp_strict_input_schemas.py) guards the invariant so any
    future tool that omits it fails CI. Generalizes the earlier four-tool
    change below to the whole registry.

  • Stricter input validation on the four Meta Ads campaign / ad-set write
    tools.
    meta_ads_campaigns_create, meta_ads_campaigns_update,
    meta_ads_ad_sets_create, and meta_ads_ad_sets_update now declare
    additionalProperties: false, so an unknown / misspelled parameter is
    rejected with a clear schema error instead of being silently dropped.

Fixed

  • meta_ads_images_upload_file returned 400 for every file, and swallowed
    Meta's error detail.
    The ad-image uploader (upload_ad_image_file) sent the
    file as a multipart/form-data upload to Graph /adimages, which Meta
    rejected with FileTypeNotSupported (error_subcode 1487411) for every file
    regardless of format — even after the multipart body was verified to be
    provably well-formed (correct boundary, image/png part header, raw magic
    bytes, no double base64). Multipart to this endpoint is a dead end, so the
    uploader now sends the image via the base64 bytes form-body variant — the
    same shape the URL-based upload_ad_image path already uses successfully in
    production: it reads the file, base64-encodes it, and posts
    {"bytes": <base64>} as a plain form body. Separately, the method built its
    own one-off httpx.AsyncClient and called raise_for_status(), so the user
    only ever saw httpx's generic Client error '400 Bad Request' while Meta's
    real diagnostics were discarded. It now routes through the shared _request
    machinery (retries, 429/backoff and rate-limit monitoring included), and that
    machinery now surfaces error_subcode and fbtrace_id alongside the existing
    error.message, so a failed upload reports Meta's actual reason (this
    error-surfacing portion is field-confirmed working). The upload's auth shape
    also changed to match the shared machinery: the access token is no longer sent
    as an access_token form field and instead rides the Authorization: Bearer
    header only. The name parameter is retained for API compatibility but is no
    longer transmitted (the bytes variant has no documented name field).

Breaking Changes

  • Every builtin MCP tool (all 203) declares `additionalProperties: false` on its top‑level `inputSchema`; calls with unknown or misspelled parameters now fail validation with a clear error naming the offending key.

Weekly OSS security release digest.

The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.

No spam, unsubscribe anytime.

Share this release

Track logly/mureo

Get notified when new releases ship.

Sign up free

About logly/mureo

Framework for AI agents (Claude Code, Cursor, Codex, Gemini) to operate Google Ads, Meta Ads, and Search Console. Grounded in a local STRATEGY.md — not metric-chasing. Defense-in-depth security, local-first. Apache 2.0.

All releases →

Related context

Beta — feedback welcome: [email protected]