Serve input_required handlers on 2025-era connections via a legacy fulfilment shim#2381
Open
felixweinberger wants to merge 2 commits into
Open
Serve input_required handlers on 2025-era connections via a legacy fulfilment shim#2381felixweinberger wants to merge 2 commits into
felixweinberger wants to merge 2 commits into
Conversation
…lfilment shim A tools/call, prompts/get, or resources/read handler that returns an input_required result on a 2025-era connection is now served by a default-on shim at the server seam: each embedded request is sent as a real server-to-client request (elicitation/create, sampling/createMessage, roots/list) over the live session, stamped with the originating request id for stream association, and the handler is re-entered with the collected inputResponses until it returns a final result. Handlers are written once in the input_required style and serve both eras; ServerOptions.inputRequired.legacyShim: false restores the previous loud -32603 failure. Round semantics mirror the client auto-fulfilment driver: per-round replaced inputResponses, byte-exact requestState echo with the verify hook running every round against the round's own context, paced requestState-only rounds, shared round-cap accounting (default 8), and elicitation accepted content passed through unvalidated for the handler to check. Legs carry an explicit human-paced timeout (inputRequired.roundTimeoutMs, default 600s) with a live resetTimeoutOnProgress, URL-mode legs synthesize the elicitationId the 2025-11-25 wire requires, and one synthetic progress tick per completed round (progressToken-gated, monotonic above handler-emitted progress) keeps watchdog clients alive. Failures surface per family: isError tool results for tools/call, JSON-RPC errors for prompts/resources. The shim's own capability pre-check reads the per-request resolved view, so capability-less clients and stateless per-request legacy serving get a clean refusal before any wire traffic. ctx.mcpReq.requestState becomes a typed accessor: requestState<T>() returns the verify hook's decoded payload (createRequestStateCodec's verify), the raw wire string without a hook, or undefined. New typed readers for inputResponses ship from the server package: a schema-aware acceptedContent overload, a discriminated inputResponse view, and samplingText.
🦋 Changeset detectedLatest commit: 7d2cbf8 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
The discriminated inputResponse() view and the schema-aware acceptedContent overload carry protocol knowledge (bare response shape discrimination, the documented validation path for unvalidated accepted content) and stay public. Text extraction from a sampling response is a content convenience handlers write themselves — the migration guide now shows it as a one-liner over the discriminated view instead of shipping a samplingText export.
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.
Serve
input_requiredhandlers on 2025-era connections: a default-on legacy fulfilment shim at the server seam converts each embedded request of aninput_requiredreturn into a real server→client request (elicitation/create,sampling/createMessage,roots/list) over the live session and re-enters the handler with the collectedinputResponsesuntil a final result. Handlers are written once in the 2026 style and serve both eras.Motivation and Context
Today a handler that wants to serve both protocol eras has to implement every interactive conversation twice: an awaited push-style arm for 2025-era connections and an
input_requiredstate machine for 2026-07-28 — and the SDK fails loudly (-32603) if the 2026-style return reaches a 2025-era request. This PR makes theinput_requiredform the single way to write interactive handlers:ServerOptions.inputRequired.legacyShim: falserestores the loud failure) mirrors the client auto-fulfilment driver exactly so a handler cannot tell which era fulfilled it: per-round REPLACEDinputResponses, byte-exactrequestStateecho with the configured verify hook running every round against the round's own context, paced requestState-only rounds, and a round cap (inputRequired.maxRounds, default 8) sharing the driver's accounting and message. Elicitation accepted content passes through unvalidated, exactly as the modern driver does, so the handler's recovery path (schema-awareacceptedContent→ re-ask) behaves identically per era.relatedRequestId) and an explicit human-paced timeout (inputRequired.roundTimeoutMs, default 600s) plus a liveresetTimeoutOnProgress(legs carry a progressToken, so a client reporting progress mid-leg extends the leg). URL-mode legs synthesize theelicitationIdthe 2025-11-25 wire requires (CSPRNG-backed). One synthetic progress tick per completed round (only when the originating request carried a progressToken) stays monotonic above any handler-emitted progress on the same token.enforceStrictCapabilities) reads the per-request resolved capability view: capability-less clients and stateless per-request legacy serving (no initialize, no server→client channel) get a clean typed refusal before any wire traffic — never a hang. Failures surface per family:isErrortool results fortools/call, JSON-RPC errors forprompts/get/resources/read; server bugs (malformed input-required results) fail loudly on both eras.requestState:ctx.mcpReq.requestStatebecomes an accessor —ctx.mcpReq.requestState<T>()returns the verify hook's decoded payload (createRequestStateCodec.verify), the raw wire string with no hook, orundefined. The hook's resolved value is now load-bearing (documented); codec users read verified state with no second decode call.inputResponsesreaders from@modelcontextprotocol/server: a schema-awareacceptedContent(responses, key, schema)overload, a discriminatedinputResponse(responses, key)view (missing | elicit | sampling | roots), andsamplingText(responses, key).How Has This Been Tested?
legacyInputRequiredShim.test.ts(26 tests: happy paths across all three embedded kinds incl. concurrent legs, REPLACE/echo semantics, round-cap exhaustion per family, capability gating incl. the bare-elicitation:{}-means-form rule and the stateless refusal, leg failures, decline pass-through, unvalidated-content recovery, leg timeout 600s vs the 60s default and progress-based reset under fake timers, synthetic progress gating + monotonicity, requestState verify-per-round + typed accessor + the frozen-32602, URL-legelicitationIdsynthesis, knob validation) andlegacyShimWriteOnce.test.ts(a multi-phase HMAC-codec write-once tool completing its full elicit → custom-count → sampling conversation on a 2025 session).typescript:mrtr:legacy-shim:write-once-on-2025running on every stateful arm (stdio, in-memory, sessionful Streamable HTTP, legacy SSE) with a realClientanswering the realelicitation/create; a serveStdio entry test; reader unit tests in core-internal.Breaking Changes
ctx.mcpReq.requestState(v2 alpha surface) changes from an optionalstringproperty to an always-present typed accessor: reads becomectx.mcpReq.requestState<string>(). Truthiness no longer means "has state", and a configuredrequestState.verifyhook's resolved value now backs the accessor (verifiers that are not decoders should resolveundefined).input_requiredreturn on a 2025-era request is now fulfilled by the shim instead of failing with-32603. The previous behavior is available viaServerOptions.inputRequired.legacyShim: false. Documented indocs/migration/support-2026-07-28.md(new section "Legacy shim forinput_required") with a changeset for@modelcontextprotocol/serverand@modelcontextprotocol/core-internal.Types of changes
Checklist
Additional context
test/conformance/src/everythingServer.ts) is updated to the accessor read; the conformance baseline is unchanged.legacyShim: falseescape hatch — the default-on fulfilment is the deliberate behavior change this PR makes.examples/mrtrstory on dual eras to demo the shim end-to-end (its stateless-HTTP legacy leg needs the documented refusal handling), and simplifyexamples/elicitation's era branches away.