From 84f46ac609e9163e012cd050fcb96e7294dfea91 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Sun, 18 Oct 2026 14:58:24 +0200 Subject: [PATCH 1/2] [Server] Point a refused gateway request at InputRequiredResult --- src/Server/Stateless/StatelessProtocol.php | 7 ++++--- tests/Unit/Server/Stateless/StatelessProtocolTest.php | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Server/Stateless/StatelessProtocol.php b/src/Server/Stateless/StatelessProtocol.php index a8c49683..53e4c3b4 100644 --- a/src/Server/Stateless/StatelessProtocol.php +++ b/src/Server/Stateless/StatelessProtocol.php @@ -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; diff --git a/tests/Unit/Server/Stateless/StatelessProtocolTest.php b/tests/Unit/Server/Stateless/StatelessProtocolTest.php index 3e2c9d26..5938667c 100644 --- a/tests/Unit/Server/Stateless/StatelessProtocolTest.php +++ b/tests/Unit/Server/Stateless/StatelessProtocolTest.php @@ -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')] From e3fbbe6b8fbfd233a76f24fa1c50f2fd3986795b Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Sun, 18 Oct 2026 15:11:32 +0200 Subject: [PATCH 2/2] Align sampling test and docs with the new refusal message --- docs/handlers/input-required.md | 16 +++++++++++----- tests/Integration/SamplingTest.php | 5 +++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/handlers/input-required.md b/docs/handlers/input-required.md index 10bf7857..5139e936 100644 --- a/docs/handlers/input-required.md +++ b/docs/handlers/input-required.md @@ -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 diff --git a/tests/Integration/SamplingTest.php b/tests/Integration/SamplingTest.php index dc48c8ce..5dc917f8 100644 --- a/tests/Integration/SamplingTest.php +++ b/tests/Integration/SamplingTest.php @@ -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()); } }