pub struct IdempotencyKey(/* private fields */);Expand description
The key a side effect is deduplicated by.
Deliberately not Copy, for the reason RequestId is not: it wraps an
Arc<str>, and duplicating a string key is not something anyone means to
do. Two “copies” of one key are the same key, and a function that needed
only to read it should have taken a reference.
Implementations§
Source§impl IdempotencyKey
impl IdempotencyKey
Sourcepub fn from_request_id(id: &RequestId) -> Self
pub fn from_request_id(id: &RequestId) -> Self
The key, as the request id a retrying client repeats.
Renders the id through its own Display, so a numeric id and a string
id that print the same are the same key. That is the JSON-RPC reading:
an id is whatever appears in the id member, and a client that retries
repeats it exactly.
Sourcepub fn for_request(session: &SessionId, id: &RequestId) -> Self
pub fn for_request(session: &SessionId, id: &RequestId) -> Self
The key for one request within one session.
A request id alone is not a key, and the reason is that ids are only
unique per connection: every MCP client numbers its first request 1,
so ("session-a", 7) and ("session-b", 7) are two different calls. A
store keyed on the bare id would answer the second session’s 7 with
the first session’s result — a cross-session data leak wearing the
costume of a duplicate.
The separator is a byte that cannot appear in either half. SessionId
renders as an integer and RequestId is a number or a string, and a
string id is free to contain anything the peer typed, so the join has to
be unambiguous in the general case: session = 1, id = "2:3" and
session = 12, id = "3" both render, and only a separator that the id
cannot contain keeps them apart. A / is the choice because MCP session
ids and the ids a client sends are both opaque, and a / in a client-chosen
id is legal — so this is a delimiter, not an escape, and the two halves
are distinguished by the first separator rather than by a scan.
Sourcepub fn for_method_call(
session: &SessionId,
id: &RequestId,
method: &str,
target: &str,
) -> Self
pub fn for_method_call( session: &SessionId, id: &RequestId, method: &str, target: &str, ) -> Self
The key for a method invocation, which is the form every observable-side-effect handler wants.
Self::for_request keys on the session and the request id alone, and
that is exactly right for retrying the same call: a client whose POST
never got an answer resends the identical request and must not run the
tool twice.
It is wrong as the only form, because a request id is not a promise that
the same id names the same operation. JSON-RPC 2.0 requires the peer to
be able to reuse ids across a sequence of requests, and real clients
do — the conformance suite restarts its counter at 1 per scenario
against a session it never closed. Keyed on the id alone, the second
tools/call of a session is answered with the first one’s recording: ask
test_audio_content as id 1 and test_embedded_resource as id 2 in
separate scenarios sharing a connection, and the second is handed back
an audio block. That is worse than a wrong answer, because the caller
cannot tell it from a correct one — it is a valid CallToolResult
carrying the wrong tool’s output.
Adding the method and the tool name makes the key identify what was
asked as well as who asked and when, so a genuinely repeated
tools/call for the same tool still replays, and a different tool under
a reused id runs. It cannot distinguish a retry of a different-argument
call under the same id, which is a caller violating JSON-RPC in the other
direction; treating that as a fresh call is the answer that loses less.