Skip to content

Commit 718bc60

Browse files
authored
feat(transport): add non-blocking stdio_pipes transport (#1304)
* feat(transport): add non-blocking stdio_pipes transport `transport::stdio()` reads and writes through Tokio's blocking thread pool, so every message on a piped stdio connection pays a cross-thread hand-off. MCP clients launch stdio servers with FIFOs for stdin/stdout, which Tokio's Unix pipe driver can drive from the IO reactor instead. `stdio_pipes()` wraps the standard streams as non-blocking pipes when both are FIFOs and otherwise falls back to the blocking `stdio()` handles, so the returned `(StdioReader, StdioWriter)` pair still converts into a transport on every platform. TTYs, files, sockets, and Windows keep today's behavior. `stdio()` is left unchanged because changing its return type would be a breaking public API change; this constructor is additive. The `transport-io` feature now enables `tokio/net` for the pipe types. Refs #1301 * fix(transport): reopen the standard streams on Linux try_clone_to_owned duplicates the descriptor, so the duplicate shares its open file description with fd 0 or 1. Tokio's O_NONBLOCK then leaked into the process's own standard streams, and into children that inherit them. stdio_pipes() now opens /proc/self/fd/0 and /1 with O_NONBLOCK on Linux. The kernel gives the new descriptors their own file descriptions, so the flags stay private to the transport. macOS has no /proc, so it keeps the blocking fallback; the pipe variant is Linux-only now. Tests: reopening a FIFO keeps the source descriptor blocking, descriptor 0 keeps its flags across stdio_pipes(), and the round-trip and non-FIFO cases moved to Linux. Signed-off-by: itsmunzir <itsmunzir@users.noreply.github.com> * fix(transport): address review feedback on stdio_pipes - Reopen fd 0 and 1 first, then check the FIFOs on the descriptors that go to Tokio. - Keep the platform-specific handle in private enums with Debug. Callers only need AsyncRead/AsyncWrite. - Log the taken path with tracing::debug!. - Drop the vacuous stdin-flags test; the owned-FIFO round trip covers the reopen path on the runner. Signed-off-by: itsmunzir <munzirbh@gmail.com> * fix(transport): let the FIFO child see EOF in the round-trip test Signed-off-by: itsmunzir <munzirbh@gmail.com> --------- Signed-off-by: itsmunzir <itsmunzir@users.noreply.github.com> Signed-off-by: itsmunzir <munzirbh@gmail.com> Co-authored-by: itsmunzir <itsmunzir@users.noreply.github.com>
1 parent e957a53 commit 718bc60

3 files changed

Lines changed: 386 additions & 2 deletions

File tree

‎crates/rmcp/Cargo.toml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ chrono = { version = "0.4.38", default-features = false, features = [
122122
"oldtime",
123123
] }
124124

125+
# the stdio pipe transport reopens `/proc/self/fd` with `O_NONBLOCK`
126+
[target.'cfg(target_os = "linux")'.dependencies]
127+
libc = "0.2"
128+
125129
[features]
126130
default = ["base64", "macros", "server"]
127131
local = ["rmcp-macros?/local"]
@@ -174,7 +178,7 @@ transport-streamable-http-client-unix-socket = [
174178
]
175179

176180
transport-async-rw = ["tokio/io-util", "tokio-util/codec"]
177-
transport-io = ["transport-async-rw", "tokio/io-std"]
181+
transport-io = ["transport-async-rw", "tokio/io-std", "tokio/net"]
178182
transport-child-process = [
179183
"transport-async-rw",
180184
"tokio/process",

‎crates/rmcp/src/transport.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ pub use child_process::{ConfigureCommandExt, TokioChildProcess};
9191
#[cfg(feature = "transport-io")]
9292
pub mod io;
9393
#[cfg(feature = "transport-io")]
94-
pub use io::stdio;
94+
pub use io::{StdioReader, StdioWriter, stdio, stdio_pipes};
9595

9696
#[cfg(feature = "auth")]
9797
pub mod auth;

0 commit comments

Comments
 (0)