adloop 0.22.0__tar.gz → 0.22.1__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 (66) hide show
  1. {adloop-0.22.0 → adloop-0.22.1}/PKG-INFO +1 -1
  2. {adloop-0.22.0 → adloop-0.22.1}/pyproject.toml +1 -1
  3. {adloop-0.22.0 → adloop-0.22.1}/pyproject.toml.orig +1 -1
  4. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/rules/adloop.md +2 -0
  5. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/runtime.py +19 -1
  6. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/server.py +54 -2
  7. {adloop-0.22.0 → adloop-0.22.1}/README.md +0 -0
  8. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/__init__.py +0 -0
  9. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/__main__.py +0 -0
  10. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/_mcp_patches.py +0 -0
  11. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/__init__.py +0 -0
  12. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/ai_max.py +0 -0
  13. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/brands.py +0 -0
  14. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/client.py +0 -0
  15. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/conversion_actions.py +0 -0
  16. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/currency.py +0 -0
  17. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/custom_conversion_goals.py +0 -0
  18. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/enums.py +0 -0
  19. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/forecast.py +0 -0
  20. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/gaql.py +0 -0
  21. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/image_fetch.py +0 -0
  22. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/pmax.py +0 -0
  23. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/read.py +0 -0
  24. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/validate_only.py +0 -0
  25. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ads/write.py +0 -0
  26. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/auth.py +0 -0
  27. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/cli.py +0 -0
  28. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/config.py +0 -0
  29. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/crossref.py +0 -0
  30. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/diagnostics.py +0 -0
  31. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ga4/__init__.py +0 -0
  32. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ga4/client.py +0 -0
  33. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ga4/reports.py +0 -0
  34. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ga4/tracking.py +0 -0
  35. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/ga4/write.py +0 -0
  36. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/gsc/__init__.py +0 -0
  37. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/gsc/client.py +0 -0
  38. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/gsc/reports.py +0 -0
  39. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/gtm/__init__.py +0 -0
  40. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/gtm/client.py +0 -0
  41. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/gtm/entities.py +0 -0
  42. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/gtm/read.py +0 -0
  43. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/gtm/write.py +0 -0
  44. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/merchant/__init__.py +0 -0
  45. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/merchant/client.py +0 -0
  46. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/merchant/read.py +0 -0
  47. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/pagespeed.py +0 -0
  48. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/reddit/__init__.py +0 -0
  49. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/reddit/auth.py +0 -0
  50. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/reddit/client.py +0 -0
  51. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/reddit/read.py +0 -0
  52. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/reddit/schedule.py +0 -0
  53. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/reddit/write.py +0 -0
  54. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/rules/__init__.py +0 -0
  55. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/rules/commands/analyze-performance.md +0 -0
  56. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/rules/commands/budget-plan.md +0 -0
  57. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/rules/commands/create-ad.md +0 -0
  58. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/rules/commands/create-campaign.md +0 -0
  59. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
  60. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/rules/commands/optimize-campaign.md +0 -0
  61. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/rules_install.py +0 -0
  62. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/safety/__init__.py +0 -0
  63. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/safety/audit.py +0 -0
  64. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/safety/guards.py +0 -0
  65. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/safety/preview.py +0 -0
  66. {adloop-0.22.0 → adloop-0.22.1}/src/adloop/tracking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: adloop
3
- Version: 0.22.0
3
+ Version: 0.22.1
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.22.0"
3
+ version = "0.22.1"
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.22.0"
3
+ version = "0.22.1"
4
4
  description = "The AI command center for Google Ads, Reddit Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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,24 @@ _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
+
49
67
  def deployment_mode() -> DeploymentMode:
50
68
  """Return the process-wide deployment mode ("local" or "server")."""
51
69
  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
