1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(tag = "transport", rename_all = "snake_case")]
7pub enum McpTransport {
8 Stdio {
10 command: String,
11 args: Vec<String>,
12 #[serde(default)]
13 env: Option<HashMap<String, String>>,
14 },
15 Sse { url: String },
17 Http {
19 url: String,
20 #[serde(default)]
21 headers: Option<HashMap<String, String>>,
22 },
23 Sdk {
25 #[serde(skip)]
26 instance: Option<()>, },
28}
29
30#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct McpToolResult {
52 pub content: Vec<ToolContent>,
53 #[serde(default)]
54 pub is_error: bool,
55}
56
57#[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}