1mod connection;
3mod tool;
4mod lazy;
5
6use std::collections::HashMap;
7use std::sync::Arc;
8use tokio::sync::Mutex;
9use crate::ToolRegistry;
10
11pub use tool::McpTool;
12pub use lazy::McpConnectTool;
13
14#[derive(Debug, Clone, serde::Deserialize)]
16pub struct McpServerConfig {
17 pub command: String,
18 #[serde(default)]
19 pub args: Vec<String>,
20 #[serde(default)]
21 pub env: HashMap<String, String>,
22}
23
24#[derive(Debug, Clone, serde::Deserialize)]
26pub struct McpConfig {
27 #[serde(rename = "mcpServers", default)]
28 pub mcp_servers: HashMap<String, McpServerConfig>,
29}
30
31#[derive(Debug, Clone)]
33pub(crate) struct McpToolDef {
34 pub(crate) name: String,
35 pub(crate) description: String,
36 pub(crate) input_schema: serde_json::Value,
37}
38
39pub fn load_mcp_config() -> Option<McpConfig> {
41 let path = crate::config::resolve_read_path("mcp.json");
42 if !path.exists() {
43 return None;
44 }
45
46 let content = std::fs::read_to_string(&path).ok()?;
47 match serde_json::from_str::<McpConfig>(&content) {
48 Ok(config) => Some(config),
49 Err(e) => {
50 tracing::warn!("Failed to parse MCP config at {}: {}", path.display(), e);
51 None
52 }
53 }
54}
55
56pub async fn connect_mcp_servers(registry: &mut ToolRegistry) -> usize {
59 let config = match load_mcp_config() {
60 Some(c) => c,
61 None => return 0,
62 };
63
64 let mut total_tools = 0;
65
66 for (server_name, server_config) in &config.mcp_servers {
67 tracing::info!(server = %server_name, command = %server_config.command, "Connecting to MCP server");
68
69 match connection::McpConnection::start(server_config).await {
70 Ok(mut conn) => {
71 match conn.list_tools().await {
72 Ok(tools) => {
73 let tool_count = tools.len();
74 let connection = Arc::new(Mutex::new(conn));
75
76 for tool_def in tools {
77 let prefixed_name = format!("ext__{}__{}", server_name, tool_def.name);
80
81 let mcp_tool = McpTool {
82 tool_name: prefixed_name,
83 server_tool_name: tool_def.name.clone(),
84 server_name: server_name.clone(),
85 description: format!("[MCP:{}] {}", server_name, tool_def.description),
86 input_schema: tool_def.input_schema,
87 connection: Arc::clone(&connection),
88 };
89
90 registry.register(Arc::new(mcp_tool));
91 total_tools += 1;
92 }
93
94 tracing::info!(
95 server = %server_name,
96 tools = tool_count,
97 "MCP server connected — {} tools registered",
98 tool_count
99 );
100 }
101 Err(e) => {
102 tracing::error!(server = %server_name, error = %e, "Failed to list MCP tools");
103 }
104 }
105 }
106 Err(e) => {
107 tracing::error!(server = %server_name, error = %e, "Failed to connect to MCP server");
108 }
109 }
110 }
111
112 total_tools
113}
114
115pub async fn setup_lazy_mcp(registry: &Arc<tokio::sync::RwLock<crate::ToolRegistry>>) -> usize {
118 let config = match load_mcp_config() {
119 Some(c) => c,
120 None => return 0,
121 };
122
123 let server_count = config.mcp_servers.len();
124 if server_count == 0 {
125 return 0;
126 }
127
128 let server_names: Vec<&str> = config.mcp_servers.keys().map(|s| s.as_str()).collect();
129 tracing::info!(servers = ?server_names, "MCP lazy loading: {} servers available", server_count);
130
131 let connect_tool = McpConnectTool::new(
132 config.mcp_servers,
133 );
134
135 registry.write().await.register(Arc::new(connect_tool));
136
137 server_count
138}
139
140#[cfg(test)]
141mod tests {
142 use serde_json::json;
143 use super::*;
144
145 #[test]
146 fn test_mcp_config_deserialize() {
147 let json_str = r#"{"mcpServers": {"test": {"command": "echo", "args": ["hi"]}}}"#;
148 let config: McpConfig = serde_json::from_str(json_str).unwrap();
149
150 assert_eq!(config.mcp_servers.len(), 1);
151 assert!(config.mcp_servers.contains_key("test"));
152
153 let server = &config.mcp_servers["test"];
154 assert_eq!(server.command, "echo");
155 assert_eq!(server.args, vec!["hi"]);
156 }
157
158 #[test]
159 fn test_mcp_config_empty_servers() {
160 let json_str = r#"{"mcpServers": {}}"#;
161 let config: McpConfig = serde_json::from_str(json_str).unwrap();
162
163 assert_eq!(config.mcp_servers.len(), 0);
164 assert!(config.mcp_servers.is_empty());
165 }
166
167 #[test]
168 fn test_mcp_server_config_defaults() {
169 let json_str = r#"{"command": "echo"}"#;
170 let server_config: McpServerConfig = serde_json::from_str(json_str).unwrap();
171
172 assert_eq!(server_config.command, "echo");
173 assert_eq!(server_config.args, Vec::<String>::new());
174 assert_eq!(server_config.env, HashMap::new());
175 }
176
177 #[test]
178 fn test_mcp_config_deserialize_from_value() {
179 let json_value = json!({
180 "mcpServers": {
181 "test": {
182 "command": "echo",
183 "args": ["hi"]
184 }
185 }
186 });
187
188 let config: McpConfig = serde_json::from_value(json_value).unwrap();
189
190 assert_eq!(config.mcp_servers.len(), 1);
191 assert!(config.mcp_servers.contains_key("test"));
192
193 let server = &config.mcp_servers["test"];
194 assert_eq!(server.command, "echo");
195 assert_eq!(server.args, vec!["hi"]);
196 }
197
198 #[test]
199 fn test_load_mcp_config_returns_some_or_none() {
200 let result = load_mcp_config();
203
204 match result {
207 Some(_config) => {
208 }
211 None => {
212 }
215 }
216 }
217}