-
-
Notifications
You must be signed in to change notification settings - Fork 50
Description
Hi!
The StreamTransport
implementation in the MCP SDK seems to have an issue where client disconnections are not properly detected, causing PHP workers to potentially run indefinitely and exhausting FPM worker limits.
The issue seems to comes from how PHP's connection_aborted()
function works with TCP connections. In the current implementation:
// StreamTransport.php
public function isConnected(): bool
{
return 0 === connection_aborted();
}
// Server.php
while ($transport->isConnected()) {
// ... processing loop
usleep(1000);
}
The core issue: connection_aborted()
returns 0
(connection alive) until PHP attempts to flush data to the client and detects the disconnection. With Server-Sent Events, if no events are being sent, PHP never attempts to write to the connection and therefore may never discovers that the client has disconnected (there are multiple StackOverflow issues discussing about this).
Steps to Reproduce
- Clone the demo repository with SSE enabled (vendor directory included with modified
symfony/mcp-sdk
for logging demonstration) - Start server:
symfony serve
- Connect with:
curl 'http://localhost:8000/_mcp/sse'
- Terminate the curl connection (Ctrl/Cmd+C)
- Server continues logging "Transport is still connected" indefinitely
The demo repository includes a modified vendor/symfony/mcp-sdk/src/Server.php
with added logging on line 33 to demonstrate the infinite loop behavior when connections are not properly detected as closed. We can also observe FPM workers not closing if needed (even with pm.max_spare_servers = 1
.
Potentiel solutions
With both solution we should make StreamTransport
aware of last ping time, so we may need to remove the readonly
on this class. Is it okay?
Implement MCP Ping
The MCP specification includes a ping utility, that seems specifically designed for connection health checks:
// In StreamTransport, add periodic ping functionality
private function sendPing(): void
{
$ping = json_encode([
'jsonrpc' => '2.0',
'id' => uniqid(),
'method' => 'ping'
]);
$this->flushEvent('message', $ping);
}
public function isConnected(): bool
{
// Send ping periodically (e.g., every 30 seconds)
if ($this->shouldSendPing()) {
$this->sendPing();
}
return 0 === connection_aborted();
}
When the client is disconnected, the ping packet won't be acknowledged, and PHP will detect the broken connection.
Simple periodic heartbeat characters
Send a minimal heartbeat character periodically (similar to what Figma's MCP server does):
public function isConnected(): bool
{
if ($this->shouldSendHeartbeat()) {
$this->flushEvent('heartbeat', ' '); // Send minimal data
}
return 0 === connection_aborted();
}
Example from Figma's MCP server, every 30 seconds it send a :
:

I'm happy to contribute a PR implementing the preferred solution, or any other better solution. Please let me know which approach aligns best.