From cd29e6f5c093655a0ff6072282f36e0c867f90a0 Mon Sep 17 00:00:00 2001 From: devmaha <4321989+devmaha@users.noreply.github.com> Date: Sat, 1 Aug 2026 10:08:56 +0530 Subject: [PATCH 1/9] Add files via upload Add Initial SEP Draft for - Request Idempotency --- seps/sep-0000-request-idempotency.md | 205 +++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 seps/sep-0000-request-idempotency.md diff --git a/seps/sep-0000-request-idempotency.md b/seps/sep-0000-request-idempotency.md new file mode 100644 index 000000000..07f6e278a --- /dev/null +++ b/seps/sep-0000-request-idempotency.md @@ -0,0 +1,205 @@ +# SEP-0000: Request Idempotency + +## Preamble + +- **Title:** Request Idempotency +- **Author:** Amit Gautam (amit.gautam@abluva.com) +- **Status:** proposal +- **Type:** Standards Track +- **Created:** 2026-08-01 +- **PR:** _(full PR URL, filled in once the PR is opened)_ + +## Abstract + +MCP has no mechanism for a client to indicate that a request is a retry of one the server may already have processed, and no mechanism for a server to detect and deduplicate such a retry. When a response is lost after a request has already executed — a network timeout, a dropped connection, a proxy failure — the client cannot distinguish "never arrived" from "arrived and executed, but the response was lost," and has no standard way to retry safely. This SEP proposes an optional `idempotencyKey` field on `tools/call`, allowing a client to mark a request as a possible duplicate of a prior one and a server to return the original result rather than re-execute a side effect. The proposal is deliberately narrow: it does not claim protocol-wide scope on day one, specifies required behavior rather than storage implementation, and states an explicit answer for the hardest edge case — a reused key presented with different arguments. + +### How to read this proposal + +This document is organized so a reviewer can stop after any section and already have a complete picture at that level of detail — nothing later contradicts or narrows what came before. + +- **For the core proposal in one pass:** Abstract, then Specification. That's the whole normative mechanism — the field, the capability, the three request outcomes, equivalence, retention, scope. Everything needed to implement or object to the design is there. +- **For why this is needed at all:** Motivation. This is deliberately evidence-heavy (working-group history, independent implementations, industry precedent) rather than a short pitch, because "is this actually a protocol problem" is the question most likely to end a proposal early, and it deserves to be answered with sources rather than assertion. Skippable if you're only evaluating the mechanism itself, not whether it should exist. +- **For why specific design choices were made, and what alternatives were rejected:** Rationale. Each subsection there answers one specific "why not X instead" question independently — read only the ones relevant to whatever design choice you want to push on; they don't depend on each other. +- **For edge cases, limits, and honest tradeoffs:** the companion FAQ document. Concurrency, crash behavior, tool versioning, capability granularity, and several open questions this SEP does not claim to have resolved are addressed there in more depth than fits naturally in the SEP body, organized by how settled each answer is. +- **For legal/process boilerplate:** Backward Compatibility and Reference Implementation are short and can be skimmed; Security Implications is short but not boilerplate and is worth reading in full. + +## Motivation + +The Model Context Protocol currently offers no way to answer a question every non-trivial tool server eventually faces: "is this call a retry of one I already executed, or a new request?" A `tools/call` that mutates state (charges a payment, merges a branch, sends a notification) carries no field that would let the server recognize a retry and return the original outcome instead of executing the side effect again. + +This is not a hypothetical concern. It is an acknowledged gap in the protocol's own design history. SEP-1686 (Tasks), in describing why it does not let a requestor specify a task's identifier, states directly: + +> "If the response is lost (network failure, timeout), the requestor cannot deterministically retry without potentially creating duplicate tasks. However, several mitigations exist: Requestors can use `tasks/list` to discover tasks they may have created. Receivers can implement deduplication based on request parameters if needed. Transport-level reliability mechanisms (such as those in Streamable HTTP) can reduce the likelihood of lost responses. Importantly, the need for idempotency is not unique to tasks — it applies to all MCP messages. Rather than solving idempotency specifically for task creation, it was agreed that a dedicated proposal should introduce a general mechanism for message idempotency across the protocol." + +Three things are worth drawing out from this passage. First, the working group explicitly named this a protocol-scope concern, not a Tasks-scope one — this SEP is that dedicated proposal, not a new argument for why one should exist. Second, the mitigations named as adequate for Tasks are transport- and convention-level workarounds ("if needed," "can reduce the likelihood of") rather than a protocol guarantee, and they do not close the gap for an ordinary `tools/call` that never uses Tasks at all. Third, the transport-layer proposals cited elsewhere in that same discussion (SEP-1335, SEP-1442, SEP-1597) address connection resilience and stream management — whether a dropped connection can be resumed — which is a different problem from whether a redelivered or re-attempted logical request executes its side effect exactly once. A resumed connection and a request retried after a lost response can both need this mechanism, but neither the transport-resilience SEPs nor Tasks provide it directly for plain tool calls. + +The gap has since been partially, narrowly acknowledged a second time. SEP-2663 (Tasks Extension) redesigns the Tasks polling lifecycle specifically to prevent a retried `tasks/get` from re-triggering a downstream side effect, by splitting the method into a pure read (`tasks/get`) and a separate write (`tasks/update`). That fix is real but scoped entirely to the Tasks extension's own two-method surface; it says nothing about a bare `tools/call`, which is the case most MCP interactions actually involve and the case SEP-1686's own rationale named as in scope. + +Independent evidence outside the working group's own discussion shows implementers are not waiting for a protocol answer — they are building ad hoc, incompatible ones today. A paper on production MCP deployment patterns (arXiv:2603.13417) instructs server builders to accept a client-supplied `idempotency_key` argument and cache key-to-response mappings themselves, choosing their own storage backend and expiry window. A separate technical write-up ("Six Fatal Flaws of the Model Context Protocol") independently lists idempotency keys as something a well-built MCP server needs but the protocol does not provide. Neither source agrees with the other on field name, storage approach, or retention policy — which is the signal that this is a live, current interoperability problem rather than a plausible future one. Even within the Tasks discussion itself, in a review comment responding to a suggestion that task identifiers should never be client-specified, the Tasks proposal's own author noted, in passing, that a requester specifying its own identifier "is how idempotency tokens work," using the concept as an already-understood point of reference. More directly still: a separate reviewer on that same thread, CaitieM20 (Microsoft), responding to the specific trade-off Tasks makes between client-generated and server-generated identifiers, wrote plainly that idempotency "is important, but this design is coupling Idempotency Keys to the Task State Updates provided by TaskId... I'd recommend we add Idempotency Keys as a metadata item in a future SEP that clients can choose to use and servers can choose to support. This is a much cleaner separation of concerns." That is an independent, named, on-the-record request for the exact mechanism this SEP proposes, made during review of a different SEP, months before this proposal existed — not a passing turn of phrase, but a direct ask that a general idempotency mechanism be specified separately from Tasks, which is precisely this SEP's scope. + +That vocabulary has substantial precedent outside MCP entirely. Stripe's `Idempotency-Key` header is the best-known instance, and it is not an isolated convention — an IETF draft (`draft-ietf-httpapi-idempotency-key-header`) documents the same header adopted independently by Adyen, Dwolla, WorldPay, and Yandex, among others. Amazon's own engineering literature (the Builders' Library entry "Making Retries Safe with Idempotent APIs") describes the identical motivating scenario behind AWS's `ClientToken` convention: a client that received no response, because of a timeout rather than a failure, needs a safe way to retry without risking a duplicate side effect. This SEP proposes the same shape of mechanism for MCP, not because MCP should imitate any one of these systems specifically, but because the same problem recurring independently across payments APIs, cloud infrastructure APIs, and now MCP server implementations is evidence the underlying gap is real and that a per-implementation answer will keep being reinvented, incompatibly, until the protocol supplies one. + +## Specification + +### Overview + +This SEP adds one optional field, `idempotencyKey`, to the parameters of a `tools/call` request. A server that supports this feature MUST treat two `tools/call` requests bearing the same `idempotencyKey` as the same logical operation, subject to the matching rules below, and MUST NOT execute the underlying tool a second time for a request it recognizes as a duplicate. This normative requirement applies once an implementation has successfully recorded a request for deduplication; it is not a guarantee of exactly-once execution across an implementation's own failure between executing a side effect and persisting the corresponding record — see "Implementation ordering is intentionally unspecified," below, for that limit stated explicitly. + +### Capability declaration + +This SEP introduces the `tools.idempotency` capability, declared during initialization, rather than treating `idempotencyKey` as a bare optional field with no negotiation: + +```json +{ + "capabilities": { + "tools": { + "idempotency": {} + } + } +} +``` + +This capability intentionally contains no structured metadata in this proposal — its role is to declare that the mechanism is supported at all, not to advertise parameters like retention duration (see "Retention," below, for why that is left to server-level documentation rather than capability metadata in this SEP). + +An explicit capability was chosen over silent acceptance of an unrecognized parameter because the two give a client meaningfully different guarantees. If a client sent `idempotencyKey` to any server and simply hoped for the best, it would have no way to distinguish "the server ignored this and executed my request twice" from "the server honored it and deduplicated correctly" — exactly the false sense of protection this SEP exists to prevent, not merely repeat in a new form. Declaring the capability lets a client know, before it depends on the guarantee, whether a given server's deduplication behavior is normative rather than accidental or absent. This is consistent with how MCP already uses capability negotiation elsewhere: capabilities distinguish protocol-level guarantees a party can rely on (whether a server supports Tasks, elicitation, or sampling) from ordinary request parameters a party is free to send or ignore. Idempotent retry handling is a protocol guarantee in the same sense — it changes what a client can assume about the server's behavior, not merely what data the request carries — which makes capability negotiation the existing mechanism this SEP should use, not a new pattern invented for this proposal alone. A client that requires deduplication for correctness SHOULD verify the server has advertised the capability before relying on it; sending the field to a server that has not advertised support provides no interoperability guarantee, even though the field itself is harmless to send. + +Clients MAY send `idempotencyKey` regardless of whether the server has declared this capability. A server that has not implemented this SEP will not recognize the field as meaningful and will handle the request according to its own existing parameter-validation rules — permissively ignoring the unrecognized field, or, for a stricter server, potentially rejecting the request outright (see Backward Compatibility). + +### Request format + +```json +{ + "method": "tools/call", + "params": { + "name": "charge_payment", + "arguments": { "amount": 4200, "currency": "usd" }, + "idempotencyKey": "b3b6c1e4-6b8b-4f1a-9c1e-2a6e6b1d9f2a" + } +} +``` + +`idempotencyKey` is an opaque string, client-generated. This SEP does not mandate a specific format (a UUID is a reasonable default, but is not required), and it does not mandate that the key be globally unique — only that a server treat repetition of the same key, within its retention window, as a signal to deduplicate rather than re-execute. + +### Server behavior on a repeated key + +When a server that declares this capability receives a `tools/call` whose `idempotencyKey` matches one it has already processed, is currently processing, or has retained: + +1. **If a request bearing the same key is still executing** — the original call has not yet produced a result to record — the server MUST NOT begin a second execution of the tool for the duplicate request, and MUST NOT block the duplicate request indefinitely waiting for the original to finish. Instead, the server SHOULD reject the duplicate immediately with an error indicating a request with this key is already in progress, allowing the client to retry after a short delay rather than either duplicating execution or hanging. As with the conflict case below, servers SHOULD populate `error.data` with a structured value identifying this specific failure mode (for example `{"type": "idempotency_key_in_progress", "idempotencyKey": ""}`), so a generic client can distinguish it from other errors programmatically. Rejection is specified rather than left as one option among several — for instance, attaching the duplicate to the original execution's eventual result — because leaving the choice open would mean two conforming servers could behave differently for the identical wire-level situation, which is exactly the kind of interoperability gap this SEP exists to close, not reproduce at a smaller scale. This is also the same handling Stripe uses for the identical case directly: Stripe returns a 409 response stating a request using the same idempotency key is already in progress, rather than queuing, sharing the in-flight result, or executing a second time — corroborating that rejection is a workable, precedented choice, though the reason for specifying it here is the interoperability argument above, not the precedent by itself. A future revision of this mechanism could specify attaching to the in-flight execution's eventual result instead, if evidence emerges that rejection is insufficient in practice; this SEP takes the simpler, already-precedented approach for now. +2. **If the new request's arguments are equivalent to the arguments of a completed original call** (see "Request equivalence," below), the server MUST NOT execute the tool again. It MUST return the previously produced result — whether that result was a successful response or an error response — unless that result has since expired from the server's retention window (see "Retention," below), in which case the server MAY treat the request as new. Replaying the original error rather than re-attempting execution is deliberate: a client retrying after a lost response has no way to know whether the original call succeeded or failed, and re-executing on the assumption that the original must have failed reintroduces the exact duplicate-execution risk this SEP exists to prevent. +3. **If the new request's arguments differ from the arguments of the original call**, the server MUST reject the request with an error indicating the key was previously used with different arguments. The server MUST NOT execute the tool with the new arguments, and MUST NOT silently return the cached result from the original, differently-parameterized call. This SEP does not define a new JSON-RPC error code for this case, since a generic client should not need to distinguish idempotency conflicts by error code from every other application-specific failure a server might raise. Instead, servers SHOULD populate the standard JSON-RPC `error.data` field — an existing, application-defined extension point, not a new mechanism this SEP introduces — with a structured value identifying the failure as an idempotency key conflict, for example `{"type": "idempotency_key_conflict", "idempotencyKey": ""}`. This lets a generic client detect the conflict programmatically, without parsing the human-readable `error.message` string, while still requiring no new reserved error code and no change to the JSON-RPC error envelope itself. + +Reusing an idempotency key with different request parameters is treated as a client error, not as a hint to guess which behavior the caller intended. + +### Request equivalence + +Two requests bearing the same `idempotencyKey` are equivalent if they specify the same tool name and equivalent arguments as parsed JSON values. This comparison operates on the parsed argument values, not their serialized representation: `{"a": 1, "b": 2}` and `{"b": 2, "a": 1}` MUST be treated as equivalent despite differing key order, and servers MUST NOT treat two requests as different solely because of differences in whitespace, key ordering, or other serialization detail that does not change the parsed value. Equivalence is determined solely by tool name and argument values; whether a tool's own implementation has changed between the original call and a retry within the retention window does not affect this comparison. A server that replaces a tool's implementation while a previously-recorded call remains within its retention window will still replay that call's original recorded result, not a result produced by the new implementation — this is a direct consequence of what this SEP defines equivalence over, not a separate rule, and it is a cost of longer retention windows that server operators should weigh explicitly rather than discover by surprise. + +A server MUST have a deterministic method for this comparison and MUST apply it consistently. Servers MAY canonicalize arguments before comparing them (for example, by hashing a canonical serialization), provided the canonicalization is stable and the result is the same as it would be for a direct comparison of the parsed values — a server MUST NOT adopt a canonicalization that treats semantically-equivalent arguments as different, since that would silently narrow the cases in which this SEP's deduplication guarantee applies. This SEP does not mandate a specific canonicalization scheme (RFC 8785 is one reasonable choice among several); it mandates the outcome any conforming scheme must produce. Two ambiguities are resolved explicitly rather than left open, since a mismatch on either would cause otherwise-conforming servers to disagree about whether two retries of the same logical call are, in fact, the same call: + +- **Number representation.** For the purposes of this comparison, numeric values representing the same mathematical value MUST compare equal — `1` and `1.0` MUST be treated as the same value. RFC 8259 establishes the premise this rule builds on (JSON's grammar makes no distinction between integer and real number representations), but the RFC itself defines syntax, not a comparison semantics; the equality rule here is this SEP's own normative requirement, not a claim about what RFC 8259 mandates on its own. +- **String comparison.** Comparison operates on parsed JSON values, not on the serialized JSON text — two differently-escaped encodings of the same string (for example, a literal character versus its `\uXXXX` escape) MUST be treated as equivalent, since both parse to the same value. This SEP does not require Unicode normalization (e.g., NFC/NFD) beyond what parsing already resolves; two strings that are distinct after parsing remain distinct for this comparison. +- **Duplicate object keys.** RFC 8259 permits parsers to differ on how a JSON object with a repeated key (e.g. `{"x": 1, "x": 2}`) is resolved. This SEP does not introduce a new rule for this case: whatever a server's JSON parser resolves such an object to is the value this comparison operates on, consistent with the general principle above that comparison is defined over parsed values, not raw text. A client that depends on predictable behavior in the presence of duplicate keys already depends on its server's general JSON-parsing behavior outside the scope of this SEP; this proposal does not attempt to standardize JSON parsing itself. To avoid this ambiguity in practice, clients SHOULD NOT send argument objects containing duplicate member names. + +### Retention + +A server MUST retain enough state to answer a repeated key within a bounded window; indefinite retention is not required, and this SEP intentionally leaves the specific duration undefined. Actual retention windows vary enormously across comparable production systems for legitimate reasons — Stripe advertises 24 hours, PayPal's retention is API-specific and can extend to 45 days, Amazon SQS FIFO deduplicates within a 5-minute window — which is itself evidence that the right duration is a deployment decision, not a protocol default this SEP should fix. + +Servers SHOULD document their retention window through server-level documentation (e.g., a README, hosted API reference, or equivalent) accessible to integrators, or through protocol-level metadata if and when MCP defines a mechanism for servers to advertise such metadata generally; this SEP does not itself define a new protocol field for advertising the window, since doing so is a separable concern from the deduplication mechanism this SEP specifies. Advertising retention programmatically — for example, as structured data within the `tools.idempotency` capability itself — may be valuable future work, since it would let a client reason about retry safety at runtime rather than relying on documentation it cannot inspect; this SEP intentionally does not attempt it, to keep this proposal limited to the deduplication mechanism rather than also introducing new capability metadata on the strength of a need this research did not specifically find evidence for. This is a SHOULD rather than a MUST: some MCP servers, particularly small local or embedded ones, have no natural channel for published documentation at all, and requiring one would be a heavier bar than the deduplication guarantee itself needs. A client integrating against a server that has not published a retention window should treat the absence conservatively — that is, assume no particular duration is guaranteed — rather than assume a specific default. A request presented with a key outside the server's retention window MAY be treated as new. + +### Scope + +This SEP proposes `idempotencyKey` on `tools/call` specifically. It intentionally does not define idempotency semantics for other request types. Every concrete piece of evidence motivating this proposal — Tasks' own rationale, the independently-reinvented server-side conventions, the industry precedent cited above — concerns tool execution and other side-effecting operations, not `initialize`, `ping`, `listTools`, `listResources`, or notification messages, for which no evidence of duplicate-execution risk was found during this research. Extending the mechanism to additional request types is left as an explicit avenue for a future, narrower proposal, informed by whichever additional request types demonstrate the same failure mode in practice. This narrower scope is also a closer match to SEP-1036's own precedent (one field added to one existing capability) than a protocol-wide claim would be, even though SEP-1686's own language ("applies to all MCP messages") would technically support the broader scope. + +A `tools/call` response is atomic under the current specification: a tool may emit `notifications/progress` messages while executing, but the call itself still produces a single, final result rather than a streamed one. This SEP's mechanism concerns only that final result — the interaction described in "Server behavior on a repeated key" is unaffected by whether progress notifications occurred during the original execution, since those notifications are not part of what this SEP replays or deduplicates. If a future SEP introduces genuinely streamed or partial `tools/call` results, that SEP would need its own idempotency treatment; this proposal does not attempt to anticipate what that would look like. + +### Implementation ordering is intentionally unspecified + +This SEP does not specify when an implementation records an idempotency key relative to executing the underlying operation — whether a server persists the key before execution, after execution, or as part of a single transaction alongside it. Implementations with transactional storage, external side effects, or distributed execution have different correctness trade-offs in this respect, and those trade-offs are implementation concerns rather than protocol concerns: the observable behavior this SEP defines (a matching retry is deduplicated; a conflicting retry is rejected) is the same regardless of how a given server achieves it internally. A server crashing between executing a side effect and persisting the corresponding record is a real failure mode a production implementation needs to consider, but it is not one this protocol-level specification can resolve on a given implementation's behalf. + +To state the limit explicitly: this SEP specifies the behavior expected of a conforming implementation during normal operation. It does not, and cannot, guarantee exactly-once execution under arbitrary implementation failure. A server that executes a side effect and crashes before persisting the corresponding idempotency record will, if retried, execute that side effect again — the same outcome as if this SEP did not exist. This is not a gap unique to MCP; no protocol-level field can guarantee exactly-once execution across an implementation's own crash boundary, which is why HTTP idempotency-key proposals make the same distinction. What this SEP guarantees is deduplication of retries the server's own bookkeeping actually observed, not resilience to failures inside that bookkeeping itself. + +### Relationship to SEP-2663 (Tasks Extension) + +The `tasks/get` / `tasks/update` split introduced by SEP-2663 and the mechanism in this SEP are complementary, not redundant, and a reviewer should not read one as making the other unnecessary. The Tasks split prevents an accidental *write* from occurring during what is meant to be a *read* — a retried `tasks/get` cannot itself trigger a side effect, because reading is now structurally separate from mutating. The mechanism in this SEP prevents a retried *write* — an ordinary `tools/call` invocation of a side-effecting tool — from executing its side effect twice. An implementation with the Tasks split but no idempotency key can still duplicate a retried write; an implementation with an idempotency key but no read/write separation still risks a write occurring somewhere a read was expected. Both are needed for the different failure modes they each address. + +## Rationale + +Each subsection below answers one specific design question independently; none depends on reading the others first, so a reviewer can jump directly to whichever choice they want to examine. + +### Why replay the original response, including errors, rather than re-attempting execution? + +Idempotency is about preserving the outcome of an operation that already happened, not giving the caller another attempt at a better one. A client retrying after a lost response cannot know whether the original call succeeded, failed, or crashed partway through — only that it doesn't have a response. Re-executing on the assumption that a missing response means the original must have failed reintroduces the exact duplicate-execution risk this SEP exists to prevent, since the original could just as easily have succeeded. This mirrors Stripe's own documented behavior: Stripe states it stores the original result "regardless of whether it succeeds or fails," and replays it on a matching retry "including 500 errors." A related question worth answering directly, since it recurs whenever idempotency is discussed: if the original execution returned a transient internal error, shouldn't a retry get a fresh attempt rather than the same cached error? No — that is an application-level recovery question, not an idempotency question. This SEP defines what happens to a retry of the *same* logical operation; a decision to attempt the operation *again*, under a new logical identity, is exactly what generating a new `idempotencyKey` is for. Recovering from a partial or failed execution remains the caller's responsibility once it observes the replayed outcome, not a behavior this SEP asks servers to attempt on the caller's behalf. + +### Why not `_meta`? + +An idempotency key would necessarily be an implementation-specific `_meta` key rather than one of the small set of reserved `io.modelcontextprotocol/*` names the specification governs, and the current specification defines no interoperability requirement that implementation-specific `_meta` content be preserved end-to-end. This is not only a specification-text concern: three independent client frameworks — Microsoft's Agent Framework, Agno, and OpenAI's Agents SDK — have each shipped code that drops `_meta` from tool results entirely, with the Microsoft issue explicitly describing the behavior as a specification violation. Whichever way each case is best characterized, the combined effect is the same: a deduplication guarantee resting on a field that multiple independent, real-world clients have already dropped is not a guarantee a server can rely on. A dedicated, required field carries a normative weight `_meta` does not. + +### Why not an HTTP header, following Stripe's precedent directly? + +MCP is deliberately transport-agnostic, and stdio — the primary local transport, and the default for Claude Desktop, Claude Code, and most IDE integrations — has no header concept at all; it is newline-delimited JSON-RPC over stdin and stdout. A header-shaped mechanism would need a separate design for every transport MCP supports and would be inapplicable outright wherever no header layer exists. A JSON-RPC parameter, by contrast, is available identically regardless of transport. + +### Why not the JSON-RPC `id` field, which already exists on every request? + +Per the JSON-RPC 2.0 specification, `id` exists "to correlate the context between" a request and its response — it is a transport-exchange correlator, not a deduplication token. Nothing in JSON-RPC requires a client to reuse the same `id` on a retry (implementations commonly mint a fresh one), and nothing requires a server to retain `id`s of requests it has already completed once a response has been sent. Even a retry that happened to reuse the original `id` would have no server-side guarantee of being checked against it. + +### Why not place `idempotencyKey` at the envelope level, given a future extension to other request types might make that the more natural home? + +Placing the field inside `params`, alongside a tool call's other arguments, is a direct consequence of the scope decision above: since this proposal deliberately limits itself to `tools/call`, the field belongs where `tools/call`'s other parameters live, not in the shared JSON-RPC envelope every request type uses. If a future proposal extends this mechanism to additional request types based on demonstrated need, that proposal would need to decide at that time whether to promote the field to the envelope or keep it defined per-method; nothing in this SEP forecloses either choice. Generalizing the field's placement now, on the strength of a hypothetical future extension rather than demonstrated need, would repeat the scope overreach this SEP otherwise avoids. + +### Why this scope, and why not claim the full protocol-wide scope SEP-1686's language supports? + +SEP-1036 (URL Mode Elicitation) succeeded by adding one field to one existing capability, not by generalizing OAuth handling across the entire protocol at once. This SEP follows the same minimal-opening-move shape deliberately: the evidence gathered supports `tools/call` specifically, and claiming broader scope on the strength of narrower evidence would invite an easy scope objection this proposal does not need to take on. This is consistent with MCP's own stated design principle of convergence over choice — favoring one well-specified path over a broader, less-evidenced one — and with standardization over innovation, which favors codifying a pattern already shown to recur over inventing a wider one that has not yet been demonstrated as necessary. + +### Alternatives considered + +- **A structured granted-vs-requested idempotency negotiation, analogous to OAuth incremental scope consent.** Considered and rejected as solving a different problem (authorization) with no clear connection to the deduplication question this SEP addresses. +- **Solving this purely as a convention documented in server best-practice guides, without a protocol field.** This is close to the status quo, and the independent-reinvention evidence above is direct evidence that convention alone has not converged implementations on a shared, interoperable answer; a client cannot know in advance which convention a given server has adopted, if any. +- **A general-purpose envelope-level field on every JSON-RPC message, matching SEP-1686's literal "applies to all MCP messages" framing.** Considered and set aside in favor of the narrower `tools/call`-only scope above, on the grounds that the evidence gathered does not yet support the wider claim, though nothing in this design prevents a future SEP from extending the field to additional request types if the same failure mode is demonstrated for them. + +### Community input + +Community discussion has not yet begun. This section will be updated as review proceeds. + +## Backward Compatibility + +This proposal is additive and introduces no backward incompatibility. `idempotencyKey` is an optional parameter; a server that does not implement this SEP (including every server implementing MCP today) will not recognize the field as meaningful and will behave according to its own existing request-validation rules — a permissive server ignores the unrecognized field, while a strict server may reject the request outright, exactly as either would for any other parameter its schema does not define. This SEP does not change that existing behavior in either direction. A client MAY omit `idempotencyKey` entirely and receive the same behavior as before this proposal. There is no scenario in which adopting this SEP requires an existing client or server to change behavior it currently exhibits; the only behavior change is opt-in, for parties that choose to send and honor the new field. + +## Reference Implementation + +A prototype implementation exists, built against the official MCP Python SDK (`mcp` >= 1.28.1, requiring Python >= 3.10) and run over a genuine stdio client/server session rather than a mock. It demonstrates, as observable behavior: + +1. The duplicate-execution failure mode with no protection (`charge_unguarded`): a retried call after a simulated lost response executes the underlying side effect a second time. +2. The proposed mechanism (`charge_guarded`): an identical retry bearing the same `idempotencyKey` is deduplicated — the side effect executes once, and the cached result from the original call is returned. +3. The conflict case: a key reused with different arguments is rejected explicitly, with no execution and no replay, matching the behavior specified above. +4. Backward compatibility: a call to `charge_guarded` that omits `idempotencyKey` behaves identically to the unguarded tool. + +The prototype is intentionally minimal and does not model persistence across process restarts, distributed deployment across multiple server instances, or crash recovery — see its own README for the complete list of simplifications and why each one was made. It demonstrates protocol behavior, not implementation strategy: it proves the mechanism's observable semantics are achievable without any change to the underlying transport or SDK, not that any particular storage or deployment architecture is production-ready. Per the SEP process, this is offered as a prototype implementation at proposal stage; a reference implementation meeting the "Final" status bar will be completed after acceptance, incorporating whatever refinements emerge from working-group review. + +## Security Implications + +- **Key confidentiality.** An `idempotencyKey` is not a secret and MUST NOT be treated as one; it identifies a request for deduplication purposes only and carries no authorization weight of its own. A server MUST NOT use possession of a previously-issued key as evidence of the caller's identity or authority — ordinary MCP authorization applies to every request bearing a key exactly as it would without one. +- **Cross-client or cross-session key collision.** If a server's deduplication store is not scoped per-authenticated-session (or per-authorized-caller, where applicable), a key collision between two unrelated clients could cause one client's request to be treated as a duplicate of another's, or could allow one party to observe that another has issued a request with a given key. Servers MUST scope their idempotency records to whatever authorization boundary already governs access to the underlying tool call — an OAuth-authenticated session, a stdio process's local user, a workspace or team boundary, or any other boundary a given deployment already enforces — so that a key is only ever matched against a prior request from the same authorized caller. This SEP does not define what that boundary is, since MCP itself does not define a single uniform authorization model across deployments; it requires only that whatever boundary already exists for the tool call itself also governs the idempotency record, so this mechanism cannot become a way to bypass an authorization boundary the rest of the deployment already enforces. +- **Resource exhaustion.** Because a server must retain state to detect repeated keys, an unbounded number of unique keys from a malicious or malfunctioning client could be used to exhaust server-side storage. Servers SHOULD apply the same rate-limiting and resource-quota mechanisms they already apply to other server-side state (e.g., Tasks) to their idempotency records, and the retention-window design in this SEP (a bounded window rather than indefinite retention) is intended in part to limit this exposure. +- **Denial of service via conflicting keys.** A client that reuses a key with different arguments will have its request rejected per this SEP's conflict-handling rule; this is intended behavior and not a new denial-of-service vector distinct from any other request a server chooses to reject. +- **Key predictability.** While a key is not a secret (see above), a client using low-entropy, predictable keys (sequential integers, timestamps alone) increases the chance of accidental collision between unrelated requests, which could cause one operation to be mistaken for a retry of another. Clients SHOULD generate keys with sufficient uniqueness — a UUID or an equivalent random identifier is sufficient — to make accidental collision negligible; this is a robustness recommendation for clients, not a security boundary the server is responsible for enforcing. +- **Replay amplification.** A client that retries a matching request many times causes the server to return its cached result each time rather than re-execute; for a large cached response, repeated retrieval could itself become a resource concern distinct from the execution cost this SEP eliminates. Servers MAY rate-limit repeated retrieval of a cached response the same way they would rate-limit any other repeated request, independent of whether this SEP's deduplication mechanism is involved. +- **Authorization changes between the original call and a replay.** A replayed response MUST still be subject to whatever authorization check would ordinarily govern access to it — this SEP does not require, or permit, a server to disclose a previously recorded response to a caller whose authorization has since been revoked or has changed such that they would no longer be permitted to receive it. Caching a result for replay is not an exemption from the server's normal authorization checks at the time of the replay; it is a statement about what result to return once those checks pass, not a bypass of them. + +## References + +Full URLs for every source cited in this SEP, kept here for reviewers and for later reading — not required by the SEP format itself, but preserved for traceability since this document draws on many external sources. + +- SEP-1686 (Tasks): https://modelcontextprotocol.io/community/seps/1686 (also tracked at https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1732) +- SEP-2663 (Tasks Extension): https://modelcontextprotocol.io/community/seps/2663 +- SEP-1036 (URL Mode Elicitation): https://modelcontextprotocol.io/seps/1036-url-mode-elicitation-for-secure-out-of-band-intera +- SEP-1788 (Clarify `_meta` Property Reserved Keys): https://modelcontextprotocol.io/community/seps/1788 +- MCP SEP Guidelines (prototype vs. reference implementation status): https://modelcontextprotocol.io/community/sep-guidelines +- MCP Design Principles: https://modelcontextprotocol.io/community/design-principles +- RFC 8259 (The JavaScript Object Notation (JSON) Data Interchange Format): https://www.rfc-editor.org/rfc/rfc8259 +- RFC 7942 (Improving Awareness of Running Code: The Implementation Status Section): https://www.rfc-editor.org/rfc/rfc7942 +- JSON-RPC 2.0 Specification: https://www.jsonrpc.org/specification +- IETF draft, HTTP Idempotency-Key Header Field: https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/ +- Stripe idempotency documentation: https://stripe.com/docs/api/idempotent_requests / https://docs.stripe.com/error-handling +- Amazon Builders' Library, "Making retries safe with idempotent APIs" (Malcolm Featonby): https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/ +- Saltzer, J.H., Reed, D.P., Clark, D.D. "End-to-End Arguments in System Design." ACM TOCS 2, no. 4 (1984): 277–288. https://web.mit.edu/Saltzer/www/publications/endtoend/endtoend.pdf +- Microsoft `agent-framework` GitHub issue (confirmed `_meta` dropped on tool results): https://github.com/microsoft/agent-framework/issues/2284 From e57c1814cd9e7408cb75a6e814b997cc508bd5f6 Mon Sep 17 00:00:00 2001 From: devmaha <4321989+devmaha@users.noreply.github.com> Date: Sat, 1 Aug 2026 10:49:27 +0530 Subject: [PATCH 2/9] "Request Idempotency" SEP Proposal Draft --- seps/sep-0000-idempotency-faq.md | 68 ++++++++++++++++++++++++++++ seps/sep-0000-request-idempotency.md | 12 ++--- 2 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 seps/sep-0000-idempotency-faq.md diff --git a/seps/sep-0000-idempotency-faq.md b/seps/sep-0000-idempotency-faq.md new file mode 100644 index 000000000..4f145ddea --- /dev/null +++ b/seps/sep-0000-idempotency-faq.md @@ -0,0 +1,68 @@ +# FAQ: Request Idempotency SEP + +Companion to `sep-0000-request-idempotency.md`. Answers to questions likely to come up in review. + +--- + +## Design decisions + +**Why isn't the JSON-RPC `id` field sufficient?** +`id` exists to correlate a response with its request, not to detect duplicates. A retry commonly gets a fresh `id`, and JSON-RPC doesn't require a server to remember the `id`s of requests it has already completed. + +**Why isn't `_meta` sufficient?** +An idempotency key placed in `_meta` would be implementation-specific, with no guarantee it survives end-to-end. That's not just a theoretical concern — three client frameworks already in use (Microsoft's Agent Framework, Agno, and OpenAI's Agents SDK) drop `_meta` from tool results entirely, and one of their own maintainers has called this a spec violation. A field that real clients already strip out isn't something you can build a guarantee on. + +**Why isn't this a transport-level mechanism, like an HTTP header?** +Because MCP supports transports that have no header layer at all. Stdio — the default for Claude Desktop, Claude Code, and most IDE integrations — is newline-delimited JSON-RPC with no concept of headers. A header-based design simply wouldn't apply everywhere MCP is used. + +**SEP-1686 says idempotency "applies to all MCP messages" — why does this SEP only cover `tools/call`?** +SEP-1686 was describing the scope of the problem, not requiring the first fix to solve it everywhere at once. Every piece of evidence gathered — the ad hoc conventions servers have already built, the industry precedent, SEP-1686's own motivating example — is about side-effecting tool calls specifically. Nothing suggests `initialize`, `ping`, `listTools`, or notifications carry the same duplicate-execution risk. Starting narrow and letting a later SEP extend the mechanism if evidence turns up elsewhere is the same approach SEP-1036 took. + +**Why require a capability negotiation instead of just letting servers ignore the field if they don't support it?** +Because silent ignoring is worse than an explicit error. If a client sends `idempotencyKey` with no way to confirm the server actually honors it, it has no way to tell "deduplicated correctly" apart from "ignored and executed twice." The capability lets a client check that guarantee is real before it depends on it. Sending the key to a server that hasn't advertised support does no harm — it just doesn't buy you anything. + +**Why replay the original error too, not just successful results?** +Because a client that never got a response has no way to know whether the original call succeeded, failed, or crashed midway. Replaying whatever actually happened — including an error — is what keeps the retry honest. Guessing "it must have failed" and re-executing would defeat the whole point. This also matches how Stripe's own idempotency keys behave: they replay the original outcome regardless of whether it was a success or an error. + +**Why not require a specific comparison algorithm, like RFC 8785 canonicalization, to check whether two requests match?** +What matters for interoperability is that two servers reach the same answer to "are these the same request," not how they get there. A server with large, complex arguments might hash them; one with small payloads might just compare directly. Both are fine as long as they agree on the outcome, which is why the SEP specifies the comparison rules themselves (how numbers and strings should be treated) rather than mandating one library or algorithm. + +**Why reject a reused key with different arguments, instead of replaying the cached result or just treating it as a new call?** +Both of those alternatives do the wrong thing quietly. Replaying the cached result would apply the first operation's outcome to what the client meant as a second, different operation. Treating it as new defeats the whole purpose of the key, and the client never finds out something went wrong. Rejecting the call outright is the only option that actually surfaces the problem — which is almost always a client-side bug — so the client can fix it, typically by generating a new key. + +**Isn't this just Stripe's idempotency key, copied into MCP?** +The core idea — a client-generated identifier for a retry — is the same, because it's solving the same problem: a client that can't tell "this never arrived" from "this arrived, ran, and the response got lost." Everything specific to how it's wired into MCP (where the field sits in the request, how support is negotiated, why it's scoped to `tools/call`) is new here, not carried over from Stripe. + +**Why does this need to live in the protocol — couldn't an SDK just handle retries safely on its own?** +Because an SDK-level fix only protects clients built on that SDK. MCP deployments routinely mix languages and frameworks — a Python server, a TypeScript client, a Go gateway in between — and no SDK can make another SDK's users follow its convention. If every SDK solved this on its own, you'd end up with the same fragmentation already happening today, just one layer down instead of fixed. + +**Isn't idempotency really a per-tool or per-application concern, not something the protocol should standardize?** +The protocol already standardizes how a client asks a server to run a tool — that's what `tools/call` is for. So it makes sense for the protocol to also standardize how a client says "this call is a retry of one I already made." Whether a specific tool, like `charge_payment`, needs to behave idempotently is an application question. But whether a client and server can even agree on what a retry means is a protocol question, and right now MCP has no answer to it — which is exactly why every server ends up inventing its own field name and storage approach. + +**Doesn't the fact that well-built servers already handle this on their own mean the protocol doesn't need to get involved?** +It's actually the opposite — that's the argument for standardizing it. This is the same situation SEP-1036 was in: implementers weren't incapable of returning login URLs and having clients recognize them, they were already doing it, just each in their own incompatible way. The hard part was never building the mechanism. It's getting independent implementations to agree on the same interface, which only a shared standard can do. + +--- + +## Tradeoffs — real costs, not hidden ones + +**Does "must not execute twice" actually hold if the server crashes?** +Not unconditionally. If a server runs the side effect and then crashes before it saves the record of having done so, a retry afterward will run it again. This isn't a gap specific to this proposal — no protocol field can guarantee exactly-once execution across a server's own crash, and HTTP-based idempotency keys have the same limit. The SEP's language should be read as describing normal operation, not an absolute guarantee that survives any failure. See the "Implementation ordering is intentionally unspecified" section in the Specification for the full explanation. + +**Is documenting the retention window a MUST or just a SHOULD?** +It's a SHOULD. Some MCP servers — small, local, embedded ones — have no real way to publish documentation, and a hard MUST would shut them out of conforming to the SEP at all. That's a genuine tradeoff: a client talking to a server with an undocumented retention window has no way to know how long a retry stays safe. Reasonable people could land on MUST instead if they weight interoperability more heavily than accommodating the smallest implementations — this is a judgment call, not a settled question. + +**How much real interoperability does this create, if storage, retention, and comparison logic are all left up to each server?** +What's actually standardized is the retry key itself, the decision to deduplicate, and what happens on conflict — not how a server stores state, how long it keeps it, or how it implements comparison. That's the same scope HTTP idempotency keys take on, and it's meant to standardize only the parts two implementations actually need to agree on to interoperate. It's a narrower guarantee than a fully specified wire protocol, and that's a deliberate tradeoff, not an oversight. + +**What happens if the tool's implementation changes between the original call and a retry — does the server replay the old result or run the new code?** +A retry within the retention window always replays the original result, because equivalence is defined by the tool name and arguments, not by which version of the tool ran. On a server that ships tool changes frequently and keeps a long retention window, a client could get back a result produced by logic that's since been replaced. The reasoning for this default: a retry should get the outcome of the operation the client actually asked for, not a different one substituted silently. A server that finds this tradeoff unacceptable can simply use a shorter retention window. + +**If the world changes after the original call — say, someone manually undoes a delete — and a client later retries and gets back the original "deleted successfully" response, is that a bug?** +No, that's expected behavior, not a bug. Replay means returning what happened at the time of the original call, not re-checking current state. Idempotency guarantees that the same request produces the same recorded outcome — it doesn't guarantee that the response still reflects reality. Trying to do both would turn this into a staleness-detection mechanism, which is a separate and harder problem this SEP doesn't attempt to solve. A client that needs to know whether a cached result is still accurate needs a different mechanism for that. + +**Should the capability advertise the retention window itself, instead of leaving that to server documentation?** +That would be a reasonable addition for a future SEP. Something like `"idempotency": {"retentionSeconds": 86400}` would let a client check retry safety programmatically instead of relying on docs it can't read at runtime. It isn't included here because it adds a separate concern to a proposal that's meant to stay narrow, and because nothing so far suggests retention-window discovery is causing the same kind of incompatible workarounds that motivated the core mechanism. + +**Is there enough real-world MCP implementation experience behind this yet?** +This is the weakest part of the case. There's solid evidence the underlying problem is real — independent papers, engineering write-ups, and a working prototype ([github.com/abluva/mcp-request-idempotency-reference](https://github.com/abluva/mcp-request-idempotency-reference)) all point the same way. What's thinner is evidence of multiple MCP-native implementations already running in production and independently converging on this design — what exists so far mostly describes the problem and sketches ad hoc fixes, rather than shipped, competing implementations. More implementations — ideally in more than one language, from more than one author — during review would genuinely help close that gap. diff --git a/seps/sep-0000-request-idempotency.md b/seps/sep-0000-request-idempotency.md index 07f6e278a..1ec61cc70 100644 --- a/seps/sep-0000-request-idempotency.md +++ b/seps/sep-0000-request-idempotency.md @@ -61,7 +61,7 @@ This SEP introduces the `tools.idempotency` capability, declared during initiali This capability intentionally contains no structured metadata in this proposal — its role is to declare that the mechanism is supported at all, not to advertise parameters like retention duration (see "Retention," below, for why that is left to server-level documentation rather than capability metadata in this SEP). -An explicit capability was chosen over silent acceptance of an unrecognized parameter because the two give a client meaningfully different guarantees. If a client sent `idempotencyKey` to any server and simply hoped for the best, it would have no way to distinguish "the server ignored this and executed my request twice" from "the server honored it and deduplicated correctly" — exactly the false sense of protection this SEP exists to prevent, not merely repeat in a new form. Declaring the capability lets a client know, before it depends on the guarantee, whether a given server's deduplication behavior is normative rather than accidental or absent. This is consistent with how MCP already uses capability negotiation elsewhere: capabilities distinguish protocol-level guarantees a party can rely on (whether a server supports Tasks, elicitation, or sampling) from ordinary request parameters a party is free to send or ignore. Idempotent retry handling is a protocol guarantee in the same sense — it changes what a client can assume about the server's behavior, not merely what data the request carries — which makes capability negotiation the existing mechanism this SEP should use, not a new pattern invented for this proposal alone. A client that requires deduplication for correctness SHOULD verify the server has advertised the capability before relying on it; sending the field to a server that has not advertised support provides no interoperability guarantee, even though the field itself is harmless to send. +An explicit capability was chosen over silent acceptance of an unrecognized parameter because the two give a client meaningfully different guarantees. If a client sent `idempotencyKey` to any server and simply hoped for the best, it would have no way to distinguish "the server ignored this and executed my request twice" from "the server honored it and deduplicated correctly" — exactly the ambiguity this SEP is meant to remove, not something it should end up recreating. Declaring the capability lets a client know, before it depends on the guarantee, whether a given server's deduplication behavior is normative rather than accidental or absent. This is consistent with how MCP already uses capability negotiation elsewhere: capabilities distinguish protocol-level guarantees a party can rely on (whether a server supports Tasks, elicitation, or sampling) from ordinary request parameters a party is free to send or ignore. Idempotent retry handling is a protocol guarantee in the same sense — it changes what a client can assume about the server's behavior, not merely what data the request carries — which makes capability negotiation the existing mechanism this SEP should use, not a new pattern invented for this proposal alone. A client that requires deduplication for correctness SHOULD verify the server has advertised the capability before relying on it; sending the field to a server that has not advertised support provides no interoperability guarantee, even though the field itself is harmless to send. Clients MAY send `idempotencyKey` regardless of whether the server has declared this capability. A server that has not implemented this SEP will not recognize the field as meaningful and will handle the request according to its own existing parameter-validation rules — permissively ignoring the unrecognized field, or, for a stricter server, potentially rejecting the request outright (see Backward Compatibility). @@ -84,8 +84,8 @@ Clients MAY send `idempotencyKey` regardless of whether the server has declared When a server that declares this capability receives a `tools/call` whose `idempotencyKey` matches one it has already processed, is currently processing, or has retained: -1. **If a request bearing the same key is still executing** — the original call has not yet produced a result to record — the server MUST NOT begin a second execution of the tool for the duplicate request, and MUST NOT block the duplicate request indefinitely waiting for the original to finish. Instead, the server SHOULD reject the duplicate immediately with an error indicating a request with this key is already in progress, allowing the client to retry after a short delay rather than either duplicating execution or hanging. As with the conflict case below, servers SHOULD populate `error.data` with a structured value identifying this specific failure mode (for example `{"type": "idempotency_key_in_progress", "idempotencyKey": ""}`), so a generic client can distinguish it from other errors programmatically. Rejection is specified rather than left as one option among several — for instance, attaching the duplicate to the original execution's eventual result — because leaving the choice open would mean two conforming servers could behave differently for the identical wire-level situation, which is exactly the kind of interoperability gap this SEP exists to close, not reproduce at a smaller scale. This is also the same handling Stripe uses for the identical case directly: Stripe returns a 409 response stating a request using the same idempotency key is already in progress, rather than queuing, sharing the in-flight result, or executing a second time — corroborating that rejection is a workable, precedented choice, though the reason for specifying it here is the interoperability argument above, not the precedent by itself. A future revision of this mechanism could specify attaching to the in-flight execution's eventual result instead, if evidence emerges that rejection is insufficient in practice; this SEP takes the simpler, already-precedented approach for now. -2. **If the new request's arguments are equivalent to the arguments of a completed original call** (see "Request equivalence," below), the server MUST NOT execute the tool again. It MUST return the previously produced result — whether that result was a successful response or an error response — unless that result has since expired from the server's retention window (see "Retention," below), in which case the server MAY treat the request as new. Replaying the original error rather than re-attempting execution is deliberate: a client retrying after a lost response has no way to know whether the original call succeeded or failed, and re-executing on the assumption that the original must have failed reintroduces the exact duplicate-execution risk this SEP exists to prevent. +1. **If a request bearing the same key is still executing** — the original call has not yet produced a result to record — the server MUST NOT begin a second execution of the tool for the duplicate request, and MUST NOT block the duplicate request indefinitely waiting for the original to finish. Instead, the server SHOULD reject the duplicate immediately with an error indicating a request with this key is already in progress, allowing the client to retry after a short delay rather than either duplicating execution or hanging. As with the conflict case below, servers SHOULD populate `error.data` with a structured value identifying this specific failure mode (for example `{"type": "idempotency_key_in_progress", "idempotencyKey": ""}`), so a generic client can distinguish it from other errors programmatically. Rejection is specified rather than left as one option among several — for instance, attaching the duplicate to the original execution's eventual result — because leaving the choice open would mean two conforming servers could behave differently for the identical wire-level situation, which would recreate, at smaller scale, the same interoperability gap this proposal is meant to close. This is also the same handling Stripe uses for the identical case directly: Stripe returns a 409 response stating a request using the same idempotency key is already in progress, rather than queuing, sharing the in-flight result, or executing a second time — corroborating that rejection is a workable, precedented choice, though the reason for specifying it here is the interoperability argument above, not the precedent by itself. A future revision of this mechanism could specify attaching to the in-flight execution's eventual result instead, if evidence emerges that rejection is insufficient in practice; this SEP takes the simpler, already-precedented approach for now. +2. **If the new request's arguments are equivalent to the arguments of a completed original call** (see "Request equivalence," below), the server MUST NOT execute the tool again. It MUST return the previously produced result — whether that result was a successful response or an error response — unless that result has since expired from the server's retention window (see "Retention," below), in which case the server MAY treat the request as new. Replaying the original error rather than re-attempting execution is deliberate: a client retrying after a lost response has no way to know whether the original call succeeded or failed, and re-executing on the assumption that the original must have failed brings back the very duplicate-execution risk this SEP is designed to eliminate. 3. **If the new request's arguments differ from the arguments of the original call**, the server MUST reject the request with an error indicating the key was previously used with different arguments. The server MUST NOT execute the tool with the new arguments, and MUST NOT silently return the cached result from the original, differently-parameterized call. This SEP does not define a new JSON-RPC error code for this case, since a generic client should not need to distinguish idempotency conflicts by error code from every other application-specific failure a server might raise. Instead, servers SHOULD populate the standard JSON-RPC `error.data` field — an existing, application-defined extension point, not a new mechanism this SEP introduces — with a structured value identifying the failure as an idempotency key conflict, for example `{"type": "idempotency_key_conflict", "idempotencyKey": ""}`. This lets a generic client detect the conflict programmatically, without parsing the human-readable `error.message` string, while still requiring no new reserved error code and no change to the JSON-RPC error envelope itself. Reusing an idempotency key with different request parameters is treated as a client error, not as a hint to guess which behavior the caller intended. @@ -128,7 +128,7 @@ Each subsection below answers one specific design question independently; none d ### Why replay the original response, including errors, rather than re-attempting execution? -Idempotency is about preserving the outcome of an operation that already happened, not giving the caller another attempt at a better one. A client retrying after a lost response cannot know whether the original call succeeded, failed, or crashed partway through — only that it doesn't have a response. Re-executing on the assumption that a missing response means the original must have failed reintroduces the exact duplicate-execution risk this SEP exists to prevent, since the original could just as easily have succeeded. This mirrors Stripe's own documented behavior: Stripe states it stores the original result "regardless of whether it succeeds or fails," and replays it on a matching retry "including 500 errors." A related question worth answering directly, since it recurs whenever idempotency is discussed: if the original execution returned a transient internal error, shouldn't a retry get a fresh attempt rather than the same cached error? No — that is an application-level recovery question, not an idempotency question. This SEP defines what happens to a retry of the *same* logical operation; a decision to attempt the operation *again*, under a new logical identity, is exactly what generating a new `idempotencyKey` is for. Recovering from a partial or failed execution remains the caller's responsibility once it observes the replayed outcome, not a behavior this SEP asks servers to attempt on the caller's behalf. +Idempotency is about preserving the outcome of an operation that already happened, not giving the caller another attempt at a better one. A client retrying after a lost response cannot know whether the original call succeeded, failed, or crashed partway through — only that it doesn't have a response. Re-executing on the assumption that a missing response means the original must have failed brings back the same duplicate-execution risk this proposal is meant to eliminate — the original call could just as easily have succeeded. This mirrors Stripe's own documented behavior: Stripe states it stores the original result "regardless of whether it succeeds or fails," and replays it on a matching retry "including 500 errors." A related question worth answering directly, since it recurs whenever idempotency is discussed: if the original execution returned a transient internal error, shouldn't a retry get a fresh attempt rather than the same cached error? No — that is an application-level recovery question, not an idempotency question. This SEP defines what happens to a retry of the *same* logical operation; a decision to attempt the operation *again*, under a new logical identity, is exactly what generating a new `idempotencyKey` is for. Recovering from a partial or failed execution remains the caller's responsibility once it observes the replayed outcome, not a behavior this SEP asks servers to attempt on the caller's behalf. ### Why not `_meta`? @@ -166,7 +166,7 @@ This proposal is additive and introduces no backward incompatibility. `idempoten ## Reference Implementation -A prototype implementation exists, built against the official MCP Python SDK (`mcp` >= 1.28.1, requiring Python >= 3.10) and run over a genuine stdio client/server session rather than a mock. It demonstrates, as observable behavior: +A prototype implementation exists at [github.com/abluva/mcp-request-idempotency-reference](https://github.com/abluva/mcp-request-idempotency-reference), built against the official MCP Python SDK (`mcp` >= 1.9.0, < 2.0.0; requiring Python >= 3.10) and run over a genuine stdio client/server session rather than a mock. It demonstrates, as observable behavior: 1. The duplicate-execution failure mode with no protection (`charge_unguarded`): a retried call after a simulated lost response executes the underlying side effect a second time. 2. The proposed mechanism (`charge_guarded`): an identical retry bearing the same `idempotencyKey` is deduplicated — the side effect executes once, and the cached result from the original call is returned. @@ -202,4 +202,4 @@ Full URLs for every source cited in this SEP, kept here for reviewers and for la - Stripe idempotency documentation: https://stripe.com/docs/api/idempotent_requests / https://docs.stripe.com/error-handling - Amazon Builders' Library, "Making retries safe with idempotent APIs" (Malcolm Featonby): https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/ - Saltzer, J.H., Reed, D.P., Clark, D.D. "End-to-End Arguments in System Design." ACM TOCS 2, no. 4 (1984): 277–288. https://web.mit.edu/Saltzer/www/publications/endtoend/endtoend.pdf -- Microsoft `agent-framework` GitHub issue (confirmed `_meta` dropped on tool results): https://github.com/microsoft/agent-framework/issues/2284 +- Microsoft `agent-framework` GitHub issue (confirmed `_meta` dropped on tool results): https://github.com/microsoft/agent-framework/issues/2284 \ No newline at end of file From 609abd634396c44f621502aaa7b272275e1efaa7 Mon Sep 17 00:00:00 2001 From: devmaha <4321989+devmaha@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:28:01 +0530 Subject: [PATCH 3/9] Rename SEP files to match PR number, update preamble - 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 --- ...000-idempotency-faq.md => sep-3182-idempotency-faq.md} | 2 +- ...est-idempotency.md => sep-3182-request-idempotency.md} | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) rename seps/{sep-0000-idempotency-faq.md => sep-3182-idempotency-faq.md} (99%) rename seps/{sep-0000-request-idempotency.md => sep-3182-request-idempotency.md} (98%) diff --git a/seps/sep-0000-idempotency-faq.md b/seps/sep-3182-idempotency-faq.md similarity index 99% rename from seps/sep-0000-idempotency-faq.md rename to seps/sep-3182-idempotency-faq.md index 4f145ddea..eb974364a 100644 --- a/seps/sep-0000-idempotency-faq.md +++ b/seps/sep-3182-idempotency-faq.md @@ -1,6 +1,6 @@ # FAQ: Request Idempotency SEP -Companion to `sep-0000-request-idempotency.md`. Answers to questions likely to come up in review. +Companion to `sep-3182-request-idempotency.md`. Answers to questions likely to come up in review. --- diff --git a/seps/sep-0000-request-idempotency.md b/seps/sep-3182-request-idempotency.md similarity index 98% rename from seps/sep-0000-request-idempotency.md rename to seps/sep-3182-request-idempotency.md index 1ec61cc70..dc82593e1 100644 --- a/seps/sep-0000-request-idempotency.md +++ b/seps/sep-3182-request-idempotency.md @@ -1,4 +1,4 @@ -# SEP-0000: Request Idempotency +# SEP-3182: Request Idempotency ## Preamble @@ -7,7 +7,7 @@ - **Status:** proposal - **Type:** Standards Track - **Created:** 2026-08-01 -- **PR:** _(full PR URL, filled in once the PR is opened)_ +- **PR:** https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3182 ## Abstract @@ -120,7 +120,7 @@ To state the limit explicitly: this SEP specifies the behavior expected of a con ### Relationship to SEP-2663 (Tasks Extension) -The `tasks/get` / `tasks/update` split introduced by SEP-2663 and the mechanism in this SEP are complementary, not redundant, and a reviewer should not read one as making the other unnecessary. The Tasks split prevents an accidental *write* from occurring during what is meant to be a *read* — a retried `tasks/get` cannot itself trigger a side effect, because reading is now structurally separate from mutating. The mechanism in this SEP prevents a retried *write* — an ordinary `tools/call` invocation of a side-effecting tool — from executing its side effect twice. An implementation with the Tasks split but no idempotency key can still duplicate a retried write; an implementation with an idempotency key but no read/write separation still risks a write occurring somewhere a read was expected. Both are needed for the different failure modes they each address. +The `tasks/get` / `tasks/update` split introduced by SEP-2663 and the mechanism in this SEP are complementary, not redundant, and a reviewer should not read one as making the other unnecessary. The Tasks split prevents an accidental _write_ from occurring during what is meant to be a _read_ — a retried `tasks/get` cannot itself trigger a side effect, because reading is now structurally separate from mutating. The mechanism in this SEP prevents a retried _write_ — an ordinary `tools/call` invocation of a side-effecting tool — from executing its side effect twice. An implementation with the Tasks split but no idempotency key can still duplicate a retried write; an implementation with an idempotency key but no read/write separation still risks a write occurring somewhere a read was expected. Both are needed for the different failure modes they each address. ## Rationale @@ -128,7 +128,7 @@ Each subsection below answers one specific design question independently; none d ### Why replay the original response, including errors, rather than re-attempting execution? -Idempotency is about preserving the outcome of an operation that already happened, not giving the caller another attempt at a better one. A client retrying after a lost response cannot know whether the original call succeeded, failed, or crashed partway through — only that it doesn't have a response. Re-executing on the assumption that a missing response means the original must have failed brings back the same duplicate-execution risk this proposal is meant to eliminate — the original call could just as easily have succeeded. This mirrors Stripe's own documented behavior: Stripe states it stores the original result "regardless of whether it succeeds or fails," and replays it on a matching retry "including 500 errors." A related question worth answering directly, since it recurs whenever idempotency is discussed: if the original execution returned a transient internal error, shouldn't a retry get a fresh attempt rather than the same cached error? No — that is an application-level recovery question, not an idempotency question. This SEP defines what happens to a retry of the *same* logical operation; a decision to attempt the operation *again*, under a new logical identity, is exactly what generating a new `idempotencyKey` is for. Recovering from a partial or failed execution remains the caller's responsibility once it observes the replayed outcome, not a behavior this SEP asks servers to attempt on the caller's behalf. +Idempotency is about preserving the outcome of an operation that already happened, not giving the caller another attempt at a better one. A client retrying after a lost response cannot know whether the original call succeeded, failed, or crashed partway through — only that it doesn't have a response. Re-executing on the assumption that a missing response means the original must have failed brings back the same duplicate-execution risk this proposal is meant to eliminate — the original call could just as easily have succeeded. This mirrors Stripe's own documented behavior: Stripe states it stores the original result "regardless of whether it succeeds or fails," and replays it on a matching retry "including 500 errors." A related question worth answering directly, since it recurs whenever idempotency is discussed: if the original execution returned a transient internal error, shouldn't a retry get a fresh attempt rather than the same cached error? No — that is an application-level recovery question, not an idempotency question. This SEP defines what happens to a retry of the _same_ logical operation; a decision to attempt the operation _again_, under a new logical identity, is exactly what generating a new `idempotencyKey` is for. Recovering from a partial or failed execution remains the caller's responsibility once it observes the replayed outcome, not a behavior this SEP asks servers to attempt on the caller's behalf. ### Why not `_meta`? From 688d7bfbf66d42804b43d94b8aa2d88d9167e195 Mon Sep 17 00:00:00 2001 From: devmaha <4321989+devmaha@users.noreply.github.com> Date: Sat, 1 Aug 2026 13:04:30 +0530 Subject: [PATCH 4/9] Fix Prettier formatting in SEP-3182 --- seps/sep-3182-request-idempotency.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seps/sep-3182-request-idempotency.md b/seps/sep-3182-request-idempotency.md index dc82593e1..eafd89951 100644 --- a/seps/sep-3182-request-idempotency.md +++ b/seps/sep-3182-request-idempotency.md @@ -202,4 +202,4 @@ Full URLs for every source cited in this SEP, kept here for reviewers and for la - Stripe idempotency documentation: https://stripe.com/docs/api/idempotent_requests / https://docs.stripe.com/error-handling - Amazon Builders' Library, "Making retries safe with idempotent APIs" (Malcolm Featonby): https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/ - Saltzer, J.H., Reed, D.P., Clark, D.D. "End-to-End Arguments in System Design." ACM TOCS 2, no. 4 (1984): 277–288. https://web.mit.edu/Saltzer/www/publications/endtoend/endtoend.pdf -- Microsoft `agent-framework` GitHub issue (confirmed `_meta` dropped on tool results): https://github.com/microsoft/agent-framework/issues/2284 \ No newline at end of file +- Microsoft `agent-framework` GitHub issue (confirmed `_meta` dropped on tool results): https://github.com/microsoft/agent-framework/issues/2284 From f90db4073a3e0ca6a8d691a33d305e3c6bc2315a Mon Sep 17 00:00:00 2001 From: devmaha <4321989+devmaha@users.noreply.github.com> Date: Sat, 1 Aug 2026 14:37:27 +0530 Subject: [PATCH 5/9] Add stateless-spec and MRTR FAQ entries for SEP-3182 --- seps/sep-3182-idempotency-faq.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/seps/sep-3182-idempotency-faq.md b/seps/sep-3182-idempotency-faq.md index eb974364a..9788e69ac 100644 --- a/seps/sep-3182-idempotency-faq.md +++ b/seps/sep-3182-idempotency-faq.md @@ -42,6 +42,12 @@ The protocol already standardizes how a client asks a server to run a tool — t **Doesn't the fact that well-built servers already handle this on their own mean the protocol doesn't need to get involved?** It's actually the opposite — that's the argument for standardizing it. This is the same situation SEP-1036 was in: implementers weren't incapable of returning login URLs and having clients recognize them, they were already doing it, just each in their own incompatible way. The hard part was never building the mechanism. It's getting independent implementations to agree on the same interface, which only a shared standard can do. +**MCP 2026-07-28 made the protocol stateless and removed sessions and the `initialize` handshake — does this proposal still make sense?** +Yes, and the change actually strengthens the case for it. Capability discovery moves to `server/discover`, which this proposal already reflects. The bigger effect is on deployment: session affinity used to mean a retry had some chance of landing back on the same server instance that handled the original request, which gave an in-memory dedup store a kind of accidental partial protection. Under the stateless core, any instance can handle any request, so that accidental protection is gone — a production server now needs a dedup store that's actually shared across instances, not per-process. The mechanism this SEP defines doesn't change; what changes is that skipping it is riskier than it used to be. + +**How does this interact with Multi Round-Trip Requests (MRTR)?** +They solve different problems and shouldn't be conflated. An MRTR continuation is the client knowingly resuming an operation the server explicitly said isn't finished yet — reissuing `tools/call` with `inputResponses` and the echoed `requestState`. A retry under this SEP is the client re-sending a request because it doesn't know whether an earlier, identical request already completed. Don't attach the same `idempotencyKey` to an MRTR continuation expecting it to trigger this SEP's deduplication — that's not the scenario it's built for. + --- ## Tradeoffs — real costs, not hidden ones @@ -65,4 +71,4 @@ No, that's expected behavior, not a bug. Replay means returning what happened at That would be a reasonable addition for a future SEP. Something like `"idempotency": {"retentionSeconds": 86400}` would let a client check retry safety programmatically instead of relying on docs it can't read at runtime. It isn't included here because it adds a separate concern to a proposal that's meant to stay narrow, and because nothing so far suggests retention-window discovery is causing the same kind of incompatible workarounds that motivated the core mechanism. **Is there enough real-world MCP implementation experience behind this yet?** -This is the weakest part of the case. There's solid evidence the underlying problem is real — independent papers, engineering write-ups, and a working prototype ([github.com/abluva/mcp-request-idempotency-reference](https://github.com/abluva/mcp-request-idempotency-reference)) all point the same way. What's thinner is evidence of multiple MCP-native implementations already running in production and independently converging on this design — what exists so far mostly describes the problem and sketches ad hoc fixes, rather than shipped, competing implementations. More implementations — ideally in more than one language, from more than one author — during review would genuinely help close that gap. +This is the weakest part of the case. There's solid evidence the underlying problem is real — independent papers, engineering write-ups, and a working prototype ([github.com/abluva/mcp-request-idempotency-reference](https://github.com/abluva/mcp-request-idempotency-reference)) all point the same way. What's thinner is evidence of multiple MCP-native implementations already running in production and independently converging on this design — what exists so far mostly describes the problem and sketches ad hoc fixes, rather than shipped, competing implementations. More implementations — ideally in more than one language, from more than one author — during review would genuinely help close that gap. \ No newline at end of file From 74809871592d194e04e14dc52543abc9d8c5cedc Mon Sep 17 00:00:00 2001 From: devmaha <4321989+devmaha@users.noreply.github.com> Date: Sat, 1 Aug 2026 14:39:05 +0530 Subject: [PATCH 6/9] Update SEP-3182 for the 2026-07-28 stateless spec - 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 --- seps/sep-3182-request-idempotency.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/seps/sep-3182-request-idempotency.md b/seps/sep-3182-request-idempotency.md index eafd89951..ad4e0e7b7 100644 --- a/seps/sep-3182-request-idempotency.md +++ b/seps/sep-3182-request-idempotency.md @@ -47,7 +47,7 @@ This SEP adds one optional field, `idempotencyKey`, to the parameters of a `tool ### Capability declaration -This SEP introduces the `tools.idempotency` capability, declared during initialization, rather than treating `idempotencyKey` as a bare optional field with no negotiation: +This SEP introduces the `tools.idempotency` capability, discoverable via `server/discover` (SEP-2575), rather than treating `idempotencyKey` as a bare optional field with no negotiation: ```json { @@ -61,7 +61,7 @@ This SEP introduces the `tools.idempotency` capability, declared during initiali This capability intentionally contains no structured metadata in this proposal — its role is to declare that the mechanism is supported at all, not to advertise parameters like retention duration (see "Retention," below, for why that is left to server-level documentation rather than capability metadata in this SEP). -An explicit capability was chosen over silent acceptance of an unrecognized parameter because the two give a client meaningfully different guarantees. If a client sent `idempotencyKey` to any server and simply hoped for the best, it would have no way to distinguish "the server ignored this and executed my request twice" from "the server honored it and deduplicated correctly" — exactly the ambiguity this SEP is meant to remove, not something it should end up recreating. Declaring the capability lets a client know, before it depends on the guarantee, whether a given server's deduplication behavior is normative rather than accidental or absent. This is consistent with how MCP already uses capability negotiation elsewhere: capabilities distinguish protocol-level guarantees a party can rely on (whether a server supports Tasks, elicitation, or sampling) from ordinary request parameters a party is free to send or ignore. Idempotent retry handling is a protocol guarantee in the same sense — it changes what a client can assume about the server's behavior, not merely what data the request carries — which makes capability negotiation the existing mechanism this SEP should use, not a new pattern invented for this proposal alone. A client that requires deduplication for correctness SHOULD verify the server has advertised the capability before relying on it; sending the field to a server that has not advertised support provides no interoperability guarantee, even though the field itself is harmless to send. +An explicit capability was chosen over silent acceptance of an unrecognized parameter because the two give a client meaningfully different guarantees. If a client sent `idempotencyKey` to any server and simply hoped for the best, it would have no way to distinguish "the server ignored this and executed my request twice" from "the server honored it and deduplicated correctly" — exactly the ambiguity this SEP is meant to remove, not something it should end up recreating. Declaring the capability lets a client know, before it depends on the guarantee, whether a given server's deduplication behavior is normative rather than accidental or absent. This is consistent with how MCP already uses capability negotiation elsewhere: capabilities distinguish protocol-level guarantees a party can rely on (whether a server supports Tasks, elicitation, or sampling) from ordinary request parameters a party is free to send or ignore. Idempotent retry handling is a protocol guarantee in the same sense — it changes what a client can assume about the server's behavior, not merely what data the request carries — which makes discoverable capabilities the existing mechanism this SEP should use, not a new pattern invented for this proposal alone. A client that requires deduplication for correctness SHOULD check `server/discover` for this capability before relying on it; sending the field to a server that has not advertised support provides no interoperability guarantee, even though the field itself is harmless to send. Clients MAY send `idempotencyKey` regardless of whether the server has declared this capability. A server that has not implemented this SEP will not recognize the field as meaningful and will handle the request according to its own existing parameter-validation rules — permissively ignoring the unrecognized field, or, for a stricter server, potentially rejecting the request outright (see Backward Compatibility). @@ -108,7 +108,7 @@ Servers SHOULD document their retention window through server-level documentatio ### Scope -This SEP proposes `idempotencyKey` on `tools/call` specifically. It intentionally does not define idempotency semantics for other request types. Every concrete piece of evidence motivating this proposal — Tasks' own rationale, the independently-reinvented server-side conventions, the industry precedent cited above — concerns tool execution and other side-effecting operations, not `initialize`, `ping`, `listTools`, `listResources`, or notification messages, for which no evidence of duplicate-execution risk was found during this research. Extending the mechanism to additional request types is left as an explicit avenue for a future, narrower proposal, informed by whichever additional request types demonstrate the same failure mode in practice. This narrower scope is also a closer match to SEP-1036's own precedent (one field added to one existing capability) than a protocol-wide claim would be, even though SEP-1686's own language ("applies to all MCP messages") would technically support the broader scope. +This SEP proposes `idempotencyKey` on `tools/call` specifically. It intentionally does not define idempotency semantics for other request types. Every concrete piece of evidence motivating this proposal — Tasks' own rationale, the independently-reinvented server-side conventions, the industry precedent cited above — concerns tool execution and other side-effecting operations, not `server/discover`, `ping`, `listTools`, `listResources`, or notification messages, for which no evidence of duplicate-execution risk was found during this research. Extending the mechanism to additional request types is left as an explicit avenue for a future, narrower proposal, informed by whichever additional request types demonstrate the same failure mode in practice. This narrower scope is also a closer match to SEP-1036's own precedent (one field added to one existing capability) than a protocol-wide claim would be, even though SEP-1686's own language ("applies to all MCP messages") would technically support the broader scope. A `tools/call` response is atomic under the current specification: a tool may emit `notifications/progress` messages while executing, but the call itself still produces a single, final result rather than a streamed one. This SEP's mechanism concerns only that final result — the interaction described in "Server behavior on a repeated key" is unaffected by whether progress notifications occurred during the original execution, since those notifications are not part of what this SEP replays or deduplicates. If a future SEP introduces genuinely streamed or partial `tools/call` results, that SEP would need its own idempotency treatment; this proposal does not attempt to anticipate what that would look like. @@ -122,6 +122,12 @@ To state the limit explicitly: this SEP specifies the behavior expected of a con The `tasks/get` / `tasks/update` split introduced by SEP-2663 and the mechanism in this SEP are complementary, not redundant, and a reviewer should not read one as making the other unnecessary. The Tasks split prevents an accidental _write_ from occurring during what is meant to be a _read_ — a retried `tasks/get` cannot itself trigger a side effect, because reading is now structurally separate from mutating. The mechanism in this SEP prevents a retried _write_ — an ordinary `tools/call` invocation of a side-effecting tool — from executing its side effect twice. An implementation with the Tasks split but no idempotency key can still duplicate a retried write; an implementation with an idempotency key but no read/write separation still risks a write occurring somewhere a read was expected. Both are needed for the different failure modes they each address. +### Relationship to the 2026-07-28 stateless core and MRTR + +The 2026-07-28 revision removes protocol-level sessions and session affinity (SEP-2567) and the `initialize` handshake (SEP-2575): any server instance can now handle any request, with no sticky routing to the instance that handled a prior one. This changes the practical stakes of this SEP rather than the mechanism itself. Under the previous session-based model, a retry sent on the same session had some chance of reaching the same server instance that processed the original request, so an implementation could get partial, accidental protection from an in-memory, per-process dedup store. Under the stateless model that accidental protection is gone: a retry is no longer more likely to land on the instance that executed the original call than on any other. A server implementing this SEP in a horizontally-scaled, stateless deployment MUST therefore make its idempotency record store visible to every instance handling that server's traffic — for example, a shared cache or database — rather than process-local memory; a per-instance store does not satisfy the deduplication guarantee this SEP defines once requests are no longer pinned to one instance. This makes the mechanism this SEP proposes more necessary under the stateless core, not less, since the informal protection statelessness removes was never a guarantee to begin with. + +Multi Round-Trip Requests (MRTR, SEP-2322) introduce a different notion of a client re-issuing `tools/call`: when a tool returns an `input_required` result, the client gathers the requested inputs and reissues the same call with `inputResponses` and the echoed `requestState`. This is not a retry under this SEP's terms, and implementations should not conflate the two. A retry this SEP addresses is a client re-sending a request because it does not know whether a previous, structurally identical request completed. An MRTR continuation is the client knowingly resuming an operation the server has explicitly said is not yet finished, carrying new information the original request did not have. A client SHOULD NOT attach the same `idempotencyKey` to an MRTR continuation expecting this SEP's deduplication behavior to apply to it; the two mechanisms address different problems and are not designed to compose automatically. Whether — and how — `idempotencyKey` might usefully apply to the sequence of calls in an MRTR exchange is left to future work, since no evidence of a duplicate-execution failure mode specific to MRTR was found during this research. + ## Rationale Each subsection below answers one specific design question independently; none depends on reading the others first, so a reviewer can jump directly to whichever choice they want to examine. From 0289cc6b2c408ef0c0db98c0619bc41e5505436a Mon Sep 17 00:00:00 2001 From: devmaha <4321989+devmaha@users.noreply.github.com> Date: Sat, 1 Aug 2026 14:42:18 +0530 Subject: [PATCH 7/9] Fix Prettier Error --- seps/sep-3182-idempotency-faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seps/sep-3182-idempotency-faq.md b/seps/sep-3182-idempotency-faq.md index 9788e69ac..402e4157d 100644 --- a/seps/sep-3182-idempotency-faq.md +++ b/seps/sep-3182-idempotency-faq.md @@ -71,4 +71,4 @@ No, that's expected behavior, not a bug. Replay means returning what happened at That would be a reasonable addition for a future SEP. Something like `"idempotency": {"retentionSeconds": 86400}` would let a client check retry safety programmatically instead of relying on docs it can't read at runtime. It isn't included here because it adds a separate concern to a proposal that's meant to stay narrow, and because nothing so far suggests retention-window discovery is causing the same kind of incompatible workarounds that motivated the core mechanism. **Is there enough real-world MCP implementation experience behind this yet?** -This is the weakest part of the case. There's solid evidence the underlying problem is real — independent papers, engineering write-ups, and a working prototype ([github.com/abluva/mcp-request-idempotency-reference](https://github.com/abluva/mcp-request-idempotency-reference)) all point the same way. What's thinner is evidence of multiple MCP-native implementations already running in production and independently converging on this design — what exists so far mostly describes the problem and sketches ad hoc fixes, rather than shipped, competing implementations. More implementations — ideally in more than one language, from more than one author — during review would genuinely help close that gap. \ No newline at end of file +This is the weakest part of the case. There's solid evidence the underlying problem is real — independent papers, engineering write-ups, and a working prototype ([github.com/abluva/mcp-request-idempotency-reference](https://github.com/abluva/mcp-request-idempotency-reference)) all point the same way. What's thinner is evidence of multiple MCP-native implementations already running in production and independently converging on this design — what exists so far mostly describes the problem and sketches ad hoc fixes, rather than shipped, competing implementations. More implementations — ideally in more than one language, from more than one author — during review would genuinely help close that gap. From 1fed1c89ea687ae2038cc879f7831b9d5564151d Mon Sep 17 00:00:00 2001 From: devmaha <4321989+devmaha@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:33:51 +0530 Subject: [PATCH 8/9] Reflect explicit MRTR exclusion Matches the corresponding SEP edit: MRTR continuation requests are out of scope for this SEP rather than left ambiguous. No other FAQ answers affected. --- seps/sep-3182-idempotency-faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seps/sep-3182-idempotency-faq.md b/seps/sep-3182-idempotency-faq.md index 402e4157d..450dde924 100644 --- a/seps/sep-3182-idempotency-faq.md +++ b/seps/sep-3182-idempotency-faq.md @@ -46,7 +46,7 @@ It's actually the opposite — that's the argument for standardizing it. This is Yes, and the change actually strengthens the case for it. Capability discovery moves to `server/discover`, which this proposal already reflects. The bigger effect is on deployment: session affinity used to mean a retry had some chance of landing back on the same server instance that handled the original request, which gave an in-memory dedup store a kind of accidental partial protection. Under the stateless core, any instance can handle any request, so that accidental protection is gone — a production server now needs a dedup store that's actually shared across instances, not per-process. The mechanism this SEP defines doesn't change; what changes is that skipping it is riskier than it used to be. **How does this interact with Multi Round-Trip Requests (MRTR)?** -They solve different problems and shouldn't be conflated. An MRTR continuation is the client knowingly resuming an operation the server explicitly said isn't finished yet — reissuing `tools/call` with `inputResponses` and the echoed `requestState`. A retry under this SEP is the client re-sending a request because it doesn't know whether an earlier, identical request already completed. Don't attach the same `idempotencyKey` to an MRTR continuation expecting it to trigger this SEP's deduplication — that's not the scenario it's built for. +They solve different problems and shouldn't be conflated. An MRTR exchange consists of multiple independent `tools/call` requests (SEP-2322). This SEP does **not** define idempotency semantics for MRTR continuation requests carrying `requestState` and `inputResponses`; those requests are intentionally outside the scope of this proposal, and this SEP should not be read as encouraging reuse of a single `idempotencyKey` across an entire MRTR exchange. Whether MRTR continuations should participate in this mechanism — and, if so, what constitutes request equivalence for them — is left to future work. --- From 124ea1caab3b911b3ea57eb66d86e676d3e56efa Mon Sep 17 00:00:00 2001 From: devmaha <4321989+devmaha@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:35:38 +0530 Subject: [PATCH 9/9] Explicitly exclude MRTR continuations from this SEP's scope 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. --- seps/sep-3182-request-idempotency.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seps/sep-3182-request-idempotency.md b/seps/sep-3182-request-idempotency.md index ad4e0e7b7..5311365c7 100644 --- a/seps/sep-3182-request-idempotency.md +++ b/seps/sep-3182-request-idempotency.md @@ -126,7 +126,7 @@ The `tasks/get` / `tasks/update` split introduced by SEP-2663 and the mechanism The 2026-07-28 revision removes protocol-level sessions and session affinity (SEP-2567) and the `initialize` handshake (SEP-2575): any server instance can now handle any request, with no sticky routing to the instance that handled a prior one. This changes the practical stakes of this SEP rather than the mechanism itself. Under the previous session-based model, a retry sent on the same session had some chance of reaching the same server instance that processed the original request, so an implementation could get partial, accidental protection from an in-memory, per-process dedup store. Under the stateless model that accidental protection is gone: a retry is no longer more likely to land on the instance that executed the original call than on any other. A server implementing this SEP in a horizontally-scaled, stateless deployment MUST therefore make its idempotency record store visible to every instance handling that server's traffic — for example, a shared cache or database — rather than process-local memory; a per-instance store does not satisfy the deduplication guarantee this SEP defines once requests are no longer pinned to one instance. This makes the mechanism this SEP proposes more necessary under the stateless core, not less, since the informal protection statelessness removes was never a guarantee to begin with. -Multi Round-Trip Requests (MRTR, SEP-2322) introduce a different notion of a client re-issuing `tools/call`: when a tool returns an `input_required` result, the client gathers the requested inputs and reissues the same call with `inputResponses` and the echoed `requestState`. This is not a retry under this SEP's terms, and implementations should not conflate the two. A retry this SEP addresses is a client re-sending a request because it does not know whether a previous, structurally identical request completed. An MRTR continuation is the client knowingly resuming an operation the server has explicitly said is not yet finished, carrying new information the original request did not have. A client SHOULD NOT attach the same `idempotencyKey` to an MRTR continuation expecting this SEP's deduplication behavior to apply to it; the two mechanisms address different problems and are not designed to compose automatically. Whether — and how — `idempotencyKey` might usefully apply to the sequence of calls in an MRTR exchange is left to future work, since no evidence of a duplicate-execution failure mode specific to MRTR was found during this research. +Multi Round-Trip Requests (MRTR, SEP-2322) introduce a different notion of a client re-issuing `tools/call`: when a tool returns an `input_required` result, the client gathers the requested inputs and reissues the same call with `inputResponses` and the echoed `requestState`. This is not a retry under this SEP's terms, and implementations should not conflate the two. A retry this SEP addresses is a client re-sending a request because it does not know whether a previous, structurally identical request completed. An MRTR continuation is the client knowingly resuming an operation the server has explicitly said is not yet finished, carrying new information the original request did not have. This SEP does not define idempotency semantics for `tools/call` requests carrying MRTR continuation fields such as `requestState` and `inputResponses`; such requests are intentionally outside the scope of this proposal, and the request-equivalence rules defined elsewhere in this SEP should not be read as applying to them. Whether — and how — idempotency should apply to MRTR continuations, including what constitutes equivalence for those requests, is left to future work once implementation experience demonstrates the need. ## Rationale