1use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(tag = "type", rename_all = "snake_case")]
44#[non_exhaustive]
45pub enum ToolConfig {
46 Bash {},
48 Filesystem {},
50 WebSearch {},
52 WebFetch {},
54 CodeExecution {},
56 Custom {
58 name: String,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 description: Option<String>,
63 input_schema: serde_json::Value,
65 },
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case")]
105pub struct McpServerConfig {
106 pub name: String,
108 pub transport: String,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub command: Option<String>,
113 #[serde(default, skip_serializing_if = "Vec::is_empty")]
115 pub args: Vec<String>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub url: Option<String>,
119 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
121 pub env: HashMap<String, String>,
122 #[serde(default, skip_serializing_if = "Vec::is_empty")]
124 pub auto_approve: Vec<String>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct SkillRef {
149 pub skill_id: String,
151}
152
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct PermissionPolicy {
188 pub default: PermissionMode,
190 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
192 pub tools: HashMap<String, PermissionMode>,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
200#[serde(rename_all = "snake_case")]
201pub enum PermissionMode {
202 AutoApprove,
204 Prompt,
206 Deny,
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use serde_json::json;
214
215 #[test]
218 fn test_bash_tool_serialization() {
219 let tool = ToolConfig::Bash {};
220 let serialized = serde_json::to_value(&tool).unwrap();
221 assert_eq!(serialized, json!({"type": "bash"}));
222
223 let deserialized: ToolConfig = serde_json::from_value(serialized).unwrap();
224 assert!(matches!(deserialized, ToolConfig::Bash {}));
225 }
226
227 #[test]
228 fn test_filesystem_tool_serialization() {
229 let tool = ToolConfig::Filesystem {};
230 let serialized = serde_json::to_value(&tool).unwrap();
231 assert_eq!(serialized, json!({"type": "filesystem"}));
232
233 let deserialized: ToolConfig = serde_json::from_value(serialized).unwrap();
234 assert!(matches!(deserialized, ToolConfig::Filesystem {}));
235 }
236
237 #[test]
238 fn test_web_search_tool_serialization() {
239 let tool = ToolConfig::WebSearch {};
240 let serialized = serde_json::to_value(&tool).unwrap();
241 assert_eq!(serialized, json!({"type": "web_search"}));
242
243 let deserialized: ToolConfig = serde_json::from_value(serialized).unwrap();
244 assert!(matches!(deserialized, ToolConfig::WebSearch {}));
245 }
246
247 #[test]
248 fn test_web_fetch_tool_serialization() {
249 let tool = ToolConfig::WebFetch {};
250 let serialized = serde_json::to_value(&tool).unwrap();
251 assert_eq!(serialized, json!({"type": "web_fetch"}));
252
253 let deserialized: ToolConfig = serde_json::from_value(serialized).unwrap();
254 assert!(matches!(deserialized, ToolConfig::WebFetch {}));
255 }
256
257 #[test]
258 fn test_code_execution_tool_serialization() {
259 let tool = ToolConfig::CodeExecution {};
260 let serialized = serde_json::to_value(&tool).unwrap();
261 assert_eq!(serialized, json!({"type": "code_execution"}));
262
263 let deserialized: ToolConfig = serde_json::from_value(serialized).unwrap();
264 assert!(matches!(deserialized, ToolConfig::CodeExecution {}));
265 }
266
267 #[test]
268 fn test_custom_tool_with_description_serialization() {
269 let schema = json!({
270 "type": "object",
271 "properties": {
272 "city": {"type": "string"}
273 },
274 "required": ["city"]
275 });
276
277 let tool = ToolConfig::Custom {
278 name: "get_weather".to_string(),
279 description: Some("Get the current weather for a city".to_string()),
280 input_schema: schema.clone(),
281 };
282
283 let serialized = serde_json::to_value(&tool).unwrap();
284 assert_eq!(serialized["type"], "custom");
285 assert_eq!(serialized["name"], "get_weather");
286 assert_eq!(serialized["description"], "Get the current weather for a city");
287 assert_eq!(serialized["input_schema"], schema);
288
289 let deserialized: ToolConfig = serde_json::from_value(serialized).unwrap();
290 match deserialized {
291 ToolConfig::Custom { name, description, input_schema } => {
292 assert_eq!(name, "get_weather");
293 assert_eq!(description, Some("Get the current weather for a city".to_string()));
294 assert_eq!(input_schema, schema);
295 }
296 _ => panic!("Expected Custom variant"),
297 }
298 }
299
300 #[test]
301 fn test_custom_tool_without_description_serialization() {
302 let schema = json!({"type": "object"});
303
304 let tool = ToolConfig::Custom {
305 name: "my_tool".to_string(),
306 description: None,
307 input_schema: schema.clone(),
308 };
309
310 let serialized = serde_json::to_value(&tool).unwrap();
311 assert_eq!(serialized["type"], "custom");
312 assert_eq!(serialized["name"], "my_tool");
313 assert!(serialized.get("description").is_none());
315 assert_eq!(serialized["input_schema"], schema);
316
317 let deserialized: ToolConfig = serde_json::from_value(serialized).unwrap();
318 match deserialized {
319 ToolConfig::Custom { name, description, .. } => {
320 assert_eq!(name, "my_tool");
321 assert_eq!(description, None);
322 }
323 _ => panic!("Expected Custom variant"),
324 }
325 }
326
327 #[test]
328 fn test_tool_config_vec_round_trip() {
329 let tools = vec![
330 ToolConfig::Bash {},
331 ToolConfig::WebSearch {},
332 ToolConfig::Custom {
333 name: "deploy".to_string(),
334 description: Some("Deploy the app".to_string()),
335 input_schema: json!({"type": "object", "properties": {"env": {"type": "string"}}}),
336 },
337 ];
338
339 let serialized = serde_json::to_value(&tools).unwrap();
340 let deserialized: Vec<ToolConfig> = serde_json::from_value(serialized).unwrap();
341 assert_eq!(deserialized.len(), 3);
342 assert!(matches!(deserialized[0], ToolConfig::Bash {}));
343 assert!(matches!(deserialized[1], ToolConfig::WebSearch {}));
344 assert!(matches!(deserialized[2], ToolConfig::Custom { .. }));
345 }
346
347 #[test]
348 fn test_unknown_tool_type_rejected() {
349 let json_str = r#"{"type": "unknown_tool"}"#;
350 let result: Result<ToolConfig, _> = serde_json::from_str(json_str);
351 assert!(result.is_err(), "Unknown tool type should be rejected");
352 }
353
354 #[test]
357 fn test_mcp_server_stdio_serialization() {
358 let config = McpServerConfig {
359 name: "filesystem".to_string(),
360 transport: "stdio".to_string(),
361 command: Some("npx".to_string()),
362 args: vec!["-y".to_string(), "@modelcontextprotocol/server-filesystem".to_string()],
363 url: None,
364 env: HashMap::from([("HOME".to_string(), "/tmp".to_string())]),
365 auto_approve: vec!["read_file".to_string(), "list_dir".to_string()],
366 };
367
368 let serialized = serde_json::to_value(&config).unwrap();
369 assert_eq!(serialized["name"], "filesystem");
370 assert_eq!(serialized["transport"], "stdio");
371 assert_eq!(serialized["command"], "npx");
372 assert_eq!(serialized["args"], json!(["-y", "@modelcontextprotocol/server-filesystem"]));
373 assert!(serialized.get("url").is_none());
374 assert_eq!(serialized["env"]["HOME"], "/tmp");
375 assert_eq!(serialized["auto_approve"], json!(["read_file", "list_dir"]));
376
377 let deserialized: McpServerConfig = serde_json::from_value(serialized).unwrap();
378 assert_eq!(deserialized.name, "filesystem");
379 assert_eq!(deserialized.transport, "stdio");
380 assert_eq!(deserialized.command, Some("npx".to_string()));
381 assert_eq!(deserialized.args.len(), 2);
382 assert_eq!(deserialized.url, None);
383 assert_eq!(deserialized.env.get("HOME").unwrap(), "/tmp");
384 assert_eq!(deserialized.auto_approve.len(), 2);
385 }
386
387 #[test]
388 fn test_mcp_server_sse_serialization() {
389 let config = McpServerConfig {
390 name: "remote-tools".to_string(),
391 transport: "sse".to_string(),
392 command: None,
393 args: vec![],
394 url: Some("https://mcp.example.com/sse".to_string()),
395 env: HashMap::new(),
396 auto_approve: vec![],
397 };
398
399 let serialized = serde_json::to_value(&config).unwrap();
400 assert_eq!(serialized["name"], "remote-tools");
401 assert_eq!(serialized["transport"], "sse");
402 assert!(serialized.get("command").is_none());
404 assert!(serialized.get("args").is_none());
405 assert_eq!(serialized["url"], "https://mcp.example.com/sse");
406 assert!(serialized.get("env").is_none());
407 assert!(serialized.get("auto_approve").is_none());
408
409 let deserialized: McpServerConfig = serde_json::from_value(serialized).unwrap();
410 assert_eq!(deserialized.name, "remote-tools");
411 assert_eq!(deserialized.transport, "sse");
412 assert_eq!(deserialized.command, None);
413 assert!(deserialized.args.is_empty());
414 assert_eq!(deserialized.url, Some("https://mcp.example.com/sse".to_string()));
415 assert!(deserialized.env.is_empty());
416 assert!(deserialized.auto_approve.is_empty());
417 }
418
419 #[test]
420 fn test_mcp_server_from_json_string() {
421 let json_str = r#"{
422 "name": "my-server",
423 "transport": "stdio",
424 "command": "node",
425 "args": ["server.js"],
426 "env": {"PORT": "3000"}
427 }"#;
428
429 let config: McpServerConfig = serde_json::from_str(json_str).unwrap();
430 assert_eq!(config.name, "my-server");
431 assert_eq!(config.transport, "stdio");
432 assert_eq!(config.command, Some("node".to_string()));
433 assert_eq!(config.args, vec!["server.js"]);
434 assert_eq!(config.env.get("PORT").unwrap(), "3000");
435 assert!(config.auto_approve.is_empty());
436 }
437
438 #[test]
441 fn test_skill_ref_serialization() {
442 let skill = SkillRef { skill_id: "code-review-v2".to_string() };
443
444 let serialized = serde_json::to_value(&skill).unwrap();
445 assert_eq!(serialized, json!({"skill_id": "code-review-v2"}));
446
447 let deserialized: SkillRef = serde_json::from_value(serialized).unwrap();
448 assert_eq!(deserialized.skill_id, "code-review-v2");
449 }
450
451 #[test]
452 fn test_skill_ref_vec_round_trip() {
453 let skills = vec![
454 SkillRef { skill_id: "code-review-v2".to_string() },
455 SkillRef { skill_id: "testing-assistant".to_string() },
456 ];
457
458 let serialized = serde_json::to_value(&skills).unwrap();
459 let deserialized: Vec<SkillRef> = serde_json::from_value(serialized).unwrap();
460 assert_eq!(deserialized.len(), 2);
461 assert_eq!(deserialized[0].skill_id, "code-review-v2");
462 assert_eq!(deserialized[1].skill_id, "testing-assistant");
463 }
464
465 #[test]
468 fn test_permission_mode_auto_approve_serialization() {
469 let mode = PermissionMode::AutoApprove;
470 let serialized = serde_json::to_value(mode).unwrap();
471 assert_eq!(serialized, json!("auto_approve"));
472
473 let deserialized: PermissionMode = serde_json::from_value(serialized).unwrap();
474 assert_eq!(deserialized, PermissionMode::AutoApprove);
475 }
476
477 #[test]
478 fn test_permission_mode_prompt_serialization() {
479 let mode = PermissionMode::Prompt;
480 let serialized = serde_json::to_value(mode).unwrap();
481 assert_eq!(serialized, json!("prompt"));
482
483 let deserialized: PermissionMode = serde_json::from_value(serialized).unwrap();
484 assert_eq!(deserialized, PermissionMode::Prompt);
485 }
486
487 #[test]
488 fn test_permission_mode_deny_serialization() {
489 let mode = PermissionMode::Deny;
490 let serialized = serde_json::to_value(mode).unwrap();
491 assert_eq!(serialized, json!("deny"));
492
493 let deserialized: PermissionMode = serde_json::from_value(serialized).unwrap();
494 assert_eq!(deserialized, PermissionMode::Deny);
495 }
496
497 #[test]
500 fn test_permission_policy_with_overrides_serialization() {
501 let policy = PermissionPolicy {
502 default: PermissionMode::Prompt,
503 tools: HashMap::from([
504 ("read_file".to_string(), PermissionMode::AutoApprove),
505 ("delete_file".to_string(), PermissionMode::Deny),
506 ]),
507 };
508
509 let serialized = serde_json::to_value(&policy).unwrap();
510 assert_eq!(serialized["default"], "prompt");
511 assert_eq!(serialized["tools"]["read_file"], "auto_approve");
512 assert_eq!(serialized["tools"]["delete_file"], "deny");
513
514 let deserialized: PermissionPolicy = serde_json::from_value(serialized).unwrap();
515 assert_eq!(deserialized.default, PermissionMode::Prompt);
516 assert_eq!(deserialized.tools.get("read_file"), Some(&PermissionMode::AutoApprove));
517 assert_eq!(deserialized.tools.get("delete_file"), Some(&PermissionMode::Deny));
518 }
519
520 #[test]
521 fn test_permission_policy_without_overrides_serialization() {
522 let policy =
523 PermissionPolicy { default: PermissionMode::AutoApprove, tools: HashMap::new() };
524
525 let serialized = serde_json::to_value(&policy).unwrap();
526 assert_eq!(serialized["default"], "auto_approve");
527 assert!(serialized.get("tools").is_none());
529
530 let deserialized: PermissionPolicy = serde_json::from_value(serialized).unwrap();
531 assert_eq!(deserialized.default, PermissionMode::AutoApprove);
532 assert!(deserialized.tools.is_empty());
533 }
534
535 #[test]
536 fn test_permission_policy_from_json_string() {
537 let json_str = r#"{
538 "default": "deny",
539 "tools": {
540 "read_file": "auto_approve",
541 "write_file": "prompt"
542 }
543 }"#;
544
545 let policy: PermissionPolicy = serde_json::from_str(json_str).unwrap();
546 assert_eq!(policy.default, PermissionMode::Deny);
547 assert_eq!(policy.tools.len(), 2);
548 assert_eq!(policy.tools.get("read_file"), Some(&PermissionMode::AutoApprove));
549 assert_eq!(policy.tools.get("write_file"), Some(&PermissionMode::Prompt));
550 }
551
552 #[test]
553 fn test_permission_policy_default_only_from_json() {
554 let json_str = r#"{"default": "auto_approve"}"#;
555 let policy: PermissionPolicy = serde_json::from_str(json_str).unwrap();
556 assert_eq!(policy.default, PermissionMode::AutoApprove);
557 assert!(policy.tools.is_empty());
558 }
559
560 #[test]
563 fn test_canon_tool_config_wire_shape() {
564 let tools_json = json!([
566 {"type": "bash"},
567 {"type": "filesystem"},
568 {"type": "web_search"},
569 {"type": "web_fetch"},
570 {"type": "code_execution"},
571 {
572 "type": "custom",
573 "name": "get_weather",
574 "description": "Get weather for a location",
575 "input_schema": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
576 }
577 ]);
578
579 let tools: Vec<ToolConfig> = serde_json::from_value(tools_json.clone()).unwrap();
580 assert_eq!(tools.len(), 6);
581
582 let reserialized = serde_json::to_value(&tools).unwrap();
584 assert_eq!(reserialized, tools_json);
585 }
586
587 #[test]
588 fn test_canon_mcp_server_wire_shape() {
589 let mcp_json = json!({
591 "name": "my-mcp-server",
592 "transport": "stdio",
593 "command": "npx",
594 "args": ["-y", "@modelcontextprotocol/server-filesystem"],
595 "env": {"HOME": "/tmp"},
596 "auto_approve": ["read_file", "list_dir"]
597 });
598
599 let config: McpServerConfig = serde_json::from_value(mcp_json.clone()).unwrap();
600 let reserialized = serde_json::to_value(&config).unwrap();
601 assert_eq!(reserialized, mcp_json);
602 }
603
604 #[test]
605 fn test_canon_permission_policy_wire_shape() {
606 let policy_json = json!({
608 "default": "prompt",
609 "tools": {
610 "read_file": "auto_approve",
611 "delete_file": "deny"
612 }
613 });
614
615 let policy: PermissionPolicy = serde_json::from_value(policy_json.clone()).unwrap();
616 let reserialized = serde_json::to_value(&policy).unwrap();
617
618 assert_eq!(reserialized["default"], policy_json["default"]);
620 assert_eq!(reserialized["tools"]["read_file"], policy_json["tools"]["read_file"]);
621 assert_eq!(reserialized["tools"]["delete_file"], policy_json["tools"]["delete_file"]);
622 }
623
624 #[test]
625 fn test_debug_and_clone_impls() {
626 let tool = ToolConfig::Custom {
627 name: "test".to_string(),
628 description: None,
629 input_schema: json!({}),
630 };
631 let debug_str = format!("{tool:?}");
632 assert!(debug_str.contains("Custom"));
633
634 let cloned = tool.clone();
635 let original_json = serde_json::to_value(&tool).unwrap();
636 let cloned_json = serde_json::to_value(&cloned).unwrap();
637 assert_eq!(original_json, cloned_json);
638
639 let mode = PermissionMode::Prompt;
640 let mode_clone = mode;
641 assert_eq!(mode, mode_clone);
642 assert_eq!(format!("{mode:?}"), "Prompt");
643 }
644}