adloop 0.6.2__tar.gz → 0.6.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 (30) hide show
  1. {adloop-0.6.2 → adloop-0.6.3}/PKG-INFO +2 -1
  2. {adloop-0.6.2 → adloop-0.6.3}/README.md +1 -0
  3. {adloop-0.6.2 → adloop-0.6.3}/pyproject.toml +1 -1
  4. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/__init__.py +1 -1
  5. adloop-0.6.3/src/adloop/_mcp_patches.py +161 -0
  6. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/server.py +2 -1
  7. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/__main__.py +0 -0
  8. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/__init__.py +0 -0
  9. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/client.py +0 -0
  10. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/currency.py +0 -0
  11. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/forecast.py +0 -0
  12. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/gaql.py +0 -0
  13. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/pmax.py +0 -0
  14. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/read.py +0 -0
  15. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/write.py +0 -0
  16. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/auth.py +0 -0
  17. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/bundled_credentials.json +0 -0
  18. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/cli.py +0 -0
  19. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/config.py +0 -0
  20. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/crossref.py +0 -0
  21. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/diagnostics.py +0 -0
  22. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ga4/__init__.py +0 -0
  23. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ga4/client.py +0 -0
  24. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ga4/reports.py +0 -0
  25. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ga4/tracking.py +0 -0
  26. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/safety/__init__.py +0 -0
  27. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/safety/audit.py +0 -0
  28. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/safety/guards.py +0 -0
  29. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/safety/preview.py +0 -0
  30. {adloop-0.6.2 → adloop-0.6.3}/src/adloop/tracking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: adloop
3
- Version: 0.6.2
3
+ Version: 0.6.3
4
4
  Summary: Stop switching between Google Ads, GA4, and your code editor to figure out why conversions dropped.
5
5
  Keywords: mcp,google-ads,google-analytics,ga4,cursor,marketing
6
6
  Author: Daniel Klose
@@ -394,6 +394,7 @@ What's been shipped and what's next:
394
394
  - ~~Retry/backoff for API rate limits~~ ✓
395
395
  - ~~Setup wizard (`adloop init`)~~ ✓
396
396
  - ~~Claude Code support~~ ✓ — `CLAUDE.md`, `.mcp.json`, `.claude/rules/`, `.claude/commands/`, CLI wizard snippets
397
+ - **Claude Desktop one-click install** — `adloop install claude-desktop` (and/or a `.dxt` extension bundle) that writes the AdLoop MCP entry into `claude_desktop_config.json` automatically, so Claude Desktop + Cowork users don't have to hand-edit JSON
397
398
  - ~~PyPI package~~ ✓ — `pip install adloop`
