adloop 0.27.2__tar.gz → 0.28.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.27.2 → adloop-0.28.0}/PKG-INFO +11 -3
- {adloop-0.27.2 → adloop-0.28.0}/README.md +10 -2
- {adloop-0.27.2 → adloop-0.28.0}/pyproject.toml +1 -1
- {adloop-0.27.2 → adloop-0.28.0}/pyproject.toml.orig +1 -1
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/cli.py +6 -3
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/server.py +34 -8
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/__init__.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/__main__.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/_mcp_patches.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/__init__.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/ai_max.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/brands.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/client.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/conversion_actions.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/currency.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/custom_conversion_goals.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/enums.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/forecast.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/gaql.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/image_fetch.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/pmax.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/read.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/shopping.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/validate_only.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/write.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/auth.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/config.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/crossref.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/diagnostics.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ga4/__init__.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ga4/client.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ga4/reports.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ga4/tracking.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ga4/write.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gsc/__init__.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gsc/client.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gsc/reports.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gtm/__init__.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gtm/client.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gtm/entities.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gtm/read.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gtm/write.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/merchant/__init__.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/merchant/client.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/merchant/read.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/pagespeed.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/reddit/__init__.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/reddit/auth.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/reddit/client.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/reddit/read.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/reddit/schedule.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/reddit/write.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/__init__.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/adloop.md +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/commands/analyze-performance.md +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/commands/budget-plan.md +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/commands/create-ad.md +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/commands/create-campaign.md +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/commands/optimize-campaign.md +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules_install.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/runtime.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/safety/__init__.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/safety/approval.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/safety/approval_card.html +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/safety/audit.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/safety/guards.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/safety/preview.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/tracking.py +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/upload_card.html +0 -0
- {adloop-0.27.2 → adloop-0.28.0}/src/adloop/uploads.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: adloop
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.28.0
|
|
4
4
|
Summary: The AI command center for Google Ads, Reddit Ads, GA4, and tracking code.
|
|
5
5
|
Keywords: mcp,google-ads,reddit-ads,google-analytics,ga4,cursor,marketing
|
|
6
6
|
Author: Daniel Klose
|
|
@@ -557,7 +557,7 @@ All configuration lives in `~/.adloop/config.yaml`. See [`config.yaml.example`](
|
|
|
557
557
|
|
|
558
558
|
### Toolsets — trim the context footprint
|
|
559
559
|
|
|
560
|
-
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
|
|
560
|
+
Most MCP clients (claude.ai, ChatGPT, Cursor, …) load **every tool schema into the model's context at the start of every conversation**. With AdLoop's full catalog that's a noticeable share of the context window, 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):
|
|
561
561
|
|
|
562
562
|
```json
|
|
563
563
|
"env": { "ADLOOP_TOOLSETS": "ads,ga4" }
|
|
@@ -574,7 +574,15 @@ Most MCP clients (claude.ai, ChatGPT, Cursor, …) load **every tool schema into
|
|
|
574
574
|
| `merchant` | Merchant Center feed health |
|
|
575
575
|
| `reddit` | Reddit Ads reads, writes, and planning |
|
|
576
576
|
|
|
577
|
-
|
|
577
|
+
Google Ads can be narrowed further. Use one of these instead of `ads`:
|
|
578
|
+
|
|
579
|
+
| Subset | Covers |
|
|
580
|
+
|---|---|
|
|
581
|
+
| `ads:read` | Ads reports, GAQL, and the Ads/Analytics cross-checks (`attribution_check`, `analyze_campaign_conversions`, `landing_page_analysis`) |
|
|
582
|
+
| `ads:write` | Ads drafts and changes; turns on `ads:read` too, because a draft needs the reads first (IDs, bidding strategy) |
|
|
583
|
+
| `ads:plan` | Keyword ideas and budget forecasts (`discover_keywords`, `estimate_budget`) |
|
|
584
|
+
|
|
585
|
+
`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:read,ga4` costs about 40% of `ads,ga4` for audits and diagnosis, and an `ads:plan`-only client for keyword research pays about 5% of the full catalog. Toolsets are per *client*, not per install: one AdLoop config can serve a trimmed Cursor and a full-catalog Claude Code side by side.
|
|
578
586
|
|
|
579
587
|
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.
|
|
580
588
|
|
|
@@ -529,7 +529,7 @@ All configuration lives in `~/.adloop/config.yaml`. See [`config.yaml.example`](
|
|
|
529
529
|
|
|
530
530
|
### Toolsets — trim the context footprint
|
|
531
531
|
|
|
532
|
-
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
|
|
532
|
+
Most MCP clients (claude.ai, ChatGPT, Cursor, …) load **every tool schema into the model's context at the start of every conversation**. With AdLoop's full catalog that's a noticeable share of the context window, 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):
|
|
533
533
|
|
|
534
534
|
```json
|
|
535
535
|
"env": { "ADLOOP_TOOLSETS": "ads,ga4" }
|
|
@@ -546,7 +546,15 @@ Most MCP clients (claude.ai, ChatGPT, Cursor, …) load **every tool schema into
|
|
|
546
546
|
| `merchant` | Merchant Center feed health |
|
|
547
547
|
| `reddit` | Reddit Ads reads, writes, and planning |
|
|
548
548
|
|
|
549
|
-
|
|
549
|
+
Google Ads can be narrowed further. Use one of these instead of `ads`:
|
|
550
|
+
|
|
551
|
+
| Subset | Covers |
|
|
552
|
+
|---|---|
|
|
553
|
+
| `ads:read` | Ads reports, GAQL, and the Ads/Analytics cross-checks (`attribution_check`, `analyze_campaign_conversions`, `landing_page_analysis`) |
|
|
554
|
+
| `ads:write` | Ads drafts and changes; turns on `ads:read` too, because a draft needs the reads first (IDs, bidding strategy) |
|
|
555
|
+
| `ads:plan` | Keyword ideas and budget forecasts (`discover_keywords`, `estimate_budget`) |
|
|
556
|
+
|
|
557
|
+
`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:read,ga4` costs about 40% of `ads,ga4` for audits and diagnosis, and an `ads:plan`-only client for keyword research pays about 5% of the full catalog. Toolsets are per *client*, not per install: one AdLoop config can serve a trimmed Cursor and a full-catalog Claude Code side by side.
|
|
550
558
|
|
|
551
559
|
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.
|
|
552
560
|
|
|
@@ -281,13 +281,16 @@ def _prompt_toolsets() -> str:
|
|
|
281
281
|
Client-side (env), not config.yaml: the same machine can run one
|
|
282
282
|
full-catalog client and one trimmed client off one AdLoop config.
|
|
283
283
|
"""
|
|
284
|
-
from adloop.server import TOOLSETS
|
|
284
|
+
from adloop.server import ADS_SUBSETS, TOOLSETS
|
|
285
285
|
|
|
286
286
|
_print(" AdLoop can expose only some toolsets to your AI client —")
|
|
287
287
|
_print(" a smaller tool list costs less context per conversation.")
|
|
288
288
|
_print()
|
|
289
289
|
for slug, description in TOOLSETS.items():
|
|
290
290
|
_print(f" {slug:<9} {description}")
|
|
291
|
+
_print(" Or only part of Google Ads instead of ads:")
|
|
292
|
+
for slug, description in ADS_SUBSETS.items():
|
|
293
|
+
_print(f" {slug:<9} {description}")
|
|
291
294
|
_print()
|
|
292
295
|
_print(" (health_check and confirm_and_apply are always included.)")
|
|
293
296
|
if _prompt_bool("Expose the full toolset?", default=True):
|
|
@@ -295,12 +298,12 @@ def _prompt_toolsets() -> str:
|
|
|
295
298
|
while True:
|
|
296
299
|
raw = _prompt("Toolsets (comma-separated, e.g. ads,ga4)")
|
|
297
300
|
chosen = [part.strip().lower() for part in raw.split(",") if part.strip()]
|
|
298
|
-
unknown = [slug for slug in chosen if slug not in TOOLSETS]
|
|
301
|
+
unknown = [slug for slug in chosen if slug not in TOOLSETS and slug not in ADS_SUBSETS]
|
|
299
302
|
if chosen and not unknown:
|
|
300
303
|
return ",".join(dict.fromkeys(chosen))
|
|
301
304
|
_print(
|
|
302
305
|
f" ⚠ Unknown toolset(s): {', '.join(unknown) or '(none given)'}."
|
|
303
|
-
f" Valid: {', '.join(TOOLSETS)}"
|
|
306
|
+
f" Valid: {', '.join([*TOOLSETS, *ADS_SUBSETS])}"
|
|
304
307
|
)
|
|
305
308
|
|
|
306
309
|
|
|
@@ -48,6 +48,18 @@ TOOLSETS: dict[str, str] = {
|
|
|
48
48
|
"reddit": "Reddit Ads reads, writes, and planning",
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
# Finer selections inside "ads" (issue #56): every ads tool also carries
|
|
52
|
+
# exactly one of these, derived in _tool() from its annotations, so "ads"
|
|
53
|
+
# keeps its meaning and a smaller session can ask for just the part it
|
|
54
|
+
# needs. The cross-reference tools that query Ads carry "ads:read" next to
|
|
55
|
+
# their "tracking" tag. Not part of the Cloud contract above.
|
|
56
|
+
ADS_SUBSETS: dict[str, str] = {
|
|
57
|
+
"ads:read": "Google Ads reports, GAQL and the Ads/Analytics cross-checks",
|
|
58
|
+
"ads:write": "Google Ads drafts and changes (turns on ads:read too)",
|
|
59
|
+
"ads:plan": "Keyword ideas and budget forecasts",
|
|
60
|
+
}
|
|
61
|
+
_ADS_PLAN_TOOLS = frozenset({"discover_keywords", "estimate_budget"})
|
|
62
|
+
|
|
51
63
|
|
|
52
64
|
def _coerce_json_string_to_list(value):
|
|
53
65
|
"""Decode a JSON-array-shaped string into a native list.
|
|
@@ -387,17 +399,25 @@ def _tool(*, title: str, annotations: ToolAnnotations, tags: set[str], **kwargs)
|
|
|
387
399
|
description = f"{description}\n\n{docs}"
|
|
388
400
|
name = kwargs.get("name") or fn.__name__
|
|
389
401
|
options = dict(kwargs)
|
|
402
|
+
tool_tags = set(tags)
|
|
403
|
+
if "ads" in tool_tags and not tool_tags & set(ADS_SUBSETS):
|
|
404
|
+
if name in _ADS_PLAN_TOOLS:
|
|
405
|
+
tool_tags.add("ads:plan")
|
|
406
|
+
elif annotations.read_only_hint:
|
|
407
|
+
tool_tags.add("ads:read")
|
|
408
|
+
else:
|
|
409
|
+
tool_tags.add("ads:write")
|
|
390
410
|
if not annotations.read_only_hint and name not in _NO_CARD_TOOLS:
|
|
391
411
|
# Write tools return a preview; apps with MCP Apps support
|
|
392
412
|
# render it as the approval card.
|
|
393
413
|
options["meta"] = {**(options.get("meta") or {}), "ui": {"resourceUri": CARD_URI}}
|
|
394
414
|
fn = _with_approval_card(fn)
|
|
395
415
|
if not is_app_only(options.get("meta")):
|
|
396
|
-
_TOOL_TAGS[name] = frozenset(
|
|
416
|
+
_TOOL_TAGS[name] = frozenset(tool_tags)
|
|
397
417
|
return mcp.tool(
|
|
398
418
|
title=title,
|
|
399
419
|
annotations=annotations,
|
|
400
|
-
tags=
|
|
420
|
+
tags=tool_tags,
|
|
401
421
|
description=description,
|
|
402
422
|
**options,
|
|
403
423
|
)(fn)
|
|
@@ -1739,7 +1759,7 @@ def get_change_history(
|
|
|
1739
1759
|
@_tool(
|
|
1740
1760
|
title="Campaign conversions against Analytics",
|
|
1741
1761
|
annotations=_READONLY,
|
|
1742
|
-
tags={"tracking"},
|
|
1762
|
+
tags={"tracking", "ads:read"},
|
|
1743
1763
|
)
|
|
1744
1764
|
@_safe
|
|
1745
1765
|
def analyze_campaign_conversions(
|
|
@@ -1780,7 +1800,7 @@ def analyze_campaign_conversions(
|
|
|
1780
1800
|
)
|
|
1781
1801
|
|
|
1782
1802
|
|
|
1783
|
-
@_tool(title="Landing page analysis", annotations=_READONLY, tags={"tracking"})
|
|
1803
|
+
@_tool(title="Landing page analysis", annotations=_READONLY, tags={"tracking", "ads:read"})
|
|
1784
1804
|
@_safe
|
|
1785
1805
|
def landing_page_analysis(
|
|
1786
1806
|
date_range_start: str = "",
|
|
@@ -1815,7 +1835,7 @@ def landing_page_analysis(
|
|
|
1815
1835
|
)
|
|
1816
1836
|
|
|
1817
1837
|
|
|
1818
|
-
@_tool(title="Attribution check", annotations=_READONLY, tags={"tracking"})
|
|
1838
|
+
@_tool(title="Attribution check", annotations=_READONLY, tags={"tracking", "ads:read"})
|
|
1819
1839
|
@_safe
|
|
1820
1840
|
def attribution_check(
|
|
1821
1841
|
date_range_start: str = "",
|
|
@@ -6475,18 +6495,24 @@ def _apply_toolsets_env() -> None:
|
|
|
6475
6495
|
|
|
6476
6496
|
Unset/empty = the full catalog. Core tools (health_check,
|
|
6477
6497
|
confirm_and_apply) survive every selection. A smaller tools/list costs
|
|
6478
|
-
less context in MCP clients that load all tool schemas upfront.
|
|
6498
|
+
less context in MCP clients that load all tool schemas upfront. Besides
|
|
6499
|
+
the toolsets, the ads subsets (ads:read, ads:write, ads:plan) narrow
|
|
6500
|
+
Google Ads further; ads:write brings ads:read along, because a draft
|
|
6501
|
+
needs the reads first (IDs, bidding strategy).
|
|
6479
6502
|
"""
|
|
6480
6503
|
raw = _os.getenv("ADLOOP_TOOLSETS", "").strip()
|
|
6481
6504
|
if not raw:
|
|
6482
6505
|
return
|
|
6483
6506
|
requested = {part.strip().lower() for part in raw.split(",") if part.strip()}
|
|
6484
|
-
unknown = sorted(requested - set(TOOLSETS))
|
|
6507
|
+
unknown = sorted(requested - set(TOOLSETS) - set(ADS_SUBSETS))
|
|
6485
6508
|
if unknown:
|
|
6486
6509
|
raise ValueError(
|
|
6487
6510
|
f"ADLOOP_TOOLSETS names unknown toolset(s): {', '.join(unknown)}. "
|
|
6488
|
-
f"Valid toolsets: {', '.join(TOOLSETS)}.
|
|
6511
|
+
f"Valid toolsets: {', '.join([*TOOLSETS, *ADS_SUBSETS])}. "
|
|
6512
|
+
"Example: ADLOOP_TOOLSETS=ads,ga4 or ADLOOP_TOOLSETS=ads:read,ga4"
|
|
6489
6513
|
)
|
|
6514
|
+
if "ads:write" in requested:
|
|
6515
|
+
requested.add("ads:read")
|
|
6490
6516
|
global _ENABLED_TAGS
|
|
6491
6517
|
_ENABLED_TAGS = requested | {"core"}
|
|
6492
6518
|
mcp.enable(tags=_ENABLED_TAGS, only=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|