Extend resolver DI to sampling and roots requests#3049
Conversation
📚 Documentation preview
|
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.
1a12981 to
bc3b145
Compare
A short page for the two deprecated ask-the-client features: the Sample/ListRoots resolver way with tested snippets, the capability gate, and a warning box carrying the SEP-2577 deprecation scope (functional for at least twelve months before eligibility for removal, with the spec's suggested migrations). Also reworks dash-heavy sentences in this branch's earlier doc additions into plainer structure.
| def _result_type( | ||
| marker: Sample | ListRoots, | ||
| ) -> type[CreateMessageResult] | type[CreateMessageResultWithTools] | type[ListRootsResult]: | ||
| """The result model a `Sample`/`ListRoots` response must validate against.""" | ||
| if isinstance(marker, ListRoots): | ||
| return ListRootsResult | ||
| return CreateMessageResult if marker.params.tools is None else CreateMessageResultWithTools |
There was a problem hiding this comment.
🟡 _result_type picks the answer's validation model based only on tools is None, while _require_capability treats a Sample with tool_choice (but no tools) as a tools-mode request. Such a marker is therefore gated as requiring sampling.tools, yet a tools-capable client answering with array content (allowed by the wire schema and SamplingFnT) gets rejected with ToolError 'received a response of the wrong kind' (or a raw ValidationError on the legacy leg). Aligning _result_type with the same wants_tools condition (tools is not None or tool_choice is not None) would fix it.
Extended reasoning...
The inconsistency. _require_capability (resolve.py:683) computes wants_tools = marker.params.tools is not None or marker.params.tool_choice is not None — matching the spec (ToolChoice: the client MUST error if received without sampling.tools) and the existing validate_sampling_tools helper. But _result_type (resolve.py:719) picks the model the client's answer is validated against with only marker.params.tools is None. So a Sample(tool_choice=..., tools=None) — a shape the SDK explicitly supports and constructs in this PR's own tests (_ask_with_tool_choice in tests/server/mcpserver/test_resolve.py) — is gated as a tools-mode request but its answer is validated against the plain single-content CreateMessageResult.
How it manifests. A client that declares sampling.tools receives a CreateMessageRequest carrying toolChoice. SamplingFnT explicitly allows returning CreateMessageResultWithTools, whose content may be a list of blocks, and both the 2025-11-25 and 2026-07-28 wire schemas for CreateMessageResult allow array content unconditionally — so a schema-valid answer can carry a content list. On the 2026 leg _fulfil (resolve.py:604-612) re-validates that answer against _result_type(marker) == CreateMessageResult, whose SDK model has a single SamplingContent content field (no list arm), and raises ToolError "Resolver ... received a response of the wrong kind". On the pre-2026 leg send_request(_render_request(marker), _result_type(marker)) raises a raw pydantic ValidationError. Either way a spec-valid response to the request the server itself sent is rejected — the exact failure mode the PR's design note says the against-the-expected-model validation strategy was chosen to avoid.
Step-by-step proof.
- Server registers
_ask_with_tool_choice-style resolver:Sample([...], max_tokens=16, tool_choice=ToolChoice(mode="none")), consumed by a tool. - Client connects with
sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability())— required, because_require_capabilityseestool_choice is not None→wants_tools=Trueand would otherwise refuse with -32021. - First
tools/callreturns anInputRequiredResultcontaining theCreateMessageRequestwithtoolChoice. - The client's sampler answers with
CreateMessageResultWithTools(role="assistant", content=[TextContent(...)], model="m")— permitted bySamplingFnT's return type and valid against the wire schema (array content is unconditionally allowed). - On the retry round
_fulfilcalls_result_type(marker), which returns plainCreateMessageResultbecausetools is None.model_validateon the array-content wire dict fails (the SDK model'scontentis a single block), and the call dies withToolError 'Resolver ... received a response of the wrong kind'— despite every party having followed the spec.
Why it's a nit, and the refutation. A refuting reviewer noted that the deprecated ServerSession.create_message overloads make the same tools-only pick (the no-tools overload allows tool_choice and returns plain CreateMessageResult), and that the docs consistently say "CreateMessageResultWithTools when the request carries tools". That's fair as far as precedent goes, and it's why this isn't blocking: the trigger requires the unusual tool_choice-without-tools shape plus a client that answers such a request in the array shape, and the mainstream tools / no-tools cases are unaffected. But the gate/result-type disagreement is new code introduced together in this PR, and the two conditions sit a few lines apart answering what should be the same question ("is this a tools-mode sampling request?"). The SDK's own test annotates the tool_choice-only consumer as Annotated[CreateMessageResultWithTools, Resolve(_ask_with_tool_choice)], so even the repo's own modeling treats tool_choice-only as tools-mode — while the runtime would inject a plain CreateMessageResult (no content_as_list), contradicting that annotation for a well-behaved single-content answer too.
Fix. Reuse the same condition in _result_type: return CreateMessageResultWithTools when marker.params.tools is not None or marker.params.tool_choice is not None (i.e. share wants_tools). One line, keeps the gate and the answer model in agreement, and CreateMessageResultWithTools still accepts single-content answers so the existing no-tool-use path is unaffected.
| elif isinstance(marker, Sample): | ||
| sampling = capabilities.sampling if capabilities is not None else None | ||
| wants_tools = marker.params.tools is not None or marker.params.tool_choice is not None | ||
| if sampling is not None and (not wants_tools or sampling.tools is not None): | ||
| return | ||
| required = ClientCapabilities( | ||
| sampling=SamplingCapability(tools=SamplingToolsCapability() if wants_tools else None) | ||
| ) | ||
| name = "sampling.tools" if wants_tools else "sampling" |
There was a problem hiding this comment.
🟡 The new per-kind capability gate in _require_capability checks the base sampling capability (and sampling.tools when the request carries tools/tool_choice), but never checks sampling.context, so a resolver returning Sample(..., include_context='thisServer'/'allServers') passes the gate against a client that only declared 'sampling': {} and the request is sent with a parameter the client never declared support for. Since include_context is publicly exposed on Sample and SamplingCapability.context is already part of the SDK's capability model (Connection.check_capability handles it), consider also gating on sampling.context when include_context is non-'none' — or, given SEP-2596 deprecates those values, not exposing include_context on the new Sample marker at all.
Extended reasoning...
What the gap is. The Sample marker publicly exposes include_context (resolve.py:136) and forwards it verbatim into CreateMessageRequestParams (resolve.py:149). The egress gate this PR introduces, _require_capability (resolve.py:681-689), checks the base sampling capability and — when the request carries tools/tool_choice — the sampling.tools sub-capability, but never looks at marker.params.include_context. SamplingCapability.context is defined in mcp-types as “Present if the client supports non-none values for includeContext parameter,” and the SDK already treats it as a checkable sub-capability (Connection.check_capability handles capability.sampling.context at connection.py:387), so the same declare-before-use semantics apply to it as to sampling.tools.\n\nConcrete walk-through. A resolver returns Sample(messages, max_tokens=50, include_context='allServers'). The client is an SDK Client with sampling_callback set and no sampling_capabilities, so it declares only "sampling": {}. In _require_capability, the Sample branch computes wants_tools = False (no tools/tool_choice), the condition sampling is not None and (not wants_tools or ...) is true, and the function returns without raising. The CreateMessageRequest — with includeContext: "allServers" — is then queued into input_requests (2026-07-28) or sent over the back-channel (2025-11-25). Nothing else on either leg inspects include_context: the deprecated ServerSession.create_message() only runs validate_sampling_tools, and the resolver’s legacy leg calls send_request directly. So the server sends a request parameter the client never declared support for — the class of egress the gate exists to prevent, and inconsistent with how the same branch treats the sibling sampling.tools sub-capability three lines above.\n\nWhy this may be intentional — and why it is still worth a decision. One verifier argued the omission mirrors the pre-existing enforcement boundary (the legacy create_message() path also only validates tools, and there has never been a validate_sampling_context), that the PR description explicitly enumerates the gate’s coverage, and that unlike tools (which changes the response shape to CreateMessageResultWithTools, so a non-declaring client genuinely cannot answer correctly), a client lacking sampling.context can still fully answer the request by ignoring the context hint. Those points are fair and are exactly why this is not merge-blocking. But the docs page added in this PR itself notes that non-none include_context values “need a capability almost no client declares,” yet the code neither enforces that capability nor prevents the parameter from being set — the middle ground where the parameter is exposed, documented as capability-gated, but not actually gated.\n\nImpact. Low. Non-none include_context is itself soft-deprecated (SEP-2596), the docs steer users away from it, and the failure mode is a client receiving a parameter it may simply ignore or refuse — not a crash or wrong-shaped result. The gate is just silently incomplete for one of the parameters the new public Sample API exposes.\n\nHow to fix. Either (a) in the Sample branch of _require_capability, also require sampling.context when marker.params.include_context is 'thisServer' or 'allServers', and include SamplingCapability(context=SamplingContextCapability()) in the -32021 requiredCapabilities payload — mirroring the wants_tools handling; or (b) drop include_context from the Sample marker’s constructor entirely, since the values that would need the capability are deprecated and the docs already tell users to leave it alone. Either resolves the inconsistency; a one-line note in the PR description would also suffice if the omission is a deliberate scoping choice.
The legacy capability gate keyed on whether the handshake's declaration was visible, which let a session initialized with a bare notifications/initialized (no declared capabilities, live back-channel) receive ungated requests. Gate on the request-scoped channel's can_send_request instead, the channel these sends actually ride, so any sendable session is checked and a channel-less one keeps failing with its no-back-channel error. Adds ServerSession.can_send_request and a bare-initialized regression test, and corrects docs wording: tool_choice counts toward sampling.tools, the sampling.tools declaration needs the explicit client field, and the v1 claims are scoped to what v1 checked.
There was a problem hiding this comment.
4 issues found across 6 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/mcp/server/mcpserver/resolve.py">
<violation number="1" location="src/mcp/server/mcpserver/resolve.py:125">
P2: `_result_type` selects the validation model based solely on `marker.params.tools is None`, but `_require_capability` gates on `tools is not None or tool_choice is not None`. A `Sample(tool_choice=..., tools=None)` is therefore gated as a tools-mode request (requiring `sampling.tools`), yet the response is validated against plain `CreateMessageResult` which only accepts single content. A client that legitimately answers with array content (valid for `CreateMessageResultWithTools`) gets a `ToolError` or `ValidationError`. Aligning `_result_type` with the same `wants_tools` condition (`tools is not None or tool_choice is not None`) would keep the gate and the answer model in agreement.</violation>
</file>
<file name="docs/handlers/dependencies.md">
<violation number="1" location="docs/handlers/dependencies.md:145">
P3: This sentence reads as a typo (`form elicitation`) in the capability list, which makes the migration guidance less clear at the exact point users check required declarations. Replacing `form` with `for` keeps the capability rule unambiguous.</violation>
<violation number="2" location="docs/handlers/dependencies.md:145">
P2: This sentence reads as if missing client capability always yields `-32021`, but the rest of this change set documents that non-sendable pre-2026 sessions still fail with the no-back-channel error first. Adding the same qualifier here would keep the dependency docs consistent and avoid misleading migration expectations for legacy transports.</violation>
</file>
<file name="docs/migration.md">
<violation number="1" location="docs/migration.md:48">
P3: There’s a typo in the capability list (`form �elicitation�`) that makes the migration guidance harder to read and easy to miscopy. Rewording to list the capability names directly keeps the migration note clear.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| """A resolver's request to sample the client's LLM via `sampling/createMessage`. | ||
|
|
||
| The framework injects a `CreateMessageResult` (`CreateMessageResultWithTools` when `tools` are | ||
| given); requires the `sampling` capability (`sampling.tools` when `tools` or `tool_choice` are given). On |
There was a problem hiding this comment.
P2: _result_type selects the validation model based solely on marker.params.tools is None, but _require_capability gates on tools is not None or tool_choice is not None. A Sample(tool_choice=..., tools=None) is therefore gated as a tools-mode request (requiring sampling.tools), yet the response is validated against plain CreateMessageResult which only accepts single content. A client that legitimately answers with array content (valid for CreateMessageResultWithTools) gets a ToolError or ValidationError. Aligning _result_type with the same wants_tools condition (tools is not None or tool_choice is not None) would keep the gate and the answer model in agreement.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mcp/server/mcpserver/resolve.py, line 125:
<comment>`_result_type` selects the validation model based solely on `marker.params.tools is None`, but `_require_capability` gates on `tools is not None or tool_choice is not None`. A `Sample(tool_choice=..., tools=None)` is therefore gated as a tools-mode request (requiring `sampling.tools`), yet the response is validated against plain `CreateMessageResult` which only accepts single content. A client that legitimately answers with array content (valid for `CreateMessageResultWithTools`) gets a `ToolError` or `ValidationError`. Aligning `_result_type` with the same `wants_tools` condition (`tools is not None or tool_choice is not None`) would keep the gate and the answer model in agreement.</comment>
<file context>
@@ -122,7 +122,7 @@ class Sample:
The framework injects a `CreateMessageResult` (`CreateMessageResultWithTools` when `tools` are
- given); requires the `sampling` capability (`sampling.tools` when tools are given). On
+ given); requires the `sampling` capability (`sampling.tools` when `tools` or `tool_choice` are given). On
>= 2026-07-28 the request must render identically across retry rounds, and the sampled result
rides `request_state` on every later round. `include_context` other than "none" is deprecated in the draft spec.
</file context>
| --8<-- "docs_src/dependencies/tutorial004.py" | ||
| ``` | ||
|
|
||
| * The framework routes these exactly like `Elicit`: inside the multi-round-trip `tools/call` on **2026-07-28**, over the standalone server->client request on **2025-11-25**. An undeclared capability refuses the call with a `-32021` protocol error (`sampling`, `roots`, form `elicitation`; `sampling.tools` when the request carries `tools` or `tool_choice`). |
There was a problem hiding this comment.
P2: This sentence reads as if missing client capability always yields -32021, but the rest of this change set documents that non-sendable pre-2026 sessions still fail with the no-back-channel error first. Adding the same qualifier here would keep the dependency docs consistent and avoid misleading migration expectations for legacy transports.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/handlers/dependencies.md, line 145:
<comment>This sentence reads as if missing client capability always yields `-32021`, but the rest of this change set documents that non-sendable pre-2026 sessions still fail with the no-back-channel error first. Adding the same qualifier here would keep the dependency docs consistent and avoid misleading migration expectations for legacy transports.</comment>
<file context>
@@ -142,7 +142,7 @@ Elicitation is one of the three questions a resolver can ask, and the multi-roun
-* The framework routes these exactly like Elicit: inside the multi-round-trip tools/call on 2026-07-28, over the standalone server->client request on 2025-11-25. On either transport it refuses with a -32021 protocol error when the client never declared the matching capability (sampling, roots, elicitation; sampling.tools when the request carries tools).
+* The framework routes these exactly like Elicit: inside the multi-round-trip tools/call on 2026-07-28, over the standalone server->client request on 2025-11-25. An undeclared capability refuses the call with a -32021 protocol error (sampling, roots, form elicitation; sampling.tools when the request carries tools or tool_choice).
- Everything the info box above says about questions applies unchanged: a
Samplerequest is matched to its recorded result by its exact rendering, so build it deterministically from the tool's arguments and earlier answers; the client then pays for the LLM call once per tool call, not once per round. The recorded result ridesrequest_statefor the rest of the call, so a very large completion makes every remaining round-trip heavier. - The standalone sampling and roots features are deprecated at 2026-07-28 (SEP-2577). New servers that need the client's model ask through this carrier; servers that don't should integrate with an LLM provider directly.
include_contextvalues other than"none"are themselves deprecated; avoid them.
</file context>
</details>
```suggestion
* The framework routes these exactly like `Elicit`: inside the multi-round-trip `tools/call` on **2026-07-28**, over the standalone server->client request on **2025-11-25**. On sendable request channels, an undeclared capability refuses the call with a `-32021` protocol error (`sampling`, `roots`, `elicitation`; `sampling.tools` when the request carries `tools` or `tool_choice`); on sessions with no back-channel, the usual no-back-channel error still applies.
| --8<-- "docs_src/dependencies/tutorial004.py" | ||
| ``` | ||
|
|
||
| * The framework routes these exactly like `Elicit`: inside the multi-round-trip `tools/call` on **2026-07-28**, over the standalone server->client request on **2025-11-25**. An undeclared capability refuses the call with a `-32021` protocol error (`sampling`, `roots`, form `elicitation`; `sampling.tools` when the request carries `tools` or `tool_choice`). |
There was a problem hiding this comment.
P3: This sentence reads as a typo (form elicitation) in the capability list, which makes the migration guidance less clear at the exact point users check required declarations. Replacing form with for keeps the capability rule unambiguous.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/handlers/dependencies.md, line 145:
<comment>This sentence reads as a typo (`form elicitation`) in the capability list, which makes the migration guidance less clear at the exact point users check required declarations. Replacing `form` with `for` keeps the capability rule unambiguous.</comment>
<file context>
@@ -142,7 +142,7 @@ Elicitation is one of the three questions a resolver can ask, and the multi-roun
-* The framework routes these exactly like Elicit: inside the multi-round-trip tools/call on 2026-07-28, over the standalone server->client request on 2025-11-25. On either transport it refuses with a -32021 protocol error when the client never declared the matching capability (sampling, roots, elicitation; sampling.tools when the request carries tools).
+* The framework routes these exactly like Elicit: inside the multi-round-trip tools/call on 2026-07-28, over the standalone server->client request on 2025-11-25. An undeclared capability refuses the call with a -32021 protocol error (sampling, roots, form elicitation; sampling.tools when the request carries tools or tool_choice).
- Everything the info box above says about questions applies unchanged: a
Samplerequest is matched to its recorded result by its exact rendering, so build it deterministically from the tool's arguments and earlier answers; the client then pays for the LLM call once per tool call, not once per round. The recorded result ridesrequest_statefor the rest of the call, so a very large completion makes every remaining round-trip heavier. - The standalone sampling and roots features are deprecated at 2026-07-28 (SEP-2577). New servers that need the client's model ask through this carrier; servers that don't should integrate with an LLM provider directly.
include_contextvalues other than"none"are themselves deprecated; avoid them.
</file context>
</details>
```suggestion
* The framework routes these exactly like `Elicit`: inside the multi-round-trip `tools/call` on **2026-07-28**, over the standalone server->client request on **2025-11-25**. An undeclared capability refuses the call with a `-32021` protocol error (`sampling`, `roots`, for `elicitation`; `sampling.tools` when the request carries `tools` or `tool_choice`).
| A v1 server could send elicitation, sampling, and roots requests to clients | ||
| that never declared the matching capability; only tools-bearing sampling was | ||
| checked. In v2 the `Resolve(...)` markers (`Elicit`, `Sample`, `ListRoots`) | ||
| enforce the spec's egress rule: an undeclared capability (form `elicitation`, |
There was a problem hiding this comment.
P3: There’s a typo in the capability list (form �elicitation�) that makes the migration guidance harder to read and easy to miscopy. Rewording to list the capability names directly keeps the migration note clear.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/migration.md, line 48:
<comment>There’s a typo in the capability list (`form �elicitation�`) that makes the migration guidance harder to read and easy to miscopy. Rewording to list the capability names directly keeps the migration note clear.</comment>
<file context>
@@ -42,18 +42,21 @@ dependencies elicit via `Resolve(...)`: the resolver owns that tool's
+A v1 server could send elicitation, sampling, and roots requests to clients
+that never declared the matching capability; only tools-bearing sampling was
+checked. In v2 the `Resolve(...)` markers (`Elicit`, `Sample`, `ListRoots`)
+enforce the spec's egress rule: an undeclared capability (form `elicitation`,
+`sampling`, or `roots`, plus `sampling.tools` when the request carries `tools`
+or `tool_choice`) fails the call with a `-32021`
</file context>
| enforce the spec's egress rule: an undeclared capability (form `elicitation`, | |
| enforce the spec's egress rule: an undeclared capability (`elicitation`, |
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 unconditionally; on pre-2026 sessions the gate applies wherever the request could actually be sent (it reads the request channel's own sendability), which covers sessions initialized with a barenotifications/initialized, while a session that cannot carry a server-initiated request at all keeps failing with the usual no-back-channel error.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