adloop 0.22.0__tar.gz → 0.22.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {adloop-0.22.0 → adloop-0.22.2}/PKG-INFO +1 -1
- {adloop-0.22.0 → adloop-0.22.2}/pyproject.toml +1 -1
- {adloop-0.22.0 → adloop-0.22.2}/pyproject.toml.orig +1 -1
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/gsc/reports.py +6 -2
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/merchant/read.py +5 -2
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/reddit/read.py +6 -2
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/rules/adloop.md +2 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/runtime.py +33 -1
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/server.py +65 -7
- {adloop-0.22.0 → adloop-0.22.2}/README.md +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/__init__.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/__main__.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/_mcp_patches.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/__init__.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/ai_max.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/brands.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/client.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/conversion_actions.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/currency.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/custom_conversion_goals.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/enums.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/forecast.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/gaql.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/image_fetch.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/pmax.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/read.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/validate_only.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ads/write.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/auth.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/cli.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/config.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/crossref.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/diagnostics.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ga4/__init__.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ga4/client.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ga4/reports.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ga4/tracking.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/ga4/write.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/gsc/__init__.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/gsc/client.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/gtm/__init__.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/gtm/client.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/gtm/entities.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/gtm/read.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/gtm/write.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/merchant/__init__.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/merchant/client.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/pagespeed.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/reddit/__init__.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/reddit/auth.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/reddit/client.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/reddit/schedule.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/reddit/write.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/rules/__init__.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/rules/commands/analyze-performance.md +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/rules/commands/budget-plan.md +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/rules/commands/create-ad.md +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/rules/commands/create-campaign.md +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/rules/commands/optimize-campaign.md +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/rules_install.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/safety/__init__.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/safety/audit.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/safety/guards.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/safety/preview.py +0 -0
- {adloop-0.22.0 → adloop-0.22.2}/src/adloop/tracking.py +0 -0
|
@@ -101,9 +101,13 @@ def run_gsc_report(
|
|
|
101
101
|
site_url = config.gsc.site_url
|
|
102
102
|
|
|
103
103
|
if not site_url:
|
|
104
|
+
from adloop.runtime import default_setting_hint
|
|
105
|
+
|
|
104
106
|
return {
|
|
105
|
-
"error": "site_url is required. Pass it as an argument
|
|
106
|
-
"
|
|
107
|
+
"error": "site_url is required. Pass it as an argument (see "
|
|
108
|
+
"list_gsc_sites) or "
|
|
109
|
+
+ default_setting_hint("gsc.site_url", "Settings → Google & accounts")
|
|
110
|
+
+ "."
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
dimensions = [_normalize_dimension(d) for d in (dimensions or ["query"])]
|
|
@@ -126,6 +126,8 @@ def get_merchant_feed_health(
|
|
|
126
126
|
|
|
127
127
|
account_id = str(account_id or "").strip() or config.merchant.account_id.strip()
|
|
128
128
|
if not account_id:
|
|
129
|
+
from adloop.runtime import default_setting_hint
|
|
130
|
+
|
|
129
131
|
return {
|
|
130
132
|
"error": (
|
|
131
133
|
"account_id is required: no Merchant Center account was "
|
|
@@ -133,8 +135,9 @@ def get_merchant_feed_health(
|
|
|
133
135
|
),
|
|
134
136
|
"hint": (
|
|
135
137
|
"Call list_merchant_accounts to find the numeric Merchant "
|
|
136
|
-
"Center ID and pass it as account_id, or
|
|
137
|
-
"merchant.account_id
|
|
138
|
+
"Center ID and pass it as account_id, or "
|
|
139
|
+
+ default_setting_hint("merchant.account_id", "Settings → Google & accounts")
|
|
140
|
+
+ "."
|
|
138
141
|
),
|
|
139
142
|
}
|
|
140
143
|
if not account_id.isdigit():
|
|
@@ -103,9 +103,13 @@ def resolve_account(config: AdLoopConfig, ad_account_id: str = "") -> str:
|
|
|
103
103
|
"""Explicit id wins, then ``reddit.ad_account_id`` from the config."""
|
|
104
104
|
account = (ad_account_id or config.reddit.ad_account_id or "").strip()
|
|
105
105
|
if not account:
|
|
106
|
+
from adloop.runtime import default_setting_hint
|
|
107
|
+
|
|
106
108
|
raise ValueError(
|
|
107
|
-
"ad_account_id is required
|
|
108
|
-
"list_reddit_accounts) or
|
|
109
|
+
"ad_account_id is required: pass it explicitly (see "
|
|
110
|
+
"list_reddit_accounts) or "
|
|
111
|
+
+ default_setting_hint("reddit.ad_account_id", "Settings → Reddit Ads")
|
|
112
|
+
+ "."
|
|
109
113
|
)
|
|
110
114
|
return account
|
|
111
115
|
|
|
@@ -14,6 +14,8 @@ You have access to AdLoop MCP tools that connect Google Ads, Reddit Ads and Goog
|
|
|
14
14
|
|------|-------------|----------------|
|
|
15
15
|
| `health_check` | First thing to run when tools are failing — tests OAuth token, GA4, Ads, and (when configured) Reddit Ads connectivity | (none) |
|
|
16
16
|
|
|
17
|
+
**A tool you expect is missing?** Apps such as claude.ai, ChatGPT and Perplexity cache AdLoop's tool list from when the connection was made. `health_check` returns `adloop_version` and `tools_offered`, the tools this server offers to you. If the user asks for something a tool in `tools_offered` does and that tool isn't in your own tool list, don't improvise with other tools: tell the user their app shows an older list and to refresh the AdLoop connection (claude.ai: Settings → Connectors → AdLoop; ChatGPT: the AdLoop plugin's settings; Perplexity: remove and add the connector again; coding agents: reconnect the MCP server or start a new session).
|
|
18
|
+
|
|
17
19
|
**GA4 has two surfaces in health_check:** `ga4_admin` (listing properties) and `ga4_data` (every report). `ga4_data: error` with `SERVICE_DISABLED` means the Google Analytics Data API is not enabled in the Google Cloud project — tell the user to enable it; it is not a permissions or consent problem.
|
|
18
20
|
|
|
19
21
|
**If health_check reports Google auth errors:** Tell the user to delete `~/.adloop/token.json` and re-run any tool to trigger re-authorization. If tokens keep expiring weekly, the GCP consent screen needs to be published from "Testing" to "In production". **Reddit auth errors** (`REDDIT_INVALID_GRANT`, `REDDIT_NOT_CONNECTED`) are fixed by re-running the Reddit step of `adloop init` (self-hosted) or reconnecting under Settings → Reddit Ads (AdLoop Cloud); the Google token is unrelated.
|
|
@@ -26,7 +26,7 @@ from __future__ import annotations
|
|
|
26
26
|
import threading
|
|
27
27
|
from contextlib import contextmanager
|
|
28
28
|
from contextvars import ContextVar
|
|
29
|
-
from typing import TYPE_CHECKING, Iterator, Literal
|
|
29
|
+
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Literal
|
|
30
30
|
|
|
31
31
|
if TYPE_CHECKING:
|
|
32
32
|
from adloop.config import AdLoopConfig
|
|
@@ -46,6 +46,38 @@ _default_config_lock = threading.Lock()
|
|
|
46
46
|
_deployment_mode: DeploymentMode = "local"
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
# Which registered tools the current caller is actually offered, given each
|
|
50
|
+
# tool's tags. Self-hosted, every enabled tool is; a hosted runtime narrows it
|
|
51
|
+
# per request (a key's toolsets, tools it cannot serve) so health_check can
|
|
52
|
+
# report the same list the caller's tools/list shows.
|
|
53
|
+
ToolVisibility = Callable[[dict[str, frozenset[str]]], Iterable[str]]
|
|
54
|
+
_tool_visibility: ToolVisibility | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def set_tool_visibility(fn: ToolVisibility | None) -> None:
|
|
58
|
+
"""Install the per-request tool filter. Called once at startup."""
|
|
59
|
+
global _tool_visibility
|
|
60
|
+
_tool_visibility = fn
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def tool_visibility() -> ToolVisibility | None:
|
|
64
|
+
return _tool_visibility
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def default_setting_hint(config_key: str, dashboard_page: str) -> str:
|
|
68
|
+
"""How to set a missing default, phrased for where AdLoop runs.
|
|
69
|
+
|
|
70
|
+
Self-hosted it is a key in the config file; a hosted runtime has no
|
|
71
|
+
config file the user can edit, so it names the dashboard page instead.
|
|
72
|
+
"""
|
|
73
|
+
if _deployment_mode == "server":
|
|
74
|
+
return (
|
|
75
|
+
f"choose a default under {dashboard_page} in the AdLoop Cloud "
|
|
76
|
+
"dashboard (agencies: per client under Settings → Clients)"
|
|
77
|
+
)
|
|
78
|
+
return f"set {config_key} in the config (~/.adloop/config.yaml)"
|
|
79
|
+
|
|
80
|
+
|
|
49
81
|
def deployment_mode() -> DeploymentMode:
|
|
50
82
|
"""Return the process-wide deployment mode ("local" or "server")."""
|
|
51
83
|
return _deployment_mode
|
|
@@ -9,7 +9,7 @@ from typing import Annotated, Callable
|
|
|
9
9
|
|
|
10
10
|
from fastmcp import FastMCP
|
|
11
11
|
from fastmcp.utilities.docstring_parsing import parse_docstring
|
|
12
|
-
from mcp.types import ToolAnnotations
|
|
12
|
+
from mcp.types import Icon, ToolAnnotations
|
|
13
13
|
from pydantic import BeforeValidator
|
|
14
14
|
|
|
15
15
|
from adloop import diagnostics
|
|
@@ -155,10 +155,13 @@ def _gtm_defaults(account_id: str, container_id: str) -> tuple[str, str]:
|
|
|
155
155
|
account_id = account_id or cfg.account_id
|
|
156
156
|
container_id = container_id or cfg.container_id
|
|
157
157
|
if not account_id or not container_id:
|
|
158
|
+
from adloop.runtime import default_setting_hint
|
|
159
|
+
|
|
158
160
|
raise ValueError(
|
|
159
|
-
"gtm_account_id and gtm_container_id are required
|
|
160
|
-
"explicitly (see list_gtm_accounts / list_gtm_containers) or
|
|
161
|
-
"gtm.account_id / gtm.container_id
|
|
161
|
+
"gtm_account_id and gtm_container_id are required: pass them "
|
|
162
|
+
"explicitly (see list_gtm_accounts / list_gtm_containers) or "
|
|
163
|
+
+ default_setting_hint("gtm.account_id / gtm.container_id", "Settings → Google & accounts")
|
|
164
|
+
+ "."
|
|
162
165
|
)
|
|
163
166
|
return account_id, container_id
|
|
164
167
|
|
|
@@ -166,8 +169,22 @@ def _gtm_defaults(account_id: str, container_id: str) -> tuple[str, str]:
|
|
|
166
169
|
mcp = FastMCP(
|
|
167
170
|
"AdLoop",
|
|
168
171
|
instructions=_build_orchestration_instructions(),
|
|
172
|
+
website_url="https://getadloop.com",
|
|
173
|
+
# Clients that support MCP server icons show these instead of guessing
|
|
174
|
+
# from the domain's favicon. Square and full-bleed, so a client's own
|
|
175
|
+
# rounded mask leaves no stray corners.
|
|
176
|
+
icons=[
|
|
177
|
+
Icon(src="https://getadloop.com/icon-512.png", mimeType="image/png", sizes=["512x512"]),
|
|
178
|
+
Icon(src="https://getadloop.com/favicon.svg", mimeType="image/svg+xml", sizes=["any"]),
|
|
179
|
+
],
|
|
169
180
|
)
|
|
170
181
|
|
|
182
|
+
# name -> tags of every registered tool, so health_check can say which tools
|
|
183
|
+
# this server offers without reaching into FastMCP internals.
|
|
184
|
+
_TOOL_TAGS: dict[str, frozenset[str]] = {}
|
|
185
|
+
# Tags enabled by ADLOOP_TOOLSETS (None: the full catalog).
|
|
186
|
+
_ENABLED_TAGS: set[str] | None = None
|
|
187
|
+
|
|
171
188
|
|
|
172
189
|
# The API each toolset calls, linked at the end of every tool description:
|
|
173
190
|
# directory reviews ask descriptions to reference the API they target.
|
|
@@ -217,6 +234,7 @@ def _tool(*, title: str, annotations: ToolAnnotations, tags: set[str], **kwargs)
|
|
|
217
234
|
description = parse_docstring(fn).description or inspect.cleandoc(fn.__doc__ or "")
|
|
218
235
|
if docs and "https://" not in description:
|
|
219
236
|
description = f"{description}\n\n{docs}"
|
|
237
|
+
_TOOL_TAGS[kwargs.get("name") or fn.__name__] = frozenset(tags)
|
|
220
238
|
return mcp.tool(
|
|
221
239
|
title=title,
|
|
222
240
|
annotations=annotations,
|
|
@@ -585,9 +603,44 @@ def health_check() -> dict:
|
|
|
585
603
|
elif status.get("reddit") == "error" and status.get("reddit_hint"):
|
|
586
604
|
status["hint"] = status["reddit_hint"]
|
|
587
605
|
|
|
606
|
+
status["adloop_version"] = _adloop_version()
|
|
607
|
+
status["tools_offered"] = _offered_tool_names()
|
|
608
|
+
status["tools_note"] = (
|
|
609
|
+
"These are the tools this AdLoop server offers to you. If one of "
|
|
610
|
+
"them is missing from your own tool list, your app is showing a "
|
|
611
|
+
"cached list from an earlier version: ask the user to refresh the "
|
|
612
|
+
"AdLoop connection (claude.ai: Settings → Connectors → AdLoop; "
|
|
613
|
+
"ChatGPT: the AdLoop plugin's settings; Perplexity: remove and add "
|
|
614
|
+
"the connector again; coding agents: reconnect the MCP server or "
|
|
615
|
+
"start a new session)."
|
|
616
|
+
)
|
|
588
617
|
return status
|
|
589
618
|
|
|
590
619
|
|
|
620
|
+
def _adloop_version() -> str:
|
|
621
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
622
|
+
|
|
623
|
+
try:
|
|
624
|
+
return version("adloop")
|
|
625
|
+
except PackageNotFoundError:
|
|
626
|
+
return "unknown"
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _offered_tool_names() -> list[str]:
|
|
630
|
+
"""The tools the current caller sees in tools/list: enabled by
|
|
631
|
+
ADLOOP_TOOLSETS, then narrowed by the runtime's visibility hook."""
|
|
632
|
+
from adloop.runtime import tool_visibility
|
|
633
|
+
|
|
634
|
+
tools = {
|
|
635
|
+
name: tags
|
|
636
|
+
for name, tags in _TOOL_TAGS.items()
|
|
637
|
+
if _ENABLED_TAGS is None or tags & _ENABLED_TAGS
|
|
638
|
+
}
|
|
639
|
+
hook = tool_visibility()
|
|
640
|
+
names = hook(tools) if hook is not None else tools
|
|
641
|
+
return sorted(names)
|
|
642
|
+
|
|
643
|
+
|
|
591
644
|
# ---------------------------------------------------------------------------
|
|
592
645
|
# GA4 Read Tools
|
|
593
646
|
# ---------------------------------------------------------------------------
|
|
@@ -1740,9 +1793,12 @@ def list_gtm_containers(gtm_account_id: str = "") -> dict:
|
|
|
1740
1793
|
|
|
1741
1794
|
gtm_account_id = gtm_account_id or current_config().gtm.account_id
|
|
1742
1795
|
if not gtm_account_id:
|
|
1796
|
+
from adloop.runtime import default_setting_hint
|
|
1797
|
+
|
|
1743
1798
|
raise ValueError(
|
|
1744
|
-
"gtm_account_id is required
|
|
1745
|
-
"
|
|
1799
|
+
"gtm_account_id is required: call list_gtm_accounts first or "
|
|
1800
|
+
+ default_setting_hint("gtm.account_id", "Settings → Google & accounts")
|
|
1801
|
+
+ "."
|
|
1746
1802
|
)
|
|
1747
1803
|
return _impl(current_config(), account_id=gtm_account_id)
|
|
1748
1804
|
|
|
@@ -5728,7 +5784,9 @@ def _apply_toolsets_env() -> None:
|
|
|
5728
5784
|
f"ADLOOP_TOOLSETS names unknown toolset(s): {', '.join(unknown)}. "
|
|
5729
5785
|
f"Valid toolsets: {', '.join(TOOLSETS)}. Example: ADLOOP_TOOLSETS=ads,ga4"
|
|
5730
5786
|
)
|
|
5731
|
-
|
|
5787
|
+
global _ENABLED_TAGS
|
|
5788
|
+
_ENABLED_TAGS = requested | {"core"}
|
|
5789
|
+
mcp.enable(tags=_ENABLED_TAGS, only=True)
|
|
5732
5790
|
|
|
5733
5791
|
|
|
5734
5792
|
_apply_toolsets_env()
|
|
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
|