Skip to content

SEP-3182: Request Idempotency - #3182

Open
devmaha wants to merge 14 commits into
modelcontextprotocol:mainfrom
abluva:main
Open

SEP-3182: Request Idempotency#3182
devmaha wants to merge 14 commits into
modelcontextprotocol:mainfrom
abluva:main

Conversation

@devmaha

@devmaha devmaha commented Aug 1, 2026

Copy link
Copy Markdown

Summary

Adds an optional idempotencyKey field to tools/call, letting a client mark a request as a possible retry of one the server may already have processed, and a server return the original result instead of re-executing a side effect. Introduces a corresponding tools.idempotency capability for negotiation.

Motivation and Context

MCP has no way for a server to distinguish "this request never arrived" from "this request arrived, executed, and the response was lost" — so a client retrying after a dropped connection or timeout has no safe way to do so for a side-effecting tool call. This is an acknowledged gap, not a new observation: SEP-1686 (Tasks) explicitly deferred a general idempotency mechanism to "a dedicated proposal" rather than solving it narrowly for Tasks. In the absence of one, independent implementations are already reinventing incompatible ad hoc conventions to work around it (cited in the Motivation section of the SEP). This proposal is that dedicated mechanism, scoped deliberately narrowly: one optional field, one new capability, explicit conflict semantics for the hardest edge case (a reused key with different arguments).

How Has This Been Tested?

A reference implementation (linked below) demonstrates the mechanism end to end against a real MCP client/server exchange over stdio:

  • Unguarded retry (today's behavior): a lost-response retry double-executes a side effect.
  • Guarded retry with idempotencyKey: the same retry is deduplicated and returns the original result without re-executing.
  • Conflict case: the same key reused with different arguments is rejected outright rather than replayed or silently executed.

Reference implementation: abluva/mcp-request-idempotency-reference

Breaking Changes

None. The field and capability are both opt-in and additive — servers and clients that don't implement this proposal are unaffected, and existing tools/call requests without idempotencyKey behave exactly as they do today.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • [x ] I have read the MCP Documentation
  • My code follows the repository's style guidelines — SEP follows the standard template (Preamble/Abstract/Motivation/Specification/Rationale/Backward Compatibility/Reference Implementation/Security Implications)
  • New and existing tests pass locally — reference implementation's three scenarios verified passing
  • I have added appropriate error handling — explicit rejection behavior specified for the key-reuse-with-different-arguments conflict case
  • I have added or updated documentation as needed — companion FAQ included addressing anticipated questions and open tradeoffs

Additional context

A companion FAQ, included in this PR, addresses anticipated objections (why not _meta, why not an HTTP header, why tools/call only) and is explicit about genuine open tradeoffs not yet resolved (capability granularity, interaction with cancellation and batch requests).

devmaha added 7 commits August 1, 2026 10:08
Add Initial SEP Draft for - Request Idempotency
- seps/0000-request-idempotency.md -> seps/3182-request-idempotency.md

- seps/0000-request-idempotency-faq.md -> seps/3182-request-idempotency-faq.md

- Update SEP-0000 header and PR field to SEP-3182
- Capability negotiation now via server/discover, not initialize (removed by SEP-2575)
- Add "Relationship to the 2026-07-28 stateless core and MRTR" section
- Fix outdated initialize reference in Scope section
@devmaha devmaha changed the title Request Idempotency SEP 3182 - Request Idempotency Aug 1, 2026
@devmaha devmaha changed the title SEP 3182 - Request Idempotency SEP-3182: Request Idempotency Aug 1, 2026
@Silentpartnercoding

Copy link
Copy Markdown

The new stateless/MRTR section draws a useful distinction between continuing an MRTR exchange and retrying a request. I think one lost-response case still falls between those definitions.

SEP-2322 makes the initial and continuation tools/call requests independent. That suggests each round can carry its own key: K1 for the call that returns input_required, then K2 for the continuation that can produce the side effect. If the K2 response is lost, retrying that same continuation is exactly the ambiguity this SEP is meant to solve. But the current equivalence rule compares only tool name and arguments; it ignores the continuation's inputResponses and requestState, even though those values can change the operation's outcome.

A compact conformance sequence would be:

  • K1 returns input_required;
  • K2 plus the echoed state and responses completes once;
  • an identical retry with K2 replays the final result;
  • changed state or responses with K2 conflicts before dispatch.

That does not reuse one key across the MRTR exchange; it scopes a key to each independent round. I would either include inputResponses and requestState in equivalence when present, or explicitly exclude MRTR-bearing calls from this SEP.

The linked reference implementation has a separate wire-level mismatch: client_demo.py passes idempotencyKey inside the tool's arguments, server.py declares it as a charge_guarded parameter, and that tool performs the deduplication. It never sends params.idempotencyKey, discovers tools.idempotency, or handles duplicate/conflict cases before generic tool dispatch. It is a useful application-level failure-mode demo, but it does not yet demonstrate the proposed protocol mechanism. A protocol fixture could make that boundary observable by checking the request field and discovery capability, then proving a duplicate or conflict never reaches the tool handler.

@devmaha

devmaha commented Aug 2, 2026

Copy link
Copy Markdown
Author

Thanks for the detailed review, both observations are helpful.

On MRTR, I think you've identified a genuine ambiguity rather than a disagreement. My intent was exactly what you describe: the initial request and each continuation are independent tools/calls, so they would naturally use different idempotency keys rather than one key spanning the entire exchange. The current FAQ was only trying to discourage reusing a single key across the whole MRTR flow.

The gap, as you point out, is that if this SEP is taken to apply to MRTR continuation requests, the current equivalence rule doesn't account for requestState or inputResponses, even though they influence the logical operation. I think there are two coherent approaches: explicitly extend equivalence to include those fields, or explicitly exclude MRTR-bearing tools/calls from this SEP. I'm currently leaning toward the latter, since one of the goals of this proposal has been to stay narrowly scoped rather than partially specifying MRTR semantics without implementation experience. I'll revisit that section and the accompanying FAQ to make the intended scope clearer. If you think the initial proposal should cover MRTR continuations as well, I'd be interested in hearing the reasoning or implementation experience behind that view.

On the reference implementation, that's also a fair observation. The current prototype was written to demonstrate the behavioral semantics—the duplicate-execution failure mode and the proposed deduplication/conflict behavior—rather than the final protocol integration. It predates the draft settling on a dedicated params.idempotencyKey field and capability negotiation, which is why the key is currently passed through as a tool parameter. I'll update the prototype so it aligns with the current draft, with deduplication happening before tool dispatch, using params.idempotencyKey, advertising the capability, and ensuring duplicate/conflicting requests are intercepted before reaching the tool handler.

Thanks again for taking the time to dig into the details—this is exactly the kind of review that helps tighten both the specification and the prototype.

devmaha added 2 commits August 2, 2026 22:33
Matches the corresponding SEP edit: MRTR continuation requests
are out of scope for this SEP rather than left ambiguous. No
other FAQ answers affected.
Reviewer noted that if this SEP's equivalence rule (tool name +
arguments) were applied to an MRTR continuation, it would ignore
inputResponses/requestState even though those can change the
operation's outcome. Rather than partially extend equivalence
without implementation experience, the SEP now states the exclusion
explicitly. No other section changes; equivalence, retention,
capability, and conflict semantics are unaffected.
@devmaha

