Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion src/Types/ClientRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
76 changes: 76 additions & 0 deletions tests/Types/ClientRequestInitializeMetaTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Mcp\Tests\Types;

use Mcp\Types\ClientRequest;
use Mcp\Types\InitializeRequest;
use Mcp\Types\InitializeRequestParams;
use Mcp\Types\Meta;
use PHPUnit\Framework\TestCase;

/**
* An `initialize` request may carry `_meta` on the wire (SEP-414 trace
* context is allowed on any request). It arrives as a decoded array and has
* to be converted to `Meta` like every other request family does, otherwise
* the typed `?Meta` parameter of `InitializeRequestParams` raises a
* TypeError and the handshake dies before the session can answer.
*/
final class ClientRequestInitializeMetaTest extends TestCase
{
public function testInitializeWithArrayMetaIsConvertedToMeta(): void
{
$request = ClientRequest::fromMethodAndParams('initialize', [
'protocolVersion' => '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);
}
}