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}
62
63#[derive(Debug, Clone, Default, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub struct McpToolAnnotations {
68 #[serde(default)]
70 pub read_only_hint: bool,
71 #[serde(default)]
73 pub destructive_hint: bool,
74 #[serde(default)]
76 pub idempotent_hint: bool,
77 #[serde(default)]
79 pub open_world_hint: bool,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum McpServerTrust {
87 Trusted,
89 #[default]
92 Untrusted,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct McpToolSpec {
98 pub name: String,
99 #[serde(default)]
100 pub description: String,
101 #[serde(default)]
102 pub input_schema: Value,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub annotations: Option<McpToolAnnotations>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct McpResource {
111 pub uri: String,
112 pub name: String,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub description: Option<String>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 #[serde(rename = "mimeType")]
117 pub mime_type: Option<String>,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct McpResourceContent {
123 pub uri: String,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 #[serde(rename = "mimeType")]
126 pub mime_type: Option<String>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub text: Option<String>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub blob: Option<String>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct McpPrompt {
136 pub name: String,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub description: Option<String>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub arguments: Option<Vec<McpPromptArgument>>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct McpPromptArgument {
146 pub name: String,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub description: Option<String>,
149 #[serde(default)]
150 pub required: bool,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct McpPromptMessage {
156 pub role: String,
157 pub content: String,
158}
159
160#[allow(clippy::large_enum_variant)]
166enum McpTransport {
167 Stdio {
169 stdin: ChildStdin,
170 reader: BufReader<ChildStdout>,
171 child: Option<Child>,
172 },
173 HttpSse {
175 client: reqwest::Client,
176 sse_url: String,
178 post_url: Option<String>,
180 buffer: String,
182 },
183}
184
185impl fmt::Debug for McpTransport {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 match self {
188 Self::Stdio { .. } => f.debug_struct("Stdio").finish(),
189 Self::HttpSse {
190 sse_url, post_url, ..
191 } => f
192 .debug_struct("HttpSse")
193 .field("sse_url", sse_url)
194 .field("post_url", post_url)
195 .finish(),
196 }
197 }
198}
199
200pub struct McpClient {
202 transport: McpTransport,
203 next_id: u64,
204 capabilities: ServerCapabilities,
206 server_name: String,
208 read_timeout: Duration,
213}
214
215pub const DEFAULT_STDIO_READ_TIMEOUT: Duration = Duration::from_secs(10);
219
220#[derive(Debug, Clone, Default)]
222pub struct ServerCapabilities {
223 pub tools: bool,
224 pub resources: bool,
225 pub prompts: bool,
226}
227
228impl fmt::Debug for McpClient {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 f.debug_struct("McpClient")
231 .field("transport", &self.transport)
232 .field("next_id", &self.next_id)
233 .field("capabilities", &self.capabilities)
234 .finish()
235 }
236}
237
238impl Drop for McpClient {
239 fn drop(&mut self) {
240 if let McpTransport::Stdio { ref mut child, .. } = &mut self.transport {
241 if let Some(mut child) = child.take() {
242 let _ = child.start_kill();
243 }
244 }
245 }
246}
247
248impl McpClient {
249 pub async fn spawn(server: &McpServer) -> Result<Self> {
254 Self::spawn_with_timeout(server, DEFAULT_STDIO_READ_TIMEOUT).await
255 }
256
257 pub async fn spawn_with_timeout(server: &McpServer, read_timeout: Duration) -> Result<Self> {
261 if let Some(url) = &server.url {
262 Self::spawn_http_sse(server, url).await
265 } else {
266 Self::spawn_stdio(server, read_timeout).await
267 }
268 }
269
270 async fn spawn_stdio(server: &McpServer, read_timeout: Duration) -> Result<Self> {
272 let mut child = Command::new(&server.command)
273 .args(&server.args)
274 .stdin(std::process::Stdio::piped())
275 .stdout(std::process::Stdio::piped())
276 .stderr(std::process::Stdio::null())
277 .spawn()
278 .map_err(|e| Error::Mcp {
279 server: server.name.clone(),
280 message: format!("failed to spawn: {e}"),
281 })?;
282
283 let stdin = child.stdin.take().ok_or_else(|| Error::Mcp {
284 server: server.name.clone(),
285 message: "failed to open stdin".into(),
286 })?;
287 let stdout = child.stdout.take().ok_or_else(|| Error::Mcp {
288 server: server.name.clone(),
289 message: "failed to open stdout".into(),
290 })?;
291 let reader = BufReader::new(stdout);
292
293 let mut client = Self {
294 transport: McpTransport::Stdio {
295 stdin,
296 reader,
297 child: Some(child),
298 },
299 next_id: 1,
300 capabilities: ServerCapabilities::default(),
301 server_name: server.name.clone(),
302 read_timeout,
303 };
304
305 client.do_initialize(&server.name).await?;
306 Ok(client)
307 }
308
309 async fn spawn_http_sse(server: &McpServer, url: &str) -> Result<Self> {
311 let http_client = reqwest::Client::builder()
312 .timeout(Duration::from_secs(30))
313 .build()
314 .map_err(|e| Error::Mcp {
315 server: server.name.clone(),
316 message: format!("failed to build HTTP client: {e}"),
317 })?;
318
319 let response = http_client
322 .get(url)
323 .header("Accept", "text/event-stream")
324 .send()
325 .await
326 .map_err(|e| Error::Mcp {
327 server: server.name.clone(),
328 message: format!("failed to connect to SSE endpoint `{url}`: {e}"),
329 })?;
330
331 if !response.status().is_success() {
332 return Err(Error::Mcp {
333 server: server.name.clone(),
334 message: format!("SSE endpoint `{url}` returned HTTP {}", response.status()),
335 });
336 }
337
338 let mut stream = response.bytes_stream();
341 let mut sse_buffer = String::new();
342 let mut post_url: Option<String> = None;
343 let mut found_endpoint = false;
344
345 let mut total_read = 0usize;
347 let max_read = 65536;
348
349 while total_read < max_read {
350 match timeout(Duration::from_secs(10), stream.next()).await {
351 Ok(Some(Ok(chunk))) => {
352 let chunk_str = String::from_utf8_lossy(&chunk);
353 total_read += chunk_str.len();
354 sse_buffer.push_str(&chunk_str);
355
356 if let Some(endpoint) = parse_sse_endpoint(&sse_buffer) {
358 post_url = Some(endpoint);
359 found_endpoint = true;
360 break;
361 }
362 }
363 Ok(Some(Err(e))) => {
364 return Err(Error::Mcp {
365 server: server.name.clone(),
366 message: format!("error reading SSE stream from `{url}`: {e}"),
367 });
368 }
369 Ok(None) => break, Err(_) => {
371 return Err(Error::Mcp {
372 server: server.name.clone(),
373 message: format!("timeout reading SSE stream from `{url}`"),
374 });
375 }
376 }
377 }
378
379 if !found_endpoint {
380 return Err(Error::Mcp {
381 server: server.name.clone(),
382 message: format!(
383 "SSE endpoint `{url}` did not send an `endpoint` event. Received data: {}",
384 &sse_buffer[..sse_buffer.len().min(200)]
385 ),
386 });
387 }
388
389 let mut client = Self {
390 transport: McpTransport::HttpSse {
391 client: http_client,
392 sse_url: url.to_string(),
393 post_url,
394 buffer: sse_buffer,
395 },
396 next_id: 1,
397 capabilities: ServerCapabilities::default(),
398 server_name: server.name.clone(),
399 read_timeout: DEFAULT_STDIO_READ_TIMEOUT,
402 };
403
404 client.do_initialize(&server.name).await?;
405 Ok(client)
406 }
407
408 async fn do_initialize(&mut self, server_name: &str) -> Result<()> {
410 let init_result: Value = self
411 .send_request(
412 "initialize",
413 serde_json::json!({
414 "protocolVersion": "2024-11-05",
415 "capabilities": {},
416 "clientInfo": {
417 "name": "recursive-agent",
418 "version": "0.1.0"
419 }
420 }),
421 )
422 .await?;
423
424 if let Some(server_proto) = init_result.get("protocolVersion").and_then(|v| v.as_str()) {
426 if server_proto != "2024-11-05" {
427 tracing::warn!(
429 target: "recursive::mcp",
430 server = %server_name,
431 server_protocol = %server_proto,
432 "MCP server protocol version mismatch"
433 );
434 }
435 }
436
437 if let Some(caps) = init_result.get("capabilities") {
439 self.capabilities.tools = caps.get("tools").and_then(|v| v.as_bool()).unwrap_or(false);
440 self.capabilities.resources = caps
441 .get("resources")
442 .and_then(|v| v.as_bool())
443 .unwrap_or(false);
444 self.capabilities.prompts = caps
445 .get("prompts")
446 .and_then(|v| v.as_bool())
447 .unwrap_or(false);
448 }
449
450 self.send_notification("notifications/initialized", serde_json::json!({}))
452 .await?;
453
454 Ok(())
455 }
456
457 pub async fn list_tools(&mut self) -> Result<Vec<McpToolSpec>> {
459 let result: Value = self
460 .send_request("tools/list", serde_json::json!({}))
461 .await?;
462
463 let tools_arr = result
464 .get("tools")
465 .and_then(|v| v.as_array())
466 .ok_or_else(|| Error::Mcp {
467 server: self.server_name.clone(),
468 message: "`tools/list` response missing `tools` array".into(),
469 })?;
470
471 let mut specs = Vec::with_capacity(tools_arr.len());
472 for item in tools_arr {
473 let name = item
474 .get("name")
475 .and_then(|v| v.as_str())
476 .ok_or_else(|| Error::Mcp {
477 server: self.server_name.clone(),
478 message: "tool entry missing `name`".into(),
479 })?;
480 let description = item
481 .get("description")
482 .and_then(|v| v.as_str())
483 .unwrap_or("");
484 let input_schema = item
485 .get("inputSchema")
486 .cloned()
487 .unwrap_or(serde_json::json!({"type": "object"}));
488 let annotations = item
490 .get("annotations")
491 .and_then(|v| serde_json::from_value::<McpToolAnnotations>(v.clone()).ok());
492 specs.push(McpToolSpec {
493 name: name.to_string(),
494 description: description.to_string(),
495 input_schema,
496 annotations,
497 });
498 }
499
500 Ok(specs)
501 }
502
503 pub async fn call_tool(&mut self, name: &str, arguments: Value) -> Result<String> {
507 let result: Value = self
508 .send_request(
509 "tools/call",
510 serde_json::json!({
511 "name": name,
512 "arguments": arguments,
513 }),
514 )
515 .await?;
516
517 if result
519 .get("isError")
520 .and_then(|v| v.as_bool())
521 .unwrap_or(false)
522 {
523 let error_msg = result
524 .get("content")
525 .and_then(|v| v.as_array())
526 .and_then(|arr| arr.first())
527 .and_then(|c| c.get("text"))
528 .and_then(|v| v.as_str())
529 .unwrap_or("unknown error");
530 return Err(Error::Tool {
531 name: name.to_string(),
532 message: error_msg.to_string(),
533 });
534 }
535
536 let content = result
538 .get("content")
539 .and_then(|v| v.as_array())
540 .map(|arr| {
541 arr.iter()
542 .filter_map(|c| {
543 if c.get("type").and_then(|v| v.as_str()) == Some("text") {
544 c.get("text").and_then(|v| v.as_str())
545 } else {
546 None
547 }
548 })
549 .collect::<Vec<_>>()
550 .join("\n")
551 })
552 .unwrap_or_default();
553
554 Ok(content)
555 }
556
557 pub async fn list_resources(&mut self) -> Result<Vec<McpResource>> {
559 if !self.capabilities.resources {
560 return Err(Error::Mcp {
561 server: self.server_name.clone(),
562 message: "server does not advertise `resources` capability".into(),
563 });
564 }
565
566 let result: Value = self
567 .send_request("resources/list", serde_json::json!({}))
568 .await?;
569
570 let resources_arr = result
571 .get("resources")
572 .and_then(|v| v.as_array())
573 .ok_or_else(|| Error::Mcp {
574 server: self.server_name.clone(),
575 message: "`resources/list` response missing `resources` array".into(),
576 })?;
577
578 let mut resources = Vec::with_capacity(resources_arr.len());
579 for item in resources_arr {
580 let resource: McpResource =
581 serde_json::from_value(item.clone()).map_err(|e| Error::Mcp {
582 server: self.server_name.clone(),
583 message: format!("failed to parse resource: {e}"),
584 })?;
585 resources.push(resource);
586 }
587
588 Ok(resources)
589 }
590
591 pub async fn read_resource(&mut self, uri: &str) -> Result<Vec<McpResourceContent>> {
594 if !self.capabilities.resources {
595 return Err(Error::Mcp {
596 server: self.server_name.clone(),
597 message: "server does not advertise `resources` capability".into(),
598 });
599 }
600
601 let result: Value = self
602 .send_request("resources/read", serde_json::json!({ "uri": uri }))
603 .await?;
604
605 let contents_arr = result
606 .get("contents")
607 .and_then(|v| v.as_array())
608 .ok_or_else(|| Error::Mcp {
609 server: self.server_name.clone(),
610 message: "`resources/read` response missing `contents` array".into(),
611 })?;
612
613 let mut contents = Vec::with_capacity(contents_arr.len());
614 for item in contents_arr {
615 let content: McpResourceContent =
616 serde_json::from_value(item.clone()).map_err(|e| Error::Mcp {
617 server: self.server_name.clone(),
618 message: format!("failed to parse resource content: {e}"),
619 })?;
620 contents.push(content);
621 }
622
623 Ok(contents)
624 }
625
626 pub async fn list_prompts(&mut self) -> Result<Vec<McpPrompt>> {
628 if !self.capabilities.prompts {
629 return Err(Error::Mcp {
630 server: self.server_name.clone(),
631 message: "server does not advertise `prompts` capability".into(),
632 });
633 }
634
635 let result: Value = self
636 .send_request("prompts/list", serde_json::json!({}))
637 .await?;
638
639 let prompts_arr = result
640 .get("prompts")
641 .and_then(|v| v.as_array())
642 .ok_or_else(|| Error::Mcp {
643 server: self.server_name.clone(),
644 message: "`prompts/list` response missing `prompts` array".into(),
645 })?;
646
647 let mut prompts = Vec::with_capacity(prompts_arr.len());
648 for item in prompts_arr {
649 let prompt: McpPrompt =
650 serde_json::from_value(item.clone()).map_err(|e| Error::Mcp {
651 server: self.server_name.clone(),
652 message: format!("failed to parse prompt: {e}"),
653 })?;
654 prompts.push(prompt);
655 }
656
657 Ok(prompts)
658 }
659
660 pub async fn get_prompt(
663 &mut self,
664 name: &str,
665 arguments: Option<HashMap<String, String>>,
666 ) -> Result<Vec<McpPromptMessage>> {
667 if !self.capabilities.prompts {
668 return Err(Error::Mcp {
669 server: self.server_name.clone(),
670 message: "server does not advertise `prompts` capability".into(),
671 });
672 }
673
674 let mut params = serde_json::json!({ "name": name });
675 if let Some(args) = arguments {
676 params["arguments"] = serde_json::to_value(args).map_err(|e| Error::Mcp {
677 server: self.server_name.clone(),
678 message: format!("failed to serialize prompt arguments: {e}"),
679 })?;
680 }
681
682 let result: Value = self.send_request("prompts/get", params).await?;
683
684 let messages_arr = result
685 .get("messages")
686 .and_then(|v| v.as_array())
687 .ok_or_else(|| Error::Mcp {
688 server: self.server_name.clone(),
689 message: "`prompts/get` response missing `messages` array".into(),
690 })?;
691
692 let mut messages = Vec::with_capacity(messages_arr.len());
693 for item in messages_arr {
694 let role = item
695 .get("role")
696 .and_then(|v| v.as_str())
697 .ok_or_else(|| Error::Mcp {
698 server: self.server_name.clone(),
699 message: "prompt message missing `role`".into(),
700 })?;
701 let content = item
702 .get("content")
703 .and_then(|v| v.get("text"))
704 .and_then(|v| v.as_str())
705 .ok_or_else(|| Error::Mcp {
706 server: self.server_name.clone(),
707 message: "prompt message missing `content.text`".into(),
708 })?;
709 messages.push(McpPromptMessage {
710 role: role.to_string(),
711 content: content.to_string(),
712 });
713 }
714
715 Ok(messages)
716 }
717
718 async fn send_request(&mut self, method: &str, params: Value) -> Result<Value> {
724 let id = self.next_id;
725 self.next_id += 1;
726
727 let request = serde_json::json!({
728 "jsonrpc": "2.0",
729 "id": id,
730 "method": method,
731 "params": params,
732 });
733
734 self.write_line(&request).await?;
735 self.read_response(id).await
736 }
737
738 async fn send_notification(&mut self, method: &str, params: Value) -> Result<()> {
740 let notification = serde_json::json!({
741 "jsonrpc": "2.0",
742 "method": method,
743 "params": params,
744 });
745
746 self.write_line(¬ification).await
747 }
748
749 async fn write_line(&mut self, value: &Value) -> Result<()> {
751 match &mut self.transport {
752 McpTransport::Stdio { stdin, .. } => {
753 let line = serde_json::to_string(value)?;
754 let mut full = line.into_bytes();
755 full.push(b'\n');
756 stdin.write_all(&full).await.map_err(|e| Error::Mcp {
757 server: self.server_name.clone(),
758 message: format!("write error: {e}"),
759 })?;
760 stdin.flush().await.map_err(|e| Error::Mcp {
761 server: self.server_name.clone(),
762 message: format!("flush error: {e}"),
763 })?;
764 Ok(())
765 }
766 McpTransport::HttpSse {
767 client, post_url, ..
768 } => {
769 let url = post_url.as_ref().ok_or_else(|| Error::Mcp {
770 server: self.server_name.clone(),
771 message: "HTTP transport: no POST endpoint available".into(),
772 })?;
773 let body = serde_json::to_string(value)?;
774 let response = client
775 .post(url)
776 .header("Content-Type", "application/json")
777 .body(body)
778 .send()
779 .await
780 .map_err(|e| Error::Mcp {
781 server: self.server_name.clone(),
782 message: format!("HTTP POST error: {e}"),
783 })?;
784 if !response.status().is_success() {
785 return Err(Error::Mcp {
786 server: self.server_name.clone(),
787 message: format!(
788 "HTTP POST to `{url}` returned HTTP {}",
789 response.status()
790 ),
791 });
792 }
793 Ok(())
794 }
795 }
796 }
797
798 async fn read_response(&mut self, expected_id: u64) -> Result<Value> {
800 match &mut self.transport {
801 McpTransport::Stdio { reader, .. } => {
802 Self::read_stdio_response(reader, expected_id, &self.server_name, self.read_timeout)
803 .await
804 }
805 McpTransport::HttpSse {
806 client,
807 sse_url,
808 post_url,
809 buffer,
810 } => {
811 Self::read_sse_response(
812 client,
813 sse_url,
814 post_url,
815 buffer,
816 expected_id,
817 &self.server_name,
818 )
819 .await
820 }
821 }
822 }
823
824 async fn read_stdio_response(
826 reader: &mut BufReader<ChildStdout>,
827 expected_id: u64,
828 server_name: &str,
829 read_timeout: Duration,
830 ) -> Result<Value> {
831 let mut line_buf = String::new();
832
833 loop {
834 line_buf.clear();
835
836 let read_future = reader.read_line(&mut line_buf);
837 match timeout(read_timeout, read_future).await {
838 Ok(Ok(0)) => {
839 return Err(Error::Mcp {
840 server: server_name.to_string(),
841 message: "server closed stdout unexpectedly".into(),
842 });
843 }
844 Ok(Ok(_)) => {
845 let trimmed = line_buf.trim();
846 if trimmed.is_empty() {
847 continue;
848 }
849
850 let parsed: Value = serde_json::from_str(trimmed).map_err(|e| Error::Mcp {
851 server: server_name.to_string(),
852 message: format!("server returned non-JSON line: {e}; line: {trimmed}"),
853 })?;
854
855 if let Some(resp_id) = parsed.get("id") {
856 if resp_id.as_u64() == Some(expected_id) {
857 if let Some(err) = parsed.get("error") {
858 let msg = err
859 .get("message")
860 .and_then(|v| v.as_str())
861 .unwrap_or("unknown error");
862 let code = err.get("code").and_then(|v| v.as_i64()).unwrap_or(-1);
863 return Err(Error::Mcp {
864 server: server_name.to_string(),
865 message: format!("error (code {code}): {msg}"),
866 });
867 }
868 if let Some(result) = parsed.get("result") {
869 return Ok(result.clone());
870 }
871 return Err(Error::Mcp {
872 server: server_name.to_string(),
873 message: "response missing both `result` and `error`".into(),
874 });
875 }
876 }
877 }
878 Ok(Err(e)) => {
879 return Err(Error::Mcp {
880 server: server_name.to_string(),
881 message: format!("read error: {e}"),
882 });
883 }
884 Err(_) => {
885 return Err(Error::Mcp {
886 server: server_name.to_string(),
887 message: "server timed out (no response within 10s)".into(),
888 });
889 }
890 }
891 }
892 }
893
894 async fn read_sse_response(
896 client: &reqwest::Client,
897 sse_url: &str,
898 _post_url: &Option<String>,
899 buffer: &mut String,
900 expected_id: u64,
901 server_name: &str,
902 ) -> Result<Value> {
903 if !buffer.is_empty() {
905 if let Some(result) = parse_sse_response(buffer, expected_id, server_name) {
906 return result;
907 }
908 }
909
910 let response = client
912 .get(sse_url)
913 .header("Accept", "text/event-stream")
914 .send()
915 .await
916 .map_err(|e| Error::Mcp {
917 server: server_name.to_string(),
918 message: format!("failed to reconnect to SSE endpoint `{sse_url}`: {e}"),
919 })?;
920
921 if !response.status().is_success() {
922 return Err(Error::Mcp {
923 server: server_name.to_string(),
924 message: format!(
925 "SSE endpoint `{sse_url}` returned HTTP {}",
926 response.status()
927 ),
928 });
929 }
930
931 let mut stream = response.bytes_stream();
932
933 loop {
934 match timeout(Duration::from_secs(10), stream.next()).await {
935 Ok(Some(Ok(chunk))) => {
936 let chunk_str = String::from_utf8_lossy(&chunk);
937 buffer.push_str(&chunk_str);
938
939 if let Some(result) = parse_sse_response(buffer, expected_id, server_name) {
940 return result;
941 }
942 }
943 Ok(Some(Err(e))) => {
944 return Err(Error::Mcp {
945 server: server_name.to_string(),
946 message: format!("error reading SSE stream from `{sse_url}`: {e}"),
947 });
948 }
949 Ok(None) => {
950 return Err(Error::Mcp {
951 server: server_name.to_string(),
952 message: format!("SSE stream ended without response for id {expected_id}"),
953 });
954 }
955 Err(_) => {
956 return Err(Error::Mcp {
957 server: server_name.to_string(),
958 message: format!("SSE timed out waiting for response for id {expected_id}"),
959 });
960 }
961 }
962 }
963 }
964}
965
966fn parse_sse_endpoint(buffer: &str) -> Option<String> {
973 let mut current_event: Option<&str> = None;
974
975 for line in buffer.lines() {
976 let line = line.trim();
977 if line.starts_with("event:") {
978 current_event = Some(line.strip_prefix("event:").unwrap_or("").trim());
979 } else if line.starts_with("data:") && current_event == Some("endpoint") {
980 let data = line.strip_prefix("data:").unwrap_or("").trim();
981 if !data.is_empty() {
982 return Some(data.to_string());
983 }
984 }
985 if line.is_empty() {
987 current_event = None;
988 }
989 }
990
991 None
992}
993
994fn parse_sse_response(buffer: &str, expected_id: u64, server_name: &str) -> Option<Result<Value>> {
998 let mut current_data = String::new();
999
1000 for line in buffer.lines() {
1001 let trimmed = line.trim();
1002 if trimmed.starts_with("data:") {
1003 let data = trimmed.strip_prefix("data:").unwrap_or("").trim();
1004 current_data.push_str(data);
1005 } else if trimmed.is_empty() && !current_data.is_empty() {
1006 if let Ok(parsed) = serde_json::from_str::<Value>(¤t_data) {
1008 if let Some(resp_id) = parsed.get("id") {
1009 if resp_id.as_u64() == Some(expected_id) {
1010 if let Some(err) = parsed.get("error") {
1011 let msg = err
1012 .get("message")
1013 .and_then(|v| v.as_str())
1014 .unwrap_or("unknown error");
1015 let code = err.get("code").and_then(|v| v.as_i64()).unwrap_or(-1);
1016 return Some(Err(Error::Mcp {
1017 server: server_name.to_string(),
1018 message: format!("error (code {code}): {msg}"),
1019 }));
1020 }
1021 if let Some(result) = parsed.get("result") {
1022 return Some(Ok(result.clone()));
1023 }
1024 return Some(Err(Error::Mcp {
1025 server: server_name.to_string(),
1026 message: "response missing both `result` and `error`".into(),
1027 }));
1028 }
1029 }
1030 }
1031 current_data.clear();
1032 } else if !trimmed.starts_with("event:")
1033 && !trimmed.starts_with("data:")
1034 && !trimmed.is_empty()
1035 {
1036 if !trimmed.starts_with(':')
1039 && !trimmed.starts_with("id:")
1040 && !trimmed.starts_with("retry:")
1041 {
1042 current_data.clear();
1043 }
1044 }
1045 }
1046
1047 if !current_data.is_empty() {
1049 if let Ok(parsed) = serde_json::from_str::<Value>(¤t_data) {
1050 if let Some(resp_id) = parsed.get("id") {
1051 if resp_id.as_u64() == Some(expected_id) {
1052 if let Some(err) = parsed.get("error") {
1053 let msg = err
1054 .get("message")
1055 .and_then(|v| v.as_str())
1056 .unwrap_or("unknown error");
1057 let code = err.get("code").and_then(|v| v.as_i64()).unwrap_or(-1);
1058 return Some(Err(Error::Mcp {
1059 server: server_name.to_string(),
1060 message: format!("error (code {code}): {msg}"),
1061 }));
1062 }
1063 if let Some(result) = parsed.get("result") {
1064 return Some(Ok(result.clone()));
1065 }
1066 }
1067 }
1068 }
1069 }
1070
1071 None
1072}
1073
1074#[derive(Debug, Clone, Serialize, Deserialize)]
1080pub struct JsonRpcRequest {
1081 pub jsonrpc: String,
1082 #[serde(default, skip_serializing_if = "Option::is_none")]
1083 pub id: Option<serde_json::Value>,
1084 pub method: String,
1085 #[serde(default)]
1086 pub params: serde_json::Value,
1087}
1088
1089#[derive(Debug, Clone, Serialize, Deserialize)]
1091pub struct JsonRpcResponse {
1092 pub jsonrpc: String,
1093 #[serde(default, skip_serializing_if = "Option::is_none")]
1094 pub id: Option<serde_json::Value>,
1095 #[serde(default, skip_serializing_if = "Option::is_none")]
1096 pub result: Option<serde_json::Value>,
1097 #[serde(default, skip_serializing_if = "Option::is_none")]
1098 pub error: Option<JsonRpcError>,
1099}
1100
1101impl JsonRpcResponse {
1102 pub fn success(id: Option<serde_json::Value>, result: serde_json::Value) -> Self {
1104 Self {
1105 jsonrpc: "2.0".to_string(),
1106 id,
1107 result: Some(result),
1108 error: None,
1109 }
1110 }
1111
1112 pub fn error(id: Option<serde_json::Value>, code: i64, message: impl Into<String>) -> Self {
1114 Self {
1115 jsonrpc: "2.0".to_string(),
1116 id,
1117 result: None,
1118 error: Some(JsonRpcError {
1119 code,
1120 message: message.into(),
1121 data: None,
1122 }),
1123 }
1124 }
1125
1126 pub fn method_not_found(id: Option<serde_json::Value>, method: &str) -> Self {
1128 Self::error(id, -32601, format!("Method not found: {method}"))
1129 }
1130
1131 pub fn internal_error(id: Option<serde_json::Value>, message: impl Into<String>) -> Self {
1133 Self::error(id, -32603, message)
1134 }
1135}
1136
1137#[derive(Debug, Clone, Serialize, Deserialize)]
1139pub struct JsonRpcError {
1140 pub code: i64,
1141 pub message: String,
1142 #[serde(default, skip_serializing_if = "Option::is_none")]
1143 pub data: Option<serde_json::Value>,
1144}
1145
1146pub async fn dispatch_request(
1156 request: &JsonRpcRequest,
1157 client: &mut McpClient,
1158) -> Option<JsonRpcResponse> {
1159 let id = request.id.clone();
1160
1161 match request.method.as_str() {
1162 "initialize" => {
1163 let result = serde_json::json!({
1166 "protocolVersion": "2024-11-05",
1167 "capabilities": {
1168 "tools": true,
1169 "resources": true,
1170 "prompts": true
1171 },
1172 "serverInfo": {
1173 "name": "recursive-agent",
1174 "version": "0.1.0"
1175 }
1176 });
1177 Some(JsonRpcResponse::success(id, result))
1178 }
1179 "notifications/initialized" => {
1180 None
1182 }
1183 "tools/list" => match client.list_tools().await {
1184 Ok(tools) => {
1185 let tools_arr: Vec<serde_json::Value> = tools
1186 .into_iter()
1187 .map(|t| {
1188 serde_json::json!({
1189 "name": t.name,
1190 "description": t.description,
1191 "inputSchema": t.input_schema,
1192 })
1193 })
1194 .collect();
1195 Some(JsonRpcResponse::success(
1196 id,
1197 serde_json::json!({ "tools": tools_arr }),
1198 ))
1199 }
1200 Err(e) => Some(JsonRpcResponse::internal_error(id, e.to_string())),
1201 },
1202 "tools/call" => {
1203 let name = request
1204 .params
1205 .get("name")
1206 .and_then(|v| v.as_str())
1207 .unwrap_or("");
1208 let arguments = request.params.get("arguments").cloned().unwrap_or_default();
1209
1210 match client.call_tool(name, arguments).await {
1211 Ok(text) => {
1212 let result = serde_json::json!({
1213 "content": [{"type": "text", "text": text}]
1214 });
1215 Some(JsonRpcResponse::success(id, result))
1216 }
1217 Err(e) => {
1218 let result = serde_json::json!({
1221 "isError": true,
1222 "content": [{"type": "text", "text": e.to_string()}]
1223 });
1224 Some(JsonRpcResponse::success(id, result))
1225 }
1226 }
1227 }
1228 "resources/list" => match client.list_resources().await {
1229 Ok(resources) => {
1230 let resources_arr: Vec<serde_json::Value> = resources
1231 .into_iter()
1232 .map(|r| {
1233 serde_json::json!({
1234 "uri": r.uri,
1235 "name": r.name,
1236 "description": r.description,
1237 "mimeType": r.mime_type,
1238 })
1239 })
1240 .collect();
1241 Some(JsonRpcResponse::success(
1242 id,
1243 serde_json::json!({ "resources": resources_arr }),
1244 ))
1245 }
1246 Err(e) => Some(JsonRpcResponse::internal_error(id, e.to_string())),
1247 },
1248 "resources/read" => {
1249 let uri = request
1250 .params
1251 .get("uri")
1252 .and_then(|v| v.as_str())
1253 .unwrap_or("");
1254
1255 match client.read_resource(uri).await {
1256 Ok(contents) => {
1257 let contents_arr: Vec<serde_json::Value> = contents
1258 .into_iter()
1259 .map(|c| {
1260 serde_json::json!({
1261 "uri": c.uri,
1262 "mimeType": c.mime_type,
1263 "text": c.text,
1264 "blob": c.blob,
1265 })
1266 })
1267 .collect();
1268 Some(JsonRpcResponse::success(
1269 id,
1270 serde_json::json!({ "contents": contents_arr }),
1271 ))
1272 }
1273 Err(e) => Some(JsonRpcResponse::internal_error(id, e.to_string())),
1274 }
1275 }
1276 "prompts/list" => match client.list_prompts().await {
1277 Ok(prompts) => {
1278 let prompts_arr: Vec<serde_json::Value> = prompts
1279 .into_iter()
1280 .map(|p| {
1281 serde_json::json!({
1282 "name": p.name,
1283 "description": p.description,
1284 "arguments": p.arguments,
1285 })
1286 })
1287 .collect();
1288 Some(JsonRpcResponse::success(
1289 id,
1290 serde_json::json!({ "prompts": prompts_arr }),
1291 ))
1292 }
1293 Err(e) => Some(JsonRpcResponse::internal_error(id, e.to_string())),
1294 },
1295 "prompts/get" => {
1296 let name = request
1297 .params
1298 .get("name")
1299 .and_then(|v| v.as_str())
1300 .unwrap_or("");
1301 let arguments = request
1302 .params
1303 .get("arguments")
1304 .and_then(|v| serde_json::from_value::<HashMap<String, String>>(v.clone()).ok());
1305
1306 match client.get_prompt(name, arguments).await {
1307 Ok(messages) => {
1308 let messages_arr: Vec<serde_json::Value> = messages
1309 .into_iter()
1310 .map(|m| {
1311 serde_json::json!({
1312 "role": m.role,
1313 "content": {"type": "text", "text": m.content},
1314 })
1315 })
1316 .collect();
1317 Some(JsonRpcResponse::success(
1318 id,
1319 serde_json::json!({ "messages": messages_arr }),
1320 ))
1321 }
1322 Err(e) => Some(JsonRpcResponse::internal_error(id, e.to_string())),
1323 }
1324 }
1325 _ => Some(JsonRpcResponse::method_not_found(id, &request.method)),
1326 }
1327}
1328
1329pub struct McpTool {
1336 client: Arc<Mutex<McpClient>>,
1337 spec: McpToolSpec,
1338 server_name: String,
1339 trust: McpServerTrust,
1342}
1343
1344impl McpTool {
1345 pub fn new(
1346 client: Arc<Mutex<McpClient>>,
1347 spec: McpToolSpec,
1348 server_name: impl Into<String>,
1349 ) -> Self {
1350 Self {
1351 client,
1352 spec,
1353 server_name: server_name.into(),
1354 trust: McpServerTrust::Untrusted,
1355 }
1356 }
1357
1358 pub fn with_trust(mut self, trust: McpServerTrust) -> Self {
1360 self.trust = trust;
1361 self
1362 }
1363}
1364
1365#[async_trait]
1366impl Tool for McpTool {
1367 fn spec(&self) -> ToolSpec {
1368 ToolSpec {
1369 name: format!("mcp__{}__{}", self.server_name, self.spec.name),
1370 description: format!("[mcp:{}] {}", self.server_name, self.spec.description),
1371 parameters: self.spec.input_schema.clone(),
1372 }
1373 }
1374
1375 fn side_effect_class(&self) -> crate::tools::ToolSideEffect {
1376 use crate::tools::ToolSideEffect;
1377 let Some(ann) = &self.spec.annotations else {
1379 return ToolSideEffect::External;
1380 };
1381 if self.trust != McpServerTrust::Trusted {
1382 return ToolSideEffect::External;
1383 }
1384 if ann.read_only_hint {
1385 ToolSideEffect::ReadOnly
1386 } else if ann.open_world_hint {
1387 ToolSideEffect::External
1388 } else {
1389 ToolSideEffect::Mutating
1390 }
1391 }
1392
1393 async fn execute(&self, arguments: Value) -> Result<String> {
1394 let mut client = self.client.lock().await;
1395 client.call_tool(&self.spec.name, arguments).await
1396 }
1397}
1398
1399pub fn load_mcp_config(path: &std::path::Path) -> Result<Vec<McpServer>> {
1409 let contents = std::fs::read_to_string(path).map_err(|e| Error::Mcp {
1410 server: "config".into(),
1411 message: format!("failed to read config `{}`: {e}", path.display()),
1412 })?;
1413 let parsed: McpConfigFile = serde_json::from_str(&contents).map_err(|e| Error::Mcp {
1414 server: "config".into(),
1415 message: format!("failed to parse config `{}`: {e}", path.display()),
1416 })?;
1417 Ok(parsed.servers)
1418}
1419
1420#[derive(Debug, Deserialize)]
1421struct McpConfigFile {
1422 servers: Vec<McpServer>,
1423}
1424
1425#[derive(Debug, Deserialize)]
1431#[serde(deny_unknown_fields)]
1432struct McpDiscoveryFile {
1433 #[serde(rename = "mcpServers")]
1434 mcp_servers: HashMap<String, McpServerConfig>,
1435}
1436
1437pub async fn discover_mcp_servers(workspace: &Path) -> Result<Vec<McpServer>> {
1445 let primary = workspace.join(".mcp.json");
1447 if primary.exists() {
1448 let configs = load_mcp_discovery_config(&primary).await?;
1449 if !configs.is_empty() {
1450 return Ok(configs);
1451 }
1452 }
1453
1454 let fallback = workspace.join(".recursive").join("mcp.json");
1456 if fallback.exists() {
1457 let configs = load_mcp_discovery_config(&fallback).await?;
1458 return Ok(configs);
1459 }
1460
1461 Ok(Vec::new())
1462}
1463
1464async fn load_mcp_discovery_config(path: &Path) -> Result<Vec<McpServer>> {
1489 let contents = tokio::fs::read_to_string(path)
1490 .await
1491 .map_err(|e| Error::Mcp {
1492 server: "discovery".into(),
1493 message: format!("failed to read discovery config `{}`: {e}", path.display()),
1494 })?;
1495
1496 if contents.trim().is_empty() {
1498 return Ok(Vec::new());
1499 }
1500
1501 let parsed: McpDiscoveryFile = serde_json::from_str(&contents).map_err(|e| Error::Mcp {
1502 server: "discovery".into(),
1503 message: format!("failed to parse discovery config `{}`: {e}", path.display()),
1504 })?;
1505
1506 let servers: Vec<McpServer> = parsed
1507 .mcp_servers
1508 .into_iter()
1509 .map(|(name, config)| McpServer {
1510 name,
1511 command: config.command,
1512 args: config.args,
1513 url: config.url,
1514 })
1515 .collect();
1516
1517 Ok(servers)
1518}
1519
1520#[cfg(test)]
1525mod tests {
1526 use super::*;
1527
1528 #[test]
1535 fn load_mcp_config_parses_correctly() {
1536 let dir = tempfile::TempDir::new().unwrap();
1537 let path = dir.path().join("mcp.json");
1538 std::fs::write(
1539 &path,
1540 r#"{
1541 "servers": [
1542 {"name": "fs", "command": "mcp-fs", "args": ["--root", "."]},
1543 {"name": "github", "command": "mcp-gh", "args": []}
1544 ]
1545 }"#,
1546 )
1547 .unwrap();
1548
1549 let servers = load_mcp_config(&path).unwrap();
1550 assert_eq!(servers.len(), 2);
1551 assert_eq!(servers[0].name, "fs");
1552 assert_eq!(servers[0].command, "mcp-fs");
1553 assert_eq!(servers[0].args, vec!["--root", "."]);
1554 assert_eq!(servers[1].name, "github");
1555 assert_eq!(servers[1].command, "mcp-gh");
1556 assert!(servers[1].args.is_empty());
1557 }
1558
1559 #[test]
1560 fn load_mcp_config_empty_servers() {
1561 let dir = tempfile::TempDir::new().unwrap();
1562 let path = dir.path().join("empty.json");
1563 std::fs::write(&path, r#"{"servers": []}"#).unwrap();
1564
1565 let servers = load_mcp_config(&path).unwrap();
1566 assert!(servers.is_empty());
1567 }
1568
1569 #[test]
1570 fn load_mcp_config_missing_file_errors() {
1571 let dir = tempfile::TempDir::new().unwrap();
1572 let path = dir.path().join("nonexistent.json");
1573 let err = load_mcp_config(&path).unwrap_err();
1574 assert!(err.to_string().contains("failed to read config"));
1575 }
1576
1577 #[tokio::test]
1582 async fn discover_finds_dot_mcp_json_in_workspace_root() {
1583 let dir = tempfile::TempDir::new().unwrap();
1584 let mcp_path = dir.path().join(".mcp.json");
1585 tokio::fs::write(
1586 &mcp_path,
1587 r#"{
1588 "mcpServers": {
1589 "fs": {
1590 "command": "mcp-fs",
1591 "args": ["--root", "."]
1592 }
1593 }
1594 }"#,
1595 )
1596 .await
1597 .unwrap();
1598
1599 let servers = discover_mcp_servers(dir.path()).await.unwrap();
1600 assert_eq!(servers.len(), 1);
1601 assert_eq!(servers[0].name, "fs");
1602 assert_eq!(servers[0].command, "mcp-fs");
1603 assert_eq!(servers[0].args, vec!["--root", "."]);
1604 }
1605
1606 #[tokio::test]
1607 async fn discover_returns_empty_vec_when_no_config_file() {
1608 let dir = tempfile::TempDir::new().unwrap();
1609 let servers = discover_mcp_servers(dir.path()).await.unwrap();
1610 assert!(servers.is_empty());
1611 }
1612
1613 #[tokio::test]
1614 async fn discover_parses_claude_code_format_with_env() {
1615 let dir = tempfile::TempDir::new().unwrap();
1616 let mcp_path = dir.path().join(".mcp.json");
1617 tokio::fs::write(
1618 &mcp_path,
1619 r#"{
1620 "mcpServers": {
1621 "github": {
1622 "command": "mcp-gh",
1623 "args": [],
1624 "env": {
1625 "GITHUB_TOKEN": "abc123"
1626 }
1627 },
1628 "filesystem": {
1629 "command": "mcp-fs",
1630 "args": ["--root", "/tmp"]
1631 }
1632 }
1633 }"#,
1634 )
1635 .await
1636 .unwrap();
1637
1638 let servers = discover_mcp_servers(dir.path()).await.unwrap();
1639 assert_eq!(servers.len(), 2);
1640
1641 let gh = servers.iter().find(|s| s.name == "github").unwrap();
1642 assert_eq!(gh.command, "mcp-gh");
1643 assert!(gh.args.is_empty());
1644
1645 let fs = servers.iter().find(|s| s.name == "filesystem").unwrap();
1646 assert_eq!(fs.command, "mcp-fs");
1647 assert_eq!(fs.args, vec!["--root", "/tmp"]);
1648 }
1649
1650 #[tokio::test]
1651 async fn discover_finds_dot_recursive_mcp_json_as_fallback() {
1652 let dir = tempfile::TempDir::new().unwrap();
1653 let recursive_dir = dir.path().join(".recursive");
1654 tokio::fs::create_dir(&recursive_dir).await.unwrap();
1655 let mcp_path = recursive_dir.join("mcp.json");
1656 tokio::fs::write(
1657 &mcp_path,
1658 r#"{
1659 "mcpServers": {
1660 "db": {
1661 "command": "mcp-db",
1662 "args": ["--port", "5432"]
1663 }
1664 }
1665 }"#,
1666 )
1667 .await
1668 .unwrap();
1669
1670 let servers = discover_mcp_servers(dir.path()).await.unwrap();
1671 assert_eq!(servers.len(), 1);
1672 assert_eq!(servers[0].name, "db");
1673 assert_eq!(servers[0].command, "mcp-db");
1674 }
1675
1676 #[tokio::test]
1677 async fn discover_dot_mcp_json_takes_priority_over_dot_recursive() {
1678 let dir = tempfile::TempDir::new().unwrap();
1679
1680 tokio::fs::write(
1682 dir.path().join(".mcp.json"),
1683 r#"{
1684 "mcpServers": {
1685 "primary": {
1686 "command": "primary-server",
1687 "args": []
1688 }
1689 }
1690 }"#,
1691 )
1692 .await
1693 .unwrap();
1694
1695 let recursive_dir = dir.path().join(".recursive");
1697 tokio::fs::create_dir(&recursive_dir).await.unwrap();
1698 tokio::fs::write(
1699 recursive_dir.join("mcp.json"),
1700 r#"{
1701 "mcpServers": {
1702 "fallback": {
1703 "command": "fallback-server",
1704 "args": []
1705 }
1706 }
1707 }"#,
1708 )
1709 .await
1710 .unwrap();
1711
1712 let servers = discover_mcp_servers(dir.path()).await.unwrap();
1713 assert_eq!(servers.len(), 1);
1714 assert_eq!(servers[0].name, "primary");
1715 }
1716
1717 #[tokio::test]
1718 async fn discover_malformed_json_returns_descriptive_error() {
1719 let dir = tempfile::TempDir::new().unwrap();
1720 tokio::fs::write(dir.path().join(".mcp.json"), "not valid json")
1721 .await
1722 .unwrap();
1723
1724 let err = discover_mcp_servers(dir.path()).await.unwrap_err();
1725 let msg = err.to_string();
1726 assert!(
1727 msg.contains("failed to parse discovery config"),
1728 "error should mention parsing failure: {msg}"
1729 );
1730 }
1731
1732 #[tokio::test]
1733 async fn discover_empty_file_returns_empty_vec() {
1734 let dir = tempfile::TempDir::new().unwrap();
1735 tokio::fs::write(dir.path().join(".mcp.json"), "")
1736 .await
1737 .unwrap();
1738
1739 let servers = discover_mcp_servers(dir.path()).await.unwrap();
1740 assert!(servers.is_empty());
1741 }
1742
1743 #[tokio::test]
1744 async fn discover_empty_mcp_servers_object_returns_empty_vec() {
1745 let dir = tempfile::TempDir::new().unwrap();
1746 tokio::fs::write(dir.path().join(".mcp.json"), r#"{"mcpServers": {}}"#)
1747 .await
1748 .unwrap();
1749
1750 let servers = discover_mcp_servers(dir.path()).await.unwrap();
1751 assert!(servers.is_empty());
1752 }
1753
1754 #[tokio::test]
1759 async fn test_http_sse_discovery_with_url() {
1760 let dir = tempfile::TempDir::new().unwrap();
1761 let mcp_path = dir.path().join(".mcp.json");
1762 tokio::fs::write(
1763 &mcp_path,
1764 r#"{
1765 "mcpServers": {
1766 "remote": {
1767 "url": "http://example.com/sse"
1768 }
1769 }
1770 }"#,
1771 )
1772 .await
1773 .unwrap();
1774
1775 let servers = discover_mcp_servers(dir.path()).await.unwrap();
1776 assert_eq!(servers.len(), 1);
1777 assert_eq!(servers[0].name, "remote");
1778 assert_eq!(servers[0].command, "");
1779 assert!(servers[0].args.is_empty());
1780 assert_eq!(servers[0].url.as_deref(), Some("http://example.com/sse"));
1781 }
1782
1783 #[test]
1784 fn test_parse_sse_endpoint() {
1785 let buffer = "event: endpoint\ndata: http://localhost:3000/message\n\n";
1786 assert_eq!(
1787 parse_sse_endpoint(buffer),
1788 Some("http://localhost:3000/message".to_string())
1789 );
1790
1791 let buffer = "event: message\ndata: {\"key\": \"value\"}\n\n";
1793 assert_eq!(parse_sse_endpoint(buffer), None);
1794
1795 let buffer = "event: endpoint\ndata: \n\n";
1797 assert_eq!(parse_sse_endpoint(buffer), None);
1798 }
1799
1800 #[test]
1801 fn test_parse_sse_response() {
1802 let buffer = "data: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"key\":\"value\"}}\n\n";
1803 let result = parse_sse_response(buffer, 1, "test");
1804 assert!(result.is_some());
1805 let result = result.unwrap().unwrap();
1806 assert_eq!(result.get("key").and_then(|v| v.as_str()), Some("value"));
1807
1808 let result = parse_sse_response(buffer, 2, "test");
1810 assert!(result.is_none());
1811
1812 let buffer =
1814 "data: {\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{\"code\":-32601,\"message\":\"Method not found\"}}\n\n";
1815 let result = parse_sse_response(buffer, 1, "test");
1816 assert!(result.is_some());
1817 assert!(result.unwrap().is_err());
1818 }
1819
1820 #[test]
1821 fn test_parse_sse_response_multiline_data() {
1822 let buffer =
1823 "data: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\"\ndata: :{\"key\":\"value\"}}\n\n";
1824 let result = parse_sse_response(buffer, 1, "test");
1825 assert!(result.is_some());
1826 let result = result.unwrap().unwrap();
1827 assert_eq!(result.get("key").and_then(|v| v.as_str()), Some("value"));
1828 }
1829
1830 #[test]
1831 fn test_parse_sse_response_empty_data_lines() {
1832 let buffer =
1835 "data: \ndata: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"ok\":true}}\ndata: \n\n";
1836 let result = parse_sse_response(buffer, 1, "test");
1837 assert!(result.is_some());
1838 let result = result.unwrap().unwrap();
1839 assert_eq!(result.get("ok").and_then(|v| v.as_bool()), Some(true));
1840 }
1841
1842 #[test]
1843 fn test_parse_sse_response_invalid_json() {
1844 let buffer = "data: not-json-at-all\n\n";
1847 let result = parse_sse_response(buffer, 1, "test");
1848 assert!(result.is_none());
1850 }
1851
1852 #[test]
1853 fn test_parse_sse_response_no_data_prefix() {
1854 let buffer = "some random line\nanother line\n\n";
1856 let result = parse_sse_response(buffer, 1, "test");
1857 assert!(result.is_none());
1858 }
1859
1860 #[tokio::test]
1861 async fn test_http_sse_config_url_takes_priority() {
1862 let dir = tempfile::TempDir::new().unwrap();
1865 let mcp_path = dir.path().join(".mcp.json");
1866 tokio::fs::write(
1867 &mcp_path,
1868 r#"{
1869 "mcpServers": {
1870 "hybrid": {
1871 "command": "some-binary",
1872 "args": ["--flag"],
1873 "url": "http://example.com/sse"
1874 }
1875 }
1876 }"#,
1877 )
1878 .await
1879 .unwrap();
1880
1881 let servers = discover_mcp_servers(dir.path()).await.unwrap();
1882 assert_eq!(servers.len(), 1);
1883 assert_eq!(servers[0].name, "hybrid");
1884 assert_eq!(servers[0].url.as_deref(), Some("http://example.com/sse"));
1886 assert_eq!(servers[0].command, "some-binary");
1888
1889 let server = &servers[0];
1891 assert!(
1892 server.url.is_some(),
1893 "url should be present, meaning HTTP+SSE transport is selected"
1894 );
1895 }
1896
1897 #[test]
1898 fn test_load_mcp_config_with_url() {
1899 let dir = tempfile::TempDir::new().unwrap();
1900 let path = dir.path().join("mcp.json");
1901 std::fs::write(
1902 &path,
1903 r#"{
1904 "servers": [
1905 {"name": "local", "command": "mcp-fs", "args": ["--root", "."]},
1906 {"name": "remote", "url": "http://localhost:3000/sse"}
1907 ]
1908 }"#,
1909 )
1910 .unwrap();
1911
1912 let servers = load_mcp_config(&path).unwrap();
1913 assert_eq!(servers.len(), 2);
1914 assert_eq!(servers[0].name, "local");
1915 assert_eq!(servers[0].command, "mcp-fs");
1916 assert!(servers[0].url.is_none());
1917 assert_eq!(servers[1].name, "remote");
1918 assert_eq!(servers[1].command, "");
1919 assert_eq!(servers[1].url.as_deref(), Some("http://localhost:3000/sse"));
1920 }
1921}