examples: MRTR dual-path options for 2025-11 client compat#1701
Draft
felixweinberger wants to merge 12 commits intomainfrom
Draft
examples: MRTR dual-path options for 2025-11 client compat#1701felixweinberger wants to merge 12 commits intomainfrom
felixweinberger wants to merge 12 commits intomainfrom
Conversation
Five demo servers registering the same weather-lookup tool, each showing a different approach to the top-left quadrant of the SEP-2322 compatibility matrix: server CAN hold SSE, client is on 2025-11, tool code is MRTR-native. Follow-up to #1597 and modelcontextprotocol#2322 comment 4083481545. - shimMrtrCanonical.ts (A): SDK emulates retry loop over SSE - shimAwaitCanonical.ts (B): await-style, exception-based MRTR shim - explicitVersionBranch.ts (C): one handler, version branch inline - dualRegistration.ts (D): two handlers, SDK picks by version - degradeOnly.ts (E): MRTR-only, old clients get an error Exploratory, not intended to merge as-is.
|
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
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.
The client side of the dual-path story (new SDK, old server). Unlike the server options A-E, there's only one sensible shape: the elicitation handler signature is identical whether the request arrives via SSE push or embedded in IncompleteResult, so the SDK routes to one user-supplied function from both paths. Lives in examples/client/src/mrtr-dual-path/ (parallel dir) because the client package isn't a dep of examples/server. README in the server dir points to it.
sdkLib.ts (~110 lines): type shims, IncompleteResult parsing, the retry loop, withMrtr() helper. Stand-in for what the SDK ships. clientDualPath.ts (~55 lines): just the app-developer surface. handleElicitation + one withMrtr() call + one callTool() call. The point being: the app code is identical to today.
9 tasks
Both rows of the quadrant collapse to E: horizontally scaled servers get it by necessity, SSE-capable servers get it by default. The server works for old clients either way - version negotiation succeeds, tools/list complete, non-eliciting tools unaffected. Only elicitation is unavailable. A/C/D reframed as opt-in exceptions for servers that choose to carry SSE infra through the transition.
Tool author chooses: proceed with a default (unit preference is nice-to-have, default to metric) or error (confirmation is essential, tell them to upgrade). Both are valid E; the SDK just surfaces 'elicitation unavailable' and the tool decides. The weather demo now defaults and returns a real result for old clients. The error path is in a comment block for tools where the elicitation is blocking.
F and G address a different axis from A-E. A-E are about dual-path (old client vs new). F and G are about the MRTR footgun: code above the inputResponses guard runs on every retry, so a DB write there executes N times for N-round elicitation. F (ctx.once): idempotency guard inside the monolithic handler. Opt-in, one line per side-effect. Makes safe code visually distinct from unsafe code; doesn't eliminate the footgun, makes it reviewable. G (ToolBuilder): Marcelo's step decomposition. incompleteStep may return IncompleteResult or data; endStep runs exactly once when all steps complete. No 'above the guard' zone because the SDK's step-tracking is the guard. Boilerplate: two function defs + .build() to replace the 3-line check. Both depend on requestState integrity - real SDK MUST HMAC-sign the blob or the client can forge step-done markers. Demos use plain base64 for clarity.
4 tasks
Counterpart to python-sdk#2322's linear.py. The Option B footgun was: await elicit() LOOKS like a suspension but is actually a re-entry point. H fixes that by making it a REAL suspension — the Promise chain is held in a ContinuationStore across MRTR rounds, keyed by request_state. Mechanism: Round 1 spawns the handler as a detached Promise; elicit() sends IncompleteResult through a channel and parks on recv. Round 2's retry resolves the channel; the handler continues from where it stopped. No re-entry, no double-execution, zero migration from SSE-era code. Trade-off: server holds the frame in memory between rounds. Client sees pure MRTR (no SSE), but server is stateful within a tool call. Horizontal scale needs sticky routing on the request_state token. README gains a 'Recommended tiers' table: H = easy/default (most servers), E+F/G = stateless/advanced (lambda, ephemeral workers), A/C/D = transition compat (opt-in SSE), B = don't ship.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #1597 and modelcontextprotocol#2322 (comment). Counterpart to python-sdk#2322. Same weather-lookup tool throughout so the diff between files is the argument.
Recommended tiers
ContinuationStore)request_stateH is the "keep
await" option done safely — SSE-era ergonomics, MRTR wire protocol, zero migration, zero footgun. The price is server-side state (continuation frame in memory), so horizontal scale needs sticky routing. If your deployment can't do that (lambda, truly ephemeral workers), drop to the stateless tier: write guard-first handlers (E) and usectx.once(F) orToolBuilder(G) to keep side-effects safe.What to look at
optionA–optionEclientDualPath.ts+sdkLib.tsoptionF,optionG,optionHOptions
await elicit()IncompleteResultctx.onceonce()guard in requestState.build()await ctx.elicit()All eight present identical wire behaviour to each client version — the server's internal choice doesn't leak. That's the cleanest argument against per-feature
-mrtrcapability flags.Client side: new client → old server
clientDualPath.ts— what the app developer writes. ~55 lines: onehandleElicitation, onewithMrtrregistration, onecallTool. Delta from today: zero.sdkLib.ts— what the SDK would ship. Retry loop,IncompleteResultparsing,mrtrOnlyopt-out.Motivation and Context
The comment thread landed on: bottom-left (MRTR-only server + old client) is "E by necessity." Top-left is "E by default, A/C/D if you opt in."
F/G/H respond to SDK-WG feedback that the naive MRTR handler is de-facto GOTO — re-entry jumps to the top, side-effects above the guard are a landmine. F is opt-in mitigation, G is structural decomposition, H is genuine suspension via ContinuationStore (no re-entry at all, but server-side stateful).
How Has This Been Tested?
Typecheck + lint clean. Server demos runnable against Inspector with
DEMO_PROTOCOL_VERSION=2025-11/2026-06.IncompleteResultsmuggled through as JSON text block — same hack as #1597.Breaking Changes
None. Demo-only.
Types of changes
Checklist
Additional context
Exploratory — not intended to merge as-is. The
shims.tsmachinery (sseRetryShim,MrtrCtx.once,ToolBuilder,ContinuationStore,linearMrtr) and clientsdkLib.tssketch what each option needs from the SDK.