v0.6.3
v0.6.3 — Fix MCP server crash on host cancellation
Under Claude Cowork, Claude Code (post-September 2025 update), and any other MCP host that sends notifications/cancelled, long-running AdLoop tool calls could silently crash the MCP server mid-session. The symptom from the user side: "AdLoop is healthy across the board… then it immediately goes offline after the first tool call". This release fixes the underlying cause at the MCP protocol layer, not at the AdLoop payload layer — it affects anyone running on a large MCC or running GA4 reports across many properties.
The bug
This is upstream bug modelcontextprotocol/python-sdk#2416, a cancellation race in mcp 1.27.0 that fires whenever the MCP host cancels an in-flight request while the tool handler is still running:
- Host sends
notifications/cancelled— usually because its internal per-tool timeout fired partway through a long Google Ads / GA4 call. - The SDK's receive loop calls
responder.cancel(), which synchronously setsself._completed = Trueand sends an error JSON-RPC response. - The tool handler — which is running synchronous Python (the Google Ads and GA4 clients are sync) — finishes normally a moment later and calls
await message.respond(response). respond()hitsassert not self._completedinmcp/shared/session.py:129→AssertionError: Request already responded to.- The assertion escapes the anyio
TaskGroup, tears down the stdio transport, and the entire MCP server process exits. - The host auto-respawns it, but from the user's perspective every subsequent tool call "just disconnected".
Large agency MCCs (100+ accounts) and full GA4 property sweeps make this dramatically more likely because those tools run long enough for the host's timeout to fire.
The fix
AdLoop 0.6.3 monkey-patches mcp.shared.session.RequestResponder.respond and .cancel at server startup with guard-based versions that cannot double-send a JSON-RPC response. Both paths check self._completed synchronously before any await, so exactly one side sends exactly one response and the loser silently returns. No more assertion, no more TaskGroup crash, no more silent process death.
The patch is self-removing: at startup it inspects the installed mcp source and skips patching if the racey assert not self._completed line is no longer present. Once upstream #2416 lands, upgrading the dependency is enough to deactivate the workaround — no AdLoop code changes needed.
Implemented in the new src/adloop/_mcp_patches.py module, installed from server.py alongside diagnostics.install().
Also in this release
- Lightweight
health_check— now a single-row probe (SELECT customer.id … LIMIT 1) instead of a fullcustomer_clientenumeration. On a 166-account MCC the old path took multiple seconds and returned a large payload for no diagnostic benefit; the new probe is sub-second regardless of account count. list_accounts(limit=50)— accepts alimitparameter with a default of 50, and returns a truncation note + hint when there are more accounts available. Callers that actually need the full list can raise the limit explicitly; most workflows can passcustomer_iddirectly to other tools without enumerating accounts at all.- Opt-in diagnostic instrumentation — set
ADLOOP_DEBUG=1and AdLoop emits structured[adloop-debug] event=…lines to stderr covering process start, tool call start/end, periodic heartbeats, signal receipt (SIGTERM/SIGHUP/SIGINT/SIGPIPE), andatexit. Designed specifically to distinguish graceful exits from signal-driven kills when investigating host-side disconnects. Was the instrumentation that pinned down this bug. - Roadmap — added "Claude Desktop one-click install" (
adloop install claude-desktopand/or a.dxtextension) so Cowork and Desktop users won't have to hand-editclaude_desktop_config.json.
Action for maintainers
src/adloop/_mcp_patches.py is a temporary workaround for python-sdk#2416. Watch the upstream issue. When it closes and a fixed mcp release is pinned, delete src/adloop/_mcp_patches.py and its import/call in src/adloop/server.py. The patch already self-disarms via source inspection, so leaving it in place after an upstream fix is safe but redundant.
Install / upgrade
pipx upgrade adloop # or
pip install --upgrade adloop
# or run on-demand:
uvx adloop@0.6.3Then restart your MCP host (Claude Desktop, Cursor, Claude Code, etc.). Tools that previously disconnected mid-call on large MCCs — list_accounts, get_campaign_performance, run_gaql, GA4 reporting across many properties — should now stay connected.
Credits
Bug report and initial reproduction from a Cowork user running a 166-account MCC and 141-property GA4 setup. The detail in that report (exact tool-call sequence + "healthy, then offline" signature) was what pointed at a cancellation race rather than a payload-size issue.
Full Changelog: v0.6.2...v0.6.3