Skip to content

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 15 May 06:35
· 2 commits to main since this release

v0.8.0 — Shared-set linkage + four bug fixes

A focused bug-fix-plus release cleaning up four real-world issues that landed in the tracker over the past few weeks, plus the missing CampaignSharedSet write surface. No new OAuth scopes — everything in this release operates inside the existing adwords + analytics.readonly permission set, important while the app's Google verification is in flight.

New tools

  • attach_shared_set_to_campaigns(shared_set_id, campaign_ids) — creates CampaignSharedSet linkages so the campaigns inherit the shared set's criteria (most commonly: shared negative keyword lists). Closes the gap between "create a shared negative list" and "have it actually apply to a campaign". Newly-built campaigns don't auto-inherit shared lists, so this is the easy-to-forget step right after draft_campaign.
  • detach_shared_set_from_campaigns(shared_set_id, campaign_ids) — removes the linkages. The shared set and its keywords stay intact; only the per-campaign attachment record goes. Uses the composite {campaign_id}~{shared_set_id} resource name so no GAQL lookup is needed.

Both tools follow the existing draft → preview → confirm_and_apply flow, validate shared_set_id + campaign_ids are numeric and non-empty, dedupe input lists, and use partial_failure=True so per-operation errors (e.g. attaching a set to a campaign that already has it) don't fail the whole batch — succeeded vs failed linkages are surfaced separately in the apply response. Credit to @SullyGitHub (PR #22) for the full implementation.

Bug fixes

