The client can receive requests and notifications from the server when configured with appropriate handlers.
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\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: present $request->requestedSchema->properties and collect input.
$content = [];
foreach ($request->requestedSchema->properties as $name => $definition) {
$answer = readline($definition->title.': ');
if (false === $answer) {
// No input available — let the server know the user cancelled.
return new ElicitResult(ElicitAction::Cancel);
}
$content[$name] = $answer;
}
return new ElicitResult(ElicitAction::Accept, $content);
}
}
$client = Client::builder()
->setCapabilities(new ClientCapabilities(elicitation: true))
->addRequestHandler(new ElicitationRequestHandler(new ConsoleElicitationCallback))
->build();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');
```
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\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\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');
```
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\Handler\Notification\LoggingNotificationHandler;
use Mcp\Schema\Notification\LoggingMessageNotification;
use Mcp\Schema\Enum\LoggingLevel;
$loggingHandler = new LoggingNotificationHandler(
static function (LoggingMessageNotification $notification) {
// Route to your application's logging system
$level = $notification->level;
$message = $notification->data;
match ($level) {
LoggingLevel::Debug => logger()->debug($message),
LoggingLevel::Info => logger()->info($message),
LoggingLevel::Warning => logger()->warning($message),
LoggingLevel::Error => logger()->error($message),
default => logger()->info($message),
};
}
);
$client = Client::builder()
->addNotificationHandler($loggingHandler)
->build();
// Set minimum log level (optional)
$client->setLoggingLevel(LoggingLevel::Info);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\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();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();See examples/client/stdio_roots.php for a runnable example: it calls the
inspect_workspace_roots tool of the client-communication demo server, which
answers by issuing the roots/list request back to the client.