1use std::sync::atomic::{AtomicU64, Ordering};
2
3use codei_config::McpServer;
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
7use tokio::process::{Child, ChildStdin, Command};
8use tokio::time::{timeout, Duration};
9use tracing::{debug, warn};
10
11use crate::error::McpError;
12
13const PROTOCOL_VERSION: &str = "2024-11-05";
14const REQUEST_TIMEOUT_SECS: u64 = 30;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct McpToolInfo {
19 pub name: String,
20 pub description: String,
21 #[serde(rename = "inputSchema", default)]
22 pub input_schema: Value,
23}
24
25#[derive(Debug, Clone, Deserialize)]
27pub struct McpToolCallResult {
28 #[serde(default)]
29 pub content: Vec<McpContentBlock>,
30 #[serde(default)]
31 pub is_error: bool,
32}
33
34#[derive(Debug, Clone, Deserialize)]
35pub struct McpContentBlock {
36 #[serde(rename = "type", default)]
37 pub kind: String,
38 #[serde(default)]
39 pub text: String,
40}
41
42impl McpToolCallResult {
43 pub fn text(&self) -> String {
44 self.content
45 .iter()
46 .filter(|b| b.kind == "text" || b.kind.is_empty())
47 .map(|b| b.text.as_str())
48 .collect::<Vec<_>>()
49 .join("\n")
50 }
51}
52
53pub struct McpClient {
55 server_name: String,
56 child: Child,
57 stdin: ChildStdin,
58 reader: BufReader<tokio::process::ChildStdout>,
59 next_id: AtomicU64,
60}
61
62impl McpClient {
63 pub async fn connect(server: &McpServer) -> Result<Self, McpError> {
64 let mut cmd = Command::new(&server.command);
65 cmd.args(&server.args);
66 for (key, value) in &server.env {
67 cmd.env(key, value);
68 }
69 cmd.stdin(std::process::Stdio::piped())
70 .stdout(std::process::Stdio::piped())
71 .stderr(std::process::Stdio::piped())
72 .kill_on_drop(true);
73
74 let mut child = cmd.spawn().map_err(|source| McpError::Spawn {
75 name: server.name.clone(),
76 source,
77 })?;
78
79 let stdin = child.stdin.take().ok_or_else(|| McpError::Protocol {
80 server: server.name.clone(),
81 message: "missing stdin".into(),
82 })?;
83 let stdout = child.stdout.take().ok_or_else(|| McpError::Protocol {
84 server: server.name.clone(),
85 message: "missing stdout".into(),
86 })?;
87
88 let mut client = Self {
89 server_name: server.name.clone(),
90 child,
91 stdin,
92 reader: BufReader::new(stdout),
93 next_id: AtomicU64::new(1),
94 };
95
96 client.initialize().await?;
97 Ok(client)
98 }
99
100 pub fn server_name(&self) -> &str {
101 &self.server_name
102 }
103
104 async fn initialize(&mut self) -> Result<(), McpError> {
105 let result = self
106 .request(
107 "initialize",
108 json!({
109 "protocolVersion": PROTOCOL_VERSION,
110 "capabilities": {},
111 "clientInfo": {
112 "name": "codei",
113 "version": env!("CARGO_PKG_VERSION"),
114 }
115 }),
116 )
117 .await?;
118
119 debug!(
120 server = %self.server_name,
121 result = %result,
122 "MCP initialize complete"
123 );
124
125 self.notify("notifications/initialized", json!({})).await?;
126 Ok(())
127 }
128
129 pub async fn list_tools(&mut self) -> Result<Vec<McpToolInfo>, McpError> {
130 let result = self.request("tools/list", json!({})).await?;
131 let tools = result
132 .get("tools")
133 .and_then(|v| v.as_array())
134 .cloned()
135 .unwrap_or_default();
136 let mut parsed = Vec::new();
137 for tool in tools {
138 let info: McpToolInfo =
139 serde_json::from_value(tool).map_err(|err| McpError::Protocol {
140 server: self.server_name.clone(),
141 message: format!("invalid tool definition: {err}"),
142 })?;
143 parsed.push(info);
144 }
145 Ok(parsed)
146 }
147
148 pub async fn call_tool(
149 &mut self,
150 name: &str,
151 arguments: Value,
152 ) -> Result<McpToolCallResult, McpError> {
153 let result = self
154 .request(
155 "tools/call",
156 json!({
157 "name": name,
158 "arguments": arguments,
159 }),
160 )
161 .await?;
162 serde_json::from_value(result).map_err(|err| McpError::Protocol {
163 server: self.server_name.clone(),
164 message: format!("invalid tools/call response: {err}"),
165 })
166 }
167
168 async fn notify(&mut self, method: &str, params: Value) -> Result<(), McpError> {
169 let message = json!({
170 "jsonrpc": "2.0",
171 "method": method,
172 "params": params,
173 });
174 self.write_message(&message).await
175 }
176
177 async fn request(&mut self, method: &str, params: Value) -> Result<Value, McpError> {
178 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
179 let message = json!({
180 "jsonrpc": "2.0",
181 "id": id,
182 "method": method,
183 "params": params,
184 });
185 self.write_message(&message).await?;
186
187 let response = timeout(
188 Duration::from_secs(REQUEST_TIMEOUT_SECS),
189 self.read_response(id),
190 )
191 .await
192 .map_err(|_| McpError::Timeout {
193 secs: REQUEST_TIMEOUT_SECS,
194 })??;
195
196 Ok(response)
197 }
198
199 async fn read_response(&mut self, id: u64) -> Result<Value, McpError> {
200 loop {
201 if self.child.try_wait()?.is_some() {
202 return Err(McpError::Exited {
203 name: self.server_name.clone(),
204 });
205 }
206
207 let line = self.read_line().await?;
208 if line.trim().is_empty() {
209 continue;
210 }
211
212 let value: Value = serde_json::from_str(&line)?;
213 if value.get("method").is_some() && value.get("id").is_none() {
214 debug!(server = %self.server_name, %line, "MCP notification");
215 continue;
216 }
217
218 if value.get("id").and_then(|v| v.as_u64()) != Some(id) {
219 warn!(server = %self.server_name, %line, "unexpected MCP response id");
220 continue;
221 }
222
223 if let Some(error) = value.get("error") {
224 let message = error
225 .get("message")
226 .and_then(|v| v.as_str())
227 .unwrap_or("unknown error");
228 return Err(McpError::Protocol {
229 server: self.server_name.clone(),
230 message: message.to_string(),
231 });
232 }
233
234 return Ok(value.get("result").cloned().unwrap_or(Value::Null));
235 }
236 }
237
238 async fn write_message(&mut self, message: &Value) -> Result<(), McpError> {
239 let line = serde_json::to_string(message)?;
240 debug!(server = %self.server_name, %line, "MCP send");
241 self.stdin.write_all(line.as_bytes()).await?;
242 self.stdin.write_all(b"\n").await?;
243 self.stdin.flush().await?;
244 Ok(())
245 }
246
247 async fn read_line(&mut self) -> Result<String, McpError> {
248 let mut line = String::new();
249 self.reader.read_line(&mut line).await?;
250 if line.is_empty() {
251 return Err(McpError::Exited {
252 name: self.server_name.clone(),
253 });
254 }
255 Ok(line)
256 }
257}