Skip to main content

soul_core/executor/
mcp.rs

1//! MCP executor — delegates tool calls to MCP servers.
2
3use std::sync::Arc;
4
5use tokio::sync::{mpsc, Mutex};
6
7use crate::error::SoulResult;
8use crate::mcp::McpClient;
9use crate::tool::ToolOutput;
10use crate::types::ToolDefinition;
11
12use super::ToolExecutor;
13
14/// Executes tools by forwarding to an MCP server.
15pub struct McpExecutor {
16    client: Arc<Mutex<McpClient>>,
17}
18
19impl McpExecutor {
20    pub fn new(client: Arc<Mutex<McpClient>>) -> Self {
21        Self { client }
22    }
23}
24
25#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
26#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
27impl ToolExecutor for McpExecutor {
28    async fn execute(
29        &self,
30        definition: &ToolDefinition,
31        _call_id: &str,
32        arguments: serde_json::Value,
33        _partial_tx: Option<mpsc::UnboundedSender<String>>,
34    ) -> SoulResult<ToolOutput> {
35        let client = self.client.lock().await;
36        let result = client.call_tool(&definition.name, arguments).await?;
37
38        let text: String = result
39            .content
40            .iter()
41            .filter_map(|c| match c {
42                crate::mcp::McpContent::Text { text } => Some(text.as_str()),
43                _ => None,
44            })
45            .collect::<Vec<_>>()
46            .join("\n");
47
48        if result.is_error {
49            Ok(ToolOutput::error(text))
50        } else {
51            Ok(ToolOutput::success(text))
52        }
53    }
54
55    fn executor_name(&self) -> &str {
56        "mcp"
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use crate::mcp::protocol::{JsonRpcId, JsonRpcResponse};
64    use crate::mcp::transport::mock::MockTransport;
65    use serde_json::json;
66
67    fn test_def() -> ToolDefinition {
68        ToolDefinition {
69            name: "test_tool".into(),
70            description: "Test".into(),
71            input_schema: json!({"type": "object"}),
72        }
73    }
74
75    #[tokio::test]
76    async fn delegates_to_mcp_client() {
77        let init_resp = JsonRpcResponse::success(
78            JsonRpcId::Number(0),
79            json!({
80                "protocolVersion": "2024-11-05",
81                "serverInfo": {"name": "test"},
82                "capabilities": {}
83            }),
84        );
85        let call_resp = JsonRpcResponse::success(
86            JsonRpcId::Number(0),
87            json!({
88                "content": [{"type": "text", "text": "mcp result"}],
89                "isError": false
90            }),
91        );
92
93        let transport = Box::new(MockTransport::new(vec![init_resp, call_resp]));
94        let client = McpClient::new(transport);
95        let client_arc = Arc::new(Mutex::new(client));
96
97        {
98            let mut c = client_arc.lock().await;
99            c.initialize().await.unwrap();
100        }
101
102        let executor = McpExecutor::new(client_arc);
103        let result = executor
104            .execute(&test_def(), "c1", json!({}), None)
105            .await
106            .unwrap();
107        assert_eq!(result.content, "mcp result");
108        assert!(!result.is_error);
109    }
110
111    #[tokio::test]
112    async fn propagates_mcp_errors() {
113        let init_resp = JsonRpcResponse::success(
114            JsonRpcId::Number(0),
115            json!({
116                "protocolVersion": "2024-11-05",
117                "serverInfo": {"name": "test"},
118                "capabilities": {}
119            }),
120        );
121        let call_resp = JsonRpcResponse::success(
122            JsonRpcId::Number(0),
123            json!({
124                "content": [{"type": "text", "text": "error occurred"}],
125                "isError": true
126            }),
127        );
128
129        let transport = Box::new(MockTransport::new(vec![init_resp, call_resp]));
130        let client = McpClient::new(transport);
131        let client_arc = Arc::new(Mutex::new(client));
132
133        {
134            let mut c = client_arc.lock().await;
135            c.initialize().await.unwrap();
136        }
137
138        let executor = McpExecutor::new(client_arc);
139        let result = executor
140            .execute(&test_def(), "c1", json!({}), None)
141            .await
142            .unwrap();
143        assert!(result.is_error);
144    }
145
146    #[test]
147    fn executor_name() {
148        let transport = Box::new(MockTransport::new(vec![]));
149        let client = McpClient::new(transport);
150        let executor = McpExecutor::new(Arc::new(Mutex::new(client)));
151        assert_eq!(executor.executor_name(), "mcp");
152    }
153}