Extend resolver DI to sampling and roots requests#3049
Open
maxisbey wants to merge 2 commits into
Open
Conversation
Resolvers can now return Sample(...) or ListRoots() in addition to Elicit: on 2026-07-28 sessions the request batches into the multi-round-trip InputRequiredResult flow, on 2025-11-25 it goes over the standalone back-channel request. One rendering produces the identical wire request on both transports, and marker-routed legacy sends bypass the deprecated session wrappers so no SEP-2577 warning fires for the compatibility path. Sampling and roots results are persisted in request_state like elicited answers (the client pays for an LLM call once per tool call, not once per round), pinned to the exact rendered request. Because the response union cannot always discriminate the two sampling result shapes, an answer is validated against the marker's expected model rather than trusting the union member. The elicitation-only capability check generalizes to a per-kind gate applied before sending on either transport: sampling, roots, and elicitation - including sampling.tools when the request carries tools, reported in full in the -32021 requiredCapabilities payload. This also gates the previously unchecked 2025 elicitation leg (documented in the migration guide). Client gains sampling_capabilities so sampling sub-capabilities like tools support can be declared alongside sampling_callback.
Contributor
📚 Documentation preview
|
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.
Resolvers can now return
Sample(...)orListRoots()in addition toElicit, covering all three request kinds the multi-round-trip flow allows (SEP-2322): elicitation, sampling, and roots.Motivation and Context
The resolver dependency-injection API (#2969, #2986) only supported asking the user via
Elicit. The multi-round-tripinputRequestsunion is a closed set of three request kinds, and the client half already dispatches all three to the standard callbacks — this fills in the server half so a tool dependency can also sample the client's LLM or fetch its roots:Design notes:
_render_requestproduces the wire request used both as the 2026-07-28inputRequestsentry and as the pre-2026 back-channel payload, so the two transports send identical shapes by construction. The legacy legs for sampling/roots callsend_requestdirectly rather than the@deprecatedsession wrappers: the deprecated thing (SEP-2577) is the standalone feature, and marker-routed compatibility sends shouldn't warn — directctx.session.create_message()still does.CreateMessageResult,CreateMessageResultWithToolswhen the request carries tools,ListRootsResult).requestStatelike elicited answers, pinned to the exact rendered request, so the client pays for an LLM call once per tool call rather than once per retry round. The state encoding is unchanged and byte-compatible with in-flight state.InputResponsesunion cannot discriminate a no-tool-use answer to a tools request (a single content block parses as the plain result shape), so trusting the union member would reject spec-valid responses.elicitationform,sampling— plussampling.toolswhen the request carriestools/tool_choice— orroots) and refuses with-32021 MISSING_REQUIRED_CLIENT_CAPABILITYcarrying the fullrequiredCapabilitiespayload. On 2026-07-28 an absent declaration is meaningful by contract (capabilities arrive per-request, and servers must not infer them from prior requests), so the gate fires; on pre-2026 sessions it applies only when the handshake's declaration is visible - on a stateless server the declaration is merely invisible, and that session has no back-channel to receive the request anyway, so the send path's no-back-channel error remains the truthful one.Clientgainssampling_capabilitiesso sampling sub-capabilities like tools support can finally be declared from the high-level client (ClientSessionalready accepted it).How Has This Been Tested?
Beyond the unit/e2e suite (all three kinds batched in one round, cross-kind resolver chains over three rounds, capability refusals on both eras, state restore, no-tool-use answers to tools requests), the branch was exercised as a real application: an
MCPServerprocess on streamable HTTP with a separate client process — 2026-07-28 auto negotiation with elicit+sample+roots fulfilled through the retry loop, 2025-11-25 legacy over the back-channel withMCPDeprecationWarningpromoted to error (none fired), a stdio subprocess negotiating 2026-07-28, and live-32021probes verifying the payloads and that the session stays usable after a refusal.Wire compatibility: no new wire shapes — the conformance everything-server already exercises all three embedded kinds, and the suite is unaffected. One gap worth noting: the conformance suite covers
-32021mechanics elsewhere but has no dedicated scenario for the "server MUST NOT send aninputRequestsentry the client has not declared support for" egress rule specifically; happy to raise that on the conformance repo.Breaking Changes
Resolver-routed requests now enforce the capability egress rule on pre-2026 sessions too: a 2025-11-25 client that answered elicitations without declaring the
elicitationcapability now gets-32021instead of being asked. Documented indocs/migration.md(declare the capability — the SDK client does this automatically when the callback is set — or drop the asking dependency). Directctx.elicit()/ctx.session.*calls outside resolvers are unaffected.Types of changes
Checklist
Additional context
Out of scope, noted for follow-ups:
ClientSessionGroupcannot declare sampling sub-capabilities (the same pre-existing gapClienthad before this PR), and the elicitation legacy leg's validation still lives inelicit_with_validationwhile sampling/roots go throughsend_request— kept as-is to leave the shipped elicitation path untouched.AI Disclaimer