Skip to content

[MCP SDK] StreamTransport connection not properly detected as closed, causing infinite worker loops #283

@Gregory-Gerard

Description

@Gregory-Gerard

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

  1. Clone the demo repository with SSE enabled (vendor directory included with modified symfony/mcp-sdk for logging demonstration)
  2. Start server: symfony serve
  3. Connect with: curl 'http://localhost:8000/_mcp/sse'
  4. Terminate the curl connection (Ctrl/Cmd+C)
  5. 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 ::

Image

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions