#[cfg(feature = "mcp")]
pub mod config;
#[cfg(feature = "mcp")]
pub mod tool;
#[cfg(feature = "mcp")]
use crate::tools::{ToolDyn, ToolRegistry};
#[cfg(feature = "mcp")]
use anyhow::{Context, Result};
#[cfg(feature = "mcp")]
use choreo_mcp::{McpClient, McpServerConfig};
#[cfg(feature = "mcp")]
use std::collections::HashMap;
#[cfg(feature = "mcp")]
use std::sync::{Arc, Mutex};
#[cfg(feature = "mcp")]
use tool::McpToolWrapper;
#[cfg(feature = "mcp")]
use tracing::{debug, error, info, warn};
#[cfg(feature = "mcp")]
pub struct McpManager {
clients: HashMap<String, Arc<Mutex<McpClient>>>,
}
#[cfg(feature = "mcp")]
impl McpManager {
fn spawn_server(cfg: &McpServerConfig) -> Result<McpClient> {
info!(
server = %cfg.slug,
command = %cfg.command,
"spawning MCP server"
);
McpClient::spawn(cfg).with_context(|| format!("failed to spawn MCP server '{}'", cfg.slug))
}
fn register_server_tools(
slug: &str,
client: &mut McpClient,
registry: &mut ToolRegistry,
shared: Arc<Mutex<McpClient>>,
) {
match client.list_tools() {
Ok(tools) => {
registry.register_dynamic_group(
format!("mcp/{slug}"),
format!("MCP server: {}", client.server_name()),
);
info!(
server = %slug,
name = %client.server_name(),
tool_count = tools.len(),
"registered MCP server tools"
);
for mcp_tool in tools {
let description = mcp_tool.description.unwrap_or_default();
let wrapper = McpToolWrapper::new(
slug,
&mcp_tool.name,
&description,
mcp_tool.input_schema,
Arc::clone(&shared),
);
registry.register_dynamic(
wrapper.name().to_string(),
wrapper.group().to_string(),
Box::new(wrapper),
);
}
}
Err(e) => {
error!(
server = %slug,
error = %e,
"failed to list MCP tools, shutting down server"
);
}
}
}
pub fn from_config(registry: &mut ToolRegistry) -> Self {
let configs = match config::load_mcp_config() {
Ok(c) => c,
Err(e) => {
warn!("failed to load MCP config: {e}");
Vec::new()
}
};
let mut manager = Self {
clients: HashMap::new(),
};
let mut handles: Vec<(String, std::thread::JoinHandle<anyhow::Result<McpClient>>)> =
Vec::new();
for cfg in &configs {
let slug = cfg.slug.clone();
let cfg_clone = cfg.clone();
let handle = std::thread::spawn(move || Self::spawn_server(&cfg_clone));
handles.push((slug, handle));
}
for (slug, handle) in handles {
match handle.join() {
Ok(Ok(client)) => {
let shared = Arc::new(Mutex::new(client));
let mut guard = match shared.lock() {
Ok(g) => g,
Err(e) => {
error!(
server = %slug,
"MCP client lock poisoned: {e}"
);
continue;
}
};
Self::register_server_tools(&slug, &mut guard, registry, Arc::clone(&shared));
drop(guard);
manager.clients.insert(slug, shared);
}
Ok(Err(e)) => {
error!(server = %slug, error = %e, "failed to spawn MCP server");
}
Err(_) => {
error!(server = %slug, "MCP server spawn thread panicked");
}
}
}
manager
}
pub fn shutdown_all(&mut self) {
info!(count = self.clients.len(), "shutting down MCP servers");
for (slug, shared) in self.clients.drain() {
debug!(server = %slug, "waiting for MCP client lock + shutdown");
match shared.lock() {
Ok(mut client) => {
debug!(server = %slug, "shutting down MCP server");
client.shutdown();
}
Err(e) => {
warn!(
server = %slug,
"MCP client lock poisoned during shutdown: {e}"
);
}
}
debug!(server = %slug, "MCP server shut down");
}
info!("all MCP servers shut down");
}
pub fn empty() -> Self {
Self {
clients: HashMap::new(),
}
}
pub fn clients(&self) -> &HashMap<String, Arc<Mutex<McpClient>>> {
&self.clients
}
}
#[cfg(feature = "mcp")]
impl Drop for McpManager {
fn drop(&mut self) {
self.shutdown_all();
}
}
#[cfg(not(feature = "mcp"))]
mod imp {
use crate::tools::ToolRegistry;
pub struct McpManager;
impl McpManager {
pub fn from_config(_registry: &mut ToolRegistry) -> Self {
Self
}
pub fn shutdown_all(&mut self) {}
pub fn empty() -> Self {
Self
}
}
}
#[cfg(not(feature = "mcp"))]
pub use imp::McpManager;
#[cfg(test)]
#[cfg(feature = "mcp")]
mod tests {
use super::*;
#[test]
fn empty_creates_manager_with_no_clients() {
let manager = McpManager::empty();
assert!(manager.clients().is_empty());
}
#[test]
fn shutdown_all_on_empty_is_noop() {
let mut manager = McpManager::empty();
manager.shutdown_all();
assert!(manager.clients().is_empty());
}
#[test]
fn drop_empty_manager_is_noop() {
let manager = McpManager::empty();
drop(manager);
}
#[test]
fn from_config_with_no_file_creates_empty() {
let mut registry = crate::tools::ToolRegistry::new();
let manager = McpManager::from_config(&mut registry);
assert!(manager.clients().is_empty());
}
}