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.
Files changed (71) hide show
  1. {adloop-0.27.2 → adloop-0.28.0}/PKG-INFO +11 -3
  2. {adloop-0.27.2 → adloop-0.28.0}/README.md +10 -2
  3. {adloop-0.27.2 → adloop-0.28.0}/pyproject.toml +1 -1
  4. {adloop-0.27.2 → adloop-0.28.0}/pyproject.toml.orig +1 -1
  5. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/cli.py +6 -3
  6. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/server.py +34 -8
  7. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/__init__.py +0 -0
  8. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/__main__.py +0 -0
  9. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/_mcp_patches.py +0 -0
  10. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/__init__.py +0 -0
  11. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/ai_max.py +0 -0
  12. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/brands.py +0 -0
  13. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/client.py +0 -0
  14. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/conversion_actions.py +0 -0
  15. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/currency.py +0 -0
  16. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/custom_conversion_goals.py +0 -0
  17. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/enums.py +0 -0
  18. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/forecast.py +0 -0
  19. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/gaql.py +0 -0
  20. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/image_fetch.py +0 -0
  21. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/pmax.py +0 -0
  22. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/read.py +0 -0
  23. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/shopping.py +0 -0
  24. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/validate_only.py +0 -0
  25. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ads/write.py +0 -0
  26. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/auth.py +0 -0
  27. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/config.py +0 -0
  28. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/crossref.py +0 -0
  29. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/diagnostics.py +0 -0
  30. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ga4/__init__.py +0 -0
  31. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ga4/client.py +0 -0
  32. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ga4/reports.py +0 -0
  33. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ga4/tracking.py +0 -0
  34. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/ga4/write.py +0 -0
  35. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gsc/__init__.py +0 -0
  36. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gsc/client.py +0 -0
  37. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gsc/reports.py +0 -0
  38. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gtm/__init__.py +0 -0
  39. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gtm/client.py +0 -0
  40. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gtm/entities.py +0 -0
  41. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gtm/read.py +0 -0
  42. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/gtm/write.py +0 -0
  43. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/merchant/__init__.py +0 -0
  44. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/merchant/client.py +0 -0
  45. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/merchant/read.py +0 -0
  46. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/pagespeed.py +0 -0
  47. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/reddit/__init__.py +0 -0
  48. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/reddit/auth.py +0 -0
  49. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/reddit/client.py +0 -0
  50. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/reddit/read.py +0 -0
  51. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/reddit/schedule.py +0 -0
  52. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/reddit/write.py +0 -0
  53. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/__init__.py +0 -0
  54. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/adloop.md +0 -0
  55. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/commands/analyze-performance.md +0 -0
  56. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/commands/budget-plan.md +0 -0
  57. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/commands/create-ad.md +0 -0
  58. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/commands/create-campaign.md +0 -0
  59. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
  60. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules/commands/optimize-campaign.md +0 -0
  61. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/rules_install.py +0 -0
  62. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/runtime.py +0 -0
  63. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/safety/__init__.py +0 -0
  64. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/safety/approval.py +0 -0
  65. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/safety/approval_card.html +0 -0
  66. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/safety/audit.py +0 -0
  67. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/safety/guards.py +0 -0
  68. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/safety/preview.py +0 -0
  69. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/tracking.py +0 -0
  70. {adloop-0.27.2 → adloop-0.28.0}/src/adloop/upload_card.html +0 -0
  71. {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.27.2
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 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):
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
- `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.
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 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):
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
- `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.
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.27.2"
3
+ version = "0.28.0"
4
4
  description = "The AI command center for Google Ads, Reddit Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.27.2"
3
+ version = "0.28.0"
4
4
  description = "The AI command center for Google Ads, Reddit Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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(tags)
416
+ _TOOL_TAGS[name] = frozenset(tool_tags)
397
417
  return mcp.tool(
398
418
  title=title,
399
419
  annotations=annotations,
400
- tags=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)}. Example: ADLOOP_TOOLSETS=ads,ga4"
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