<?php
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mcp\Server\Stateless;
use Mcp\Exception\MissingRequestMetaException;
use Mcp\Schema\ClientCapabilities;
use Mcp\Schema\Enum\LoggingLevel;
use Mcp\Schema\Implementation;
use Mcp\Server\Wire\InboundClassifier;
* The per-request metadata that replaces the `initialize` handshake in the
* modern era (SEP-2575).
*
* `protocolVersion` and `clientCapabilities` are structurally required; a
* server cannot decide how to answer without them. `clientInfo` is a SHOULD,
* and a request omitting it MUST NOT be refused.
*
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class RequestMeta
{
public const PROTOCOL_VERSION = 'io.modelcontextprotocol/protocolVersion';
public const CLIENT_INFO = 'io.modelcontextprotocol/clientInfo';
public const CLIENT_CAPABILITIES = 'io.modelcontextprotocol/clientCapabilities';
public const LOG_LEVEL = 'io.modelcontextprotocol/logLevel';
public const SERVER_INFO = 'io.modelcontextprotocol/serverInfo';
public const SUBSCRIPTION_ID = 'io.modelcontextprotocol/subscriptionId';
* OpenTelemetry trace context, exempt from the prefix rule by name so it
* matches what the wider ecosystem already puts on the wire.
*
* @see https://www.w3.org/TR/trace-context/
*/
public const TRACE_KEYS = ['traceparent', 'tracestate', 'baggage'];
* @param array<string, string> $traceContext W3C trace context carried through from the request
*/
public function __construct(
public readonly string $protocolVersion,
public readonly ClientCapabilities $clientCapabilities,
public readonly ?Implementation $clientInfo = null,
public readonly ?LoggingLevel $logLevel = null,
public readonly array $traceContext = [],
) {
}
* @param array<string, mixed>|null $params the request's `params` member, if any
* @param array<string, string> $headers request headers, case-insensitively matched; a
* transport without a header layer (stdio) passes none
*
* @throws MissingRequestMetaException when a structurally required member is absent or malformed
*/
public static function fromParams(?array $params, array $headers = []): self
{
$meta = $params['_meta'] ?? null;
if (!\is_array($meta)) {
throw new MissingRequestMetaException('Request is missing the required "params._meta" member.');
}
$version = $meta[self::PROTOCOL_VERSION] ?? null;
if (!\is_string($version) || '' === $version) {
throw new MissingRequestMetaException(\sprintf('Request "_meta" is missing the required "%s" member.', self::PROTOCOL_VERSION));
}
$capabilities = $meta[self::CLIENT_CAPABILITIES] ?? null;
if (!\is_array($capabilities) && !$capabilities instanceof \stdClass) {
throw new MissingRequestMetaException(\sprintf('Request "_meta" is missing the required "%s" member.', self::CLIENT_CAPABILITIES));
}
$clientInfo = $meta[self::CLIENT_INFO] ?? null;
return new self(
$version,
ClientCapabilities::fromArray((array) $capabilities),
\is_array($clientInfo) ? Implementation::fromArray($clientInfo) : null,
self::parseLogLevel($meta[self::LOG_LEVEL] ?? null),
self::parseTraceContext($meta, $headers),
);
}
* An unparseable level means "none requested": failing the caller's actual
* work over a diagnostic preference would be worse.
*/
private static function parseLogLevel(mixed $level): ?LoggingLevel
{
return \is_string($level) ? LoggingLevel::tryFrom($level) : null;
}
* Carried through opaquely: the values are the tracing ecosystem's to
* interpret, and a malformed one is not this server's to reject.
*
* `_meta` wins over the native W3C header of the same name when both are
* present — it is the more specific of the two, scoped to this one call
* rather than the whole HTTP request, and a caller that put it there did
* so deliberately.
*
* @param array<string, mixed> $meta
* @param array<string, string> $headers
*
* @return array<string, string>
*/
private static function parseTraceContext(array $meta, array $headers): array
{
$context = [];
foreach (self::TRACE_KEYS as $key) {
if (\is_string($meta[$key] ?? null)) {
$context[$key] = $meta[$key];
} elseif (null !== $header = InboundClassifier::header($headers, $key)) {
$context[$key] = $header;
}
}
return $context;
}
}