Revert of stateless MCP elicitation support
Contributed by pushpak1300
Part of the mcp v1.0.2 release
What changed
Pull request #356 reverts #328, "Add stateless MCP elicitation support," removing the entire stateless elicitation subsystem from the package. The stateless elicitation feature let a tool, prompt, or resource handler pause mid-request and ask the connected MCP client for more input via $request->ask(), carrying answers and arbitrary state across separate JSON-RPC requests by encrypting a requestState blob into the response (with a one-hour TTL) instead of relying on a server-side session.
This revert deletes all of it:
Request::ask(),remember(),canAsk(),clientSupports(),inputResponses(),state(),shareStateWith(), andsetInputResponses()InputRequiredException, theinput_requiredresult type, and the@throwsdeclaration on theMethodcontract- The
ElicitResponseclass (includingaccepted(),declined(),get(), schema assertions, and validation rules) and theElicitationActionenum - The
inputResponsesandrequestStateparams onJsonRpcRequest, includingencodeRequestState()and its decryption and scope-integrity checks ToolSearch's special handling for tools that request user input- Testing helpers:
withClientCapabilities(),assertInputRequired(),assertElicits(),respond(),respondWith(),decline(),cancel(), andassertErrorCode()
As housekeeping, phpunit.xml.dist no longer injects an APP_KEY, since nothing in the test suite encrypts request state anymore.
Why it was pulled back
Stateless elicitation was an ambitious piece of machinery. It involved client capability negotiation (elicitation.form, sampling, roots, and friends), deterministically hashed elicitation keys, encrypted scope-bound state passed back and forth with the client, and exception-driven control flow bubbling through runMethodHandle() and the response concerns. In short, it touched nearly every layer of the server request lifecycle.
The revert message doesn't include a detailed rationale, but the breadth of the removal signals that the team wants this feature redesigned before it stabilizes. Practically, the package returns to a simpler, synchronous request/response model: a tool's inputs come entirely from its declared schema and arguments, and every response is reported with resultType: 'complete'.
Upgrade impact
How this affects you depends on your path from v1.0.1:
- Upgrading directly from v1.0.1: there is nothing to migrate. None of these APIs existed in v1.0.1, so the package surface you knew is restored.
- Coming from the release that shipped #328: this is a breaking change. Calls to
$request->ask()will fail with "Call to undefined method",usestatements for the removed classes will be fatal errors, and tests using the removed assertions will error out.
If you implemented the Method contract yourself or constructed TestResponse manually, note that its constructor no longer accepts the $server and $request arguments.
Updating your code
A tool written against the stateless elicitation API looked like this:
class BookingTool extends Tool
{
public function handle(Request $request): Response
{
$answer = $request->ask(
'Which email should receive the booking confirmation?',
fn (JsonSchema $schema) => $schema->string('email')->required(),
);
if (! $answer->accepted()) {
return Response::text('Booking cancelled.');
}
return Response::text("Confirmation sent to {$answer->get('email')}.");
}
}
After the revert, collect everything up front through the tool's schema instead:
use Laravel\Mcp\Support\JsonSchema;
class BookingTool extends Tool
{
public function schema(): JsonSchema
{
return JsonSchema::object()
->string('email', 'Email address for the confirmation.')
->required();
}
public function handle(Request $request): Response
{
$email = $request->string('email');
return Response::text("Confirmation sent to {$email}.");
}
}
Updating your tests
The multi-step elicit-then-respond test flow is gone. Tests that previously chained assertElicits() and respond() should simply invoke the tool with complete arguments:
it('sends the confirmation', function () {
BookingServer::tool(BookingTool::class, ['email' => '[email protected]'])
->assertOk()
->assertSee('Confirmation sent to [email protected].');
});
If you used assertErrorCode() for general error assertions, replace it with direct assertions on the JSON-RPC error payload.
Takeaways
- #356 fully reverts #328; all stateless elicitation APIs (
ask(),remember(),InputRequiredException,ElicitResponse,ElicitationAction) are removed. - Upgrading straight from v1.0.1 is a no-op for this change; upgraders who adopted #328's APIs must remove that code.
- The encrypted
requestState/inputResponsestransport params no longer exist, so clients must not send them. - Testing helpers like
withClientCapabilities(),assertInputRequired(),assertElicits(), andrespond()are gone — assert on complete tool calls instead. - Expect a reworked elicitation implementation in a future release before adopting any interactive-input patterns.