Skip to content

Latest commit

 

History

History
304 lines (240 loc) · 12.2 KB

server-requests.md

File metadata and controls

304 lines (240 loc) · 12.2 KB

Server-Initiated Communication

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.

Elicitation (User Input Requests)

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.

Sampling (LLM Requests)

Deprecated since protocol revision 2026-07-28 (SEP-2577), earliest removal 2027-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();

Sampling with Tools

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.

Logging Notifications

Deprecated since protocol revision 2026-07-28 (SEP-2577), earliest removal 2027-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.

Roots

Deprecated since protocol revision 2026-07-28 (SEP-2577), earliest removal 2027-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.