soul_core/executor/
mcp.rs1use 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
14pub 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}