devmaha commented Aug 2, 2026

Copy link
Copy Markdown
Author

Pushed the fixes for both points:

MRTR: explicitly scoped MRTR continuation requests out of this SEP, rather than leaving it ambiguous whether the existing equivalence rule applied to them. inputResponses/requestState aren't accounted for in equivalence today, and I'd rather say so directly than partially extend the mechanism without implementation experience behind it. Happy to revisit if there's a concrete case for covering it now.
Reference implementation: reworked so idempotencyKey travels as params.idempotencyKey (sibling of arguments, matching the SEP's request format) and deduplication happens in server-side dispatch before charge_guarded is ever invoked — the tool function itself now has no idempotency logic at all. Verified end-to-end, including a genuine concurrent-duplicate case (two overlapping requests for the same key — exactly one succeeds, the other is rejected as in-progress) and retention-window expiry. Also tightened the SDK version-pin note: on mcp==2.0.0, idempotencyKey isn't just an import-away — it's silently dropped at construction with no error, so the demo would appear to work while deduplicating nothing. The pin and the note both now reflect that explicitly.

Thanks again for the close read — both were real gaps, not just wording.

@devmaha

devmaha commented Aug 5, 2026

Copy link
Copy Markdown
Author

Happy to change the proposal to use a reserved _meta key instead if reviewers feel that's a better fit for MCP's extension model. My preference is still a dedicated field because the guarantee this SEP is trying to standardize depends on the value surviving end-to-end, and today multiple SDKs/frameworks have been observed dropping _meta (as discussed in the SEP/FAQ). If the WG believes _meta is the right long-term home and is comfortable making preservation a normative requirement, I'm happy to update the proposal accordingly.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants