diff --git a/CHANGELOG.md b/CHANGELOG.md index f085872a..96f6000c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to `mcp/sdk` will be documented in this file. +0.9.0 +----- + +* Add `HttpTransport::getSessionId()` to read the server-minted `Mcp-Session-Id`: a request-scoped caller can persist it and pass it back through the constructor's `$headers` on a later transport. Always `null` on `2026-07-28`, which removed protocol-level sessions. + 0.8.0 ----- diff --git a/src/Client/Transport/HttpTransport.php b/src/Client/Transport/HttpTransport.php index b5499e85..3c12ea74 100644 --- a/src/Client/Transport/HttpTransport.php +++ b/src/Client/Transport/HttpTransport.php @@ -121,6 +121,22 @@ public function onHeaders(callable $callback): void $this->headerCallback = $callback; } + /** + * The session ID minted by the server for this connection, if any. + * + * Captured from the Mcp-Session-Id response header of a handshake-era + * server; always null on 2026-07-28, which removed protocol-level sessions + * (SEP-2567). A caller that cannot keep the transport alive between + * interactions can persist it and pass it back through the constructor's + * $headers on a later transport, where it wins over the tracked value. + * + * @return string|null null until the server mints a session, and again after close() + */ + public function getSessionId(): ?string + { + return $this->sessionId; + } + public function send(string $data): void { $request = $this->requestFactory->createRequest('POST', $this->endpoint) diff --git a/tests/Unit/Client/Transport/HttpTransportTest.php b/tests/Unit/Client/Transport/HttpTransportTest.php index 6c6119e1..83a77e0b 100644 --- a/tests/Unit/Client/Transport/HttpTransportTest.php +++ b/tests/Unit/Client/Transport/HttpTransportTest.php @@ -94,6 +94,53 @@ public function sendRequest(RequestInterface $request): ResponseInterface $this->assertSame('test-server', $client->getServerInfo()?->name); } + #[TestDox('the server-minted session ID is exposed while connected and cleared on close')] + public function testSessionIdIsExposedAndClearedOnClose(): void + { + $httpClient = new class implements ClientInterface { + public function sendRequest(RequestInterface $request): ResponseInterface + { + $decoded = json_decode((string) $request->getBody(), true); + + if ('initialize' !== ($decoded['method'] ?? null)) { + return new Response(202); + } + + $payload = json_encode([ + 'jsonrpc' => '2.0', + 'id' => $decoded['id'], + 'result' => [ + 'protocolVersion' => '2025-11-25', + 'capabilities' => ['tools' => ['listChanged' => false]], + 'serverInfo' => ['name' => 'test-server', 'version' => '1.0.0'], + ], + ]); + + return new Response(200, [ + 'Content-Type' => 'application/json', + 'Mcp-Session-Id' => 'session-abc123', + ], $payload); + } + }; + + $transport = new HttpTransport('http://localhost/mcp', [], $httpClient, $this->factory, $this->factory); + + $this->assertNull($transport->getSessionId()); + + $client = Client::builder() + ->setClientInfo('test-client', '1.0.0') + ->setInitTimeout(1) + ->build(); + + $client->connect($transport); + + $this->assertSame('session-abc123', $transport->getSessionId()); + + $client->disconnect(); + + $this->assertNull($transport->getSessionId()); + } + #[TestDox('SSE stream is aborted before the buffer can exceed the configured cap')] public function testSseBufferIsBoundedByConfiguredCap(): void {