adloop 0.19.1__tar.gz → 0.19.3__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 (64) hide show
  1. {adloop-0.19.1 → adloop-0.19.3}/PKG-INFO +1 -1
  2. {adloop-0.19.1 → adloop-0.19.3}/pyproject.toml +1 -1
  3. {adloop-0.19.1 → adloop-0.19.3}/pyproject.toml.orig +1 -1
  4. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/write.py +21 -1
  5. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/server.py +138 -119
  6. {adloop-0.19.1 → adloop-0.19.3}/README.md +0 -0
  7. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/__init__.py +0 -0
  8. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/__main__.py +0 -0
  9. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/_mcp_patches.py +0 -0
  10. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/__init__.py +0 -0
  11. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/ai_max.py +0 -0
  12. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/brands.py +0 -0
  13. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/client.py +0 -0
  14. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/conversion_actions.py +0 -0
  15. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/currency.py +0 -0
  16. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/custom_conversion_goals.py +0 -0
  17. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/enums.py +0 -0
  18. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/forecast.py +0 -0
  19. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/gaql.py +0 -0
  20. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/image_fetch.py +0 -0
  21. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/pmax.py +0 -0
  22. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/read.py +0 -0
  23. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ads/validate_only.py +0 -0
  24. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/auth.py +0 -0
  25. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/cli.py +0 -0
  26. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/config.py +0 -0
  27. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/crossref.py +0 -0
  28. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/diagnostics.py +0 -0
  29. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ga4/__init__.py +0 -0
  30. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ga4/client.py +0 -0
  31. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ga4/reports.py +0 -0
  32. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ga4/tracking.py +0 -0
  33. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/ga4/write.py +0 -0
  34. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/gsc/__init__.py +0 -0
  35. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/gsc/client.py +0 -0
  36. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/gsc/reports.py +0 -0
  37. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/gtm/__init__.py +0 -0
  38. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/gtm/client.py +0 -0
  39. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/gtm/read.py +0 -0
  40. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/merchant/__init__.py +0 -0
  41. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/merchant/client.py +0 -0
  42. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/merchant/read.py +0 -0
  43. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/pagespeed.py +0 -0
  44. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/reddit/__init__.py +0 -0
  45. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/reddit/auth.py +0 -0
  46. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/reddit/client.py +0 -0
  47. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/reddit/read.py +0 -0
  48. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/reddit/schedule.py +0 -0
  49. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/reddit/write.py +0 -0
  50. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/rules/__init__.py +0 -0
  51. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/rules/adloop.md +0 -0
  52. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/rules/commands/analyze-performance.md +0 -0
  53. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/rules/commands/budget-plan.md +0 -0
  54. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/rules/commands/create-ad.md +0 -0
  55. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/rules/commands/create-campaign.md +0 -0
  56. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
  57. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/rules/commands/optimize-campaign.md +0 -0
  58. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/rules_install.py +0 -0
  59. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/runtime.py +0 -0
  60. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/safety/__init__.py +0 -0
  61. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/safety/audit.py +0 -0
  62. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/safety/guards.py +0 -0
  63. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/safety/preview.py +0 -0
  64. {adloop-0.19.1 → adloop-0.19.3}/src/adloop/tracking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: adloop
3
- Version: 0.19.1
3
+ Version: 0.19.3
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.19.1"
3
+ version = "0.19.3"
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.19.1"
3
+ version = "0.19.3"
4
4
  description = "The AI command center for Google Ads, Reddit Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -2780,7 +2780,27 @@ def confirm_and_apply(
2780
2780
  "objects an earlier step would create, so Google could "
2781
2781
  "only validate the step(s) before them."
2782
2782
  )
2783
- if forced_by_config:
2783
+ from adloop.runtime import deployment_mode as _deployment_mode
2784
+
2785
+ if forced_by_config and _deployment_mode() == "server":
2786
+ # Hosted: the flag is the workspace's live-changes setting,
2787
+ # switched in the dashboard. There is no config file to edit
2788
+ # and no server for the user to restart.
2789
+ response["dry_run_forced_by"] = "config.safety.require_dry_run"
2790
+ response["remediation"] = (
2791
+ "Live changes are switched off for this workspace (or for "
2792
+ "this client). The workspace owner can switch them on in "
2793
+ "the AdLoop Cloud dashboard under Settings → Safety. Passing "
2794
+ "dry_run=false on this tool will keep being overridden until "
2795
+ "then."
2796
+ )
2797
+ response["message"] = (
2798
+ "dry_run=false was IGNORED because live changes are switched "
2799
+ "off for this workspace. No changes were made. Retrying this "
2800
+ "tool with dry_run=false will not succeed until live changes "
2801
+ "are switched on in the dashboard."
2802
+ )
2803
+ elif forced_by_config:
2784
2804
  # The caller passed dry_run=false but safety.require_dry_run
2785
2805
  # forced it back on. Tell them exactly why and how to unlock
2786
2806
  # real writes — without this, agents (e.g. Claude Code) retry
@@ -166,6 +166,20 @@ mcp = FastMCP(
166
166
  )
167
167
 
168
168
 
169
+ def _tool(*, title: str, annotations: ToolAnnotations, **kwargs):
170
+ """Register a tool with its title in both places MCP defines one.
171
+
172
+ The tool's own ``title`` is what clients display; ``annotations.title``
173
+ is what directory reviews (Claude's connector portal) check. One title,
174
+ copied into the shared annotation preset, keeps them from drifting.
175
+ """
176
+ return mcp.tool(
177
+ title=title,
178
+ annotations=annotations.model_copy(update={"title": title}),
179
+ **kwargs,
180
+ )
181
+
182
+
169
183
  def _reddit_structured_error(exc: Exception) -> dict | None:
170
184
  """Reddit-specific translations; keyed on exception type so Reddit's
171
185
  ``invalid_grant`` never gets the "Reconnect Google" hint below."""
@@ -310,13 +324,20 @@ def _structured_error(fn_name: str, exc: Exception) -> dict:
310
324
  }
311
325
 
312
326
  if "deleted_client" in err_lower or "invalid_client" in err_lower:
