Skip to content

What ServerContext exposes to a handler

Every handler closure receives a ServerContext as its last argument.

Property Purpose
$context->requestId The originating RequestId.
$context->cancellation An Amp\Cancellation token. Pass it to any await() so client notifications/cancelled can interrupt long-running work.
$context->meta The request's _meta object: the client's protocolVersion and clientCapabilities, the optional clientInfo and logLevel, plus progressToken. Read client capabilities per request, never inferred from a prior one.
$context->receiveContext What the transport knew about the delivery. Over Streamable HTTP that is request (the PSR-7 ServerRequestInterface) and authInfo (the VerifiedAccessToken an authentication middleware verified, see authorization). Both are null over stdio.
$context->inputResponses The client's answers to a prior InputRequiredResult, keyed by the identifiers that result assigned, or null on a first call.
$context->requestState The opaque continuation token that result carried, echoed back unchanged, or null on a first call.

$context->meta->logLevel is the revision's per-request replacement for the removed logging/setLevel method. It is the client's requested minimum level for this request, as a LoggingLevel case, or null when the request carried none. The SDK parses and re-encodes the field, but attaches no behaviour to it. The PSR-3 logger logs at whatever level it is configured with, so honouring the request is a handler's own choice:

use Nexus\Mcp\Core\Schema\Enum\LoggingLevel;

executor: static function (?array $args, ServerContext $context) use ($logger): CallToolResult {
    if (LoggingLevel::Debug === $context->meta->logLevel) {
        $logger->debug('Resolving {name} verbosely for this request.', ['name' => $args['name'] ?? '?']);
    }

    return lookUp($args);
},

Reporting progress

reportProgress(float $progress, ?float $total = null, ?string $message = null) emits a notifications/progress tied to the request being handled:

executor: static function (?array $args, ServerContext $context): CallToolResult {
    $steps = ['fetch', 'transform', 'store'];

    foreach ($steps as $i => $step) {
        runStep($step);
        $context->reportProgress(progress: (float) ($i + 1), total: (float) count($steps), message: $step);
    }

    return new CallToolResult(content: [new TextContent(text: 'Done.')]);
},

The spec has progress increase with every notification. total and message are optional. When the original request carried no progressToken in its _meta, the call does nothing. A handler therefore reports unconditionally, and the client decides whether progress flows by supplying the token. The client-side counterpart is streaming progress from callTool.

Cancellation

Over stdio, an inbound notifications/cancelled that names an in-flight request fires that request's $context->cancellation. A handler that threaded the token into its awaits is interrupted with a CancelledException. Whatever the handler was going to answer is dropped. The spec forbids responding to a request the client cancelled, and the dispatcher enforces that whether the handler threw or ran to completion.

A cancellation that names an ID the server does not know is ignored, as the spec asks, since the request may have finished first.

executor: static function (?array $args, ServerContext $context): CallToolResult {
    $rows = fetchSlowly($query, $context->cancellation);

    return new CallToolResult(content: [new TextContent(text: renderRows($rows))]);
},

Over Streamable HTTP, the client closes the response stream instead, and the SDK ignores an inbound notifications/cancelled there. The client numbers requests in its own ID space, not the one the server dispatches under. See subscriptions for the same rule on listen streams. The client-side half is request timeouts, whose elapsed deadline is what sends notifications/cancelled.