Skip to main content

clawedcode_mcp/
lib.rs

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(&notif)?;
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}