398
399
  - ~~Bundled OAuth credentials~~ ✓ — no Google Cloud project required, auto-discovery of GA4/Ads accounts (currently capped at 100 users pending Google verification — use [Advanced Setup](#advanced-setup-custom-google-cloud-project) in the meantime)
399
400
  - ~~Headless server support~~ ✓ — manual URL copy-paste flow for servers without a browser
@@ -370,6 +370,7 @@ What's been shipped and what's next:
370
370
  - ~~Retry/backoff for API rate limits~~ ✓
371
371
  - ~~Setup wizard (`adloop init`)~~ ✓
372
372
  - ~~Claude Code support~~ ✓ — `CLAUDE.md`, `.mcp.json`, `.claude/rules/`, `.claude/commands/`, CLI wizard snippets
373
+ - **Claude Desktop one-click install** — `adloop install claude-desktop` (and/or a `.dxt` extension bundle) that writes the AdLoop MCP entry into `claude_desktop_config.json` automatically, so Claude Desktop + Cowork users don't have to hand-edit JSON
373
374
  - ~~PyPI package~~ ✓ — `pip install adloop`
374
375
  - ~~Bundled OAuth credentials~~ ✓ — no Google Cloud project required, auto-discovery of GA4/Ads accounts (currently capped at 100 users pending Google verification — use [Advanced Setup](#advanced-setup-custom-google-cloud-project) in the meantime)
375
376
  - ~~Headless server support~~ ✓ — manual URL copy-paste flow for servers without a browser
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.6.2"
3
+ version = "0.6.3"
4
4
  description = "Stop switching between Google Ads, GA4, and your code editor to figure out why conversions dropped."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -2,7 +2,7 @@
2
2
 
3
3
  import sys
4
4
 
5
- __version__ = "0.6.2"
5
+ __version__ = "0.6.3"
6
6
 
7
7
 
8
8
  def main() -> None:
@@ -0,0 +1,161 @@
1
+ """Runtime patches for upstream ``mcp`` / ``fastmcp`` bugs.
2
+
3
+ This module exists to work around specific upstream defects that affect AdLoop
4
+ users in the wild. Every patch in here is a **temporary workaround** — the long-
5
+ term fix belongs upstream. Each entry below records exactly which upstream issue
6
+ it tracks and the condition under which this file should stop patching.
7
+
8
+ Currently tracked:
9
+
10
+ - **modelcontextprotocol/python-sdk#2416** — ``AssertionError: Request already
11
+ responded to`` cancellation race in ``mcp`` 1.27.0. Reliably crashes the MCP
12
+ server whenever the host (Claude Cowork, Claude Code post-update, etc.) sends
13
+ ``notifications/cancelled`` while a synchronous tool handler is still running.
14
+ We replace the two racing methods (``respond`` / ``cancel``) with guard-based
15
+ versions that cannot double-send a JSON-RPC response.
16
+
17
+ **Remove this patch** once ``mcp`` upstream ships a fix (watch the issue,
18
+ check the installed source for the ``assert not self._completed`` line — if
19
+ it's gone, we detect that automatically and skip patching).
20
+
21
+ Design goals:
22
+
23
+ 1. **Self-removing.** Each patch inspects the upstream source first and bails
24
+ out silently when the bug appears fixed. Upgrading the dependency is then
25
+ enough to deactivate the workaround — no code changes needed in AdLoop.
26
+ 2. **Idempotent.** ``install()`` is safe to call multiple times.
27
+ 3. **Never fatal.** If patching itself fails (e.g. upstream restructured the
28
+ module so inspection breaks), we log and continue — the server must start
29
+ even if the patch can't be applied.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import inspect
35
+ import sys
36
+ from typing import Any
37
+
38
+ from adloop import diagnostics
39
+
40
+
41
+ def _log(message: str) -> None:
42
+ """Write a single patch-status line via the diagnostics channel.
43
+
44
+ Falls back to a plain ``stderr`` write when diagnostics are disabled so
45
+ maintainers can still see patch activity if they go looking.
46
+ """
47
+ line = f"[adloop-patches] {message}"
48
+ if diagnostics.enabled():
49
+ diagnostics._emit("patch", message=message) # type: ignore[attr-defined]
50
+ return
51
+ try:
52
+ sys.stderr.write(line + "\n")
53
+ sys.stderr.flush()
54
+ except Exception:
55
+ pass
56
+
57
+
58
+ def _patch_request_responder_cancel_race() -> None:
59
+ """Apply the fix for python-sdk issue #2416.
60
+
61
+ Replaces ``RequestResponder.respond`` and ``RequestResponder.cancel`` with
62
+ versions that guard against double-responding via a synchronous
63
+ ``self._completed`` check before any ``await``. Exactly one of the two
64
+ methods wins the race; the loser returns without sending a second JSON-RPC
65
+ response, which in turn prevents the anyio TaskGroup from tearing the
66
+ entire stdio transport down.
67
+
68
+ Autodetects the upstream fix by inspecting the installed ``respond``
69
+ source: if the signature ``assert not self._completed`` is no longer
70
+ present, we assume upstream landed a fix and skip patching.
71
+ """
72
+ try:
73
+ from mcp.shared import session as _session_module
74
+ except Exception as exc: # pragma: no cover — mcp is a hard dep
75
+ _log(f"skip pysdk-2416: cannot import mcp.shared.session ({exc})")
76
+ return
77
+
78
+ responder_cls = getattr(_session_module, "RequestResponder", None)
79
+ if responder_cls is None:
80
+ _log("skip pysdk-2416: RequestResponder class not found")
81
+ return
82
+
83
+ try:
84
+ original_respond_src = inspect.getsource(responder_cls.respond)
85
+ except (OSError, TypeError) as exc:
86
+ _log(f"skip pysdk-2416: cannot inspect respond() source ({exc})")
87
+ return
88
+
89
+ if "assert not self._completed" not in original_respond_src:
90
+ # Upstream fixed the race (or moved it). No need to patch further.
91
+ _log("skip pysdk-2416: upstream source no longer contains the racey assert")
92
+ return
93
+
94
+ if getattr(responder_cls, "_adloop_2416_patched", False):
95
+ return
96
+
97
+ async def respond(self: Any, response: Any) -> None:
98
+ """Patched RequestResponder.respond — see python-sdk#2416."""
99
+ if not self._entered:
100
+ raise RuntimeError("RequestResponder must be used as a context manager")
101
+ # Upstream asserts here; we return early instead. If a concurrent
102
+ # cancel() already marked the request complete, silently drop this
103
+ # response — the cancel path has already sent one error response and a
104
+ # second JSON-RPC reply for the same request ID would be a protocol
105
+ # violation as well as crashing the TaskGroup on the assert.
106
+ if self._completed:
107
+ return
108
+ if not self.cancelled:
109
+ self._completed = True
110
+ await self._session._send_response(
111
+ request_id=self.request_id, response=response
112
+ )
113
+
114
+ async def cancel(self: Any) -> None:
115
+ """Patched RequestResponder.cancel — see python-sdk#2416."""
116
+ if not self._entered:
117
+ raise RuntimeError("RequestResponder must be used as a context manager")
118
+ if not self._cancel_scope:
119
+ raise RuntimeError("No active cancel scope")
120
+
121
+ self._cancel_scope.cancel()
122
+ # Symmetrical guard: if the handler's respond() already ran before the
123
+ # cancel notification arrived, don't send a second response.
124
+ if self._completed:
125
+ return
126
+ self._completed = True
127
+
128
+ from mcp.types import ErrorData # imported lazily to avoid startup cost
129
+
130
+ await self._session._send_response(
131
+ request_id=self.request_id,
132
+ response=ErrorData(code=0, message="Request cancelled", data=None),
133
+ )
134
+
135
+ responder_cls.respond = respond
136
+ responder_cls.cancel = cancel
137
+ responder_cls._adloop_2416_patched = True # type: ignore[attr-defined]
138
+
139
+ _log(
140
+ "applied pysdk-2416: RequestResponder.respond/cancel guarded against "
141
+ "cancellation double-respond race"
142
+ )
143
+
144
+
145
+ _INSTALLED = False
146
+
147
+
148
+ def install() -> None:
149
+ """Apply all AdLoop runtime patches. Idempotent.
150
+
151
+ Call this exactly once at server startup, before ``FastMCP`` begins
152
+ accepting messages. Calling it multiple times is safe — we gate on a
153
+ module-level flag and each individual patch also has its own reentry
154
+ guard.
155
+ """
156
+ global _INSTALLED
157
+ if _INSTALLED:
158
+ return
159
+ _INSTALLED = True
160
+
161
+ _patch_request_responder_cancel_race()
@@ -8,10 +8,11 @@ from typing import Callable
8
8
  from fastmcp import FastMCP
9
9
  from mcp.types import ToolAnnotations
10
10
 
11
- from adloop import diagnostics
11
+ from adloop import _mcp_patches, diagnostics
12
12
  from adloop.config import load_config
13
13
 
14
14
  diagnostics.install()
15
+ _mcp_patches.install()
15
16
 
16
17
  _READONLY = ToolAnnotations(readOnlyHint=True, destructiveHint=False)
17
18
  _WRITE = ToolAnnotations(readOnlyHint=False, destructiveHint=False)
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