1use async_trait::async_trait;
13use futures_util::StreamExt;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use std::collections::HashMap;
17use std::fmt;
18use std::path::Path;
19use std::sync::Arc;
20use std::time::Duration;
21use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
22use tokio::process::{Child, ChildStdin, ChildStdout, Command};
23use tokio::sync::Mutex;
24use tokio::time::timeout;
25
26use crate::error::{Error, Result};
27use crate::llm::ToolSpec;
28use crate::tools::Tool;
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct McpServerConfig {
37 #[serde(default)]
39 pub command: String,
40 #[serde(default)]
41 pub args: Vec<String>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub env: Option<HashMap<String, String>>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub url: Option<String>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct McpServer {
52 pub name: String,
53 #[serde(default)]
55 pub command: String,
56 #[serde(default)]
57 pub args: Vec<String>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub url: Option<String>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub env: Option<HashMap<String, String>>,
64}
65
66impl From<(String, McpServerConfig)> for McpServer {
67 fn from((name, config): (String, McpServerConfig)) -> Self {
68 Self {
69 name,
70 command: config.command,
71 args: config.args,
72 url: config.url,
73 env: config.env,
74 }
75 }
76}
77
78#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct McpToolAnnotations {
83 #[serde(default)]
85 pub read_only_hint: bool,
86 #[serde(default)]
88 pub destructive_hint: bool,
89 #[serde(default)]
91 pub idempotent_hint: bool,
92 #[serde(default)]
94 pub open_world_hint: bool,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case")]
101pub enum McpServerTrust {
102 Trusted,
104 #[default]
107 Untrusted,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct McpToolSpec {
113 pub name: String,
114 #[serde(default)]
115 pub description: String,
116 #[serde(default)]
117 pub input_schema: Value,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub annotations: Option<McpToolAnnotations>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct McpResource {
126 pub uri: String,
127 pub name: String,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub description: Option<String>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 #[serde(rename = "mimeType")]
132 pub mime_type: Option<String>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct McpResourceContent {
138 pub uri: String,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 #[serde(rename = "mimeType")]
141 pub mime_type: Option<String>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub text: Option<String>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub blob: Option<String>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct McpPrompt {
151 pub name: String,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub description: Option<String>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub arguments: Option<Vec<McpPromptArgument>>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct McpPromptArgument {
161 pub name: String,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub description: Option<String>,
164 #[serde(default)]
165 pub required: bool,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct McpPromptMessage {
171 pub role: String,
172 pub content: String,
173}
174
175#[allow(clippy::large_enum_variant)]
181enum McpTransport {
182 Stdio {
184 stdin: ChildStdin,
185 reader: BufReader<ChildStdout>,
186 child: Option<Child>,
187 },
188 HttpSse {
190 client: reqwest::Client,
191 sse_url: String,
193 post_url: Option<String>,
195 buffer: String,
197 },
198}
199
200impl fmt::Debug for McpTransport {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 match self {
203 Self::Stdio { .. } => f.debug_struct("Stdio").finish(),
204 Self::HttpSse {
205 sse_url, post_url, ..
206 } => f
207 .debug_struct("HttpSse")
208 .field("sse_url", sse_url)
209 .field("post_url", post_url)
210 .finish(),
211 }
212 }
213}
214
215#[derive(Debug, Clone)]
217pub struct ElicitationRequest {
218 pub mcp_server_name: String,
219 pub message: String,
220 pub mode: Option<String>,
221 pub url: Option<String>,
222 pub elicitation_id: Option<String>,
223 pub requested_schema: Option<Value>,
224 pub title: Option<String>,
225 pub display_name: Option<String>,
226 pub description: Option<String>,
227}
228
229#[async_trait]
231pub trait ElicitationHandler: Send + Sync {
232 async fn elicit(&self, request: ElicitationRequest) -> Option<Value>;
233}
234
235pub type SharedElicitationHandler = Arc<tokio::sync::RwLock<Option<Arc<dyn ElicitationHandler>>>>;
238
239pub fn new_elicitation_slot() -> SharedElicitationHandler {
241 Arc::new(tokio::sync::RwLock::new(None))
242}
243
244pub struct McpClient {
246 transport: McpTransport,
247 next_id: u64,
248 capabilities: ServerCapabilities,
250 server_name: String,
252 read_timeout: Duration,
257 elicitation: Option<SharedElicitationHandler>,
259}
260
261pub const DEFAULT_STDIO_READ_TIMEOUT: Duration = Duration::from_secs(10);
265
266#[derive(Debug, Clone, Default)]
268pub struct ServerCapabilities {
269 pub tools: bool,
270 pub resources: bool,
271 pub prompts: bool,
272}
273
274impl fmt::Debug for McpClient {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 f.debug_struct("McpClient")
277 .field("transport", &self.transport)
278 .field("next_id", &self.next_id)
279 .field("capabilities", &self.capabilities)
280 .finish()
281 }
282}
283
284impl Drop for McpClient {
285 fn drop(&mut self) {
286 if let McpTransport::Stdio { ref mut child, .. } = &mut self.transport {
287 if let Some(mut child) = child.take() {
288 let _ = child.start_kill();
289 }
290 }
291 }
292}
293
294impl McpClient {
295 pub async fn spawn(server: &McpServer) -> Result<Self> {
300 Self::spawn_with_timeout(server, DEFAULT_STDIO_READ_TIMEOUT).await
301 }
302
303 pub async fn spawn_with_timeout(server: &McpServer, read_timeout: Duration) -> Result<Self> {
307 if let Some(url) = &server.url {
308 Self::spawn_http_sse(server, url).await
311 } else {
312 Self::spawn_stdio(server, read_timeout).await
313 }
314 }
315
316 async fn spawn_stdio(server: &McpServer, read_timeout: Duration) -> Result<Self> {
318 let mut cmd = Command::new(&server.command);
319 cmd.args(&server.args)
320 .stdin(std::process::Stdio::piped())
321 .stdout(std::process::Stdio::piped())
322 .stderr(std::process::Stdio::null());
323 if let Some(env) = &server.env {
324 cmd.envs(env);
325 }
326 let mut child = cmd.spawn().map_err(|e| Error::Mcp {
327 server: server.name.clone(),
328 message: format!("failed to spawn: {e}"),
329 })?;
330
331 let stdin = child.stdin.take().ok_or_else(|| Error::Mcp {
332 server: server.name.clone(),
333 message: "failed to open stdin".into(),
334 })?;
335 let stdout = child.stdout.take().ok_or_else(|| Error::Mcp {
336 server: server.name.clone(),
337 message: "failed to open stdout".into(),
338 })?;
339 let reader = BufReader::new(stdout);
340
341 let mut client = Self {
342 transport: McpTransport::Stdio {
343 stdin,
344 reader,
345 child: Some(child),
346 },
347 next_id: 1,
348 capabilities: ServerCapabilities::default(),
349 server_name: server.name.clone(),
350 read_timeout,
351 elicitation: None,
352 };
353
354 client.do_initialize(&server.name).await?;
355 Ok(client)
356 }
357
358 async fn spawn_http_sse(server: &McpServer, url: &str) -> Result<Self> {
360 let http_client = reqwest::Client::builder()
361 .timeout(Duration::from_secs(30))
362 .build()
363 .map_err(|e| Error::Mcp {
364 server: server.name.clone(),
365 message: format!("failed to build HTTP client: {e}"),
366 })?;
367
368 let response = http_client
371 .get(url)
372 .header("Accept", "text/event-stream")
373 .send()
374 .await
375 .map_err(|e| Error::Mcp {
376 server: server.name.clone(),
377 message: format!("failed to connect to SSE endpoint `{url}`: {e}"),
378 })?;
379
380 if !response.status().is_success() {
381 return Err(Error::Mcp {
382 server: server.name.clone(),
383 message: format!("SSE endpoint `{url}` returned HTTP {}", response.status()),
384 });
385 }
386
387 let mut stream = response.bytes_stream();
390 let mut sse_buffer = String::new();
391 let mut post_url: Option<String> = None;
392 let mut found_endpoint = false;
393
394 let mut total_read = 0usize;
396 let max_read = 65536;
397
398 while total_read < max_read {
399 match timeout(Duration::from_secs(10), stream.next()).await {
400 Ok(Some(Ok(chunk))) => {
401 let chunk_str = String::from_utf8_lossy(&chunk);
402 total_read += chunk_str.len();
403 sse_buffer.push_str(&chunk_str);
404
405 if let Some(endpoint) = parse_sse_endpoint(&sse_buffer) {
407 post_url = Some(endpoint);
408 found_endpoint = true;
409 break;
410 }
411 }
412 Ok(Some(Err(e))) => {
413 return Err(Error::Mcp {
414 server: server.name.clone(),
415 message: format!("error reading SSE stream from `{url}`: {e}"),
416 });
417 }
418 Ok(None) => break, Err(_) => {
420 return Err(Error::Mcp {
421 server: server.name.clone(),
422 message: format!("timeout reading SSE stream from `{url}`"),
423 });
424 }
425 }
426 }
427
428 if !found_endpoint {
429 return Err(Error::Mcp {
430 server: server.name.clone(),
431 message: format!(
432 "SSE endpoint `{url}` did not send an `endpoint` event. Received data: {}",
433 &sse_buffer[..sse_buffer.len().min(200)]
434 ),
435 });
436 }
437
438 let mut client = Self {
439 transport: McpTransport::HttpSse {
440 client: http_client,
441 sse_url: url.to_string(),
442 post_url,
443 buffer: sse_buffer,
444 },
445 next_id: 1,
446 capabilities: ServerCapabilities::default(),
447 server_name: server.name.clone(),
448 read_timeout: DEFAULT_STDIO_READ_TIMEOUT,
451 elicitation: None,
452 };
453
454 client.do_initialize(&server.name).await?;
455 Ok(client)
456 }
457
458 async fn do_initialize(&mut self, server_name: &str) -> Result<()> {
460 let init_result: Value = self
461 .send_request(
462 "initialize",
463 serde_json::json!({
464 "protocolVersion": "2024-11-05",
465 "capabilities": {},
466 "clientInfo": {
467 "name": "recursive-agent",
468 "version": "0.1.0"
469 }
470 }),
471 )
472 .await?;
473
474 if let Some(server_proto) = init_result.get("protocolVersion").and_then(|v| v.as_str()) {
476 if server_proto != "2024-11-05" {
477 tracing::warn!(
479 target: "recursive::mcp",
480 server = %server_name,
481 server_protocol = %server_proto,
482 "MCP server protocol version mismatch"
483 );
484 }
485 }
486
487 if let Some(caps) = init_result.get("capabilities") {
489 self.capabilities.tools = caps.get("tools").and_then(|v| v.as_bool()).unwrap_or(false);
490 self.capabilities.resources = caps
491 .get("resources")
492 .and_then(|v| v.as_bool())
493 .unwrap_or(false);
494 self.capabilities.prompts = caps
495 .get("prompts")
496 .and_then(|v| v.as_bool())
497 .unwrap_or(false);
498 }
499
500 self.send_notification("notifications/initialized", serde_json::json!({}))
502 .await?;
503
504 Ok(())
505 }
506
507 pub async fn list_tools(&mut self) -> Result<Vec<McpToolSpec>> {
509 let result: Value = self
510 .send_request("tools/list", serde_json::json!({}))
511 .await?;
512
513 let tools_arr = result
514 .get("tools")
515 .and_then(|v| v.as_array())
516 .ok_or_else(|| Error::Mcp {
517 server: self.server_name.clone(),
518 message: "`tools/list` response missing `tools` array".into(),
519 })?;
520
521 let mut specs = Vec::with_capacity(tools_arr.len());
522 for item in tools_arr {
523 let name = item
524 .get("name")
525 .and_then(|v| v.as_str())
526 .ok_or_else(|| Error::Mcp {
527 server: self.server_name.clone(),
528 message: "tool entry missing `name`".into(),
529 })?;
530 let description = item
531 .get("description")
532 .and_then(|v| v.as_str())
533 .unwrap_or("");
534 let input_schema = item
535 .get("inputSchema")
536 .cloned()
537 .unwrap_or(serde_json::json!({"type": "object"}));
538 let annotations = item
540 .get("annotations")
541 .and_then(|v| serde_json::from_value::<McpToolAnnotations>(v.clone()).ok());
542 specs.push(McpToolSpec {
543 name: name.to_string(),
544 description: description.to_string(),
545 input_schema,
546 annotations,
547 });
548 }
549
550 Ok(specs)
551 }
552
553 pub fn with_elicitation(mut self, slot: SharedElicitationHandler) -> Self {
555 self.elicitation = Some(slot);
556 self
557 }
558
559 pub async fn call_tool(&mut self, name: &str, arguments: Value) -> Result<String> {
567 let result: Value = match self
568 .send_request(
569 "tools/call",
570 serde_json::json!({
571 "name": name,
572 "arguments": arguments,
573 }),
574 )
575 .await
576 {
577 Ok(v) => v,
578 Err(Error::Mcp { server, message }) if message.contains("code -32042") => {
579 if let Some(slot) = &self.elicitation {
582 let handler = slot.read().await.clone();
583 if let Some(h) = handler {
584 let req = parse_elicitation_from_error(&server, &message);
585 let _ = h.elicit(req).await;
586 }
587 }
588 return Err(Error::Mcp { server, message });
589 }
590 Err(e) => return Err(e),
591 };
592
593 if result
595 .get("isError")
596 .and_then(|v| v.as_bool())
597 .unwrap_or(false)
598 {
599 let error_msg = result
600 .get("content")
601 .and_then(|v| v.as_array())
602 .and_then(|arr| arr.first())
603 .and_then(|c| c.get("text"))
604 .and_then(|v| v.as_str())
605 .unwrap_or("unknown error");
606 return Err(Error::Tool {
607 name: name.to_string(),
608 call_id: None,
609 message: error_msg.to_string(),
610 });
611 }
612
613 let content = result
615 .get("content")
616 .and_then(|v| v.as_array())
617 .map(|arr| {
618 arr.iter()
619 .filter_map(|c| {
620 if c.get("type").and_then(|v| v.as_str()) == Some("text") {
621 c.get("text").and_then(|v| v.as_str())
622 } else {
623 None
624 }
625 })
626 .collect::<Vec<_>>()
627 .join("\n")
628 })
629 .unwrap_or_default();
630
631 Ok(content)
632 }
633
634 pub async fn list_resources(&mut self) -> Result<Vec<McpResource>> {
636 if !self.capabilities.resources {
637 return Err(Error::Mcp {
638 server: self.server_name.clone(),
639 message: "server does not advertise `resources` capability".into(),
640 });
641 }
642
643 let result: Value = self
644 .send_request("resources/list", serde_json::json!({}))
645 .await?;
646
647 let resources_arr = result
648 .get("resources")
649 .and_then(|v| v.as_array())
650 .ok_or_else(|| Error::Mcp {
651 server: self.server_name.clone(),
652 message: "`resources/list` response missing `resources` array".into(),
653 })?;
654
655 let mut resources = Vec::with_capacity(resources_arr.len());
656 for item in resources_arr {
657 let resource: McpResource =
658 serde_json::from_value(item.clone()).map_err(|e| Error::Mcp {
659 server: self.server_name.clone(),
660 message: format!("failed to parse resource: {e}"),
661 })?;
662 resources.push(resource);
663 }
664
665 Ok(resources)
666 }
667
668 pub async fn read_resource(&mut self, uri: &str) -> Result<Vec<McpResourceContent>> {
671 if !self.capabilities.resources {
672 return Err(Error::Mcp {
673 server: self.server_name.clone(),
674 message: "server does not advertise `resources` capability".into(),
675 });
676 }
677
678 let result: Value = self
679 .send_request("resources/read", serde_json::json!({ "uri": uri }))
680 .await?;
681
682 let contents_arr = result
683 .get("contents")
684 .and_then(|v| v.as_array())
685 .ok_or_else(|| Error::Mcp {
686 server: self.server_name.clone(),
687 message: "`resources/read` response missing `contents` array".into(),
688 })?;
689
690 let mut contents = Vec::with_capacity(contents_arr.len());
691 for item in contents_arr {
692 let content: McpResourceContent =
693 serde_json::from_value(item.clone()).map_err(|e| Error::Mcp {
694 server: self.server_name.clone(),
695 message: format!("failed to parse resource content: {e}"),
696 })?;
697 contents.push(content);
698 }
699
700 Ok(contents)
701 }
702
703 pub async fn list_prompts(&mut self) -> Result<Vec<McpPrompt>> {
705 if !self.capabilities.prompts {
706 return Err(Error::Mcp {
707 server: self.server_name.clone(),
708 message: "server does not advertise `prompts` capability".into(),
709 });
710 }
711
712 let result: Value = self
713 .send_request("prompts/list", serde_json::json!({}))
714 .await?;
715
716 let prompts_arr = result
717 .get("prompts")
718 .and_then(|v| v.as_array())
719 .ok_or_else(|| Error::Mcp {
720 server: self.server_name.clone(),
721 message: "`prompts/list` response missing `prompts` array".into(),
722 })?;
723
724 let mut prompts = Vec::with_capacity(prompts_arr.len());
725 for item in prompts_arr {
726 let prompt: McpPrompt =
727 serde_json::from_value(item.clone()).map_err(|e| Error::Mcp {
728 server: self.server_name.clone(),
729 message: format!("failed to parse prompt: {e}"),
730 })?;
731 prompts.push(prompt);
732 }
733
734 Ok(prompts)
735 }
736
737 pub async fn get_prompt(
740 &mut self,
741 name: &str,
742 arguments: Option<HashMap<String, String>>,
743 ) -> Result<Vec<McpPromptMessage>> {
744 if !self.capabilities.prompts {
745 return Err(Error::Mcp {
746 server: self.server_name.clone(),
747 message: "server does not advertise `prompts` capability".into(),
748 });
749 }
750
751 let mut params = serde_json::json!({ "name": name });
752 if let Some(args) = arguments {
753 params["arguments"] = serde_json::to_value(args).map_err(|e| Error::Mcp {
754 server: self.server_name.clone(),
755 message: format!("failed to serialize prompt arguments: {e}"),
756 })?;
757 }
758
759 let result: Value = self.send_request("prompts/get", params).await?;
760
761 let messages_arr = result
762 .get("messages")
763 .and_then(|v| v.as_array())
764 .ok_or_else(|| Error::Mcp {
765 server: self.server_name.clone(),
766 message: "`prompts/get` response missing `messages` array".into(),
767 })?;
768
769 let mut messages = Vec::with_capacity(messages_arr.len());
770 for item in messages_arr {
771 let role = item
772 .get("role")
773 .and_then(|v| v.as_str())
774 .ok_or_else(|| Error::Mcp {
775 server: self.server_name.clone(),
776 message: "prompt message missing `role`".into(),
777 })?;
778 let content = item
779 .get("content")
780 .and_then(|v| v.get("text"))
781 .and_then(|v| v.as_str())
782 .ok_or_else(|| Error::Mcp {
783 server: self.server_name.clone(),
784 message: "prompt message missing `content.text`".into(),
785 })?;
786 messages.push(McpPromptMessage {
787 role: role.to_string(),
788 content: content.to_string(),
789 });
790 }
791
792 Ok(messages)
793 }
794
795 async fn send_request(&mut self, method: &str, params: Value) -> Result<Value> {
801 let id = self.next_id;
802 self.next_id += 1;
803
804 let request = serde_json::json!({
805 "jsonrpc": "2.0",
806 "id": id,
807 "method": method,
808 "params": params,
809 });
810
811 self.write_line(&request).await?;
812 self.read_response(id).await
813 }
814
815 async fn send_notification(&mut self, method: &str, params: Value) -> Result<()> {
817 let notification = serde_json::json!({
818 "jsonrpc": "2.0",
819 "method": method,
820 "params": params,
821 });
822
823 self.write_line(¬ification).await
824 }
825
826 async fn write_line(&mut self, value: &Value) -> Result<()> {
828 match &mut self.transport {
829 McpTransport::Stdio { stdin, .. } => {
830 let line = serde_json::to_string(value)?;
831 let mut full = line.into_bytes();
832 full.push(b'\n');
833 stdin.write_all(&full).await.map_err(|e| Error::Mcp {
834 server: self.server_name.clone(),
835 message: format!("write error: {e}"),
836 })?;
837 stdin.flush().await.map_err(|e| Error::Mcp {
838 server: self.server_name.clone(),
839 message: format!("flush error: {e}"),
840 })?;
841 Ok(())
842 }
843 McpTransport::HttpSse {
844 client, post_url, ..
845 } => {
846 let url = post_url.as_ref().ok_or_else(|| Error::Mcp {
847 server: self.server_name.clone(),
848 message: "HTTP transport: no POST endpoint available".into(),
849 })?;
850 let body = serde_json::to_string(value)?;
851 let response = client
852 .post(url)
853 .header("Content-Type", "application/json")
854 .body(body)
855 .send()
856 .await
857 .map_err(|e| Error::Mcp {
858 server: self.server_name.clone(),
859 message: format!("HTTP POST error: {e}"),
860 })?;
861 if !response.status().is_success() {
862 return Err(Error::Mcp {
863 server: self.server_name.clone(),
864 message: format!(
865 "HTTP POST to `{url}` returned HTTP {}",
866 response.status()
867 ),
868 });
869 }
870 Ok(())
871 }
872 }
873 }
874
875 async fn read_response(&mut self, expected_id: u64) -> Result<Value> {
877 match &mut self.transport {
878 McpTransport::Stdio { reader, .. } => {
879 Self::read_stdio_response(reader, expected_id, &self.server_name, self.read_timeout)
880 .await
881 }
882 McpTransport::HttpSse {
883 client,
884 sse_url,
885 post_url,
886 buffer,
887 } => {
888 Self::read_sse_response(
889 client,
890 sse_url,
891 post_url,
892 buffer,
893 expected_id,
894 &self.server_name,
895 )
896 .await
897 }
898 }
899 }
900
901 async fn read_stdio_response(
903 reader: &mut BufReader<ChildStdout>,
904 expected_id: u64,
905 server_name: &str,
906 read_timeout: Duration,
907 ) -> Result<Value> {
908 let mut line_buf = String::new();
909
910 loop {
911 line_buf.clear();
912
913 let read_future = reader.read_line(&mut line_buf);
914 match timeout(read_timeout, read_future).await {
915 Ok(Ok(0)) => {
916 return Err(Error::Mcp {
917 server: server_name.to_string(),
918 message: "server closed stdout unexpectedly".into(),
919 });
920 }
921 Ok(Ok(_)) => {
922 let trimmed = line_buf.trim();
923 if trimmed.is_empty() {
924 continue;
925 }
926
927 let parsed: Value = serde_json::from_str(trimmed).map_err(|e| Error::Mcp {
928 server: server_name.to_string(),
929 message: format!("server returned non-JSON line: {e}; line: {trimmed}"),
930 })?;
931
932 if let Some(resp_id) = parsed.get("id") {
933 if resp_id.as_u64() == Some(expected_id) {
934 if let Some(err) = parsed.get("error") {
935 let msg = err
936 .get("message")
937 .and_then(|v| v.as_str())
938 .unwrap_or("unknown error");
939 let code = err.get("code").and_then(|v| v.as_i64()).unwrap_or(-1);
940 return Err(Error::Mcp {
941 server: server_name.to_string(),
942 message: format!("error (code {code}): {msg}"),
943 });
944 }
945 if let Some(result) = parsed.get("result") {
946 return Ok(result.clone());
947 }
948 return Err(Error::Mcp {
949 server: server_name.to_string(),
950 message: "response missing both `result` and `error`".into(),
951 });
952 }
953 }
954 }
955 Ok(Err(e)) => {
956 return Err(Error::Mcp {
957 server: server_name.to_string(),
958 message: format!("read error: {e}"),
959 });
960 }
961 Err(_) => {
962 return Err(Error::Mcp {
963 server: server_name.to_string(),
964 message: format!(
965 "server timed out (no response within {}s)",
966 read_timeout.as_secs()
967 ),
968 });
969 }
970 }
971 }
972 }
973
974 async fn read_sse_response(
976 client: &reqwest::Client,
977 sse_url: &str,
978 _post_url: &Option<String>,
979 buffer: &mut String,
980 expected_id: u64,
981 server_name: &str,
982 ) -> Result<Value> {
983 if !buffer.is_empty() {
985 if let Some(result) = parse_sse_response(buffer, expected_id, server_name) {
986 return result;
987 }
988 }
989
990 let response = client
992 .get(sse_url)
993 .header("Accept", "text/event-stream")
994 .send()
995 .await
996 .map_err(|e| Error::Mcp {
997 server: server_name.to_string(),
998 message: format!("failed to reconnect to SSE endpoint `{sse_url}`: {e}"),
999 })?;
1000
1001 if !response.status().is_success() {
1002 return Err(Error::Mcp {
1003 server: server_name.to_string(),
1004 message: format!(
1005 "SSE endpoint `{sse_url}` returned HTTP {}",
1006 response.status()
1007 ),
1008 });
1009 }
1010
1011 let mut stream = response.bytes_stream();
1012
1013 loop {
1014 match timeout(Duration::from_secs(10), stream.next()).await {
1015 Ok(Some(Ok(chunk))) => {
1016 let chunk_str = String::from_utf8_lossy(&chunk);
1017 buffer.push_str(&chunk_str);
1018
1019 if let Some(result) = parse_sse_response(buffer, expected_id, server_name) {
1020 return result;
1021 }
1022 }
1023 Ok(Some(Err(e))) => {
1024 return Err(Error::Mcp {
1025 server: server_name.to_string(),
1026 message: format!("error reading SSE stream from `{sse_url}`: {e}"),
1027 });
1028 }
1029 Ok(None) => {
1030 return Err(Error::Mcp {
1031 server: server_name.to_string(),
1032 message: format!("SSE stream ended without response for id {expected_id}"),
1033 });
1034 }
1035 Err(_) => {
1036 return Err(Error::Mcp {
1037 server: server_name.to_string(),
1038 message: format!("SSE timed out waiting for response for id {expected_id}"),
1039 });
1040 }
1041 }
1042 }
1043 }
1044}
1045
1046fn parse_sse_endpoint(buffer: &str) -> Option<String> {
1053 let mut current_event: Option<&str> = None;
1054
1055 for line in buffer.lines() {
1056 let line = line.trim();
1057 if line.starts_with("event:") {
1058 current_event = Some(line.strip_prefix("event:").unwrap_or("").trim());
1059 } else if line.starts_with("data:") && current_event == Some("endpoint") {
1060 let data = line.strip_prefix("data:").unwrap_or("").trim();
1061 if !data.is_empty() {
1062 return Some(data.to_string());
1063 }
1064 }
1065 if line.is_empty() {
1067 current_event = None;
1068 }
1069 }
1070
1071 None
1072}
1073
1074fn parse_sse_response(buffer: &str, expected_id: u64, server_name: &str) -> Option<Result<Value>> {
1078 let mut current_data = String::new();
1079
1080 for line in buffer.lines() {
1081 let trimmed = line.trim();
1082 if trimmed.starts_with("data:") {
1083 let data = trimmed.strip_prefix("data:").unwrap_or("").trim();
1084 current_data.push_str(data);
1085 } else if trimmed.is_empty() && !current_data.is_empty() {
1086 if let Ok(parsed) = serde_json::from_str::<Value>(¤t_data) {
1088 if let Some(resp_id) = parsed.get("id") {
1089 if resp_id.as_u64() == Some(expected_id) {
1090 if let Some(err) = parsed.get("error") {
1091 let msg = err
1092 .get("message")
1093 .and_then(|v| v.as_str())
1094 .unwrap_or("unknown error");
1095 let code = err.get("code").and_then(|v| v.as_i64()).unwrap_or(-1);
1096 return Some(Err(Error::Mcp {
1097 server: server_name.to_string(),
1098 message: format!("error (code {code}): {msg}"),
1099 }));
1100 }
1101 if let Some(result) = parsed.get("result") {
1102 return Some(Ok(result.clone()));
1103 }
1104 return Some(Err(Error::Mcp {
1105 server: server_name.to_string(),
1106 message: "response missing both `result` and `error`".into(),
1107 }));
1108 }
1109 }
1110 }
1111 current_data.clear();
1112 } else if !trimmed.starts_with("event:")
1113 && !trimmed.starts_with("data:")
1114 && !trimmed.is_empty()
1115 {
1116 if !trimmed.starts_with(':')
1119 && !trimmed.starts_with("id:")
1120 && !trimmed.starts_with("retry:")
1121 {
1122 current_data.clear();
1123 }
1124 }
1125 }
1126
1127 if !current_data.is_empty() {
1129 if let Ok(parsed) = serde_json::from_str::<Value>(¤t_data) {
1130 if let Some(resp_id) = parsed.get("id") {
1131 if resp_id.as_u64() == Some(expected_id) {
1132 if let Some(err) = parsed.get("error") {
1133 let msg = err
1134 .get("message")
1135 .and_then(|v| v.as_str())
1136 .unwrap_or("unknown error");
1137 let code = err.get("code").and_then(|v| v.as_i64()).unwrap_or(-1);
1138 return Some(Err(Error::Mcp {
1139 server: server_name.to_string(),
1140 message: format!("error (code {code}): {msg}"),
1141 }));
1142 }
1143 if let Some(result) = parsed.get("result") {
1144 return Some(Ok(result.clone()));
1145 }
1146 }
1147 }
1148 }
1149 }
1150
1151 None
1152}
1153
1154#[derive(Debug, Clone, Serialize, Deserialize)]
1160pub struct JsonRpcRequest {
1161 pub jsonrpc: String,
1162 #[serde(default, skip_serializing_if = "Option::is_none")]
1163 pub id: Option<serde_json::Value>,
1164 pub method: String,
1165 #[serde(default)]
1166 pub params: serde_json::Value,
1167}
1168
1169#[derive(Debug, Clone, Serialize, Deserialize)]
1171pub struct JsonRpcResponse {
1172 pub jsonrpc: String,
1173 #[serde(default, skip_serializing_if = "Option::is_none")]
1174 pub id: Option<serde_json::Value>,
1175 #[serde(default, skip_serializing_if = "Option::is_none")]
1176 pub result: Option<serde_json::Value>,
1177 #[serde(default, skip_serializing_if = "Option::is_none")]
1178 pub error: Option<JsonRpcError>,
1179}
1180
1181impl JsonRpcResponse {
1182 pub fn success(id: Option<serde_json::Value>, result: serde_json::Value) -> Self {
1184 Self {
1185 jsonrpc: "2.0".to_string(),
1186 id,
1187 result: Some(result),
1188 error: None,
1189 }
1190 }
1191
1192 pub fn error(id: Option<serde_json::Value>, code: i64, message: impl Into<String>) -> Self {
1194 Self {
1195 jsonrpc: "2.0".to_string(),
1196 id,
1197 result: None,
1198 error: Some(JsonRpcError {
1199 code,
1200 message: message.into(),
1201 data: None,
1202 }),
1203 }
1204 }
1205
1206 pub fn method_not_found(id: Option<serde_json::Value>, method: &str) -> Self {
1208 Self::error(id, -32601, format!("Method not found: {method}"))
1209 }
1210
1211 pub fn internal_error(id: Option<serde_json::Value>, message: impl Into<String>) -> Self {
1213 Self::error(id, -32603, message)
1214 }
1215}
1216
1217#[derive(Debug, Clone, Serialize, Deserialize)]
1219pub struct JsonRpcError {
1220 pub code: i64,
1221 pub message: String,
1222 #[serde(default, skip_serializing_if = "Option::is_none")]
1223 pub data: Option<serde_json::Value>,
1224}
1225
1226pub async fn dispatch_request(
1236 request: &JsonRpcRequest,
1237 client: &mut McpClient,
1238) -> Option<JsonRpcResponse> {
1239 let id = request.id.clone();
1240
1241 match request.method.as_str() {
1242 "initialize" => {
1243 let result = serde_json::json!({
1246 "protocolVersion": "2024-11-05",
1247 "capabilities": {
1248 "tools": true,
1249 "resources": true,
1250 "prompts": true
1251 },
1252 "serverInfo": {
1253 "name": "recursive-agent",
1254 "version": "0.1.0"
1255 }
1256 });
1257 Some(JsonRpcResponse::success(id, result))
1258 }
1259 "notifications/initialized" => {
1260 None
1262 }
1263 "tools/list" => match client.list_tools().await {
1264 Ok(tools) => {
1265 let tools_arr: Vec<serde_json::Value> = tools
1266 .into_iter()
1267 .map(|t| {
1268 serde_json::json!({
1269 "name": t.name,
1270 "description": t.description,
1271 "inputSchema": t.input_schema,
1272 })
1273 })
1274 .collect();
1275 Some(JsonRpcResponse::success(
1276 id,
1277 serde_json::json!({ "tools": tools_arr }),
1278 ))
1279 }
1280 Err(e) => Some(JsonRpcResponse::internal_error(id, e.to_string())),
1281 },
1282 "tools/call" => {
1283 let name = request
1284 .params
1285 .get("name")
1286 .and_then(|v| v.as_str())
1287 .unwrap_or("");
1288 let arguments = request.params.get("arguments").cloned().unwrap_or_default();
1289
1290 match client.call_tool(name, arguments).await {
1291 Ok(text) => {
1292 let result = serde_json::json!({
1293 "content": [{"type": "text", "text": text}]
1294 });
1295 Some(JsonRpcResponse::success(id, result))
1296 }
1297 Err(e) => {
1298 let result = serde_json::json!({
1301 "isError": true,
1302 "content": [{"type": "text", "text": e.to_string()}]
1303 });
1304 Some(JsonRpcResponse::success(id, result))
1305 }
1306 }
1307 }
1308 "resources/list" => match client.list_resources().await {
1309 Ok(resources) => {
1310 let resources_arr: Vec<serde_json::Value> = resources
1311 .into_iter()
1312 .map(|r| {
1313 serde_json::json!({
1314 "uri": r.uri,
1315 "name": r.name,
1316 "description": r.description,
1317 "mimeType": r.mime_type,
1318 })
1319 })
1320 .collect();
1321 Some(JsonRpcResponse::success(
1322 id,
1323 serde_json::json!({ "resources": resources_arr }),
1324 ))
1325 }
1326 Err(e) => Some(JsonRpcResponse::internal_error(id, e.to_string())),
1327 },
1328 "resources/read" => {
1329 let uri = request
1330 .params
1331 .get("uri")
1332 .and_then(|v| v.as_str())
1333 .unwrap_or("");
1334
1335 match client.read_resource(uri).await {
1336 Ok(contents) => {
1337 let contents_arr: Vec<serde_json::Value> = contents
1338 .into_iter()
1339 .map(|c| {
1340 serde_json::json!({
1341 "uri": c.uri,
1342 "mimeType": c.mime_type,
1343 "text": c.text,
1344 "blob": c.blob,
1345 })
1346 })
1347 .collect();
1348 Some(JsonRpcResponse::success(
1349 id,
1350 serde_json::json!({ "contents": contents_arr }),
1351 ))
1352 }
1353 Err(e) => Some(JsonRpcResponse::internal_error(id, e.to_string())),
1354 }
1355 }
1356 "prompts/list" => match client.list_prompts().await {
1357 Ok(prompts) => {
1358 let prompts_arr: Vec<serde_json::Value> = prompts
1359 .into_iter()
1360 .map(|p| {
1361 serde_json::json!({
1362 "name": p.name,
1363 "description": p.description,
1364 "arguments": p.arguments,
1365 })
1366 })
1367 .collect();
1368 Some(JsonRpcResponse::success(
1369 id,
1370 serde_json::json!({ "prompts": prompts_arr }),
1371 ))
1372 }
1373 Err(e) => Some(JsonRpcResponse::internal_error(id, e.to_string())),
1374 },
1375 "prompts/get" => {
1376 let name = request
1377 .params
1378 .get("name")
1379 .and_then(|v| v.as_str())
1380 .unwrap_or("");
1381 let arguments = request
1382 .params
1383 .get("arguments")
1384 .and_then(|v| serde_json::from_value::<HashMap<String, String>>(v.clone()).ok());
1385
1386 match client.get_prompt(name, arguments).await {
1387 Ok(messages) => {
1388 let messages_arr: Vec<serde_json::Value> = messages
1389 .into_iter()
1390 .map(|m| {
1391 serde_json::json!({
1392 "role": m.role,
1393 "content": {"type": "text", "text": m.content},
1394 })
1395 })
1396 .collect();
1397 Some(JsonRpcResponse::success(
1398 id,
1399 serde_json::json!({ "messages": messages_arr }),
1400 ))
1401 }
1402 Err(e) => Some(JsonRpcResponse::internal_error(id, e.to_string())),
1403 }
1404 }
1405 _ => Some(JsonRpcResponse::method_not_found(id, &request.method)),
1406 }
1407}
1408
1409fn parse_elicitation_from_error(server: &str, message: &str) -> ElicitationRequest {
1410 let human = message
1412 .split_once(": ")
1413 .map(|(_, rest)| rest.to_string())
1414 .unwrap_or_else(|| message.to_string());
1415 ElicitationRequest {
1416 mcp_server_name: server.to_string(),
1417 message: human,
1418 mode: Some("url".into()),
1419 url: None,
1420 elicitation_id: None,
1421 requested_schema: None,
1422 title: None,
1423 display_name: None,
1424 description: None,
1425 }
1426}
1427
1428pub struct McpTool {
1435 client: Arc<Mutex<McpClient>>,
1436 spec: McpToolSpec,
1437 server_name: String,
1438 trust: McpServerTrust,
1441}
1442
1443impl McpTool {
1444 pub fn new(
1445 client: Arc<Mutex<McpClient>>,
1446 spec: McpToolSpec,
1447 server_name: impl Into<String>,
1448 ) -> Self {
1449 Self {
1450 client,
1451 spec,
1452 server_name: server_name.into(),
1453 trust: McpServerTrust::Untrusted,
1454 }
1455 }
1456
1457 pub fn with_trust(mut self, trust: McpServerTrust) -> Self {
1459 self.trust = trust;
1460 self
1461 }
1462}
1463
1464#[async_trait]
1465impl Tool for McpTool {
1466 fn is_deferred(&self) -> bool {
1467 true
1468 }
1469
1470 fn spec(&self) -> ToolSpec {
1471 ToolSpec {
1472 name: format!("mcp__{}__{}", self.server_name, self.spec.name),
1473 description: format!("[mcp:{}] {}", self.server_name, self.spec.description),
1474 parameters: self.spec.input_schema.clone(),
1475 }
1476 }
1477
1478 fn side_effect_class(&self) -> crate::tools::ToolSideEffect {
1479 use crate::tools::ToolSideEffect;
1480 let Some(ann) = &self.spec.annotations else {
1482 return ToolSideEffect::External;
1483 };
1484 if self.trust != McpServerTrust::Trusted {
1485 return ToolSideEffect::External;
1486 }
1487 if ann.read_only_hint {
1488 ToolSideEffect::ReadOnly
1489 } else if ann.open_world_hint {
1490 ToolSideEffect::External
1491 } else {
1492 ToolSideEffect::Mutating
1493 }
1494 }
1495
1496 async fn execute(&self, arguments: Value) -> Result<String> {
1497 let mut client = self.client.lock().await;
1498 client.call_tool(&self.spec.name, arguments).await
1499 }
1500}
1501
1502pub fn load_mcp_config(path: &std::path::Path) -> Result<Vec<McpServer>> {
1512 let contents = std::fs::read_to_string(path).map_err(|e| Error::Mcp {
1513 server: "config".into(),
1514 message: format!("failed to read config `{}`: {e}", path.display()),
1515 })?;
1516 let parsed: McpConfigFile = serde_json::from_str(&contents).map_err(|e| Error::Mcp {
1517 server: "config".into(),
1518 message: format!("failed to parse config `{}`: {e}", path.display()),
1519 })?;
1520 Ok(parsed.servers)
1521}
1522
1523#[derive(Debug, Deserialize)]
1524struct McpConfigFile {
1525 servers: Vec<McpServer>,
1526}
1527
1528#[derive(Debug, Deserialize)]
1534#[serde(deny_unknown_fields)]
1535struct McpDiscoveryFile {
1536 #[serde(rename = "mcpServers")]
1537 mcp_servers: HashMap<String, McpServerConfig>,
1538}
1539
1540pub async fn discover_mcp_servers(workspace: &Path) -> Result<Vec<McpServer>> {
1548 let primary = workspace.join(".mcp.json");
1550 if primary.exists() {
1551 let configs = load_mcp_discovery_config(&primary).await?;
1552 if !configs.is_empty() {
1553 return Ok(configs);
1554 }
1555 }
1556
1557 let fallback = workspace.join(".recursive").join("mcp.json");
1559 if fallback.exists() {
1560 let configs = load_mcp_discovery_config(&fallback).await?;
1561 return Ok(configs);
1562 }
1563
1564 Ok(Vec::new())
1565}
1566
1567async fn load_mcp_discovery_config(path: &Path) -> Result<Vec<McpServer>> {
1592 let contents = tokio::fs::read_to_string(path)
1593 .await
1594 .map_err(|e| Error::Mcp {
1595 server: "discovery".into(),
1596 message: format!("failed to read discovery config `{}`: {e}", path.display()),
1597 })?;
1598
1599 if contents.trim().is_empty() {
1601 return Ok(Vec::new());
1602 }
1603
1604 let parsed: McpDiscoveryFile = serde_json::from_str(&contents).map_err(|e| Error::Mcp {
1605 server: "discovery".into(),
1606 message: format!("failed to parse discovery config `{}`: {e}", path.display()),
1607 })?;
1608
1609 let servers: Vec<McpServer> = parsed
1610 .mcp_servers
1611 .into_iter()
1612 .map(McpServer::from)
1613 .collect();
1614
1615 Ok(servers)
1616}
1617
1618#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1624pub enum KillOutcome {
1625 Graceful,
1627 Timeout,
1629}
1630
1631pub async fn kill_gracefully(
1648 child: &mut tokio::process::Child,
1649 grace_period: std::time::Duration,
1650 server_name: &str,
1651) -> KillOutcome {
1652 #[cfg(unix)]
1653 {
1654 use std::os::unix::process::ExitStatusExt;
1655 use tokio::time::timeout;
1656
1657 tracing::info!(server = %server_name, "kill-gracefully: sending SIGTERM");
1659 let _ = child.start_kill(); match timeout(grace_period, child.wait()).await {
1663 Ok(Ok(status)) => {
1664 if let Some(signal) = status.signal() {
1665 tracing::info!(
1666 server = %server_name,
1667 signal = signal,
1668 "kill-gracefully: process exited via signal {signal}"
1669 );
1670 } else {
1671 tracing::info!(
1672 server = %server_name,
1673 "kill-gracefully: process exited gracefully"
1674 );
1675 }
1676 KillOutcome::Graceful
1677 }
1678 Ok(Err(e)) => {
1679 tracing::warn!(
1681 server = %server_name,
1682 error = %e,
1683 "kill-gracefully: wait() error after SIGTERM, sending SIGKILL"
1684 );
1685 let _ = child.start_kill();
1686 let _ = child.wait().await;
1687 KillOutcome::Timeout
1688 }
1689 Err(_elapsed) => {
1690 tracing::warn!(
1692 server = %server_name,
1693 grace_ms = grace_period.as_millis(),
1694 "kill-gracefully: SIGTERM timeout, sending SIGKILL"
1695 );
1696 let pid = child.id().unwrap_or(0);
1699 let _ = std::process::Command::new("kill")
1700 .args(["-9", &pid.to_string()])
1701 .status();
1702 let _ = child.wait().await;
1703 KillOutcome::Timeout
1704 }
1705 }
1706 }
1707
1708 #[cfg(not(unix))]
1709 {
1710 let _ = (grace_period, server_name);
1711 tracing::info!("kill-gracefully: non-Unix, sending kill immediately");
1712 let _ = child.start_kill();
1713 let _ = child.wait().await;
1714 KillOutcome::Graceful
1715 }
1716}
1717
1718pub const DEFAULT_KILL_GRACE_PERIOD: std::time::Duration = std::time::Duration::from_secs(5);
1720
1721#[derive(Debug, Clone, Default)]
1732pub struct McpToolRouter {
1733 server_prefixes: std::collections::HashMap<String, Vec<String>>,
1737}
1738
1739impl McpToolRouter {
1740 pub fn new() -> Self {
1742 Self::default()
1743 }
1744
1745 pub fn register_server(&mut self, server_name: &str, tool_names: &[String]) {
1750 let entry = self.server_prefixes.entry(server_name.to_string());
1751 let prefixes = entry.or_default();
1752 for name in tool_names {
1753 let prefixed = format!("mcp__{}__{}", server_name, name);
1755 if !prefixes.contains(name) {
1756 prefixes.push(name.clone());
1757 }
1758 if !prefixes.contains(&prefixed) {
1759 prefixes.push(prefixed);
1760 }
1761 }
1762 }
1763
1764 pub fn route(&self, tool_name: &str) -> Option<&str> {
1769 for (server, prefixes) in &self.server_prefixes {
1771 if prefixes.iter().any(|p| p == tool_name) {
1772 return Some(server.as_str());
1773 }
1774 }
1775 for (server, prefixes) in &self.server_prefixes {
1777 if prefixes.iter().any(|p| p == tool_name) {
1778 return Some(server.as_str());
1779 }
1780 }
1781 None
1782 }
1783}
1784
1785#[cfg(test)]
1790mod tests {
1791 use super::*;
1792
1793 #[test]
1800 fn load_mcp_config_parses_correctly() {
1801 let dir = tempfile::TempDir::new().unwrap();
1802 let path = dir.path().join("mcp.json");
1803 std::fs::write(
1804 &path,
1805 r#"{
1806 "servers": [
1807 {"name": "fs", "command": "mcp-fs", "args": ["--root", "."]},
1808 {"name": "github", "command": "mcp-gh", "args": []}
1809 ]
1810 }"#,
1811 )
1812 .unwrap();
1813
1814 let servers = load_mcp_config(&path).unwrap();
1815 assert_eq!(servers.len(), 2);
1816 assert_eq!(servers[0].name, "fs");
1817 assert_eq!(servers[0].command, "mcp-fs");
1818 assert_eq!(servers[0].args, vec!["--root", "."]);
1819 assert_eq!(servers[1].name, "github");
1820 assert_eq!(servers[1].command, "mcp-gh");
1821 assert!(servers[1].args.is_empty());
1822 }
1823
1824 #[test]
1825 fn load_mcp_config_empty_servers() {
1826 let dir = tempfile::TempDir::new().unwrap();
1827 let path = dir.path().join("empty.json");
1828 std::fs::write(&path, r#"{"servers": []}"#).unwrap();
1829
1830 let servers = load_mcp_config(&path).unwrap();
1831 assert!(servers.is_empty());
1832 }
1833
1834 #[test]
1835 fn load_mcp_config_missing_file_errors() {
1836 let dir = tempfile::TempDir::new().unwrap();
1837 let path = dir.path().join("nonexistent.json");
1838 let err = load_mcp_config(&path).unwrap_err();
1839 assert!(err.to_string().contains("failed to read config"));
1840 }
1841
1842 #[tokio::test]
1847 async fn discover_finds_dot_mcp_json_in_workspace_root() {
1848 let dir = tempfile::TempDir::new().unwrap();
1849 let mcp_path = dir.path().join(".mcp.json");
1850 tokio::fs::write(
1851 &mcp_path,
1852 r#"{
1853 "mcpServers": {
1854 "fs": {
1855 "command": "mcp-fs",
1856 "args": ["--root", "."]
1857 }
1858 }
1859 }"#,
1860 )
1861 .await
1862 .unwrap();
1863
1864 let servers = discover_mcp_servers(dir.path()).await.unwrap();
1865 assert_eq!(servers.len(), 1);
1866 assert_eq!(servers[0].name, "fs");
1867 assert_eq!(servers[0].command, "mcp-fs");
1868 assert_eq!(servers[0].args, vec!["--root", "."]);
1869 }
1870
1871 #[tokio::test]
1872 async fn discover_returns_empty_vec_when_no_config_file() {
1873 let dir = tempfile::TempDir::new().unwrap();
1874 let servers = discover_mcp_servers(dir.path()).await.unwrap();
1875 assert!(servers.is_empty());
1876 }
1877
1878 #[tokio::test]
1879 async fn discover_parses_claude_code_format_with_env() {
1880 let dir = tempfile::TempDir::new().unwrap();
1881 let mcp_path = dir.path().join(".mcp.json");
1882 tokio::fs::write(
1883 &mcp_path,
1884 r#"{
1885 "mcpServers": {
1886 "github": {
1887 "command": "mcp-gh",
1888 "args": [],
1889 "env": {
1890 "GITHUB_TOKEN": "abc123"
1891 }
1892 },
1893 "filesystem": {
1894 "command": "mcp-fs",
1895 "args": ["--root", "/tmp"]
1896 }
1897 }
1898 }"#,
1899 )
1900 .await
1901 .unwrap();
1902
1903 let servers = discover_mcp_servers(dir.path()).await.unwrap();
1904 assert_eq!(servers.len(), 2);
1905
1906 let gh = servers.iter().find(|s| s.name == "github").unwrap();
1907 assert_eq!(gh.command, "mcp-gh");
1908 assert!(gh.args.is_empty());
1909
1910 let fs = servers.iter().find(|s| s.name == "filesystem").unwrap();
1911 assert_eq!(fs.command, "mcp-fs");
1912 assert_eq!(fs.args, vec!["--root", "/tmp"]);
1913 }
1914
1915 #[tokio::test]
1916 async fn discover_finds_dot_recursive_mcp_json_as_fallback() {
1917 let dir = tempfile::TempDir::new().unwrap();
1918 let recursive_dir = dir.path().join(".recursive");
1919 tokio::fs::create_dir(&recursive_dir).await.unwrap();
1920 let mcp_path = recursive_dir.join("mcp.json");
1921 tokio::fs::write(
1922 &mcp_path,
1923 r#"{
1924 "mcpServers": {
1925 "db": {
1926 "command": "mcp-db",
1927 "args": ["--port", "5432"]
1928 }
1929 }
1930 }"#,
1931 )
1932 .await
1933 .unwrap();
1934
1935 let servers = discover_mcp_servers(dir.path()).await.unwrap();
1936 assert_eq!(servers.len(), 1);
1937 assert_eq!(servers[0].name, "db");
1938 assert_eq!(servers[0].command, "mcp-db");
1939 }
1940
1941 #[tokio::test]
1942 async fn discover_dot_mcp_json_takes_priority_over_dot_recursive() {
1943 let dir = tempfile::TempDir::new().unwrap();
1944
1945 tokio::fs::write(
1947 dir.path().join(".mcp.json"),
1948 r#"{
1949 "mcpServers": {
1950 "primary": {
1951 "command": "primary-server",
1952 "args": []
1953 }
1954 }
1955 }"#,
1956 )
1957 .await
1958 .unwrap();
1959
1960 let recursive_dir = dir.path().join(".recursive");
1962 tokio::fs::create_dir(&recursive_dir).await.unwrap();
1963 tokio::fs::write(
1964 recursive_dir.join("mcp.json"),
1965 r#"{
1966 "mcpServers": {
1967 "fallback": {
1968 "command": "fallback-server",
1969 "args": []
1970 }
1971 }
1972 }"#,
1973 )
1974 .await
1975 .unwrap();
1976
1977 let servers = discover_mcp_servers(dir.path()).await.unwrap();
1978 assert_eq!(servers.len(), 1);
1979 assert_eq!(servers[0].name, "primary");
1980 }
1981
1982 #[tokio::test]
1983 async fn discover_malformed_json_returns_descriptive_error() {
1984 let dir = tempfile::TempDir::new().unwrap();
1985 tokio::fs::write(dir.path().join(".mcp.json"), "not valid json")
1986 .await
1987 .unwrap();
1988
1989 let err = discover_mcp_servers(dir.path()).await.unwrap_err();
1990 let msg = err.to_string();
1991 assert!(
1992 msg.contains("failed to parse discovery config"),
1993 "error should mention parsing failure: {msg}"
1994 );
1995 }
1996
1997 #[tokio::test]
1998 async fn discover_empty_file_returns_empty_vec() {
1999 let dir = tempfile::TempDir::new().unwrap();
2000 tokio::fs::write(dir.path().join(".mcp.json"), "")
2001 .await
2002 .unwrap();
2003
2004 let servers = discover_mcp_servers(dir.path()).await.unwrap();
2005 assert!(servers.is_empty());
2006 }
2007
2008 #[tokio::test]
2009 async fn discover_empty_mcp_servers_object_returns_empty_vec() {
2010 let dir = tempfile::TempDir::new().unwrap();
2011 tokio::fs::write(dir.path().join(".mcp.json"), r#"{"mcpServers": {}}"#)
2012 .await
2013 .unwrap();
2014
2015 let servers = discover_mcp_servers(dir.path()).await.unwrap();
2016 assert!(servers.is_empty());
2017 }
2018
2019 #[tokio::test]
2024 async fn test_http_sse_discovery_with_url() {
2025 let dir = tempfile::TempDir::new().unwrap();
2026 let mcp_path = dir.path().join(".mcp.json");
2027 tokio::fs::write(
2028 &mcp_path,
2029 r#"{
2030 "mcpServers": {
2031 "remote": {
2032 "url": "http://example.com/sse"
2033 }
2034 }
2035 }"#,
2036 )
2037 .await
2038 .unwrap();
2039
2040 let servers = discover_mcp_servers(dir.path()).await.unwrap();
2041 assert_eq!(servers.len(), 1);
2042 assert_eq!(servers[0].name, "remote");
2043 assert_eq!(servers[0].command, "");
2044 assert!(servers[0].args.is_empty());
2045 assert_eq!(servers[0].url.as_deref(), Some("http://example.com/sse"));
2046 }
2047
2048 #[test]
2049 fn test_parse_sse_endpoint() {
2050 let buffer = "event: endpoint\ndata: http://localhost:3000/message\n\n";
2051 assert_eq!(
2052 parse_sse_endpoint(buffer),
2053 Some("http://localhost:3000/message".to_string())
2054 );
2055
2056 let buffer = "event: message\ndata: {\"key\": \"value\"}\n\n";
2058 assert_eq!(parse_sse_endpoint(buffer), None);
2059
2060 let buffer = "event: endpoint\ndata: \n\n";
2062 assert_eq!(parse_sse_endpoint(buffer), None);
2063 }
2064
2065 #[test]
2066 fn test_parse_sse_response() {
2067 let buffer = "data: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"key\":\"value\"}}\n\n";
2068 let result = parse_sse_response(buffer, 1, "test");
2069 assert!(result.is_some());
2070 let result = result.unwrap().unwrap();
2071 assert_eq!(result.get("key").and_then(|v| v.as_str()), Some("value"));
2072
2073 let result = parse_sse_response(buffer, 2, "test");
2075 assert!(result.is_none());
2076
2077 let buffer =
2079 "data: {\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{\"code\":-32601,\"message\":\"Method not found\"}}\n\n";
2080 let result = parse_sse_response(buffer, 1, "test");
2081 assert!(result.is_some());
2082 assert!(result.unwrap().is_err());
2083 }
2084
2085 #[test]
2086 fn test_parse_sse_response_multiline_data() {
2087 let buffer =
2088 "data: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\"\ndata: :{\"key\":\"value\"}}\n\n";
2089 let result = parse_sse_response(buffer, 1, "test");
2090 assert!(result.is_some());
2091 let result = result.unwrap().unwrap();
2092 assert_eq!(result.get("key").and_then(|v| v.as_str()), Some("value"));
2093 }
2094
2095 #[test]
2096 fn test_parse_sse_response_empty_data_lines() {
2097 let buffer =
2100 "data: \ndata: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"ok\":true}}\ndata: \n\n";
2101 let result = parse_sse_response(buffer, 1, "test");
2102 assert!(result.is_some());
2103 let result = result.unwrap().unwrap();
2104 assert_eq!(result.get("ok").and_then(|v| v.as_bool()), Some(true));
2105 }
2106
2107 #[test]
2108 fn test_parse_sse_response_invalid_json() {
2109 let buffer = "data: not-json-at-all\n\n";
2112 let result = parse_sse_response(buffer, 1, "test");
2113 assert!(result.is_none());
2115 }
2116
2117 #[test]
2118 fn test_parse_sse_response_no_data_prefix() {
2119 let buffer = "some random line\nanother line\n\n";
2121 let result = parse_sse_response(buffer, 1, "test");
2122 assert!(result.is_none());
2123 }
2124
2125 #[tokio::test]
2126 async fn test_http_sse_config_url_takes_priority() {
2127 let dir = tempfile::TempDir::new().unwrap();
2130 let mcp_path = dir.path().join(".mcp.json");
2131 tokio::fs::write(
2132 &mcp_path,
2133 r#"{
2134 "mcpServers": {
2135 "hybrid": {
2136 "command": "some-binary",
2137 "args": ["--flag"],
2138 "url": "http://example.com/sse"
2139 }
2140 }
2141 }"#,
2142 )
2143 .await
2144 .unwrap();
2145
2146 let servers = discover_mcp_servers(dir.path()).await.unwrap();
2147 assert_eq!(servers.len(), 1);
2148 assert_eq!(servers[0].name, "hybrid");
2149 assert_eq!(servers[0].url.as_deref(), Some("http://example.com/sse"));
2151 assert_eq!(servers[0].command, "some-binary");
2153
2154 let server = &servers[0];
2156 assert!(
2157 server.url.is_some(),
2158 "url should be present, meaning HTTP+SSE transport is selected"
2159 );
2160 }
2161
2162 #[test]
2163 fn test_load_mcp_config_with_url() {
2164 let dir = tempfile::TempDir::new().unwrap();
2165 let path = dir.path().join("mcp.json");
2166 std::fs::write(
2167 &path,
2168 r#"{
2169 "servers": [
2170 {"name": "local", "command": "mcp-fs", "args": ["--root", "."]},
2171 {"name": "remote", "url": "http://localhost:3000/sse"}
2172 ]
2173 }"#,
2174 )
2175 .unwrap();
2176
2177 let servers = load_mcp_config(&path).unwrap();
2178 assert_eq!(servers.len(), 2);
2179 assert_eq!(servers[0].name, "local");
2180 assert_eq!(servers[0].command, "mcp-fs");
2181 assert!(servers[0].url.is_none());
2182 assert_eq!(servers[1].name, "remote");
2183 assert_eq!(servers[1].command, "");
2184 assert_eq!(servers[1].url.as_deref(), Some("http://localhost:3000/sse"));
2185 }
2186
2187 #[test]
2190 fn parse_sse_endpoint_data_before_event_header_returns_none() {
2191 let buffer = "data: http://localhost:3000/message\nevent: endpoint\n\n";
2194 let result = parse_sse_endpoint(buffer);
2200 assert!(
2203 result.is_none(),
2204 "data: before event: endpoint must not match; got {result:?}"
2205 );
2206 }
2207
2208 #[test]
2209 fn parse_sse_endpoint_non_endpoint_event_ignored() {
2210 let buffer = "event: message\ndata: http://localhost:3000/endpoint-data\n\n";
2213 assert_eq!(
2214 parse_sse_endpoint(buffer),
2215 None,
2216 "event: message must not expose data: as endpoint"
2217 );
2218 }
2219
2220 #[test]
2221 fn parse_sse_endpoint_event_then_empty_data_returns_none() {
2222 let buffer = "event: endpoint\ndata: \n\n";
2225 assert_eq!(
2226 parse_sse_endpoint(buffer),
2227 None,
2228 "empty data after endpoint event must return None"
2229 );
2230 }
2231
2232 #[test]
2235 fn parse_sse_response_error_code_minus_one_when_absent() {
2236 let buffer = "data: {\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{\"message\":\"oops\"}}\n\n";
2239 let result = parse_sse_response(buffer, 1, "srv");
2240 assert!(result.is_some(), "must parse error response");
2241 let err_msg = result.unwrap().unwrap_err().to_string();
2242 assert!(
2243 err_msg.contains("-1"),
2244 "missing code should default to -1; got: {err_msg}"
2245 );
2246 }
2247
2248 #[test]
2249 fn parse_sse_response_no_trailing_newline_still_parses() {
2250 let buffer = "data: {\"jsonrpc\":\"2.0\",\"id\":5,\"result\":{\"v\":42}}";
2253 let result = parse_sse_response(buffer, 5, "srv");
2254 assert!(result.is_some(), "no trailing newline must still parse");
2255 let val = result.unwrap().unwrap();
2256 assert_eq!(val.get("v").and_then(|v| v.as_i64()), Some(42));
2257 }
2258
2259 #[test]
2260 fn parse_sse_response_id_field_mismatch_returns_none() {
2261 let buffer = "data: {\"jsonrpc\":\"2.0\",\"id\":3,\"result\":{\"ok\":true}}\n\n";
2264 assert!(
2265 parse_sse_response(buffer, 99, "srv").is_none(),
2266 "wrong id must return None"
2267 );
2268 assert!(
2270 parse_sse_response(buffer, 3, "srv").is_some(),
2271 "correct id must return Some"
2272 );
2273 }
2274
2275 #[test]
2276 fn parse_sse_response_sse_field_prefix_lines_do_not_reset_data() {
2277 let buffer = "data: {\"jsonrpc\":\"2.0\",\"id\":7,\"result\"\n: comment ignored\ndata: :{\"v\":7}}\n\n";
2280 let result = parse_sse_response(buffer, 7, "srv");
2281 assert!(
2282 result.is_some(),
2283 "comment line must not reset data accumulator"
2284 );
2285 }
2286
2287 #[test]
2288 fn parse_sse_response_unknown_line_resets_data_accumulator() {
2289 let buffer =
2293 "data: {\"jsonrpc\":\"2.0\",\"id\":8\nunknown: stuff\ndata: ,\"result\":{}}\n\n";
2294 let result = parse_sse_response(buffer, 8, "srv");
2296 assert!(
2298 result.is_none(),
2299 "unknown SSE field must reset accumulator; incomplete JSON should yield None"
2300 );
2301 }
2302
2303 #[test]
2306 fn method_not_found_uses_negative_error_code() {
2307 let resp = JsonRpcResponse::method_not_found(Some(serde_json::json!(1)), "unknown_method");
2310 let err_obj = resp
2311 .error
2312 .expect("method_not_found must set the error field");
2313 assert_eq!(
2314 err_obj.code, -32601,
2315 "method_not_found must use the JSON-RPC -32601 error code (negative)"
2316 );
2317 assert!(
2318 err_obj.message.contains("unknown_method"),
2319 "error message must include the method name: {}",
2320 err_obj.message
2321 );
2322 }
2323
2324 #[test]
2325 fn internal_error_uses_negative_error_code() {
2326 let resp = JsonRpcResponse::internal_error(None, "something failed");
2328 let err_obj = resp.error.expect("internal_error must set the error field");
2329 assert_eq!(
2330 err_obj.code, -32603,
2331 "internal_error must use the JSON-RPC -32603 error code (negative)"
2332 );
2333 }
2334}