adloop 0.12.0__tar.gz → 0.13.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {adloop-0.12.0 → adloop-0.13.2}/PKG-INFO +48 -10
  2. {adloop-0.12.0 → adloop-0.13.2}/README.md +46 -8
  3. adloop-0.13.2/pyproject.toml +50 -0
  4. adloop-0.12.0/pyproject.toml → adloop-0.13.2/pyproject.toml.orig +2 -2
  5. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/__init__.py +21 -2
  6. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/ads/client.py +1 -1
  7. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/ads/forecast.py +34 -35
  8. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/ads/pmax.py +1 -1
  9. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/ads/write.py +119 -17
  10. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/auth.py +80 -3
  11. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/cli.py +203 -20
  12. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/config.py +6 -0
  13. adloop-0.13.2/src/adloop/ga4/write.py +100 -0
  14. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/gsc/reports.py +10 -1
  15. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/gtm/read.py +13 -1
  16. adloop-0.13.2/src/adloop/merchant/__init__.py +1 -0
  17. adloop-0.13.2/src/adloop/merchant/client.py +108 -0
  18. adloop-0.13.2/src/adloop/merchant/read.py +232 -0
  19. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/rules/adloop.md +14 -1
  20. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/safety/preview.py +5 -0
  21. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/server.py +168 -66
  22. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/__main__.py +0 -0
  23. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/_mcp_patches.py +0 -0
  24. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/ads/__init__.py +0 -0
  25. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/ads/currency.py +0 -0
  26. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/ads/enums.py +0 -0
  27. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/ads/gaql.py +0 -0
  28. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/ads/read.py +0 -0
  29. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/crossref.py +0 -0
  30. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/diagnostics.py +0 -0
  31. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/ga4/__init__.py +0 -0
  32. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/ga4/client.py +0 -0
  33. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/ga4/reports.py +0 -0
  34. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/ga4/tracking.py +0 -0
  35. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/gsc/__init__.py +0 -0
  36. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/gsc/client.py +0 -0
  37. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/gtm/__init__.py +0 -0
  38. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/gtm/client.py +0 -0
  39. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/pagespeed.py +0 -0
  40. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/rules/__init__.py +0 -0
  41. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/rules/commands/analyze-performance.md +0 -0
  42. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/rules/commands/budget-plan.md +0 -0
  43. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/rules/commands/create-ad.md +0 -0
  44. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/rules/commands/create-campaign.md +0 -0
  45. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
  46. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/rules/commands/optimize-campaign.md +0 -0
  47. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/rules_install.py +0 -0
  48. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/runtime.py +0 -0
  49. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/safety/__init__.py +0 -0
  50. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/safety/audit.py +0 -0
  51. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/safety/guards.py +0 -0
  52. {adloop-0.12.0 → adloop-0.13.2}/src/adloop/tracking.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: adloop
3
- Version: 0.12.0
3
+ Version: 0.13.2
4
4
  Summary: The AI command center for Google Ads, GA4, and tracking code.
5
5
  Keywords: mcp,google-ads,google-analytics,ga4,cursor,marketing
6
6
  Author: Daniel Klose
7
7
  Author-email: Daniel Klose <info@daniel-klose.com>
8
8
  License: MIT
9
9
  Requires-Dist: fastmcp>=3.0.0
10
- Requires-Dist: google-ads>=29.0.0
10
+ Requires-Dist: google-ads>=31.1.0
11
11
  Requires-Dist: google-analytics-data>=0.20.0
12
12
  Requires-Dist: google-analytics-admin>=0.27.0
13
13
  Requires-Dist: google-api-python-client>=2.100.0
