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.
- {adloop-0.6.2 → adloop-0.6.3}/PKG-INFO +2 -1
- {adloop-0.6.2 → adloop-0.6.3}/README.md +1 -0
- {adloop-0.6.2 → adloop-0.6.3}/pyproject.toml +1 -1
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/__init__.py +1 -1
- adloop-0.6.3/src/adloop/_mcp_patches.py +161 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/server.py +2 -1
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/__main__.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/__init__.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/client.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/currency.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/forecast.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/gaql.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/pmax.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/read.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ads/write.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/auth.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/bundled_credentials.json +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/cli.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/config.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/crossref.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/diagnostics.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ga4/__init__.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ga4/client.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ga4/reports.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/ga4/tracking.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/safety/__init__.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/safety/audit.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/safety/guards.py +0 -0
- {adloop-0.6.2 → adloop-0.6.3}/src/adloop/safety/preview.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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
|