rmcp::transport::stdio() returns (tokio::io::Stdin, tokio::io::Stdout). In tokio, each read
from and write to these handles goes through the blocking thread pool, so every message pays a
cross-thread hand-off. Under concurrent tools/call, a profile of an rmcp stdio server
shows most of its time in __psynch_mutexwait / cvsignal, not in actual work.
When the server's stdin and stdout are pipes, which is how MCP clients launch servers,
readiness-driven pipes avoid the hand-off:
use std::os::fd::AsFd;
use tokio::net::unix::pipe;
let stdin = std::io::stdin().as_fd().try_clone_to_owned()?;
let stdout = std::io::stdout().as_fd().try_clone_to_owned()?;
// Both fail unless the descriptors are FIFOs; fall back to transport::stdio() then.
let transport = (pipe::Receiver::from_owned_fd(stdin)?, pipe::Sender::from_owned_fd(stdout)?);
The fallback keeps TTYs, files and Windows on the current path.
Numbers: the same trivial tool, 32 concurrent callers over one stdio connection, on an
Apple M3 Pro. Reproducible harness:
https://github.com/igorvieira/Carmy/tree/main/benches/compare
| server |
throughput |
rmcp 3.4 with transport::stdio() |
~41k req/s |
the same rmcp server over tokio::net::unix::pipe |
~86k req/s |
@modelcontextprotocol/sdk (TypeScript), for reference |
~73k req/s |
Would a pipe-based fast path in transport::stdio(), or a separate stdio_pipes()
constructor, be welcome? I'm happy to send a PR.
rmcp::transport::stdio()returns(tokio::io::Stdin, tokio::io::Stdout). In tokio, each readfrom and write to these handles goes through the blocking thread pool, so every message pays a
cross-thread hand-off. Under concurrent
tools/call, a profile of anrmcpstdio servershows most of its time in
__psynch_mutexwait/cvsignal, not in actual work.When the server's stdin and stdout are pipes, which is how MCP clients launch servers,
readiness-driven pipes avoid the hand-off:
The fallback keeps TTYs, files and Windows on the current path.
Numbers: the same trivial tool, 32 concurrent callers over one stdio connection, on an
Apple M3 Pro. Reproducible harness:
https://github.com/igorvieira/Carmy/tree/main/benches/compare
rmcp3.4 withtransport::stdio()rmcpserver overtokio::net::unix::pipe@modelcontextprotocol/sdk(TypeScript), for referenceWould a pipe-based fast path in
transport::stdio(), or a separatestdio_pipes()constructor, be welcome? I'm happy to send a PR.