@@ -34,7 +34,7 @@ Description-Content-Type: text/markdown
34
34
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
35
35
  [![Python 3.11+](https://img.shields.io/badge/Python-3.11+-3776AB.svg?logo=python&logoColor=white)](https://www.python.org/downloads/)
36
36
  [![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-8A2BE2.svg)](https://modelcontextprotocol.io)
37
- [![Google Ads API](https://img.shields.io/badge/Google%20Ads-API%20v23-4285F4.svg?logo=google-ads&logoColor=white)](https://developers.google.com/google-ads/api/docs/start)
37
+ [![Google Ads API](https://img.shields.io/badge/Google%20Ads-API%20v24-4285F4.svg?logo=google-ads&logoColor=white)](https://developers.google.com/google-ads/api/docs/start)
38
38
  [![GA4 Data API](https://img.shields.io/badge/GA4-Data%20API-E37400.svg?logo=google-analytics&logoColor=white)](https://developers.google.com/analytics/devguides/reporting/data/v1)
39
39
  [![GitHub stars](https://img.shields.io/github/stars/kLOsk/adloop?style=social)](https://github.com/kLOsk/adloop)
40
40
 
@@ -45,13 +45,15 @@ An MCP server that gives your AI assistant read + write access to Google Ads and
45
45
  </div>
46
46
 
47
47
  > [!TIP]
48
- > **[AdLoop Cloud](https://getadloop.com) is the hosted version of this project — live now, free during beta (limited seats).** Connect Google in two clicks and use the same 64 tools from claude.ai, ChatGPT, Claude Code, Cursor, or Gemini. No Google Cloud project, no developer token, no OAuth verification wait. EU-hosted, GDPR-first, DPA included.
48
+ > **[AdLoop Cloud](https://getadloop.com) is the hosted version of this project — live now, free during beta (limited seats).** Connect Google in two clicks and use the full toolset from claude.ai, ChatGPT, Claude Code, Cursor, or Gemini. No Google Cloud project, no developer token, no OAuth verification wait. EU-hosted, GDPR-first, DPA included.
49
+
50
+ > 📚 **Documentation: [docs.getadloop.com](https://docs.getadloop.com)** — setup guides per AI client, toolsets, the safety model, and troubleshooting for both editions.
49
51
 
50
52
  ---
51
53
 
52
54
  ## Cloud or Self-Hosted?
53
55
 
54
- Both versions run the same 64 tools with the same safety model. The difference is who handles the plumbing:
56
+ Both versions run the same tools with the same safety model. The difference is who handles the plumbing:
55
57
 
56
58
  | | ☁️ [AdLoop Cloud](https://getadloop.com) | 🛠️ Self-hosted (this repo) |
57
59
  |---|---|---|
@@ -89,7 +91,7 @@ Every tool exists because of an actual problem hit while running real Google Ads
89
91
 
90
92
  The best features come from real workflows. If you're using AdLoop and find yourself wishing it could do something it can't, **open an issue describing your situation** — not just "add feature X" but "I was trying to do Y and couldn't because Z." The context matters more than the request.
91
93
 
92
- ## All 64 Tools
94
+ ## All Tools
93
95
 
94
96
  > **Quick start:** `pip install adloop` or `git clone https://github.com/kLOsk/adloop.git && cd adloop && uv sync && uv run adloop init` — or zero setup on [AdLoop Cloud](https://getadloop.com)
95
97
 
@@ -182,6 +184,15 @@ These tools read the live GTM container and join it with the codebase + GA4 to f
182
184
  |------|-------------|
183
185
  | `analyze_page_speed` | PageSpeed Insights for landing pages — Lighthouse score, Core Web Vitals, real-user CrUX data, top fixes. No OAuth needed (optional API key). |
184
186
 
187
+ ### Merchant Center Tools
188
+
189
+ | Tool | What It Does |
190
+ |------|-------------|
191
+ | `list_merchant_accounts` | Discover accessible Merchant Center accounts |
192
+ | `get_merchant_feed_health` | Feed health — approved/pending/disapproved counts per reporting context, top product issues with docs, account-level issues. Disapprovals silently starve Shopping/PMax. |
193
+
194
+ > **Setup for Merchant Center tools** — Enable the **Merchant API** in your GCP project (the Content API for Shopping is deprecated). The Merchant API has no read-only scope; AdLoop uses it strictly read-only. Upgrading OAuth users re-authorize once.
195
+
185
196
  > **Setup for GSC tools** — Enable the **Search Console API** in your GCP project. Upgrading OAuth users must re-authorize once for the new scope (delete `~/.adloop/token.json`, run any tool). The killer combo: cross-reference organic queries with `get_keyword_performance` to find paid/organic cannibalization and untapped keyword opportunities.
186
197
 
187
198
  ### Planning Tools
@@ -189,7 +200,7 @@ These tools read the live GTM container and join it with the codebase + GA4 to f
189
200
  | Tool | What It Does |
190
201
  |------|-------------|
191
202
  | `discover_keywords` | Discover new keyword ideas from seed keywords and/or a URL — with optional per-month search history + seasonality insights (`include_monthly_volumes`) using Google Ads Keyword Planner. Returns avg monthly searches, competition level, and top-of-page bid range. |
192
- | `estimate_budget` | Forecast clicks, impressions, and cost for a set of keywords using Google Ads Keyword Planner. Supports geo/language targeting. Essential for budget planning before launching campaigns. |
203
+ | `estimate_budget` | Forecast clicks, cost, and conversions for a set of keywords using Google Ads Keyword Planner. Supports geo/language targeting. Essential for budget planning before launching campaigns. |
193
204
 
194
205
  ### Google Ads Write Tools
195
206
 
@@ -208,6 +219,7 @@ All write operations follow a **draft → preview → confirm** workflow. Nothin
208
219
  | `draft_keywords` | Propose keyword additions with match types. Proactively checks bidding strategy — blocks BROAD match on Manual CPC campaigns. |
209
220
  | `add_negative_keywords` | Propose negative keywords directly on a campaign |
210
221
  | `add_negative_locations` | Propose negative geo exclusions on a campaign — exclude cities/regions while keeping broader positive targets |
222
+ | `draft_key_event` | Mark a GA4 event as a key event (conversion) — the fix for "fires but isn't tracked as a conversion" |
211
223
  | `draft_demographic_targeting` | Propose demographic criteria (age, gender, parental status, income) — exclusions by default |
212
224
  | `propose_negative_keyword_list` | Draft a shared negative keyword list (SharedSet) and attach it to a campaign — reusable across multiple campaigns |
213
225
  | `pause_entity` | Pause a campaign, ad group, ad, or keyword |
@@ -248,6 +260,7 @@ AdLoop manages real ad spend, so safety is not optional.
248
260
 
249
261
  - **Two-step writes.** Every mutation returns a preview first. A separate `confirm_and_apply` call is required to execute.
250
262
  - **Dry-run by default.** Even `confirm_and_apply` defaults to `dry_run=true`. Real changes require explicit `dry_run=false`.
263
+ - **Two-phase apply (optional).** With `safety.two_phase_apply: true`, `confirm_and_apply` refuses `dry_run=false` until the plan has completed one dry-run pass — preview-then-apply becomes server-enforced instead of a convention.
251
264
  - **Budget caps.** Configurable maximum daily budget — the server rejects anything above the cap.
252
265
  - **Audit log.** Every operation (including dry runs) is logged to `~/.adloop/audit.log`.
253
266
  - **New campaigns and ads are PAUSED.** Nothing goes live without manual enablement.
@@ -296,8 +309,10 @@ The wizard walks you through:
296
309
  3. **MCC Account ID** — your Manager Account ID (top bar in the MCC UI)
297
310
  4. **OAuth sign-in** — opens a browser to sign in with Google (or prints a URL for headless servers)
298
311
  5. **Auto-discovers your accounts** — finds your GA4 properties and Ads accounts automatically
299
- 6. **Safety defaults** — budget cap and dry-run preference
300
- 7. **Editor config snippets** — prints MCP configuration for both Cursor and Claude Code
312
+ 6. **Optional services** — pin a GTM container, a Search Console property (both auto-discovered too), and a PageSpeed API key; skip any of them with Enter
313
+ 7. **Safety defaults** — budget cap and dry-run preference
314
+ 8. **Toolsets** — optionally expose only part of the tool catalog to your AI client (see [Toolsets](#toolsets--trim-the-context-footprint))
315
+ 9. **Editor config snippets** — prints MCP configuration for both Cursor and Claude Code, including your toolset selection
301
316
 
302
317
  ### Requirements
303
318
 
@@ -431,14 +446,37 @@ All configuration lives in `~/.adloop/config.yaml`. See [`config.yaml.example`](
431
446
  | `ads` | `login_customer_id` | — | Your MCC account ID |
432
447
  | `safety` | `max_daily_budget` | `50.00` | Maximum allowed daily budget per campaign |
433
448
  | `safety` | `require_dry_run` | `true` | Force all writes to dry-run mode |
449
+ | `safety` | `two_phase_apply` | `false` | Refuse real applies until the plan had a dry-run pass |
434
450
  | `safety` | `blocked_operations` | `[]` | Operations to block entirely |
435
451
 
452
+ ### Toolsets — trim the context footprint
453
+
454
+ Most MCP clients (claude.ai, ChatGPT, Cursor, …) load **every tool schema into the model's context at the start of every conversation**. AdLoop's full catalog costs roughly 18k tokens per session that way — paid before you type a word. If you only use part of AdLoop, expose a subset with the `ADLOOP_TOOLSETS` environment variable in your MCP client's `env` block (the `adloop init` wizard offers this and writes it into the snippets for you):
455
+
456
+ ```json
457
+ "env": { "ADLOOP_TOOLSETS": "ads,ga4" }
458
+ ```
459
+
460
+ | Toolset | Covers |
461
+ |---|---|
462
+ | `ads` | Google Ads reads, writes, and planning (Keyword Planner) |
463
+ | `ga4` | Google Analytics reports, realtime, key events |
464
+ | `tracking` | Cross-channel attribution + tracking code generation |
465
+ | `gtm` | Google Tag Manager audits and reads |
466
+ | `gsc` | Search Console reads |
467
+ | `web` | PageSpeed / Core Web Vitals |
468
+ | `merchant` | Merchant Center feed health |
469
+
470
+ `health_check` and `confirm_and_apply` are always included, whatever you select. Unset = the full catalog; unknown names fail at startup with the valid list. The effect is real: `ads,ga4` drops the session cost to ~13k tokens, and a `ga4`-only client pays ~2k — nearly 90% less. Toolsets are per *client*, not per install: one AdLoop config can serve a trimmed Cursor and a full-catalog Claude Code side by side.
471
+
472
+ On [AdLoop Cloud](https://getadloop.com), the same feature is per API key: pick toolsets when creating a key in the dashboard, and that key's tools/list is trimmed server-side for whichever AI client uses it.
473
+
436
474
  ## Project Structure
437
475
 
438
476
  ```
439
477
  src/adloop/
440
478
  ├── __init__.py # Entry point — routes 'adloop init' to wizard, otherwise starts MCP server
441
- ├── server.py # FastMCP server — 64 tool registrations with safety annotations
479
+ ├── server.py # FastMCP server — 67 tool registrations with safety annotations
442
480
  ├── config.py # Config loader (~/.adloop/config.yaml)
443
481
  ├── auth.py # OAuth 2.0 flow (user-supplied credentials, headless fallback) + service accounts; GA4 / Ads / GTM scopes
444
482
  ├── cli.py # Interactive 'adloop init' setup wizard
@@ -8,7 +8,7 @@
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
9
9
  [![Python 3.11+](https://img.shields.io/badge/Python-3.11+-3776AB.svg?logo=python&logoColor=white)](https://www.python.org/downloads/)
10
10
  [![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-8A2BE2.svg)](https://modelcontextprotocol.io)
11
- [![Google Ads API](https://img.shields.io/badge/Google%20Ads-API%20v23-4285F4.svg?logo=google-ads&logoColor=white)](https://developers.google.com/google-ads/api/docs/start)
11
+ [![Google Ads API](https://img.shields.io/badge/Google%20Ads-API%20v24-4285F4.svg?logo=google-ads&logoColor=white)](https://developers.google.com/google-ads/api/docs/start)
12
12
  [![GA4 Data API](https://img.shields.io/badge/GA4-Data%20API-E37400.svg?logo=google-analytics&logoColor=white)](https://developers.google.com/analytics/devguides/reporting/data/v1)
13
13
  [![GitHub stars](https://img.shields.io/github/stars/kLOsk/adloop?style=social)](https://github.com/kLOsk/adloop)
14
14
 
@@ -19,13 +19,15 @@ An MCP server that gives your AI assistant read + write access to Google Ads and
19
19
  </div>
20
20
 
21
21
  > [!TIP]
22
- > **[AdLoop Cloud](https://getadloop.com) is the hosted version of this project — live now, free during beta (limited seats).** Connect Google in two clicks and use the same 64 tools from claude.ai, ChatGPT, Claude Code, Cursor, or Gemini. No Google Cloud project, no developer token, no OAuth verification wait. EU-hosted, GDPR-first, DPA included.
22
+ > **[AdLoop Cloud](https://getadloop.com) is the hosted version of this project — live now, free during beta (limited seats).** Connect Google in two clicks and use the full toolset from claude.ai, ChatGPT, Claude Code, Cursor, or Gemini. No Google Cloud project, no developer token, no OAuth verification wait. EU-hosted, GDPR-first, DPA included.
23
+
24
+ > 📚 **Documentation: [docs.getadloop.com](https://docs.getadloop.com)** — setup guides per AI client, toolsets, the safety model, and troubleshooting for both editions.
23
25
 
24
26
  ---
25
27
 
26
28
  ## Cloud or Self-Hosted?
27
29
 
28
- Both versions run the same 64 tools with the same safety model. The difference is who handles the plumbing:
30
+ Both versions run the same tools with the same safety model. The difference is who handles the plumbing:
29
31
 
30
32
  | | ☁️ [AdLoop Cloud](https://getadloop.com) | 🛠️ Self-hosted (this repo) |
31
33
  |---|---|---|
@@ -63,7 +65,7 @@ Every tool exists because of an actual problem hit while running real Google Ads
63
65
 
64
66
  The best features come from real workflows. If you're using AdLoop and find yourself wishing it could do something it can't, **open an issue describing your situation** — not just "add feature X" but "I was trying to do Y and couldn't because Z." The context matters more than the request.
65
67
 
66
- ## All 64 Tools
68
+ ## All Tools
67
69
 
68
70
  > **Quick start:** `pip install adloop` or `git clone https://github.com/kLOsk/adloop.git && cd adloop && uv sync && uv run adloop init` — or zero setup on [AdLoop Cloud](https://getadloop.com)
69
71
 
@@ -156,6 +158,15 @@ These tools read the live GTM container and join it with the codebase + GA4 to f
156
158
  |------|-------------|
157
159
  | `analyze_page_speed` | PageSpeed Insights for landing pages — Lighthouse score, Core Web Vitals, real-user CrUX data, top fixes. No OAuth needed (optional API key). |
158
160
 
161
+ ### Merchant Center Tools
162
+
163
+ | Tool | What It Does |
164
+ |------|-------------|
165
+ | `list_merchant_accounts` | Discover accessible Merchant Center accounts |
166
+ | `get_merchant_feed_health` | Feed health — approved/pending/disapproved counts per reporting context, top product issues with docs, account-level issues. Disapprovals silently starve Shopping/PMax. |
167
+
168
+ > **Setup for Merchant Center tools** — Enable the **Merchant API** in your GCP project (the Content API for Shopping is deprecated). The Merchant API has no read-only scope; AdLoop uses it strictly read-only. Upgrading OAuth users re-authorize once.
169
+
159
170
  > **Setup for GSC tools** — Enable the **Search Console API** in your GCP project. Upgrading OAuth users must re-authorize once for the new scope (delete `~/.adloop/token.json`, run any tool). The killer combo: cross-reference organic queries with `get_keyword_performance` to find paid/organic cannibalization and untapped keyword opportunities.
160
171
 
161
172
  ### Planning Tools
@@ -163,7 +174,7 @@ These tools read the live GTM container and join it with the codebase + GA4 to f
163
174
  | Tool | What It Does |
164
175
  |------|-------------|
165
176
  | `discover_keywords` | Discover new keyword ideas from seed keywords and/or a URL — with optional per-month search history + seasonality insights (`include_monthly_volumes`) using Google Ads Keyword Planner. Returns avg monthly searches, competition level, and top-of-page bid range. |
166
- | `estimate_budget` | Forecast clicks, impressions, and cost for a set of keywords using Google Ads Keyword Planner. Supports geo/language targeting. Essential for budget planning before launching campaigns. |
177
+ | `estimate_budget` | Forecast clicks, cost, and conversions for a set of keywords using Google Ads Keyword Planner. Supports geo/language targeting. Essential for budget planning before launching campaigns. |
167
178
 
168
179
  ### Google Ads Write Tools
169
180
 
@@ -182,6 +193,7 @@ All write operations follow a **draft → preview → confirm** workflow. Nothin
182
193
  | `draft_keywords` | Propose keyword additions with match types. Proactively checks bidding strategy — blocks BROAD match on Manual CPC campaigns. |
183
194
  | `add_negative_keywords` | Propose negative keywords directly on a campaign |
184
195
  | `add_negative_locations` | Propose negative geo exclusions on a campaign — exclude cities/regions while keeping broader positive targets |
196
+ | `draft_key_event` | Mark a GA4 event as a key event (conversion) — the fix for "fires but isn't tracked as a conversion" |
185
197
  | `draft_demographic_targeting` | Propose demographic criteria (age, gender, parental status, income) — exclusions by default |
186
198
  | `propose_negative_keyword_list` | Draft a shared negative keyword list (SharedSet) and attach it to a campaign — reusable across multiple campaigns |
187
199
  | `pause_entity` | Pause a campaign, ad group, ad, or keyword |
@@ -222,6 +234,7 @@ AdLoop manages real ad spend, so safety is not optional.
222
234
 
223
235
  - **Two-step writes.** Every mutation returns a preview first. A separate `confirm_and_apply` call is required to execute.
224
236
  - **Dry-run by default.** Even `confirm_and_apply` defaults to `dry_run=true`. Real changes require explicit `dry_run=false`.
237
+ - **Two-phase apply (optional).** With `safety.two_phase_apply: true`, `confirm_and_apply` refuses `dry_run=false` until the plan has completed one dry-run pass — preview-then-apply becomes server-enforced instead of a convention.
225
238
  - **Budget caps.** Configurable maximum daily budget — the server rejects anything above the cap.
226
239
  - **Audit log.** Every operation (including dry runs) is logged to `~/.adloop/audit.log`.
227
240
  - **New campaigns and ads are PAUSED.** Nothing goes live without manual enablement.
@@ -270,8 +283,10 @@ The wizard walks you through:
270
283
  3. **MCC Account ID** — your Manager Account ID (top bar in the MCC UI)
271
284
  4. **OAuth sign-in** — opens a browser to sign in with Google (or prints a URL for headless servers)
272
285
  5. **Auto-discovers your accounts** — finds your GA4 properties and Ads accounts automatically
273
- 6. **Safety defaults** — budget cap and dry-run preference
274
- 7. **Editor config snippets** — prints MCP configuration for both Cursor and Claude Code
286
+ 6. **Optional services** — pin a GTM container, a Search Console property (both auto-discovered too), and a PageSpeed API key; skip any of them with Enter
287
+ 7. **Safety defaults** — budget cap and dry-run preference
288
+ 8. **Toolsets** — optionally expose only part of the tool catalog to your AI client (see [Toolsets](#toolsets--trim-the-context-footprint))
289
+ 9. **Editor config snippets** — prints MCP configuration for both Cursor and Claude Code, including your toolset selection
275
290
 
276
291
  ### Requirements
277
292
 
@@ -405,14 +420,37 @@ All configuration lives in `~/.adloop/config.yaml`. See [`config.yaml.example`](
405
420
  | `ads` | `login_customer_id` | — | Your MCC account ID |
406
421
  | `safety` | `max_daily_budget` | `50.00` | Maximum allowed daily budget per campaign |
407
422
  | `safety` | `require_dry_run` | `true` | Force all writes to dry-run mode |
423
+ | `safety` | `two_phase_apply` | `false` | Refuse real applies until the plan had a dry-run pass |
408
424
  | `safety` | `blocked_operations` | `[]` | Operations to block entirely |
409
425
 
426
+ ### Toolsets — trim the context footprint
427
+
428
+ Most MCP clients (claude.ai, ChatGPT, Cursor, …) load **every tool schema into the model's context at the start of every conversation**. AdLoop's full catalog costs roughly 18k tokens per session that way — paid before you type a word. If you only use part of AdLoop, expose a subset with the `ADLOOP_TOOLSETS` environment variable in your MCP client's `env` block (the `adloop init` wizard offers this and writes it into the snippets for you):
429
+
430
+ ```json
431
+ "env": { "ADLOOP_TOOLSETS": "ads,ga4" }
432
+ ```
433
+
434
+ | Toolset | Covers |
435
+ |---|---|
436
+ | `ads` | Google Ads reads, writes, and planning (Keyword Planner) |
437
+ | `ga4` | Google Analytics reports, realtime, key events |
438
+ | `tracking` | Cross-channel attribution + tracking code generation |
439
+ | `gtm` | Google Tag Manager audits and reads |
440
+ | `gsc` | Search Console reads |
441
+ | `web` | PageSpeed / Core Web Vitals |
442
+ | `merchant` | Merchant Center feed health |
443
+
444
+ `health_check` and `confirm_and_apply` are always included, whatever you select. Unset = the full catalog; unknown names fail at startup with the valid list. The effect is real: `ads,ga4` drops the session cost to ~13k tokens, and a `ga4`-only client pays ~2k — nearly 90% less. Toolsets are per *client*, not per install: one AdLoop config can serve a trimmed Cursor and a full-catalog Claude Code side by side.
445
+
446
+ On [AdLoop Cloud](https://getadloop.com), the same feature is per API key: pick toolsets when creating a key in the dashboard, and that key's tools/list is trimmed server-side for whichever AI client uses it.
447
+
410
448
  ## Project Structure
411
449
 
412
450
  ```
413
451
  src/adloop/
414
452
  ├── __init__.py # Entry point — routes 'adloop init' to wizard, otherwise starts MCP server
415
- ├── server.py # FastMCP server — 64 tool registrations with safety annotations
453
+ ├── server.py # FastMCP server — 67 tool registrations with safety annotations
416
454
  ├── config.py # Config loader (~/.adloop/config.yaml)
417
455
  ├── auth.py # OAuth 2.0 flow (user-supplied credentials, headless fallback) + service accounts; GA4 / Ads / GTM scopes
418
456
  ├── cli.py # Interactive 'adloop init' setup wizard
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "adloop"
3
+ version = "0.13.2"
4
+ description = "The AI command center for Google Ads, GA4, and tracking code."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ keywords = [
8
+ "mcp",
9
+ "google-ads",
10
+ "google-analytics",
11
+ "ga4",
12
+ "cursor",
13
+ "marketing",
14
+ ]
15
+ dependencies = [
16
+ "fastmcp>=3.0.0",
17
+ "google-ads>=31.1.0",
18
+ "google-analytics-data>=0.20.0",
19
+ "google-analytics-admin>=0.27.0",
20
+ "google-api-python-client>=2.100.0",
21
+ "google-auth-oauthlib>=1.0.0",
22
+ "google-api-python-client>=2.0.0",
23
+ "pyyaml>=6.0",
24
+ ]
25
+
26
+ [[project.authors]]
27
+ name = "Daniel Klose"
28
+ email = "info@daniel-klose.com"
29
+
30
+ [project.license]
31
+ text = "MIT"
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/kLOsk/adloop"
35
+ Repository = "https://github.com/kLOsk/adloop"
36
+ Issues = "https://github.com/kLOsk/adloop/issues"
37
+ Changelog = "https://github.com/kLOsk/adloop/releases"
38
+
39
+ [project.scripts]
40
+ adloop = "adloop:main"
41
+
42
+ [project.optional-dependencies]
43
+ dev = [
44
+ "pytest>=8.0",
45
+ "pytest-asyncio>=0.23",
46
+ ]
47
+
48
+ [build-system]
49
+ requires = ["uv_build>=0.10.8,<0.11.0"]
50
+ build-backend = "uv_build"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.12.0"
3
+ version = "0.13.2"
4
4
  description = "The AI command center for Google Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -11,7 +11,7 @@ license = { text = "MIT" }
11
11
  keywords = ["mcp", "google-ads", "google-analytics", "ga4", "cursor", "marketing"]
12
12
  dependencies = [
13
13
  "fastmcp>=3.0.0",
14
- "google-ads>=29.0.0",
14
+ "google-ads>=31.1.0",
15
15
  "google-analytics-data>=0.20.0",
16
16
  "google-analytics-admin>=0.27.0",
17
17
  "google-api-python-client>=2.100.0",
@@ -9,6 +9,25 @@ except PackageNotFoundError: # running from a source tree without install
9
9
  __version__ = "0.0.0.dev0"
10
10
 
11
11
 
12
+ def install_runtime_patches() -> None:
13
+ """Arm the upstream-bug workarounds for a long-running host process.
14
+
15
+ Importing ``adloop.server`` is deliberately free of process-global side
16
+ effects so the server can be embedded in an ASGI app. That leaves the
17
+ workarounds in ``_mcp_patches`` unarmed for any embedder, including a
18
+ hosted deployment, which is exactly where they matter most: the
19
+ cancellation race they guard against (python-sdk#2416) fires when a
20
+ host cancels a slow tool call, and it takes the transport down with it.
21
+
22
+ Embedders should call this once during application startup. It is
23
+ idempotent, never raises, and each patch inside self-disarms once the
24
+ upstream bug it tracks is fixed.
25
+ """
26
+ from adloop import _mcp_patches
27
+
28
+ _mcp_patches.install()
29
+
30
+
12
31
  def main() -> None:
13
32
  """Entry point for `adloop` console script.
14
33
 
@@ -50,10 +69,10 @@ def main() -> None:
50
69
  # the stdio cancellation-race monkeypatch) are deliberately installed
51
70
  # here — in the stdio entry point — rather than at adloop.server import
52
71
  # time, so embedding the server in an ASGI app stays side-effect-free.
53
- from adloop import _mcp_patches, diagnostics
72
+ from adloop import diagnostics
54
73
 
55
74
  diagnostics.install()
56
- _mcp_patches.install()
75
+ install_runtime_patches()
57
76
 
58
77
  from adloop.server import mcp
59
78
 
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
16
16
  # Pin the API version so library upgrades don't silently break field names,
17
17
  # enum values, or mutate operation structures. Bump this deliberately when
18
18
  # migrating to a new API version — never let it float to the library default.
19
- GOOGLE_ADS_API_VERSION = "v23"
19
+ GOOGLE_ADS_API_VERSION = "v24"
20
20
 
21
21
 
22
22
  def get_ads_client(config: AdLoopConfig) -> GoogleAdsClient:
@@ -22,13 +22,16 @@ def estimate_budget(
22
22
  forecast_days: int = 30,
23
23
  customer_id: str = "",
24
24
  ) -> dict:
25
- """Forecast clicks, impressions, and cost for a set of keywords.
25
+ """Forecast clicks, cost, and conversions for a set of keywords.
26
26
 
27
27
  Uses KeywordPlanIdeaService.GenerateKeywordForecastMetrics to estimate
28
28
  campaign performance without creating anything. Useful for budget planning
29
29
  before launching a new campaign.
30
30
 
31
31
  keywords: list of {"text": str, "match_type": "EXACT|PHRASE|BROAD", "max_cpc": float (optional)}
32
+ Since Ads API v24 the forecast takes no per-keyword bids — the
33
+ highest max_cpc across the list becomes the campaign-level manual
34
+ CPC cap.
32
35
  geo_target_id: geo target constant (2276=Germany, 2840=USA, 2826=UK, 2250=France)
33
36
  language_id: language constant (1000=English, 1001=German, 1002=French, 1003=Spanish)
34
37
  forecast_days: number of days to forecast (default 30)
@@ -45,9 +48,6 @@ def estimate_budget(
45
48
  kp_service = client.get_service("KeywordPlanIdeaService")
46
49
 
47
50
  campaign = client.get_type("CampaignToForecast")
48
- campaign.keyword_plan_network = (
49
- client.enums.KeywordPlanNetworkEnum.GOOGLE_SEARCH
50
- )
51
51
 
52
52
  max_bid = max(
53
53
  (int(kw.get("max_cpc", 0) * 1_000_000) for kw in keywords),
@@ -57,11 +57,9 @@ def estimate_budget(
57
57
  max_bid = _DEFAULT_MAX_CPC_MICROS
58
58
  campaign.bidding_strategy.manual_cpc_bidding_strategy.max_cpc_bid_micros = max_bid
59
59
 
60
- geo_modifier = client.get_type("CriterionBidModifier")
61
- geo_modifier.geo_target_constant = googleads_service.geo_target_constant_path(
62
- geo_target_id
60
+ campaign.geo_target_constants.append(
61
+ googleads_service.geo_target_constant_path(geo_target_id)
63
62
  )
64
- campaign.geo_modifiers.append(geo_modifier)
65
63
 
66
64
  campaign.language_constants.append(
67
65
  googleads_service.language_constant_path(language_id)
@@ -74,15 +72,13 @@ def estimate_budget(
74
72
  if not text:
75
73
  continue
76
74
  match_type = kw.get("match_type", "BROAD").upper()
77
- cpc_micros = int(kw.get("max_cpc", 0) * 1_000_000) or _DEFAULT_MAX_CPC_MICROS
78
75
 
79
- biddable = client.get_type("BiddableKeyword")
80
- biddable.max_cpc_bid_micros = cpc_micros
81
- biddable.keyword.text = text
82
- biddable.keyword.match_type = getattr(
76
+ keyword = client.get_type("KeywordInfo")
77
+ keyword.text = text
78
+ keyword.match_type = getattr(
83
79
  client.enums.KeywordMatchTypeEnum, match_type, client.enums.KeywordMatchTypeEnum.BROAD
84
80
  )
85
- ad_group.biddable_keywords.append(biddable)
81
+ ad_group.keywords.append(keyword)
86
82
 
87
83
  campaign.ad_groups.append(ad_group)
88
84
 
@@ -98,29 +94,29 @@ def estimate_budget(
98
94
  response = kp_service.generate_keyword_forecast_metrics(request=request)
99
95
  metrics = response.campaign_forecast_metrics
100
96
 
101
- # KeywordForecastMetrics fields are ``optional`` in v23, so the SDK
102
- # returns ``None`` for unset fields and the actual integer (including
103
- # 0) otherwise. Falsy checks like ``int(v) if v else None`` would
97
+ # KeywordForecastMetrics fields are ``optional``, so the SDK returns
98
+ # ``None`` for unset fields and the actual number (including 0)
99
+ # otherwise. Falsy checks like ``int(v) if v else None`` would
104
100
  # silently map a real 0-click or 0-cost forecast to None and the
105
101
  # caller couldn't tell "no data" apart from "data says zero" — same
106
102
  # bug class as discover_keywords (issue Bug 2). Use ``is not None``
107
103
  # throughout and the shared ``_micros_to_currency`` helper for the
108
104
  # micros→currency conversions.
105
+ # v24 dropped impressions/click_through_rate from the forecast and
106
+ # added conversions/average_cpa_micros.
109
107
  clicks = getattr(metrics, "clicks", None)
110
- impressions = getattr(metrics, "impressions", None)
111
108
  avg_cpc_micros = getattr(metrics, "average_cpc_micros", None)
112
109
  cost_micros = getattr(metrics, "cost_micros", None)
113
- ctr = getattr(metrics, "click_through_rate", None)
110
+ conversions = getattr(metrics, "conversions", None)
111
+ avg_cpa_micros = getattr(metrics, "average_cpa_micros", None)
114
112
 
115
113
  total_cost = _micros_to_currency(cost_micros)
116
114
  avg_cpc = _micros_to_currency(avg_cpc_micros)
115
+ avg_cpa = _micros_to_currency(avg_cpa_micros)
117
116
 
118
117
  days = max(forecast_days, 1)
119
118
  daily = {
120
119
  "clicks": round(clicks / days, 1) if clicks is not None else None,
121
- "impressions": (
122
- round(impressions / days, 1) if impressions is not None else None
123
- ),
124
120
  "cost": round(total_cost / days, 2) if total_cost is not None else None,
125
121
  }
126
122
 
@@ -147,15 +143,16 @@ def estimate_budget(
147
143
  f"most available traffic (estimated daily cost: {daily['cost']:.2f})."
148
144
  )
149
145
 
150
- if (
151
- impressions is not None
152
- and clicks is not None
153
- and impressions > 0
154
- and clicks == 0
155
- ):
146
+ if clicks is not None and clicks == 0:
147
+ insights.append(
148
+ "Forecast shows zero clicks — keywords may be too niche, too "
149
+ "generic, or the max CPC too low for competitive positions."
150
+ )
151
+
152
+ if conversions is not None and conversions > 0 and avg_cpa is not None:
156
153
  insights.append(
157
- "Forecast shows impressions but zero clicks — keywords may be too "
158
- "generic or CPCs too low for competitive positions."
154
+ f"Estimated {conversions:.1f} conversions at ~{avg_cpa:.2f} avg "
155
+ f"CPA (based on historical conversion rates for these keywords)."
159
156
  )
160
157
 
161
158
  return {
@@ -164,10 +161,12 @@ def estimate_budget(
164
161
  "end": end_date.isoformat(),
165
162
  },
166
163
  "estimated_clicks": clicks,
167
- "estimated_impressions": impressions,
168
164
  "estimated_cost": total_cost,
169
165
  "estimated_avg_cpc": avg_cpc,
170
- "estimated_ctr": round(ctr, 4) if ctr is not None else None,
166
+ "estimated_conversions": (
167
+ round(conversions, 1) if conversions is not None else None
168
+ ),
169
+ "estimated_avg_cpa": avg_cpa,
171
170
  "daily_estimates": daily,
172
171
  "keywords_used": len([kw for kw in keywords if kw.get("text")]),
173
172
  "insights": insights,
@@ -227,7 +226,7 @@ def _build_keyword_ideas_rest_body(
227
226
  ) -> dict:
228
227
  """Build the JSON body for the REST generateKeywordIdeas endpoint.
229
228
 
230
- Schema follows google-ads REST v23 (camelCase). Exactly one of
229
+ Schema follows google-ads REST v24 (camelCase). Exactly one of
231
230
  ``keywordSeed`` / ``urlSeed`` / ``keywordAndUrlSeed`` is set based on
232
231
  which inputs were provided.
233
232
  """
@@ -255,7 +254,7 @@ def _post_keyword_ideas_rest_page(
255
254
 
256
255
  Issue #37: ``KeywordPlanIdeaService.GenerateKeywordIdeas`` over gRPC sits
257
256
  in a tight quota bucket that exhausts after a small number of sequential
258
- calls and returns ``RESOURCE_EXHAUSTED`` regardless of QPS. The REST v23
257
+ calls and returns ``RESOURCE_EXHAUSTED`` regardless of QPS. The REST
259
258
  endpoint for the same method lives in a separate, much larger quota
260
259
  bucket, so this swap eliminates the 429s that made ``discover_keywords``
261
260
  unusable for any multi-geo or repeat-call workflow. Filed against
@@ -389,7 +388,7 @@ def discover_keywords(
389
388
  seasonality insight — the "Google Trends" view for keyword demand.
390
389
 
391
390
  Network: this tool intentionally bypasses the google-ads gRPC client for
392
- KeywordPlanIdeaService and calls the v23 REST endpoint directly. The
391
+ KeywordPlanIdeaService and calls the versioned REST endpoint directly. The
393
392
  gRPC quota bucket for this single method exhausts almost immediately
394
393
  under sequential single-geo calls (issue #37); REST sits in a separate,
395
394
  much larger bucket and works without issue. All other Ads tools still
@@ -121,7 +121,7 @@ def get_asset_performance(
121
121
  content. ``primary_status`` shows whether the asset is eligible to serve
122
122
  (ELIGIBLE, NOT_ELIGIBLE, PAUSED, PENDING).
123
123
 
124
- Note: the Google Ads API v23 does not expose per-asset performance labels
124
+ Note: the Google Ads API does not expose per-asset performance labels
125
125
  (BEST/GOOD/LOW) for PMax assets via GAQL. Use ``get_detailed_asset_performance``
126
126
  to see which asset *combinations* Google selects most — that's the closest
127
127
  proxy for individual asset quality.