1use anyhow::{Context, Result, anyhow};
12use std::sync::atomic::AtomicUsize;
13use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
14use tokio::process::{Child, ChildStdin, ChildStdout, Command};
15use tokio::sync::{Mutex, RwLock};
16use tokio::task::JoinHandle;
17use tokio::time::{Duration, timeout};
18
19use crate::protocol::*;
20
21pub struct McpClient {
40 server: McpServer,
42 child: RwLock<Option<Child>>,
47 stdin: Mutex<Option<tokio::io::BufWriter<ChildStdin>>>,
51 stdout: Mutex<Option<BufReader<ChildStdout>>>,
55 initialized: RwLock<bool>,
57 tool_cache: RwLock<Option<Vec<McpTool>>>,
59 server_info: RwLock<Option<ServerInfo>>,
61 request_timeout: Duration,
63 stderr_task: Mutex<Option<JoinHandle<()>>>,
66 next_id: AtomicUsize,
69}
70
71impl McpClient {
72 pub fn new(server: McpServer) -> Self {
76 Self {
77 server,
78 child: RwLock::new(None),
79 stdin: Mutex::new(None),
80 stdout: Mutex::new(None),
81 initialized: RwLock::new(false),
82 tool_cache: RwLock::new(None),
83 server_info: RwLock::new(None),
84 request_timeout: Duration::from_secs(30),
85 stderr_task: Mutex::new(None),
86 next_id: AtomicUsize::new(1),
87 }
88 }
89
90 #[must_use]
92 pub fn with_timeout(mut self, timeout: Duration) -> Self {
93 self.request_timeout = timeout;
94 self
95 }
96
97 pub async fn initialize(&self) -> Result<()> {
103 if *self.initialized.read().await {
104 return Ok(());
105 }
106
107 crate::validation::validate_mcp_command(&self.server.command)
115 .map_err(|reason| anyhow!("MCP server '{}' rejected: {reason}", self.server.name))?;
116 let safe_env = crate::validation::sanitize_env(&self.server.env);
117
118 let mut child = Command::new(&self.server.command)
123 .args(&self.server.args)
124 .envs(safe_env)
125 .stdin(std::process::Stdio::piped())
126 .stdout(std::process::Stdio::piped())
127 .stderr(std::process::Stdio::piped())
128 .kill_on_drop(true)
129 .spawn()
130 .with_context(|| format!("Failed to spawn MCP server '{}'", self.server.name))?;
131
132 let stdin = child
133 .stdin
134 .take()
135 .expect("stdin not captured — stdin was piped");
136 let stdout = child
137 .stdout
138 .take()
139 .expect("stdout not captured — stdout was piped");
140 let stderr = child
141 .stderr
142 .take()
143 .expect("stderr not captured — stderr was piped");
144
145 let stderr_server_name = self.server.name.clone();
149 let stderr_task = tokio::spawn(async move {
150 let mut reader = BufReader::new(stderr);
151 let mut line = String::new();
152 loop {
153 line.clear();
154 match reader.read_line(&mut line).await {
155 Ok(0) => break, Ok(_) => {
157 let trimmed = line.trim_end_matches(['\n', '\r']);
158 if !trimmed.is_empty() {
159 tracing::debug!(
160 server = %stderr_server_name,
161 stream = "stderr",
162 "{}",
163 trimmed
164 );
165 }
166 }
167 Err(e) => {
168 tracing::debug!(
169 server = %stderr_server_name,
170 stream = "stderr",
171 error = %e,
172 "stderr drain stopping"
173 );
174 break;
175 }
176 }
177 }
178 });
179
180 *self.stdin.lock().await = Some(tokio::io::BufWriter::new(stdin));
182 *self.stdout.lock().await = Some(BufReader::new(stdout));
183 *self.stderr_task.lock().await = Some(stderr_task);
184
185 *self.child.write().await = Some(child);
187
188 let params = InitializeParams::default();
192 let request = McpRequest::with_id(self.next_id(), "initialize")
193 .with_params(serde_json::to_value(¶ms)?);
194
195 let response = match self.do_request(request).await {
199 Ok(resp) => resp,
200 Err(e) => {
201 self.cleanup_child().await;
202 return Err(e);
203 }
204 };
205
206 let result_json = response.into_result()?;
208 let init_result: InitializeResult = serde_json::from_value(result_json)?;
209
210 *self.server_info.write().await = Some(init_result.server_info.clone());
211 *self.initialized.write().await = true;
212
213 let notification = McpRequest::notification("notifications/initialized");
216 self.send_notification(notification).await?;
217
218 tracing::debug!(
219 server = %self.server.name,
220 version = %init_result.server_info.version,
221 "MCP server initialized"
222 );
223
224 Ok(())
225 }
226
227 pub async fn is_initialized(&self) -> bool {
229 *self.initialized.read().await
230 }
231
232 pub async fn server_info(&self) -> Option<ServerInfo> {
234 self.server_info.read().await.clone()
235 }
236
237 async fn do_request(&self, request: McpRequest) -> Result<McpResponse> {
244 let request_id = request.id.clone();
245
246 let mut stdin_guard = self.stdin.lock().await;
248 let stdin = stdin_guard
249 .as_mut()
250 .ok_or_else(|| anyhow!("stdin not available on '{}'", self.server.name))?;
251
252 let json = request.to_jsonl()?;
254 timeout(self.request_timeout, async {
255 stdin.write_all(&json).await?;
256 stdin.flush().await?;
257 Ok::<(), tokio::io::Error>(())
258 })
259 .await
260 .map_err(|e| anyhow::anyhow!("MCP request timed out (write): {e}"))??;
261
262 let mut stdout_guard = self.stdout.lock().await;
264 let stdout = stdout_guard
265 .as_mut()
266 .ok_or_else(|| anyhow!("stdout not available on '{}'", self.server.name))?;
267
268 loop {
274 let line: std::io::Result<Option<String>> = timeout(self.request_timeout, async {
275 stdout.lines().next_line().await
276 })
277 .await
278 .map_err(|e| anyhow::anyhow!("MCP request timed out (read): {e}"))?;
279
280 let response_str: String = line
281 .context("Failed to read MCP response line from stdout")?
282 .with_context(|| format!("MCP server {} returned no response", self.server.name))?;
283
284 let value: serde_json::Value = serde_json::from_str(&response_str)
287 .with_context(|| format!("Failed to parse MCP message JSON: {response_str}"))?;
288
289 if value.get("method").is_some() {
292 tracing::debug!(
293 server = %self.server.name,
294 method = ?value.get("method"),
295 "MCP server sent a notification/server request; skipping"
296 );
297 continue;
298 }
299
300 let got_id = value.get("id");
302 if got_id != Some(&request_id) {
303 tracing::warn!(
308 server = %self.server.name,
309 expected_id = ?request_id,
310 got_id = ?got_id,
311 "MCP response ID mismatch, skipping"
312 );
313 continue;
314 }
315
316 let parsed: McpResponse = serde_json::from_value(value)
317 .with_context(|| format!("Failed to parse MCP response: {response_str}"))?;
318 return Ok(parsed);
319 }
320 }
321
322 async fn send_notification(&self, notification: McpRequest) -> Result<()> {
324 let mut stdin_guard = self.stdin.lock().await;
325 let stdin = stdin_guard
326 .as_mut()
327 .ok_or_else(|| anyhow!("stdin not available on '{}'", self.server.name))?;
328
329 let json = notification.to_jsonl()?;
330 stdin.write_all(&json).await?;
331 stdin.flush().await?;
332
333 Ok(())
334 }
335
336 pub(crate) async fn send_request(&self, request: McpRequest) -> Result<McpResponse> {
343 {
345 let child = self.child.read().await;
346 if child.is_none() {
347 tracing::warn!(
348 server = %self.server.name,
349 "MCP server not running, attempting auto-start"
350 );
351 drop(child);
352 self.restart().await?;
354 }
355 }
356
357 let request_for_retry = request.clone();
360 match self.do_request(request).await {
361 Ok(resp) => Ok(resp),
362 Err(e) => {
363 let err_str = e.to_string();
365 let is_comm_error = err_str.contains("not available")
366 || err_str.contains("broken pipe")
367 || err_str.contains("timed out")
368 || err_str.contains("no response")
369 || err_str.contains("reset by peer");
370
371 if is_comm_error {
372 tracing::warn!(
373 server = %self.server.name,
374 error = %err_str,
375 "MCP communication error, attempting auto-restart + retry"
376 );
377 self.restart().await?;
378 self.do_request(request_for_retry).await
381 } else {
382 Err(e)
383 }
384 }
385 }
386 }
387
388 fn next_id(&self) -> usize {
390 self.next_id
391 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
392 }
393
394 async fn cleanup_child(&self) {
398 *self.stdin.lock().await = None;
400 *self.stdout.lock().await = None;
401
402 if let Some(handle) = self.stderr_task.lock().await.take() {
404 handle.abort();
405 }
406
407 if let Some(mut child) = self.child.write().await.take() {
411 let _ = child.kill().await;
412 let _ = child.wait().await;
413 }
414
415 *self.initialized.write().await = false;
416 }
417
418 pub async fn list_tools(&self) -> Result<Vec<McpTool>> {
422 if let Some(cached) = self.tool_cache.read().await.clone() {
424 return Ok(cached);
425 }
426
427 self.refresh_tools().await
428 }
429
430 pub async fn refresh_tools(&self) -> Result<Vec<McpTool>> {
432 let request = McpRequest::with_id(self.next_id(), "tools/list");
433 let response = self.send_request(request).await?;
434
435 let result_json = response.into_result()?;
436 let tools_result: McpToolsResult = serde_json::from_value(result_json)?;
437
438 let tools = tools_result.tools;
439 *self.tool_cache.write().await = Some(tools.clone());
440
441 tracing::debug!(
442 server = %self.server.name,
443 count = tools.len(),
444 "Refreshed tool cache"
445 );
446
447 Ok(tools)
448 }
449
450 pub async fn call_tool(
454 &self,
455 tool_name: &str,
456 arguments: serde_json::Value,
457 ) -> Result<McpToolCallResult> {
458 let params = serde_json::json!({
459 "name": tool_name,
460 "arguments": arguments,
461 });
462
463 let request = McpRequest::with_id(self.next_id(), "tools/call").with_params(params);
464 let response = self.send_request(request).await?;
465
466 let result_json = response.into_result()?;
467 let call_result: McpToolCallResult = serde_json::from_value(result_json)?;
468
469 tracing::debug!(
470 server = %self.server.name,
471 tool = tool_name,
472 "Tool call completed"
473 );
474
475 Ok(call_result)
476 }
477
478 pub async fn call_tool_text(
482 &self,
483 tool_name: &str,
484 arguments: serde_json::Value,
485 ) -> Result<String> {
486 let result = self.call_tool(tool_name, arguments).await?;
487
488 for block in result.content {
489 if let McpContentBlock::Text { text } = block {
490 return Ok(text);
491 }
492 }
493
494 Err(anyhow!("Tool '{tool_name}' returned no text content"))
495 }
496
497 pub async fn shutdown(&self) -> Result<()> {
502 *self.stdin.lock().await = None;
504 *self.stdout.lock().await = None;
505
506 if let Some(handle) = self.stderr_task.lock().await.take() {
508 handle.abort();
509 }
510
511 let mut child_guard = self.child.write().await;
512
513 if let Some(mut child) = child_guard.take() {
514 tracing::debug!(server = %self.server.name, "Shutting down MCP server");
515
516 let _ = child.try_wait();
518
519 child.kill().await?;
521 let _ = child.wait().await;
522 }
523
524 *self.initialized.write().await = false;
525 *self.tool_cache.write().await = None;
526
527 Ok(())
528 }
529
530 pub async fn restart(&self) -> Result<()> {
532 self.shutdown().await?;
533 self.initialize().await
534 }
535
536 pub fn server(&self) -> &McpServer {
538 &self.server
539 }
540}
541
542impl std::fmt::Debug for McpClient {
543 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
544 f.debug_struct("McpClient")
545 .field("server", &self.server.name)
546 .field("initialized", &self.initialized)
547 .finish()
548 }
549}
550
551#[cfg(test)]
556mod tests {
557 use super::*;
558 use tokio::time::Duration;
559
560 #[test]
563 fn test_client_construction() {
564 let server = McpServer::new("test-server", "npx");
565 let client = McpClient::new(server);
566
567 assert_eq!(client.server.name, "test-server");
569 assert_eq!(client.server.command, "npx");
570 }
571
572 #[test]
573 fn test_client_with_timeout() {
574 let server = McpServer::new("test", "echo");
575 let client = McpClient::new(server).with_timeout(Duration::from_secs(60));
576
577 assert_eq!(client.server.name, "test");
581 }
582
583 #[test]
584 fn test_client_with_timeout_short() {
585 let server = McpServer::new("test", "sleep");
586 let client = McpClient::new(server).with_timeout(Duration::from_millis(50));
587
588 assert_eq!(client.server.name, "test");
589 }
591
592 #[test]
593 fn test_client_debug_format() {
594 let server = McpServer::new("debug-test", "echo");
595 let client = McpClient::new(server);
596
597 let debug_str = format!("{client:?}");
598
599 assert!(debug_str.contains("debug-test"));
601 assert!(debug_str.contains("McpClient"));
602 }
603
604 #[test]
605 fn test_client_debug_different_servers() {
606 let server1 = McpServer::new("server-a", "cmd1");
607 let server2 = McpServer::new("server-b", "cmd2");
608
609 let client1 = McpClient::new(server1);
610 let client2 = McpClient::new(server2);
611
612 let debug1 = format!("{client1:?}");
613 let debug2 = format!("{client2:?}");
614
615 assert!(debug1.contains("server-a"));
616 assert!(debug2.contains("server-b"));
617 assert_ne!(debug1, debug2);
618 }
619
620 #[tokio::test]
621 async fn test_is_initialized_false_on_new() {
622 let server = McpServer::new("test", "echo");
623 let client = McpClient::new(server);
624
625 assert!(!client.is_initialized().await);
627 }
628
629 #[tokio::test]
630 async fn test_is_initialized_after_failed_init() {
631 let server = McpServer::new("ghost", "nonexistent-binary-xyz-123");
632 let client = McpClient::new(server);
633
634 let result = client.initialize().await;
636 assert!(result.is_err());
637 assert!(!client.is_initialized().await);
638 }
639
640 #[tokio::test]
641 async fn test_shutdown_when_not_running() {
642 let server = McpServer::new("test-shutdown", "echo");
643 let client = McpClient::new(server);
644
645 let result = client.shutdown().await;
647 assert!(result.is_ok());
648
649 assert!(!client.is_initialized().await);
651 }
652
653 #[tokio::test]
654 async fn test_shutdown_idempotent() {
655 let server = McpServer::new("test-idempotent", "echo");
656 let client = McpClient::new(server);
657
658 let first = client.shutdown().await;
660 assert!(first.is_ok());
661
662 let second = client.shutdown().await;
664 assert!(second.is_ok());
665 }
666
667 #[test]
668 fn test_client_server_config_passed_through() {
669 let server = McpServer::new("config-test", "npx")
670 .with_args(vec!["-y".to_string(), "@some/mcp-server".to_string()])
671 .with_env("DEBUG", "true");
672
673 let client = McpClient::new(server);
674
675 assert_eq!(client.server.name, "config-test");
676 assert_eq!(client.server.command, "npx");
677 assert_eq!(client.server.args, vec!["-y", "@some/mcp-server"]);
678 assert_eq!(client.server.env.get("DEBUG"), Some(&"true".to_string()));
679 }
680
681 #[test]
682 fn test_client_server_method() {
683 let server = McpServer::new("method-test", "python");
684 let client = McpClient::new(server);
685
686 let retrieved_server = client.server();
688 assert_eq!(retrieved_server.name, "method-test");
689 }
690
691 #[tokio::test]
692 async fn test_server_info_none_on_new_client() {
693 let server = McpServer::new("test", "echo");
694 let client = McpClient::new(server);
695
696 assert!(client.server_info().await.is_none());
698 }
699
700 #[tokio::test]
701 async fn test_initialize_already_initialized_skipped() {
702 let server = McpServer::new("echo", "echo");
703 let client = McpClient::new(server);
704
705 let _ = client.initialize().await;
707
708 let result = client.initialize().await;
710 assert!(result.is_err() || result.is_ok());
712 }
713
714 #[test]
715 fn test_client_default_timeout_is_30_seconds() {
716 let server = McpServer::new("test", "echo");
717 let client = McpClient::new(server);
718
719 assert_eq!(client.server.name, "test");
722 }
723
724 #[tokio::test]
725 async fn test_shutdown_clears_initialized_flag() {
726 let server = McpServer::new("test-clear", "echo");
727 let client = McpClient::new(server);
728
729 assert!(!client.is_initialized().await);
731
732 client.shutdown().await.unwrap();
734 assert!(!client.is_initialized().await);
735 }
736}