Skip to content
LaravelBlog

Latest articles

All articles

Revert of stateless MCP elicitation support

## 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()`, and `setInputResponses()` - `InputRequiredException`, the `input_required` result type, and the `@throws` declaration on the `Method` contract - The `ElicitResponse` class (including `accepted()`, `declined()`, `get()`, schema assertions, and validation rules) and the `ElicitationAction` enum - The `inputResponses` and `requestState` params on `JsonRpcRequest`, including `encodeRequestState()` 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()`, and `assertErrorCode()` 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", `use` statements 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: ```php 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: ```php 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: ```php 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` / `inputResponses` transport params no longer exist, so clients must not send them. - Testing helpers like `withClientCapabilities()`, `assertInputRequired()`, `assertElicits()`, and `respond()` are gone — assert on complete tool calls instead. - Expect a reworked elicitation implementation in a future release before adopting any interactive-input patterns.

LaravelBlogBot
LaravelBlogBot

Browse by topic

Most read

  1. 01 laravel/ai v1.2.0: OpenAI Decisions classification, sturdier structured output, and new Anthropic defaults 78 views
  2. 02 laravel/ai: structured output format no longer dropped when provider options set output_config 78 views
  3. 03 Add OpenAI Decisions classification with image attachments 63 views
  4. 04 laravel/framework: Opt-In defaults() Trait for Eloquent Models Lands in 13.35.0 62 views

Get new articles by email

One email each time a new article is published.

Questions and answers

View all FAQs

LaravelBlog is a publication about the Laravel ecosystem. It follows the packages it tracks on GitHub and, for every new release, publishes one article per meaningful pull request plus a summary that explains what changed, why it matters, and where to find it in the code.

Coverage is not fixed. Packages are added from the admin panel, and any public GitHub repository can be tracked. Once a package is enabled its newest release is synced immediately, then every future release is picked up through webhooks or the daily sync.

Articles are written from the release notes, the pull request descriptions, and the code diffs, then published in Markdown with a title, summary, tags, and a Sources list. Every article links back to the release and pull request it is based on, so you can check the original code yourself.

Articles are authored by LaravelBlogBot so they are easy to tell apart from personal posts. The original pull request author is credited as a contributor, with a profile page that lists the releases and articles they helped ship plus a link to their GitHub account.

Each pull request article is published as soon as its release is processed. The release summary is written on the next scheduled sweep, once every qualifying article exists, so a full release usually appears within a few hours of going live.