pub mod adapter;
pub mod config;
use anyhow::Result;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, Command};
use tracing::debug;
pub use adapter::{discover_mcp_tools, McpToolAdapter};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpTool {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
pub struct McpStdioClient {
process: Child,
stdin: tokio::process::ChildStdin,
stdout: BufReader<tokio::process::ChildStdout>,
}
pub struct McpHttpClient {
client: Client,
url: String,
}
impl McpHttpClient {
pub fn new(url: impl Into<String>) -> Self {
Self {
client: Client::new(),
url: url.into(),
}
}
async fn send_request(&self, request: &serde_json::Value) -> Result<serde_json::Value> {
let response = self
.client
.post(&self.url)
.json(request)
.send()
.await?
.error_for_status()?
.json::<serde_json::Value>()
.await?;
Ok(response)
}
pub async fn list_tools(&self) -> Result<Vec<McpTool>> {
let request = serde_json::json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
});
let response = self.send_request(&request).await?;
let tools = response
.get("result")
.and_then(|r| r.get("tools"))
.and_then(|t| t.as_array())
.map(|arr| {
arr.iter()
.filter_map(|tool| {
Some(McpTool {
name: tool.get("name")?.as_str()?.to_string(),
description: tool.get("description")?.as_str()?.to_string(),
parameters: tool.get("parameters")?.clone(),
})
})
.collect()
})
.unwrap_or_default();
Ok(tools)
}
pub async fn call_tool(&self, name: &str, arguments: serde_json::Value) -> Result<String> {
let request = serde_json::json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": { "name": name, "arguments": arguments }
});
let response = self.send_request(&request).await?;
if let Some(error) = response.get("error") {
anyhow::bail!("MCP tool error: {:?}", error);
}
let content = response
.get("result")
.and_then(|r| r.get("content"))
.and_then(|c| c.as_array())
.and_then(|arr| arr.first())
.and_then(|item| item.get("text"))
.and_then(|t| t.as_str())
.unwrap_or("No content returned");
Ok(content.to_string())
}
}
impl McpStdioClient {
pub async fn new(
command: &str,
args: &[&str],
envs: &std::collections::HashMap<String, String>,
) -> Result<Self> {
let mut process = Command::new(command)
.args(args)
.envs(envs)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdin = process.stdin.take().unwrap();
let stdout = BufReader::new(process.stdout.take().unwrap());
let mut client = Self {
process,
stdin,
stdout,
};
client.initialize().await?;
Ok(client)
}
async fn initialize(&mut self) -> Result<()> {
let init_request = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "enact-mcp",
"version": "0.1.0"
}
}
});
self.send_request(&init_request).await?;
let response = self.read_response().await?;
debug!("MCP initialized: {:?}", response);
Ok(())
}
async fn send_request(&mut self, request: &serde_json::Value) -> Result<()> {
let request_str = request.to_string();
debug!("Sending MCP request: {}", request_str);
self.stdin.write_all(request_str.as_bytes()).await?;
self.stdin.write_all(b"\n").await?;
self.stdin.flush().await?;
Ok(())
}
async fn read_response(&mut self) -> Result<serde_json::Value> {
let mut line = String::new();
self.stdout.read_line(&mut line).await?;
debug!("Received MCP response: {}", line);
let response: serde_json::Value = serde_json::from_str(&line)?;
Ok(response)
}
pub async fn list_tools(&mut self) -> Result<Vec<McpTool>> {
let request = serde_json::json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
});
self.send_request(&request).await?;
let response = self.read_response().await?;
let tools = response
.get("result")
.and_then(|r| r.get("tools"))
.and_then(|t| t.as_array())
.map(|arr| {
arr.iter()
.filter_map(|tool| {
Some(McpTool {
name: tool.get("name")?.as_str()?.to_string(),
description: tool.get("description")?.as_str()?.to_string(),
parameters: tool.get("parameters")?.clone(),
})
})
.collect()
})
.unwrap_or_default();
Ok(tools)
}
pub async fn call_tool(&mut self, name: &str, arguments: serde_json::Value) -> Result<String> {
let request = serde_json::json!({
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": name,
"arguments": arguments
}
});
self.send_request(&request).await?;
let response = self.read_response().await?;
if let Some(error) = response.get("error") {
anyhow::bail!("MCP tool error: {:?}", error);
}
let content = response
.get("result")
.and_then(|r| r.get("content"))
.and_then(|c| c.as_array())
.and_then(|arr| arr.first())
.and_then(|item| item.get("text"))
.and_then(|t| t.as_str())
.unwrap_or("No content returned");
Ok(content.to_string())
}
}
impl Drop for McpStdioClient {
fn drop(&mut self) {
let _ = self.process.start_kill();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mcp_tool_creation() {
let tool = McpTool {
name: "test".to_string(),
description: "Test tool".to_string(),
parameters: serde_json::json!({}),
};
assert_eq!(tool.name, "test");
}
}