[Server] Resume every suspended fiber on stdio (#580) · modelcontextprotocol/php-sdk@53e7bbe · GitHub
Skip to content

Commit 53e7bbe

Browse files
authored
[Server] Resume every suspended fiber on stdio (#580)
Concurrent tool calls that report progress or elicit replaced each other's fiber, so only the last one finished. Protocol tracks the awaited request per fiber, and the transport callbacks get the fiber.
1 parent 9c279f9 commit 53e7bbe

9 files changed

Lines changed: 231 additions & 56 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions

‎src/Server/Protocol.php‎

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ class Protocol
7777
private const INTERNAL_ERROR_MESSAGE = 'Internal server error.';
7878

7979
/**
80-
* The client request each transport's fiber is suspended on. Pending requests are stored in the
81-
* session, which concurrent streams share, so a stream must only poll the one its fiber sent.
80+
* The client request each suspended fiber waits on. Pending requests are stored in the session,
81+
* which concurrent calls share, so a fiber must only be resumed with the answer to the one it sent.
8282
*
83-
* @var \WeakMap<TransportInterface<mixed>, int>
83+
* @var \WeakMap<McpFiber, int>
8484
*/
8585
private \WeakMap $awaitedRequestIds;
8686

@@ -116,19 +116,12 @@ public function connect(TransportInterface $transport): void
116116

117117
$transport->setOutgoingMessagesProvider($this->consumeOutgoingMessages(...));
118118

119-
// The transport keeps these callbacks, so they reference it weakly to not keep it alive.
120-
$transportRef = \WeakReference::create($transport);
121-
122-
$transport->setPendingRequestsProvider(fn (Uuid $sessionId): array => $this->getAwaitedPendingRequests($transportRef->get(), $sessionId));
119+
$transport->setPendingRequestsProvider(fn (Uuid $sessionId, \Fiber $fiber): array => $this->getAwaitedPendingRequests($fiber, $sessionId));
123120

124121
$transport->setResponseFinder($this->checkResponse(...));
125122

126-
$transport->setFiberYieldHandler(function (mixed $yieldedValue, ?Uuid $sessionId) use ($transportRef): void {
127-
$requestId = $this->handleFiberYield($yieldedValue, $sessionId);
128-
129-
if (null !== $transport = $transportRef->get()) {
130-
$this->trackAwaitedRequest($transport, $requestId);
131-
}
123+
$transport->setFiberYieldHandler(function (mixed $yieldedValue, ?Uuid $sessionId, \Fiber $fiber): void {
124+
$this->trackAwaitedRequest($fiber, $this->handleFiberYield($yieldedValue, $sessionId));
132125
});
133126

134127
$this->logger->info('Protocol connected to transport', ['transport' => $transport::class]);
@@ -385,7 +378,7 @@ private function handleRequest(TransportInterface $transport, Request $request,
385378
throw new RuntimeException('Failed to save the session of a suspended request.');
386379
}
387380

388-
$this->trackAwaitedRequest($transport, $awaitedRequestId);
381+
$this->trackAwaitedRequest($fiber, $awaitedRequestId);
389382
$transport->attachFiberToSession($fiber, $session->getId());
390383

391384
return;
@@ -739,27 +732,27 @@ public function handleFiberYield(mixed $yieldedValue, ?Uuid $sessionId): ?int
739732
}
740733

741734
/**
742-
* @param TransportInterface<mixed> $transport
735+
* @param McpFiber $fiber
743736
*/
744-
private function trackAwaitedRequest(TransportInterface $transport, ?int $requestId): void
737+
private function trackAwaitedRequest(\Fiber $fiber, ?int $requestId): void
745738
{
746739
if (null === $requestId) {
747-
unset($this->awaitedRequestIds[$transport]);
740+
unset($this->awaitedRequestIds[$fiber]);
748741

749742
return;
750743
}
751744

752-
$this->awaitedRequestIds[$transport] = $requestId;
745+
$this->awaitedRequestIds[$fiber] = $requestId;
753746
}
754747

755748
/**
756-
* @param TransportInterface<mixed>|null $transport
749+
* @param McpFiber $fiber
757750
*
758751
* @return array<int, mixed>
759752
*/
760-
private function getAwaitedPendingRequests(?TransportInterface $transport, Uuid $sessionId): array
753+
private function getAwaitedPendingRequests(\Fiber $fiber, Uuid $sessionId): array
761754
{
762-
$requestId = null !== $transport ? $this->awaitedRequestIds[$transport] ?? null : null;
755+
$requestId = $this->awaitedRequestIds[$fiber] ?? null;
763756
if (null === $requestId) {
764757
return [];
765758
}

‎src/Server/Transport/BaseTransport.php‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,14 @@ protected function getOutgoingMessages(?Uuid $sessionId): array
8686
}
8787

8888
/**
89+
* @param McpFiber $fiber
90+
*
8991
* @return array<int, array<string, mixed>>
9092
*/
91-
protected function getPendingRequests(?Uuid $sessionId): array
93+
protected function getPendingRequests(?Uuid $sessionId, \Fiber $fiber): array
9294
{
9395
if ($sessionId && \is_callable($this->pendingRequestsProvider)) {
94-
return ($this->pendingRequestsProvider)($sessionId);
96+
return ($this->pendingRequestsProvider)($sessionId, $fiber);
9597
}
9698

9799
return [];
@@ -111,15 +113,16 @@ protected function checkForResponse(int $requestId, ?Uuid $sessionId): Response|
111113

112114
/**
113115
* @param FiberSuspend|null $yielded
116+
* @param McpFiber $fiber the fiber that yielded
114117
*/
115-
protected function handleFiberYield(mixed $yielded, ?Uuid $sessionId): void
118+
protected function handleFiberYield(mixed $yielded, ?Uuid $sessionId, \Fiber $fiber): void
116119
{
117120
if (null === $yielded || !\is_callable($this->fiberYieldHandler)) {
118121
return;
119122
}
120123

121124
try {
122-
($this->fiberYieldHandler)($yielded, $sessionId);
125+
($this->fiberYieldHandler)($yielded, $sessionId, $fiber);
123126
} catch (\Throwable $e) {
124127
$this->logger->error('Fiber yield handler failed.', [
125128
'exception' => $e,

‎src/Server/Transport/ManagesTransportCallbacks.php‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* @phpstan-import-type FiberReturn from \Mcp\Server\Transport\TransportInterface
2323
* @phpstan-import-type FiberResume from \Mcp\Server\Transport\TransportInterface
2424
* @phpstan-import-type FiberSuspend from \Mcp\Server\Transport\TransportInterface
25+
* @phpstan-import-type McpFiber from \Mcp\Server\Transport\TransportInterface
2526
*
2627
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
2728
* */
@@ -36,13 +37,13 @@ trait ManagesTransportCallbacks
3637
/** @var callable(Uuid): array<int, array{message: string, context: array<string, mixed>}> */
3738
protected $outgoingMessagesProvider;
3839

39-
/** @var callable(Uuid): array<int, array<string, mixed>> */
40+
/** @var callable(Uuid, McpFiber): array<int, array<string, mixed>> */
4041
protected $pendingRequestsProvider;
4142

4243
/** @var (callable(int, Uuid): (Response<array<string, mixed>>|Error|null))|null */
4344
protected $responseFinder;
4445

45-
/** @var callable(FiberSuspend|null, ?Uuid): void */
46+
/** @var callable(FiberSuspend|null, ?Uuid, McpFiber): void */
4647
protected $fiberYieldHandler;
4748

4849
public function onMessage(callable $listener): void
@@ -74,7 +75,7 @@ public function setResponseFinder(callable $finder): void
7475
}
7576

7677
/**
77-
* @param callable(FiberSuspend|null, ?Uuid): void $handler
78+
* @param callable(FiberSuspend|null, ?Uuid, McpFiber): void $handler
7879
*/
7980
public function setFiberYieldHandler(callable $handler): void
8081
{

‎src/Server/Transport/StdioTransport.php‎

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Mcp\Server\Transport\Stdio\RunnerState;
2323
use Mcp\Server\Wire\InboundClassifier;
2424
use Psr\Log\LoggerInterface;
25+
use Symfony\Component\Uid\Uuid;
2526

2627
/**
2728
* Serves one client over the standard streams, in whichever protocol era it
@@ -79,6 +80,14 @@ class StdioTransport extends BaseTransport implements StatelessAwareTransportInt
7980
*/
8081
private array $streams = [];
8182

83+
/**
84+
* Handshake-era handlers suspended on the client, by object id: the
85+
* client pipelines its requests, so several can be in flight at once.
86+
*
87+
* @var array<int, McpFiber>
88+
*/
89+
private array $fibers = [];
90+
8291
/**
8392
* @param resource $input
8493
* @param resource $output
@@ -124,14 +133,20 @@ public function send(string $data, array $context): void
124133
$this->writeLine($data);
125134
}
126135

136+
public function attachFiberToSession(\Fiber $fiber, Uuid $sessionId): void
137+
{
138+
$this->fibers[spl_object_id($fiber)] = $fiber;
139+
$this->sessionId = $sessionId;
140+
}
141+
127142
public function listen(): int
128143
{
129144
$this->logger->info('StdioTransport is listening for messages on STDIN...');
130145
stream_set_blocking($this->input, false);
131146

132147
while (!feof($this->input) && RunnerState::RUNNING === $this->runnerControl->getState()) {
133148
$this->processInput();
134-
$this->processFiber();
149+
$this->processFibers();
135150
$this->processStreams();
136151
$this->flushOutgoingMessages();
137152
}
@@ -356,27 +371,35 @@ private function writeError(Error $error): void
356371
$this->writeLine(json_encode($error, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES));
357372
}
358373

359-
private function processFiber(): void
374+
/**
375+
* Resumes every suspended handler that can go on: one that sent a
376+
* notification right away, one that sent a request once its answer is in
377+
* or it timed out.
378+
*/
379+
private function processFibers(): void
360380
{
361-
if (null === $this->sessionFiber) {
362-
return;
363-
}
364-
365-
if ($this->sessionFiber->isTerminated()) {
366-
$this->handleFiberTermination($this->sessionFiber);
367-
368-
return;
369-
}
381+
foreach ($this->fibers as $key => $fiber) {
382+
if ($fiber->isSuspended()) {
383+
$this->resumeFiber($fiber);
384+
}
370385

371-
if (!$this->sessionFiber->isSuspended()) {
372-
return;
386+
if ($fiber->isTerminated()) {
387+
unset($this->fibers[$key]);
388+
$this->handleFiberTermination($fiber);
389+
}
373390
}
391+
}
374392

375-
$pendingRequests = $this->getPendingRequests($this->sessionId);
393+
/**
394+
* @param McpFiber $fiber
395+
*/
396+
private function resumeFiber(\Fiber $fiber): void
397+
{
398+
$pendingRequests = $this->getPendingRequests($this->sessionId, $fiber);
376399

377400
if (empty($pendingRequests)) {
378-
$yielded = $this->sessionFiber->resume();
379-
$this->handleFiberYield($yielded, $this->sessionId);
401+
$yielded = $fiber->resume();
402+
$this->handleFiberYield($yielded, $this->sessionId, $fiber);
380403

381404
return;
382405
}
@@ -387,8 +410,8 @@ private function processFiber(): void
387410
$response = $this->checkForResponse($requestId, $this->sessionId);
388411

389412
if (null !== $response) {
390-
$yielded = $this->sessionFiber->resume($response);
391-
$this->handleFiberYield($yielded, $this->sessionId);
413+
$yielded = $fiber->resume($response);
414+
$this->handleFiberYield($yielded, $this->sessionId, $fiber);
392415

393416
return;
394417
}
@@ -410,8 +433,6 @@ private function handleFiberTermination(\Fiber $fiber): void
410433
$this->logger->error('STDIO: Failed to encode final Fiber result.', ['exception' => $e]);
411434
}
412435
}
413-
414-
$this->sessionFiber = null;
415436
}
416437

417438
private function flushOutgoingMessages(): void

‎src/Server/Transport/StreamableHttpTransport.php‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,11 @@ protected function createStreamedResponse(): ResponseInterface
298298
while ($fiber->isSuspended()) {
299299
$this->flushOutgoingMessages($this->sessionId);
300300

301-
$pendingRequests = $this->getPendingRequests($this->sessionId);
301+
$pendingRequests = $this->getPendingRequests($this->sessionId, $fiber);
302302

303303
if (empty($pendingRequests)) {
304304
$yielded = $fiber->resume();
305-
$this->handleFiberYield($yielded, $this->sessionId);
305+
$this->handleFiberYield($yielded, $this->sessionId, $fiber);
306306
continue;
307307
}
308308

@@ -314,7 +314,7 @@ protected function createStreamedResponse(): ResponseInterface
314314

315315
if (null !== $response) {
316316
$yielded = $fiber->resume($response);
317-
$this->handleFiberYield($yielded, $this->sessionId);
317+
$this->handleFiberYield($yielded, $this->sessionId, $fiber);
318318
$resumed = true;
319319
break;
320320
}

‎src/Server/Transport/TransportInterface.php‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,11 @@ public function onSessionEnd(callable $listener): void;
9797
public function setOutgoingMessagesProvider(callable $provider): void;
9898

9999
/**
100-
* Set a provider function to retrieve all pending server-initiated requests.
100+
* Set a provider function to retrieve the pending server-initiated request a suspended Fiber waits on.
101101
*
102102
* The transport calls this to decide if it should wait for a client response before resuming a Fiber.
103103
*
104-
* @param callable(Uuid $sessionId): array<int, array<string, mixed>> $provider
104+
* @param callable(Uuid $sessionId, McpFiber $fiber): array<int, array<string, mixed>> $provider
105105
*/
106106
public function setPendingRequestsProvider(callable $provider): void;
107107

@@ -118,7 +118,7 @@ public function setResponseFinder(callable $finder): void;
118118
* The transport calls this to let the Protocol handle new requests/notifications
119119
* that are yielded from a Fiber's execution.
120120
*
121-
* @param callable(FiberSuspend|null, ?Uuid $sessionId): void $handler
121+
* @param callable(FiberSuspend|null, ?Uuid $sessionId, McpFiber $fiber): void $handler
122122
*/
123123
public function setFiberYieldHandler(callable $handler): void;
124124

‎tests/Unit/Fixtures/PollingLoopTransport.php‎

Lines changed: 6 additions & 2 deletions

0 commit comments

Comments
 (0)