docs/stateless-lifecycle.md walks the revision end to end - per-request _meta, server/discover, multi round-trip requests, caching and subscriptions - and a runnable example on each side shows what that looks like in code. CI matrixes both conformance roles over both revisions. --spec-version is cumulative across the dated revisions, so 2025-11-25 covers the two before it; 2026-07-28 is not cumulative with them and needs its own run against its own baseline. The runner is pinned so a PR only goes red for reasons in the PR, and conformance-weekly tracks the moving target instead - on both revisions, since the draft scenarios ship only on the alpha dist-tag. The Inspector cannot reach a modern-lifecycle server (it opens with initialize), so the example is covered by integration tests instead: one drives it with hand-built HTTP the way a conforming client would, one drives it with this SDK's client, and one drives the single endpoint from both eras.
5.6 KiB
Client Communication
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. This page describes the handshake era, where a server sends its own JSON-RPC requests to the client. The modern lifecycle removed that: sampling, elicitation and roots are carried back inside the result instead, andClientGateway::sample(),elicit()andlistRoots()raise aLogicExceptionthere. Logging and progress still work as described below — they simply travel on the request's own response stream, and the client opts into each. See The 2026-07-28 Lifecycle.
Table of Contents
ClientGateway
Every communication back to client is handled using the Mcp\Server\ClientGateway and its dedicated methods per
operation. To use the ClientGateway in your code, you need to use method argument injection for RequestContext.
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:
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(...);
The same object also carries the protocol revision negotiated for the current request, which is useful when a feature is only available from a certain revision on:
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
}
Sampling
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.
With sampling servers can request clients to execute "completions" or "generations" with a language model for them:
$result = $clientGateway->sample('Roses are red, violets are', 350, 90, ['temperature' => 0.5]);
The sample method accepts four arguments:
message, which is required and accepts a string, an instance ofContentor an array ofSamplingMessageinstances.maxTokens, which defaults to1000timeoutin seconds, which defaults to120optionswhich might includesystemPrompt,preferencesfor model choice,includeContext,temperature,stopSequences,metadata,tools, andtoolChoice
Both tools/toolChoice and includeContext are gated on what the client advertised, so check before sending:
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:
$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.
Logging
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.
The Logging utility enables servers to send structured log messages as notification to clients:
use Mcp\Schema\Enum\LoggingLevel;
$clientGateway->log(LoggingLevel::Warning, 'The end is near.');
Progress
With a Progress notification a server can update a client while an operation is ongoing:
$clientGateway->progress(4.2, 10, 'Downloading needed images.');
Notification
Lastly, the server can push all kind of notifications, that implement the Mcp\Schema\JsonRpc\Notification interface
to the client to:
$clientGateway->notify($yourNotification);