Skip to content

Lost-response retry on a mutating tools/call re-executes external effects — reproduction evidence and a spec-clarification question #3394

Description

@arjun2075

Problem-question

On the tested MCP SDK path, a mutating tools/call commits its external
effect and then loses its response; the client perceives a timeout. When the
application retries with a fresh JSON-RPC request ID, the tool executes again
and a second external effect occurs. MCP 2025-11-25 defines no normative
retry or duplicate-effect semantics for tools/call, so this is
spec-permitted, implementation-specific behavior — not a protocol defect.

Question for maintainers: "When a mutating tools/call may have completed
but its response is lost, should the MCP specification explicitly describe
the call outcome as ambiguous and warn clients that retrying can re-execute
external effects unless the tool/application provides its own stable
deduplication mechanism?"

Minimal reproduction

Mutating tool, one ledger row per execution (baseline tool is
intentionally non-idempotent — no dedup, so the ledger authoritatively
counts executions): (1) deterministic fault hook drops the attempt-1
tools/call response after the handler returned (effect committed first);
(2) client perceives a timeout via read_timeout_seconds (MCPError) — the
SDK does not automatically retry, so the retry is application policy:
exactly one retry, identical arguments, fresh JSON-RPC id, as the spec
requires (request IDs "MUST NOT have been previously used by the requestor
within the same session"); (3) count ledger rows.

Observed behavior

Attempt 1 (jsonrpc_id=2): effect committed, response dropped; client sends
notifications/cancelled for the timed-out id. Attempt 2 (jsonrpc_id=3,
application retry): tool re-executes, second effect committed. Ledger:
2 external effects for 1 logical operation, deterministic (20/20 on
clean checkouts).

Positive control

Same sequence with a stable logical-operation key and dedup at the
application effect layer: attempt 2 returns deduped, 1 external effect
recorded. Application-level idempotency successfully mitigated the
reproduced behavior. MCP did not dedup; the application effect layer did —
one mitigation, not a prescribed solution.

Specification context

  • server/tools (2025-11-25): tools/call execution semantics
    (execute-once, dedup, effect guarantees), retry after a timeout, and
    duplicate detection are all UNDEFINED — the section is normative about
    wire shapes, not about what a completed tool call did.
  • idempotentHint (since 2025-03-26, PR ToolAnnotations #185) is NON-NORMATIVE: a tool
    description, not a retry protocol; no RFC 2119 obligation either side.
  • basic/utilities/cancellation covers races only (MUST handle gracefully;
    SHOULD stop processing; MAY ignore already-completed) — not
    retry-after-loss.
  • The spec's fresh-ID requirement actively prevents ID reuse by which a
    server might otherwise correlate a retry — the retry is structurally
    unrecognizable to the server as a duplicate.

Why this matters

The tested MCP SDK path permits a retrying application to execute a
mutating tool more than once after an ambiguous lost-response outcome.
tools/call currently does not define normative retry or duplicate-effect
semantics for this case. Applications can mitigate this using stable
idempotency at the effect boundary. The experiment motivates clarifying the
expected responsibility split between client, server, tool, and downstream
effect provider, so independent implementations converge on documented
expectations rather than ad-hoc retry conventions. Related prior art
(reference only, not revival): PR #3182 (closed 2026-08-23 on policy
grounds, not merits) proposed a normative idempotencyKey; this issue asks
only whether the spec should document the responsibility boundary.

Possible documentation-conformance options

Which vehicle would maintainers prefer, if any?

  1. Non-normative spec guidance (e.g. on server/tools): the call
    outcome after a lost response is ambiguous; retrying MAY re-execute
    external effects; stable deduplication lives outside the protocol.
    Candidate text: "The protocol defines no idempotency,
    duplicate-detection, or exactly-once semantics for tools/call. A
    client that retries a tool call after a timeout or lost response MAY
    cause the tool to execute more than once; the server is not required to
    recognize a retry as a duplicate. Implementations and application layers
    that require bounded effect multiplicity should coordinate retry
    identity and duplicate suppression outside the protocol (e.g.,
    application-level idempotency keys or durable result lookup)."
  2. Tool-author guidance on declaring and handling retry-relevant
    properties.
  3. Client retry guidance for effectful calls (caution before retrying a
    timed-out mutating call).
  4. A conformance vector: lost response after effect commit → retry with
    new request ID → count effects; positive control with a stable
    application-level key.
  5. Future normative retry semantics — explicitly out of scope here.

Environment

mcp Python SDK 2.2.0 / mcp-types 2.2.0, real ClientSession →
MCPServer over memory streams, negotiated protocolVersion 2025-11-25,
Python 3.12.3.

Reproduction link-commands

Self-contained scripts (local, not posted), ~7 s per run:

cd ~/workspace/agent-continuity-conformance
.venv/bin/python upstream/mcp-b1/reproduction/b1_lost_response_repro.py
.venv/bin/python upstream/mcp-b1/reproduction/b1_positive_control.py

Baseline prints authoritative effect count: 2; the control prints 1
(see evidence-summary.md for wire excerpts and frozen evidence hashes).

Limitations

  • In-memory transport only; single deterministic fault (response dropped
    after commit); other fault shapes not tested.
  • The retry is scripted application policy, not an SDK retry policy — the
    SDK performs no automatic retry.
  • No protocol defect is claimed; generalization is limited to the tested
    path.

This is implementation evidence plus a spec-clarification question. No
normative change is proposed; no option above is prescribed.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions