diff --git a/CHANGELOG.md b/CHANGELOG.md index 666c7db2..4a8b87ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to `mcp/sdk` will be documented in this file. * 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. * Fix `HttpTransport` omitting the `MCP-Protocol-Version` header on requests after the `initialize` handshake, including the closing `DELETE`. * Fix memory growing with every `tools/call` in long-lived processes like FrankenPHP workers or stdio servers. +* Fix the stdio transport answering a request before the notifications its handler sent, so a client could miss them. * Fix OIDC discovery rejecting issuers with a trailing slash (e.g. Authentik, Auth0). * Fix `Client::connect()` rejecting an empty `serverInfo.version`, as the Python SDK sends, and leaving the transport open when initialization fails on a malformed result: the transport is now closed and the failure wrapped in a `ConnectionException`. * Fix parallel elicitations on one session getting each other's answers: requests to the client get UUID ids instead of a session counter, and a request or notification a handler sends goes out on the stream of its own call. diff --git a/src/Server/Transport/StdioTransport.php b/src/Server/Transport/StdioTransport.php index aa2db0e5..54ecd2cb 100644 --- a/src/Server/Transport/StdioTransport.php +++ b/src/Server/Transport/StdioTransport.php @@ -425,6 +425,9 @@ private function handleFiberTermination(\Fiber $fiber): void { $finalResult = $fiber->getReturn(); + // A client may stop reading once it has its answer, so what the call sent goes out first. + $this->flushOutgoingMessages(); + if (null !== $finalResult) { try { $encoded = json_encode($finalResult, \JSON_THROW_ON_ERROR); diff --git a/tests/Unit/Server/Transport/StdioSuspendedFibersTest.php b/tests/Unit/Server/Transport/StdioSuspendedFibersTest.php index 7c3e9957..8d696249 100644 --- a/tests/Unit/Server/Transport/StdioSuspendedFibersTest.php +++ b/tests/Unit/Server/Transport/StdioSuspendedFibersTest.php @@ -14,7 +14,9 @@ use Mcp\Event\ServerRequestEvent; use Mcp\Schema\Elicitation\ElicitationSchema; use Mcp\Schema\Elicitation\StringSchemaDefinition; +use Mcp\Schema\Enum\LoggingLevel; use Mcp\Schema\Enum\ProtocolVersion; +use Mcp\Schema\Notification\LoggingMessageNotification; use Mcp\Server; use Mcp\Server\RequestContext; use Mcp\Server\Transport\StdioTransport; @@ -64,6 +66,15 @@ public function testConcurrentElicitationsResumeWithTheirOwnAnswer(): void $this->assertSame('second', $this->answer($lines, 3)['result']['content'][0]['text'] ?? null); } + #[TestDox('notifications a call sent reach the client before its answer')] + public function testNotificationsPrecedeTheAnswer(): void + { + $lines = $this->exchange([self::call(2, 'announce')]); + + $this->assertSame(['notifications/message', null], array_map(static fn (array $line): ?string => $line['method'] ?? null, \array_slice($lines, 1))); + $this->assertSame('announced', $lines[2]['result']['content'][0]['text'] ?? null); + } + /** * @param list> $lines * @@ -149,6 +160,11 @@ public function dispatch(object $event): object return $result->content['name'] ?? 'none'; }, name: 'ask', description: 'Asks for a name') + ->addTool(static function (RequestContext $context): string { + $context->getClientGateway()->notify(new LoggingMessageNotification(LoggingLevel::Info, 'starting')); + + return 'announced'; + }, name: 'announce', description: 'Notifies, then answers') ->setEventDispatcher($dispatcher) ->build();