Skip to main content

qwencode_rs/types/
mcp.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// MCP server transport type
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(tag = "transport", rename_all = "snake_case")]
7pub enum McpTransport {
8    /// stdio transport
9    Stdio {
10        command: String,
11        args: Vec<String>,
12        #[serde(default)]
13        env: Option<HashMap<String, String>>,
14    },
15    /// Server-Sent Events transport
16    Sse { url: String },
17    /// HTTP transport
18    Http {
19        url: String,
20        #[serde(default)]
21        headers: Option<HashMap<String, String>>,
22    },
23    /// SDK embedded server
24    Sdk {
25        #[serde(skip)]
26        instance: Option<()>, // Placeholder for SDK server instance
27    },
28}
29
30/// MCP server configuration
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct McpServerConfig {
33    pub name: String,
34    pub transport: McpTransport,
35    #[serde(default)]
36    pub timeout_ms: Option<u64>,
37    #[serde(default)]
38    pub tools: Option<Vec<McpToolDefinition>>,
39}
40
41/// MCP tool definition
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct McpToolDefinition {
44    pub name: String,
45    pub description: String,
46    pub input_schema: serde_json::Value,
47}
48
49/// MCP tool result
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct McpToolResult {
52    pub content: Vec<ToolContent>,
53    #[serde(default)]
54    pub is_error: bool,
55}
56
57/// Tool content types
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(tag = "type", rename_all = "snake_case")]
60pub enum ToolContent {
61    Text {
62        text: String,
63    },
64    Image {
65        data: String,
66        mime_type: String,
67    },
68    Resource {
69        uri: String,
70        mime_type: String,
71        text: Option<String>,
72        blob: Option<String>,
73    },
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn test_mcp_transport_stdio() {
82        let transport = McpTransport::Stdio {
83            command: "node".to_string(),
84            args: vec!["server.js".to_string()],
85            env: Some(HashMap::from([("PORT".to_string(), "3000".to_string())])),
86        };
87
88        match &transport {
89            McpTransport::Stdio { command, args, env } => {
90                assert_eq!(command, "node");
91                assert_eq!(args, &vec!["server.js"]);
92                assert!(env.is_some());
93            }
94            _ => panic!("Expected Stdio variant"),
95        }
96    }
97
98    #[test]
99    fn test_mcp_transport_sse() {
100        let transport = McpTransport::Sse {
101            url: "http://localhost:3000/sse".to_string(),
102        };
103
104        match &transport {
105            McpTransport::Sse { url } => {
106                assert_eq!(url, "http://localhost:3000/sse");
107            }
108            _ => panic!("Expected Sse variant"),
109        }
110    }
111
112    #[test]
113    fn test_mcp_transport_http() {
114        let transport = McpTransport::Http {
115            url: "http://localhost:3000/mcp".to_string(),
116            headers: Some(HashMap::from([(
117                "Authorization".to_string(),
118                "Bearer token".to_string(),
119            )])),
120        };
121
122        match &transport {
123            McpTransport::Http { url, headers } => {
124                assert_eq!(url, "http://localhost:3000/mcp");
125                assert!(headers.is_some());
126            }
127            _ => panic!("Expected Http variant"),
128        }
129    }
130
131    #[test]
132    fn test_mcp_server_config_creation() {
133        let config = McpServerConfig {
134            name: "test-server".to_string(),
135            transport: McpTransport::Stdio {
136                command: "python".to_string(),
137                args: vec!["mcp_server.py".to_string()],
138                env: None,
139            },
140            timeout_ms: Some(30000),
141            tools: None,
142        };
143
144        assert_eq!(config.name, "test-server");
145        assert_eq!(config.timeout_ms, Some(30000));
146    }
147
148    #[test]
149    fn test_mcp_tool_definition() {
150        let tool = McpToolDefinition {
151            name: "calculate".to_string(),
152            description: "Perform calculation".to_string(),
153            input_schema: serde_json::json!({
154                "type": "object",
155                "properties": {
156                    "expression": {"type": "string"}
157                }
158            }),
159        };
160
161        assert_eq!(tool.name, "calculate");
162        assert_eq!(tool.description, "Perform calculation");
163        assert!(tool.input_schema.is_object());
164    }
165
166    #[test]
167    fn test_mcp_tool_result() {
168        let result = McpToolResult {
169            content: vec![ToolContent::Text {
170                text: "42".to_string(),
171            }],
172            is_error: false,
173        };
174
175        assert_eq!(result.content.len(), 1);
176        assert!(!result.is_error);
177
178        match &result.content[0] {
179            ToolContent::Text { text } => assert_eq!(text, "42"),
180            _ => panic!("Expected Text content"),
181        }
182    }
183
184    #[test]
185    fn test_tool_content_text() {
186        let content = ToolContent::Text {
187            text: "Hello world".to_string(),
188        };
189
190        let serialized = serde_json::to_string(&content).unwrap();
191        assert!(serialized.contains("\"type\":\"text\""));
192        assert!(serialized.contains("\"text\":\"Hello world\""));
193    }
194
195    #[test]
196    fn test_tool_content_image() {
197        let content = ToolContent::Image {
198            data: "base64data".to_string(),
199            mime_type: "image/png".to_string(),
200        };
201
202        let serialized = serde_json::to_string(&content).unwrap();
203        assert!(serialized.contains("\"type\":\"image\""));
204        assert!(serialized.contains("\"mime_type\":\"image/png\""));
205    }
206
207    #[test]
208    fn test_tool_content_resource() {
209        let content = ToolContent::Resource {
210            uri: "file:///test.txt".to_string(),
211            mime_type: "text/plain".to_string(),
212            text: Some("content".to_string()),
213            blob: None,
214        };
215
216        let serialized = serde_json::to_string(&content).unwrap();
217        assert!(serialized.contains("\"type\":\"resource\""));
218        assert!(serialized.contains("\"uri\":\"file:///test.txt\""));
219    }
220
221    #[test]
222    fn test_mcp_tool_result_with_error() {
223        let result = McpToolResult {
224            content: vec![ToolContent::Text {
225                text: "Error occurred".to_string(),
226            }],
227            is_error: true,
228        };
229
230        assert!(result.is_error);
231    }
232
233    #[test]
234    fn test_mcp_tool_result_multiple_contents() {
235        let result = McpToolResult {
236            content: vec![
237                ToolContent::Text {
238                    text: "Result".to_string(),
239                },
240                ToolContent::Image {
241                    data: "img_data".to_string(),
242                    mime_type: "image/jpeg".to_string(),
243                },
244            ],
245            is_error: false,
246        };
247
248        assert_eq!(result.content.len(), 2);
249        assert!(matches!(&result.content[0], ToolContent::Text { .. }));
250        assert!(matches!(&result.content[1], ToolContent::Image { .. }));
251    }
252
253    #[test]
254    fn test_stdio_transport_without_env() {
255        let transport = McpTransport::Stdio {
256            command: "bash".to_string(),
257            args: vec!["script.sh".to_string()],
258            env: None,
259        };
260
261        match &transport {
262            McpTransport::Stdio { env, .. } => {
263                assert!(env.is_none());
264            }
265            _ => panic!("Expected Stdio variant"),
266        }
267    }
268
269    #[test]
270    fn test_http_transport_without_headers() {
271        let transport = McpTransport::Http {
272            url: "http://localhost:8080".to_string(),
273            headers: None,
274        };
275
276        match &transport {
277            McpTransport::Http { headers, .. } => {
278                assert!(headers.is_none());
279            }
280            _ => panic!("Expected Http variant"),
281        }
282    }
283}