1#![cfg(feature = "mcp")]
38
39use std::collections::HashMap;
40use std::path::Path;
41use std::sync::atomic::{AtomicBool, Ordering};
42use std::sync::{Arc, OnceLock};
43
44use modular_agent_core::{AgentContext, AgentError, AgentValue, async_trait};
45use rmcp::{
46 model::{CallToolRequestParam, CallToolResult},
47 service::ServiceExt,
48 transport::{ConfigureCommandExt, TokioChildProcess},
49};
50use serde::Deserialize;
51use tokio::process::Command;
52use tokio::sync::Mutex as AsyncMutex;
53
54use crate::tool::{Tool, ToolInfo, register_tool};
55
56struct MCPTool {
60 server_name: String,
62 server_config: MCPServerConfig,
64 tool: rmcp::model::Tool,
66 info: ToolInfo,
68}
69
70impl MCPTool {
71 fn new(
80 name: String,
81 server_name: String,
82 server_config: MCPServerConfig,
83 tool: rmcp::model::Tool,
84 ) -> Self {
85 let info = ToolInfo::new(
86 name,
87 tool.description.clone().unwrap_or_default().into_owned(),
88 serde_json::to_value(&tool.input_schema).ok(),
89 );
90 Self {
91 server_name,
92 server_config,
93 tool,
94 info,
95 }
96 }
97
98 async fn tool_call(
104 &self,
105 _ctx: AgentContext,
106 value: AgentValue,
107 ) -> Result<AgentValue, AgentError> {
108 let arguments = value.as_object().map(|obj| {
109 obj.iter()
110 .map(|(k, v)| {
111 (
112 k.clone(),
113 serde_json::to_value(v).unwrap_or(serde_json::Value::Null),
114 )
115 })
116 .collect::<serde_json::Map<String, serde_json::Value>>()
117 });
118
119 let entry = {
120 let mut pool = connection_pool().lock().await;
121 pool.get_or_create(&self.server_name, &self.server_config)
122 .await?
123 };
124
125 let tool_result = match self.call_once(&entry, arguments.clone()).await {
126 Ok(result) => result,
127 Err(e) => {
128 log::warn!(
129 "MCP tool call '{}' failed ({}); reconnecting to server '{}' and retrying",
130 self.tool.name,
131 e,
132 self.server_name
133 );
134 entry.dead.store(true, Ordering::Release);
135 let entry = {
136 let mut pool = connection_pool().lock().await;
137 pool.invalidate(&self.server_name);
138 pool.get_or_create(&self.server_name, &self.server_config)
139 .await?
140 };
141 self.call_once(&entry, arguments)
144 .await
145 .inspect_err(|_| entry.dead.store(true, Ordering::Release))?
146 }
147 };
148
149 call_tool_result_to_agent_value(tool_result)
150 }
151
152 async fn call_once(
158 &self,
159 entry: &PoolEntry,
160 arguments: Option<serde_json::Map<String, serde_json::Value>>,
161 ) -> Result<CallToolResult, AgentError> {
162 let connection = entry.conn.lock().await;
163 let service = connection.service.as_ref().ok_or_else(|| {
164 AgentError::Other(format!(
165 "MCP service for '{}' is not available (tool '{}')",
166 self.server_name, self.info.name
167 ))
168 })?;
169 service
170 .call_tool(CallToolRequestParam {
171 name: self.tool.name.clone(),
172 arguments,
173 task: None,
174 })
175 .await
176 .map_err(|e| {
177 AgentError::Other(format!("Failed to call MCP tool '{}': {e}", self.info.name))
178 })
179 }
180}
181
182#[async_trait]
183impl Tool for MCPTool {
184 fn info(&self) -> &ToolInfo {
185 &self.info
186 }
187
188 async fn call(&self, ctx: AgentContext, args: AgentValue) -> Result<AgentValue, AgentError> {
189 self.tool_call(ctx, args).await
190 }
191}
192
193#[derive(Debug, Deserialize)]
210pub struct MCPConfig {
211 #[serde(rename = "mcpServers")]
213 pub mcp_servers: HashMap<String, MCPServerConfig>,
214}
215
216#[derive(Debug, Clone, Deserialize)]
220pub struct MCPServerConfig {
221 pub command: String,
223
224 pub args: Vec<String>,
226
227 #[serde(default)]
229 pub env: Option<HashMap<String, String>>,
230}
231
232type MCPService = rmcp::service::RunningService<rmcp::service::RoleClient, ()>;
234
235struct MCPConnection {
237 service: Option<MCPService>,
239}
240
241#[derive(Clone)]
247struct PoolEntry {
248 conn: Arc<AsyncMutex<MCPConnection>>,
249 dead: Arc<AtomicBool>,
250}
251
252struct MCPConnectionPool {
257 connections: HashMap<String, PoolEntry>,
259}
260
261impl MCPConnectionPool {
262 fn new() -> Self {
264 Self {
265 connections: HashMap::new(),
266 }
267 }
268
269 async fn get_or_create(
274 &mut self,
275 server_name: &str,
276 config: &MCPServerConfig,
277 ) -> Result<PoolEntry, AgentError> {
278 if let Some(entry) = self.connections.get(server_name) {
279 let service_gone = entry
283 .conn
284 .try_lock()
285 .map(|c| c.service.is_none())
286 .unwrap_or(false);
287 if !entry.dead.load(Ordering::Acquire) && !service_gone {
288 log::debug!("Reusing existing MCP connection for '{}'", server_name);
289 return Ok(entry.clone());
290 }
291 log::info!(
292 "Discarding dead MCP connection for '{}', creating a new one",
293 server_name
294 );
295 if let Some(stale) = self.connections.remove(server_name) {
296 cancel_in_background(stale, server_name.to_string());
297 }
298 }
299
300 log::info!(
301 "Starting MCP server '{}' (command: {})",
302 server_name,
303 config.command
304 );
305
306 let service = ()
308 .serve(
309 TokioChildProcess::new(Command::new(&config.command).configure(|cmd| {
310 for arg in &config.args {
311 cmd.arg(arg);
312 }
313 if let Some(env) = &config.env {
314 for (key, value) in env {
315 cmd.env(key, value);
316 }
317 }
318 }))
319 .map_err(|e| {
320 log::error!("Failed to start MCP process for '{}': {}", server_name, e);
321 AgentError::Other(format!(
322 "Failed to start MCP process for '{}': {e}",
323 server_name
324 ))
325 })?,
326 )
327 .await
328 .map_err(|e| {
329 log::error!("Failed to start MCP service for '{}': {}", server_name, e);
330 AgentError::Other(format!(
331 "Failed to start MCP service for '{}': {e}",
332 server_name
333 ))
334 })?;
335
336 log::info!("Successfully started MCP server '{}'", server_name);
337
338 let entry = PoolEntry {
339 conn: Arc::new(AsyncMutex::new(MCPConnection {
340 service: Some(service),
341 })),
342 dead: Arc::new(AtomicBool::new(false)),
343 };
344 self.connections
345 .insert(server_name.to_string(), entry.clone());
346 Ok(entry)
347 }
348
349 fn invalidate(&mut self, server_name: &str) {
354 let is_dead = self
355 .connections
356 .get(server_name)
357 .is_some_and(|e| e.dead.load(Ordering::Acquire));
358 if is_dead && let Some(entry) = self.connections.remove(server_name) {
359 log::info!("Invalidating MCP connection for '{}'", server_name);
360 cancel_in_background(entry, server_name.to_string());
361 }
362 }
363
364 fn take_all(&mut self) -> Vec<(String, PoolEntry)> {
370 self.connections.drain().collect()
371 }
372}
373
374fn cancel_in_background(entry: PoolEntry, server_name: String) {
380 entry.dead.store(true, Ordering::Release);
381 tokio::spawn(async move {
382 let mut connection = entry.conn.lock().await;
383 if let Some(service) = connection.service.take()
384 && let Err(e) = service.cancel().await
385 {
386 log::warn!("Failed to cancel MCP service '{}': {}", server_name, e);
387 }
388 });
389}
390
391static CONNECTION_POOL: OnceLock<AsyncMutex<MCPConnectionPool>> = OnceLock::new();
393
394fn connection_pool() -> &'static AsyncMutex<MCPConnectionPool> {
396 CONNECTION_POOL.get_or_init(|| AsyncMutex::new(MCPConnectionPool::new()))
397}
398
399pub async fn shutdown_all_mcp_connections() -> Result<(), AgentError> {
426 log::info!("Shutting down all MCP server connections");
427 let entries = { connection_pool().lock().await.take_all() };
428 for (name, entry) in entries {
429 entry.dead.store(true, Ordering::Release);
430 let conn = entry.conn.clone();
435 match tokio::time::timeout(std::time::Duration::from_secs(5), conn.lock()).await {
436 Ok(mut connection) => {
437 if let Some(service) = connection.service.take() {
438 if let Err(e) = service.cancel().await {
439 log::error!("Failed to cancel MCP service '{}': {}", name, e);
440 } else {
441 log::debug!("Successfully shut down MCP server '{}'", name);
442 }
443 }
444 }
445 Err(_) => {
446 log::warn!(
447 "MCP connection '{}' busy during shutdown; cancelling in background",
448 name
449 );
450 cancel_in_background(entry, name);
451 }
452 }
453 }
454 log::info!("All MCP server connections shut down");
455 Ok(())
456}
457
458async fn register_tools_from_server(
472 server_name: String,
473 server_config: MCPServerConfig,
474) -> Result<Vec<String>, AgentError> {
475 log::debug!("Registering tools from MCP server '{}'", server_name);
476
477 let entry = {
479 let mut pool = connection_pool().lock().await;
480 pool.get_or_create(&server_name, &server_config).await?
481 };
482
483 log::debug!("Listing tools from MCP server '{}'", server_name);
485 let tools_list = {
486 let connection = entry.conn.lock().await;
487 let service = connection.service.as_ref().ok_or_else(|| {
488 log::error!("MCP service for '{}' is not available", server_name);
489 AgentError::Other(format!(
490 "MCP service for '{}' is not available",
491 server_name
492 ))
493 })?;
494 service.list_tools(Default::default()).await.map_err(|e| {
495 log::error!("Failed to list MCP tools for '{}': {}", server_name, e);
496 AgentError::Other(format!(
497 "Failed to list MCP tools for '{}': {e}",
498 server_name
499 ))
500 })?
501 };
502
503 let mut registered_tool_names = Vec::new();
504
505 for tool_info in tools_list.tools {
507 let mcp_tool_name = format!("{}::{}", server_name, tool_info.name);
508 registered_tool_names.push(mcp_tool_name.clone());
509
510 register_tool(MCPTool::new(
511 mcp_tool_name.clone(),
512 server_name.clone(),
513 server_config.clone(),
514 tool_info,
515 ));
516 log::debug!("Registered MCP tool '{}'", mcp_tool_name);
517 }
518
519 log::info!(
520 "Registered {} tools from MCP server '{}'",
521 registered_tool_names.len(),
522 server_name
523 );
524
525 Ok(registered_tool_names)
526}
527
528pub async fn register_tools_from_mcp_json<P: AsRef<Path>>(
548 json_path: P,
549) -> Result<Vec<String>, AgentError> {
550 let path = json_path.as_ref();
551 log::info!("Loading MCP configuration from: {}", path.display());
552
553 let json_content = std::fs::read_to_string(path).map_err(|e| {
555 log::error!("Failed to read MCP config file '{}': {}", path.display(), e);
556 AgentError::Other(format!("Failed to read MCP config file: {e}"))
557 })?;
558
559 let config: MCPConfig = serde_json::from_str(&json_content).map_err(|e| {
561 log::error!("Failed to parse MCP config JSON: {}", e);
562 AgentError::Other(format!("Failed to parse MCP config JSON: {e}"))
563 })?;
564
565 log::info!("Found {} MCP servers in config", config.mcp_servers.len());
566
567 let mut registered_tool_names = Vec::new();
568
569 for (server_name, server_config) in config.mcp_servers {
571 let tools = register_tools_from_server(server_name, server_config).await?;
572 registered_tool_names.extend(tools);
573 }
574
575 log::info!(
576 "Successfully registered {} MCP tools total",
577 registered_tool_names.len()
578 );
579
580 Ok(registered_tool_names)
581}
582
583fn call_tool_result_to_agent_value(result: CallToolResult) -> Result<AgentValue, AgentError> {
588 let mut contents = Vec::new();
589 for c in result.content.iter() {
590 match &c.raw {
591 rmcp::model::RawContent::Text(text) => {
592 contents.push(AgentValue::string(text.text.clone()));
593 }
594 _ => {
595 }
597 }
598 }
599 let data = AgentValue::array(contents.into());
600 if result.is_error == Some(true) {
601 return Err(AgentError::Other(
602 serde_json::to_string(&data).map_err(|e| AgentError::InvalidValue(e.to_string()))?,
603 ));
604 }
605 Ok(data)
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611
612 fn test_entry(dead: bool) -> PoolEntry {
613 PoolEntry {
614 conn: Arc::new(AsyncMutex::new(MCPConnection { service: None })),
615 dead: Arc::new(AtomicBool::new(dead)),
616 }
617 }
618
619 fn bogus_config() -> MCPServerConfig {
620 MCPServerConfig {
621 command: "modular-agent-test-nonexistent-command".to_string(),
622 args: Vec::new(),
623 env: None,
624 }
625 }
626
627 #[tokio::test]
628 async fn invalidate_removes_dead_entry() {
629 let mut pool = MCPConnectionPool::new();
630 pool.connections.insert("s".to_string(), test_entry(true));
631 pool.invalidate("s");
632 assert!(pool.connections.is_empty());
633 }
634
635 #[tokio::test]
636 async fn invalidate_keeps_entry_not_marked_dead() {
637 let mut pool = MCPConnectionPool::new();
640 pool.connections.insert("s".to_string(), test_entry(false));
641 pool.invalidate("s");
642 assert!(pool.connections.contains_key("s"));
643 }
644
645 #[tokio::test]
646 async fn get_or_create_reuses_busy_connection() {
647 let mut pool = MCPConnectionPool::new();
648 let entry = test_entry(false);
649 pool.connections.insert("s".to_string(), entry.clone());
650 let _guard = entry.conn.try_lock().unwrap();
654 let got = pool.get_or_create("s", &bogus_config()).await.unwrap();
655 assert!(Arc::ptr_eq(&got.conn, &entry.conn));
656 }
657
658 #[tokio::test]
659 async fn get_or_create_discards_dead_entry() {
660 let mut pool = MCPConnectionPool::new();
661 pool.connections.insert("s".to_string(), test_entry(true));
662 assert!(pool.get_or_create("s", &bogus_config()).await.is_err());
665 assert!(pool.connections.is_empty());
666 }
667
668 #[tokio::test]
669 async fn get_or_create_discards_entry_with_missing_service() {
670 let mut pool = MCPConnectionPool::new();
671 pool.connections.insert("s".to_string(), test_entry(false));
672 assert!(pool.get_or_create("s", &bogus_config()).await.is_err());
673 assert!(pool.connections.is_empty());
674 }
675}