Skip to content

Handle closed connection in StreamTransport to avoid stuck worker #290

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

Gregory-Gerard
Copy link

@Gregory-Gerard Gregory-Gerard commented Aug 8, 2025

Q A
Bug fix? yes
New feature? yes
Docs? no
Issues Fix #283
License MIT

WIP: I'll update this description with full details as I progress 👍

@OskarStark
Copy link
Contributor

Hmm one is called runtime, the other one transport, having a runtime interface on a transport feels weird 😐 any ideas?

@Gregory-Gerard
Copy link
Author

Gregory-Gerard commented Aug 8, 2025

Yes it feels weird to me too. But I don't have other ideas for now for testing StreamTransport and mocking connection_aborted for example.

I could override connection_aborted (I found an example in Slim framework) in StreamTransport in my test, but I don't really like this idea too.

If someone have guidance, I'll happily modify this!

@OskarStark OskarStark requested a review from Copilot August 8, 2025 10:56
@carsonbot carsonbot changed the title [MCP SDK] Handle closed connection in StreamTransport to avoid stuck worker Handle closed connection in StreamTransport to avoid stuck worker Aug 8, 2025
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR abstracts the StreamTransport I/O operations behind a RuntimeInterface to enable testing and improve testability. The main purpose is to handle closed connections in the transport layer to avoid stuck workers.

  • Introduces RuntimeInterface and Runtime implementation to wrap PHP's built-in functions
  • Adds InMemoryRuntime test fixture to enable unit testing of transport behavior
  • Refactors StreamTransport to use the runtime interface instead of direct PHP function calls

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/mcp-sdk/src/Server/Transport/Sse/RuntimeInterface.php Defines interface for runtime operations (connection status, output buffering, etc.)
src/mcp-sdk/src/Server/Transport/Sse/Runtime.php Production implementation wrapping PHP's built-in functions
src/mcp-sdk/src/Server/Transport/Sse/StreamTransport.php Updated to use runtime interface with default parameter for backward compatibility
src/mcp-sdk/tests/Fixtures/InMemoryRuntime.php Test implementation capturing output for assertions
src/mcp-sdk/tests/Server/Transport/Sse/StreamTransportTest.php Comprehensive unit tests for StreamTransport behavior

@Gregory-Gerard
Copy link
Author

@OskarStark would you be okay if I try the namespaced function override approach just for connection_aborted via a test-only polyfill? We can keep flush/write as-is and assert via PHP’s output buffering.

I couldn’t find other example testing this; even HttpFoundation’s EventStreamResponse doesn’t check specifically connection_aborted.

This would avoids the Runtime dependency and any constructor changes. If you’re fine with it, I’ll add a dev-only polyfill that shadows Symfony\AI\McpSdk\Server\Transport\Sse\connection_aborted() in tests.

@OskarStark
Copy link
Contributor

I would say give it a try, why not

@Gregory-Gerard Gregory-Gerard force-pushed the streamtransport-not-closed-properly branch 2 times, most recently from 56edb8a to 85b91c4 Compare August 8, 2025 14:01
@Gregory-Gerard
Copy link
Author

Re @OskarStark! I implemented the namespaced override only for connection_aborted. StreamTransport keeps native calls; tests assert via output capture; and no new RuntimeInterface. If you’re good with this, I’ll proceed with the concrete SSE ping/heartbeat fix.

@Gregory-Gerard Gregory-Gerard force-pushed the streamtransport-not-closed-properly branch from 85b91c4 to dee0559 Compare August 8, 2025 14:04
@OskarStark
Copy link
Contributor

Yes why not 🙌🙂

@Gregory-Gerard Gregory-Gerard force-pushed the streamtransport-not-closed-properly branch from dee0559 to 06f4cfb Compare August 8, 2025 14:56
@Gregory-Gerard Gregory-Gerard force-pushed the streamtransport-not-closed-properly branch from 00b17ff to d5a43c0 Compare August 11, 2025 06:51
@Gregory-Gerard
Copy link
Author

Gregory-Gerard commented Aug 11, 2025

Hi @OskarStark @chr-hertel (friendly tag because you were tagged in the issue originally)!

Quick update:

  • Still WIP. Next up: make ping interval and request timeouts configurable per MCP spec (https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/ping). I may introduce a small immutable ServerConfig injected into Server to avoid constructor bloat.
  • Ping (or rather the timeout) is non-strict for now because some clients don't reply explicitly to pings. I'll add a "strict" mode (default off) so we can enforce spec behavior without breaking common clients (spec: https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle#timeouts, e.g. issue with a strict timeout When using it in VSCode with Github Copilot, a timeout error is thrown. punkpeye/fastmcp#38).
  • Major changes so far:
    • Added PendingResponse/PendingResponseBag to track outgoing requests, resolve inbound responses/errors, and GC on timeout. This currently requires JsonRpcHandler to yield Response|Error. Longer-term I'd prefer splitting JSON encode/decode from processing handlers as proposed in PR Draft: Streamable Http #240, but seems out of scope here, but happy to adjust later if you prefer.
    • Added KeepAliveSessionInterface + KeepAliveSession to automate periodic pings.
    • Server::sendRequest(...) with callback and timeout handling is in; next I'll add per-request timeout overrides and emit a cancel notification to the client on timeout (which implies adding sendNotification), still per the spec.
    • Injected a ClockInterface to make testing easier and remove a direct dependency on system clock with usleep
  • If everything is okay, I will of course update the mcp-bundle with new configuration, docs, etc.
  • Also, last thing but not least, we may need to hardcode the ping capability in RequestHandlers (because most implementation does), and client/server SHOULD always respond to a ping, any ideas about this?

WDYT about my directions? Happy to have feedback before polishing thing!

public function __construct(
private JsonRpcHandler $jsonRpcHandler,
private ClockInterface $clock,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about adding a default Clock like in Symfony Messenger, wdyt?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

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