Skip to content

Commit 8f52db6

Browse files
doc: client-side invisibility + sseRetryShim infra constraint
All five options present identical wire behaviour per client version; the server's internal choice doesn't leak. That's the cleanest argument against per-feature -mrtr capability flags. Also sharpens the sseRetryShim warning: it only works on SSE-capable infra, and that constraint lives nowhere near the tool registration.
1 parent f9fc447 commit 8f52db6

2 files changed

Lines changed: 19 additions & 3 deletions

File tree

examples/server/src/mrtr-dual-path/README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,17 @@ Bottom-left is discounted: no amount of SDK work fills it when the server infra
2727
"Hidden re-entry" = the handler function is invoked more than once for a single logical tool call, and the author can't tell from the source text. A is safe because MRTR-native code has the re-entry guard (`if (!prefs) return`) visible in the source even though the _loop_ is
2828
hidden. B is unsafe because `await elicit()` looks like a suspension point but is actually a re-entry point on MRTR sessions — see the `auditLog` landmine in that file.
2929

30+
## Client impact
31+
32+
None. All five options present identical wire behaviour to each client version. A 2025-11 client sees either a standard `elicitation/create` over SSE (A/B/C/D) or a `CallToolResult` with `isError: true` (E) — both vanilla 2025-11 shapes. A 2026-06 client sees `IncompleteResult`
33+
in every case. The server's internal choice doesn't leak. This is the cleanest argument against per-feature `-mrtr` capability flags: there's nothing for them to signal, because the client's behaviour is already fully determined by `protocolVersion` plus the existing
34+
`elicitation`/`sampling` capabilities.
35+
3036
## Trade-offs
3137

32-
**A vs E** is the core tension. Same author-facing code (MRTR-native), the only difference is whether old clients get served. A requires shipping and maintaining `sseRetryShim` in the SDK; E requires shipping nothing. If elicitation-using tools are rare and old clients upgrade on
33-
a reasonable timeline, E's cost (a few tools error for a few months) is lower than A's cost (permanent SDK machinery).
38+
**A vs E** is the core tension. Same author-facing code (MRTR-native), the only difference is whether old clients get served. A requires shipping and maintaining `sseRetryShim` in the SDK; E requires shipping nothing. A also carries a deployment-time hazard E doesn't: the shim
39+
calls real SSE under the hood, so if the SDK ships it and someone uses it on MRTR-only infra, it fails at runtime when an old client connects — a constraint that lives nowhere near the tool code. E fails predictably (same error every time, from the first test); A fails only when
40+
old client + wrong infra coincide.
3441

3542
**B** is the zero-migration option. Every existing `await ctx.elicitInput()` handler keeps working. The hidden re-entry on MRTR sessions is the price: a handler that does anything non-idempotent above the await is broken, and nothing warns you. Only safe if you can enforce "no
3643
side effects before await" as a lint rule, which is hard in practice.

examples/server/src/mrtr-dual-path/shims.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,16 @@ export type MrtrHandler<TArgs> = (args: TArgs, mrtr: MrtrParams, ctx: ServerCont
113113
*
114114
* This is the "⚠️ clunky but possible" shim from the comment's matrix. The
115115
* tool author doesn't see the loop; they write MRTR-native code and it
116-
* transparently works for old clients too (if server infra holds SSE).
116+
* transparently works for old clients too.
117+
*
118+
* This is only valid on server infra that can actually hold SSE — the
119+
* `ctx.mcpReq.elicitInput()` call below is a real SSE round-trip. On a
120+
* horizontally-scaled deployment that can't (the whole reason to adopt
121+
* MRTR in the first place), this shim fails at runtime when an old client
122+
* connects — the elicit goes out on a stream the LB has already dropped,
123+
* or was never held open. Nothing at registration time catches that; it's
124+
* a deployment-time constraint living far from the tool code. If that's
125+
* the deployment, use option E instead.
117126
*
118127
* Hidden cost: the handler is silently re-invoked. The MRTR shape makes that
119128
* safe *by construction* (re-entry point is explicit — the `if (!prefs)`

0 commit comments

Comments
 (0)