Skip to content

v0.6.3

Choose a tag to compare

@github-actions github-actions released this 24 Apr 09:16
· 19 commits to main since this release

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:

  1. Host sends notifications/cancelled — usually because its internal per-tool timeout fired partway through a long Google Ads / GA4 call.
  2. The SDK's receive loop calls responder.cancel(), which synchronously sets self._completed = True and sends an error JSON-RPC response.
  3. 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).
  4. respond() hits assert not self._completed in mcp/shared/session.py:129AssertionError: Request already responded to.
  5. The assertion escapes the anyio TaskGroup, tears down the stdio transport, and the entire MCP server process exits.
  6. 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 full customer_client enumeration. 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 a limit parameter 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 pass customer_id directly to other tools without enumerating accounts at all.
  • Opt-in diagnostic instrumentation — set ADLOOP_DEBUG=1 and AdLoop emits structured [adloop-debug] event=… lines to stderr covering process start, tool call start/end, periodic heartbeats, signal receipt (SIGTERM/SIGHUP/SIGINT/SIGPIPE), and atexit. 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-desktop and/or a .dxt extension) so Cowork and Desktop users won't have to hand-edit claude_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.3

Then 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