Repository navigation
MCP tools/call cancellation does not terminate Neo4j transaction #829
Description
Summary
When an MCP client cancels a tools/call in flight, the underlying Neo4j transaction keeps running until it completes or is manually terminated at the Neo4j layer. There is no cancel/reset surface exposed on the CGC side, which means any hung query becomes operator-visible CPU load until someone opens cypher-shell and runs TERMINATE TRANSACTION.
Evidence
Observed behaviour
- An MCP client (Claude Code) issued
get_repository_stats, then cancelled the call after it had not returned in a reasonable time. - The Neo4j transaction dispatched by that call continued running for 21+ minutes after cancellation, pegging a CPU core at ~102% on the host machine.
- The only way to stop it was
docker exec neo4j-cgc cypher-shell ... "TERMINATE TRANSACTION '<id>'"after locating the id viaSHOW TRANSACTIONS.
Likely root cause
src/codegraphcontext/server.py dispatches blocking Neo4j calls like this (paraphrased from line ~361):
result = await asyncio.to_thread(lambda: session.run(query, **params).data())asyncio.to_thread makes the call awaitable, but once the blocking work is handed to the worker thread it is uncancellable from the asyncio side. Cancelling the surrounding task simply stops the coroutine from receiving the result; the Neo4j session.run continues to completion.
Impact
- Any runaway query hangs the MCP connection indefinitely from the user's point of view.
- The host CPU stays pegged on the Neo4j side until an operator manually terminates the transaction.
- There is no CGC-side surface (tool, admin endpoint, config toggle) that can stop an in-flight query. The escape hatch is always "open cypher-shell, TERMINATE TRANSACTION".
Suggested direction
Transactions started in response to tools/call should be associated with a cancellation token derived from the MCP request. On cancellation, CGC should call the Neo4j dbms.killTransaction / TERMINATE TRANSACTION equivalent for that transaction id. At minimum, per-tool query timeouts (configurable, with a sane default) would prevent unbounded runaway calls.
Environment
- CGC version: 0.2.3 (pip)
- Neo4j: running in Docker container (
neo4j-cgc) - Host: Windows 11, Python 3.14
- MCP client: Claude Code
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsBacklog tasks
- StatusShow more project fieldsWebDevs
Activity
github-actions commentedon Apr 17, 2026
Hi! 👋 Join our CodeGraphContext Discord channel to collaborate: https://discord.gg/dR4QY32uYQ
Shashankss1205 commentedon Aug 2, 2026
Linking #1228, and noting that it will not close this one — worth being explicit so the two aren't conflated.
tools/callcancellation doesn't terminate the running database query. Confirmed atserver.py:734:result = await asyncio.to_thread(handler, **args). Work handed toto_threadis not cancellable, so the query runs to completion regardless. Fixing it needs a server-side query timeout or an explicitTERMINATE TRANSACTION.cancel_job()toJobManager— background indexing-job bookkeeping.core/jobs.py:21definesJobStatus.CANCELLEDand nothing ever sets it.Different subsystems; implementing #1228 leaves the 21 minutes of pegged CPU you measured completely unaddressed.
A third angle on the same general gap: #1536 —
switch_contexttears down the DB driver while an indexing job is still running, because nothing tracks or waits on in-flight work.