Skip to main content

apollo/
mcp.rs

1//! MCP (Model Context Protocol) client for integrating with Codex and other MCP servers
2//! Implements JSON-RPC 2.0 over stdio transport
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::sync::Arc;
7use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
8use tokio::process::{Child, ChildStdin, ChildStdout, Command};
9use tokio::sync::Mutex;
10
11/// MCP client for communicating with MCP servers (like Codex)
12pub struct McpClient {
13    process: Arc<Mutex<Child>>,
14    stdin: Arc<Mutex<ChildStdin>>,
15    stdout: Arc<Mutex<BufReader<ChildStdout>>>,
16    request_id: Arc<Mutex<u64>>,
17}
18
19#[derive(Debug, Serialize)]
20struct JsonRpcRequest {
21    jsonrpc: String,
22    id: u64,
23    method: String,
24    params: Option<Value>,
25}
26
27#[derive(Debug, Deserialize)]
28struct JsonRpcResponse {
29    #[allow(dead_code)]
30    jsonrpc: String,
31    #[allow(dead_code)]
32    id: u64,
33    #[serde(default)]
34    result: Option<Value>,
35    #[serde(default)]
36    error: Option<JsonRpcError>,
37}
38
39#[derive(Debug, Deserialize)]
40struct JsonRpcError {
41    code: i32,
42    message: String,
43    #[serde(default)]
44    #[allow(dead_code)]
45    data: Option<Value>,
46}
47
48impl McpClient {
49    /// Spawn a new MCP server process
50    pub async fn spawn(command: &str, args: &[&str]) -> anyhow::Result<Self> {
51        let mut child = Command::new(command)
52            .args(args)
53            .kill_on_drop(true)
54            .stdin(std::process::Stdio::piped())
55            .stdout(std::process::Stdio::piped())
56            .stderr(std::process::Stdio::inherit())
57            .spawn()?;
58
59        let stdin = child
60            .stdin
61            .take()
62            .ok_or_else(|| anyhow::anyhow!("Failed to capture stdin"))?;
63        let stdout = child
64            .stdout
65            .take()
66            .ok_or_else(|| anyhow::anyhow!("Failed to capture stdout"))?;
67
68        Ok(Self {
69            process: Arc::new(Mutex::new(child)),
70            stdin: Arc::new(Mutex::new(stdin)),
71            stdout: Arc::new(Mutex::new(BufReader::new(stdout))),
72            request_id: Arc::new(Mutex::new(0)),
73        })
74    }
75
76    async fn next_request_id(&self) -> u64 {
77        let mut counter = self.request_id.lock().await;
78        *counter += 1;
79        *counter
80    }
81
82    async fn send_request(&self, request: &JsonRpcRequest) -> anyhow::Result<()> {
83        let request_json = serde_json::to_string(request)?;
84        let mut stdin = self.stdin.lock().await;
85        stdin.write_all(request_json.as_bytes()).await?;
86        stdin.write_all(b"\n").await?;
87        stdin.flush().await?;
88        Ok(())
89    }
90
91    async fn read_response(&self) -> anyhow::Result<JsonRpcResponse> {
92        let mut stdout = self.stdout.lock().await;
93        let mut line = String::new();
94        stdout.read_line(&mut line).await?;
95        let response: JsonRpcResponse = serde_json::from_str(&line)?;
96        Ok(response)
97    }
98
99    /// Send a request and wait for response
100    pub async fn call(&self, method: &str, params: Option<Value>) -> anyhow::Result<Value> {
101        let id = self.next_request_id().await;
102
103        let request = JsonRpcRequest {
104            jsonrpc: "2.0".to_string(),
105            id,
106            method: method.to_string(),
107            params,
108        };
109
110        self.send_request(&request).await?;
111        let response = self.read_response().await?;
112
113        if let Some(error) = response.error {
114            anyhow::bail!("MCP error {}: {}", error.code, error.message);
115        }
116
117        response
118            .result
119            .ok_or_else(|| anyhow::anyhow!("No result in MCP response"))
120    }
121
122    /// Initialize the MCP connection
123    pub async fn initialize(&self, client_info: Value) -> anyhow::Result<Value> {
124        self.call(
125            "initialize",
126            Some(serde_json::json!({
127                "protocolVersion": "2024-11-05",
128                "capabilities": {},
129                "clientInfo": client_info
130            })),
131        )
132        .await
133    }
134
135    /// List available tools from the MCP server
136    pub async fn list_tools(&self) -> anyhow::Result<Vec<McpTool>> {
137        let result = self.call("tools/list", None).await?;
138
139        let tools = result
140            .get("tools")
141            .and_then(|t| t.as_array())
142            .ok_or_else(|| anyhow::anyhow!("Invalid tools/list response"))?;
143
144        tools
145            .iter()
146            .map(|t| serde_json::from_value(t.clone()))
147            .collect::<Result<Vec<_>, _>>()
148            .map_err(|e| anyhow::anyhow!("Failed to parse tool: {}", e))
149    }
150
151    /// Call a tool
152    pub async fn call_tool(&self, name: &str, arguments: Value) -> anyhow::Result<Value> {
153        self.call(
154            "tools/call",
155            Some(serde_json::json!({
156                "name": name,
157                "arguments": arguments
158            })),
159        )
160        .await
161    }
162
163    /// Shutdown the MCP server
164    pub async fn shutdown(&self) -> anyhow::Result<()> {
165        self.call("shutdown", None).await?;
166
167        let mut process = self.process.lock().await;
168        process.kill().await?;
169
170        Ok(())
171    }
172}
173
174#[derive(Debug, Serialize, Deserialize)]
175pub struct McpTool {
176    pub name: String,
177    pub description: Option<String>,
178    pub input_schema: Value,
179}
180
181/// Codex-specific MCP client wrapper
182pub struct CodexClient {
183    mcp: McpClient,
184}
185
186impl CodexClient {
187    /// Spawn Codex in MCP server mode
188    pub async fn spawn() -> anyhow::Result<Self> {
189        let mcp = McpClient::spawn("codex", &["mcp-server"]).await?;
190
191        // Initialize
192        mcp.initialize(serde_json::json!({
193            "name": "apollo",
194            "version": "0.1.0"
195        }))
196        .await?;
197
198        Ok(Self { mcp })
199    }
200
201    /// Run a Codex coding session
202    pub async fn run_session(&self, goal: &str, repo_path: &str) -> anyhow::Result<Value> {
203        self.mcp
204            .call_tool(
205                "run",
206                serde_json::json!({
207                    "goal": goal,
208                    "path": repo_path
209                }),
210            )
211            .await
212    }
213
214    /// Get available tools from Codex
215    pub async fn list_tools(&self) -> anyhow::Result<Vec<McpTool>> {
216        self.mcp.list_tools().await
217    }
218
219    /// Shutdown Codex
220    pub async fn shutdown(&self) -> anyhow::Result<()> {
221        self.mcp.shutdown().await
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[tokio::test]
230    #[ignore] // Requires codex binary
231    async fn test_codex_spawn() {
232        let client = CodexClient::spawn().await;
233        assert!(client.is_ok());
234    }
235}