Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions docs/handlers/input-required.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,17 @@ They can return `true` there, but `sample()` and `listRoots()` still fail (see b
## What not to call

`ClientGateway::sample()` and `listRoots()` belong to the handshake era. Revision
`2026-07-28` removed both, so calling one there fails the call with an internal error
(`-32603`). The error happens outside your handler, so a `try`/`catch` around the call
never runs. Take what they gave you from tool arguments, resource URIs or server
configuration instead. `elicit()` and `elicitUrl()` are unaffected: elicitation survived
that revision, as an ask carried in the result.
`2026-07-28` has no server-initiated requests, so calling one there fails the call with an
internal error (`-32603`). The error happens outside your handler, so a `try`/`catch` around
the call never runs. To sample or list roots on that revision,
[return the ask](#returning-the-ask) as an `InputRequiredResult` with a
`CreateSamplingMessageRequest` or `ListRootsRequest`, and read the answer with
`samplingResult()` or `rootsResult()`. `elicit()` and `elicitUrl()` are unaffected: the SDK
turns them into such an ask for you.

Sampling and roots are deprecated since `2026-07-28`
([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)). For new
code, prefer tool arguments, resource URIs or server configuration.

## Which revision called

Expand Down
7 changes: 4 additions & 3 deletions src/Server/Stateless/StatelessProtocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -723,9 +723,10 @@ private function readNotification(mixed $suspended, RequestMeta $meta): ?Notific
if (!$suspended instanceof NotificationSuspension) {
if ($suspended instanceof RequestSuspension) {
// Elicitation never reaches here — it is answered in run(). What
// is left are the kinds this revision removed outright, and no
// multi round-trip shape brings them back.
throw new LogicException('This protocol revision has no server-initiated requests: sampling and roots were removed with it, so take what you need through tool arguments, resource URIs or server configuration instead. Elicitation is the one ask that survived, as a multi round-trip request.');
// is left, sampling and roots, cannot be sent from inside a request
// on this revision, but a handler still gets them answered by
// returning them as an InputRequiredResult.
throw new LogicException(\sprintf('This protocol revision has no server-initiated requests, so "%s" cannot be sent from inside a request. Return it from the handler as an InputRequiredResult instead, and read the answer from RequestContext::getInputContext() when the client retries.', $suspended->request::getMethod()));
}

return null;
Expand Down
5 changes: 3 additions & 2 deletions tests/Integration/SamplingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,10 @@ public function testSamplingIsUnavailableOnTheModernEra(): void

try {
$client->callTool('summarize', ['text' => 'hello']);
$this->fail('Sampling must not be reachable on 2026-07-28, which removed it.');
$this->fail('Sampling must not be sent from inside a request on 2026-07-28.');
} catch (RequestException $e) {
$this->assertStringContainsString('sampling and roots were removed', $e->getMessage());
$this->assertStringContainsString('"sampling/createMessage"', $e->getMessage());
$this->assertStringContainsString('InputRequiredResult', $e->getMessage());
}
}

Expand Down
2 changes: 2 additions & 0 deletions tests/Unit/Server/Stateless/StatelessProtocolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,8 @@ public function testServerInitiatedRequestIsRefused(): void

$this->assertSame(500, $result->httpStatus);
$this->assertStringContainsString('no server-initiated requests', $body['error']['message']);
$this->assertStringContainsString('"roots/list"', $body['error']['message']);
$this->assertStringContainsString('InputRequiredResult', $body['error']['message']);
}

#[TestDox('a gateway elicitation becomes the ask this revision carries in the result')]
Expand Down
Loading