1pub mod adapter;
7pub mod config;
8
9use anyhow::Result;
10use reqwest::Client;
11use serde::{Deserialize, Serialize};
12use std::process::Stdio;
13use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
14use tokio::process::{Child, Command};
15use tracing::debug;
16
17pub use adapter::{discover_mcp_tools, McpToolAdapter};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct McpTool {
22 pub name: String,
23 pub description: String,
24 pub parameters: serde_json::Value,
25}
26
27pub struct McpStdioClient {
29 process: Child,
30 stdin: tokio::process::ChildStdin,
31 stdout: BufReader<tokio::process::ChildStdout>,
32}
33
34pub struct McpHttpClient {
36 client: Client,
37 url: String,
38}
39
40impl McpHttpClient {
41 pub fn new(url: impl Into<String>) -> Self {
42 Self {
43 client: Client::new(),
44 url: url.into(),
45 }
46 }
47
48 async fn send_request(&self, request: &serde_json::Value) -> Result<serde_json::Value> {
49 let response = self
50 .client
51 .post(&self.url)
52 .json(request)
53 .send()
54 .await?
55 .error_for_status()?
56 .json::<serde_json::Value>()
57 .await?;
58 Ok(response)
59 }
60
61 pub async fn list_tools(&self) -> Result<Vec<McpTool>> {
62 let request = serde_json::json!({
63 "jsonrpc": "2.0",
64 "id": 2,
65 "method": "tools/list"
66 });
67 let response = self.send_request(&request).await?;
68 let tools = response
69 .get("result")
70 .and_then(|r| r.get("tools"))
71 .and_then(|t| t.as_array())
72 .map(|arr| {
73 arr.iter()
74 .filter_map(|tool| {
75 Some(McpTool {
76 name: tool.get("name")?.as_str()?.to_string(),
77 description: tool.get("description")?.as_str()?.to_string(),
78 parameters: tool.get("parameters")?.clone(),
79 })
80 })
81 .collect()
82 })
83 .unwrap_or_default();
84 Ok(tools)
85 }
86
87 pub async fn call_tool(&self, name: &str, arguments: serde_json::Value) -> Result<String> {
88 let request = serde_json::json!({
89 "jsonrpc": "2.0",
90 "id": 3,
91 "method": "tools/call",
92 "params": { "name": name, "arguments": arguments }
93 });
94 let response = self.send_request(&request).await?;
95 if let Some(error) = response.get("error") {
96 anyhow::bail!("MCP tool error: {:?}", error);
97 }
98 let content = response
99 .get("result")
100 .and_then(|r| r.get("content"))
101 .and_then(|c| c.as_array())
102 .and_then(|arr| arr.first())
103 .and_then(|item| item.get("text"))
104 .and_then(|t| t.as_str())
105 .unwrap_or("No content returned");
106 Ok(content.to_string())
107 }
108}
109
110impl McpStdioClient {
111 pub async fn new(
114 command: &str,
115 args: &[&str],
116 envs: &std::collections::HashMap<String, String>,
117 ) -> Result<Self> {
118 let mut process = Command::new(command)
119 .args(args)
120 .envs(envs)
121 .stdin(Stdio::piped())
122 .stdout(Stdio::piped())
123 .stderr(Stdio::piped())
124 .spawn()?;
125
126 let stdin = process.stdin.take().unwrap();
127 let stdout = BufReader::new(process.stdout.take().unwrap());
128
129 let mut client = Self {
130 process,
131 stdin,
132 stdout,
133 };
134
135 client.initialize().await?;
137
138 Ok(client)
139 }
140
141 async fn initialize(&mut self) -> Result<()> {
142 let init_request = serde_json::json!({
143 "jsonrpc": "2.0",
144 "id": 1,
145 "method": "initialize",
146 "params": {
147 "protocolVersion": "2024-11-05",
148 "capabilities": {},
149 "clientInfo": {
150 "name": "enact-mcp",
151 "version": "0.1.0"
152 }
153 }
154 });
155
156 self.send_request(&init_request).await?;
157 let response = self.read_response().await?;
158 debug!("MCP initialized: {:?}", response);
159
160 Ok(())
161 }
162
163 async fn send_request(&mut self, request: &serde_json::Value) -> Result<()> {
164 let request_str = request.to_string();
165 debug!("Sending MCP request: {}", request_str);
166
167 self.stdin.write_all(request_str.as_bytes()).await?;
168 self.stdin.write_all(b"\n").await?;
169 self.stdin.flush().await?;
170
171 Ok(())
172 }
173
174 async fn read_response(&mut self) -> Result<serde_json::Value> {
175 let mut line = String::new();
176 self.stdout.read_line(&mut line).await?;
177
178 debug!("Received MCP response: {}", line);
179 let response: serde_json::Value = serde_json::from_str(&line)?;
180 Ok(response)
181 }
182
183 pub async fn list_tools(&mut self) -> Result<Vec<McpTool>> {
185 let request = serde_json::json!({
186 "jsonrpc": "2.0",
187 "id": 2,
188 "method": "tools/list"
189 });
190
191 self.send_request(&request).await?;
192 let response = self.read_response().await?;
193
194 let tools = response
195 .get("result")
196 .and_then(|r| r.get("tools"))
197 .and_then(|t| t.as_array())
198 .map(|arr| {
199 arr.iter()
200 .filter_map(|tool| {
201 Some(McpTool {
202 name: tool.get("name")?.as_str()?.to_string(),
203 description: tool.get("description")?.as_str()?.to_string(),
204 parameters: tool.get("parameters")?.clone(),
205 })
206 })
207 .collect()
208 })
209 .unwrap_or_default();
210
211 Ok(tools)
212 }
213
214 pub async fn call_tool(&mut self, name: &str, arguments: serde_json::Value) -> Result<String> {
216 let request = serde_json::json!({
217 "jsonrpc": "2.0",
218 "id": 3,
219 "method": "tools/call",
220 "params": {
221 "name": name,
222 "arguments": arguments
223 }
224 });
225
226 self.send_request(&request).await?;
227 let response = self.read_response().await?;
228
229 if let Some(error) = response.get("error") {
230 anyhow::bail!("MCP tool error: {:?}", error);
231 }
232
233 let content = response
234 .get("result")
235 .and_then(|r| r.get("content"))
236 .and_then(|c| c.as_array())
237 .and_then(|arr| arr.first())
238 .and_then(|item| item.get("text"))
239 .and_then(|t| t.as_str())
240 .unwrap_or("No content returned");
241
242 Ok(content.to_string())
243 }
244}
245
246impl Drop for McpStdioClient {
247 fn drop(&mut self) {
248 let _ = self.process.start_kill();
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_mcp_tool_creation() {
258 let tool = McpTool {
259 name: "test".to_string(),
260 description: "Test tool".to_string(),
261 parameters: serde_json::json!({}),
262 };
263 assert_eq!(tool.name, "test");
264 }
265}