1use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::Duration;
11
12use serde_json::{Value, json};
13use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
14use tokio::process::{Child, ChildStdin, ChildStdout, Command};
15use tokio::sync::Mutex;
16
17const MCP_PROTOCOL_VERSION: &str = "2024-11-05";
19
20#[derive(Debug, Clone)]
22pub enum McpConnectionParams {
23 Stdio {
25 command: String,
27 args: Vec<String>,
29 timeout: Option<Duration>,
31 },
32 Sse {
34 url: String,
36 headers: Option<HashMap<String, String>>,
38 },
39}
40
41struct StdioConnection {
43 #[allow(dead_code)]
45 child: Child,
46 stdin: ChildStdin,
48 stdout: BufReader<ChildStdout>,
50}
51
52pub struct McpSessionManager {
54 params: McpConnectionParams,
55 stdio: Mutex<Option<StdioConnection>>,
57 next_id: AtomicU64,
59}
60
61impl McpSessionManager {
62 pub fn new(params: McpConnectionParams) -> Self {
64 Self {
65 params,
66 stdio: Mutex::new(None),
67 next_id: AtomicU64::new(1),
68 }
69 }
70
71 pub fn params(&self) -> &McpConnectionParams {
73 &self.params
74 }
75
76 fn next_id(&self) -> u64 {
77 self.next_id.fetch_add(1, Ordering::Relaxed)
78 }
79
80 pub async fn list_tools(&self) -> Result<Vec<McpToolInfo>, McpError> {
85 match &self.params {
86 McpConnectionParams::Stdio { .. } => self.stdio_list_tools().await,
87 #[cfg(feature = "mcp-http")]
88 McpConnectionParams::Sse { .. } => self.http_list_tools().await,
89 #[cfg(not(feature = "mcp-http"))]
90 McpConnectionParams::Sse { .. } => Err(McpError::ConnectionFailed(
91 "mcp-http feature not enabled".to_string(),
92 )),
93 }
94 }
95
96 pub async fn call_tool(&self, name: &str, args: Value) -> Result<Value, McpError> {
102 match &self.params {
103 McpConnectionParams::Stdio { .. } => self.stdio_call_tool(name, args).await,
104 #[cfg(feature = "mcp-http")]
105 McpConnectionParams::Sse { .. } => self.http_call_tool(name, args).await,
106 #[cfg(not(feature = "mcp-http"))]
107 McpConnectionParams::Sse { .. } => Err(McpError::ConnectionFailed(
108 "mcp-http feature not enabled".to_string(),
109 )),
110 }
111 }
112
113 async fn stdio_list_tools(&self) -> Result<Vec<McpToolInfo>, McpError> {
118 let timeout = self.stdio_timeout();
119 let mut guard = self.stdio.lock().await;
120 self.ensure_connected(&mut guard, timeout).await?;
121 let conn = guard.as_mut().expect("connection established above");
122
123 let id = self.next_id();
124 let req = json!({
125 "jsonrpc": "2.0",
126 "id": id,
127 "method": "tools/list",
128 "params": {},
129 });
130 let result = stdio_request(conn, id, &req, timeout).await?;
131 parse_tools_list(&result)
132 }
133
134 async fn stdio_call_tool(&self, name: &str, args: Value) -> Result<Value, McpError> {
135 let timeout = self.stdio_timeout();
136 let mut guard = self.stdio.lock().await;
137 self.ensure_connected(&mut guard, timeout).await?;
138 let conn = guard.as_mut().expect("connection established above");
139
140 let id = self.next_id();
141 let arguments = if args.is_null() { json!({}) } else { args };
142 let req = json!({
143 "jsonrpc": "2.0",
144 "id": id,
145 "method": "tools/call",
146 "params": { "name": name, "arguments": arguments },
147 });
148 let result = stdio_request(conn, id, &req, timeout).await?;
149 check_tool_result(&result, name)
150 }
151
152 fn stdio_timeout(&self) -> Option<Duration> {
153 match &self.params {
154 McpConnectionParams::Stdio { timeout, .. } => *timeout,
155 _ => None,
156 }
157 }
158
159 async fn ensure_connected(
161 &self,
162 guard: &mut Option<StdioConnection>,
163 timeout: Option<Duration>,
164 ) -> Result<(), McpError> {
165 if guard.is_some() {
166 return Ok(());
167 }
168 let (command, args) = match &self.params {
169 McpConnectionParams::Stdio { command, args, .. } => (command.clone(), args.clone()),
170 _ => {
171 return Err(McpError::ConnectionFailed(
172 "stdio transport requested for non-stdio params".to_string(),
173 ));
174 }
175 };
176
177 let mut child = Command::new(&command)
178 .args(&args)
179 .stdin(std::process::Stdio::piped())
180 .stdout(std::process::Stdio::piped())
181 .stderr(std::process::Stdio::null())
182 .spawn()
183 .map_err(|e| {
184 McpError::ConnectionFailed(format!("failed to spawn MCP server '{command}': {e}"))
185 })?;
186
187 let stdin = child.stdin.take().ok_or_else(|| {
188 McpError::ConnectionFailed("MCP server stdin not available".to_string())
189 })?;
190 let stdout = child.stdout.take().ok_or_else(|| {
191 McpError::ConnectionFailed("MCP server stdout not available".to_string())
192 })?;
193
194 let mut conn = StdioConnection {
195 child,
196 stdin,
197 stdout: BufReader::new(stdout),
198 };
199
200 let id = self.next_id();
202 let init = json!({
203 "jsonrpc": "2.0",
204 "id": id,
205 "method": "initialize",
206 "params": {
207 "protocolVersion": MCP_PROTOCOL_VERSION,
208 "capabilities": {},
209 "clientInfo": { "name": "gemini-adk-rs", "version": "0.6.0" },
210 },
211 });
212 stdio_request(&mut conn, id, &init, timeout)
213 .await
214 .map_err(|e| McpError::ConnectionFailed(format!("MCP initialize failed: {e}")))?;
215
216 let initialized = json!({
217 "jsonrpc": "2.0",
218 "method": "notifications/initialized",
219 });
220 stdio_write(&mut conn, &initialized).await.map_err(|e| {
221 McpError::ConnectionFailed(format!("MCP initialized notify failed: {e}"))
222 })?;
223
224 *guard = Some(conn);
225 Ok(())
226 }
227
228 #[cfg(feature = "mcp-http")]
233 async fn http_list_tools(&self) -> Result<Vec<McpToolInfo>, McpError> {
234 let id = self.next_id();
235 let req = json!({
236 "jsonrpc": "2.0",
237 "id": id,
238 "method": "tools/list",
239 "params": {},
240 });
241 let result = self.http_request(id, &req).await?;
242 parse_tools_list(&result)
243 }
244
245 #[cfg(feature = "mcp-http")]
246 async fn http_call_tool(&self, name: &str, args: Value) -> Result<Value, McpError> {
247 let id = self.next_id();
248 let arguments = if args.is_null() { json!({}) } else { args };
249 let req = json!({
250 "jsonrpc": "2.0",
251 "id": id,
252 "method": "tools/call",
253 "params": { "name": name, "arguments": arguments },
254 });
255 let result = self.http_request(id, &req).await?;
256 check_tool_result(&result, name)
257 }
258
259 #[cfg(feature = "mcp-http")]
260 #[cfg(not(feature = "mcp-http"))]
261 async fn http_request(&self, _id: u64, _req: &Value) -> Result<Value, McpError> {
262 Err(McpError::ConnectionFailed(
263 "SSE/HTTP MCP transport requires the `mcp-http` feature".to_string(),
264 ))
265 }
266
267 #[cfg(feature = "mcp-http")]
268 async fn http_request(&self, id: u64, req: &Value) -> Result<Value, McpError> {
269 let (url, headers) = match &self.params {
270 McpConnectionParams::Sse { url, headers } => (url.clone(), headers.clone()),
271 _ => {
272 return Err(McpError::ConnectionFailed(
273 "HTTP transport requested for non-SSE params".to_string(),
274 ));
275 }
276 };
277
278 let client = reqwest::Client::new();
279 let mut builder = client
280 .post(&url)
281 .header("content-type", "application/json")
282 .header("accept", "application/json")
283 .json(req);
284 if let Some(hdrs) = headers {
285 for (k, v) in hdrs {
286 builder = builder.header(k, v);
287 }
288 }
289
290 let resp = builder
291 .send()
292 .await
293 .map_err(|e| McpError::ConnectionFailed(format!("MCP HTTP request failed: {e}")))?;
294 if !resp.status().is_success() {
295 return Err(McpError::ConnectionFailed(format!(
296 "MCP HTTP request returned status {}",
297 resp.status()
298 )));
299 }
300 let body: Value = resp
301 .json()
302 .await
303 .map_err(|e| McpError::Other(format!("invalid MCP HTTP response body: {e}")))?;
304 extract_result(&body, id)
305 }
306}
307
308async fn stdio_write(conn: &mut StdioConnection, msg: &Value) -> Result<(), McpError> {
314 let mut line = serde_json::to_string(msg)
315 .map_err(|e| McpError::Other(format!("failed to serialize JSON-RPC: {e}")))?;
316 line.push('\n');
317 conn.stdin
318 .write_all(line.as_bytes())
319 .await
320 .map_err(|e| McpError::ConnectionFailed(format!("failed to write to MCP server: {e}")))?;
321 conn.stdin.flush().await.map_err(|e| {
322 McpError::ConnectionFailed(format!("failed to flush MCP server stdin: {e}"))
323 })?;
324 Ok(())
325}
326
327async fn stdio_request(
330 conn: &mut StdioConnection,
331 id: u64,
332 req: &Value,
333 timeout: Option<Duration>,
334) -> Result<Value, McpError> {
335 let fut = async {
336 stdio_write(conn, req).await?;
337 loop {
338 let mut line = String::new();
339 let n = conn.stdout.read_line(&mut line).await.map_err(|e| {
340 McpError::ConnectionFailed(format!("failed to read from MCP server: {e}"))
341 })?;
342 if n == 0 {
343 return Err(McpError::ConnectionFailed(
345 "MCP server closed connection before responding".to_string(),
346 ));
347 }
348 let trimmed = line.trim();
349 if trimmed.is_empty() {
350 continue;
351 }
352 let msg: Value = match serde_json::from_str(trimmed) {
353 Ok(v) => v,
354 Err(_) => continue,
356 };
357 match msg.get("id").and_then(value_id_as_u64) {
359 Some(resp_id) if resp_id == id => return extract_result(&msg, id),
360 _ => continue,
361 }
362 }
363 };
364
365 match timeout {
366 Some(dur) => match tokio::time::timeout(dur, fut).await {
367 Ok(res) => res,
368 Err(_) => Err(McpError::ConnectionFailed(format!(
369 "MCP request timed out after {dur:?}"
370 ))),
371 },
372 None => fut.await,
373 }
374}
375
376fn value_id_as_u64(v: &Value) -> Option<u64> {
382 if let Some(n) = v.as_u64() {
383 return Some(n);
384 }
385 v.as_str().and_then(|s| s.parse::<u64>().ok())
386}
387
388fn extract_result(msg: &Value, id: u64) -> Result<Value, McpError> {
390 if let Some(err) = msg.get("error") {
391 let code = err
392 .get("code")
393 .and_then(serde_json::Value::as_i64)
394 .unwrap_or(0);
395 let message = err
396 .get("message")
397 .and_then(|m| m.as_str())
398 .unwrap_or("unknown error");
399 return Err(McpError::ToolCallFailed(format!(
400 "JSON-RPC error {code}: {message}"
401 )));
402 }
403 match msg.get("result") {
404 Some(result) => Ok(result.clone()),
405 None => Err(McpError::Other(format!(
406 "JSON-RPC response (id {id}) has neither result nor error"
407 ))),
408 }
409}
410
411fn parse_tools_list(result: &Value) -> Result<Vec<McpToolInfo>, McpError> {
413 let tools = result
414 .get("tools")
415 .and_then(|t| t.as_array())
416 .ok_or_else(|| McpError::Other("tools/list result missing 'tools' array".to_string()))?;
417
418 let mut out = Vec::with_capacity(tools.len());
419 for t in tools {
420 let name = t
421 .get("name")
422 .and_then(|n| n.as_str())
423 .ok_or_else(|| McpError::Other("tool entry missing 'name'".to_string()))?
424 .to_string();
425 let description = t
426 .get("description")
427 .and_then(|d| d.as_str())
428 .unwrap_or("")
429 .to_string();
430 let input_schema = t
431 .get("inputSchema")
432 .cloned()
433 .unwrap_or_else(|| json!({"type": "object"}));
434 out.push(McpToolInfo {
435 name,
436 description,
437 input_schema,
438 });
439 }
440 Ok(out)
441}
442
443fn check_tool_result(result: &Value, name: &str) -> Result<Value, McpError> {
445 if result
446 .get("isError")
447 .and_then(serde_json::Value::as_bool)
448 .unwrap_or(false)
449 {
450 return Err(McpError::ToolCallFailed(format!(
451 "tool '{name}' reported isError: {result}"
452 )));
453 }
454 Ok(result.clone())
455}
456
457#[derive(Debug, Clone)]
459pub struct McpToolInfo {
460 pub name: String,
462 pub description: String,
464 pub input_schema: serde_json::Value,
466}
467
468#[derive(Debug, thiserror::Error)]
470pub enum McpError {
471 #[error("Connection failed: {0}")]
473 ConnectionFailed(String),
474 #[error("Not connected: {0}")]
476 NotConnected(String),
477 #[error("Tool call failed: {0}")]
479 ToolCallFailed(String),
480 #[error("{0}")]
482 Other(String),
483}