One handler asks the user something on any revision now. Where the client can be asked mid-request it still is; where it cannot, the ask becomes the input_required result that revision carries and the same call returns the answer once the client re-sends it. Costs one handler entry per ask, so side effects belong after the last question. Answers from earlier rounds travel in the requestState, which is why asking more than once needs Builder::setRequestState(). sample() and listRoots() still raise a LogicException there: that revision removed them outright.
143 lines
6.5 KiB
Markdown
143 lines
6.5 KiB
Markdown
# Talking back to the client
|
|
|
|
MCP supports various ways a server can communicate back to a client on top of the main
|
|
request-response flow.
|
|
|
|
> **Protocol revision `2026-07-28`.** Logging, progress and notifications work as described
|
|
> below on every revision; under the modern lifecycle they travel on the request's own
|
|
> response stream and the client opts into each. Sampling is the exception — see its section.
|
|
> Asking the user something has a page of its own: [Asking for input](input-required.md).
|
|
|
|
## ClientGateway
|
|
|
|
Every communication back to client is handled using the `Mcp\Server\ClientGateway` and its dedicated methods per
|
|
operation. Reach it through method argument injection for `RequestContext`. (A `ClientGateway`-typed parameter is
|
|
injected too, but unlike `RequestContext` it is not excluded from the generated input schema, so it would show up as
|
|
an argument of your tool.)
|
|
|
|
Every reference of a MCP element, that translates to an actual method call, can just add an type-hinted argument for the
|
|
`RequestContext` and the SDK will take care to include the gateway in the arguments of the method call:
|
|
|
|
```php
|
|
use Mcp\Capability\Attribute\McpTool;
|
|
use Mcp\Server\RequestContext;
|
|
|
|
class MyService
|
|
{
|
|
#[McpTool(name: 'my_tool', description: 'My Tool Description')]
|
|
public function myTool(RequestContext $context): string
|
|
{
|
|
$context->getClientGateway()->log(...);
|
|
```
|
|
|
|
## Request metadata
|
|
|
|
`RequestContext` also carries what the current request said about itself, which is useful when a feature is only
|
|
available from a certain revision on:
|
|
|
|
```php
|
|
use Mcp\Schema\Enum\ProtocolVersion;
|
|
|
|
if ($context->getProtocolVersion()->isAtLeast(ProtocolVersion::V2026_07_28)) {
|
|
// e.g. a bare list is only valid as `structuredContent` from this revision on
|
|
}
|
|
```
|
|
|
|
Two more accessors read what the client declared, and both work in either
|
|
[protocol era](../protocol-versions.md) — negotiated once during the handshake, or declared per request from
|
|
`2026-07-28` on:
|
|
|
|
```php
|
|
$context->getClientCapabilities(); // what this client declared, or null in the handshake era
|
|
$context->getTraceContext(); // traceparent / tracestate / baggage, verbatim
|
|
```
|
|
|
|
`ClientGateway`'s capability probes — `supportsElicitation()`, `supportsSampling()`, `supportsRoots()` and the
|
|
sub-capability variants — read the same declaration.
|
|
|
|
W3C trace context is passed through exactly as it arrived, and echoed onto every notification the request causes,
|
|
so a span stays joined across the response stream. Reading it adds no OpenTelemetry dependency — the values are
|
|
strings.
|
|
|
|
## Sampling
|
|
|
|
> **Deprecated** since protocol revision `2026-07-28` ([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)), earliest removal `2027-07-28`. It keeps working on a handshake-era connection until then, but that revision removed server-initiated requests outright, so `sample()` — like `listRoots()` — raises a `LogicException` when a modern-era client made the call. New integrations should call an LLM provider's API directly instead.
|
|
|
|
With [sampling](https://modelcontextprotocol.io/specification/2025-11-25/client/sampling) servers can request clients to
|
|
execute "completions" or "generations" with a language model for them:
|
|
|
|
```php
|
|
$result = $clientGateway->sample('Roses are red, violets are', 350, 90, ['temperature' => 0.5]);
|
|
```
|
|
|
|
The `sample` method accepts four arguments:
|
|
|
|
1. `message`, which is **required** and accepts a string, an instance of `Content` or an array of `Mcp\Schema\Content\SamplingMessage` instances.
|
|
2. `maxTokens`, which defaults to `1000`
|
|
3. `timeout` in seconds, which defaults to `120`
|
|
4. `options` which might include `systemPrompt`, `preferences` for model choice, `includeContext`, `temperature`,
|
|
`stopSequences`, `metadata`, `tools`, and `toolChoice`
|
|
|
|
Both `tools`/`toolChoice` and `includeContext` are gated on what the client advertised, so check before sending:
|
|
|
|
```php
|
|
if ($clientGateway->supportsSamplingTools()) {
|
|
$result = $clientGateway->sample($messages, options: ['tools' => $tools]);
|
|
}
|
|
```
|
|
|
|
A server **must not** send `tools` or `toolChoice` to a client that did not advertise `sampling.tools`. The
|
|
`includeContext` values other than `none` are soft-deprecated and should only be sent when the client advertises
|
|
`sampling.context` — `supportsSamplingContext()` reports that one.
|
|
|
|
### Tool loops
|
|
|
|
When the model wants to call a tool, the result comes back with `stopReason: 'toolUse'` and one or more
|
|
`ToolUseContent` blocks. Execute them, then send a follow-up request with the assistant's message and a user message
|
|
carrying a matching `ToolResultContent` for every `ToolUseContent`:
|
|
|
|
```php
|
|
$messages[] = new SamplingMessage(Role::Assistant, $result->content);
|
|
$messages[] = new SamplingMessage(Role::User, [new ToolResultContent($toolUse->id, [new TextContent($output)])]);
|
|
```
|
|
|
|
The specification is strict about the shape of that exchange: tool results may not be mixed with other content in a
|
|
message, and every tool use must be answered before the conversation continues. `sample()` checks these rules before
|
|
sending and throws an `InvalidArgumentException` rather than letting the client reject the request with `-32602`.
|
|
Use `$result->getContentBlocks()` to iterate the response regardless of whether it holds one block or a list.
|
|
|
|
[Find more details to sampling payload in the specification.](https://modelcontextprotocol.io/specification/2025-11-25/client/sampling#protocol-messages)
|
|
|
|
## Logging
|
|
|
|
> **Deprecated** since protocol revision `2026-07-28` ([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)), earliest removal `2027-07-28`. Logging keeps working until then; new integrations should log to stderr (stdio) or use OpenTelemetry instead.
|
|
|
|
The [Logging](https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging) utility enables servers
|
|
to send structured log messages as notification to clients:
|
|
|
|
```php
|
|
use Mcp\Schema\Enum\LoggingLevel;
|
|
|
|
$clientGateway->log(LoggingLevel::Warning, 'The end is near.');
|
|
```
|
|
|
|
## Progress
|
|
|
|
With a [Progress](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress#progress)
|
|
notification a server can update a client while an operation is ongoing:
|
|
|
|
```php
|
|
$clientGateway->progress(4.2, 10, 'Downloading needed images.');
|
|
```
|
|
|
|
Progress is opt-in by the client in both eras: it sends a `progressToken` with the request, and without one this
|
|
call sends nothing.
|
|
|
|
## Notification
|
|
|
|
Lastly, the server can push all kind of notifications, that extend the abstract `Mcp\Schema\JsonRpc\Notification` class
|
|
to the client to:
|
|
|
|
```php
|
|
$clientGateway->notify($yourNotification);
|
|
```
|