diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c144a6..8c2921f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,21 @@ This file was introduced during the v1.7.x series. Structured entries below cove **v1.6.0 and later**; earlier releases can be reviewed via the [Git tag history](https://github.com/logiscape/mcp-sdk-php/tags). +## [Unreleased] + +### Fixed + +- An `initialize` request carrying `_meta` no longer crashes the server with + a `TypeError`. `ClientRequest::createInitializeRequest()` passed the decoded + `_meta` array straight into the `?Meta` parameter of + `InitializeRequestParams`, so validation aborted with + `InitializeRequestParams::__construct(): Argument #4 ($_meta) must be of + type ?Mcp\Types\Meta, array given` before the handshake could be answered — + every request family other than `initialize` already converted `_meta` + through `extractMeta()`. Clients that attach trace context to the handshake + (SEP-414; Claude's web client sends `traceparent` / `baggage` there) could + not connect at all. Backported from the `v2` line. + ## [1.7.4] ### Fixed diff --git a/src/Types/ClientRequest.php b/src/Types/ClientRequest.php index a2b09e1..74cfe63 100644 --- a/src/Types/ClientRequest.php +++ b/src/Types/ClientRequest.php @@ -185,11 +185,16 @@ private static function createInitializeRequest(array $params): self { throw new \InvalidArgumentException('protocolVersion is required for initialize.'); } + // Wire `_meta` arrives as a decoded array — convert it like every + // other request family does. An initialize may legitimately carry + // `_meta` (SEP-414 trace context is allowed on any request), and + // passing the raw array to the typed ?Meta parameter crashed the + // session with a TypeError before the handshake could answer. $initializeParams = new InitializeRequestParams( protocolVersion: $params['protocolVersion'], capabilities: $capabilities, clientInfo: $clientInfo, - _meta: $params['_meta'] ?? null + _meta: self::extractMeta($params) ); return new self(new InitializeRequest($initializeParams)); diff --git a/tests/Types/ClientRequestInitializeMetaTest.php b/tests/Types/ClientRequestInitializeMetaTest.php new file mode 100644 index 0000000..6cfa9b5 --- /dev/null +++ b/tests/Types/ClientRequestInitializeMetaTest.php @@ -0,0 +1,76 @@ + '2025-06-18', + 'capabilities' => [], + 'clientInfo' => ['name' => 'test-client', 'version' => '1.0.0'], + '_meta' => [ + 'traceparent' => '00-0cb6548322595e40d670720bbba87ac4-fc8b88c394fe972b-01', + 'progressToken' => 42, + ], + ]); + + $inner = $request->getRequest(); + $this->assertInstanceOf(InitializeRequest::class, $inner); + + $params = $inner->params; + $this->assertInstanceOf(InitializeRequestParams::class, $params); + $this->assertInstanceOf(Meta::class, $params->_meta); + $this->assertSame( + '00-0cb6548322595e40d670720bbba87ac4-fc8b88c394fe972b-01', + $params->_meta->traceparent + ); + $this->assertSame(42, $params->_meta->progressToken); + + // The whole request must stay valid: validate() walks into _meta. + $request->validate(); + } + + public function testInitializeWithoutMetaKeepsNullMeta(): void + { + $request = ClientRequest::fromMethodAndParams('initialize', [ + 'protocolVersion' => '2025-06-18', + 'capabilities' => [], + 'clientInfo' => ['name' => 'test-client', 'version' => '1.0.0'], + ]); + + $params = $request->getRequest()->params; + $this->assertInstanceOf(InitializeRequestParams::class, $params); + $this->assertNull($params->_meta); + } + + public function testInitializeWithScalarMetaIsIgnoredRatherThanFatal(): void + { + $request = ClientRequest::fromMethodAndParams('initialize', [ + 'protocolVersion' => '2025-06-18', + 'capabilities' => [], + 'clientInfo' => ['name' => 'test-client', 'version' => '1.0.0'], + '_meta' => 'not-an-object', + ]); + + $params = $request->getRequest()->params; + $this->assertInstanceOf(InitializeRequestParams::class, $params); + $this->assertNull($params->_meta); + } +}