1use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::{Arc, Mutex as StdMutex};
12use std::time::Duration;
13use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncWriteExt, BufReader};
14use tokio::process::{Child, ChildStderr};
15use tokio::sync::{oneshot, Mutex};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct McpServerConfig {
20 pub name: String,
22 pub command: String,
24 #[serde(default)]
26 pub args: Vec<String>,
27 #[serde(default)]
29 pub env: HashMap<String, String>,
30 pub cwd: Option<String>,
32}
33
34type Pending = Arc<StdMutex<HashMap<u64, oneshot::Sender<McpResponse>>>>;
36
37pub struct McpServer {
47 config: McpServerConfig,
48 child: Child,
49 stdin: tokio::io::BufWriter<tokio::process::ChildStdin>,
50 next_id: u64,
51 pending: Pending,
52 reader: tokio::task::JoinHandle<()>,
54 stderr_reader: tokio::task::JoinHandle<()>,
58 alive: Arc<AtomicBool>,
61 request_timeout: Duration,
65}
66
67impl Drop for McpServer {
68 fn drop(&mut self) {
69 self.reader.abort();
73 self.stderr_reader.abort();
74 let _ = self.child.start_kill();
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct McpToolInfo {
81 pub name: String,
82 pub description: Option<String>,
83 #[serde(rename = "inputSchema")]
84 pub input_schema: Option<Value>,
85}
86
87#[async_trait::async_trait]
97pub trait McpSession: Send {
98 async fn list_tools(&mut self) -> Result<Vec<McpToolInfo>, String>;
100 async fn call_tool(&mut self, name: &str, arguments: Value) -> Result<Value, String>;
102 async fn call_tool_with_timeout(
107 &mut self,
108 name: &str,
109 arguments: Value,
110 _timeout: Option<Duration>,
111 ) -> Result<Value, String> {
112 self.call_tool(name, arguments).await
113 }
114 fn name(&self) -> &str;
116}
117
118#[async_trait::async_trait]
119impl McpSession for McpServer {
120 async fn list_tools(&mut self) -> Result<Vec<McpToolInfo>, String> {
121 McpServer::list_tools(self).await
122 }
123 async fn call_tool(&mut self, name: &str, arguments: Value) -> Result<Value, String> {
124 McpServer::call_tool(self, name, arguments).await
125 }
126 async fn call_tool_with_timeout(
127 &mut self,
128 name: &str,
129 arguments: Value,
130 timeout: Option<Duration>,
131 ) -> Result<Value, String> {
132 McpServer::call_tool_with_timeout(self, name, arguments, timeout).await
133 }
134 fn name(&self) -> &str {
135 McpServer::name(self)
136 }
137}
138
139#[derive(Debug, Serialize)]
141struct McpRequest {
142 jsonrpc: &'static str,
143 method: String,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 params: Option<Value>,
146 id: u64,
147}
148
149#[derive(Debug, Deserialize)]
151struct McpResponse {
152 result: Option<Value>,
153 error: Option<McpError>,
154 id: Option<u64>,
156}
157
158#[derive(Debug, Deserialize)]
159struct McpError {
160 #[allow(dead_code)]
161 code: Option<i64>,
162 message: String,
163}
164
165fn route_line(line: &str, pending: &StdMutex<HashMap<u64, oneshot::Sender<McpResponse>>>) {
171 let resp: McpResponse = match serde_json::from_str(line) {
172 Ok(r) => r,
173 Err(_) => return, };
175 if let Some(id) = resp.id {
176 if let Some(tx) = pending.lock().unwrap().remove(&id) {
177 let _ = tx.send(resp);
179 }
180 }
182}
183
184async fn reader_loop<R: AsyncBufRead + Unpin>(
188 mut stdout: R,
189 pending: Pending,
190 alive: Arc<AtomicBool>,
191 server_name: String,
192) {
193 let mut line = String::new();
194 loop {
195 line.clear();
196 match stdout.read_line(&mut line).await {
197 Ok(0) | Err(_) => break, Ok(_) => route_line(&line, &pending),
199 }
200 }
201 alive.store(false, Ordering::SeqCst);
202 pending.lock().unwrap().clear(); tracing::debug!(server = %server_name, "MCP reader exited; connection closed");
204}
205
206async fn stderr_drain_loop(stderr: ChildStderr, server_name: String) {
209 let mut lines = BufReader::new(stderr).lines();
210 while let Ok(Some(line)) = lines.next_line().await {
211 tracing::debug!(server = %server_name, "mcp stderr: {line}");
212 }
213}
214
215impl McpServer {
216 pub async fn start(config: McpServerConfig) -> Result<Self, String> {
218 let mut cmd = crate::spawn::program_command(&config.command);
222 cmd.args(&config.args)
223 .stdin(std::process::Stdio::piped())
224 .stdout(std::process::Stdio::piped())
225 .stderr(std::process::Stdio::piped());
226
227 if let Some(ref cwd) = config.cwd {
228 cmd.current_dir(cwd);
229 }
230 for (k, v) in &config.env {
231 cmd.env(k, v);
232 }
233
234 let mut child = cmd
235 .spawn()
236 .map_err(|e| format!("failed to start MCP server '{}': {}", config.name, e))?;
237
238 let stdin = child
239 .stdin
240 .take()
241 .ok_or_else(|| "MCP server has no stdin".to_string())?;
242 let stdout = child
243 .stdout
244 .take()
245 .ok_or_else(|| "MCP server has no stdout".to_string())?;
246 let stderr = child
247 .stderr
248 .take()
249 .ok_or_else(|| "MCP server has no stderr".to_string())?;
250
251 let pending: Pending = Arc::new(StdMutex::new(HashMap::new()));
252 let alive = Arc::new(AtomicBool::new(true));
253 let reader = tokio::spawn(reader_loop(
254 BufReader::new(stdout),
255 Arc::clone(&pending),
256 Arc::clone(&alive),
257 config.name.clone(),
258 ));
259 let stderr_reader = tokio::spawn(stderr_drain_loop(stderr, config.name.clone()));
260
261 let mut server = Self {
262 config,
263 child,
264 stdin: tokio::io::BufWriter::new(stdin),
265 next_id: 1,
266 pending,
267 reader,
268 stderr_reader,
269 alive,
270 request_timeout: Duration::from_secs(600),
276 };
277
278 server
280 .send_request(
281 "initialize",
282 Some(serde_json::json!({
283 "protocolVersion": "2024-11-05",
284 "capabilities": {},
285 "clientInfo": {
286 "name": "car-runtime",
287 "version": env!("CARGO_PKG_VERSION")
288 }
289 })),
290 )
291 .await?;
292
293 let notification = serde_json::json!({
295 "jsonrpc": "2.0",
296 "method": "notifications/initialized"
297 });
298 let msg =
299 serde_json::to_string(¬ification).map_err(|e| format!("serialize error: {e}"))?;
300 server.write_message(&msg).await?;
301
302 Ok(server)
303 }
304
305 async fn reconnect(&mut self) -> Result<(), String> {
307 tracing::warn!(server = %self.config.name, "MCP connection closed; reconnecting (server-side state is lost)");
308 self.reader.abort(); self.stderr_reader.abort();
310 let _ = self.child.kill().await;
311 let fresh = Box::pin(Self::start(self.config.clone())).await?;
316 *self = fresh;
317 Ok(())
318 }
319
320 async fn write_message(&mut self, msg: &str) -> Result<(), String> {
322 self.stdin
323 .write_all(msg.as_bytes())
324 .await
325 .map_err(|e| format!("write to MCP server: {e}"))?;
326 self.stdin
327 .write_all(b"\n")
328 .await
329 .map_err(|e| format!("write newline: {e}"))?;
330 self.stdin
331 .flush()
332 .await
333 .map_err(|e| format!("flush: {e}"))?;
334 Ok(())
335 }
336
337 async fn send_request(&mut self, method: &str, params: Option<Value>) -> Result<Value, String> {
338 self.send_request_with_timeout(method, params, None).await
339 }
340
341 async fn send_request_with_timeout(
346 &mut self,
347 method: &str,
348 params: Option<Value>,
349 timeout: Option<Duration>,
350 ) -> Result<Value, String> {
351 let await_timeout = timeout.unwrap_or(self.request_timeout);
352 if !self.alive.load(Ordering::SeqCst) {
354 self.reconnect().await.map_err(|e| {
355 format!(
356 "MCP session '{}' is dead and reconnect failed: {e}",
357 self.config.name
358 )
359 })?;
360 }
361
362 let id = self.next_id;
363 self.next_id += 1;
364
365 let (tx, rx) = oneshot::channel();
368 self.pending.lock().unwrap().insert(id, tx);
369
370 let req = McpRequest {
371 jsonrpc: "2.0",
372 method: method.to_string(),
373 params,
374 id,
375 };
376 let msg = serde_json::to_string(&req).map_err(|e| format!("serialize error: {e}"))?;
377
378 if let Err(e) = self.write_message(&msg).await {
379 self.pending.lock().unwrap().remove(&id);
380 self.alive.store(false, Ordering::SeqCst); return Err(e);
382 }
383
384 let resp = match tokio::time::timeout(await_timeout, rx).await {
389 Ok(Ok(resp)) => resp,
390 Ok(Err(_)) => {
391 return Err(format!(
392 "MCP server '{}' closed the connection",
393 self.config.name
394 ))
395 }
396 Err(_) => {
397 self.pending.lock().unwrap().remove(&id);
398 return Err(format!("MCP request '{method}' timed out"));
399 }
400 };
401
402 if let Some(err) = resp.error {
403 return Err(format!("MCP error: {}", err.message));
404 }
405 resp.result
406 .ok_or_else(|| "MCP server returned no result".to_string())
407 }
408
409 pub async fn list_tools(&mut self) -> Result<Vec<McpToolInfo>, String> {
411 let result = self.send_request("tools/list", None).await?;
412 let tools = result
413 .get("tools")
414 .and_then(|t| t.as_array())
415 .cloned()
416 .unwrap_or_default();
417
418 tools
419 .into_iter()
420 .map(|t| serde_json::from_value(t).map_err(|e| format!("invalid tool definition: {e}")))
421 .collect()
422 }
423
424 pub async fn call_tool(&mut self, name: &str, arguments: Value) -> Result<Value, String> {
426 self.call_tool_with_timeout(name, arguments, None).await
427 }
428
429 pub async fn call_tool_with_timeout(
434 &mut self,
435 name: &str,
436 arguments: Value,
437 timeout: Option<Duration>,
438 ) -> Result<Value, String> {
439 let result = self
440 .send_request_with_timeout(
441 "tools/call",
442 Some(serde_json::json!({
443 "name": name,
444 "arguments": arguments,
445 })),
446 timeout,
447 )
448 .await?;
449
450 parse_tool_result(result)
451 }
452
453 pub async fn shutdown(mut self) {
455 let _ = self.stdin.shutdown().await;
456 let _ = self.child.kill().await;
457 let _ = self.child.wait().await;
458 }
459
460 pub fn name(&self) -> &str {
462 &self.config.name
463 }
464}
465
466pub struct McpToolExecutor {
472 servers: Arc<Mutex<HashMap<String, Arc<Mutex<dyn McpSession>>>>>,
473 tool_routes: Arc<Mutex<HashMap<String, String>>>,
475 fallback: Option<Arc<dyn super::ToolExecutor>>,
477}
478
479impl McpToolExecutor {
480 pub fn new() -> Self {
481 Self {
482 servers: Arc::new(Mutex::new(HashMap::new())),
483 tool_routes: Arc::new(Mutex::new(HashMap::new())),
484 fallback: None,
485 }
486 }
487
488 pub fn with_fallback(mut self, fallback: Arc<dyn super::ToolExecutor>) -> Self {
489 self.fallback = Some(fallback);
490 self
491 }
492
493 pub async fn add_server(&self, mut server: McpServer) -> Result<Vec<String>, String> {
496 let server_name = server.config.name.clone();
497 let tools = server.list_tools().await?;
498
499 let tool_names: Vec<String> = tools
500 .iter()
501 .map(|t| format!("mcp_{}_{}", server_name, t.name))
502 .collect();
503
504 {
506 let mut routes = self.tool_routes.lock().await;
507 for (info, canonical_name) in tools.iter().zip(tool_names.iter()) {
508 routes.insert(canonical_name.clone(), server_name.clone());
509 routes.insert(info.name.clone(), server_name.clone());
511 }
512 }
513
514 let session: Arc<Mutex<dyn McpSession>> = Arc::new(Mutex::new(server));
516 self.servers.lock().await.insert(server_name, session);
517
518 Ok(tool_names)
519 }
520
521 pub async fn add_session(&self, name: impl Into<String>, session: Arc<Mutex<dyn McpSession>>) {
528 self.servers.lock().await.insert(name.into(), session);
529 }
530
531 pub async fn set_route(&self, tool: impl Into<String>, server: impl Into<String>) {
536 self.tool_routes
537 .lock()
538 .await
539 .insert(tool.into(), server.into());
540 }
541
542 pub async fn remove_session(&self, name: &str, session: &Arc<Mutex<dyn McpSession>>) -> bool {
549 let mut servers = self.servers.lock().await;
550 if !servers
551 .get(name)
552 .is_some_and(|current| Arc::ptr_eq(current, session))
553 {
554 return false;
555 }
556 servers.remove(name);
557 self.tool_routes.lock().await.retain(|_, v| v != name);
560 true
561 }
562
563 pub async fn remove_server(&self, name: &str) {
565 self.servers.lock().await.remove(name);
566 self.tool_routes.lock().await.retain(|_, v| v != name);
567 }
568
569 pub async fn clear_routes_for_server(&self, name: &str) {
572 self.tool_routes.lock().await.retain(|_, v| v != name);
573 }
574
575 pub async fn remove_route(&self, tool: &str) {
579 self.tool_routes.lock().await.remove(tool);
580 }
581
582 pub async fn handles(&self, tool: &str) -> bool {
584 self.tool_routes.lock().await.contains_key(tool)
585 }
586
587 pub async fn session(&self, name: &str) -> Option<Arc<Mutex<dyn McpSession>>> {
595 self.servers.lock().await.get(name).cloned()
596 }
597
598 pub fn share_with_fallback(&self, fallback: Arc<dyn super::ToolExecutor>) -> Self {
603 Self {
604 servers: Arc::clone(&self.servers),
605 tool_routes: Arc::clone(&self.tool_routes),
606 fallback: Some(fallback),
607 }
608 }
609
610 pub async fn tool_schemas(&self) -> Vec<(String, car_ir::ToolSchema)> {
612 let mut schemas = Vec::new();
613 let servers: Vec<(String, Arc<Mutex<dyn McpSession>>)> = self
617 .servers
618 .lock()
619 .await
620 .iter()
621 .map(|(name, session)| (name.clone(), Arc::clone(session)))
622 .collect();
623 for (server_name, server) in servers.iter() {
624 let mut srv = server.lock().await;
625 if let Ok(tools) = srv.list_tools().await {
626 for tool in tools {
627 let canonical_name = format!("mcp_{}_{}", server_name, tool.name);
628 schemas.push((
629 server_name.clone(),
630 car_ir::ToolSchema {
631 name: canonical_name,
632 source: car_ir::ToolSourceKind::Mcp,
633 description: tool.description.unwrap_or_default(),
634 parameters: tool
635 .input_schema
636 .unwrap_or(serde_json::json!({"type": "object"})),
637 returns: None,
638 idempotent: false,
639 cache_ttl_secs: None,
640 rate_limit: None,
641 },
642 ));
643 }
644 }
645 }
646 schemas
647 }
648
649 async fn routed_session(&self, tool: &str) -> Option<(String, Arc<Mutex<dyn McpSession>>)> {
656 let server_name = self.tool_routes.lock().await.get(tool).cloned()?;
657 let server = self.servers.lock().await.get(&server_name).cloned()?;
658 self.tool_routes
659 .lock()
660 .await
661 .get(tool)
662 .is_some_and(|routed| *routed == server_name)
663 .then_some((server_name, server))
664 }
665
666 pub async fn shutdown_all(&self) {
668 let mut servers = self.servers.lock().await;
669 servers.drain();
671 }
672}
673
674impl Default for McpToolExecutor {
675 fn default() -> Self {
676 Self::new()
677 }
678}
679
680#[async_trait::async_trait]
681impl super::ToolExecutor for McpToolExecutor {
682 async fn execute(&self, tool: &str, params: &Value) -> Result<Value, String> {
683 self.execute_with_action(tool, params, "", None).await
684 }
685
686 async fn execute_with_action(
687 &self,
688 tool: &str,
689 params: &Value,
690 action_id: &str,
691 timeout_ms: Option<u64>,
692 ) -> Result<Value, String> {
693 self.execute_with_action_in_session(tool, params, action_id, timeout_ms, None, 1)
694 .await
695 }
696
697 async fn execute_with_action_in_session(
698 &self,
699 tool: &str,
700 params: &Value,
701 action_id: &str,
702 timeout_ms: Option<u64>,
703 session_id: Option<&str>,
704 attempt: u32,
705 ) -> Result<Value, String> {
706 if let Some((server_name, server)) = self.routed_session(tool).await {
708 let mut srv = server.lock().await;
709 let bare_name = tool
711 .strip_prefix(&format!("mcp_{}_", server_name))
712 .unwrap_or(tool);
713 return srv
722 .call_tool_with_timeout(
723 bare_name,
724 params.clone(),
725 timeout_ms.map(Duration::from_millis),
726 )
727 .await;
728 }
729
730 if let Some(ref fallback) = self.fallback {
732 return fallback
733 .execute_with_action_in_session(
734 tool, params, action_id, timeout_ms, session_id, attempt,
735 )
736 .await;
737 }
738
739 Err(format!("unknown MCP tool: '{}'", tool))
740 }
741
742 async fn execute_with_action_state_in_session(
743 &self,
744 tool: &str,
745 params: &Value,
746 action_id: &str,
747 timeout_ms: Option<u64>,
748 session_id: Option<&str>,
749 attempt: u32,
750 expected_effects: &std::collections::HashMap<String, Value>,
751 return_schema: Option<&Value>,
752 ) -> Result<super::ToolExecution, String> {
753 let routed_to_live_mcp = self.routed_session(tool).await.is_some();
754 if routed_to_live_mcp {
755 return self
756 .execute_with_action_in_session(
757 tool, params, action_id, timeout_ms, session_id, attempt,
758 )
759 .await
760 .map(super::ToolExecution::output_only);
761 }
762 if let Some(ref fallback) = self.fallback {
763 return fallback
764 .execute_with_action_state_in_session(
765 tool,
766 params,
767 action_id,
768 timeout_ms,
769 session_id,
770 attempt,
771 expected_effects,
772 return_schema,
773 )
774 .await;
775 }
776 Err(format!("unknown MCP tool: '{}'", tool))
777 }
778
779 async fn execute_classified(
780 &self,
781 tool: &str,
782 params: &Value,
783 action_id: &str,
784 timeout_ms: Option<u64>,
785 session_id: Option<&str>,
786 attempt: u32,
787 expected_effects: &std::collections::HashMap<String, Value>,
788 return_schema: Option<&Value>,
789 ) -> Result<super::ToolExecution, car_ir::ToolFailure> {
790 if let Some((server_name, server)) = self.routed_session(tool).await {
794 let bare_name = tool
795 .strip_prefix(&format!("mcp_{server_name}_"))
796 .unwrap_or(tool);
797 return server
798 .lock()
799 .await
800 .call_tool_with_timeout(
801 bare_name,
802 params.clone(),
803 timeout_ms.map(Duration::from_millis),
804 )
805 .await
806 .map(super::ToolExecution::output_only)
807 .map_err(car_ir::ToolFailure::ordinary);
811 }
812 if let Some(ref fallback) = self.fallback {
813 return fallback
814 .execute_classified(
815 tool,
816 params,
817 action_id,
818 timeout_ms,
819 session_id,
820 attempt,
821 expected_effects,
822 return_schema,
823 )
824 .await;
825 }
826 Err(car_ir::ToolFailure::ordinary(format!(
827 "unknown MCP tool: '{tool}'"
828 )))
829 }
830}
831
832fn parse_tool_result(result: Value) -> Result<Value, String> {
845 let text = result
846 .get("content")
847 .and_then(|c| c.as_array())
848 .and_then(|content| {
849 let texts: Vec<&str> = content
850 .iter()
851 .filter_map(|block| {
852 if block.get("type").and_then(|t| t.as_str()) == Some("text") {
853 block.get("text").and_then(|t| t.as_str())
854 } else {
855 None
856 }
857 })
858 .collect();
859 if texts.is_empty() {
860 None
861 } else {
862 Some(texts.join("\n"))
863 }
864 });
865
866 if result.get("isError").and_then(|v| v.as_bool()) == Some(true) {
867 return Err(
868 text.unwrap_or_else(|| "tool returned isError with no text content".to_string())
869 );
870 }
871
872 if let Some(text) = text {
873 return Ok(Value::String(text));
874 }
875
876 Ok(result)
877}
878
879#[cfg(test)]
880mod tests {
881 use super::*;
882 use serde_json::json;
883
884 #[test]
885 fn tool_result_iserror_true_propagates_as_err() {
886 let r = json!({
888 "content": [{"type": "text", "text": "fetch failed"}],
889 "isError": true
890 });
891 assert_eq!(parse_tool_result(r), Err("fetch failed".to_string()));
892 }
893
894 #[test]
895 fn tool_result_iserror_true_without_text_still_errs() {
896 let r = json!({ "content": [], "isError": true });
897 assert!(parse_tool_result(r).is_err());
898 }
899
900 #[test]
901 fn tool_result_success_returns_joined_text() {
902 let r = json!({
903 "content": [{"type": "text", "text": "line1"}, {"type": "text", "text": "line2"}],
904 "isError": false
905 });
906 assert_eq!(
907 parse_tool_result(r),
908 Ok(Value::String("line1\nline2".to_string()))
909 );
910 }
911
912 #[test]
913 fn tool_result_no_iserror_field_is_success() {
914 let r = json!({ "content": [{"type": "text", "text": "ok"}] });
916 assert_eq!(parse_tool_result(r), Ok(Value::String("ok".to_string())));
917 }
918
919 #[test]
920 fn tool_result_structured_no_text_passes_through() {
921 let r = json!({ "structuredContent": {"x": 1} });
922 assert_eq!(parse_tool_result(r.clone()), Ok(r));
923 }
924
925 fn pending() -> StdMutex<HashMap<u64, oneshot::Sender<McpResponse>>> {
926 StdMutex::new(HashMap::new())
927 }
928
929 #[tokio::test]
930 async fn routes_response_to_matching_waiter() {
931 let p = pending();
932 let (tx, rx) = oneshot::channel();
933 p.lock().unwrap().insert(7, tx);
934 route_line(r#"{"jsonrpc":"2.0","id":7,"result":{"value":42}}"#, &p);
935 let resp = rx.await.expect("waiter delivered");
936 assert!(resp.result.is_some());
937 assert!(p.lock().unwrap().is_empty());
939 }
940
941 #[tokio::test]
942 async fn unknown_id_is_discarded_without_disturbing_other_waiters() {
943 let p = pending();
944 let (tx, _rx) = oneshot::channel();
945 p.lock().unwrap().insert(1, tx);
946 route_line(r#"{"jsonrpc":"2.0","id":999,"result":{}}"#, &p);
948 assert!(p.lock().unwrap().contains_key(&1));
950 }
951
952 #[test]
953 fn notifications_and_garbage_are_ignored() {
954 let p = pending();
955 route_line(
957 r#"{"jsonrpc":"2.0","method":"notifications/progress","params":{}}"#,
958 &p,
959 );
960 route_line("not json at all", &p);
961 assert!(p.lock().unwrap().is_empty());
962 }
963
964 #[tokio::test]
965 async fn error_response_is_routed_for_send_request_to_surface() {
966 let p = pending();
967 let (tx, rx) = oneshot::channel();
968 p.lock().unwrap().insert(3, tx);
969 route_line(
970 r#"{"jsonrpc":"2.0","id":3,"error":{"code":-1,"message":"tool failed"}}"#,
971 &p,
972 );
973 let resp = rx.await.unwrap();
974 assert!(resp.error.is_some());
975 assert_eq!(resp.error.unwrap().message, "tool failed");
976 }
977
978 #[tokio::test]
979 async fn reader_loop_routes_then_marks_dead_and_clears_on_eof() {
980 let pending: Pending = Arc::new(StdMutex::new(HashMap::new()));
981 let alive = Arc::new(AtomicBool::new(true));
982 let (tx, rx) = oneshot::channel();
983 let (tx2, rx2) = oneshot::channel();
985 pending.lock().unwrap().insert(1, tx);
986 pending.lock().unwrap().insert(2, tx2);
987
988 let input = b"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"ok\":true}}\n";
989 reader_loop(
990 BufReader::new(&input[..]),
991 Arc::clone(&pending),
992 Arc::clone(&alive),
993 "t".into(),
994 )
995 .await;
996
997 assert!(rx.await.unwrap().result.is_some(), "id 1 routed");
998 assert!(!alive.load(Ordering::SeqCst), "EOF marks the session dead");
999 assert!(pending.lock().unwrap().is_empty(), "waiters swept on EOF");
1000 assert!(rx2.await.is_err());
1002 }
1003
1004 #[tokio::test]
1005 async fn reader_loop_skips_noise_without_desync() {
1006 let pending: Pending = Arc::new(StdMutex::new(HashMap::new()));
1010 let alive = Arc::new(AtomicBool::new(true));
1011 let (tx, rx) = oneshot::channel();
1012 pending.lock().unwrap().insert(5, tx);
1013
1014 let input = b"garbage not json\n\
1015 {\"jsonrpc\":\"2.0\",\"method\":\"notifications/progress\",\"params\":{}}\n\
1016 {\"jsonrpc\":\"2.0\",\"id\":5,\"result\":{\"done\":true}}\n";
1017 reader_loop(
1018 BufReader::new(&input[..]),
1019 Arc::clone(&pending),
1020 alive,
1021 "t".into(),
1022 )
1023 .await;
1024
1025 assert!(
1026 rx.await.unwrap().result.is_some(),
1027 "id 5 delivered past noise"
1028 );
1029 }
1030
1031 struct RecordingSession {
1034 name: String,
1035 seen: Arc<StdMutex<Vec<Option<Duration>>>>,
1036 }
1037
1038 #[async_trait::async_trait]
1039 impl McpSession for RecordingSession {
1040 async fn list_tools(&mut self) -> Result<Vec<McpToolInfo>, String> {
1041 Ok(vec![])
1042 }
1043 async fn call_tool(&mut self, _name: &str, _arguments: Value) -> Result<Value, String> {
1044 self.seen.lock().unwrap().push(None);
1048 Ok(json!({ "ok": true }))
1049 }
1050 async fn call_tool_with_timeout(
1051 &mut self,
1052 _name: &str,
1053 _arguments: Value,
1054 timeout: Option<Duration>,
1055 ) -> Result<Value, String> {
1056 self.seen.lock().unwrap().push(timeout);
1057 Ok(json!({ "ok": true }))
1058 }
1059 fn name(&self) -> &str {
1060 &self.name
1061 }
1062 }
1063
1064 struct ForgingSession;
1066
1067 #[async_trait::async_trait]
1068 impl McpSession for ForgingSession {
1069 async fn list_tools(&mut self) -> Result<Vec<McpToolInfo>, String> {
1070 Ok(vec![])
1071 }
1072 async fn call_tool(&mut self, name: &str, _arguments: Value) -> Result<Value, String> {
1073 Err(format!("unknown tool: {name}"))
1074 }
1075 fn name(&self) -> &str {
1076 "forger"
1077 }
1078 }
1079
1080 #[tokio::test]
1085 async fn mcp_server_unknown_tool_text_never_runs_the_builtin() {
1086 let dir = tempfile::tempdir().unwrap();
1087 let secret = dir.path().join("host.txt");
1088 std::fs::write(&secret, "HOST-CONTENT-MCP").unwrap();
1089
1090 let exec = McpToolExecutor::new();
1091 exec.add_session("forger", Arc::new(Mutex::new(ForgingSession)))
1092 .await;
1093 exec.set_route("read_file", "forger").await;
1094 let rt = crate::Runtime::new().with_executor(Arc::new(exec));
1095 rt.register_agent_basics().await;
1096
1097 let mut action = car_ir::Action::new(car_ir::ActionType::ToolCall);
1098 action.id = "a0".into();
1099 action.tool = Some("read_file".into());
1100 action.parameters = [("path".to_string(), json!(secret))].into();
1101 let proposal = car_ir::ActionProposal {
1102 id: "p0".into(),
1103 source: "test".into(),
1104 actions: vec![action],
1105 timestamp: chrono::Utc::now(),
1106 context: HashMap::new(),
1107 };
1108 let result = rt.execute(&proposal).await;
1109
1110 assert_eq!(result.results[0].status, car_ir::ActionStatus::Failed);
1111 assert_eq!(
1112 result.results[0].error.as_deref(),
1113 Some("unknown tool: read_file")
1114 );
1115 assert!(
1116 result.results[0].output.is_none(),
1117 "{:?}",
1118 result.results[0]
1119 );
1120 }
1121
1122 struct TerminalFallback;
1123
1124 #[async_trait::async_trait]
1125 impl crate::ToolExecutor for TerminalFallback {
1126 async fn execute(&self, _tool: &str, _params: &Value) -> Result<Value, String> {
1127 Err("terminal callback".into())
1128 }
1129
1130 async fn execute_classified(
1131 &self,
1132 _tool: &str,
1133 _params: &Value,
1134 _action_id: &str,
1135 _timeout_ms: Option<u64>,
1136 _session_id: Option<&str>,
1137 _attempt: u32,
1138 _expected_effects: &HashMap<String, Value>,
1139 _return_schema: Option<&Value>,
1140 ) -> Result<crate::ToolExecution, car_ir::ToolFailure> {
1141 Err(car_ir::ToolFailure::terminal("terminal callback"))
1142 }
1143 }
1144
1145 #[tokio::test]
1146 async fn classified_dispatch_survives_route_removal_without_erasing_terminal_failure() {
1147 use crate::ToolExecutor;
1148 use std::task::Poll;
1149
1150 let seen = Arc::new(StdMutex::new(Vec::new()));
1151 let exec = McpToolExecutor::new().with_fallback(Arc::new(TerminalFallback));
1152 exec.add_session(
1153 "srv",
1154 Arc::new(Mutex::new(RecordingSession {
1155 name: "srv".into(),
1156 seen: Arc::clone(&seen),
1157 })),
1158 )
1159 .await;
1160 exec.set_route("mcp_srv_run", "srv").await;
1161 let params = json!({});
1162 let effects = HashMap::new();
1163 let servers = exec.servers.lock().await;
1164 let mut call = Box::pin(exec.execute_classified(
1165 "mcp_srv_run",
1166 ¶ms,
1167 "a0",
1168 Some(321),
1169 None,
1170 0,
1171 &effects,
1172 None,
1173 ));
1174 assert!(futures::poll!(call.as_mut()).is_pending());
1176 let mut routes = exec.tool_routes.lock().await;
1177 drop(servers);
1178 let outcome = futures::poll!(call.as_mut());
1180 routes.remove("mcp_srv_run");
1181 drop(routes);
1182 let result = match outcome {
1183 Poll::Ready(result) => result,
1184 Poll::Pending => call.await,
1185 };
1186 match result {
1187 Ok(_) => assert_eq!(
1188 *seen.lock().unwrap(),
1189 vec![Some(Duration::from_millis(321))]
1190 ),
1191 Err(failure) => assert_eq!(failure, car_ir::ToolFailure::terminal("terminal callback")),
1192 }
1193 let failure = exec
1195 .execute_classified("mcp_srv_run", ¶ms, "a1", None, None, 0, &effects, None)
1196 .await
1197 .unwrap_err();
1198 assert_eq!(failure, car_ir::ToolFailure::terminal("terminal callback"));
1199 }
1200
1201 #[tokio::test]
1208 async fn action_budget_reaches_the_mcp_session() {
1209 use crate::ToolExecutor;
1210
1211 let seen = Arc::new(StdMutex::new(Vec::new()));
1212 let exec = McpToolExecutor::new();
1213 exec.add_session(
1214 "srv",
1215 Arc::new(Mutex::new(RecordingSession {
1216 name: "srv".to_string(),
1217 seen: Arc::clone(&seen),
1218 })),
1219 )
1220 .await;
1221 exec.set_route("mcp_srv_run", "srv").await;
1222
1223 exec.execute_with_action("mcp_srv_run", &json!({}), "a0", Some(90_000))
1225 .await
1226 .expect("call succeeds");
1227 exec.execute("mcp_srv_run", &json!({}))
1229 .await
1230 .expect("call succeeds");
1231
1232 assert_eq!(
1233 *seen.lock().unwrap(),
1234 vec![Some(Duration::from_millis(90_000)), None],
1235 "the action budget must reach the session, not be discarded"
1236 );
1237 }
1238
1239 fn recording(name: &str) -> Arc<Mutex<dyn McpSession>> {
1240 Arc::new(Mutex::new(RecordingSession {
1241 name: name.to_string(),
1242 seen: Arc::new(StdMutex::new(Vec::new())),
1243 }))
1244 }
1245
1246 #[tokio::test]
1250 async fn removing_a_replaced_session_leaves_its_replacement_routed() {
1251 use crate::ToolExecutor;
1252
1253 let exec = McpToolExecutor::new();
1254 let old = recording("demo");
1255 exec.add_session("demo", Arc::clone(&old)).await;
1256 exec.set_route("mcp_demo_run", "demo").await;
1257 let replacement = recording("demo");
1258 exec.add_session("demo", Arc::clone(&replacement)).await;
1259 exec.set_route("mcp_demo_run", "demo").await;
1260
1261 assert!(!exec.remove_session("demo", &old).await);
1262 let current = exec.session("demo").await.expect("replacement kept");
1263 assert!(Arc::ptr_eq(¤t, &replacement));
1264 exec.execute("mcp_demo_run", &json!({}))
1265 .await
1266 .expect("the replacement still runs its tool");
1267
1268 assert!(exec.remove_session("demo", &replacement).await);
1269 assert!(exec.session("demo").await.is_none());
1270 assert!(!exec.handles("mcp_demo_run").await);
1271 }
1272
1273 #[tokio::test]
1276 async fn removing_the_registered_session_drops_it_and_only_its_routes() {
1277 let exec = McpToolExecutor::new();
1278 let demo = recording("demo");
1279 exec.add_session("demo", Arc::clone(&demo)).await;
1280 exec.set_route("mcp_demo_run", "demo").await;
1281 exec.add_session("other", recording("other")).await;
1282 exec.set_route("mcp_other_run", "other").await;
1283
1284 assert!(exec.remove_session("demo", &demo).await);
1285 assert!(exec.session("demo").await.is_none());
1286 assert!(!exec.handles("mcp_demo_run").await);
1287 assert!(exec.handles("mcp_other_run").await);
1288 }
1289
1290 struct Stalled {
1293 entered: tokio::sync::watch::Sender<bool>,
1294 released: tokio::sync::watch::Receiver<bool>,
1295 }
1296
1297 impl Stalled {
1298 async fn stall(&mut self) {
1299 self.entered.send_replace(true);
1300 let _ = self.released.wait_for(|released| *released).await;
1301 }
1302 }
1303
1304 #[async_trait::async_trait]
1305 impl McpSession for Stalled {
1306 async fn list_tools(&mut self) -> Result<Vec<McpToolInfo>, String> {
1307 self.stall().await;
1308 Ok(vec![])
1309 }
1310 async fn call_tool(&mut self, _name: &str, _arguments: Value) -> Result<Value, String> {
1311 self.stall().await;
1312 Ok(json!({}))
1313 }
1314 fn name(&self) -> &str {
1315 "slow"
1316 }
1317 }
1318
1319 #[tokio::test]
1323 async fn a_call_that_resolved_a_removed_servers_route_does_not_run_on_its_replacement() {
1324 use crate::ToolExecutor;
1325
1326 let exec = McpToolExecutor::new();
1327 exec.add_session("demo", recording("old")).await;
1328 exec.set_route("mcp_demo_run", "demo").await;
1329 let replacement_calls = Arc::new(StdMutex::new(Vec::new()));
1330 let replacement: Arc<Mutex<dyn McpSession>> = Arc::new(Mutex::new(RecordingSession {
1331 name: "new".into(),
1332 seen: Arc::clone(&replacement_calls),
1333 }));
1334
1335 let mut servers = exec.servers.lock().await;
1336 let params = json!({});
1337 let mut call = Box::pin(exec.execute("mcp_demo_run", ¶ms));
1338 assert!(futures::poll!(call.as_mut()).is_pending());
1340 exec.tool_routes.lock().await.remove("mcp_demo_run");
1341 servers.insert("demo".to_string(), replacement);
1342 drop(servers);
1343 let result = call.await;
1344
1345 assert!(result.is_err(), "{result:?}");
1346 assert!(
1347 replacement_calls.lock().unwrap().is_empty(),
1348 "the call ran on the replacement session"
1349 );
1350 }
1351
1352 async fn another_server_works_while<F, T>(in_flight: F) -> bool
1355 where
1356 F: FnOnce(Arc<McpToolExecutor>) -> T,
1357 T: std::future::Future + Send + 'static,
1358 T::Output: Send + 'static,
1359 {
1360 use crate::ToolExecutor;
1361
1362 let (release, released) = tokio::sync::watch::channel(false);
1363 let (entered, mut asked) = tokio::sync::watch::channel(false);
1364 let exec = Arc::new(McpToolExecutor::new());
1365 exec.add_session("slow", Arc::new(Mutex::new(Stalled { entered, released })))
1366 .await;
1367 exec.set_route("mcp_slow_run", "slow").await;
1368 let waiting = tokio::spawn(in_flight(Arc::clone(&exec)));
1369 let _ = asked.wait_for(|asked| *asked).await;
1372
1373 let unblocked = tokio::time::timeout(crate::LOADED_WAIT, async {
1374 exec.add_session("fast", recording("fast")).await;
1375 exec.set_route("mcp_fast_run", "fast").await;
1376 exec.execute("mcp_fast_run", &json!({})).await
1377 })
1378 .await;
1379 release.send(true).unwrap();
1380 let _ = waiting.await;
1381 matches!(unblocked, Ok(Ok(_)))
1382 }
1383
1384 #[tokio::test]
1387 async fn a_slow_server_does_not_hold_up_another() {
1388 use crate::ToolExecutor;
1389
1390 assert!(
1391 another_server_works_while(|exec| async move {
1392 exec.execute("mcp_slow_run", &json!({})).await
1393 })
1394 .await,
1395 "a tool call in flight held the server table"
1396 );
1397 assert!(
1398 another_server_works_while(|exec| async move { exec.tool_schemas().await }).await,
1399 "a listing in flight held the server table"
1400 );
1401 }
1402}