Skip to content

Extend resolver DI to sampling and roots requests#3049

Open
maxisbey wants to merge 4 commits into
mainfrom
resolve-server-requests
Open

Extend resolver DI to sampling and roots requests#3049
maxisbey wants to merge 4 commits into
mainfrom
resolve-server-requests

Conversation

@maxisbey

@maxisbey maxisbey commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Resolvers can now return Sample(...) or ListRoots() in addition to Elicit, 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-trip inputRequests union 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:

def suggest_title(genre: str) -> Sample:
    prompt = f"Suggest one {genre} book title."
    return Sample([SamplingMessage(role="user", content=TextContent(type="text", text=prompt))], max_tokens=50)

@mcp.tool()
async def recommend_book(genre: str, suggestion: Annotated[CreateMessageResult, Resolve(suggest_title)]) -> str:
    ...

Design notes:

  • One rendering, both eras. A single _render_request produces the wire request used both as the 2026-07-28 inputRequests entry and as the pre-2026 back-channel payload, so the two transports send identical shapes by construction. The legacy legs for sampling/roots call send_request directly rather than the @deprecated session wrappers: the deprecated thing (SEP-2577) is the standalone feature, and marker-routed compatibility sends shouldn't warn — direct ctx.session.create_message() still does.
  • No decline arm. Only elicitation has an accept/decline/cancel union; a client refuses sampling/roots by erroring. Consumers annotate the result type directly (CreateMessageResult, CreateMessageResultWithTools when the request carries tools, ListRootsResult).
  • Results persist across rounds. Sampling/roots results ride requestState like 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.
  • Answers are validated against the expected model, not the response union. The InputResponses union 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.
  • Per-kind capability gate. The existing elicitation-only check generalizes: before sending any of the three kinds, on either transport, the server verifies the client declared the matching capability (elicitation form, sampling — plus sampling.tools when the request carries tools/tool_choice — or roots) and refuses with -32021 MISSING_REQUIRED_CLIENT_CAPABILITY carrying the full requiredCapabilities payload. 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 bare notifications/initialized, while a session that cannot carry a server-initiated request at all keeps failing with the usual no-back-channel error.
  • Client gains sampling_capabilities so sampling sub-capabilities like tools support can finally be declared from the high-level client (ClientSession already 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 MCPServer process 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 with MCPDeprecationWarning promoted to error (none fired), a stdio subprocess negotiating 2026-07-28, and live -32021 probes 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 -32021 mechanics elsewhere but has no dedicated scenario for the "server MUST NOT send an inputRequests entry 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 elicitation capability now gets -32021 instead of being asked. Documented in docs/migration.md (declare the capability — the SDK client does this automatically when the callback is set — or drop the asking dependency). Direct ctx.elicit() / ctx.session.* calls outside resolvers are unaffected.

Types of changes

  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Out of scope, noted for follow-ups: ClientSessionGroup cannot declare sampling sub-capabilities (the same pre-existing gap Client had before this PR), and the elicitation legacy leg's validation still lives in elicit_with_validation while sampling/roots go through send_request — kept as-is to leave the shipped elicitation path untouched.

AI Disclaimer

@maxisbey maxisbey marked this pull request as ready for review July 1, 2026 22:47
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

📚 Documentation preview

Preview https://pr-3049.mcp-python-docs.pages.dev
Deployment https://3f234dcb.mcp-python-docs.pages.dev
Commit d8fe463
Triggered by @maxisbey
Updated 2026-07-02 15:12:08 UTC

maxisbey added 2 commits July 2, 2026 14:04
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.
@maxisbey maxisbey force-pushed the resolve-server-requests branch from 1a12981 to bc3b145 Compare July 2, 2026 14:04
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.
Comment on lines +713 to +719
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 _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.

  1. Server registers _ask_with_tool_choice-style resolver: Sample([...], max_tokens=16, tool_choice=ToolChoice(mode="none")), consumed by a tool.
  2. Client connects with sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability()) — required, because _require_capability sees tool_choice is not Nonewants_tools=True and would otherwise refuse with -32021.
  3. First tools/call returns an InputRequiredResult containing the CreateMessageRequest with toolChoice.
  4. The client's sampler answers with CreateMessageResultWithTools(role="assistant", content=[TextContent(...)], model="m") — permitted by SamplingFnT's return type and valid against the wire schema (array content is unconditionally allowed).
  5. On the retry round _fulfil calls _result_type(marker), which returns plain CreateMessageResult because tools is None. model_validate on the array-content wire dict fails (the SDK model's content is a single block), and the call dies with ToolError '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.

Comment on lines +681 to +689
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Sample request 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 rides request_state for 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_context values 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`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Sample request 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 rides request_state for 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_context values 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`).

Comment thread docs/migration.md
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`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
enforce the spec's egress rule: an undeclared capability (form `elicitation`,
enforce the spec's egress rule: an undeclared capability (`elicitation`,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant