adloop 0.10.0__tar.gz → 0.12.0__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.
- {adloop-0.10.0 → adloop-0.12.0}/PKG-INFO +84 -12
- {adloop-0.10.0 → adloop-0.12.0}/README.md +81 -11
- {adloop-0.10.0 → adloop-0.12.0}/pyproject.toml +3 -1
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/__init__.py +5 -1
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/ads/forecast.py +84 -2
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/ads/read.py +400 -5
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/ads/write.py +421 -3
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/auth.py +53 -1
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/config.py +32 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/crossref.py +262 -0
- adloop-0.12.0/src/adloop/gsc/client.py +22 -0
- adloop-0.12.0/src/adloop/gsc/reports.py +137 -0
- adloop-0.12.0/src/adloop/gtm/__init__.py +1 -0
- adloop-0.12.0/src/adloop/gtm/client.py +20 -0
- adloop-0.12.0/src/adloop/gtm/read.py +648 -0
- adloop-0.12.0/src/adloop/pagespeed.py +169 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/rules/adloop.md +84 -5
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/rules/commands/analyze-performance.md +2 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/rules/commands/optimize-campaign.md +2 -0
- adloop-0.12.0/src/adloop/safety/__init__.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/server.py +586 -3
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/__main__.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/_mcp_patches.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/ads/__init__.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/ads/client.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/ads/currency.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/ads/enums.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/ads/gaql.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/ads/pmax.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/cli.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/diagnostics.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/ga4/__init__.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/ga4/client.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/ga4/reports.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/ga4/tracking.py +0 -0
- {adloop-0.10.0/src/adloop/safety → adloop-0.12.0/src/adloop/gsc}/__init__.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/rules/__init__.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/rules/commands/budget-plan.md +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/rules/commands/create-ad.md +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/rules/commands/create-campaign.md +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/rules_install.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/runtime.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/safety/audit.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/safety/guards.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/safety/preview.py +0 -0
- {adloop-0.10.0 → adloop-0.12.0}/src/adloop/tracking.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: adloop
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
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
|
|
@@ -10,7 +10,9 @@ Requires-Dist: fastmcp>=3.0.0
|
|
|
10
10
|
Requires-Dist: google-ads>=29.0.0
|
|
11
11
|
Requires-Dist: google-analytics-data>=0.20.0
|
|
12
12
|
Requires-Dist: google-analytics-admin>=0.27.0
|
|
13
|
+
Requires-Dist: google-api-python-client>=2.100.0
|
|
13
14
|
Requires-Dist: google-auth-oauthlib>=1.0.0
|
|
15
|
+
Requires-Dist: google-api-python-client>=2.0.0
|
|
14
16
|
Requires-Dist: pyyaml>=6.0
|
|
15
17
|
Requires-Dist: pytest>=8.0 ; extra == 'dev'
|
|
16
18
|
Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
|
|
@@ -38,12 +40,31 @@ Description-Content-Type: text/markdown
|
|
|
38
40
|
|
|
39
41
|
An MCP server that gives your AI assistant read + write access to Google Ads and GA4 — with safety guardrails that prevent accidental spend.
|
|
40
42
|
|
|
41
|
-
`pip install adloop`
|
|
43
|
+
**[☁️ Skip the setup — use AdLoop Cloud (free beta)](https://getadloop.com)** · or self-host: `pip install adloop`
|
|
42
44
|
|
|
43
45
|
</div>
|
|
44
46
|
|
|
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.
|
|
49
|
+
|
|
45
50
|
---
|
|
46
51
|
|
|
52
|
+
## Cloud or Self-Hosted?
|
|
53
|
+
|
|
54
|
+
Both versions run the same 64 tools with the same safety model. The difference is who handles the plumbing:
|
|
55
|
+
|
|
56
|
+
| | ☁️ [AdLoop Cloud](https://getadloop.com) | 🛠️ Self-hosted (this repo) |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| **Setup** | Connect Google in two clicks | ~5 min: own Google Cloud project + `adloop init` |
|
|
59
|
+
| **Google Cloud project** | Not needed | Required (free) |
|
|
60
|
+
| **Ads developer token** | Not needed | Required (from your MCC) |
|
|
61
|
+
| **Works with** | claude.ai, ChatGPT, Claude Code, Cursor, Gemini | Claude Code, Cursor, Claude Desktop, any local MCP client |
|
|
62
|
+
| **Where your data flows** | EU servers (Germany), GDPR-first, DPA included | 100% your machine — nothing leaves it |
|
|
63
|
+
| **Updates** | Automatic | `pip install -U adloop` |
|
|
64
|
+
| **Price** | Free during beta | Free forever (MIT) |
|
|
65
|
+
|
|
66
|
+
**Not sure? [Start with Cloud](https://getadloop.com)** — it's the fastest way to see what AdLoop can do, and it's the only way to use AdLoop from claude.ai or ChatGPT. Self-host when you want everything on your own machine or need to modify the code. And if you're here to hack on AdLoop itself: welcome, keep scrolling.
|
|
67
|
+
|
|
47
68
|
## What It Solves
|
|
48
69
|
|
|
49
70
|
AdLoop exists because managing Google Ads alongside your code is a mess. These are the specific problems it handles:
|
|
@@ -58,6 +79,8 @@ AdLoop exists because managing Google Ads alongside your code is a mess. These a
|
|
|
58
79
|
|
|
59
80
|
- **"My landing page gets paid traffic but nobody converts."** AdLoop joins your ad final URLs with GA4 page-level data. See which pages get clicks but no conversions, which have high bounce rates, and which ones are orphaned from any ad campaign.
|
|
60
81
|
|
|
82
|
+
- **"Are conversions even being tagged on every page?"** AdLoop reads your live Google Tag Manager container, joins it against the events in your codebase and the events firing in GA4, and tells you exactly which conversions are being captured, which tags are paused, which page-scope filters are too narrow, and which codebase events have no tag at all — the kind of three-way audit GTM Preview can't give you in a single view.
|
|
83
|
+
|
|
61
84
|
- **"I don't know if my EU consent setup is causing data gaps."** In Europe, 30-70% of users reject analytics cookies. AdLoop accounts for this automatically — it won't diagnose a normal GDPR consent gap as broken tracking.
|
|
62
85
|
|
|
63
86
|
## Built From Real Usage
|
|
@@ -66,9 +89,9 @@ Every tool exists because of an actual problem hit while running real Google Ads
|
|
|
66
89
|
|
|
67
90
|
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.
|
|
68
91
|
|
|
69
|
-
## All
|
|
92
|
+
## All 64 Tools
|
|
70
93
|
|
|
71
|
-
> **Quick start:** `pip install adloop` or `git clone https://github.com/kLOsk/adloop.git && cd adloop && uv sync && uv run adloop init`
|
|
94
|
+
> **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)
|
|
72
95
|
|
|
73
96
|
### Diagnostics
|
|
74
97
|
|
|
@@ -103,8 +126,11 @@ The best features come from real workflows. If you're using AdLoop and find your
|
|
|
103
126
|
| `get_asset_performance` | Per-asset details for PMax — field type, serving status, content |
|
|
104
127
|
| `get_detailed_asset_performance` | Top-performing asset combinations — which headline+description+image combos Google selects most |
|
|
105
128
|
| `get_audience_performance` | Audience segment performance — remarketing, in-market, affinity, demographics |
|
|
129
|
+
| `get_demographic_targeting` | List demographic criteria (age/gender/parental status/income) on an ad group or campaign |
|
|
106
130
|
| `run_gaql` | Arbitrary GAQL queries for anything else |
|
|
107
131
|
|
|
132
|
+
> **Compact mode** — `get_campaign_performance`, `get_keyword_performance`, `get_search_terms`, and `get_ad_performance` accept `compact=true`: account totals, breakdowns, top-10 rows, and pre-computed offender lists (zero-conversion spenders, low-QS keywords, negative-keyword candidates, thin RSAs) instead of every row. ~90% smaller responses — built for account audits so raw tables don't flood your AI's context.
|
|
133
|
+
|
|
108
134
|
### Cross-Reference Tools (GA4 + Ads Combined)
|
|
109
135
|
|
|
110
136
|
These tools call both APIs internally and return unified results with auto-generated insights. They're the core of what makes AdLoop different from having separate GA4 and Ads tools.
|
|
@@ -122,11 +148,47 @@ These tools call both APIs internally and return unified results with auto-gener
|
|
|
122
148
|
| `validate_tracking` | Compare event names found in your codebase against what GA4 actually records. Returns matched, missing, and unexpected events with diagnostics. |
|
|
123
149
|
| `generate_tracking_code` | Generate ready-to-paste GA4 gtag JavaScript for any event, with recommended parameters for well-known events (sign_up, purchase, etc.) and optional trigger wrappers. |
|
|
124
150
|
|
|
151
|
+
### Google Tag Manager Tools
|
|
152
|
+
|
|
153
|
+
These tools read the live GTM container and join it with the codebase + GA4 to find tracking gaps that pure GA4 inspection can't catch — page-scoped triggers, paused tags, dynamic event names, brittle CSS selectors, and codebase events with no tag wired up at all.
|
|
154
|
+
|
|
155
|
+
| Tool | What It Does |
|
|
156
|
+
|------|-------------|
|
|
157
|
+
| `audit_event_coverage` | **The flagship.** Three-way join: codebase events ↔ GTM tags ↔ GA4 actual fires. For each event name in `expected_events`, returns one of 10 statuses (`ok`, `no_tag_no_fire`, `tag_paused`, `tag_active_but_not_firing`, `gtm_only_firing`, `ga4_only`, etc.) plus auto-generated insights for the gaps. |
|
|
158
|
+
| `list_gtm_accounts` | Discover accessible GTM accounts |
|
|
159
|
+
| `list_gtm_containers` | List containers under an account — returns numeric `container_id` (needed by other tools), public `GTM-XXXXXXX` ID, and usage context (web/iOS/Android/server) |
|
|
160
|
+
| `list_gtm_tags` | Every tag in the live container with parsed event names and resolved firing/blocking trigger names |
|
|
161
|
+
| `get_gtm_tag` | Full raw config for a single tag — every parameter, firing/blocking triggers with filter conditions, priority, pause status, sampling |
|
|
162
|
+
| `list_gtm_triggers` | Every trigger with filter conditions parsed to readable text (e.g. `{{Page Path}} contains service-promotions`, `{{Form ID}} NOT contains wf-form-...`). Renders the `negate` flag explicitly. |
|
|
163
|
+
| `get_gtm_trigger` | Full trigger config + reverse lookup of every tag that uses it. Includes parsed `element_visibility` block (selector, on-screen ratio, firing frequency) for elementVisibility triggers and `group_member_trigger_ids` for triggerGroup types |
|
|
164
|
+
| `list_gtm_variables` | Custom variables (data layer, constants, JS) plus enabled built-in variables |
|
|
165
|
+
| `list_gtm_workspaces` | List drafts (workspaces) under a container — workspace IDs are needed by `get_gtm_workspace_diff` |
|
|
166
|
+
| `get_gtm_workspace_diff` | Drafted-but-not-published changes — common cause of "I edited a tag but nothing happened". Returns `is_clean: true` when nothing is pending. |
|
|
167
|
+
| `list_gtm_versions` | Publish history with version IDs and entity counts. Use to correlate a metric drop with a recent publish. |
|
|
168
|
+
| `get_gtm_version` | Full metadata + tag/trigger names for a single historical container version |
|
|
169
|
+
|
|
170
|
+
> **Setup for GTM tools** — Enable the **Tag Manager API v2** in your GCP project, then add your AdLoop credentials' email (the OAuth user, or the service account email if using a service account) as a **Read** user on the GTM container under Admin → User Management. Service accounts pick up access on the next call. **OAuth users upgrading from an earlier AdLoop version must re-authorize once**: the GTM scope is new, so delete `~/.adloop/token.json` and run any tool to re-consent — until then GTM tools return a permissions error.
|
|
171
|
+
|
|
172
|
+
### Search Console Tools
|
|
173
|
+
|
|
174
|
+
| Tool | What It Does |
|
|
175
|
+
|------|-------------|
|
|
176
|
+
| `list_gsc_sites` | List Search Console properties the connected account can access |
|
|
177
|
+
| `run_gsc_report` | Organic search analytics — clicks, impressions, CTR, position by query/page/country/device/date |
|
|
178
|
+
|
|
179
|
+
### Web Performance Tools
|
|
180
|
+
|
|
181
|
+
| Tool | What It Does |
|
|
182
|
+
|------|-------------|
|
|
183
|
+
| `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
|
+
|
|
185
|
+
> **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
|
+
|
|
125
187
|
### Planning Tools
|
|
126
188
|
|
|
127
189
|
| Tool | What It Does |
|
|
128
190
|
|------|-------------|
|
|
129
|
-
| `discover_keywords` | Discover new keyword ideas from seed keywords and/or a URL using Google Ads Keyword Planner. Returns avg monthly searches, competition level, and top-of-page bid range. |
|
|
191
|
+
| `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. |
|
|
130
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. |
|
|
131
193
|
|
|
132
194
|
### Google Ads Write Tools
|
|
@@ -145,6 +207,8 @@ All write operations follow a **draft → preview → confirm** workflow. Nothin
|
|
|
145
207
|
| `draft_image_assets` | Create campaign image assets from local PNG, JPEG, or GIF files. |
|
|
146
208
|
| `draft_keywords` | Propose keyword additions with match types. Proactively checks bidding strategy — blocks BROAD match on Manual CPC campaigns. |
|
|
147
209
|
| `add_negative_keywords` | Propose negative keywords directly on a campaign |
|
|
210
|
+
| `add_negative_locations` | Propose negative geo exclusions on a campaign — exclude cities/regions while keeping broader positive targets |
|
|
211
|
+
| `draft_demographic_targeting` | Propose demographic criteria (age, gender, parental status, income) — exclusions by default |
|
|
148
212
|
| `propose_negative_keyword_list` | Draft a shared negative keyword list (SharedSet) and attach it to a campaign — reusable across multiple campaigns |
|
|
149
213
|
| `pause_entity` | Pause a campaign, ad group, ad, or keyword |
|
|
150
214
|
| `enable_entity` | Re-enable a paused entity |
|
|
@@ -202,6 +266,9 @@ AdLoop manages real ad spend, so safety is not optional.
|
|
|
202
266
|
>
|
|
203
267
|
> *(Upgrading from ≤0.9 with built-in credentials? Those sign-ins were retired in 0.10 — run `adloop init` once to switch to your own project.)*
|
|
204
268
|
|
|
269
|
+
> [!IMPORTANT]
|
|
270
|
+
> **Seeing `deleted_client: The OAuth client was deleted.` or `invalid_client`?** The shared Google Cloud project behind AdLoop ≤0.9's bundled credentials has been shut down, so its stored sign-ins no longer refresh. Two ways forward: **[AdLoop Cloud](https://getadloop.com)** (connect Google in two clicks, nothing to configure) or stay self-hosted with `pip install -U adloop && adloop init` to set up your own free Google Cloud project. Details in [the pinned issue (#49)](https://github.com/kLOsk/adloop/issues/49).
|
|
271
|
+
|
|
205
272
|
### Install
|
|
206
273
|
|
|
207
274
|
**From PyPI:**
|
|
@@ -344,6 +411,7 @@ Ask your AI assistant things like:
|
|
|
344
411
|
- *"Draft a new responsive search ad for my main campaign."*
|
|
345
412
|
- *"Which landing pages get paid traffic but don't convert?"*
|
|
346
413
|
- *"Is my tracking set up correctly? Compare my codebase events against GA4."*
|
|
414
|
+
- *"Audit my Google Tag Manager container — which conversions are being captured and where are the gaps?"*
|
|
347
415
|
- *"What keywords should I target for [product]? Find ideas and estimate the budget."*
|
|
348
416
|
- *"How much budget would I need for these keywords in Germany?"*
|
|
349
417
|
- *"Create a new search campaign for [product feature] with a €20/day budget."*
|
|
@@ -370,11 +438,11 @@ All configuration lives in `~/.adloop/config.yaml`. See [`config.yaml.example`](
|
|
|
370
438
|
```
|
|
371
439
|
src/adloop/
|
|
372
440
|
├── __init__.py # Entry point — routes 'adloop init' to wizard, otherwise starts MCP server
|
|
373
|
-
├── server.py # FastMCP server —
|
|
441
|
+
├── server.py # FastMCP server — 64 tool registrations with safety annotations
|
|
374
442
|
├── config.py # Config loader (~/.adloop/config.yaml)
|
|
375
|
-
├── auth.py # OAuth 2.0 flow (user-supplied credentials, headless fallback) + service accounts
|
|
443
|
+
├── auth.py # OAuth 2.0 flow (user-supplied credentials, headless fallback) + service accounts; GA4 / Ads / GTM scopes
|
|
376
444
|
├── cli.py # Interactive 'adloop init' setup wizard
|
|
377
|
-
├── crossref.py # Cross-reference tools (GA4 + Ads combined analysis)
|
|
445
|
+
├── crossref.py # Cross-reference tools (GA4 + Ads + GTM combined analysis)
|
|
378
446
|
├── tracking.py # Tracking validation + code generation tools
|
|
379
447
|
├── ga4/
|
|
380
448
|
│ ├── client.py # GA4 Data + Admin API clients
|
|
@@ -387,6 +455,9 @@ src/adloop/
|
|
|
387
455
|
│ ├── pmax.py # Performance Max tools — campaign/asset group performance, asset labels, top combinations
|
|
388
456
|
│ ├── write.py # Draft campaign, RSA, keywords; pause, enable, remove, confirm
|
|
389
457
|
│ └── forecast.py # Budget estimation + keyword discovery via Keyword Planner API
|
|
458
|
+
├── gtm/
|
|
459
|
+
│ ├── client.py # Google Tag Manager API v2 client
|
|
460
|
+
│ └── read.py # Live container fetching, tag/trigger/variable parsing, workspace diff, version history
|
|
390
461
|
└── safety/
|
|
391
462
|
├── guards.py # Budget caps, bid limits, blocked operations, Broad Match safety
|
|
392
463
|
├── preview.py # Change plans and previews
|
|
@@ -408,9 +479,10 @@ What's been shipped and what's next:
|
|
|
408
479
|
- ~~Claude Code support~~ ✓ — `CLAUDE.md`, `.mcp.json`, `.claude/rules/`, `.claude/commands/`, CLI wizard snippets
|
|
409
480
|
- **Claude Desktop one-click install** — `adloop install claude-desktop` (and/or a `.dxt` extension bundle) that writes the AdLoop MCP entry into `claude_desktop_config.json` automatically, so Claude Desktop + Cowork users don't have to hand-edit JSON
|
|
410
481
|
- ~~PyPI package~~ ✓ — `pip install adloop`
|
|
411
|
-
-
|
|
482
|
+
- ~~[AdLoop Cloud](https://getadloop.com)~~ ✓ — the hosted version, live in beta: no Google Cloud project, no developer token, connect Google in two clicks (EU-hosted, GDPR-first)
|
|
412
483
|
- ~~Headless server support~~ ✓ — manual URL copy-paste flow for servers without a browser
|
|
413
484
|
- ~~Behavioral eval suites~~ ✓ — 28 prompt-and-expectation tests covering read, write, tracking, and planning workflows
|
|
485
|
+
- ~~Google Tag Manager integration~~ ✓ — read tools for tags, triggers, variables, workspaces, and version history, plus the `audit_event_coverage` three-way join across codebase events, GTM tags, and GA4 actual fires
|
|
414
486
|
- **Community launch** — HN, Indie Hackers, r/cursor, Twitter
|
|
415
487
|
- **Video walkthrough**
|
|
416
488
|
|
|
@@ -424,14 +496,14 @@ MIT — see [LICENSE](LICENSE).
|
|
|
424
496
|
|
|
425
497
|
## Privacy
|
|
426
498
|
|
|
427
|
-
|
|
499
|
+
The open-source version runs entirely on your machine. No data is collected, stored, or transmitted to any server. See [PRIVACY.md](PRIVACY.md) for the full privacy policy. AdLoop Cloud has its own [privacy policy](https://getadloop.com/datenschutz) and [DPA](https://getadloop.com/avv).
|
|
428
500
|
|
|
429
501
|
---
|
|
430
502
|
|
|
431
503
|
<div align="center">
|
|
432
504
|
|
|
433
|
-
**If AdLoop helps you run Google Ads, GA4, and tracking code from one place — [give it a star](https://github.com/kLOsk/adloop).**
|
|
505
|
+
**If AdLoop helps you run Google Ads, GA4, and tracking code from one place — [give it a star](https://github.com/kLOsk/adloop) or [try the hosted version](https://getadloop.com).**
|
|
434
506
|
|
|
435
|
-
Made by [@kLOsk](https://github.com/kLOsk) | [Privacy Policy](PRIVACY.md)
|
|
507
|
+
Made by [@kLOsk](https://github.com/kLOsk) | [AdLoop Cloud](https://getadloop.com) | [Privacy Policy](PRIVACY.md)
|
|
436
508
|
|
|
437
509
|
</div>
|
|
@@ -14,12 +14,31 @@
|
|
|
14
14
|
|
|
15
15
|
An MCP server that gives your AI assistant read + write access to Google Ads and GA4 — with safety guardrails that prevent accidental spend.
|
|
16
16
|
|
|
17
|
-
`pip install adloop`
|
|
17
|
+
**[☁️ Skip the setup — use AdLoop Cloud (free beta)](https://getadloop.com)** · or self-host: `pip install adloop`
|
|
18
18
|
|
|
19
19
|
</div>
|
|
20
20
|
|
|
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.
|
|
23
|
+
|
|
21
24
|
---
|
|
22
25
|
|
|
26
|
+
## Cloud or Self-Hosted?
|
|
27
|
+
|
|
28
|
+
Both versions run the same 64 tools with the same safety model. The difference is who handles the plumbing:
|
|
29
|
+
|
|
30
|
+
| | ☁️ [AdLoop Cloud](https://getadloop.com) | 🛠️ Self-hosted (this repo) |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| **Setup** | Connect Google in two clicks | ~5 min: own Google Cloud project + `adloop init` |
|
|
33
|
+
| **Google Cloud project** | Not needed | Required (free) |
|
|
34
|
+
| **Ads developer token** | Not needed | Required (from your MCC) |
|
|
35
|
+
| **Works with** | claude.ai, ChatGPT, Claude Code, Cursor, Gemini | Claude Code, Cursor, Claude Desktop, any local MCP client |
|
|
36
|
+
| **Where your data flows** | EU servers (Germany), GDPR-first, DPA included | 100% your machine — nothing leaves it |
|
|
37
|
+
| **Updates** | Automatic | `pip install -U adloop` |
|
|
38
|
+
| **Price** | Free during beta | Free forever (MIT) |
|
|
39
|
+
|
|
40
|
+
**Not sure? [Start with Cloud](https://getadloop.com)** — it's the fastest way to see what AdLoop can do, and it's the only way to use AdLoop from claude.ai or ChatGPT. Self-host when you want everything on your own machine or need to modify the code. And if you're here to hack on AdLoop itself: welcome, keep scrolling.
|
|
41
|
+
|
|
23
42
|
## What It Solves
|
|
24
43
|
|
|
25
44
|
AdLoop exists because managing Google Ads alongside your code is a mess. These are the specific problems it handles:
|
|
@@ -34,6 +53,8 @@ AdLoop exists because managing Google Ads alongside your code is a mess. These a
|
|
|
34
53
|
|
|
35
54
|
- **"My landing page gets paid traffic but nobody converts."** AdLoop joins your ad final URLs with GA4 page-level data. See which pages get clicks but no conversions, which have high bounce rates, and which ones are orphaned from any ad campaign.
|
|
36
55
|
|
|
56
|
+
- **"Are conversions even being tagged on every page?"** AdLoop reads your live Google Tag Manager container, joins it against the events in your codebase and the events firing in GA4, and tells you exactly which conversions are being captured, which tags are paused, which page-scope filters are too narrow, and which codebase events have no tag at all — the kind of three-way audit GTM Preview can't give you in a single view.
|
|
57
|
+
|
|
37
58
|
- **"I don't know if my EU consent setup is causing data gaps."** In Europe, 30-70% of users reject analytics cookies. AdLoop accounts for this automatically — it won't diagnose a normal GDPR consent gap as broken tracking.
|
|
38
59
|
|
|
39
60
|
## Built From Real Usage
|
|
@@ -42,9 +63,9 @@ Every tool exists because of an actual problem hit while running real Google Ads
|
|
|
42
63
|
|
|
43
64
|
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.
|
|
44
65
|
|
|
45
|
-
## All
|
|
66
|
+
## All 64 Tools
|
|
46
67
|
|
|
47
|
-
> **Quick start:** `pip install adloop` or `git clone https://github.com/kLOsk/adloop.git && cd adloop && uv sync && uv run adloop init`
|
|
68
|
+
> **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)
|
|
48
69
|
|
|
49
70
|
### Diagnostics
|
|
50
71
|
|
|
@@ -79,8 +100,11 @@ The best features come from real workflows. If you're using AdLoop and find your
|
|
|
79
100
|
| `get_asset_performance` | Per-asset details for PMax — field type, serving status, content |
|
|
80
101
|
| `get_detailed_asset_performance` | Top-performing asset combinations — which headline+description+image combos Google selects most |
|
|
81
102
|
| `get_audience_performance` | Audience segment performance — remarketing, in-market, affinity, demographics |
|
|
103
|
+
| `get_demographic_targeting` | List demographic criteria (age/gender/parental status/income) on an ad group or campaign |
|
|
82
104
|
| `run_gaql` | Arbitrary GAQL queries for anything else |
|
|
83
105
|
|
|
106
|
+
> **Compact mode** — `get_campaign_performance`, `get_keyword_performance`, `get_search_terms`, and `get_ad_performance` accept `compact=true`: account totals, breakdowns, top-10 rows, and pre-computed offender lists (zero-conversion spenders, low-QS keywords, negative-keyword candidates, thin RSAs) instead of every row. ~90% smaller responses — built for account audits so raw tables don't flood your AI's context.
|
|
107
|
+
|
|
84
108
|
### Cross-Reference Tools (GA4 + Ads Combined)
|
|
85
109
|
|
|
86
110
|
These tools call both APIs internally and return unified results with auto-generated insights. They're the core of what makes AdLoop different from having separate GA4 and Ads tools.
|
|
@@ -98,11 +122,47 @@ These tools call both APIs internally and return unified results with auto-gener
|
|
|
98
122
|
| `validate_tracking` | Compare event names found in your codebase against what GA4 actually records. Returns matched, missing, and unexpected events with diagnostics. |
|
|
99
123
|
| `generate_tracking_code` | Generate ready-to-paste GA4 gtag JavaScript for any event, with recommended parameters for well-known events (sign_up, purchase, etc.) and optional trigger wrappers. |
|
|
100
124
|
|
|
125
|
+
### Google Tag Manager Tools
|
|
126
|
+
|
|
127
|
+
These tools read the live GTM container and join it with the codebase + GA4 to find tracking gaps that pure GA4 inspection can't catch — page-scoped triggers, paused tags, dynamic event names, brittle CSS selectors, and codebase events with no tag wired up at all.
|
|
128
|
+
|
|
129
|
+
| Tool | What It Does |
|
|
130
|
+
|------|-------------|
|
|
131
|
+
| `audit_event_coverage` | **The flagship.** Three-way join: codebase events ↔ GTM tags ↔ GA4 actual fires. For each event name in `expected_events`, returns one of 10 statuses (`ok`, `no_tag_no_fire`, `tag_paused`, `tag_active_but_not_firing`, `gtm_only_firing`, `ga4_only`, etc.) plus auto-generated insights for the gaps. |
|
|
132
|
+
| `list_gtm_accounts` | Discover accessible GTM accounts |
|
|
133
|
+
| `list_gtm_containers` | List containers under an account — returns numeric `container_id` (needed by other tools), public `GTM-XXXXXXX` ID, and usage context (web/iOS/Android/server) |
|
|
134
|
+
| `list_gtm_tags` | Every tag in the live container with parsed event names and resolved firing/blocking trigger names |
|
|
135
|
+
| `get_gtm_tag` | Full raw config for a single tag — every parameter, firing/blocking triggers with filter conditions, priority, pause status, sampling |
|
|
136
|
+
| `list_gtm_triggers` | Every trigger with filter conditions parsed to readable text (e.g. `{{Page Path}} contains service-promotions`, `{{Form ID}} NOT contains wf-form-...`). Renders the `negate` flag explicitly. |
|
|
137
|
+
| `get_gtm_trigger` | Full trigger config + reverse lookup of every tag that uses it. Includes parsed `element_visibility` block (selector, on-screen ratio, firing frequency) for elementVisibility triggers and `group_member_trigger_ids` for triggerGroup types |
|
|
138
|
+
| `list_gtm_variables` | Custom variables (data layer, constants, JS) plus enabled built-in variables |
|
|
139
|
+
| `list_gtm_workspaces` | List drafts (workspaces) under a container — workspace IDs are needed by `get_gtm_workspace_diff` |
|
|
140
|
+
| `get_gtm_workspace_diff` | Drafted-but-not-published changes — common cause of "I edited a tag but nothing happened". Returns `is_clean: true` when nothing is pending. |
|
|
141
|
+
| `list_gtm_versions` | Publish history with version IDs and entity counts. Use to correlate a metric drop with a recent publish. |
|
|
142
|
+
| `get_gtm_version` | Full metadata + tag/trigger names for a single historical container version |
|
|
143
|
+
|
|
144
|
+
> **Setup for GTM tools** — Enable the **Tag Manager API v2** in your GCP project, then add your AdLoop credentials' email (the OAuth user, or the service account email if using a service account) as a **Read** user on the GTM container under Admin → User Management. Service accounts pick up access on the next call. **OAuth users upgrading from an earlier AdLoop version must re-authorize once**: the GTM scope is new, so delete `~/.adloop/token.json` and run any tool to re-consent — until then GTM tools return a permissions error.
|
|
145
|
+
|
|
146
|
+
### Search Console Tools
|
|
147
|
+
|
|
148
|
+
| Tool | What It Does |
|
|
149
|
+
|------|-------------|
|
|
150
|
+
| `list_gsc_sites` | List Search Console properties the connected account can access |
|
|
151
|
+
| `run_gsc_report` | Organic search analytics — clicks, impressions, CTR, position by query/page/country/device/date |
|
|
152
|
+
|
|
153
|
+
### Web Performance Tools
|
|
154
|
+
|
|
155
|
+
| Tool | What It Does |
|
|
156
|
+
|------|-------------|
|
|
157
|
+
| `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
|
+
|
|
159
|
+
> **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
|
+
|
|
101
161
|
### Planning Tools
|
|
102
162
|
|
|
103
163
|
| Tool | What It Does |
|
|
104
164
|
|------|-------------|
|
|
105
|
-
| `discover_keywords` | Discover new keyword ideas from seed keywords and/or a URL using Google Ads Keyword Planner. Returns avg monthly searches, competition level, and top-of-page bid range. |
|
|
165
|
+
| `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. |
|
|
106
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. |
|
|
107
167
|
|
|
108
168
|
### Google Ads Write Tools
|
|
@@ -121,6 +181,8 @@ All write operations follow a **draft → preview → confirm** workflow. Nothin
|
|
|
121
181
|
| `draft_image_assets` | Create campaign image assets from local PNG, JPEG, or GIF files. |
|
|
122
182
|
| `draft_keywords` | Propose keyword additions with match types. Proactively checks bidding strategy — blocks BROAD match on Manual CPC campaigns. |
|
|
123
183
|
| `add_negative_keywords` | Propose negative keywords directly on a campaign |
|
|
184
|
+
| `add_negative_locations` | Propose negative geo exclusions on a campaign — exclude cities/regions while keeping broader positive targets |
|
|
185
|
+
| `draft_demographic_targeting` | Propose demographic criteria (age, gender, parental status, income) — exclusions by default |
|
|
124
186
|
| `propose_negative_keyword_list` | Draft a shared negative keyword list (SharedSet) and attach it to a campaign — reusable across multiple campaigns |
|
|
125
187
|
| `pause_entity` | Pause a campaign, ad group, ad, or keyword |
|
|
126
188
|
| `enable_entity` | Re-enable a paused entity |
|
|
@@ -178,6 +240,9 @@ AdLoop manages real ad spend, so safety is not optional.
|
|
|
178
240
|
>
|
|
179
241
|
> *(Upgrading from ≤0.9 with built-in credentials? Those sign-ins were retired in 0.10 — run `adloop init` once to switch to your own project.)*
|
|
180
242
|
|
|
243
|
+
> [!IMPORTANT]
|
|
244
|
+
> **Seeing `deleted_client: The OAuth client was deleted.` or `invalid_client`?** The shared Google Cloud project behind AdLoop ≤0.9's bundled credentials has been shut down, so its stored sign-ins no longer refresh. Two ways forward: **[AdLoop Cloud](https://getadloop.com)** (connect Google in two clicks, nothing to configure) or stay self-hosted with `pip install -U adloop && adloop init` to set up your own free Google Cloud project. Details in [the pinned issue (#49)](https://github.com/kLOsk/adloop/issues/49).
|
|
245
|
+
|
|
181
246
|
### Install
|
|
182
247
|
|
|
183
248
|
**From PyPI:**
|
|
@@ -320,6 +385,7 @@ Ask your AI assistant things like:
|
|
|
320
385
|
- *"Draft a new responsive search ad for my main campaign."*
|
|
321
386
|
- *"Which landing pages get paid traffic but don't convert?"*
|
|
322
387
|
- *"Is my tracking set up correctly? Compare my codebase events against GA4."*
|
|
388
|
+
- *"Audit my Google Tag Manager container — which conversions are being captured and where are the gaps?"*
|
|
323
389
|
- *"What keywords should I target for [product]? Find ideas and estimate the budget."*
|
|
324
390
|
- *"How much budget would I need for these keywords in Germany?"*
|
|
325
391
|
- *"Create a new search campaign for [product feature] with a €20/day budget."*
|
|
@@ -346,11 +412,11 @@ All configuration lives in `~/.adloop/config.yaml`. See [`config.yaml.example`](
|
|
|
346
412
|
```
|
|
347
413
|
src/adloop/
|
|
348
414
|
├── __init__.py # Entry point — routes 'adloop init' to wizard, otherwise starts MCP server
|
|
349
|
-
├── server.py # FastMCP server —
|
|
415
|
+
├── server.py # FastMCP server — 64 tool registrations with safety annotations
|
|
350
416
|
├── config.py # Config loader (~/.adloop/config.yaml)
|
|
351
|
-
├── auth.py # OAuth 2.0 flow (user-supplied credentials, headless fallback) + service accounts
|
|
417
|
+
├── auth.py # OAuth 2.0 flow (user-supplied credentials, headless fallback) + service accounts; GA4 / Ads / GTM scopes
|
|
352
418
|
├── cli.py # Interactive 'adloop init' setup wizard
|
|
353
|
-
├── crossref.py # Cross-reference tools (GA4 + Ads combined analysis)
|
|
419
|
+
├── crossref.py # Cross-reference tools (GA4 + Ads + GTM combined analysis)
|
|
354
420
|
├── tracking.py # Tracking validation + code generation tools
|
|
355
421
|
├── ga4/
|
|
356
422
|
│ ├── client.py # GA4 Data + Admin API clients
|
|
@@ -363,6 +429,9 @@ src/adloop/
|
|
|
363
429
|
│ ├── pmax.py # Performance Max tools — campaign/asset group performance, asset labels, top combinations
|
|
364
430
|
│ ├── write.py # Draft campaign, RSA, keywords; pause, enable, remove, confirm
|
|
365
431
|
│ └── forecast.py # Budget estimation + keyword discovery via Keyword Planner API
|
|
432
|
+
├── gtm/
|
|
433
|
+
│ ├── client.py # Google Tag Manager API v2 client
|
|
434
|
+
│ └── read.py # Live container fetching, tag/trigger/variable parsing, workspace diff, version history
|
|
366
435
|
└── safety/
|
|
367
436
|
├── guards.py # Budget caps, bid limits, blocked operations, Broad Match safety
|
|
368
437
|
├── preview.py # Change plans and previews
|
|
@@ -384,9 +453,10 @@ What's been shipped and what's next:
|
|
|
384
453
|
- ~~Claude Code support~~ ✓ — `CLAUDE.md`, `.mcp.json`, `.claude/rules/`, `.claude/commands/`, CLI wizard snippets
|
|
385
454
|
- **Claude Desktop one-click install** — `adloop install claude-desktop` (and/or a `.dxt` extension bundle) that writes the AdLoop MCP entry into `claude_desktop_config.json` automatically, so Claude Desktop + Cowork users don't have to hand-edit JSON
|
|
386
455
|
- ~~PyPI package~~ ✓ — `pip install adloop`
|
|
387
|
-
-
|
|
456
|
+
- ~~[AdLoop Cloud](https://getadloop.com)~~ ✓ — the hosted version, live in beta: no Google Cloud project, no developer token, connect Google in two clicks (EU-hosted, GDPR-first)
|
|
388
457
|
- ~~Headless server support~~ ✓ — manual URL copy-paste flow for servers without a browser
|
|
389
458
|
- ~~Behavioral eval suites~~ ✓ — 28 prompt-and-expectation tests covering read, write, tracking, and planning workflows
|
|
459
|
+
- ~~Google Tag Manager integration~~ ✓ — read tools for tags, triggers, variables, workspaces, and version history, plus the `audit_event_coverage` three-way join across codebase events, GTM tags, and GA4 actual fires
|
|
390
460
|
- **Community launch** — HN, Indie Hackers, r/cursor, Twitter
|
|
391
461
|
- **Video walkthrough**
|
|
392
462
|
|
|
@@ -400,14 +470,14 @@ MIT — see [LICENSE](LICENSE).
|
|
|
400
470
|
|
|
401
471
|
## Privacy
|
|
402
472
|
|
|
403
|
-
|
|
473
|
+
The open-source version runs entirely on your machine. No data is collected, stored, or transmitted to any server. See [PRIVACY.md](PRIVACY.md) for the full privacy policy. AdLoop Cloud has its own [privacy policy](https://getadloop.com/datenschutz) and [DPA](https://getadloop.com/avv).
|
|
404
474
|
|
|
405
475
|
---
|
|
406
476
|
|
|
407
477
|
<div align="center">
|
|
408
478
|
|
|
409
|
-
**If AdLoop helps you run Google Ads, GA4, and tracking code from one place — [give it a star](https://github.com/kLOsk/adloop).**
|
|
479
|
+
**If AdLoop helps you run Google Ads, GA4, and tracking code from one place — [give it a star](https://github.com/kLOsk/adloop) or [try the hosted version](https://getadloop.com).**
|
|
410
480
|
|
|
411
|
-
Made by [@kLOsk](https://github.com/kLOsk) | [Privacy Policy](PRIVACY.md)
|
|
481
|
+
Made by [@kLOsk](https://github.com/kLOsk) | [AdLoop Cloud](https://getadloop.com) | [Privacy Policy](PRIVACY.md)
|
|
412
482
|
|
|
413
483
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "adloop"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.12.0"
|
|
4
4
|
description = "The AI command center for Google Ads, GA4, and tracking code."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -14,7 +14,9 @@ dependencies = [
|
|
|
14
14
|
"google-ads>=29.0.0",
|
|
15
15
|
"google-analytics-data>=0.20.0",
|
|
16
16
|
"google-analytics-admin>=0.27.0",
|
|
17
|
+
"google-api-python-client>=2.100.0",
|
|
17
18
|
"google-auth-oauthlib>=1.0.0",
|
|
19
|
+
"google-api-python-client>=2.0.0",
|
|
18
20
|
"pyyaml>=6.0",
|
|
19
21
|
]
|
|
20
22
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"""AdLoop — MCP server connecting Google Ads + GA4 + codebase."""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
try:
|
|
7
|
+
__version__ = version("adloop")
|
|
8
|
+
except PackageNotFoundError: # running from a source tree without install
|
|
9
|
+
__version__ = "0.0.0.dev0"
|
|
6
10
|
|
|
7
11
|
|
|
8
12
|
def main() -> None:
|
|
@@ -295,6 +295,69 @@ def _post_keyword_ideas_rest_page(
|
|
|
295
295
|
return response.json()
|
|
296
296
|
|
|
297
297
|
|
|
298
|
+
|
|
299
|
+
_MONTH_NUMBERS = {
|
|
300
|
+
"JANUARY": 1, "FEBRUARY": 2, "MARCH": 3, "APRIL": 4, "MAY": 5,
|
|
301
|
+
"JUNE": 6, "JULY": 7, "AUGUST": 8, "SEPTEMBER": 9, "OCTOBER": 10,
|
|
302
|
+
"NOVEMBER": 11, "DECEMBER": 12,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
_MONTH_NAMES = {v: k.capitalize() for k, v in _MONTH_NUMBERS.items()}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _parse_monthly_volumes(raw: list[dict]) -> list[dict]:
|
|
309
|
+
"""REST monthlySearchVolumes → compact chronological history.
|
|
310
|
+
|
|
311
|
+
Keeps the last 24 months: enough to see seasonality across two cycles
|
|
312
|
+
without flooding the response (the API returns up to ~4 years).
|
|
313
|
+
"""
|
|
314
|
+
volumes = []
|
|
315
|
+
for row in raw:
|
|
316
|
+
month = _MONTH_NUMBERS.get(str(row.get("month", "")).upper())
|
|
317
|
+
year = _maybe_int(row.get("year"))
|
|
318
|
+
if month is None or year is None:
|
|
319
|
+
continue
|
|
320
|
+
volumes.append({
|
|
321
|
+
"year": year,
|
|
322
|
+
"month": month,
|
|
323
|
+
"searches": _maybe_int(row.get("monthlySearches")) or 0,
|
|
324
|
+
})
|
|
325
|
+
volumes.sort(key=lambda v: (v["year"], v["month"]))
|
|
326
|
+
return volumes[-24:]
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _seasonality_insight(idea: dict) -> str | None:
|
|
330
|
+
"""Name the peak months when demand is meaningfully seasonal (>=40%
|
|
331
|
+
above the keyword's own monthly average)."""
|
|
332
|
+
volumes = idea.get("monthly_search_volumes") or []
|
|
333
|
+
if len(volumes) < 12:
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
by_month: dict[int, list[int]] = {}
|
|
337
|
+
for v in volumes:
|
|
338
|
+
by_month.setdefault(v["month"], []).append(v["searches"])
|
|
339
|
+
month_avgs = {m: sum(s) / len(s) for m, s in by_month.items()}
|
|
340
|
+
overall = sum(month_avgs.values()) / len(month_avgs)
|
|
341
|
+
if overall <= 0:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
peaks = sorted(
|
|
345
|
+
(m for m, avg in month_avgs.items() if avg >= overall * 1.4),
|
|
346
|
+
key=lambda m: month_avgs[m],
|
|
347
|
+
reverse=True,
|
|
348
|
+
)
|
|
349
|
+
if not peaks:
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
names = ", ".join(_MONTH_NAMES[m] for m in peaks[:3])
|
|
353
|
+
ratio = month_avgs[peaks[0]] / overall
|
|
354
|
+
return (
|
|
355
|
+
f"'{idea['keyword']}' is seasonal: demand peaks in {names} "
|
|
356
|
+
f"(up to {ratio:.1f}x its monthly average). Plan budgets and "
|
|
357
|
+
f"campaign launches around the ramp-up, not the peak itself."
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
298
361
|
def discover_keywords(
|
|
299
362
|
config: AdLoopConfig,
|
|
300
363
|
*,
|
|
@@ -304,6 +367,7 @@ def discover_keywords(
|
|
|
304
367
|
language_id: str = "1000",
|
|
305
368
|
page_size: int = 50,
|
|
306
369
|
customer_id: str = "",
|
|
370
|
+
include_monthly_volumes: bool = False,
|
|
307
371
|
) -> dict:
|
|
308
372
|
"""Discover new keyword ideas using Google Ads Keyword Planner.
|
|
309
373
|
|
|
@@ -320,6 +384,9 @@ def discover_keywords(
|
|
|
320
384
|
geo_target_id: geo target constant (2276=Germany, 2840=USA, 2826=UK)
|
|
321
385
|
language_id: language constant (1000=English, 1001=German, 1002=French)
|
|
322
386
|
page_size: max number of keyword ideas to return (default 50, max 1000)
|
|
387
|
+
include_monthly_volumes: attach per-month search volume history (up to
|
|
388
|
+
~4 years, trimmed to the last 24 months, top 20 ideas only) plus a
|
|
389
|
+
seasonality insight — the "Google Trends" view for keyword demand.
|
|
323
390
|
|
|
324
391
|
Network: this tool intentionally bypasses the google-ads gRPC client for
|
|
325
392
|
KeywordPlanIdeaService and calls the v23 REST endpoint directly. The
|
|
@@ -358,7 +425,7 @@ def discover_keywords(
|
|
|
358
425
|
# ``_micros_to_currency`` preserve legitimate 0 values that
|
|
359
426
|
# falsy checks would otherwise silently drop. See the helper
|
|
360
427
|
# docstrings for the failure modes this defends against.
|
|
361
|
-
|
|
428
|
+
entry = {
|
|
362
429
|
"keyword": idea.get("text", ""),
|
|
363
430
|
"avg_monthly_searches": _maybe_int(metrics.get("avgMonthlySearches")),
|
|
364
431
|
"competition": competition,
|
|
@@ -369,7 +436,12 @@ def discover_keywords(
|
|
|
369
436
|
"high_top_of_page_bid": _micros_to_currency(
|
|
370
437
|
_maybe_int(metrics.get("highTopOfPageBidMicros"))
|
|
371
438
|
),
|
|
372
|
-
}
|
|
439
|
+
}
|
|
440
|
+
if include_monthly_volumes:
|
|
441
|
+
entry["monthly_search_volumes"] = _parse_monthly_volumes(
|
|
442
|
+
metrics.get("monthlySearchVolumes") or []
|
|
443
|
+
)
|
|
444
|
+
ideas.append(entry)
|
|
373
445
|
|
|
374
446
|
page_token = payload.get("nextPageToken") or ""
|
|
375
447
|
if not page_token:
|
|
@@ -378,6 +450,13 @@ def discover_keywords(
|
|
|
378
450
|
# Sort by avg monthly searches descending (None last)
|
|
379
451
|
ideas.sort(key=lambda x: x["avg_monthly_searches"] or 0, reverse=True)
|
|
380
452
|
|
|
453
|
+
if include_monthly_volumes:
|
|
454
|
+
# History is context-heavy (24 rows per keyword) — keep it on the
|
|
455
|
+
# ideas that matter and drop it from the long tail.
|
|
456
|
+
for i, idea in enumerate(ideas):
|
|
457
|
+
if i >= 20:
|
|
458
|
+
idea.pop("monthly_search_volumes", None)
|
|
459
|
+
|
|
381
460
|
insights = []
|
|
382
461
|
if ideas:
|
|
383
462
|
high_competition = [i for i in ideas if i["competition"] == "HIGH"]
|
|
@@ -399,6 +478,9 @@ def discover_keywords(
|
|
|
399
478
|
f"Highest-volume idea: '{top['keyword']}' with ~{top['avg_monthly_searches']:,} "
|
|
400
479
|
f"avg monthly searches."
|
|
401
480
|
)
|
|
481
|
+
seasonality = _seasonality_insight(top)
|
|
482
|
+
if seasonality:
|
|
483
|
+
insights.append(seasonality)
|
|
402
484
|
|
|
403
485
|
return {
|
|
404
486
|
"keyword_ideas": ideas,
|