MCP PHP SDK

InputRequiredShim

FinalYes

Serves a modern-era handler on a handshake-era connection.

Revision 2026-07-28 removed server-initiated requests: a handler that needs something from the user returns an InputRequiredResult naming it, and the client retries the whole call with the answers. The handshake era has no such retry — it has a channel back to the client instead.

This closes that gap from the server side. When a handler returns an input-required result on a handshake-era connection, each embedded request goes out as the real server-to-client request it describes (elicitation/create, sampling/createMessage, roots/list), and the handler is re-entered with the answers under the keys it asked for. A handler written once for the modern era therefore serves both, and cannot tell which fulfilled it.

The cost is that re-entry is re-execution: the handler runs again from the top each round, so it must re-derive its position from what came back rather than from anything it kept. That is already true of the modern era — the client retries the whole call there — so a portable handler is written that way regardless; it is only new for handlers that until now could rely on ClientGateway::elicit() suspending mid-body and keeping their locals. Those keep working untouched: nothing here runs unless a handler returns an ask.

Runs inside the handler's own fiber, so the wait for each answer is the same suspension ClientGateway already uses, driven by the same transport loop.

Tags
author

Christopher Hertel mail@christopher-hertel.de

Table of Contents

Constants

DEFAULT_MAX_ROUNDS  : mixed = 8
Re-entries per originating request. Deliberately below the modern client driver's allowance: this loop holds a live request open.
DEFAULT_ROUND_TIMEOUT  : mixed = 600
Seconds to wait for one answer. Legs are human-paced, so the protocol's 120s default is wrong here.

Properties

$logger  : LoggerInterface
$maxRounds  : int
$roundTimeout  : int

Methods

__construct()  : mixed
fulfill()  : Response<string|int, mixed>|Error
Runs a handler to a result the client can be given.
askOf()  : InputRequiredResult|null
The ask a handler returned, if it returned one.
collect()  : array<string, mixed>
Sends each embedded request and keeps the answer under the key it was asked under.
payloadOf()  : array<string, mixed>
The state the handler sealed last round, verified.
refuseUndeclared()  : Error|null
Refuses an ask the client never said it could answer, the way the modern era does — rather than sending a request that can only come back as an error.

Constants

DEFAULT_MAX_ROUNDS

Re-entries per originating request. Deliberately below the modern client driver's allowance: this loop holds a live request open.

public mixed DEFAULT_MAX_ROUNDS = 8

DEFAULT_ROUND_TIMEOUT

Seconds to wait for one answer. Legs are human-paced, so the protocol's 120s default is wrong here.

public mixed DEFAULT_ROUND_TIMEOUT = 600

Properties

$roundTimeout read-only

private int $roundTimeout = self::DEFAULT_ROUND_TIMEOUT

Methods

__construct()

public __construct([int $maxRounds = self::DEFAULT_MAX_ROUNDS ][, int $roundTimeout = self::DEFAULT_ROUND_TIMEOUT ][, LoggerInterface $logger = new NullLogger() ]) : mixed
Parameters
$maxRounds : int = self::DEFAULT_MAX_ROUNDS
$roundTimeout : int = self::DEFAULT_ROUND_TIMEOUT
$logger : LoggerInterface = new NullLogger()

fulfill()

Runs a handler to a result the client can be given.

public fulfill(Response<string|int, mixed>|Error $result, RequestHandlerInterface<string|int, ResultInterface|array<string, mixed>> $handler, Request $request, SessionInterface $session, RequestStateCodec|null $codec) : Response<string|int, mixed>|Error

Returns whatever the handler returned when it asks for nothing, which is every call that is not multi round-trip.

Parameters
$result : Response<string|int, mixed>|Error

what the handler returned on its first entry

$handler : RequestHandlerInterface<string|int, ResultInterface|array<string, mixed>>

the handler that produced it

$request : Request
$session : SessionInterface
$codec : RequestStateCodec|null
Return values
Response<string|int, mixed>|Error

collect()

Sends each embedded request and keeps the answer under the key it was asked under.

private collect(InputRequiredResult $ask, SessionInterface $session) : array<string, mixed>

Answers are stored as the raw result arrays InputContext parses, so this needs to know nothing about the kinds it is carrying — which is also why an extension's future kind rides through unchanged.

Parameters
$ask : InputRequiredResult
$session : SessionInterface
Return values
array<string, mixed>

payloadOf()

The state the handler sealed last round, verified.

private static payloadOf(InputRequiredResult $ask, RequestStateCodec|null $codec) : array<string, mixed>

Verified rather than trusted even though it never left this process: the handler reads it back through the same accessor either era, so it has to have been through the same check.

Parameters
$ask : InputRequiredResult
$codec : RequestStateCodec|null
Tags
throws
RequestStateException

when a state is present but does not verify

Return values
array<string, mixed>
On this page

Search results