Skip to content

fix(stdio): process messages concurrently to prevent deadlocks#736

Open
blackwell-systems wants to merge 1 commit intomodelcontextprotocol:mainfrom
blackwell-systems:fix-stdio-concurrent-messages
Open

fix(stdio): process messages concurrently to prevent deadlocks#736
blackwell-systems wants to merge 1 commit intomodelcontextprotocol:mainfrom
blackwell-systems:fix-stdio-concurrent-messages

Conversation

@blackwell-systems
Copy link
Copy Markdown

Motivation and Context

Fixes #572

StdioServerTransport.processReadBuffer() invokes each message handler sequentially: _onMessage.invoke(message) blocks the loop until the handler returns. A long-running tool call (e.g., one that waits for elicitation) blocks all subsequent messages, including pings, progress notifications, and the elicitation response itself. This creates deadlocks for any bidirectional communication pattern.

As noted by @rnett in #572:

This creates deadlocks for any bidirectional communication, which is now easy with ClientConnection. Even for ping.

Fix

Launch each message handler in its own coroutine via scope.launch instead of awaiting it inline. The ReadBuffer parsing remains sequential (required for correct JSON-RPC framing), but handler execution is now concurrent.

This is the minimal transport-level fix. A prior attempt (#610) placed concurrency in AbstractTransport but was closed due to complications with transports that already handle concurrency (SSE server). This PR scopes the change to StdioServerTransport only.

Test changes

The error-handling parameterized test (should continue processing messages after handler throws) needed a brief delay(100) after secondMessageProcessed.await() because the error callback from the first message's coroutine now fires asynchronously on IODispatcher rather than synchronously in the processing loop.

How Has This Been Tested?

All 17 StdioServerTransportTest cases pass, including the 3 parameterized error-handling variants.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling

StdioServerTransport processed messages sequentially in processReadBuffer:
each _onMessage.invoke() call blocked the loop until the handler completed.
This meant a long-running tool call blocked all subsequent messages,
including pings, elicitation responses, and progress notifications.
This also created deadlocks for any bidirectional communication.

Launch each message handler in its own coroutine via scope.launch so
handlers run concurrently. The ReadBuffer parsing remains sequential
(required for correct JSON-RPC framing), but handler execution is
now parallel.

Updated the error-handling test to account for the async error delivery
that results from concurrent dispatch.

Fixes modelcontextprotocol#572

Signed-off-by: Dayna Blackwell <dayna@blackwell-systems.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StdioServer: messages should be processed concurrently

1 participant