Skip to content

TaskStop on a backgrounded MCP tool call reports success but the server-side operation keeps running #104714

Open

Description

@gpt18

What happened

The agent ran a long MongoDB aggregation through an MCP server tool (mongodb-mcp-server). After about 120 seconds Claude Code moved the call to the background as a task. The agent then called TaskStop on it, got "Successfully stopped task", and told the user the query had been stopped.

The query had not stopped. The server-side operation kept running for about 2 hours on a production database replica, and ended only when it completed on its own.

Expected

One of these:

  • TaskStop on a backgrounded MCP call cancels the underlying request (for example an MCP notifications/cancelled the server can act on), or
  • the result of TaskStop says clearly that only the client-side wait was stopped and the server may still be working, so the agent doesn't report it as stopped.

Repro (shape)

  1. Configure an MCP server whose tool can run longer than the background threshold (e.g. a MongoDB aggregate that can't use an index on a large collection).
  2. Let the agent call it; after ~120s it is moved to a background task.
  3. Call TaskStop with that task ID.
  4. Check the server (e.g. db.currentOp()): the operation is still running.

Impact

The agent told the user, with confidence, that a heavy production query was stopped when it wasn't. Nobody knew to kill it manually.

Environment

  • Claude Code 2.1.308, macOS
  • MCP server: mongodb-mcp-server (stdio)

I'm not sure whether Claude Code sends a cancellation notification on TaskStop and the server ignores it, or sends nothing. Filing the server-side half separately with mongodb-mcp-server.

Activity

  1. Ben-Home commented on Oct 28, 2026

    @Ben-Home

    The gap is between the two identities the call has. The JSON-RPC request id was issued when the tool call started. When the call moves to a background task it gets a task id, and TaskStop acts on the task. If nothing maps the task back to the in-flight request id and emits a cancellation on the stream the request was issued on, the server has no way to learn the client stopped waiting. On stdio there is no stream close to act as an implicit signal either, unlike Streamable HTTP where a client disconnect is itself the cancellation. And notifications/cancelled is a notification by design, so a server that ignores it is indistinguishable from a client that never sent one, which is why your last paragraph is the right question to ask and a hard one to answer from outside the process.

    The useful news is that the protocol already has the shape that fixes this, rather than needing a new convention. The Tasks extension (SEP-2663) makes the operation, not the call, the durable identity: a server that negotiated the extension MAY return a CreateTaskResult in lieu of a CallToolResult, carrying taskId, status, ttlMs and pollIntervalMs; the client then polls tasks/get and cancels with tasks/cancel. The extension is explicit that cancellation is cooperative, so the server acknowledges intent without being obligated to stop, which means a client can honestly report the state instead of success.

    For a server author the practical conclusion is that transport cancellation cannot be the only stop path for anything long-running:

    • Expose long operations as a task or the same triple by hand: start returns an operation id, poll reads status, cancel takes the id. The operation id lives in the server and survives a client restart, a task move, and a dropped stream. Cancellation becomes a normal tool call with a result the agent can quote back to the user, instead of a promise that the client stopped listening.
    • Map the client request id to the driver's cancel handle at start and wire the cancelled notification to that handle. Necessary, but the task shape covers the case where the notification never arrives.
    • Never let default success text be the only signal. If TaskStop cannot prove the server stopped, the honest result is "client wait stopped, server state unknown", which is what your second expected behaviour asks for.

    We run a multi-source connector server (corpusiq.io) where queries against ad platforms and warehouses can exceed any reasonable call timeout, and this is the only shape that held up: the tool returns an operation id immediately, the agent polls, and cancel is an explicit verb. The failure mode you hit is worse than a wasted query, because the agent reported success, so wrong information reached a human who then stopped watching.

    For the report you already have the strongest evidence available: db.currentOp() output timestamped before and after TaskStop, next to the "Successfully stopped task" result. That pair makes it a client bug with a server side, rather than a bug that reads as depending on the MongoDB server's internals.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:mcpbugSomething isn't workingplatform:macosIssue specifically occurs on macOS

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions