Skip to main content

enact_mcp/
lib.rs

1//! MCP (Model Context Protocol) Client for Enact
2//!
3//! This module provides a client for the Model Context Protocol,
4//! allowing Enact to connect to MCP servers and use their tools.
5
6pub 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/// MCP Tool definition
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct McpTool {
22    pub name: String,
23    pub description: String,
24    pub parameters: serde_json::Value,
25}
26
27/// MCP Client for stdio transport
28pub struct McpStdioClient {
29    process: Child,
30    stdin: tokio::process::ChildStdin,
31    stdout: BufReader<tokio::process::ChildStdout>,
32}
33
34/// MCP Client for HTTP transport
35pub 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    /// Create a new MCP client connected to a command via stdio.
112    /// `envs` are injected into the child process environment in addition to the inherited env.
113    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        // Initialize connection
136        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    /// List available tools from the MCP server
184    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    /// Call a tool on the MCP server
215    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}