* Address review feedback on resource_link Three follow-ups from review of the resource_link work: * PromptResultFormatter: add a regression test proving the optional fields of a typed resource_link block (title, description, mimeType, size, annotations, _meta) survive formatting. The delegation to ResourceLink::fromArray() already landed while rebasing onto the PromptResultFormatter refactor, but the existing test only supplied uri/name and so could not catch a reintroduced field drop. * ResourceLink::fromArray(): validate optional fields consistently, the way the equivalent ResourceDefinition::fromArray() already does. description, mimeType and size now raise InvalidArgumentException instead of surfacing a TypeError (or silently coercing, for size's (int) cast); annotations goes through Annotations::tryFromArray(), which carries the is_array() guard that icons already had; icons uses Icon::listFromArray() so a non-array entry is reported in context. * ToolReference::extractStructuredContent(): never emit a list as structuredContent. The Content guard added earlier fixed only one instance of the real invariant - structuredContent must be a JSON object, and a PHP list can never be one. Tool::fromArray() already enforces the matching rule by rejecting an outputSchema whose type is not "object", so this aligns the runtime path with it. The test asserting the opposite for an array-typed outputSchema contradicted the phpstan type and the fromArray() check introduced alongside it in the same commit, so it is replaced by tests for the two list shapes. * Apply the structuredContent object rule to object results too `extractStructuredContent()` guards raw array results against being emitted as a JSON array, but the object branch handed back whatever `json_decode()` produced. A `JsonSerializable` returning a list or a scalar slipped straight through, producing exactly the `structuredContent` the array guard exists to prevent — and a return value that contradicts the method's own `array<string, mixed>|null` signature. Check the decoded value before returning it, and cover the object branch, which had no tests at all. * Document structured tool output `outputSchema` and `structuredContent` were undocumented: the tool return value docs covered only the `content` side, and the schema generation section is about tool parameters. Add a "Structured Output" subsection covering how to declare the schema, which return values populate `structuredContent`, and why a list has to be wrapped in a key to get structured output at all. * Gate structuredContent on the negotiated protocol revision SEP-2106 (revision 2026-07-28) widens structuredContent to any JSON value; earlier revisions require an object. Resolve the revision per request and apply the matching rule, and warn when a declared outputSchema yields none. * Resolve the protocol revision on RequestContext Keeps the `_meta`-then-session lookup in one place instead of per handler, and exposes the negotiated revision to tool handlers. * Document reading the negotiated revision from RequestContext * Warn when a self-built CallToolResult carries an invalid structuredContent Returning a CallToolResult opts out of the extraction rules, so the value is still sent unchanged — but a JSON array is not valid before SEP-2106.
3.3 KiB
Client Communication
MCP supports various ways a server can communicate back to a server on top of the main request-response flow.
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
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
Only send includeContext when the client advertises sampling.context, and only send tools or toolChoice when it
advertises sampling.tools. The context modes other than none are soft-deprecated by the current specification.
Find more details to sampling payload in the specification.
Logging
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);