327
+ from adloop.runtime import deployment_mode
328
+
313
329
  return {
314
330
  "error": (
315
331
  "Authentication failed — the OAuth client behind your stored "
316
332
  "credentials no longer exists or is invalid."
317
333
  ),
334
+ # On a hosted server the user never sees an OAuth client: the
335
+ # fix is reconnecting, and pointing at a product would be an ad.
318
336
  "hint": (
319
- "If you set up AdLoop before v0.10 with its bundled "
337
+ "Reconnect Google in your AdLoop Cloud dashboard "
338
+ "(Settings → Google), then retry."
339
+ if deployment_mode() == "server"
340
+ else "If you set up AdLoop before v0.10 with its bundled "
320
341
  "credentials: that shared Google Cloud project has been "
321
342
  "retired. Fastest fix: AdLoop Cloud (https://getadloop.com) — "
322
343
  "connect Google in two clicks, no Google Cloud project "
@@ -393,7 +414,7 @@ def _safe(fn: Callable) -> Callable:
393
414
  # ---------------------------------------------------------------------------
394
415
 
395
416
 
396
- @mcp.tool(title="Connection health check", annotations=_READONLY, tags={"core"})
417
+ @_tool(title="Connection health check", annotations=_READONLY, tags={"core"})
397
418
  @_safe
398
419
  def health_check() -> dict:
399
420
  """Test AdLoop connectivity — checks OAuth token, GA4 API, Google Ads API,
@@ -524,7 +545,7 @@ def health_check() -> dict:
524
545
  # ---------------------------------------------------------------------------
525
546
 
526
547
 
527
- @mcp.tool(title="List Analytics properties", annotations=_READONLY, tags={"ga4"})
548
+ @_tool(title="List Analytics properties", annotations=_READONLY, tags={"ga4"})
528
549
  @_safe
529
550
  def get_account_summaries() -> dict:
530
551
  """List all GA4 accounts and properties accessible by the authenticated user.
@@ -537,7 +558,7 @@ def get_account_summaries() -> dict:
537
558
  return _impl(current_config())
538
559
 
539
560
 
540
- @mcp.tool(title="Analytics report", annotations=_READONLY, tags={"ga4"})
561
+ @_tool(title="Analytics report", annotations=_READONLY, tags={"ga4"})
541
562
  @_safe
542
563
  def run_ga4_report(
543
564
  dimensions: _StrListOpt = None,
@@ -570,7 +591,7 @@ def run_ga4_report(
570
591
  )
571
592
 
572
593
 
573
- @mcp.tool(title="Analytics realtime report", annotations=_READONLY, tags={"ga4"})
594
+ @_tool(title="Analytics realtime report", annotations=_READONLY, tags={"ga4"})
574
595
  @_safe
575
596
  def run_realtime_report(
576
597
  dimensions: _StrListOpt = None,
@@ -593,7 +614,7 @@ def run_realtime_report(
593
614
  )
594
615
 
595
616
 
596
- @mcp.tool(title="List Analytics events", annotations=_READONLY, tags={"ga4"})
617
+ @_tool(title="List Analytics events", annotations=_READONLY, tags={"ga4"})
597
618
  @_safe
598
619
  def get_tracking_events(
599
620
  date_range_start: str = "28daysAgo",
@@ -620,7 +641,7 @@ def get_tracking_events(
620
641
  # ---------------------------------------------------------------------------
621
642
 
622
643
 
623
- @mcp.tool(title="List Search Console properties", annotations=_READONLY, tags={"gsc"})
644
+ @_tool(title="List Search Console properties", annotations=_READONLY, tags={"gsc"})
624
645
  @_safe
625
646
  def list_gsc_sites() -> dict:
626
647
  """List all Google Search Console properties the authenticated user can access.
@@ -634,7 +655,7 @@ def list_gsc_sites() -> dict:
634
655
  return _impl(current_config())
635
656
 
636
657
 
637
- @mcp.tool(title="Search Console report", annotations=_READONLY, tags={"gsc"})
658
+ @_tool(title="Search Console report", annotations=_READONLY, tags={"gsc"})
638
659
  @_safe
639
660
  def run_gsc_report(
640
661
  site_url: str = "",
@@ -652,7 +673,7 @@ def run_gsc_report(
652
673
  finding keyword opportunities, and cross-referencing with GA4 and Ads data.
653
674
 
654
675
  site_url: the GSC property URL (e.g. "https://example.com/" or
655
- "sc-domain:example.com"). Defaults to gsc.site_url in config.yaml.
676
+ "sc-domain:example.com"). Defaults to the configured Search Console site.
656
677
  dimensions: one or more of ["query", "page", "country", "device", "date"].
657
678
  Defaults to ["query"].
658
679
  date_range_start / date_range_end: ISO dates (YYYY-MM-DD) or relative
@@ -686,7 +707,7 @@ def run_gsc_report(
686
707
  # ---------------------------------------------------------------------------
687
708
 
688
709
 
689
- @mcp.tool(title="PageSpeed analysis", annotations=_READONLY, tags={"web"})
710
+ @_tool(title="PageSpeed analysis", annotations=_READONLY, tags={"web"})
690
711
  @_safe
691
712
  def analyze_page_speed(url: str, strategy: str = "mobile") -> dict:
692
713
  """Run PageSpeed Insights for a landing page — Lighthouse + real-user data.
@@ -705,7 +726,7 @@ def analyze_page_speed(url: str, strategy: str = "mobile") -> dict:
705
726
  return _impl(current_config(), url=url, strategy=strategy)
706
727
 
707
728
 
708
- @mcp.tool(
729
+ @_tool(
709
730
  title="List Merchant Center accounts",
710
731
  annotations=_READONLY,
711
732
  tags={"merchant"},
@@ -723,7 +744,7 @@ def list_merchant_accounts() -> dict:
723
744
  return _impl(current_config())
724
745
 
725
746
 
726
- @mcp.tool(title="Merchant Center feed health", annotations=_READONLY, tags={"merchant"})
747
+ @_tool(title="Merchant Center feed health", annotations=_READONLY, tags={"merchant"})
727
748
  @_safe
728
749
  def get_merchant_feed_health(account_id: str = "") -> dict:
729
750
  """Merchant Center feed health — disapproved products + account issues.
@@ -743,7 +764,7 @@ def get_merchant_feed_health(account_id: str = "") -> dict:
743
764
  return _impl(current_config(), account_id=account_id)
744
765
 
745
766
 
746
- @mcp.tool(title="List Google Ads accounts", annotations=_READONLY, tags={"ads"})
767
+ @_tool(title="List Google Ads accounts", annotations=_READONLY, tags={"ads"})
747
768
  @_safe
748
769
  def list_accounts(limit: int = 200) -> dict:
749
770
  """List accessible Google Ads accounts.
@@ -762,7 +783,7 @@ def list_accounts(limit: int = 200) -> dict:
762
783
  return _impl(current_config(), limit=limit)
763
784
 
764
785
 
765
- @mcp.tool(title="Campaign performance", annotations=_READONLY, tags={"ads"})
786
+ @_tool(title="Campaign performance", annotations=_READONLY, tags={"ads"})
766
787
  @_safe
767
788
  def get_campaign_performance(
768
789
  customer_id: str = "",
@@ -791,7 +812,7 @@ def get_campaign_performance(
791
812
  )
792
813
 
793
814
 
794
- @mcp.tool(title="Ad performance", annotations=_READONLY, tags={"ads"})
815
+ @_tool(title="Ad performance", annotations=_READONLY, tags={"ads"})
795
816
  @_safe
796
817
  def get_ad_performance(
797
818
  customer_id: str = "",
@@ -819,7 +840,7 @@ def get_ad_performance(
819
840
  )
820
841
 
821
842
 
822
- @mcp.tool(title="Keyword performance", annotations=_READONLY, tags={"ads"})
843
+ @_tool(title="Keyword performance", annotations=_READONLY, tags={"ads"})
823
844
  @_safe
824
845
  def get_keyword_performance(
825
846
  customer_id: str = "",
@@ -847,7 +868,7 @@ def get_keyword_performance(
847
868
  )
848
869
 
849
870
 
850
- @mcp.tool(title="Search terms report", annotations=_READONLY, tags={"ads"})
871
+ @_tool(title="Search terms report", annotations=_READONLY, tags={"ads"})
851
872
  @_safe
852
873
  def get_search_terms(
853
874
  customer_id: str = "",
@@ -875,7 +896,7 @@ def get_search_terms(
875
896
  )
876
897
 
877
898
 
878
- @mcp.tool(title="List negative keywords", annotations=_READONLY, tags={"ads"})
899
+ @_tool(title="List negative keywords", annotations=_READONLY, tags={"ads"})
879
900
  @_safe
880
901
  def get_negative_keywords(
881
902
  customer_id: str = "",
@@ -895,7 +916,7 @@ def get_negative_keywords(
895
916
  )
896
917
 
897
918
 
898
- @mcp.tool(
919
+ @_tool(
899
920
  title="List shared negative keyword lists",
900
921
  annotations=_READONLY,
901
922
  tags={"ads"},
@@ -915,7 +936,7 @@ def get_negative_keyword_lists(
915
936
  return _impl(current_config(), customer_id=customer_id or current_config().ads.customer_id)
916
937
 
917
938
 
918
- @mcp.tool(
939
+ @_tool(
919
940
  title="Keywords in a negative keyword list",
920
941
  annotations=_READONLY,
921
942
  tags={"ads"},
@@ -938,7 +959,7 @@ def get_negative_keyword_list_keywords(
938
959
  )
939
960
 
940
961
 
941
- @mcp.tool(
962
+ @_tool(
942
963
  title="Campaigns using a negative keyword list",
943
964
  annotations=_READONLY,
944
965
  tags={"ads"},
@@ -967,7 +988,7 @@ def get_negative_keyword_list_campaigns(
967
988
  # ---------------------------------------------------------------------------
968
989
 
969
990
 
970
- @mcp.tool(title="Google Ads recommendations", annotations=_READONLY, tags={"ads"})
991
+ @_tool(title="Google Ads recommendations", annotations=_READONLY, tags={"ads"})
971
992
  @_safe
972
993
  def get_recommendations(
973
994
  customer_id: str = "",
@@ -996,7 +1017,7 @@ def get_recommendations(
996
1017
  )
997
1018
 
998
1019
 
999
- @mcp.tool(title="Performance Max performance", annotations=_READONLY, tags={"ads"})
1020
+ @_tool(title="Performance Max performance", annotations=_READONLY, tags={"ads"})
1000
1021
  @_safe
1001
1022
  def get_pmax_performance(
1002
1023
  customer_id: str = "",
@@ -1027,7 +1048,7 @@ def get_pmax_performance(
1027
1048
  )
1028
1049
 
1029
1050
 
1030
- @mcp.tool(
1051
+ @_tool(
1031
1052
  title="Performance Max asset performance",
1032
1053
  annotations=_READONLY,
1033
1054
  tags={"ads"},
@@ -1060,7 +1081,7 @@ def get_asset_performance(
1060
1081
  )
1061
1082
 
1062
1083
 
1063
- @mcp.tool(
1084
+ @_tool(
1064
1085
  title="Performance Max asset combinations",
1065
1086
  annotations=_READONLY,
1066
1087
  tags={"ads"},
@@ -1087,7 +1108,7 @@ def get_detailed_asset_performance(
1087
1108
  )
1088
1109
 
1089
1110
 
1090
- @mcp.tool(title="Audience performance", annotations=_READONLY, tags={"ads"})
1111
+ @_tool(title="Audience performance", annotations=_READONLY, tags={"ads"})
1091
1112
  @_safe
1092
1113
  def get_audience_performance(
1093
1114
  customer_id: str = "",
@@ -1118,7 +1139,7 @@ def get_audience_performance(
1118
1139
  )
1119
1140
 
1120
1141
 
1121
- @mcp.tool(title="Demographic targeting", annotations=_READONLY, tags={"ads"})
1142
+ @_tool(title="Demographic targeting", annotations=_READONLY, tags={"ads"})
1122
1143
  @_safe
1123
1144
  def get_demographic_targeting(
1124
1145
  ad_group_id: str = "",
@@ -1151,7 +1172,7 @@ def get_demographic_targeting(
1151
1172
  # ---------------------------------------------------------------------------
1152
1173
 
1153
1174
 
1154
- @mcp.tool(
1175
+ @_tool(
1155
1176
  title="Campaign conversions against Analytics",
1156
1177
  annotations=_READONLY,
1157
1178
  tags={"tracking"},
@@ -1185,7 +1206,7 @@ def analyze_campaign_conversions(
1185
1206
  )
1186
1207
 
1187
1208
 
1188
- @mcp.tool(title="Landing page analysis", annotations=_READONLY, tags={"tracking"})
1209
+ @_tool(title="Landing page analysis", annotations=_READONLY, tags={"tracking"})
1189
1210
  @_safe
1190
1211
  def landing_page_analysis(
1191
1212
  date_range_start: str = "",
@@ -1211,7 +1232,7 @@ def landing_page_analysis(
1211
1232
  )
1212
1233
 
1213
1234
 
1214
- @mcp.tool(title="Attribution check", annotations=_READONLY, tags={"tracking"})
1235
+ @_tool(title="Attribution check", annotations=_READONLY, tags={"tracking"})
1215
1236
  @_safe
1216
1237
  def attribution_check(
1217
1238
  date_range_start: str = "",
@@ -1242,7 +1263,7 @@ def attribution_check(
1242
1263
  )
1243
1264
 
1244
1265
 
1245
- @mcp.tool(title="Tracking coverage audit", annotations=_READONLY, tags={"gtm"})
1266
+ @_tool(title="Tracking coverage audit", annotations=_READONLY, tags={"gtm"})
1246
1267
  @_safe
1247
1268
  def audit_event_coverage(
1248
1269
  expected_events: list[str],
@@ -1295,7 +1316,7 @@ def audit_event_coverage(
1295
1316
  )
1296
1317
 
1297
1318
 
1298
- @mcp.tool(title="List Tag Manager accounts", annotations=_READONLY, tags={"gtm"})
1319
+ @_tool(title="List Tag Manager accounts", annotations=_READONLY, tags={"gtm"})
1299
1320
  @_safe
1300
1321
  def list_gtm_accounts() -> dict:
1301
1322
  """List all GTM accounts the AdLoop service account / OAuth user can read.
@@ -1310,7 +1331,7 @@ def list_gtm_accounts() -> dict:
1310
1331
  return _impl(current_config())
1311
1332
 
1312
1333
 
1313
- @mcp.tool(title="List Tag Manager containers", annotations=_READONLY, tags={"gtm"})
1334
+ @_tool(title="List Tag Manager containers", annotations=_READONLY, tags={"gtm"})
1314
1335
  @_safe
1315
1336
  def list_gtm_containers(gtm_account_id: str = "") -> dict:
1316
1337
  """List all containers under a GTM account.
@@ -1330,7 +1351,7 @@ def list_gtm_containers(gtm_account_id: str = "") -> dict:
1330
1351
  return _impl(current_config(), account_id=gtm_account_id)
1331
1352
 
1332
1353
 
1333
- @mcp.tool(title="List Tag Manager tags", annotations=_READONLY, tags={"gtm"})
1354
+ @_tool(title="List Tag Manager tags", annotations=_READONLY, tags={"gtm"})
1334
1355
  @_safe
1335
1356
  def list_gtm_tags(gtm_account_id: str = "", gtm_container_id: str = "") -> dict:
1336
1357
  """List every tag in the LIVE GTM container.
@@ -1350,7 +1371,7 @@ def list_gtm_tags(gtm_account_id: str = "", gtm_container_id: str = "") -> dict:
1350
1371
  )
1351
1372
 
1352
1373
 
1353
- @mcp.tool(title="Tag Manager tag details", annotations=_READONLY, tags={"gtm"})
1374
+ @_tool(title="Tag Manager tag details", annotations=_READONLY, tags={"gtm"})
1354
1375
  @_safe
1355
1376
  def get_gtm_tag(
1356
1377
  tag_id: str, gtm_account_id: str = "", gtm_container_id: str = ""
@@ -1375,7 +1396,7 @@ def get_gtm_tag(
1375
1396
  )
1376
1397
 
1377
1398
 
1378
- @mcp.tool(title="List Tag Manager triggers", annotations=_READONLY, tags={"gtm"})
1399
+ @_tool(title="List Tag Manager triggers", annotations=_READONLY, tags={"gtm"})
1379
1400
  @_safe
1380
1401
  def list_gtm_triggers(gtm_account_id: str = "", gtm_container_id: str = "") -> dict:
1381
1402
  """List every trigger in the LIVE GTM container.
@@ -1395,7 +1416,7 @@ def list_gtm_triggers(gtm_account_id: str = "", gtm_container_id: str = "") -> d
1395
1416
  )
1396
1417
 
1397
1418
 
1398
- @mcp.tool(title="Tag Manager trigger details", annotations=_READONLY, tags={"gtm"})
1419
+ @_tool(title="Tag Manager trigger details", annotations=_READONLY, tags={"gtm"})
1399
1420
  @_safe
1400
1421
  def get_gtm_trigger(
1401
1422
  trigger_id: str, gtm_account_id: str = "", gtm_container_id: str = ""
@@ -1420,7 +1441,7 @@ def get_gtm_trigger(
1420
1441
  )
1421
1442
 
1422
1443
 
1423
- @mcp.tool(title="List Tag Manager variables", annotations=_READONLY, tags={"gtm"})
1444
+ @_tool(title="List Tag Manager variables", annotations=_READONLY, tags={"gtm"})
1424
1445
  @_safe
1425
1446
  def list_gtm_variables(gtm_account_id: str = "", gtm_container_id: str = "") -> dict:
1426
1447
  """List GTM variables — both custom and enabled built-in.
@@ -1442,7 +1463,7 @@ def list_gtm_variables(gtm_account_id: str = "", gtm_container_id: str = "") ->
1442
1463
  )
1443
1464
 
1444
1465
 
1445
- @mcp.tool(title="List Tag Manager workspaces", annotations=_READONLY, tags={"gtm"})
1466
+ @_tool(title="List Tag Manager workspaces", annotations=_READONLY, tags={"gtm"})
1446
1467
  @_safe
1447
1468
  def list_gtm_workspaces(gtm_account_id: str = "", gtm_container_id: str = "") -> dict:
1448
1469
  """List workspaces (drafts) under a GTM container.
@@ -1462,7 +1483,7 @@ def list_gtm_workspaces(gtm_account_id: str = "", gtm_container_id: str = "") ->
1462
1483
  )
1463
1484
 
1464
1485
 
1465
- @mcp.tool(title="Tag Manager workspace changes", annotations=_READONLY, tags={"gtm"})
1486
+ @_tool(title="Tag Manager workspace changes", annotations=_READONLY, tags={"gtm"})
1466
1487
  @_safe
1467
1488
  def get_gtm_workspace_diff(
1468
1489
  workspace_id: str, gtm_account_id: str = "", gtm_container_id: str = ""
@@ -1489,7 +1510,7 @@ def get_gtm_workspace_diff(
1489
1510
  )
1490
1511
 
1491
1512
 
1492
- @mcp.tool(title="Tag Manager version history", annotations=_READONLY, tags={"gtm"})
1513
+ @_tool(title="Tag Manager version history", annotations=_READONLY, tags={"gtm"})
1493
1514
  @_safe
1494
1515
  def list_gtm_versions(
1495
1516
  gtm_account_id: str = "", gtm_container_id: str = "", page_size: int = 50
@@ -1515,7 +1536,7 @@ def list_gtm_versions(
1515
1536
  )
1516
1537
 
1517
1538
 
1518
- @mcp.tool(title="Tag Manager version details", annotations=_READONLY, tags={"gtm"})
1539
+ @_tool(title="Tag Manager version details", annotations=_READONLY, tags={"gtm"})
1519
1540
  @_safe
1520
1541
  def get_gtm_version(
1521
1542
  container_version_id: str, gtm_account_id: str = "", gtm_container_id: str = ""
@@ -1540,7 +1561,7 @@ def get_gtm_version(
1540
1561
  )
1541
1562
 
1542
1563
 
1543
- @mcp.tool(title="Custom Google Ads query", annotations=_READONLY, tags={"ads"})
1564
+ @_tool(title="Custom Google Ads query", annotations=_READONLY, tags={"ads"})
1544
1565
  @_safe
1545
1566
  def run_gaql(
1546
1567
  query: str,
@@ -1570,7 +1591,7 @@ def run_gaql(
1570
1591
  # ---------------------------------------------------------------------------
1571
1592
 
1572
1593
 
1573
- @mcp.tool(title="Draft a campaign", annotations=_WRITE, tags={"ads"})
1594
+ @_tool(title="Draft a campaign", annotations=_WRITE, tags={"ads"})
1574
1595
  @_safe
1575
1596
  def draft_campaign(
1576
1597
  campaign_name: str,
@@ -1637,7 +1658,7 @@ def draft_campaign(
1637
1658
  )
1638
1659
 
1639
1660
 
1640
- @mcp.tool(title="Draft an ad group", annotations=_WRITE, tags={"ads"})
1661
+ @_tool(title="Draft an ad group", annotations=_WRITE, tags={"ads"})
1641
1662
  @_safe
1642
1663
  def draft_ad_group(
1643
1664
  campaign_id: str,
@@ -1670,7 +1691,7 @@ def draft_ad_group(
1670
1691
  )
1671
1692
 
1672
1693
 
1673
- @mcp.tool(title="Draft campaign changes", annotations=_WRITE, tags={"ads"})
1694
+ @_tool(title="Draft campaign changes", annotations=_WRITE, tags={"ads"})
1674
1695
  @_safe
1675
1696
  def update_campaign(
1676
1697
  campaign_id: str,
@@ -1727,7 +1748,7 @@ def update_campaign(
1727
1748
  )
1728
1749
 
1729
1750
 
1730
- @mcp.tool(title="Draft a responsive search ad", annotations=_WRITE, tags={"ads"})
1751
+ @_tool(title="Draft a responsive search ad", annotations=_WRITE, tags={"ads"})
1731
1752
  @_safe
1732
1753
  def draft_responsive_search_ad(
1733
1754
  ad_group_id: str,
@@ -1770,7 +1791,7 @@ def draft_responsive_search_ad(
1770
1791
  )
1771
1792
 
1772
1793
 
1773
- @mcp.tool(title="Update a responsive search ad", annotations=_WRITE, tags={"ads"})
1794
+ @_tool(title="Update a responsive search ad", annotations=_WRITE, tags={"ads"})
1774
1795
  @_safe
1775
1796
  def update_responsive_search_ad(
1776
1797
  ad_id: str,
@@ -1829,7 +1850,7 @@ def update_responsive_search_ad(
1829
1850
  )
1830
1851
 
1831
1852
 
1832
- @mcp.tool(title="Draft keywords", annotations=_WRITE, tags={"ads"})
1853
+ @_tool(title="Draft keywords", annotations=_WRITE, tags={"ads"})
1833
1854
  @_safe
1834
1855
  def draft_keywords(
1835
1856
  ad_group_id: str,
@@ -1851,7 +1872,7 @@ def draft_keywords(
1851
1872
  )
1852
1873
 
1853
1874
 
1854
- @mcp.tool(title="Custom conversion goals", annotations=_READONLY, tags={"ads"})
1875
+ @_tool(title="Custom conversion goals", annotations=_READONLY, tags={"ads"})
1855
1876
  @_safe
1856
1877
  def get_custom_conversion_goals(
1857
1878
  campaign_id: str = "",
@@ -1880,7 +1901,7 @@ def get_custom_conversion_goals(
1880
1901
  )
1881
1902
 
1882
1903
 
1883
- @mcp.tool(
1904
+ @_tool(
1884
1905
  title="Draft a custom conversion goal",
1885
1906
  annotations=_WRITE,
1886
1907
  tags={"ads"},
@@ -1924,7 +1945,7 @@ def draft_custom_conversion_goal(
1924
1945
  )
1925
1946
 
1926
1947
 
1927
- @mcp.tool(
1948
+ @_tool(
1928
1949
  title="Draft changes to a custom conversion goal",
1929
1950
  annotations=_WRITE,
1930
1951
  tags={"ads"},
@@ -1962,7 +1983,7 @@ def draft_update_custom_conversion_goal(
1962
1983
  )
1963
1984
 
1964
1985
 
1965
- @mcp.tool(
1986
+ @_tool(
1966
1987
  title="Draft assigning a custom conversion goal",
1967
1988
  annotations=_WRITE,
1968
1989
  tags={"ads"},
@@ -2005,7 +2026,7 @@ def draft_assign_custom_conversion_goal(
2005
2026
  )
2006
2027
 
2007
2028
 
2008
- @mcp.tool(
2029
+ @_tool(
2009
2030
  title="Draft clearing a custom conversion goal",
2010
2031
  annotations=_WRITE,
2011
2032
  tags={"ads"},
@@ -2036,7 +2057,7 @@ def draft_clear_custom_conversion_goal(
2036
2057
  )
2037
2058
 
2038
2059
 
2039
- @mcp.tool(title="Draft negative keywords", annotations=_WRITE, tags={"ads"})
2060
+ @_tool(title="Draft negative keywords", annotations=_WRITE, tags={"ads"})
2040
2061
  @_safe
2041
2062
  def add_negative_keywords(
2042
2063
  campaign_id: str,
@@ -2061,7 +2082,7 @@ def add_negative_keywords(
2061
2082
  )
2062
2083
 
2063
2084
 
2064
- @mcp.tool(title="Draft negative locations", annotations=_WRITE, tags={"ads"})
2085
+ @_tool(title="Draft negative locations", annotations=_WRITE, tags={"ads"})
2065
2086
  @_safe
2066
2087
  def add_negative_locations(
2067
2088
  campaign_id: str,
@@ -2085,7 +2106,7 @@ def add_negative_locations(
2085
2106
  )
2086
2107
 
2087
2108
 
2088
- @mcp.tool(
2109
+ @_tool(
2089
2110
  title="Draft a shared negative keyword list",
2090
2111
  annotations=_WRITE,
2091
2112
  tags={"ads"},
@@ -2117,7 +2138,7 @@ def propose_negative_keyword_list(
2117
2138
  )
2118
2139
 
2119
2140
 
2120
- @mcp.tool(
2141
+ @_tool(
2121
2142
  title="Draft additions to a negative keyword list",
2122
2143
  annotations=_WRITE,
2123
2144
  tags={"ads"},
@@ -2154,7 +2175,7 @@ def add_to_negative_keyword_list(
2154
2175
  )
2155
2176
 
2156
2177
 
2157
- @mcp.tool(title="Draft attaching a shared set", annotations=_WRITE, tags={"ads"})
2178
+ @_tool(title="Draft attaching a shared set", annotations=_WRITE, tags={"ads"})
2158
2179
  @_safe
2159
2180
  def attach_shared_set_to_campaigns(
2160
2181
  shared_set_id: str,
@@ -2185,7 +2206,7 @@ def attach_shared_set_to_campaigns(
2185
2206
  )
2186
2207
 
2187
2208
 
2188
- @mcp.tool(title="Draft detaching a shared set", annotations=_WRITE, tags={"ads"})
2209
+ @_tool(title="Draft detaching a shared set", annotations=_WRITE, tags={"ads"})
2189
2210
  @_safe
2190
2211
  def detach_shared_set_from_campaigns(
2191
2212
  shared_set_id: str,
@@ -2216,7 +2237,7 @@ def detach_shared_set_from_campaigns(
2216
2237
  )
2217
2238
 
2218
2239
 
2219
- @mcp.tool(title="AI Max settings", annotations=_READONLY, tags={"ads"})
2240
+ @_tool(title="AI Max settings", annotations=_READONLY, tags={"ads"})
2220
2241
  @_safe
2221
2242
  def get_ai_max_settings(
2222
2243
  campaign_id: str = "",
@@ -2246,7 +2267,7 @@ def get_ai_max_settings(
2246
2267
  campaign_id=campaign_id,
2247
2268
  )
2248
2269
 
2249
- @mcp.tool(title="Draft AI Max settings", annotations=_WRITE, tags={"ads"})
2270
+ @_tool(title="Draft AI Max settings", annotations=_WRITE, tags={"ads"})
2250
2271
  @_safe
2251
2272
  def draft_ai_max_settings(
2252
2273
  campaign_id: str,
@@ -2311,7 +2332,7 @@ def draft_ai_max_settings(
2311
2332
  )
2312
2333
 
2313
2334
 
2314
- @mcp.tool(
2335
+ @_tool(
2315
2336
  title="Prepare a campaign for brand exclusions",
2316
2337
  annotations=_WRITE,
2317
2338
  tags={"ads"},
@@ -2354,7 +2375,7 @@ def draft_prepare_brand_exclusions(
2354
2375
  include_paused_ad_groups=include_paused_ad_groups,
2355
2376
  )
2356
2377
 
2357
- @mcp.tool(title="Draft demographic targeting", annotations=_WRITE, tags={"ads"})
2378
+ @_tool(title="Draft demographic targeting", annotations=_WRITE, tags={"ads"})
2358
2379
  @_safe
2359
2380
  def draft_demographic_targeting(
2360
2381
  customer_id: str = "",
@@ -2406,7 +2427,7 @@ def draft_demographic_targeting(
2406
2427
  )
2407
2428
 
2408
2429
 
2409
- @mcp.tool(title="Draft ad group changes", annotations=_WRITE, tags={"ads"})
2430
+ @_tool(title="Draft ad group changes", annotations=_WRITE, tags={"ads"})
2410
2431
  @_safe
2411
2432
  def update_ad_group(
2412
2433
  ad_group_id: str,
@@ -2426,7 +2447,7 @@ def update_ad_group(
2426
2447
  )
2427
2448
 
2428
2449
 
2429
- @mcp.tool(title="Draft callouts", annotations=_WRITE, tags={"ads"})
2450
+ @_tool(title="Draft callouts", annotations=_WRITE, tags={"ads"})
2430
2451
  @_safe
2431
2452
  def draft_callouts(
2432
2453
  campaign_id: str,
@@ -2444,7 +2465,7 @@ def draft_callouts(
2444
2465
  )
2445
2466
 
2446
2467
 
2447
- @mcp.tool(title="Draft structured snippets", annotations=_WRITE, tags={"ads"})
2468
+ @_tool(title="Draft structured snippets", annotations=_WRITE, tags={"ads"})
2448
2469
  @_safe
2449
2470
  def draft_structured_snippets(
2450
2471
  campaign_id: str,
@@ -2462,7 +2483,7 @@ def draft_structured_snippets(
2462
2483
  )
2463
2484
 
2464
2485
 
2465
- @mcp.tool(title="Draft image assets", annotations=_WRITE, tags={"ads"})
2486
+ @_tool(title="Draft image assets", annotations=_WRITE, tags={"ads"})
2466
2487
  @_safe
2467
2488
  def draft_image_assets(
2468
2489
  campaign_id: str,
@@ -2496,7 +2517,7 @@ def draft_image_assets(
2496
2517
  )
2497
2518
 
2498
2519
 
2499
- @mcp.tool(title="Draft pausing an entity", annotations=_WRITE, tags={"ads"})
2520
+ @_tool(title="Draft pausing an entity", annotations=_WRITE, tags={"ads"})
2500
2521
  @_safe
2501
2522
  def pause_entity(
2502
2523
  entity_type: str,
@@ -2524,7 +2545,7 @@ def pause_entity(
2524
2545
  )
2525
2546
 
2526
2547
 
2527
- @mcp.tool(title="Draft enabling an entity", annotations=_WRITE, tags={"ads"})
2548
+ @_tool(title="Draft enabling an entity", annotations=_WRITE, tags={"ads"})
2528
2549
  @_safe
2529
2550
  def enable_entity(
2530
2551
  entity_type: str,
@@ -2552,7 +2573,7 @@ def enable_entity(
2552
2573
  )
2553
2574
 
2554
2575
 
2555
- @mcp.tool(title="Draft removing an entity", annotations=_DESTRUCTIVE, tags={"ads"})
2576
+ @_tool(title="Draft removing an entity", annotations=_DESTRUCTIVE, tags={"ads"})
2556
2577
  @_safe
2557
2578
  def remove_entity(
2558
2579
  entity_type: str,
@@ -2588,7 +2609,7 @@ def remove_entity(
2588
2609
  )
2589
2610
 
2590
2611
 
2591
- @mcp.tool(title="Draft sitelinks", annotations=_WRITE, tags={"ads"})
2612
+ @_tool(title="Draft sitelinks", annotations=_WRITE, tags={"ads"})
2592
2613
  @_safe
2593
2614
  def draft_sitelinks(
2594
2615
  campaign_id: str,
@@ -2621,7 +2642,7 @@ def draft_sitelinks(
2621
2642
  )
2622
2643
 
2623
2644
 
2624
- @mcp.tool(title="Apply a previewed change", annotations=_DESTRUCTIVE, tags={"core"})
2645
+ @_tool(title="Apply a previewed change", annotations=_DESTRUCTIVE, tags={"core"})
2625
2646
  @_safe
2626
2647
  def confirm_and_apply(
2627
2648
  plan_id: str,
@@ -2633,14 +2654,12 @@ def confirm_and_apply(
2633
2654
  to make real changes to the ad account (Google Ads or Reddit Ads — the
2634
2655
  plan knows which platform it targets).
2635
2656
 
2636
- Config override: if 'safety.require_dry_run: true' is set in the user's
2637
- config file (default ~/.adloop/config.yaml), dry_run=false is IGNORED
2638
- and this tool will keep returning DRY_RUN_SUCCESS. When that happens the
2639
- response includes 'dry_run_forced_by', 'config_path', and 'remediation'
2640
- fields — surface those to the user verbatim and STOP retrying. Calling
2641
- this tool again with dry_run=false will not change anything until the
2642
- user edits the config file, sets 'require_dry_run: false', and restarts
2643
- the AdLoop MCP server.
2657
+ Forced dry runs: when real changes are switched off (live changes off
2658
+ for the workspace in AdLoop Cloud, or safety.require_dry_run in a
2659
+ self-hosted config), dry_run=false is IGNORED and the result is still
2660
+ DRY_RUN_SUCCESS, with 'dry_run_forced_by' and 'remediation' fields.
2661
+ Pass the remediation on to the user and stop retrying: calling again
2662
+ with dry_run=false changes nothing until that setting is changed.
2644
2663
 
2645
2664
  Two-phase apply: if 'safety.two_phase_apply: true' is set (always on
2646
2665
  for AdLoop Cloud tenants), dry_run=false is REFUSED with status
@@ -2676,7 +2695,7 @@ def _reddit_account(ad_account_id: str) -> str:
2676
2695
  return ad_account_id or current_config().reddit.ad_account_id
2677
2696
 
2678
2697
 
2679
- @mcp.tool(title="List Reddit ad accounts", annotations=_READONLY, tags={"reddit"})
2698
+ @_tool(title="List Reddit ad accounts", annotations=_READONLY, tags={"reddit"})
2680
2699
  @_safe
2681
2700
  def list_reddit_accounts() -> dict:
2682
2701
  """List Reddit businesses and ad accounts the connected Reddit user can access.
@@ -2690,7 +2709,7 @@ def list_reddit_accounts() -> dict:
2690
2709
  return _impl(current_config())
2691
2710
 
2692
2711
 
2693
- @mcp.tool(
2712
+ @_tool(
2694
2713
  title="Reddit billing + posting profiles", annotations=_READONLY, tags={"reddit"}
2695
2714
  )
2696
2715
  @_safe
@@ -2705,7 +2724,7 @@ def list_reddit_funding_instruments(ad_account_id: str = "") -> dict:
2705
2724
  return _impl(current_config(), ad_account_id=_reddit_account(ad_account_id))
2706
2725
 
2707
2726
 
2708
- @mcp.tool(title="Reddit campaigns", annotations=_READONLY, tags={"reddit"})
2727
+ @_tool(title="Reddit campaigns", annotations=_READONLY, tags={"reddit"})
2709
2728
  @_safe
2710
2729
  def get_reddit_campaigns(ad_account_id: str = "", include_archived: bool = False) -> dict:
2711
2730
  """List Reddit campaigns with status, objective, budget mode and bids.
@@ -2724,7 +2743,7 @@ def get_reddit_campaigns(ad_account_id: str = "", include_archived: bool = False
2724
2743
  )
2725
2744
 
2726
2745
 
2727
- @mcp.tool(title="Reddit ad groups", annotations=_READONLY, tags={"reddit"})
2746
+ @_tool(title="Reddit ad groups", annotations=_READONLY, tags={"reddit"})
2728
2747
  @_safe
2729
2748
  def get_reddit_ad_groups(ad_account_id: str = "", campaign_id: str = "") -> dict:
2730
2749
  """List Reddit ad groups (budget, bid, pixel, weekly schedule, targeting summary), optionally per campaign.
@@ -2741,7 +2760,7 @@ def get_reddit_ad_groups(ad_account_id: str = "", campaign_id: str = "") -> dict
2741
2760
  )
2742
2761
 
2743
2762
 
2744
- @mcp.tool(title="Reddit ads", annotations=_READONLY, tags={"reddit"})
2763
+ @_tool(title="Reddit ads", annotations=_READONLY, tags={"reddit"})
2745
2764
  @_safe
2746
2765
  def get_reddit_ads(
2747
2766
  ad_account_id: str = "",
@@ -2767,7 +2786,7 @@ def get_reddit_ads(
2767
2786
  )
2768
2787
 
2769
2788
 
2770
- @mcp.tool(title="Reddit performance", annotations=_READONLY, tags={"reddit"})
2789
+ @_tool(title="Reddit performance", annotations=_READONLY, tags={"reddit"})
2771
2790
  @_safe
2772
2791
  def get_reddit_performance(
2773
2792
  ad_account_id: str = "",
@@ -2803,7 +2822,7 @@ def get_reddit_performance(
2803
2822
  )
2804
2823
 
2805
2824
 
2806
- @mcp.tool(title="Raw Reddit report", annotations=_READONLY, tags={"reddit"})
2825
+ @_tool(title="Raw Reddit report", annotations=_READONLY, tags={"reddit"})
2807
2826
  @_safe
2808
2827
  def run_reddit_report(
2809
2828
  fields: _StrList,
@@ -2839,7 +2858,7 @@ def run_reddit_report(
2839
2858
  )
2840
2859
 
2841
2860
 
2842
- @mcp.tool(title="Reddit pixels", annotations=_READONLY, tags={"reddit"})
2861
+ @_tool(title="Reddit pixels", annotations=_READONLY, tags={"reddit"})
2843
2862
  @_safe
2844
2863
  def get_reddit_pixels(ad_account_id: str = "") -> dict:
2845
2864
  """Reddit pixels of the account and when each event (purchase, sign_up, lead, ...) last fired.
@@ -2853,7 +2872,7 @@ def get_reddit_pixels(ad_account_id: str = "") -> dict:
2853
2872
  return _impl(current_config(), ad_account_id=_reddit_account(ad_account_id))
2854
2873
 
2855
2874
 
2856
- @mcp.tool(title="Search Reddit targeting", annotations=_READONLY, tags={"reddit"})
2875
+ @_tool(title="Search Reddit targeting", annotations=_READONLY, tags={"reddit"})
2857
2876
  @_safe
2858
2877
  def search_reddit_targeting(
2859
2878
  kind: str, query: str = "", country: str = "", website_url: str = "", limit: int = 25
@@ -2875,7 +2894,7 @@ def search_reddit_targeting(
2875
2894
  )
2876
2895
 
2877
2896
 
2878
- @mcp.tool(title="Reddit account change history", annotations=_READONLY, tags={"reddit"})
2897
+ @_tool(title="Reddit account change history", annotations=_READONLY, tags={"reddit"})
2879
2898
  @_safe
2880
2899
  def get_reddit_account_history(
2881
2900
  ad_account_id: str = "",
@@ -2906,7 +2925,7 @@ def get_reddit_account_history(
2906
2925
  )
2907
2926
 
2908
2927
 
2909
- @mcp.tool(title="Estimate a Reddit ad group", annotations=_READONLY, tags={"reddit"})
2928
+ @_tool(title="Estimate a Reddit ad group", annotations=_READONLY, tags={"reddit"})
2910
2929
  @_safe
2911
2930
  def estimate_reddit_ad_group(
2912
2931
  daily_budget: float | None = None,
@@ -2966,7 +2985,7 @@ def estimate_reddit_ad_group(
2966
2985
  )
2967
2986
 
2968
2987
 
2969
- @mcp.tool(title="Draft pausing a Reddit entity", annotations=_WRITE, tags={"reddit"})
2988
+ @_tool(title="Draft pausing a Reddit entity", annotations=_WRITE, tags={"reddit"})
2970
2989
  @_safe
2971
2990
  def pause_reddit_entity(
2972
2991
  entity_type: str, entity_id: str, ad_account_id: str = ""
@@ -2986,7 +3005,7 @@ def pause_reddit_entity(
2986
3005
  )
2987
3006
 
2988
3007
 
2989
- @mcp.tool(title="Draft enabling a Reddit entity", annotations=_WRITE, tags={"reddit"})
3008
+ @_tool(title="Draft enabling a Reddit entity", annotations=_WRITE, tags={"reddit"})
2990
3009
  @_safe
2991
3010
  def enable_reddit_entity(
2992
3011
  entity_type: str, entity_id: str, ad_account_id: str = ""
@@ -3006,7 +3025,7 @@ def enable_reddit_entity(
3006
3025
  )
3007
3026
 
3008
3027
 
3009
- @mcp.tool(title="Draft archiving a Reddit entity", annotations=_DESTRUCTIVE, tags={"reddit"})
3028
+ @_tool(title="Draft archiving a Reddit entity", annotations=_DESTRUCTIVE, tags={"reddit"})
3010
3029
  @_safe
3011
3030
  def remove_reddit_entity(
3012
3031
  entity_type: str, entity_id: str, ad_account_id: str = ""
@@ -3027,7 +3046,7 @@ def remove_reddit_entity(
3027
3046
  )
3028
3047
 
3029
3048
 
3030
- @mcp.tool(title="Draft Reddit campaign changes", annotations=_WRITE, tags={"reddit"})
3049
+ @_tool(title="Draft Reddit campaign changes", annotations=_WRITE, tags={"reddit"})
3031
3050
  @_safe
3032
3051
  def update_reddit_campaign(
3033
3052
  campaign_id: str,
@@ -3075,7 +3094,7 @@ def update_reddit_campaign(
3075
3094
  )
3076
3095
 
3077
3096
 
3078
- @mcp.tool(title="Draft Reddit ad group changes", annotations=_WRITE, tags={"reddit"})
3097
+ @_tool(title="Draft Reddit ad group changes", annotations=_WRITE, tags={"reddit"})
3079
3098
  @_safe
3080
3099
  def update_reddit_ad_group(
3081
3100
  ad_group_id: str,
@@ -3146,7 +3165,7 @@ def update_reddit_ad_group(
3146
3165
  )
3147
3166
 
3148
3167
 
3149
- @mcp.tool(title="Draft Reddit ad changes", annotations=_WRITE, tags={"reddit"})
3168
+ @_tool(title="Draft Reddit ad changes", annotations=_WRITE, tags={"reddit"})
3150
3169
  @_safe
3151
3170
  def update_reddit_ad(
3152
3171
  ad_id: str,
@@ -3173,7 +3192,7 @@ def update_reddit_ad(
3173
3192
  )
3174
3193
 
3175
3194
 
3176
- @mcp.tool(title="Draft a Reddit campaign", annotations=_WRITE, tags={"reddit"})
3195
+ @_tool(title="Draft a Reddit campaign", annotations=_WRITE, tags={"reddit"})
3177
3196
  @_safe
3178
3197
  def draft_reddit_campaign(
3179
3198
  campaign_name: str,
@@ -3232,7 +3251,7 @@ def draft_reddit_campaign(
3232
3251
  )
3233
3252
 
3234
3253
 
3235
- @mcp.tool(title="Draft a Reddit ad group", annotations=_WRITE, tags={"reddit"})
3254
+ @_tool(title="Draft a Reddit ad group", annotations=_WRITE, tags={"reddit"})
3236
3255
  @_safe
3237
3256
  def draft_reddit_ad_group(
3238
3257
  campaign_id: str,
@@ -3313,7 +3332,7 @@ def draft_reddit_ad_group(
3313
3332
  )
3314
3333
 
3315
3334
 
3316
- @mcp.tool(title="Draft a Reddit ad", annotations=_WRITE, tags={"reddit"})
3335
+ @_tool(title="Draft a Reddit ad", annotations=_WRITE, tags={"reddit"})
3317
3336
  @_safe
3318
3337
  def draft_reddit_ad(
3319
3338
  ad_group_id: str,
@@ -3369,7 +3388,7 @@ def draft_reddit_ad(
3369
3388
  # ---------------------------------------------------------------------------
3370
3389
 
3371
3390
 
3372
- @mcp.tool(title="Draft an Analytics key event", annotations=_WRITE, tags={"ga4"})
3391
+ @_tool(title="Draft an Analytics key event", annotations=_WRITE, tags={"ga4"})
3373
3392
  @_safe
3374
3393
  def draft_key_event(
3375
3394
  event_name: str,
@@ -3394,7 +3413,7 @@ def draft_key_event(
3394
3413
  )
3395
3414
 
3396
3415
 
3397
- @mcp.tool(title="Draft a conversion action", annotations=_WRITE, tags={"ads"})
3416
+ @_tool(title="Draft a conversion action", annotations=_WRITE, tags={"ads"})
3398
3417
  @_safe
3399
3418
  def draft_create_conversion_action(
3400
3419
  name: str,
@@ -3447,7 +3466,7 @@ def draft_create_conversion_action(
3447
3466
  )
3448
3467
 
3449
3468
 
3450
- @mcp.tool(title="Draft conversion action changes", annotations=_WRITE, tags={"ads"})
3469
+ @_tool(title="Draft conversion action changes", annotations=_WRITE, tags={"ads"})
3451
3470
  @_safe
3452
3471
  def draft_update_conversion_action(
3453
3472
  conversion_action_id: str,
@@ -3494,7 +3513,7 @@ def draft_update_conversion_action(
3494
3513
  )
3495
3514
 
3496
3515
 
3497
- @mcp.tool(title="Draft removing a conversion action", annotations=_WRITE, tags={"ads"})
3516
+ @_tool(title="Draft removing a conversion action", annotations=_WRITE, tags={"ads"})
3498
3517
  @_safe
3499
3518
  def draft_remove_conversion_action(
3500
3519
  conversion_action_id: str,
@@ -3518,7 +3537,7 @@ def draft_remove_conversion_action(
3518
3537
  )
3519
3538
 
3520
3539
 
3521
- @mcp.tool(title="Validate tracking", annotations=_READONLY, tags={"tracking"})
3540
+ @_tool(title="Validate tracking", annotations=_READONLY, tags={"tracking"})
3522
3541
  @_safe
3523
3542
  def validate_tracking(
3524
3543
  expected_events: _StrList,
@@ -3546,7 +3565,7 @@ def validate_tracking(
3546
3565
  )
3547
3566
 
3548
3567
 
3549
- @mcp.tool(title="Generate tracking code", annotations=_READONLY, tags={"tracking"})
3568
+ @_tool(title="Generate tracking code", annotations=_READONLY, tags={"tracking"})
3550
3569
  @_safe
3551
3570
  def generate_tracking_code(
3552
3571
  event_name: str,
@@ -3581,7 +3600,7 @@ def generate_tracking_code(
3581
3600
  # ---------------------------------------------------------------------------
3582
3601
 
3583
3602
 
3584
- @mcp.tool(title="Budget forecast", annotations=_READONLY, tags={"ads"})
3603
+ @_tool(title="Budget forecast", annotations=_READONLY, tags={"ads"})
3585
3604
  @_safe
3586
3605
  def estimate_budget(
3587
3606
  keywords: _DictList,
@@ -3618,7 +3637,7 @@ def estimate_budget(
3618
3637
  )
3619
3638
 
3620
3639
 
3621
- @mcp.tool(title="Keyword ideas", annotations=_READONLY, tags={"ads"})
3640
+ @_tool(title="Keyword ideas", annotations=_READONLY, tags={"ads"})
3622
3641
  @_safe
3623
3642
  def discover_keywords(
3624
3643
  seed_keywords: _StrList = [], # noqa: B006 — mutable default required for MCP JSON schema
@@ -3666,7 +3685,7 @@ def discover_keywords(
3666
3685
  # ---------------------------------------------------------------------------
3667
3686
 
3668
3687
 
3669
- @mcp.tool(title="Suggest brands", annotations=_READONLY, tags={"ads"})
3688
+ @_tool(title="Suggest brands", annotations=_READONLY, tags={"ads"})
3670
3689
  @_safe
3671
3690
  def suggest_brands(
3672
3691
  brand_prefix: str,
@@ -3703,7 +3722,7 @@ def suggest_brands(
3703
3722
  )
3704
3723
 
3705
3724
 
3706
- @mcp.tool(title="Check brand names", annotations=_READONLY, tags={"ads"})
3725
+ @_tool(title="Check brand names", annotations=_READONLY, tags={"ads"})
3707
3726
  @_safe
3708
3727
  def check_brand_names(
3709
3728
  brand_names: _StrList,
@@ -3736,7 +3755,7 @@ def check_brand_names(
3736
3755
  customer_id=customer_id or current_config().ads.customer_id,
3737
3756
  )
3738
3757
 
3739
- @mcp.tool(title="List brand lists", annotations=_READONLY, tags={"ads"})
3758
+ @_tool(title="List brand lists", annotations=_READONLY, tags={"ads"})
3740
3759
  @_safe
3741
3760
  def get_brand_lists(
3742
3761
  customer_id: str = "",
@@ -3756,7 +3775,7 @@ def get_brand_lists(
3756
3775
  customer_id=customer_id or current_config().ads.customer_id,
3757
3776
  )
3758
3777
 
3759
- @mcp.tool(title="Brands in a brand list", annotations=_READONLY, tags={"ads"})
3778
+ @_tool(title="Brands in a brand list", annotations=_READONLY, tags={"ads"})
3760
3779
  @_safe
3761
3780
  def get_brand_list_brands(
3762
3781
  shared_set_id: str,
@@ -3780,7 +3799,7 @@ def get_brand_list_brands(
3780
3799
  shared_set_id=shared_set_id,
3781
3800
  )
3782
3801
 
3783
- @mcp.tool(title="Campaigns using a brand list", annotations=_READONLY, tags={"ads"})
3802
+ @_tool(title="Campaigns using a brand list", annotations=_READONLY, tags={"ads"})
3784
3803
  @_safe
3785
3804
  def get_brand_list_campaigns(
3786
3805
  shared_set_id: str = "",
@@ -3806,7 +3825,7 @@ def get_brand_list_campaigns(
3806
3825
  shared_set_id=shared_set_id,
3807
3826
  )
3808
3827
 
3809
- @mcp.tool(title="Draft a brand list", annotations=_WRITE, tags={"ads"})
3828
+ @_tool(title="Draft a brand list", annotations=_WRITE, tags={"ads"})
3810
3829
  @_safe
3811
3830
  def propose_brand_list(
3812
3831
  list_name: str,
@@ -3844,7 +3863,7 @@ def propose_brand_list(
3844
3863
  negative=negative,
3845
3864
  )
3846
3865
 
3847
- @mcp.tool(title="Draft additions to a brand list", annotations=_WRITE, tags={"ads"})
3866
+ @_tool(title="Draft additions to a brand list", annotations=_WRITE, tags={"ads"})
3848
3867
  @_safe
3849
3868
  def add_to_brand_list(
3850
3869
  shared_set_id: str,
@@ -3874,7 +3893,7 @@ def add_to_brand_list(
3874
3893
  brand_ids=brand_ids,
3875
3894
  )
3876
3895
 
3877
- @mcp.tool(title="Draft removing brands from a list", annotations=_DESTRUCTIVE, tags={"ads"})
3896
+ @_tool(title="Draft removing brands from a list", annotations=_DESTRUCTIVE, tags={"ads"})
3878
3897
  @_safe
3879
3898
  def remove_from_brand_list(
3880
3899
  shared_set_id: str,
@@ -3904,7 +3923,7 @@ def remove_from_brand_list(
3904
3923
  criterion_ids=criterion_ids,
3905
3924
  )
3906
3925
 
3907
- @mcp.tool(title="Draft attaching a brand list", annotations=_WRITE, tags={"ads"})
3926
+ @_tool(title="Draft attaching a brand list", annotations=_WRITE, tags={"ads"})
3908
3927
  @_safe
3909
3928
  def attach_brand_list_to_campaigns(
3910
3929
  shared_set_id: str,
@@ -3941,7 +3960,7 @@ def attach_brand_list_to_campaigns(
3941
3960
  negative=negative,
3942
3961
  )
3943
3962
 
3944
- @mcp.tool(title="Draft detaching a brand list", annotations=_DESTRUCTIVE, tags={"ads"})
3963
+ @_tool(title="Draft detaching a brand list", annotations=_DESTRUCTIVE, tags={"ads"})
3945
3964
  @_safe
3946
3965
  def detach_brand_list_from_campaigns(
3947
3966
  shared_set_id: str,
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