@@ -166,8 +166,22 @@ def _gtm_defaults(account_id: str, container_id: str) -> tuple[str, str]:
166
166
  mcp = FastMCP(
167
167
  "AdLoop",
168
168
  instructions=_build_orchestration_instructions(),
169
+ website_url="https://getadloop.com",
170
+ # Clients that support MCP server icons show these instead of guessing
171
+ # from the domain's favicon. Square and full-bleed, so a client's own
172
+ # rounded mask leaves no stray corners.
173
+ icons=[
174
+ Icon(src="https://getadloop.com/icon-512.png", mimeType="image/png", sizes=["512x512"]),
175
+ Icon(src="https://getadloop.com/favicon.svg", mimeType="image/svg+xml", sizes=["any"]),
176
+ ],
169
177
  )
170
178
 
179
+ # name -> tags of every registered tool, so health_check can say which tools
180
+ # this server offers without reaching into FastMCP internals.
181
+ _TOOL_TAGS: dict[str, frozenset[str]] = {}
182
+ # Tags enabled by ADLOOP_TOOLSETS (None: the full catalog).
183
+ _ENABLED_TAGS: set[str] | None = None
184
+
171
185
 
172
186
  # The API each toolset calls, linked at the end of every tool description:
173
187
  # directory reviews ask descriptions to reference the API they target.
@@ -217,6 +231,7 @@ def _tool(*, title: str, annotations: ToolAnnotations, tags: set[str], **kwargs)
217
231
  description = parse_docstring(fn).description or inspect.cleandoc(fn.__doc__ or "")
218
232
  if docs and "https://" not in description:
219
233
  description = f"{description}\n\n{docs}"
234
+ _TOOL_TAGS[kwargs.get("name") or fn.__name__] = frozenset(tags)
220
235
  return mcp.tool(
221
236
  title=title,
222
237
  annotations=annotations,
@@ -585,9 +600,44 @@ def health_check() -> dict:
585
600
  elif status.get("reddit") == "error" and status.get("reddit_hint"):
586
601
  status["hint"] = status["reddit_hint"]
587
602
 
603
+ status["adloop_version"] = _adloop_version()
604
+ status["tools_offered"] = _offered_tool_names()
605
+ status["tools_note"] = (
606
+ "These are the tools this AdLoop server offers to you. If one of "
607
+ "them is missing from your own tool list, your app is showing a "
608
+ "cached list from an earlier version: ask the user to refresh the "
609
+ "AdLoop connection (claude.ai: Settings → Connectors → AdLoop; "
610
+ "ChatGPT: the AdLoop plugin's settings; Perplexity: remove and add "
611
+ "the connector again; coding agents: reconnect the MCP server or "
612
+ "start a new session)."
613
+ )
588
614
  return status
589
615
 
590
616
 
617
+ def _adloop_version() -> str:
618
+ from importlib.metadata import PackageNotFoundError, version
619
+
620
+ try:
621
+ return version("adloop")
622
+ except PackageNotFoundError:
623
+ return "unknown"
624
+
625
+
626
+ def _offered_tool_names() -> list[str]:
627
+ """The tools the current caller sees in tools/list: enabled by
628
+ ADLOOP_TOOLSETS, then narrowed by the runtime's visibility hook."""
629
+ from adloop.runtime import tool_visibility
630
+
631
+ tools = {
632
+ name: tags
633
+ for name, tags in _TOOL_TAGS.items()
634
+ if _ENABLED_TAGS is None or tags & _ENABLED_TAGS
635
+ }
636
+ hook = tool_visibility()
637
+ names = hook(tools) if hook is not None else tools
638
+ return sorted(names)
639
+
640
+
591
641
  # ---------------------------------------------------------------------------
592
642
  # GA4 Read Tools
593
643
  # ---------------------------------------------------------------------------
@@ -5728,7 +5778,9 @@ def _apply_toolsets_env() -> None:
5728
5778
  f"ADLOOP_TOOLSETS names unknown toolset(s): {', '.join(unknown)}. "
5729
5779
  f"Valid toolsets: {', '.join(TOOLSETS)}. Example: ADLOOP_TOOLSETS=ads,ga4"
5730
5780
  )
5731
- mcp.enable(tags=requested | {"core"}, only=True)
5781
+ global _ENABLED_TAGS
5782
+ _ENABLED_TAGS = requested | {"core"}
5783
+ mcp.enable(tags=_ENABLED_TAGS, only=True)
5732
5784
 
5733
5785
 
5734
5786
  _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