telos_agent/integrations/mcp/
manager.rs1use std::collections::HashMap;
2use std::path::Path;
3
4use serde_json::Value;
5use tokio::sync::Mutex;
6
7use crate::error::AgentError;
8use crate::integrations::mcp::client::{McpClient, McpTool};
9use crate::integrations::mcp::config::McpServerConfig;
10
11pub struct McpManager {
13 servers: Mutex<HashMap<String, McpServerHandle>>,
14}
15
16struct McpServerHandle {
17 config: McpServerConfig,
18 client: McpClient,
19 connected: bool,
20}
21
22impl McpManager {
23 pub fn new(servers: HashMap<String, McpServerConfig>) -> Self {
25 let handles = servers
26 .into_iter()
27 .map(|(id, config)| {
28 let client = McpClient::new(config.clone());
29 (id, McpServerHandle { config, client, connected: false })
30 })
31 .collect();
32 Self { servers: Mutex::new(handles) }
33 }
34
35 pub fn load_config(path: &Path) -> Result<Self, AgentError> {
42 if !path.exists() {
43 return Ok(Self { servers: Mutex::new(HashMap::new()) });
44 }
45 let content = std::fs::read_to_string(path)
46 .map_err(|e| AgentError::Config(format!("failed to read MCP config: {e}")))?;
47 let config: Value = serde_json::from_str(&content)
48 .map_err(|e| AgentError::Config(format!("failed to parse MCP config: {e}")))?;
49 let servers_map = config.get("mcpServers").and_then(|v| v.as_object());
50 let mut servers = HashMap::new();
51 if let Some(map) = servers_map {
52 for (name, server_cfg) in map {
53 let command = server_cfg.get("command").and_then(|v| v.as_str()).unwrap_or("");
54 if command.is_empty() {
55 tracing::warn!(
56 server = %name,
57 "MCP server config missing 'command' field — skipping"
58 );
59 continue;
60 }
61 let args: Vec<String> = server_cfg
62 .get("args")
63 .and_then(|v| v.as_array())
64 .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
65 .unwrap_or_default();
66 let auto_connect =
67 server_cfg.get("auto_connect").and_then(|v| v.as_bool()).unwrap_or(true);
68 let cfg = McpServerConfig {
69 command: command.to_string(),
70 args,
71 auto_connect,
72 ..McpServerConfig::default()
73 };
74 servers.insert(name.clone(), cfg);
75 }
76 }
77 Ok(Self::new(servers))
78 }
79
80 pub async fn register_server(&self, id: String, config: McpServerConfig) {
83 let mut servers = self.servers.lock().await;
84 let client = McpClient::new(config.clone());
85 servers.insert(id, McpServerHandle { config, client, connected: false });
86 }
87
88 pub async fn register_servers(&self, new_servers: HashMap<String, McpServerConfig>) {
90 let mut servers = self.servers.lock().await;
91 for (id, config) in new_servers {
92 let client = McpClient::new(config.clone());
93 servers.insert(id, McpServerHandle { config, client, connected: false });
94 }
95 }
96
97 pub async fn connect_all(&self) {
99 let mut servers = self.servers.lock().await;
100 for (id, handle) in servers.iter_mut() {
101 if handle.config.auto_connect && !handle.connected {
102 match handle.client.connect().await {
103 Ok(()) => {
104 handle.connected = true;
105 tracing::info!(server = %id, "MCP server connected");
106 }
107 Err(e) => {
108 tracing::warn!(server = %id, error = %e, "MCP server failed to connect");
109 }
110 }
111 }
112 }
113 }
114
115 pub async fn all_tools(&self) -> Vec<(String, McpTool)> {
119 let servers = self.servers.lock().await;
120 servers
121 .iter()
122 .filter(|(_, h)| h.connected)
123 .flat_map(|(id, h)| h.client.tools().into_iter().map(move |t| (id.clone(), t)))
124 .collect()
125 }
126
127 pub async fn call_tool(
129 &self,
130 server_id: &str,
131 tool_name: &str,
132 args: Value,
133 ) -> Result<Value, AgentError> {
134 let servers = self.servers.lock().await;
135 let handle = servers.get(server_id).ok_or_else(|| {
136 AgentError::ToolNotFound(format!("MCP server '{server_id}' not found"))
137 })?;
138 handle.client.call_tool(tool_name, args).await
139 }
140
141 pub async fn disconnect_all(&self) {
143 let servers = self.servers.lock().await;
144 for (_, handle) in servers.iter() {
145 handle.client.disconnect();
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use std::path::PathBuf;
154
155 #[test]
156 fn load_config_file_not_found() {
157 let path = PathBuf::from("/tmp/nonexistent-mcp-config.json");
158 let manager =
159 McpManager::load_config(&path).expect("load_config should succeed for missing file");
160 assert!(manager.servers.try_lock().unwrap().is_empty());
161 }
162
163 #[test]
164 fn load_config_valid_json() {
165 let dir = tempfile::tempdir().expect("failed to create temp dir");
166 let config_path = dir.path().join("mcp.json");
167 let json = r#"{
168 "mcpServers": {
169 "fs": {
170 "command": "npx",
171 "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
172 "auto_connect": true
173 },
174 "noargs": {
175 "command": "echo",
176 "auto_connect": false
177 }
178 }
179 }"#;
180 std::fs::write(&config_path, json).unwrap();
181
182 let manager = McpManager::load_config(&config_path)
183 .expect("load_config should succeed for valid JSON");
184 let servers = manager.servers.try_lock().unwrap();
185 assert_eq!(servers.len(), 2);
186 let fs = servers.get("fs").expect("fs server should exist");
187 assert_eq!(fs.config.command, "npx");
188 assert_eq!(fs.config.args, vec!["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]);
189 assert!(fs.config.auto_connect);
190
191 let noargs = servers.get("noargs").expect("noargs server should exist");
192 assert_eq!(noargs.config.command, "echo");
193 assert!(!noargs.config.auto_connect);
194 }
195
196 #[test]
197 fn load_config_invalid_json_returns_error() {
198 let dir = tempfile::tempdir().expect("failed to create temp dir");
199 let config_path = dir.path().join("bad.json");
200 std::fs::write(&config_path, "not json").unwrap();
201
202 let result = McpManager::load_config(&config_path);
203 match result {
204 Err(AgentError::Config(_)) => {} Err(e) => panic!("expected AgentError::Config, got: {e}"),
206 Ok(_) => panic!("expected error, got Ok"),
207 }
208 }
209
210 #[test]
211 fn new_creates_empty_manager() {
212 let manager = McpManager::new(HashMap::new());
213 let servers = manager.servers.try_lock().unwrap();
214 assert!(servers.is_empty());
215 }
216
217 #[test]
218 fn new_creates_manager_with_servers() {
219 let mut servers = HashMap::new();
220 servers.insert("test".to_string(), McpServerConfig::new("echo", vec![]));
221 let manager = McpManager::new(servers);
222 let handles = manager.servers.try_lock().unwrap();
223 assert_eq!(handles.len(), 1);
224 let handle = handles.get("test").unwrap();
225 assert_eq!(handle.config.command, "echo");
226 assert!(!handle.connected);
227 }
228}