use crate::ai::mcp::{McpConfig, McpServer};
use crate::ai::types::ToolSpec;
use crate::ai::wire;
use serde_json::Value;
const PREFIX: &str = "mcp__";
#[derive(Default)]
pub struct McpHub {
servers: Vec<(String, McpServer)>,
}
pub fn split_name(full: &str) -> Option<(&str, &str)> {
full.strip_prefix(PREFIX)?.split_once("__")
}
impl McpHub {
pub fn start<'a>(configs: impl IntoIterator<Item = (&'a String, &'a McpConfig)>) -> (Self, Vec<String>) {
let (mut servers, mut errors) = (Vec::new(), Vec::new());
for (name, config) in configs {
match McpServer::start(config) {
Ok(server) => servers.push((name.clone(), server)),
Err(error) => errors.push(format!("MCP «{name}»: {error}")),
}
}
(Self { servers }, errors)
}
pub fn is_empty(&self) -> bool {
self.servers.is_empty()
}
pub fn specs(&self) -> Vec<ToolSpec> {
self.servers
.iter()
.flat_map(|(server, handle)| {
handle.tools.iter().map(move |tool| ToolSpec {
name: wire::tool_name(&format!("{PREFIX}{server}__{}", tool.name)),
description: format!("[{server}] {}", tool.description),
schema: wire::object_schema(&tool.schema),
})
})
.collect()
}
pub fn call(&self, full_name: &str, input: &Value) -> Result<String, String> {
let found = self.servers.iter().find_map(|(server, handle)| {
let tool = handle
.tools
.iter()
.find(|tool| wire::tool_name(&format!("{PREFIX}{server}__{}", tool.name)) == full_name)?;
Some((handle, tool.name.as_str()))
});
let (handle, tool) = found
.ok_or_else(|| format!("«{full_name}»: su servidor MCP no está activo o no ofrece esa herramienta"))?;
match handle.call_tool(tool, input) {
Ok((text, false)) => Ok(text),
Ok((text, true)) => Err(text),
Err(error) => Err(error.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn separa_servidor_y_herramienta() {
assert_eq!(split_name("mcp__git__log"), Some(("git", "log")));
assert_eq!(split_name("mcp__fs__read__file"), Some(("fs", "read__file")));
assert_eq!(split_name("read_file"), None);
}
}