refactor: update various tools for better error handling and path resolution fix: improve markdown rendering and input display in TUI
98 lines
3.0 KiB
Rust
98 lines
3.0 KiB
Rust
//! MCP transport layer — manages child-process and HTTP-based transport
|
|
//! for connecting to MCP servers.
|
|
|
|
use std::{
|
|
io::{Read, Write},
|
|
process::{Child, ChildStdin, ChildStdout, Command, Stdio},
|
|
};
|
|
|
|
/// A running MCP server process connected via stdio.
|
|
///
|
|
/// Holds the child process handle plus the piped stdin/stdout streams
|
|
/// so callers can send JSON-RPC messages and read responses.
|
|
pub struct McpTransport {
|
|
process: Option<Child>,
|
|
stdin: Option<ChildStdin>,
|
|
stdout: Option<ChildStdout>,
|
|
}
|
|
|
|
impl McpTransport {
|
|
/// Spawn a child process as an MCP server over stdio.
|
|
///
|
|
/// The command is passed to `sh -c` so shell syntax (pipes, redirects, etc.)
|
|
/// works naturally. Stderr is discarded to avoid corrupting a TUI that may
|
|
/// be running in the same terminal.
|
|
pub fn start_child_process(name: &str, command: &str) -> anyhow::Result<Self> {
|
|
tracing::info!("starting MCP transport '{name}': {command}");
|
|
let mut child = Command::new("sh")
|
|
.arg("-c")
|
|
.arg(command)
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::null())
|
|
.spawn()?;
|
|
let stdin = child.stdin.take();
|
|
let stdout = child.stdout.take();
|
|
Ok(McpTransport {
|
|
process: Some(child),
|
|
stdin,
|
|
stdout,
|
|
})
|
|
}
|
|
|
|
/// Write raw bytes to the child's stdin.
|
|
pub fn send(&mut self, data: &[u8]) -> anyhow::Result<()> {
|
|
if let Some(ref mut stdin) = self.stdin {
|
|
stdin.write_all(data)?;
|
|
stdin.flush()?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Read from the child's stdout into the provided buffer.
|
|
///
|
|
/// Returns `Ok(Some(n))` with the number of bytes read,
|
|
/// `Ok(None)` on EOF, or `Err` on I/O errors.
|
|
pub fn receive(&mut self, buf: &mut [u8]) -> anyhow::Result<Option<usize>> {
|
|
match self.stdout.as_mut() {
|
|
Some(stdout) => match stdout.read(buf) {
|
|
Ok(0) => Ok(None),
|
|
Ok(n) => Ok(Some(n)),
|
|
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None),
|
|
Err(e) => Err(e.into()),
|
|
},
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
|
|
/// Gracefully shut down the child by closing stdin (sending EOF) and
|
|
/// then killing the process.
|
|
pub fn kill(&mut self) {
|
|
// Close stdin first to signal EOF to the MCP server.
|
|
let _ = self.stdin.take();
|
|
if let Some(ref mut child) = self.process {
|
|
let _ = child.kill();
|
|
let _ = child.wait();
|
|
}
|
|
}
|
|
|
|
/// Check whether the child process is still running.
|
|
pub fn is_running(&mut self) -> bool {
|
|
self.process
|
|
.as_mut()
|
|
.is_some_and(|c| matches!(c.try_wait(), Ok(None)))
|
|
}
|
|
|
|
/// Stop the child process. This is the public API alias for `kill`.
|
|
pub fn stop(&mut self) -> anyhow::Result<()> {
|
|
self.kill();
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Drop for McpTransport {
|
|
fn drop(&mut self) {
|
|
self.kill();
|
|
}
|
|
}
|