The client can receive requests and notifications from the server when configured with appropriate handlers.
On revision 2026-07-28, a server can't send requests to the client. It returns elicitation, sampling
and roots requests in an input_required result instead, and the client
answers them with the same request handlers. A default client uses this revision when the server speaks
it.
A server that sends sampling or roots requests through its ClientGateway fails on this revision.
To receive those requests, call setProtocolVersion(ProtocolVersion::V2025_11_25) on the client builder.
Handle server requests to elicit additional information from the user during tool
execution. The server sends an elicitation/create request describing the fields it
needs; your callback presents them to the user and returns an ElicitResult with one of
three actions — accept (with the collected content), decline, or cancel:
use Mcp\Client;
use Mcp\Client\Handler\Request\ElicitationRequestHandler;
use Mcp\Client\Handler\Request\ElicitationCallbackInterface;
use Mcp\Exception\ElicitationException;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Enum\ElicitAction;
use Mcp\Schema\Enum\ElicitationMode;
use Mcp\Schema\Request\ElicitRequest;
use Mcp\Schema\Result\ElicitResult;
class ConsoleElicitationCallback implements ElicitationCallbackInterface
{
public function __invoke(ElicitRequest $request): ElicitResult
{
echo $request->message.\PHP_EOL;
// In url mode there is no schema to fill in — the user completes the
// interaction in the browser instead.
if (ElicitationMode::Url === $request->mode) {
echo 'Continue in your browser: '.$request->url.\PHP_EOL;
return new ElicitResult(ElicitAction::Accept);
}
// Form mode: start from the defaults the server declared, then collect input.
$content = $request->requestedSchema->getDefaults();
foreach ($request->requestedSchema->properties as $name => $definition) {
$answer = readline(($definition->title ?? $name).': ');
if (false === $answer) {
// No input available — let the server know the user cancelled.
return new ElicitResult(ElicitAction::Cancel);
}
if ('' !== $answer) {
$content[$name] = $answer;
}
}
return new ElicitResult(ElicitAction::Accept, $content);
}
}
$client = Client::builder()
->setCapabilities(new ClientCapabilities(elicitationForm: true, elicitationUrl: true))
->addRequestHandler(new ElicitationRequestHandler(new ConsoleElicitationCallback))
->build();The handler doesn't fill in default values. getDefaults() returns the default of each field that
declares one, keyed by field name.
Return new ElicitResult(ElicitAction::Decline) when the user refuses to provide the
information, and new ElicitResult(ElicitAction::Cancel) when they dismiss the request.
Only the Accept action carries content.
!!! warning Error Handling in Elicitation Callbacks:
- **Throw `ElicitationException`** to forward a specific error message to the server
- **Any other exception** is logged but returns a generic error to the server
```php
// Good: Server receives "No interactive console available" message
throw new ElicitationException('No interactive console available');
// Bad: Server receives generic "Error while processing elicitation" message
throw new \RuntimeException('No interactive console available');
```
This holds on `2025-11-25`. On `2026-07-28`, the error is not sent to the server. Instead,
the call that asked for the input (e.g. `callTool()`) throws a `RuntimeException` with the
message.
See examples/client/stdio_elicitation.php for a runnable example against the
elicitation demo server.
Deprecated since protocol revision
2026-07-28(SEP-2577), earliest removal2027-07-28. Sampling keeps working until then; new integrations should call an LLM provider's API directly instead.
Handle server requests for LLM completions:
use Mcp\Client;
use Mcp\Client\Handler\Request\SamplingRequestHandler;
use Mcp\Client\Handler\Request\SamplingCallbackInterface;
use Mcp\Exception\SamplingException;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Request\CreateSamplingMessageRequest;
use Mcp\Schema\Result\CreateSamplingMessageResult;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Enum\Role;
class LlmSamplingCallback implements SamplingCallbackInterface
{
public function __invoke(CreateSamplingMessageRequest $request): CreateSamplingMessageResult
{
try {
// Call your LLM provider
$response = $this->llmClient->complete(
messages: $request->messages,
maxTokens: $request->maxTokens,
temperature: $request->temperature ?? 0.7,
);
return new CreateSamplingMessageResult(
role: Role::Assistant,
content: new TextContent($response->text),
model: $response->model,
stopReason: $response->stopReason,
);
} catch (\Throwable $e) {
// Throw SamplingException to surface error to server
throw new SamplingException(
"LLM sampling failed: {$e->getMessage()}",
(int) $e->getCode(),
$e
);
}
}
}
$client = Client::builder()
->setCapabilities(new ClientCapabilities(sampling: true))
->addRequestHandler(new SamplingRequestHandler(new LlmSamplingCallback))
->build();Clients that support tool-enabled sampling should advertise that capability and forward the request's tools and
toolChoice fields to their LLM provider. A provider response that requests tools can be returned as one or more
ToolUseContent blocks:
use Mcp\Client;
use Mcp\Client\Handler\Request\SamplingRequestHandler;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Content\ToolUseContent;
use Mcp\Schema\Enum\Role;
use Mcp\Schema\Result\CreateSamplingMessageResult;
$client = Client::builder()
->setCapabilities(new ClientCapabilities(
sampling: true,
samplingContext: true,
samplingTools: true,
))
->addRequestHandler(new SamplingRequestHandler($samplingCallback))
->build();
// Inside the sampling callback, after invoking the LLM provider:
return new CreateSamplingMessageResult(
role: Role::Assistant,
content: array_map(
static fn ($call) => new ToolUseContent($call->id, $call->name, $call->input),
$providerResponse->toolCalls,
),
model: $providerResponse->model,
stopReason: 'toolUse',
);The server executes the requested tools and sends their results in a later sampling request as ToolResultContent
blocks in a user message. The client should pass those blocks back to the LLM provider to continue the sampling loop.
!!! warning Error Handling in Sampling Callbacks:
When implementing sampling callbacks, error handling is critical:
- **Throw `SamplingException`** to forward specific error messages to the server
- **Any other exception** will be logged but return a generic error to the server
This distinction allows you to control what error information the server receives:
```php
// Good: Server receives "Rate limit exceeded" message
throw new SamplingException('Rate limit exceeded. Retry after 60 seconds.');
// Bad: Server receives generic "Error while sampling LLM" message
throw new \RuntimeException('Rate limit exceeded');
```
This holds on `2025-11-25`. On `2026-07-28`, the error is not sent to the server. Instead,
the call that asked for the input (e.g. `callTool()`) throws a `RuntimeException` with the
message.
Deprecated since protocol revision
2026-07-28(SEP-2577), earliest removal2027-07-28. Logging keeps working until then; new integrations should log to stderr (stdio) or use OpenTelemetry instead.
Receive structured log messages from the server:
use Mcp\Client;
use Mcp\Client\Handler\Notification\LoggingNotificationHandler;
use Mcp\Schema\Enum\LoggingLevel;
use Mcp\Schema\Notification\LoggingMessageNotification;
// $logger is your application's PSR-3 logger
$loggingHandler = new LoggingNotificationHandler(
static function (LoggingMessageNotification $notification) use ($logger) {
$message = \is_string($notification->data) ? $notification->data : json_encode($notification->data);
// MCP log levels use the same names as the PSR-3 log levels
$logger->log($notification->level->value, $message);
}
);
$client = Client::builder()
->addNotificationHandler($loggingHandler)
->build();
$client->connect($transport);
// set the minimum log level (optional), only after connect()
$client->setLoggingLevel(LoggingLevel::Info);Before connect(), setLoggingLevel() throws a ConnectionException. On revision 2026-07-28,
the client sends the level with every following request. See
Protocol versions.
Deprecated since protocol revision
2026-07-28(SEP-2577), earliest removal2027-07-28. Roots keep working until then; new integrations should pass directories or files through tool arguments, resource URIs or server configuration instead.
Roots let the client expose a list of file:// "workspace folders" that the server
is allowed to operate on. Advertise the roots capability and register a handler
that answers server roots/list requests:
use Mcp\Client;
use Mcp\Client\Handler\Request\ListRootsRequestHandler;
use Mcp\Client\Handler\Request\RootsCallbackInterface;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Request\ListRootsRequest;
use Mcp\Schema\Result\ListRootsResult;
use Mcp\Schema\Root;
class WorkspaceRootsCallback implements RootsCallbackInterface
{
public function __invoke(ListRootsRequest $request): ListRootsResult
{
return new ListRootsResult([
new Root('file:///home/user/projects/app', 'Application'),
new Root('file:///home/user/projects/library', 'Library'),
]);
}
}
$client = Client::builder()
->setCapabilities(new ClientCapabilities(roots: true, rootsListChanged: true))
->addRequestHandler(new ListRootsRequestHandler(new WorkspaceRootsCallback))
->build();Throw a RootsException in the callback to forward a specific error message to the server. Any
other exception returns a generic error, like in the elicitation and sampling callbacks. This
holds on 2025-11-25. On 2026-07-28, the error is not sent to the server. Instead, the call that
asked for the roots (e.g. callTool()) throws a RuntimeException with the message.
When the client's roots change, notify the server so it can request the updated
list via roots/list. This requires advertising the roots.listChanged
capability (rootsListChanged: true above); otherwise sendRootsListChanged()
throws a RuntimeException. On a client that is not connected it throws a
ConnectionException:
$client->sendRootsListChanged();On revision 2026-07-28, sendRootsListChanged() sends nothing. The server asks for the roots
with each call that needs them.
See examples/client/stdio_roots.php for a runnable example. It calls the
inspect_workspace_roots tool of the client-communication demo server. On revision 2026-07-28,
the tool returns an input_required result asking for the roots. The client answers it with the
roots handler and sends the call again.
On 2025-11-25, the server sends a roots/list request
to the client instead. Both ways call the same handler.