1use reqwest::blocking::Client as BlockingHttpClient;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::collections::BTreeMap;
5use std::fmt;
6use std::io::{BufRead, BufReader, Read, Write};
7use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
8
9#[derive(Debug, Clone)]
10pub struct McpToolSpec {
11 pub name: String,
12 pub description: Option<String>,
13 pub input_schema: Value,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "camelCase")]
18pub struct McpResource {
19 pub uri: String,
20 pub name: String,
21 pub mime_type: Option<String>,
22 pub description: Option<String>,
23 pub server: String,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27#[serde(rename_all = "camelCase")]
28pub struct McpResourceContent {
29 pub uri: String,
30 pub mime_type: Option<String>,
31 pub text: Option<String>,
32 pub blob: Option<String>,
33}
34
35const CLAUDEAI_SERVER_PREFIX: &str = "claude.ai ";
36
37pub fn normalize_name_for_mcp(name: &str) -> String {
38 let mut normalized = String::with_capacity(name.len());
39 for c in name.chars() {
40 if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
41 normalized.push(c);
42 } else {
43 normalized.push('_');
44 }
45 }
46 if name.starts_with(CLAUDEAI_SERVER_PREFIX) {
47 let mut collapsed = String::new();
48 let mut last_was_underscore = false;
49 for c in normalized.chars() {
50 if c == '_' {
51 if !last_was_underscore {
52 collapsed.push(c);
53 last_was_underscore = true;
54 }
55 } else {
56 collapsed.push(c);
57 last_was_underscore = false;
58 }
59 }
60 let trimmed = collapsed.trim_matches('_');
61 if trimmed.is_empty() {
62 normalized
63 } else {
64 trimmed.to_string()
65 }
66 } else {
67 normalized
68 }
69}
70
71pub fn make_mcp_tool_name(server_name: &str, tool_name: &str) -> String {
72 let prefix = format!("mcp__{}__", normalize_name_for_mcp(server_name));
73 format!("{}{}", prefix, normalize_name_for_mcp(tool_name))
74}
75
76fn parse_tool_specs(result: &Value) -> Vec<McpToolSpec> {
77 result
78 .get("tools")
79 .and_then(|t| t.as_array())
80 .map(|arr| {
81 arr.iter()
82 .filter_map(|t| {
83 let name = t.get("name")?.as_str()?.to_string();
84 let description = t
85 .get("description")
86 .and_then(|d| d.as_str())
87 .map(String::from);
88 let input_schema = t
89 .get("inputSchema")
90 .cloned()
91 .unwrap_or(serde_json::json!({}));
92 Some(McpToolSpec {
93 name,
94 description,
95 input_schema,
96 })
97 })
98 .collect()
99 })
100 .unwrap_or_default()
101}
102
103fn parse_tool_call_text(result: &Value) -> Result<String, String> {
104 result
105 .get("content")
106 .and_then(|c| c.as_array())
107 .and_then(|arr| arr.first())
108 .and_then(|item| item.get("text"))
109 .and_then(|t| t.as_str())
110 .map(String::from)
111 .ok_or_else(|| "invalid response format".into())
112}
113
114fn parse_resources(server_name: &str, result: &Value) -> Vec<McpResource> {
115 result
116 .get("resources")
117 .and_then(|value| value.as_array())
118 .map(|items| {
119 items
120 .iter()
121 .filter_map(|item| {
122 Some(McpResource {
123 uri: item.get("uri")?.as_str()?.to_string(),
124 name: item.get("name")?.as_str()?.to_string(),
125 mime_type: item
126 .get("mimeType")
127 .and_then(|value| value.as_str())
128 .map(str::to_string),
129 description: item
130 .get("description")
131 .and_then(|value| value.as_str())
132 .map(str::to_string),
133 server: server_name.to_string(),
134 })
135 })
136 .collect()
137 })
138 .unwrap_or_default()
139}
140
141fn parse_resource_contents(result: &Value) -> Vec<McpResourceContent> {
142 result
143 .get("contents")
144 .and_then(|value| value.as_array())
145 .map(|items| {
146 items
147 .iter()
148 .filter_map(|item| {
149 Some(McpResourceContent {
150 uri: item.get("uri")?.as_str()?.to_string(),
151 mime_type: item
152 .get("mimeType")
153 .and_then(|value| value.as_str())
154 .map(str::to_string),
155 text: item
156 .get("text")
157 .and_then(|value| value.as_str())
158 .map(str::to_string),
159 blob: item
160 .get("blob")
161 .and_then(|value| value.as_str())
162 .map(str::to_string),
163 })
164 })
165 .collect()
166 })
167 .unwrap_or_default()
168}
169
170struct SyncIoBridge {
171 child: Child,
172 stdin: ChildStdin,
173 stdout: BufReader<ChildStdout>,
174}
175
176impl SyncIoBridge {
177 fn new(command: &str, args: &[String], env: &BTreeMap<String, String>) -> Result<Self, String> {
178 let mut cmd = Command::new(command);
179 cmd.args(args);
180 for (k, v) in env {
181 cmd.env(k, v);
182 }
183 cmd.stdin(Stdio::piped());
184 cmd.stdout(Stdio::piped());
185 cmd.stderr(Stdio::piped());
186
187 let mut child = cmd
188 .spawn()
189 .map_err(|e| format!("failed to spawn {}: {e}", command))?;
190 let stdin = child.stdin.take().ok_or("missing child stdin")?;
191 let stdout = child.stdout.take().ok_or("missing child stdout")?;
192 Ok(Self {
193 child,
194 stdin,
195 stdout: BufReader::new(stdout),
196 })
197 }
198
199 fn stdin(&mut self) -> &mut ChildStdin {
200 &mut self.stdin
201 }
202
203 fn stdout(&mut self) -> &mut BufReader<ChildStdout> {
204 &mut self.stdout
205 }
206
207 fn kill(&mut self) {
208 let _ = self.child.kill();
209 }
210}
211
212pub struct McpStdioClient {
213 server_name: String,
214 io: SyncIoBridge,
215 initialized: bool,
216 next_id: u64,
217}
218
219impl fmt::Debug for McpStdioClient {
220 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221 f.debug_struct("McpStdioClient")
222 .field("server_name", &self.server_name)
223 .field("initialized", &self.initialized)
224 .field("next_id", &self.next_id)
225 .finish()
226 }
227}
228
229pub struct McpHttpClient {
230 server_name: String,
231 url: String,
232 client: BlockingHttpClient,
233 headers: BTreeMap<String, String>,
234 initialized: bool,
235 next_id: u64,
236}
237
238impl fmt::Debug for McpHttpClient {
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 f.debug_struct("McpHttpClient")
241 .field("server_name", &self.server_name)
242 .field("url", &self.url)
243 .field("initialized", &self.initialized)
244 .field("next_id", &self.next_id)
245 .finish()
246 }
247}
248
249impl McpStdioClient {
250 pub fn new(
251 server_name: String,
252 command: &str,
253 args: &[String],
254 env: &BTreeMap<String, String>,
255 ) -> Result<Self, String> {
256 let io = SyncIoBridge::new(command, args, env)?;
257 let mut client = Self {
258 server_name,
259 io,
260 initialized: false,
261 next_id: 1,
262 };
263 client.initialize()?;
264 Ok(client)
265 }
266
267 fn next_request_id(&mut self) -> u64 {
268 let id = self.next_id;
269 self.next_id += 1;
270 id
271 }
272
273 fn initialize(&mut self) -> Result<(), String> {
274 let request = serde_json::json!({
275 "jsonrpc": "2.0",
276 "id": self.next_request_id(),
277 "method": "initialize",
278 "params": {
279 "protocolVersion": "2024-11-05",
280 "capabilities": {},
281 "clientInfo": {
282 "name": "clawedcode",
283 "version": env!("CARGO_PKG_VERSION")
284 }
285 }
286 });
287
288 self.send_json(&request)?;
289 let _response = self.read_json()?;
290
291 let notif = serde_json::json!({
292 "jsonrpc": "2.0",
293 "method": "notifications/initialized",
294 "params": {}
295 });
296 self.send_json(¬if)?;
297
298 self.initialized = true;
299 Ok(())
300 }
301
302 fn request(&mut self, method: &str, params: Value) -> Result<Value, String> {
303 let request = serde_json::json!({
304 "jsonrpc": "2.0",
305 "id": self.next_request_id(),
306 "method": method,
307 "params": params,
308 });
309
310 self.send_json(&request)?;
311 let response = self.read_json()?;
312
313 if let Some(error) = response.get("error") {
314 let message = error
315 .get("message")
316 .and_then(|value| value.as_str())
317 .unwrap_or("unknown MCP error");
318 return Err(message.to_string());
319 }
320
321 Ok(response.get("result").cloned().unwrap_or(Value::Null))
322 }
323
324 fn send_json(&mut self, value: &serde_json::Value) -> Result<(), String> {
325 let bytes = serde_json::to_vec(value).map_err(|e| format!("serialize error: {e}"))?;
326 let stdin = self.io.stdin();
327 write!(stdin, "Content-Length: {}\r\n\r\n", bytes.len())
328 .map_err(|e| format!("write header error: {e}"))?;
329 stdin
330 .write_all(&bytes)
331 .map_err(|e| format!("write error: {e}"))?;
332 stdin.flush().map_err(|e| format!("flush error: {e}"))?;
333 Ok(())
334 }
335
336 fn read_json(&mut self) -> Result<Value, String> {
337 let stdout = self.io.stdout();
338 let mut content_length = None;
339 let mut line = String::new();
340
341 loop {
342 line.clear();
343 stdout
344 .read_line(&mut line)
345 .map_err(|e| format!("read header error: {e}"))?;
346
347 if line.is_empty() {
348 return Err("unexpected EOF while reading MCP headers".into());
349 }
350
351 if line == "\r\n" || line == "\n" {
352 break;
353 }
354
355 let trimmed = line.trim();
356 if let Some(value) = trimmed.strip_prefix("Content-Length:") {
357 content_length = Some(
358 value
359 .trim()
360 .parse()
361 .map_err(|e| format!("parse Content-Length error: {e}"))?,
362 );
363 }
364 }
365
366 let content_length = content_length.ok_or("missing Content-Length header")?;
367
368 let mut body = vec![0u8; content_length];
369 stdout
370 .read_exact(&mut body)
371 .map_err(|e| format!("read body error: {e}"))?;
372
373 serde_json::from_slice(&body).map_err(|e| format!("parse error: {e}"))
374 }
375
376 pub fn list_tools(&mut self) -> Result<Vec<McpToolSpec>, String> {
377 if !self.initialized {
378 return Err("not initialized".into());
379 }
380 let result = self.request("tools/list", serde_json::json!({}))?;
381 Ok(parse_tool_specs(&result))
382 }
383
384 pub fn call_tool(&mut self, tool_name: &str, arguments: Value) -> Result<String, String> {
385 if !self.initialized {
386 return Err("not initialized".into());
387 }
388 let result = self.request(
389 "tools/call",
390 serde_json::json!({
391 "name": tool_name,
392 "arguments": arguments
393 }),
394 )?;
395 parse_tool_call_text(&result)
396 }
397
398 pub fn list_resources(&mut self) -> Result<Vec<McpResource>, String> {
399 if !self.initialized {
400 return Err("not initialized".into());
401 }
402
403 let result = self.request("resources/list", serde_json::json!({}))?;
404 Ok(parse_resources(&self.server_name, &result))
405 }
406
407 pub fn read_resource(&mut self, uri: &str) -> Result<Vec<McpResourceContent>, String> {
408 if !self.initialized {
409 return Err("not initialized".into());
410 }
411
412 let result = self.request("resources/read", serde_json::json!({ "uri": uri }))?;
413 Ok(parse_resource_contents(&result))
414 }
415
416 pub fn server_name(&self) -> &str {
417 &self.server_name
418 }
419
420 pub fn is_initialized(&self) -> bool {
421 self.initialized
422 }
423}
424
425impl McpHttpClient {
426 pub fn new(
427 server_name: String,
428 url: String,
429 headers: BTreeMap<String, String>,
430 ) -> Result<Self, String> {
431 let client = BlockingHttpClient::builder()
432 .build()
433 .map_err(|e| format!("failed to build HTTP client: {e}"))?;
434 let mut http = Self {
435 server_name,
436 url,
437 client,
438 headers,
439 initialized: false,
440 next_id: 1,
441 };
442 http.initialize()?;
443 Ok(http)
444 }
445
446 fn next_request_id(&mut self) -> u64 {
447 let id = self.next_id;
448 self.next_id += 1;
449 id
450 }
451
452 fn initialize(&mut self) -> Result<(), String> {
453 let request = serde_json::json!({
454 "jsonrpc": "2.0",
455 "id": self.next_request_id(),
456 "method": "initialize",
457 "params": {
458 "protocolVersion": "2024-11-05",
459 "capabilities": {},
460 "clientInfo": {
461 "name": "clawedcode",
462 "version": env!("CARGO_PKG_VERSION")
463 }
464 }
465 });
466
467 let _ = self.send_request(&request)?;
468 self.initialized = true;
469 Ok(())
470 }
471
472 fn send_request(&self, body: &Value) -> Result<Value, String> {
473 let mut request = self.client.post(&self.url).json(body);
474 for (name, value) in &self.headers {
475 request = request.header(name, value);
476 }
477
478 let response = request
479 .send()
480 .map_err(|e| format!("HTTP MCP request failed: {e}"))?;
481 let status = response.status();
482 if !status.is_success() {
483 return Err(format!("HTTP MCP request failed with status {status}"));
484 }
485
486 response
487 .json::<Value>()
488 .map_err(|e| format!("HTTP MCP response parse error: {e}"))
489 }
490
491 fn request(&mut self, method: &str, params: Value) -> Result<Value, String> {
492 let request = serde_json::json!({
493 "jsonrpc": "2.0",
494 "id": self.next_request_id(),
495 "method": method,
496 "params": params,
497 });
498 let response = self.send_request(&request)?;
499 if let Some(error) = response.get("error") {
500 let message = error
501 .get("message")
502 .and_then(|value| value.as_str())
503 .unwrap_or("unknown MCP error");
504 return Err(message.to_string());
505 }
506 Ok(response.get("result").cloned().unwrap_or(Value::Null))
507 }
508
509 pub fn list_tools(&mut self) -> Result<Vec<McpToolSpec>, String> {
510 if !self.initialized {
511 return Err("not initialized".into());
512 }
513 let result = self.request("tools/list", serde_json::json!({}))?;
514 Ok(parse_tool_specs(&result))
515 }
516
517 pub fn call_tool(&mut self, tool_name: &str, arguments: Value) -> Result<String, String> {
518 if !self.initialized {
519 return Err("not initialized".into());
520 }
521 let result = self.request(
522 "tools/call",
523 serde_json::json!({
524 "name": tool_name,
525 "arguments": arguments
526 }),
527 )?;
528 parse_tool_call_text(&result)
529 }
530
531 pub fn list_resources(&mut self) -> Result<Vec<McpResource>, String> {
532 if !self.initialized {
533 return Err("not initialized".into());
534 }
535 let result = self.request("resources/list", serde_json::json!({}))?;
536 Ok(parse_resources(&self.server_name, &result))
537 }
538
539 pub fn read_resource(&mut self, uri: &str) -> Result<Vec<McpResourceContent>, String> {
540 if !self.initialized {
541 return Err("not initialized".into());
542 }
543 let result = self.request("resources/read", serde_json::json!({ "uri": uri }))?;
544 Ok(parse_resource_contents(&result))
545 }
546
547 pub fn is_initialized(&self) -> bool {
548 self.initialized
549 }
550}
551
552impl Drop for McpStdioClient {
553 fn drop(&mut self) {
554 self.io.kill();
555 }
556}
557
558pub fn discover_mcp_tools_sync(
559 servers: &BTreeMap<String, McpServerConfig>,
560) -> BTreeMap<String, Vec<McpToolSpec>> {
561 let mut result: BTreeMap<String, Vec<McpToolSpec>> = BTreeMap::new();
562
563 for (name, config) in servers {
564 match config {
565 McpServerConfig::Stdio {
566 command, args, env, ..
567 } => match McpStdioClient::new(name.clone(), command, args, env) {
568 Ok(ref mut client) => {
569 if let Ok(tools) = client.list_tools() {
570 result.insert(name.clone(), tools);
571 }
572 }
573 Err(e) => {
574 eprintln!("failed to connect to MCP server {}: {}", name, e);
575 }
576 },
577 McpServerConfig::Http { url, headers, .. } => {
578 match McpHttpClient::new(name.clone(), url.clone(), headers.clone()) {
579 Ok(ref mut client) => {
580 if let Ok(tools) = client.list_tools() {
581 result.insert(name.clone(), tools);
582 }
583 }
584 Err(e) => {
585 eprintln!("failed to connect to MCP HTTP server {}: {}", name, e);
586 }
587 }
588 }
589 _ => {}
590 }
591 }
592
593 result
594}
595
596pub fn run_mcp_tool_sync(
597 config: &McpServerConfig,
598 server_name: &str,
599 tool_name: &str,
600 arguments: Value,
601) -> Result<String, String> {
602 match config {
603 McpServerConfig::Stdio {
604 command, args, env, ..
605 } => {
606 let mut client = McpStdioClient::new(server_name.to_string(), command, args, env)?;
607 client.call_tool(tool_name, arguments)
608 }
609 McpServerConfig::Http { url, headers, .. } => {
610 let mut client =
611 McpHttpClient::new(server_name.to_string(), url.clone(), headers.clone())?;
612 client.call_tool(tool_name, arguments)
613 }
614 _ => Err("unsupported MCP server type".to_string()),
615 }
616}
617
618pub fn discover_mcp_resources_sync(
619 servers: &BTreeMap<String, McpServerConfig>,
620) -> BTreeMap<String, Vec<McpResource>> {
621 let mut result = BTreeMap::new();
622
623 for (name, config) in servers {
624 match config {
625 McpServerConfig::Stdio {
626 command, args, env, ..
627 } => match McpStdioClient::new(name.clone(), command, args, env) {
628 Ok(ref mut client) => {
629 if let Ok(resources) = client.list_resources() {
630 result.insert(name.clone(), resources);
631 }
632 }
633 Err(e) => {
634 eprintln!("failed to connect to MCP server {}: {}", name, e);
635 }
636 },
637 McpServerConfig::Http { url, headers, .. } => {
638 match McpHttpClient::new(name.clone(), url.clone(), headers.clone()) {
639 Ok(ref mut client) => {
640 if let Ok(resources) = client.list_resources() {
641 result.insert(name.clone(), resources);
642 }
643 }
644 Err(e) => {
645 eprintln!("failed to connect to MCP HTTP server {}: {}", name, e);
646 }
647 }
648 }
649 _ => {}
650 }
651 }
652 result
653}
654
655pub fn read_mcp_resource_sync(
656 config: &McpServerConfig,
657 server_name: &str,
658 uri: &str,
659) -> Result<Vec<McpResourceContent>, String> {
660 match config {
661 McpServerConfig::Stdio {
662 command, args, env, ..
663 } => {
664 let mut client = McpStdioClient::new(server_name.to_string(), command, args, env)?;
665 client.read_resource(uri)
666 }
667 McpServerConfig::Http { url, headers, .. } => {
668 let mut client =
669 McpHttpClient::new(server_name.to_string(), url.clone(), headers.clone())?;
670 client.read_resource(uri)
671 }
672 _ => Err("unsupported MCP server type".to_string()),
673 }
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize)]
677#[serde(untagged)]
678pub enum McpServerConfig {
679 Stdio {
680 #[serde(default)]
681 r#type: Option<String>,
682 command: String,
683 #[serde(default)]
684 args: Vec<String>,
685 #[serde(default)]
686 env: BTreeMap<String, String>,
687 },
688 Sse {
689 #[serde(rename = "type")]
690 r#type: String,
691 url: String,
692 #[serde(default)]
693 headers: BTreeMap<String, String>,
694 },
695 Http {
696 #[serde(rename = "type")]
697 r#type: String,
698 url: String,
699 #[serde(default)]
700 headers: BTreeMap<String, String>,
701 },
702 Ws {
703 #[serde(rename = "type")]
704 r#type: String,
705 url: String,
706 #[serde(default)]
707 headers: BTreeMap<String, String>,
708 },
709 Sdk {
710 #[serde(rename = "type")]
711 r#type: String,
712 name: String,
713 },
714}
715
716impl McpServerConfig {
717 pub fn command(&self) -> Option<String> {
718 match self {
719 McpServerConfig::Stdio { command, .. } => Some(command.clone()),
720 _ => None,
721 }
722 }
723
724 pub fn args(&self) -> &[String] {
725 match self {
726 McpServerConfig::Stdio { args, .. } => args,
727 _ => &[],
728 }
729 }
730}
731
732pub fn discover_mcp_servers(settings: &Value) -> BTreeMap<String, McpServerConfig> {
733 settings
734 .get("mcpServers")
735 .and_then(|value| serde_json::from_value(value.clone()).ok())
736 .unwrap_or_default()
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742
743 fn temp_python_mcp_server() -> std::path::PathBuf {
744 let script = r#"
745import sys
746import json
747
748def send(obj):
749 content = json.dumps(obj).encode('utf-8')
750 header = ('Content-Length: %d\r\n\r\n' % len(content)).encode('ascii')
751 sys.stdout.buffer.write(header)
752 sys.stdout.buffer.write(content)
753 sys.stdout.buffer.flush()
754
755def read_request():
756 content_length = None
757 while True:
758 header = sys.stdin.buffer.readline()
759 if not header:
760 return None
761 if header in (b'\r\n', b'\n'):
762 break
763 if header.startswith(b'Content-Length:'):
764 content_length = int(header.split(b':', 1)[1].strip())
765 if content_length is None:
766 return None
767 body = sys.stdin.buffer.read(content_length)
768 if not body:
769 return None
770 return json.loads(body)
771
772while True:
773 msg = read_request()
774 if msg is None:
775 break
776 method = msg.get("method", "")
777 id = msg.get("id")
778
779 if method == "initialize":
780 send({
781 "jsonrpc": "2.0",
782 "id": id,
783 "result": {
784 "protocolVersion": "2024-11-05",
785 "capabilities": {"tools": {}},
786 "serverInfo": {"name": "test-server", "version": "1.0.0"}
787 }
788 })
789 elif method == "notifications/initialized":
790 pass
791 elif method == "tools/list":
792 send({
793 "jsonrpc": "2.0",
794 "id": id,
795 "result": {
796 "tools": [
797 {
798 "name": "test_tool",
799 "description": "A test MCP tool",
800 "inputSchema": {
801 "type": "object",
802 "properties": {
803 "message": {"type": "string"}
804 },
805 "required": ["message"]
806 }
807 },
808 {
809 "name": "echo",
810 "description": "Echo back the input",
811 "inputSchema": {"type": "object"}
812 }
813 ]
814 }
815 })
816 elif method == "tools/call":
817 params = msg.get("params", {})
818 tool_name = params.get("name", "")
819 arguments = params.get("arguments", {})
820 if tool_name == "test_tool":
821 msg_text = arguments.get("message", "default")
822 send({
823 "jsonrpc": "2.0",
824 "id": id,
825 "result": {
826 "content": [{"type": "text", "text": f"Received: {msg_text}"}]
827 }
828 })
829 elif tool_name == "echo":
830 send({
831 "jsonrpc": "2.0",
832 "id": id,
833 "result": {
834 "content": [{"type": "text", "text": json.dumps(arguments)}]
835 }
836 })
837 else:
838 send({
839 "jsonrpc": "2.0",
840 "id": id,
841 "error": {"code": -32601, "message": f"Unknown tool: {tool_name}"}
842 })
843 elif method == "resources/list":
844 send({
845 "jsonrpc": "2.0",
846 "id": id,
847 "result": {
848 "resources": [
849 {
850 "uri": "resource://test/hello",
851 "name": "hello.txt",
852 "mimeType": "text/plain",
853 "description": "Test text resource"
854 }
855 ]
856 }
857 })
858 elif method == "resources/read":
859 params = msg.get("params", {})
860 uri = params.get("uri", "")
861 if uri == "resource://test/hello":
862 send({
863 "jsonrpc": "2.0",
864 "id": id,
865 "result": {
866 "contents": [
867 {
868 "uri": uri,
869 "mimeType": "text/plain",
870 "text": "Hello from MCP resource"
871 }
872 ]
873 }
874 })
875 else:
876 send({
877 "jsonrpc": "2.0",
878 "id": id,
879 "error": {"code": -32602, "message": f"Unknown resource: {uri}"}
880 })
881"#;
882
883 let temp_dir = std::env::temp_dir();
884 let now = std::time::SystemTime::now()
885 .duration_since(std::time::UNIX_EPOCH)
886 .unwrap()
887 .as_nanos();
888 let script_path = temp_dir.join(format!("fake_mcp_server_{}.py", now));
889 std::fs::write(&script_path, script).expect("failed to write test script");
890 script_path
891 }
892
893 #[test]
894 fn normalize_name_for_mcp_basic() {
895 assert_eq!(normalize_name_for_mcp("hello"), "hello");
896 assert_eq!(normalize_name_for_mcp("hello-world"), "hello-world");
897 assert_eq!(normalize_name_for_mcp("hello.world"), "hello_world");
898 assert_eq!(normalize_name_for_mcp("hello world"), "hello_world");
899 assert_eq!(
900 normalize_name_for_mcp("hello.world.test"),
901 "hello_world_test"
902 );
903 }
904
905 #[test]
906 fn normalize_name_for_mcp_claudeai_prefix() {
907 assert_eq!(
908 normalize_name_for_mcp("claude.ai server"),
909 "claude_ai_server"
910 );
911 assert_eq!(
912 normalize_name_for_mcp("claude.ai server"),
913 "claude_ai_server"
914 );
915 assert_eq!(
916 normalize_name_for_mcp("claude.ai server__tool"),
917 "claude_ai_server_tool"
918 );
919 assert_eq!(
920 normalize_name_for_mcp("_claude.ai server_"),
921 "_claude_ai_server_"
922 );
923 }
924
925 #[test]
926 fn make_mcp_tool_name_basic() {
927 assert_eq!(
928 make_mcp_tool_name("my-server", "my_tool"),
929 "mcp__my-server__my_tool"
930 );
931 assert_eq!(
932 make_mcp_tool_name("server-with-dashes", "tool-with-dashes"),
933 "mcp__server-with-dashes__tool-with-dashes"
934 );
935 }
936
937 #[test]
938 fn make_mcp_tool_name_preserves_valid_names() {
939 assert_eq!(
940 make_mcp_tool_name("server123", "tool456"),
941 "mcp__server123__tool456"
942 );
943 }
944
945 #[test]
946 fn make_mcp_tool_name_claudeai() {
947 assert_eq!(
948 make_mcp_tool_name("claude.ai github", "create_issue"),
949 "mcp__claude_ai_github__create_issue"
950 );
951 }
952
953 #[test]
954 fn mcp_stdio_client_connects_and_lists_tools() {
955 let script_path = temp_python_mcp_server();
956 let mut client = McpStdioClient::new(
957 "test-server".to_string(),
958 "python3",
959 &[script_path.to_str().unwrap().to_string()],
960 &BTreeMap::new(),
961 )
962 .expect("failed to connect");
963
964 assert!(client.is_initialized());
965 let tools = client.list_tools().expect("failed to list tools");
966 assert_eq!(tools.len(), 2);
967 assert_eq!(tools[0].name, "test_tool");
968 assert_eq!(tools[1].name, "echo");
969
970 std::fs::remove_file(script_path).ok();
971 }
972
973 #[test]
974 fn mcp_stdio_client_calls_tool() {
975 let script_path = temp_python_mcp_server();
976 let mut client = McpStdioClient::new(
977 "test-server".to_string(),
978 "python3",
979 &[script_path.to_str().unwrap().to_string()],
980 &BTreeMap::new(),
981 )
982 .expect("failed to connect");
983
984 let result = client
985 .call_tool("test_tool", serde_json::json!({"message": "hello"}))
986 .expect("failed to call tool");
987 assert_eq!(result, "Received: hello");
988
989 std::fs::remove_file(script_path).ok();
990 }
991
992 #[test]
993 fn run_mcp_tool_sync_integration() {
994 let script_path = temp_python_mcp_server();
995 let config = McpServerConfig::Stdio {
996 r#type: Some("stdio".to_string()),
997 command: "python3".to_string(),
998 args: vec![script_path.to_str().unwrap().to_string()],
999 env: BTreeMap::new(),
1000 };
1001 let result = run_mcp_tool_sync(
1002 &config,
1003 "test-server",
1004 "echo",
1005 serde_json::json!({"foo": "bar"}),
1006 )
1007 .expect("failed to run tool");
1008 assert!(result.contains("foo"));
1009
1010 std::fs::remove_file(script_path).ok();
1011 }
1012
1013 #[test]
1014 fn discover_mcp_tools_sync_with_single_server() {
1015 let script_path = temp_python_mcp_server();
1016 let mut servers = BTreeMap::new();
1017 servers.insert(
1018 "test".to_string(),
1019 McpServerConfig::Stdio {
1020 r#type: Some("stdio".to_string()),
1021 command: "python3".to_string(),
1022 args: vec![script_path.to_str().unwrap().to_string()],
1023 env: BTreeMap::new(),
1024 },
1025 );
1026
1027 let discovered = discover_mcp_tools_sync(&servers);
1028 assert!(discovered.contains_key("test"));
1029 let tools = discovered.get("test").unwrap();
1030 assert_eq!(tools.len(), 2);
1031
1032 std::fs::remove_file(script_path).ok();
1033 }
1034
1035 #[test]
1036 fn mcp_stdio_client_lists_resources() {
1037 let script_path = temp_python_mcp_server();
1038 let mut client = McpStdioClient::new(
1039 "test-server".to_string(),
1040 "python3",
1041 &[script_path.to_str().unwrap().to_string()],
1042 &BTreeMap::new(),
1043 )
1044 .expect("failed to connect");
1045
1046 let resources = client.list_resources().expect("failed to list resources");
1047 assert_eq!(resources.len(), 1);
1048 assert_eq!(resources[0].server, "test-server");
1049 assert_eq!(resources[0].uri, "resource://test/hello");
1050
1051 std::fs::remove_file(script_path).ok();
1052 }
1053
1054 #[test]
1055 fn mcp_stdio_client_reads_resource() {
1056 let script_path = temp_python_mcp_server();
1057 let mut client = McpStdioClient::new(
1058 "test-server".to_string(),
1059 "python3",
1060 &[script_path.to_str().unwrap().to_string()],
1061 &BTreeMap::new(),
1062 )
1063 .expect("failed to connect");
1064
1065 let contents = client
1066 .read_resource("resource://test/hello")
1067 .expect("failed to read resource");
1068 assert_eq!(contents.len(), 1);
1069 assert_eq!(contents[0].text.as_deref(), Some("Hello from MCP resource"));
1070
1071 std::fs::remove_file(script_path).ok();
1072 }
1073
1074 #[test]
1075 fn discover_mcp_resources_sync_with_single_server() {
1076 let script_path = temp_python_mcp_server();
1077 let mut servers = BTreeMap::new();
1078 servers.insert(
1079 "test".to_string(),
1080 McpServerConfig::Stdio {
1081 r#type: Some("stdio".to_string()),
1082 command: "python3".to_string(),
1083 args: vec![script_path.to_str().unwrap().to_string()],
1084 env: BTreeMap::new(),
1085 },
1086 );
1087
1088 let discovered = discover_mcp_resources_sync(&servers);
1089 assert!(discovered.contains_key("test"));
1090 let resources = discovered.get("test").unwrap();
1091 assert_eq!(resources.len(), 1);
1092 assert_eq!(resources[0].uri, "resource://test/hello");
1093
1094 std::fs::remove_file(script_path).ok();
1095 }
1096
1097 #[test]
1098 fn read_mcp_resource_sync_integration() {
1099 let script_path = temp_python_mcp_server();
1100 let config = McpServerConfig::Stdio {
1101 r#type: Some("stdio".to_string()),
1102 command: "python3".to_string(),
1103 args: vec![script_path.to_str().unwrap().to_string()],
1104 env: BTreeMap::new(),
1105 };
1106 let contents = read_mcp_resource_sync(&config, "test-server", "resource://test/hello")
1107 .expect("failed to read resource");
1108 assert_eq!(contents.len(), 1);
1109 assert_eq!(contents[0].text.as_deref(), Some("Hello from MCP resource"));
1110
1111 std::fs::remove_file(script_path).ok();
1112 }
1113
1114 mod transport_failure_tests {
1115 use super::*;
1116
1117 fn temp_exiting_mcp_server() -> std::path::PathBuf {
1118 let now = std::time::SystemTime::now()
1119 .duration_since(std::time::UNIX_EPOCH)
1120 .unwrap()
1121 .as_nanos();
1122 let script_path = std::env::temp_dir().join(format!("exiting_mcp_{}.sh", now));
1123 let script = r#"#!/bin/sh
1124echo 'Content-Length: 44\r\n\r\n{"jsonrpc":"2.0","id":1,"result":{}}' >&2
1125exit 0
1126"#;
1127 std::fs::write(&script_path, script).unwrap();
1128
1129 #[cfg(unix)]
1130 {
1131 use std::os::unix::fs::PermissionsExt;
1132 let mut perms = std::fs::metadata(&script_path).unwrap().permissions();
1133 perms.set_mode(0o755);
1134 std::fs::set_permissions(&script_path, perms).unwrap();
1135 }
1136
1137 script_path
1138 }
1139
1140 #[test]
1141 fn mcp_connection_failure_returns_clean_error() {
1142 let result = McpStdioClient::new(
1143 "nonexistent-server".to_string(),
1144 "/path/to/nonexistent/server",
1145 &[],
1146 &BTreeMap::new(),
1147 );
1148
1149 assert!(result.is_err());
1150 let error = result.unwrap_err();
1151 assert!(error.contains("failed to spawn") || error.contains("No such file"));
1152 }
1153
1154 #[test]
1155 fn discover_mcp_sync_skips_failed_servers() {
1156 let exiting_script = temp_exiting_mcp_server();
1157 let normal_script = temp_python_mcp_server();
1158
1159 let mut servers = BTreeMap::new();
1160 servers.insert(
1161 "exiting-server".to_string(),
1162 McpServerConfig::Stdio {
1163 r#type: Some("stdio".to_string()),
1164 command: exiting_script.to_str().unwrap().to_string(),
1165 args: vec![],
1166 env: BTreeMap::new(),
1167 },
1168 );
1169 servers.insert(
1170 "working-server".to_string(),
1171 McpServerConfig::Stdio {
1172 r#type: Some("stdio".to_string()),
1173 command: "python3".to_string(),
1174 args: vec![normal_script.to_str().unwrap().to_string()],
1175 env: BTreeMap::new(),
1176 },
1177 );
1178
1179 let tools = discover_mcp_tools_sync(&servers);
1180
1181 assert!(
1182 tools.contains_key("working-server"),
1183 "working server should be discovered"
1184 );
1185 let working_tools = tools.get("working-server").unwrap();
1186 assert!(
1187 !working_tools.is_empty(),
1188 "working server should have tools"
1189 );
1190
1191 std::fs::remove_file(&exiting_script).ok();
1192 std::fs::remove_file(&normal_script).ok();
1193 }
1194
1195 #[test]
1196 fn mcp_tool_execution_returns_clean_error_on_transport_failure() {
1197 let exiting_script = temp_exiting_mcp_server();
1198
1199 let config = McpServerConfig::Stdio {
1200 r#type: Some("stdio".to_string()),
1201 command: exiting_script.to_str().unwrap().to_string(),
1202 args: vec![],
1203 env: BTreeMap::new(),
1204 };
1205 let result = run_mcp_tool_sync(
1206 &config,
1207 "exiting-server",
1208 "test_tool",
1209 serde_json::json!({}),
1210 );
1211
1212 assert!(result.is_err());
1213 let error = result.unwrap_err();
1214 assert!(!error.is_empty(), "error message should be present");
1215
1216 std::fs::remove_file(&exiting_script).ok();
1217 }
1218
1219 #[test]
1220 fn mcp_connection_succeeds_after_previous_failure() {
1221 let normal_script = temp_python_mcp_server();
1222
1223 let _failed = McpStdioClient::new(
1224 "will-fail".to_string(),
1225 "/nonexistent/path/server",
1226 &[],
1227 &BTreeMap::new(),
1228 );
1229 assert!(_failed.is_err(), "first connection should fail");
1230
1231 let mut client = McpStdioClient::new(
1232 "working-server".to_string(),
1233 "python3",
1234 &[normal_script.to_str().unwrap().to_string()],
1235 &BTreeMap::new(),
1236 )
1237 .expect("second connection should succeed");
1238
1239 assert!(client.is_initialized());
1240 let tools = client.list_tools().expect("tools should work");
1241 assert!(!tools.is_empty());
1242
1243 std::fs::remove_file(&normal_script).ok();
1244 }
1245
1246 #[test]
1247 fn discover_mcp_resources_sync_handles_missing_server() {
1248 let mut servers = BTreeMap::new();
1249 servers.insert(
1250 "missing-server".to_string(),
1251 McpServerConfig::Stdio {
1252 r#type: Some("stdio".to_string()),
1253 command: "/nonexistent/server".to_string(),
1254 args: vec![],
1255 env: BTreeMap::new(),
1256 },
1257 );
1258
1259 let resources = discover_mcp_resources_sync(&servers);
1260
1261 assert!(
1262 resources.is_empty() || !resources.contains_key("missing-server"),
1263 "missing server should not appear in resources"
1264 );
1265 }
1266
1267 #[test]
1268 fn mcp_read_resource_handles_missing_server() {
1269 let config = McpServerConfig::Stdio {
1270 r#type: Some("stdio".to_string()),
1271 command: "/nonexistent/server".to_string(),
1272 args: vec![],
1273 env: BTreeMap::new(),
1274 };
1275 let result = read_mcp_resource_sync(&config, "missing-server", "resource://test/data");
1276
1277 assert!(result.is_err());
1278 assert!(!result.unwrap_err().is_empty());
1279 }
1280
1281 #[test]
1282 fn make_mcp_tool_name_normalizes_special_characters() {
1283 assert_eq!(
1284 make_mcp_tool_name("my server", "get_data"),
1285 "mcp__my_server__get_data"
1286 );
1287 assert_eq!(
1288 make_mcp_tool_name("my-server", "get.data"),
1289 "mcp__my-server__get_data"
1290 );
1291 assert_eq!(
1292 make_mcp_tool_name("My Server", "GetData"),
1293 "mcp__My_Server__GetData"
1294 );
1295 }
1296
1297 #[test]
1298 fn mcp_resource_content_deserialization() {
1299 let json = r#"{"uri":"file://test","name":"test.txt","mimeType":"text/plain","description":"A test file","server":"test-server"}"#;
1300 let resource: McpResource = serde_json::from_str(json).unwrap();
1301 assert_eq!(resource.uri, "file://test");
1302 assert_eq!(resource.name, "test.txt");
1303 assert_eq!(resource.mime_type, Some("text/plain".to_string()));
1304 assert_eq!(resource.server, "test-server");
1305 }
1306
1307 #[test]
1308 fn mcp_resource_content_fields() {
1309 let json = r#"{"uri":"file://test","mimeType":"text/plain","text":"hello world"}"#;
1310 let content: McpResourceContent = serde_json::from_str(json).unwrap();
1311 assert_eq!(content.uri, "file://test");
1312 assert_eq!(content.text, Some("hello world".to_string()));
1313 assert!(content.blob.is_none());
1314 }
1315 }
1316
1317 mod http_tests {
1318 use super::*;
1319 use std::io::{BufRead, BufReader, Read, Write};
1320 use std::net::TcpListener;
1321 use std::thread;
1322
1323 fn start_mock_http_server(
1324 responses: Vec<(String, String)>,
1325 ) -> (String, thread::JoinHandle<()>) {
1326 let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind");
1327 let addr = listener.local_addr().expect("failed to get addr");
1328 let port = addr.port();
1329
1330 let handle = thread::spawn(move || {
1331 for (_, response_body) in responses {
1332 let (mut stream, _) = listener.accept().expect("failed to accept");
1333 let mut reader = BufReader::new(&stream);
1334
1335 let mut request_body = Vec::new();
1336 loop {
1337 let mut line = String::new();
1338 reader.read_line(&mut line).expect("read line");
1339 if line == "\r\n" || line == "\n" {
1340 break;
1341 }
1342 if line.starts_with("Content-Length:") {
1343 let len: usize =
1344 line.split(':').nth(1).unwrap().trim().parse().unwrap();
1345 request_body = vec![0u8; len];
1346 }
1347 }
1348 if !request_body.is_empty() {
1349 reader.read_exact(&mut request_body).ok();
1350 }
1351
1352 let response = format!(
1353 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
1354 response_body.len(),
1355 response_body
1356 );
1357 stream.write_all(response.as_bytes()).ok();
1358 stream.flush().ok();
1359 }
1360 });
1361
1362 (format!("http://127.0.0.1:{}", port), handle)
1363 }
1364
1365 #[test]
1366 fn mcp_http_client_lists_tools() {
1367 let responses = vec![
1368 ("".to_string(), r#"{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"test","version":"1.0"}}}"#.to_string()),
1369 ("".to_string(), r#"{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"echo","description":"Echo tool","inputSchema":{"type":"object"}}]}}"#.to_string()),
1370 ];
1371
1372 let (url, handle) = start_mock_http_server(responses);
1373 let mut client = McpHttpClient::new("test-http".to_string(), url, BTreeMap::new())
1374 .expect("failed to create client");
1375
1376 assert!(client.is_initialized());
1377 let tools = client.list_tools().expect("failed to list tools");
1378 assert_eq!(tools.len(), 1);
1379 assert_eq!(tools[0].name, "echo");
1380
1381 handle.join().ok();
1382 }
1383
1384 #[test]
1385 fn mcp_http_client_calls_tool() {
1386 let responses = vec![
1387 ("".to_string(), r#"{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"test","version":"1.0"}}}"#.to_string()),
1388 ("".to_string(), r#"{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Hello"}]}}"#.to_string()),
1389 ];
1390
1391 let (url, handle) = start_mock_http_server(responses);
1392 let mut client = McpHttpClient::new("test-http".to_string(), url, BTreeMap::new())
1393 .expect("failed to create client");
1394
1395 let result = client
1396 .call_tool("echo", serde_json::json!({}))
1397 .expect("failed to call tool");
1398 assert_eq!(result, "Hello");
1399
1400 handle.join().ok();
1401 }
1402
1403 #[test]
1404 fn mcp_http_client_lists_resources() {
1405 let responses = vec![
1406 ("".to_string(), r#"{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"test","version":"1.0"}}}"#.to_string()),
1407 ("".to_string(), r#"{"jsonrpc":"2.0","id":2,"result":{"resources":[{"uri":"test://resource","name":"Test Resource","mimeType":"text/plain"}]}}"#.to_string()),
1408 ];
1409
1410 let (url, handle) = start_mock_http_server(responses);
1411 let mut client = McpHttpClient::new("test-http".to_string(), url, BTreeMap::new())
1412 .expect("failed to create client");
1413
1414 let resources = client.list_resources().expect("failed to list resources");
1415 assert_eq!(resources.len(), 1);
1416 assert_eq!(resources[0].uri, "test://resource");
1417
1418 handle.join().ok();
1419 }
1420
1421 #[test]
1422 fn mcp_http_client_reads_resource() {
1423 let responses = vec![
1424 ("".to_string(), r#"{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"test","version":"1.0"}}}"#.to_string()),
1425 ("".to_string(), r#"{"jsonrpc":"2.0","id":2,"result":{"contents":[{"uri":"test://resource","text":"content here"}]}}"#.to_string()),
1426 ];
1427
1428 let (url, handle) = start_mock_http_server(responses);
1429 let mut client = McpHttpClient::new("test-http".to_string(), url, BTreeMap::new())
1430 .expect("failed to create client");
1431
1432 let contents = client
1433 .read_resource("test://resource")
1434 .expect("failed to read resource");
1435 assert_eq!(contents.len(), 1);
1436 assert_eq!(contents[0].text.as_deref(), Some("content here"));
1437
1438 handle.join().ok();
1439 }
1440
1441 #[test]
1442 fn discover_mcp_tools_sync_http_server() {
1443 let responses = vec![
1444 ("".to_string(), r#"{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"test","version":"1.0"}}}"#.to_string()),
1445 ("".to_string(), r#"{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"http_tool","description":"HTTP tool","inputSchema":{"type":"object"}}]}}"#.to_string()),
1446 ];
1447 let (url, handle) = start_mock_http_server(responses);
1448
1449 let mut servers = BTreeMap::new();
1450 servers.insert(
1451 "http-server".to_string(),
1452 McpServerConfig::Http {
1453 r#type: "http".to_string(),
1454 url: url.clone(),
1455 headers: BTreeMap::new(),
1456 },
1457 );
1458
1459 let tools = discover_mcp_tools_sync(&servers);
1460 assert!(tools.contains_key("http-server"));
1461 let http_tools = tools.get("http-server").unwrap();
1462 assert_eq!(http_tools.len(), 1);
1463 assert_eq!(http_tools[0].name, "http_tool");
1464
1465 handle.join().ok();
1466 }
1467
1468 #[test]
1469 fn run_mcp_tool_sync_http() {
1470 let responses = vec![
1471 ("".to_string(), r#"{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"test","version":"1.0"}}}"#.to_string()),
1472 ("".to_string(), r#"{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"HTTP tool result"}]}}"#.to_string()),
1473 ];
1474 let (url, handle) = start_mock_http_server(responses);
1475
1476 let config = McpServerConfig::Http {
1477 r#type: "http".to_string(),
1478 url,
1479 headers: BTreeMap::new(),
1480 };
1481
1482 let result =
1483 run_mcp_tool_sync(&config, "http-server", "test_tool", serde_json::json!({}))
1484 .expect("failed to run tool");
1485 assert_eq!(result, "HTTP tool result");
1486
1487 handle.join().ok();
1488 }
1489
1490 #[test]
1491 fn discover_mcp_resources_sync_http_server() {
1492 let responses = vec![
1493 ("".to_string(), r#"{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"test","version":"1.0"}}}"#.to_string()),
1494 ("".to_string(), r#"{"jsonrpc":"2.0","id":2,"result":{"resources":[{"uri":"http://resource","name":"HTTP Resource"}]}}"#.to_string()),
1495 ];
1496 let (url, handle) = start_mock_http_server(responses);
1497
1498 let mut servers = BTreeMap::new();
1499 servers.insert(
1500 "http-server".to_string(),
1501 McpServerConfig::Http {
1502 r#type: "http".to_string(),
1503 url: url.clone(),
1504 headers: BTreeMap::new(),
1505 },
1506 );
1507
1508 let resources = discover_mcp_resources_sync(&servers);
1509 assert!(resources.contains_key("http-server"));
1510 let http_resources = resources.get("http-server").unwrap();
1511 assert_eq!(http_resources.len(), 1);
1512 assert_eq!(http_resources[0].uri, "http://resource");
1513
1514 handle.join().ok();
1515 }
1516
1517 #[test]
1518 fn read_mcp_resource_sync_http() {
1519 let responses = vec![
1520 ("".to_string(), r#"{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"test","version":"1.0"}}}"#.to_string()),
1521 ("".to_string(), r#"{"jsonrpc":"2.0","id":2,"result":{"contents":[{"uri":"http://resource","text":"HTTP resource content"}]}}"#.to_string()),
1522 ];
1523 let (url, handle) = start_mock_http_server(responses);
1524
1525 let config = McpServerConfig::Http {
1526 r#type: "http".to_string(),
1527 url,
1528 headers: BTreeMap::new(),
1529 };
1530
1531 let contents = read_mcp_resource_sync(&config, "http-server", "http://resource")
1532 .expect("failed to read resource");
1533 assert_eq!(contents.len(), 1);
1534 assert_eq!(contents[0].text.as_deref(), Some("HTTP resource content"));
1535
1536 handle.join().ok();
1537 }
1538 }
1539}