update_campaign silently dropped negative geo exclusions (#32)

This was a real data-safety bug. When you changed geo_target_ids on a campaign, the existing-criteria removal query filtered on campaign_criterion.type = 'LOCATION' alone — which swept up criteria with negative=TRUE and silently deleted them along with the positives. Users only noticed when traffic from an excluded region started showing up in reports.

Fix scope:

  • Restrict the removal GAQL to campaign_criterion.negative = FALSE so the positive-geo replacement only sweeps positives.
  • The preview now surfaces preserved negative geo exclusions via a new preserved_negative_geo_target_ids field plus a warning entry, so the change is explicit and auditable rather than silently happening behind the scenes.
  • The orchestration rules now call out the new behaviour so AI clients don't try to "fix" the preservation by re-adding the negatives.
  • To remove a negative geo exclusion explicitly, use remove_entity with entity_type="campaign_criterion".

Reported by @PeterrrPiperrr.

discover_keywords hit RESOURCE_EXHAUSTED after a handful of sequential calls (#37)

KeywordPlanIdeaService.GenerateKeywordIdeas sits in a notoriously tight gRPC quota bucket. 5-10 sequential calls with different geo targets was enough to trigger RESOURCE_EXHAUSTED for the rest of the session, regardless of QPS. The v23 REST endpoint for the same method lives in a separate, much larger quota bucket.

discover_keywords now calls the REST endpoint directly via AuthorizedSession:

POST https://googleads.googleapis.com/v23/customers/{cid}:generateKeywordIdeas
  • Same OAuth token (no new scopes — uses the existing adwords scope)
  • Same developer token + login-customer-id MCC header semantics
  • Pagination via nextPageToken is followed transparently so response semantics ("all matching ideas") match the previous gRPC iterator
  • HTTP 429 from REST is surfaced as a RESOURCE_EXHAUSTED string so the existing call_with_retry exponential-backoff helper still kicks in
  • All other Ads tools (run_gaql, get_campaign_performance, every draft_* write tool) keep using the gRPC client — the swap is scoped strictly to the one method that hit the quota wall.

Reported by @PeterrrPiperrr.

update_ad_group(max_cpc=...) error message was misleading for automated bidding (#31)

The old refusal — "max_cpc requires an ad group in a MANUAL_CPC campaign" — implied a technical limitation. The reality is that ad-group CPC bids are simply ignored under every automated bidding strategy (effective_cpc_bid_micros = 0 per Google's docs), so the bid would be a no-op anyway.

The error message now names the actual strategy and points at the right next step:

  • For TARGET_SPEND (Maximize Clicks): "Maximize Clicks (TARGET_SPEND) ignores ad-group CPC bids. The campaign cpc_bid_ceiling is the active constraint — set it via update_campaign(max_cpc=...) instead. No change made."
  • For any other automated strategy: "{strategy} ignores ad-group CPC bids (effective_cpc_bid_micros = 0). The campaign-level target governs spend under automated bidding. No change made."

The MANUAL_CPC happy path is unchanged. The internal helper _ad_group_uses_manual_cpc was renamed to _ad_group_campaign_bidding_strategy and now returns the actual strategy name so the error layer can be specific.

Reported by @PeterrrPiperrr.

run_ga4_report (and every other list-taking tool) rejected JSON-string list args (#28)

Some MCP clients (Cowork at the time of writing) serialize list-typed tool arguments as JSON-encoded strings rather than as native JSON arrays. So a call that should look like {"dimensions": ["pagePath"]} on the wire arrived as {"dimensions": "[\"pagePath\"]"} — Pydantic v2's strict list validator saw a string, refused to coerce, and raised Input should be a valid list. Claude Code, Cursor, and other clients send native arrays so they were unaffected; this was a serialization mismatch on the client side, locking Cowork users out of every list-taking tool.

Fix: a targeted Pydantic BeforeValidator (_coerce_json_string_to_list) now runs on every list-typed tool parameter — 25 callsites across GA4, Ads read/write, GTM, planning, tracking, and cross-reference tools. The validator detects the JSON-array shape ("[...]"), decodes it, and hands the resulting list to the standard list validator. The fix is invisible to schema generation, so well-behaved clients keep seeing the same {"type": "array", "items": {...}} schema and keep sending native arrays.

Four edge cases handled explicitly:

  • Native lists pass through untouched.
  • None passes through (optionals still accept null).
  • Bare strings that aren't valid JSON pass through so Pydantic emits the standard "valid list" error — those genuinely aren't lists.
  • Non-list JSON values (numbers, objects, quoted strings) pass through too, so they fail loudly rather than being silently wrapped.

Reported by @sg-modlab.

Also in this release

  • adloop init no longer crashes on Windows paths (#30, credit @JesseLeeStringer). The wizard previously wrote credentials_path: "c:\Users\..." inside double-quoted YAML, which the parser treated as a \U Unicode escape and rejected with ScannerError. Single-quoted YAML is now used for credentials_path — backslashes are literal in single-quoted strings, and embedded apostrophes are escaped per the YAML spec.
  • Dynamic Google Ads enum introspection helper (#33, credit @illia-sapryga). New adloop.ads.enums.enum_names() pulls valid enum member names straight from the google-ads SDK at the API version we're pinned to. Avoids the drift between hand-maintained validator sets and the SDK — during development this revealed our hardcoded ConversionActionTypeEnum set was missing 29 of 40 valid types.

Closed issues

  • #28run_ga4_report Pydantic list validation
  • #31update_ad_group max_cpc error wording
  • #32update_campaign negative-geo silent removal
  • #36 — RSA pinning support (already shipped in v0.6.0; closed as "already implemented")
  • #37discover_keywords gRPC RESOURCE_EXHAUSTED

Verification

  • 243 tests pass (184 v0.7.0 baseline + 59 new across the changes)
  • No new OAuth scopes — Google verification scope unchanged
  • All write tools continue to follow the draft → preview → confirm_and_apply flow with dry_run=true default

Install / upgrade

pipx upgrade adloop
# or
pip install --upgrade adloop
# or
uvx adloop@0.8.0

Restart your MCP host (Cursor / Claude Code / etc.) after upgrading so the new tool signatures register.

Credits

Full Changelog: v0.7.0...v0.8.0