Serialize concurrent MCP stdin writes (#33180)

## What changed

- Guard executor-backed MCP stdio sends with a single-permit semaphore so a
  second JSON-RPC message cannot start writing while the first write is pending.
- Add a regression test that drives two concurrent sends and verifies the
  process receives each newline-delimited message in order without overlap.

GitOrigin-RevId: 30c2e51827ecf57b5eb23d9ad1e876734d6e784e
This commit is contained in:
Adam Perry @ OpenAI authored and copyberry committed 2026-07-14 22:16:52 +00:00
1 parent 68c6e5df2b
commit 44954d1b4b
2 files changed
+143

No files matched your search

@@ -40,6 +40,7 @@ use rmcp::transport::Transport;
use serde_json::from_slice;
use serde_json::to_vec;
use tokio::runtime::Handle;
use tokio::sync::Semaphore;
use tokio::sync::broadcast;
use tracing::debug;
use tracing::info;
@@ -162,6 +163,10 @@ pub(super) struct ExecutorProcessTransport {
/// closes the transport.
process: Arc<dyn ExecProcess>,
/// Prevents concurrent rmcp send futures from issuing overlapping stdin writes.
/// The single-slot semaphore gives mutex semantics while its permit can safely cross `.await`.
stdin_write_semaphore: Arc<Semaphore>,
/// Pushed output/lifecycle stream for the process.
///
/// The executor process API still supports retained-output reads, but MCP
@@ -204,6 +209,7 @@ impl ExecutorProcessTransport {
let events = process.subscribe_events();
Self {
process,
stdin_write_semaphore: Arc::new(Semaphore::new(1)),
events,
program_name,
stdout: LineBuffer::default(),
@@ -231,7 +237,12 @@ impl Transport<RoleClient> for ExecutorProcessTransport {
item: TxJsonRpcMessage<RoleClient>,
) -> impl Future<Output = std::result::Result<(), Self::Error>> + Send + 'static {
let process = Arc::clone(&self.process);
let stdin_write_semaphore = Arc::clone(&self.stdin_write_semaphore);
async move {
let _stdin_write_permit = stdin_write_semaphore
.acquire()
.await
.map_err(io::Error::other)?;
// rmcp hands us a structured JSON-RPC message. Stdio transport on
// the wire is JSON plus one newline delimiter.
let mut bytes = to_vec(&item).map_err(io::Error::other)?;