1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeMap;
4use std::io::{BufRead, BufReader, Read, Write};
5use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
6
7#[derive(Debug, Clone)]
8pub struct McpToolSpec {
9 pub name: String,
10 pub description: Option<String>,
11 pub input_schema: Value,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15#[serde(rename_all = "camelCase")]
16pub struct McpResource {
17 pub uri: String,
18 pub name: String,
19 pub mime_type: Option<String>,
20 pub description: Option<String>,
21 pub server: String,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25#[serde(rename_all = "camelCase")]
26pub struct McpResourceContent {
27 pub uri: String,
28 pub mime_type: Option<String>,
29 pub text: Option<String>,
30 pub blob: Option<String>,
31}
32
33const CLAUDEAI_SERVER_PREFIX: &str = "claude.ai ";
34
35pub fn normalize_name_for_mcp(name: &str) -> String {
36 let mut normalized = String::with_capacity(name.len());
37 for c in name.chars() {
38 if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
39 normalized.push(c);
40 } else {
41 normalized.push('_');
42 }
43 }
44 if name.starts_with(CLAUDEAI_SERVER_PREFIX) {
45 let mut collapsed = String::new();
46 let mut last_was_underscore = false;
47 for c in normalized.chars() {
48 if c == '_' {
49 if !last_was_underscore {
50 collapsed.push(c);
51 last_was_underscore = true;
52 }
53 } else {
54 collapsed.push(c);
55 last_was_underscore = false;
56 }
57 }
58 let trimmed = collapsed.trim_matches('_');
59 if trimmed.is_empty() {
60 normalized
61 } else {
62 trimmed.to_string()
63 }
64 } else {
65 normalized
66 }
67}
68
69pub fn make_mcp_tool_name(server_name: &str, tool_name: &str) -> String {
70 let prefix = format!("mcp__{}__", normalize_name_for_mcp(server_name));
71 format!("{}{}", prefix, normalize_name_for_mcp(tool_name))
72}
73
74struct SyncIoBridge {
75 child: Child,
76 stdin: ChildStdin,
77 stdout: BufReader<ChildStdout>,
78}
79
80impl SyncIoBridge {
81 fn new(command: &str, args: &[String], env: &BTreeMap<String, String>) -> Result<Self, String> {
82 let mut cmd = Command::new(command);
83 cmd.args(args);
84 for (k, v) in env {
85 cmd.env(k, v);
86 }
87 cmd.stdin(Stdio::piped());
88 cmd.stdout(Stdio::piped());
89 cmd.stderr(Stdio::piped());
90
91 let mut child = cmd
92 .spawn()
93 .map_err(|e| format!("failed to spawn {}: {e}", command))?;
94 let stdin = child.stdin.take().ok_or("missing child stdin")?;
95 let stdout = child.stdout.take().ok_or("missing child stdout")?;
96 Ok(Self {
97 child,
98 stdin,
99 stdout: BufReader::new(stdout),
100 })
101 }
102
103 fn stdin(&mut self) -> &mut ChildStdin {
104 &mut self.stdin
105 }
106
107 fn stdout(&mut self) -> &mut BufReader<ChildStdout> {
108 &mut self.stdout
109 }
110
111 fn kill(&mut self) {
112 let _ = self.child.kill();
113 }
114}
115
116pub struct McpStdioClient {
117 server_name: String,
118 io: SyncIoBridge,
119 initialized: bool,
120 next_id: u64,
121}
122
123impl McpStdioClient {
124 pub fn new(
125 server_name: String,
126 command: &str,
127 args: &[String],
128 env: &BTreeMap<String, String>,
129 ) -> Result<Self, String> {
130 let io = SyncIoBridge::new(command, args, env)?;
131 let mut client = Self {
132 server_name,
133 io,
134 initialized: false,
135 next_id: 1,
136 };
137 client.initialize()?;
138 Ok(client)
139 }
140
141 fn next_request_id(&mut self) -> u64 {
142 let id = self.next_id;
143 self.next_id += 1;
144 id
145 }
146
147 fn initialize(&mut self) -> Result<(), String> {
148 let request = serde_json::json!({
149 "jsonrpc": "2.0",
150 "id": self.next_request_id(),
151 "method": "initialize",
152 "params": {
153 "protocolVersion": "2024-11-05",
154 "capabilities": {},
155 "clientInfo": {
156 "name": "clawedcode",
157 "version": "0.0.3"
158 }
159 }
160 });
161
162 self.send_json(&request)?;
163 let _response = self.read_json()?;
164
165 let notif = serde_json::json!({
166 "jsonrpc": "2.0",
167 "method": "notifications/initialized",
168 "params": {}
169 });
170 self.send_json(¬if)?;
171
172 self.initialized = true;
173 Ok(())
174 }
175
176 fn request(&mut self, method: &str, params: Value) -> Result<Value, String> {
177 let request = serde_json::json!({
178 "jsonrpc": "2.0",
179 "id": self.next_request_id(),
180 "method": method,
181 "params": params,
182 });
183
184 self.send_json(&request)?;
185 let response = self.read_json()?;
186
187 if let Some(error) = response.get("error") {
188 let message = error
189 .get("message")
190 .and_then(|value| value.as_str())
191 .unwrap_or("unknown MCP error");
192 return Err(message.to_string());
193 }
194
195 Ok(response.get("result").cloned().unwrap_or(Value::Null))
196 }
197
198 fn send_json(&mut self, value: &serde_json::Value) -> Result<(), String> {
199 let bytes = serde_json::to_vec(value).map_err(|e| format!("serialize error: {e}"))?;
200 let stdin = self.io.stdin();
201 write!(stdin, "Content-Length: {}\r\n\r\n", bytes.len())
202 .map_err(|e| format!("write header error: {e}"))?;
203 stdin
204 .write_all(&bytes)
205 .map_err(|e| format!("write error: {e}"))?;
206 stdin.flush().map_err(|e| format!("flush error: {e}"))?;
207 Ok(())
208 }
209
210 fn read_json(&mut self) -> Result<Value, String> {
211 let stdout = self.io.stdout();
212 let mut content_length = None;
213 let mut line = String::new();
214
215 loop {
216 line.clear();
217 stdout
218 .read_line(&mut line)
219 .map_err(|e| format!("read header error: {e}"))?;
220
221 if line.is_empty() {
222 return Err("unexpected EOF while reading MCP headers".into());
223 }
224
225 if line == "\r\n" || line == "\n" {
226 break;
227 }
228
229 let trimmed = line.trim();
230 if let Some(value) = trimmed.strip_prefix("Content-Length:") {
231 content_length = Some(
232 value
233 .trim()
234 .parse()
235 .map_err(|e| format!("parse Content-Length error: {e}"))?,
236 );
237 }
238 }
239
240 let content_length = content_length.ok_or("missing Content-Length header")?;
241
242 let mut body = vec![0u8; content_length];
243 stdout
244 .read_exact(&mut body)
245 .map_err(|e| format!("read body error: {e}"))?;
246
247 serde_json::from_slice(&body).map_err(|e| format!("parse error: {e}"))
248 }
249
250 pub fn list_tools(&mut self) -> Result<Vec<McpToolSpec>, String> {
251 if !self.initialized {
252 return Err("not initialized".into());
253 }
254 let result = self.request("tools/list", serde_json::json!({}))?;
255
256 let tools = result
257 .get("tools")
258 .and_then(|t| t.as_array())
259 .map(|arr| {
260 arr.iter()
261 .filter_map(|t| {
262 let name = t.get("name")?.as_str()?.to_string();
263 let description = t
264 .get("description")
265 .and_then(|d| d.as_str())
266 .map(String::from);
267 let input_schema = t
268 .get("inputSchema")
269 .cloned()
270 .unwrap_or(serde_json::json!({}));
271 Some(McpToolSpec {
272 name,
273 description,
274 input_schema,
275 })
276 })
277 .collect()
278 })
279 .unwrap_or_default();
280
281 Ok(tools)
282 }
283
284 pub fn call_tool(&mut self, tool_name: &str, arguments: Value) -> Result<String, String> {
285 if !self.initialized {
286 return Err("not initialized".into());
287 }
288 let result = self.request(
289 "tools/call",
290 serde_json::json!({
291 "name": tool_name,
292 "arguments": arguments
293 }),
294 )?;
295
296 result
297 .get("content")
298 .and_then(|c| c.as_array())
299 .and_then(|arr| arr.first())
300 .and_then(|item| item.get("text"))
301 .and_then(|t| t.as_str())
302 .map(String::from)
303 .ok_or_else(|| "invalid response format".into())
304 }
305
306 pub fn list_resources(&mut self) -> Result<Vec<McpResource>, String> {
307 if !self.initialized {
308 return Err("not initialized".into());
309 }
310
311 let result = self.request("resources/list", serde_json::json!({}))?;
312 let resources = result
313 .get("resources")
314 .and_then(|value| value.as_array())
315 .map(|items| {
316 items
317 .iter()
318 .filter_map(|item| {
319 Some(McpResource {
320 uri: item.get("uri")?.as_str()?.to_string(),
321 name: item.get("name")?.as_str()?.to_string(),
322 mime_type: item
323 .get("mimeType")
324 .and_then(|value| value.as_str())
325 .map(str::to_string),
326 description: item
327 .get("description")
328 .and_then(|value| value.as_str())
329 .map(str::to_string),
330 server: self.server_name.clone(),
331 })
332 })
333 .collect()
334 })
335 .unwrap_or_default();
336
337 Ok(resources)
338 }
339
340 pub fn read_resource(&mut self, uri: &str) -> Result<Vec<McpResourceContent>, String> {
341 if !self.initialized {
342 return Err("not initialized".into());
343 }
344
345 let result = self.request("resources/read", serde_json::json!({ "uri": uri }))?;
346 let contents = result
347 .get("contents")
348 .and_then(|value| value.as_array())
349 .map(|items| {
350 items
351 .iter()
352 .filter_map(|item| {
353 Some(McpResourceContent {
354 uri: item.get("uri")?.as_str()?.to_string(),
355 mime_type: item
356 .get("mimeType")
357 .and_then(|value| value.as_str())
358 .map(str::to_string),
359 text: item
360 .get("text")
361 .and_then(|value| value.as_str())
362 .map(str::to_string),
363 blob: item
364 .get("blob")
365 .and_then(|value| value.as_str())
366 .map(str::to_string),
367 })
368 })
369 .collect()
370 })
371 .unwrap_or_default();
372
373 Ok(contents)
374 }
375
376 pub fn server_name(&self) -> &str {
377 &self.server_name
378 }
379
380 pub fn is_initialized(&self) -> bool {
381 self.initialized
382 }
383}
384
385impl Drop for McpStdioClient {
386 fn drop(&mut self) {
387 self.io.kill();
388 }
389}
390
391pub fn discover_mcp_tools_sync(
392 servers: &BTreeMap<String, McpServerConfig>,
393) -> BTreeMap<String, Vec<McpToolSpec>> {
394 let mut result: BTreeMap<String, Vec<McpToolSpec>> = BTreeMap::new();
395
396 for (name, config) in servers {
397 if let McpServerConfig::Stdio {
398 command, args, env, ..
399 } = config
400 {
401 match McpStdioClient::new(name.clone(), command, args, env) {
402 Ok(ref mut client) => {
403 if let Ok(tools) = client.list_tools() {
404 result.insert(name.clone(), tools);
405 }
406 }
407 Err(e) => {
408 eprintln!("failed to connect to MCP server {}: {}", name, e);
409 }
410 }
411 }
412 }
413
414 result
415}
416
417pub fn run_mcp_tool_sync(
418 server_name: &str,
419 command: &str,
420 args: &[String],
421 env: &BTreeMap<String, String>,
422 tool_name: &str,
423 arguments: Value,
424) -> Result<String, String> {
425 let mut client = McpStdioClient::new(server_name.to_string(), command, args, env)?;
426 client.call_tool(tool_name, arguments)
427}
428
429pub fn discover_mcp_resources_sync(
430 servers: &BTreeMap<String, McpServerConfig>,
431) -> BTreeMap<String, Vec<McpResource>> {
432 let mut result = BTreeMap::new();
433
434 for (name, config) in servers {
435 if let McpServerConfig::Stdio {
436 command, args, env, ..
437 } = config
438 {
439 match McpStdioClient::new(name.clone(), command, args, env) {
440 Ok(ref mut client) => {
441 if let Ok(resources) = client.list_resources() {
442 result.insert(name.clone(), resources);
443 }
444 }
445 Err(e) => {
446 eprintln!("failed to connect to MCP server {}: {}", name, e);
447 }
448 }
449 }
450 }
451
452 result
453}
454
455pub fn read_mcp_resource_sync(
456 server_name: &str,
457 command: &str,
458 args: &[String],
459 env: &BTreeMap<String, String>,
460 uri: &str,
461) -> Result<Vec<McpResourceContent>, String> {
462 let mut client = McpStdioClient::new(server_name.to_string(), command, args, env)?;
463 client.read_resource(uri)
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize)]
467#[serde(untagged)]
468pub enum McpServerConfig {
469 Stdio {
470 #[serde(default)]
471 r#type: Option<String>,
472 command: String,
473 #[serde(default)]
474 args: Vec<String>,
475 #[serde(default)]
476 env: BTreeMap<String, String>,
477 },
478 Sse {
479 #[serde(rename = "type")]
480 r#type: String,
481 url: String,
482 #[serde(default)]
483 headers: BTreeMap<String, String>,
484 },
485 Http {
486 #[serde(rename = "type")]
487 r#type: String,
488 url: String,
489 #[serde(default)]
490 headers: BTreeMap<String, String>,
491 },
492 Ws {
493 #[serde(rename = "type")]
494 r#type: String,
495 url: String,
496 #[serde(default)]
497 headers: BTreeMap<String, String>,
498 },
499 Sdk {
500 #[serde(rename = "type")]
501 r#type: String,
502 name: String,
503 },
504}
505
506impl McpServerConfig {
507 pub fn command(&self) -> Option<String> {
508 match self {
509 McpServerConfig::Stdio { command, .. } => Some(command.clone()),
510 _ => None,
511 }
512 }
513
514 pub fn args(&self) -> &[String] {
515 match self {
516 McpServerConfig::Stdio { args, .. } => args,
517 _ => &[],
518 }
519 }
520}
521
522pub fn discover_mcp_servers(settings: &Value) -> BTreeMap<String, McpServerConfig> {
523 settings
524 .get("mcpServers")
525 .and_then(|value| serde_json::from_value(value.clone()).ok())
526 .unwrap_or_default()
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 fn temp_python_mcp_server() -> std::path::PathBuf {
534 let script = r#"
535import sys
536import json
537
538def send(obj):
539 content = json.dumps(obj).encode('utf-8')
540 header = ('Content-Length: %d\r\n\r\n' % len(content)).encode('ascii')
541 sys.stdout.buffer.write(header)
542 sys.stdout.buffer.write(content)
543 sys.stdout.buffer.flush()
544
545def read_request():
546 content_length = None
547 while True:
548 header = sys.stdin.buffer.readline()
549 if not header:
550 return None
551 if header in (b'\r\n', b'\n'):
552 break
553 if header.startswith(b'Content-Length:'):
554 content_length = int(header.split(b':', 1)[1].strip())
555 if content_length is None:
556 return None
557 body = sys.stdin.buffer.read(content_length)
558 if not body:
559 return None
560 return json.loads(body)
561
562while True:
563 msg = read_request()
564 if msg is None:
565 break
566 method = msg.get("method", "")
567 id = msg.get("id")
568
569 if method == "initialize":
570 send({
571 "jsonrpc": "2.0",
572 "id": id,
573 "result": {
574 "protocolVersion": "2024-11-05",
575 "capabilities": {"tools": {}},
576 "serverInfo": {"name": "test-server", "version": "1.0.0"}
577 }
578 })
579 elif method == "notifications/initialized":
580 pass
581 elif method == "tools/list":
582 send({
583 "jsonrpc": "2.0",
584 "id": id,
585 "result": {
586 "tools": [
587 {
588 "name": "test_tool",
589 "description": "A test MCP tool",
590 "inputSchema": {
591 "type": "object",
592 "properties": {
593 "message": {"type": "string"}
594 },
595 "required": ["message"]
596 }
597 },
598 {
599 "name": "echo",
600 "description": "Echo back the input",
601 "inputSchema": {"type": "object"}
602 }
603 ]
604 }
605 })
606 elif method == "tools/call":
607 params = msg.get("params", {})
608 tool_name = params.get("name", "")
609 arguments = params.get("arguments", {})
610 if tool_name == "test_tool":
611 msg_text = arguments.get("message", "default")
612 send({
613 "jsonrpc": "2.0",
614 "id": id,
615 "result": {
616 "content": [{"type": "text", "text": f"Received: {msg_text}"}]
617 }
618 })
619 elif tool_name == "echo":
620 send({
621 "jsonrpc": "2.0",
622 "id": id,
623 "result": {
624 "content": [{"type": "text", "text": json.dumps(arguments)}]
625 }
626 })
627 else:
628 send({
629 "jsonrpc": "2.0",
630 "id": id,
631 "error": {"code": -32601, "message": f"Unknown tool: {tool_name}"}
632 })
633 elif method == "resources/list":
634 send({
635 "jsonrpc": "2.0",
636 "id": id,
637 "result": {
638 "resources": [
639 {
640 "uri": "resource://test/hello",
641 "name": "hello.txt",
642 "mimeType": "text/plain",
643 "description": "Test text resource"
644 }
645 ]
646 }
647 })
648 elif method == "resources/read":
649 params = msg.get("params", {})
650 uri = params.get("uri", "")
651 if uri == "resource://test/hello":
652 send({
653 "jsonrpc": "2.0",
654 "id": id,
655 "result": {
656 "contents": [
657 {
658 "uri": uri,
659 "mimeType": "text/plain",
660 "text": "Hello from MCP resource"
661 }
662 ]
663 }
664 })
665 else:
666 send({
667 "jsonrpc": "2.0",
668 "id": id,
669 "error": {"code": -32602, "message": f"Unknown resource: {uri}"}
670 })
671"#;
672
673 let temp_dir = std::env::temp_dir();
674 let now = std::time::SystemTime::now()
675 .duration_since(std::time::UNIX_EPOCH)
676 .unwrap()
677 .as_nanos();
678 let script_path = temp_dir.join(format!("fake_mcp_server_{}.py", now));
679 std::fs::write(&script_path, script).expect("failed to write test script");
680 script_path
681 }
682
683 #[test]
684 fn normalize_name_for_mcp_basic() {
685 assert_eq!(normalize_name_for_mcp("hello"), "hello");
686 assert_eq!(normalize_name_for_mcp("hello-world"), "hello-world");
687 assert_eq!(normalize_name_for_mcp("hello.world"), "hello_world");
688 assert_eq!(normalize_name_for_mcp("hello world"), "hello_world");
689 assert_eq!(
690 normalize_name_for_mcp("hello.world.test"),
691 "hello_world_test"
692 );
693 }
694
695 #[test]
696 fn normalize_name_for_mcp_claudeai_prefix() {
697 assert_eq!(
698 normalize_name_for_mcp("claude.ai server"),
699 "claude_ai_server"
700 );
701 assert_eq!(
702 normalize_name_for_mcp("claude.ai server"),
703 "claude_ai_server"
704 );
705 assert_eq!(
706 normalize_name_for_mcp("claude.ai server__tool"),
707 "claude_ai_server_tool"
708 );
709 assert_eq!(
710 normalize_name_for_mcp("_claude.ai server_"),
711 "_claude_ai_server_"
712 );
713 }
714
715 #[test]
716 fn make_mcp_tool_name_basic() {
717 assert_eq!(
718 make_mcp_tool_name("my-server", "my_tool"),
719 "mcp__my-server__my_tool"
720 );
721 assert_eq!(
722 make_mcp_tool_name("server-with-dashes", "tool-with-dashes"),
723 "mcp__server-with-dashes__tool-with-dashes"
724 );
725 }
726
727 #[test]
728 fn make_mcp_tool_name_preserves_valid_names() {
729 assert_eq!(
730 make_mcp_tool_name("server123", "tool456"),
731 "mcp__server123__tool456"
732 );
733 }
734
735 #[test]
736 fn make_mcp_tool_name_claudeai() {
737 assert_eq!(
738 make_mcp_tool_name("claude.ai github", "create_issue"),
739 "mcp__claude_ai_github__create_issue"
740 );
741 }
742
743 #[test]
744 fn mcp_stdio_client_connects_and_lists_tools() {
745 let script_path = temp_python_mcp_server();
746 let mut client = McpStdioClient::new(
747 "test-server".to_string(),
748 "python3",
749 &[script_path.to_str().unwrap().to_string()],
750 &BTreeMap::new(),
751 )
752 .expect("failed to connect");
753
754 assert!(client.is_initialized());
755 let tools = client.list_tools().expect("failed to list tools");
756 assert_eq!(tools.len(), 2);
757 assert_eq!(tools[0].name, "test_tool");
758 assert_eq!(tools[1].name, "echo");
759
760 std::fs::remove_file(script_path).ok();
761 }
762
763 #[test]
764 fn mcp_stdio_client_calls_tool() {
765 let script_path = temp_python_mcp_server();
766 let mut client = McpStdioClient::new(
767 "test-server".to_string(),
768 "python3",
769 &[script_path.to_str().unwrap().to_string()],
770 &BTreeMap::new(),
771 )
772 .expect("failed to connect");
773
774 let result = client
775 .call_tool("test_tool", serde_json::json!({"message": "hello"}))
776 .expect("failed to call tool");
777 assert_eq!(result, "Received: hello");
778
779 std::fs::remove_file(script_path).ok();
780 }
781
782 #[test]
783 fn run_mcp_tool_sync_integration() {
784 let script_path = temp_python_mcp_server();
785 let result = run_mcp_tool_sync(
786 "test-server",
787 "python3",
788 &[script_path.to_str().unwrap().to_string()],
789 &BTreeMap::new(),
790 "echo",
791 serde_json::json!({"foo": "bar"}),
792 )
793 .expect("failed to run tool");
794 assert!(result.contains("foo"));
795
796 std::fs::remove_file(script_path).ok();
797 }
798
799 #[test]
800 fn discover_mcp_tools_sync_with_single_server() {
801 let script_path = temp_python_mcp_server();
802 let mut servers = BTreeMap::new();
803 servers.insert(
804 "test".to_string(),
805 McpServerConfig::Stdio {
806 r#type: Some("stdio".to_string()),
807 command: "python3".to_string(),
808 args: vec![script_path.to_str().unwrap().to_string()],
809 env: BTreeMap::new(),
810 },
811 );
812
813 let discovered = discover_mcp_tools_sync(&servers);
814 assert!(discovered.contains_key("test"));
815 let tools = discovered.get("test").unwrap();
816 assert_eq!(tools.len(), 2);
817
818 std::fs::remove_file(script_path).ok();
819 }
820
821 #[test]
822 fn mcp_stdio_client_lists_resources() {
823 let script_path = temp_python_mcp_server();
824 let mut client = McpStdioClient::new(
825 "test-server".to_string(),
826 "python3",
827 &[script_path.to_str().unwrap().to_string()],
828 &BTreeMap::new(),
829 )
830 .expect("failed to connect");
831
832 let resources = client.list_resources().expect("failed to list resources");
833 assert_eq!(resources.len(), 1);
834 assert_eq!(resources[0].server, "test-server");
835 assert_eq!(resources[0].uri, "resource://test/hello");
836
837 std::fs::remove_file(script_path).ok();
838 }
839
840 #[test]
841 fn mcp_stdio_client_reads_resource() {
842 let script_path = temp_python_mcp_server();
843 let mut client = McpStdioClient::new(
844 "test-server".to_string(),
845 "python3",
846 &[script_path.to_str().unwrap().to_string()],
847 &BTreeMap::new(),
848 )
849 .expect("failed to connect");
850
851 let contents = client
852 .read_resource("resource://test/hello")
853 .expect("failed to read resource");
854 assert_eq!(contents.len(), 1);
855 assert_eq!(contents[0].text.as_deref(), Some("Hello from MCP resource"));
856
857 std::fs::remove_file(script_path).ok();
858 }
859
860 #[test]
861 fn discover_mcp_resources_sync_with_single_server() {
862 let script_path = temp_python_mcp_server();
863 let mut servers = BTreeMap::new();
864 servers.insert(
865 "test".to_string(),
866 McpServerConfig::Stdio {
867 r#type: Some("stdio".to_string()),
868 command: "python3".to_string(),
869 args: vec![script_path.to_str().unwrap().to_string()],
870 env: BTreeMap::new(),
871 },
872 );
873
874 let discovered = discover_mcp_resources_sync(&servers);
875 assert!(discovered.contains_key("test"));
876 let resources = discovered.get("test").unwrap();
877 assert_eq!(resources.len(), 1);
878 assert_eq!(resources[0].uri, "resource://test/hello");
879
880 std::fs::remove_file(script_path).ok();
881 }
882
883 #[test]
884 fn read_mcp_resource_sync_integration() {
885 let script_path = temp_python_mcp_server();
886 let contents = read_mcp_resource_sync(
887 "test-server",
888 "python3",
889 &[script_path.to_str().unwrap().to_string()],
890 &BTreeMap::new(),
891 "resource://test/hello",
892 )
893 .expect("failed to read resource");
894 assert_eq!(contents.len(), 1);
895 assert_eq!(contents[0].text.as_deref(), Some("Hello from MCP resource"));
896
897 std::fs::remove_file(script_path).ok();
898 }
899}