feat(mcp): enhance MCP server registration with error handling and improve transport process management

refactor: update various tools for better error handling and path resolution
fix: improve markdown rendering and input display in TUI
This commit is contained in:
asepharyana
2026-07-20 13:12:04 +07:00
parent 89ee213454
commit 148ba4e07b
17 changed files with 242 additions and 82 deletions
+11 -3
View File
@@ -32,7 +32,13 @@ impl Default for McpManager {
impl McpManager {
pub fn register(&mut self, name: &str, transport: &str) {
/// Register an MCP server by name and transport string.
///
/// Returns an error if a server with the same name is already registered.
pub fn register(&mut self, name: &str, transport: &str) -> anyhow::Result<()> {
if self.servers.contains_key(name) {
anyhow::bail!("MCP server '{name}' is already registered");
}
self.servers.insert(
name.to_string(),
McpServerHandle {
@@ -40,10 +46,12 @@ impl McpManager {
transport: transport.to_string(),
},
);
Ok(())
}
pub fn unregister(&mut self, name: &str) {
self.servers.remove(name);
/// Remove a registered MCP server and return its handle, if it existed.
pub fn unregister(&mut self, name: &str) -> Option<McpServerHandle> {
self.servers.remove(name)
}
pub fn list(&self) -> Vec<McpServerHandle> {
+69 -16
View File
@@ -1,44 +1,97 @@
//! MCP transport layer — manages child-process and HTTP-based transport
//! for connecting to MCP servers.
use std::process::{Child, Command, Stdio};
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 {
pub fn start_child_process(command: &str, args: &[String]) -> anyhow::Result<Self> {
let child = Command::new(command)
.args(args)
/// 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::inherit())
.stderr(Stdio::null())
.spawn()?;
let stdin = child.stdin.take();
let stdout = child.stdout.take();
Ok(McpTransport {
process: Some(child),
stdin,
stdout,
})
}
pub fn stop(&mut self) -> anyhow::Result<()> {
if let Some(mut child) = self.process.take() {
if let Err(e) = child.kill() {
tracing::warn!("MCP transport kill error: {e}");
}
/// 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) {
if let Some(mut child) = self.process.take() {
if let Err(e) = child.kill() {
tracing::warn!("MCP transport kill error: {e}");
}
let _ = child.wait();
}
self.kill();
}
}