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:
@@ -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> {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user