1use crate::engine::scanner::{Scanner, ScannerConfig};
2use crate::engine::scanners::SkillScanner;
3use crate::error::Result;
4use crate::fix::AutoFixer;
5use crate::rules::{Finding, RuleEngine, ScanResult, Summary};
6use crate::scoring::RiskScore;
7use serde::{Deserialize, Serialize};
8use serde_json::{Value, json};
9use std::io::{BufRead, BufReader, Write};
10use std::path::PathBuf;
11
12pub struct McpServer {
15 rule_engine: RuleEngine,
16}
17
18#[derive(Debug, Serialize, Deserialize)]
19struct JsonRpcRequest {
20 jsonrpc: String,
21 id: Option<Value>,
22 method: String,
23 params: Option<Value>,
24}
25
26#[derive(Debug, Serialize)]
27struct JsonRpcResponse {
28 jsonrpc: String,
29 id: Option<Value>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 result: Option<Value>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 error: Option<JsonRpcError>,
34}
35
36#[derive(Debug, Serialize)]
37struct JsonRpcError {
38 code: i32,
39 message: String,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 data: Option<Value>,
42}
43
44#[derive(Debug, Serialize)]
46struct Tool {
47 name: String,
48 description: String,
49 #[serde(rename = "inputSchema")]
50 input_schema: Value,
51}
52
53impl McpServer {
54 pub fn new() -> Self {
55 Self {
56 rule_engine: RuleEngine::new(),
57 }
58 }
59
60 pub fn run(&self) -> Result<()> {
62 let stdin = std::io::stdin();
63 let mut stdout = std::io::stdout();
64 let reader = BufReader::new(stdin.lock());
65
66 eprintln!("cc-audit MCP server started");
67
68 for line in reader.lines() {
69 let line = match line {
70 Ok(l) => l,
71 Err(e) => {
72 eprintln!("Error reading input: {}", e);
73 continue;
74 }
75 };
76
77 if line.is_empty() {
78 continue;
79 }
80
81 let request: JsonRpcRequest = match serde_json::from_str(&line) {
82 Ok(r) => r,
83 Err(e) => {
84 let error_response = JsonRpcResponse {
85 jsonrpc: "2.0".to_string(),
86 id: None,
87 result: None,
88 error: Some(JsonRpcError {
89 code: -32700,
90 message: format!("Parse error: {}", e),
91 data: None,
92 }),
93 };
94 let json = serde_json::to_string(&error_response)
97 .unwrap_or_else(|_| r#"{"jsonrpc":"2.0","error":{"code":-32603,"message":"Internal error"}}"#.to_string());
98 let _ = writeln!(stdout, "{}", json);
99 let _ = stdout.flush();
100 continue;
101 }
102 };
103
104 let response = self.handle_request(request);
105 let json = serde_json::to_string(&response).unwrap_or_else(|_| {
108 r#"{"jsonrpc":"2.0","error":{"code":-32603,"message":"Internal error"}}"#
109 .to_string()
110 });
111 let _ = writeln!(stdout, "{}", json);
112 let _ = stdout.flush();
113 }
114
115 Ok(())
116 }
117
118 fn handle_request(&self, request: JsonRpcRequest) -> JsonRpcResponse {
119 let result = match request.method.as_str() {
120 "initialize" => self.handle_initialize(&request.params),
121 "tools/list" => self.handle_list_tools(),
122 "tools/call" => self.handle_tool_call(&request.params),
123 "shutdown" => {
124 eprintln!("MCP server shutting down");
125 Ok(json!({}))
126 }
127 _ => Err(JsonRpcError {
128 code: -32601,
129 message: format!("Method not found: {}", request.method),
130 data: None,
131 }),
132 };
133
134 match result {
135 Ok(value) => JsonRpcResponse {
136 jsonrpc: "2.0".to_string(),
137 id: request.id,
138 result: Some(value),
139 error: None,
140 },
141 Err(error) => JsonRpcResponse {
142 jsonrpc: "2.0".to_string(),
143 id: request.id,
144 result: None,
145 error: Some(error),
146 },
147 }
148 }
149
150 fn handle_initialize(
151 &self,
152 _params: &Option<Value>,
153 ) -> std::result::Result<Value, JsonRpcError> {
154 Ok(json!({
155 "protocolVersion": "2024-11-05",
156 "capabilities": {
157 "tools": {}
158 },
159 "serverInfo": {
160 "name": "cc-audit",
161 "version": env!("CARGO_PKG_VERSION")
162 }
163 }))
164 }
165
166 fn handle_list_tools(&self) -> std::result::Result<Value, JsonRpcError> {
167 let tools = vec![
168 Tool {
169 name: "scan".to_string(),
170 description: "Scan a file or directory for security issues".to_string(),
171 input_schema: json!({
172 "type": "object",
173 "properties": {
174 "path": {
175 "type": "string",
176 "description": "Path to scan (file or directory)"
177 }
178 },
179 "required": ["path"]
180 }),
181 },
182 Tool {
183 name: "scan_content".to_string(),
184 description: "Scan content string for security issues".to_string(),
185 input_schema: json!({
186 "type": "object",
187 "properties": {
188 "content": {
189 "type": "string",
190 "description": "Content to scan"
191 },
192 "filename": {
193 "type": "string",
194 "description": "Virtual filename for context"
195 }
196 },
197 "required": ["content"]
198 }),
199 },
200 Tool {
201 name: "check_rule".to_string(),
202 description: "Check if content matches a specific rule".to_string(),
203 input_schema: json!({
204 "type": "object",
205 "properties": {
206 "rule_id": {
207 "type": "string",
208 "description": "Rule ID to check (e.g., 'OP-001')"
209 },
210 "content": {
211 "type": "string",
212 "description": "Content to check"
213 }
214 },
215 "required": ["rule_id", "content"]
216 }),
217 },
218 Tool {
219 name: "list_rules".to_string(),
220 description: "List all available security rules".to_string(),
221 input_schema: json!({
222 "type": "object",
223 "properties": {
224 "category": {
225 "type": "string",
226 "description": "Filter by category (optional)"
227 }
228 }
229 }),
230 },
231 Tool {
232 name: "get_fix_suggestion".to_string(),
233 description: "Get a fix suggestion for a finding".to_string(),
234 input_schema: json!({
235 "type": "object",
236 "properties": {
237 "finding_id": {
238 "type": "string",
239 "description": "Finding ID (rule ID)"
240 },
241 "code": {
242 "type": "string",
243 "description": "The problematic code"
244 }
245 },
246 "required": ["finding_id", "code"]
247 }),
248 },
249 ];
250
251 Ok(json!({ "tools": tools }))
252 }
253
254 fn handle_tool_call(&self, params: &Option<Value>) -> std::result::Result<Value, JsonRpcError> {
255 let params = params.as_ref().ok_or_else(|| JsonRpcError {
256 code: -32602,
257 message: "Missing params".to_string(),
258 data: None,
259 })?;
260
261 let name = params
262 .get("name")
263 .and_then(|v| v.as_str())
264 .ok_or_else(|| JsonRpcError {
265 code: -32602,
266 message: "Missing tool name".to_string(),
267 data: None,
268 })?;
269
270 let arguments = params.get("arguments").cloned().unwrap_or(json!({}));
271
272 match name {
273 "scan" => self.tool_scan(&arguments),
274 "scan_content" => self.tool_scan_content(&arguments),
275 "check_rule" => self.tool_check_rule(&arguments),
276 "list_rules" => self.tool_list_rules(&arguments),
277 "get_fix_suggestion" => self.tool_get_fix_suggestion(&arguments),
278 _ => Err(JsonRpcError {
279 code: -32602,
280 message: format!("Unknown tool: {}", name),
281 data: None,
282 }),
283 }
284 }
285
286 fn tool_scan(&self, args: &Value) -> std::result::Result<Value, JsonRpcError> {
287 let path = args
288 .get("path")
289 .and_then(|v| v.as_str())
290 .ok_or_else(|| JsonRpcError {
291 code: -32602,
292 message: "Missing 'path' argument".to_string(),
293 data: None,
294 })?;
295
296 let path = PathBuf::from(path);
297 let scanner = SkillScanner::new();
298
299 match scanner.scan_path(&path) {
300 Ok(findings) => {
301 let summary = Summary::from_findings(&findings);
302 let risk_score = RiskScore::from_findings(&findings);
303 let result = ScanResult {
304 version: env!("CARGO_PKG_VERSION").to_string(),
305 scanned_at: chrono::Utc::now().to_rfc3339(),
306 target: path.display().to_string(),
307 summary,
308 findings,
309 risk_score: Some(risk_score),
310 };
311 Ok(json!({
312 "content": [{
313 "type": "text",
314 "text": serde_json::to_string_pretty(&result).unwrap()
315 }]
316 }))
317 }
318 Err(e) => Err(JsonRpcError {
319 code: -32000,
320 message: format!("Scan failed: {}", e),
321 data: None,
322 }),
323 }
324 }
325
326 fn tool_scan_content(&self, args: &Value) -> std::result::Result<Value, JsonRpcError> {
327 let content = args
328 .get("content")
329 .and_then(|v| v.as_str())
330 .ok_or_else(|| JsonRpcError {
331 code: -32602,
332 message: "Missing 'content' argument".to_string(),
333 data: None,
334 })?;
335
336 let filename = args
337 .get("filename")
338 .and_then(|v| v.as_str())
339 .unwrap_or("content.md");
340
341 let config = ScannerConfig::new();
342 let findings = config.check_content(content, filename);
343
344 let summary = Summary::from_findings(&findings);
345 let risk_score = RiskScore::from_findings(&findings);
346
347 Ok(json!({
348 "content": [{
349 "type": "text",
350 "text": serde_json::to_string_pretty(&json!({
351 "findings": findings,
352 "summary": summary,
353 "risk_score": risk_score
354 })).unwrap()
355 }]
356 }))
357 }
358
359 fn tool_check_rule(&self, args: &Value) -> std::result::Result<Value, JsonRpcError> {
360 let rule_id = args
361 .get("rule_id")
362 .and_then(|v| v.as_str())
363 .ok_or_else(|| JsonRpcError {
364 code: -32602,
365 message: "Missing 'rule_id' argument".to_string(),
366 data: None,
367 })?;
368
369 let content = args
370 .get("content")
371 .and_then(|v| v.as_str())
372 .ok_or_else(|| JsonRpcError {
373 code: -32602,
374 message: "Missing 'content' argument".to_string(),
375 data: None,
376 })?;
377
378 let rule = self.rule_engine.get_rule(rule_id);
380 if rule.is_none() {
381 return Ok(json!({
382 "content": [{
383 "type": "text",
384 "text": format!("Rule '{}' not found", rule_id)
385 }]
386 }));
387 }
388
389 let rule = rule.unwrap();
390
391 let mut matches = false;
393 for pattern in &rule.patterns {
394 if pattern.is_match(content) {
395 matches = true;
396 break;
397 }
398 }
399
400 Ok(json!({
401 "content": [{
402 "type": "text",
403 "text": serde_json::to_string_pretty(&json!({
404 "rule_id": rule_id,
405 "rule_name": rule.name,
406 "severity": format!("{:?}", rule.severity),
407 "matches": matches,
408 "message": if matches {
409 format!("Content matches rule: {}", rule.message)
410 } else {
411 "No match found".to_string()
412 }
413 })).unwrap()
414 }]
415 }))
416 }
417
418 fn tool_list_rules(&self, args: &Value) -> std::result::Result<Value, JsonRpcError> {
419 let category_filter = args
420 .get("category")
421 .and_then(|v| v.as_str())
422 .map(|s| s.to_lowercase());
423
424 let rules = self.rule_engine.get_all_rules();
425 let filtered: Vec<_> = rules
426 .iter()
427 .filter(|r| {
428 if let Some(ref cat) = category_filter {
429 format!("{:?}", r.category).to_lowercase().contains(cat)
430 } else {
431 true
432 }
433 })
434 .map(|r| {
435 json!({
436 "id": r.id,
437 "name": r.name,
438 "severity": format!("{:?}", r.severity),
439 "category": format!("{:?}", r.category),
440 "confidence": format!("{:?}", r.confidence)
441 })
442 })
443 .collect();
444
445 Ok(json!({
446 "content": [{
447 "type": "text",
448 "text": serde_json::to_string_pretty(&json!({
449 "total": filtered.len(),
450 "rules": filtered
451 })).unwrap()
452 }]
453 }))
454 }
455
456 fn tool_get_fix_suggestion(&self, args: &Value) -> std::result::Result<Value, JsonRpcError> {
457 let finding_id = args
458 .get("finding_id")
459 .and_then(|v| v.as_str())
460 .ok_or_else(|| JsonRpcError {
461 code: -32602,
462 message: "Missing 'finding_id' argument".to_string(),
463 data: None,
464 })?;
465
466 let code = args
467 .get("code")
468 .and_then(|v| v.as_str())
469 .ok_or_else(|| JsonRpcError {
470 code: -32602,
471 message: "Missing 'code' argument".to_string(),
472 data: None,
473 })?;
474
475 let rule = self.rule_engine.get_rule(finding_id);
477 if rule.is_none() {
478 return Ok(json!({
479 "content": [{
480 "type": "text",
481 "text": format!("No fix suggestion available for rule '{}'", finding_id)
482 }]
483 }));
484 }
485
486 let rule = rule.unwrap();
487 let finding = Finding {
488 id: finding_id.to_string(),
489 severity: rule.severity,
490 category: rule.category,
491 confidence: rule.confidence,
492 name: rule.name.to_string(),
493 location: crate::rules::Location {
494 file: "virtual".to_string(),
495 line: 1,
496 column: None,
497 },
498 code: code.to_string(),
499 message: rule.message.to_string(),
500 recommendation: rule.recommendation.to_string(),
501 fix_hint: rule.fix_hint.map(|s| s.to_string()),
502 cwe_ids: rule.cwe_ids.iter().map(|s| s.to_string()).collect(),
503 rule_severity: None,
504 client: None,
505 context: None,
506 };
507
508 let fixer = AutoFixer::new(true);
509 let fixes = fixer.generate_fixes(&[finding]);
510
511 if fixes.is_empty() {
512 Ok(json!({
513 "content": [{
514 "type": "text",
515 "text": format!("No automatic fix available for {}. Manual review recommended.\n\nRecommendation: {}", finding_id, rule.recommendation)
516 }]
517 }))
518 } else {
519 let fix = &fixes[0];
520 Ok(json!({
521 "content": [{
522 "type": "text",
523 "text": serde_json::to_string_pretty(&json!({
524 "has_fix": true,
525 "description": fix.description,
526 "original": fix.original,
527 "replacement": fix.replacement
528 })).unwrap()
529 }]
530 }))
531 }
532 }
533}
534
535impl Default for McpServer {
536 fn default() -> Self {
537 Self::new()
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544 use tempfile::TempDir;
545
546 #[test]
547 fn test_mcp_server_new() {
548 let server = McpServer::new();
549 assert!(!server.rule_engine.get_all_rules().is_empty());
550 }
551
552 #[test]
553 fn test_mcp_server_default() {
554 let server = McpServer::default();
555 assert!(!server.rule_engine.get_all_rules().is_empty());
556 }
557
558 #[test]
559 fn test_handle_initialize() {
560 let server = McpServer::new();
561 let result = server.handle_initialize(&None).unwrap();
562
563 assert!(result.get("protocolVersion").is_some());
564 assert!(result.get("serverInfo").is_some());
565 }
566
567 #[test]
568 fn test_handle_initialize_with_params() {
569 let server = McpServer::new();
570 let params = Some(json!({"clientInfo": {"name": "test"}}));
571 let result = server.handle_initialize(¶ms).unwrap();
572
573 assert!(result.get("protocolVersion").is_some());
574 }
575
576 #[test]
577 fn test_handle_list_tools() {
578 let server = McpServer::new();
579 let result = server.handle_list_tools().unwrap();
580
581 let tools = result.get("tools").unwrap().as_array().unwrap();
582 assert_eq!(tools.len(), 5);
583
584 let tool_names: Vec<&str> = tools
585 .iter()
586 .map(|t| t.get("name").unwrap().as_str().unwrap())
587 .collect();
588 assert!(tool_names.contains(&"scan"));
589 assert!(tool_names.contains(&"scan_content"));
590 assert!(tool_names.contains(&"check_rule"));
591 assert!(tool_names.contains(&"list_rules"));
592 assert!(tool_names.contains(&"get_fix_suggestion"));
593 }
594
595 #[test]
596 fn test_tool_scan_content() {
597 let server = McpServer::new();
598 let args = json!({
599 "content": "allowed-tools: *",
600 "filename": "test.md"
601 });
602
603 let result = server.tool_scan_content(&args).unwrap();
604 let content = result.get("content").unwrap().as_array().unwrap();
605 assert!(!content.is_empty());
606 }
607
608 #[test]
609 fn test_tool_scan_content_no_filename() {
610 let server = McpServer::new();
611 let args = json!({
612 "content": "some safe content"
613 });
614
615 let result = server.tool_scan_content(&args).unwrap();
616 let content = result.get("content").unwrap().as_array().unwrap();
617 assert!(!content.is_empty());
618 }
619
620 #[test]
621 fn test_tool_scan_content_missing_content() {
622 let server = McpServer::new();
623 let args = json!({});
624
625 let result = server.tool_scan_content(&args);
626 assert!(result.is_err());
627 }
628
629 #[test]
630 fn test_tool_list_rules() {
631 let server = McpServer::new();
632 let args = json!({});
633
634 let result = server.tool_list_rules(&args).unwrap();
635 let content = result.get("content").unwrap().as_array().unwrap();
636 assert!(!content.is_empty());
637 }
638
639 #[test]
640 fn test_tool_list_rules_with_category() {
641 let server = McpServer::new();
642 let args = json!({"category": "exfiltration"});
643
644 let result = server.tool_list_rules(&args).unwrap();
645 let content = result.get("content").unwrap().as_array().unwrap();
646 assert!(!content.is_empty());
647 }
648
649 #[test]
650 fn test_tool_check_rule() {
651 let server = McpServer::new();
652 let args = json!({
653 "rule_id": "OP-001",
654 "content": "allowed-tools: *"
655 });
656
657 let result = server.tool_check_rule(&args).unwrap();
658 let content = result.get("content").unwrap().as_array().unwrap();
659 assert!(!content.is_empty());
660 }
661
662 #[test]
663 fn test_tool_check_rule_no_match() {
664 let server = McpServer::new();
665 let args = json!({
666 "rule_id": "OP-001",
667 "content": "allowed-tools: Read, Write"
668 });
669
670 let result = server.tool_check_rule(&args).unwrap();
671 let content = result.get("content").unwrap().as_array().unwrap();
672 let text = content[0].get("text").unwrap().as_str().unwrap();
673 assert!(text.contains("No match found") || text.contains("matches"));
674 }
675
676 #[test]
677 fn test_tool_check_rule_not_found() {
678 let server = McpServer::new();
679 let args = json!({
680 "rule_id": "NONEXISTENT-001",
681 "content": "some content"
682 });
683
684 let result = server.tool_check_rule(&args).unwrap();
685 let content = result.get("content").unwrap().as_array().unwrap();
686 let text = content[0].get("text").unwrap().as_str().unwrap();
687 assert!(text.contains("not found"));
688 }
689
690 #[test]
691 fn test_tool_check_rule_missing_rule_id() {
692 let server = McpServer::new();
693 let args = json!({
694 "content": "some content"
695 });
696
697 let result = server.tool_check_rule(&args);
698 assert!(result.is_err());
699 }
700
701 #[test]
702 fn test_tool_check_rule_missing_content() {
703 let server = McpServer::new();
704 let args = json!({
705 "rule_id": "OP-001"
706 });
707
708 let result = server.tool_check_rule(&args);
709 assert!(result.is_err());
710 }
711
712 #[test]
713 fn test_tool_scan_valid_path() {
714 let temp_dir = TempDir::new().unwrap();
715 let test_file = temp_dir.path().join("SKILL.md");
716 std::fs::write(&test_file, "---\nallowed-tools: *\n---\n").unwrap();
717
718 let server = McpServer::new();
719 let args = json!({"path": test_file.display().to_string()});
720
721 let result = server.tool_scan(&args).unwrap();
722 let content = result.get("content").unwrap().as_array().unwrap();
723 assert!(!content.is_empty());
724 }
725
726 #[test]
727 fn test_tool_scan_invalid_path() {
728 let server = McpServer::new();
729 let args = json!({"path": "/nonexistent/path/that/does/not/exist"});
730
731 let result = server.tool_scan(&args);
732 assert!(result.is_err());
733 }
734
735 #[test]
736 fn test_tool_scan_missing_path() {
737 let server = McpServer::new();
738 let args = json!({});
739
740 let result = server.tool_scan(&args);
741 assert!(result.is_err());
742 }
743
744 #[test]
745 fn test_tool_get_fix_suggestion_valid() {
746 let server = McpServer::new();
747 let args = json!({
748 "finding_id": "OP-001",
749 "code": "allowed-tools: *"
750 });
751
752 let result = server.tool_get_fix_suggestion(&args).unwrap();
753 let content = result.get("content").unwrap().as_array().unwrap();
754 assert!(!content.is_empty());
755 }
756
757 #[test]
758 fn test_tool_get_fix_suggestion_no_fix_available() {
759 let server = McpServer::new();
760 let args = json!({
761 "finding_id": "EX-001",
762 "code": "echo hello"
763 });
764
765 let result = server.tool_get_fix_suggestion(&args).unwrap();
766 let content = result.get("content").unwrap().as_array().unwrap();
767 let text = content[0].get("text").unwrap().as_str().unwrap();
768 assert!(text.contains("No automatic fix") || text.contains("has_fix"));
769 }
770
771 #[test]
772 fn test_tool_get_fix_suggestion_rule_not_found() {
773 let server = McpServer::new();
774 let args = json!({
775 "finding_id": "NONEXISTENT-001",
776 "code": "some code"
777 });
778
779 let result = server.tool_get_fix_suggestion(&args).unwrap();
780 let content = result.get("content").unwrap().as_array().unwrap();
781 let text = content[0].get("text").unwrap().as_str().unwrap();
782 assert!(text.contains("No fix suggestion available"));
783 }
784
785 #[test]
786 fn test_tool_get_fix_suggestion_missing_finding_id() {
787 let server = McpServer::new();
788 let args = json!({
789 "code": "some code"
790 });
791
792 let result = server.tool_get_fix_suggestion(&args);
793 assert!(result.is_err());
794 }
795
796 #[test]
797 fn test_tool_get_fix_suggestion_missing_code() {
798 let server = McpServer::new();
799 let args = json!({
800 "finding_id": "OP-001"
801 });
802
803 let result = server.tool_get_fix_suggestion(&args);
804 assert!(result.is_err());
805 }
806
807 #[test]
808 fn test_handle_request_initialize() {
809 let server = McpServer::new();
810 let request = JsonRpcRequest {
811 jsonrpc: "2.0".to_string(),
812 id: Some(json!(1)),
813 method: "initialize".to_string(),
814 params: None,
815 };
816
817 let response = server.handle_request(request);
818 assert!(response.result.is_some());
819 assert!(response.error.is_none());
820 }
821
822 #[test]
823 fn test_handle_request_tools_list() {
824 let server = McpServer::new();
825 let request = JsonRpcRequest {
826 jsonrpc: "2.0".to_string(),
827 id: Some(json!(2)),
828 method: "tools/list".to_string(),
829 params: None,
830 };
831
832 let response = server.handle_request(request);
833 assert!(response.result.is_some());
834 assert!(response.error.is_none());
835 }
836
837 #[test]
838 fn test_handle_request_shutdown() {
839 let server = McpServer::new();
840 let request = JsonRpcRequest {
841 jsonrpc: "2.0".to_string(),
842 id: Some(json!(3)),
843 method: "shutdown".to_string(),
844 params: None,
845 };
846
847 let response = server.handle_request(request);
848 assert!(response.result.is_some());
849 assert!(response.error.is_none());
850 }
851
852 #[test]
853 fn test_handle_request_unknown_method() {
854 let server = McpServer::new();
855 let request = JsonRpcRequest {
856 jsonrpc: "2.0".to_string(),
857 id: Some(json!(4)),
858 method: "unknown/method".to_string(),
859 params: None,
860 };
861
862 let response = server.handle_request(request);
863 assert!(response.result.is_none());
864 assert!(response.error.is_some());
865 assert_eq!(response.error.as_ref().unwrap().code, -32601);
866 }
867
868 #[test]
869 fn test_handle_tool_call_missing_params() {
870 let server = McpServer::new();
871 let result = server.handle_tool_call(&None);
872 assert!(result.is_err());
873 assert_eq!(result.unwrap_err().code, -32602);
874 }
875
876 #[test]
877 fn test_handle_tool_call_missing_name() {
878 let server = McpServer::new();
879 let params = Some(json!({"arguments": {}}));
880 let result = server.handle_tool_call(¶ms);
881 assert!(result.is_err());
882 }
883
884 #[test]
885 fn test_handle_tool_call_unknown_tool() {
886 let server = McpServer::new();
887 let params = Some(json!({
888 "name": "unknown_tool",
889 "arguments": {}
890 }));
891 let result = server.handle_tool_call(¶ms);
892 assert!(result.is_err());
893 assert!(result.unwrap_err().message.contains("Unknown tool"));
894 }
895
896 #[test]
897 fn test_handle_tool_call_scan_content() {
898 let server = McpServer::new();
899 let params = Some(json!({
900 "name": "scan_content",
901 "arguments": {
902 "content": "safe content"
903 }
904 }));
905
906 let result = server.handle_tool_call(¶ms);
907 assert!(result.is_ok());
908 }
909
910 #[test]
911 fn test_handle_tool_call_list_rules() {
912 let server = McpServer::new();
913 let params = Some(json!({
914 "name": "list_rules",
915 "arguments": {}
916 }));
917
918 let result = server.handle_tool_call(¶ms);
919 assert!(result.is_ok());
920 }
921
922 #[test]
923 fn test_handle_tool_call_check_rule() {
924 let server = McpServer::new();
925 let params = Some(json!({
926 "name": "check_rule",
927 "arguments": {
928 "rule_id": "OP-001",
929 "content": "allowed-tools: *"
930 }
931 }));
932
933 let result = server.handle_tool_call(¶ms);
934 assert!(result.is_ok());
935 }
936
937 #[test]
938 fn test_handle_tool_call_get_fix_suggestion() {
939 let server = McpServer::new();
940 let params = Some(json!({
941 "name": "get_fix_suggestion",
942 "arguments": {
943 "finding_id": "OP-001",
944 "code": "allowed-tools: *"
945 }
946 }));
947
948 let result = server.handle_tool_call(¶ms);
949 assert!(result.is_ok());
950 }
951
952 #[test]
953 fn test_json_rpc_request_debug() {
954 let request = JsonRpcRequest {
955 jsonrpc: "2.0".to_string(),
956 id: Some(json!(1)),
957 method: "test".to_string(),
958 params: None,
959 };
960
961 let debug_str = format!("{:?}", request);
962 assert!(debug_str.contains("JsonRpcRequest"));
963 }
964
965 #[test]
966 fn test_json_rpc_response_serialization() {
967 let response = JsonRpcResponse {
968 jsonrpc: "2.0".to_string(),
969 id: Some(json!(1)),
970 result: Some(json!({"status": "ok"})),
971 error: None,
972 };
973
974 let json_str = serde_json::to_string(&response).unwrap();
975 assert!(json_str.contains("\"jsonrpc\":\"2.0\""));
976 assert!(!json_str.contains("error"));
977 }
978
979 #[test]
980 fn test_json_rpc_error_serialization() {
981 let response = JsonRpcResponse {
982 jsonrpc: "2.0".to_string(),
983 id: Some(json!(1)),
984 result: None,
985 error: Some(JsonRpcError {
986 code: -32600,
987 message: "Invalid request".to_string(),
988 data: None,
989 }),
990 };
991
992 let json_str = serde_json::to_string(&response).unwrap();
993 assert!(json_str.contains("error"));
994 assert!(json_str.contains("-32600"));
995 assert!(!json_str.contains("result"));
996 }
997
998 #[test]
999 fn test_json_rpc_error_with_data() {
1000 let error = JsonRpcError {
1001 code: -32000,
1002 message: "Server error".to_string(),
1003 data: Some(json!({"details": "additional info"})),
1004 };
1005
1006 let json_str = serde_json::to_string(&error).unwrap();
1007 assert!(json_str.contains("details"));
1008 }
1009
1010 #[test]
1011 fn test_tool_struct_serialization() {
1012 let tool = Tool {
1013 name: "test_tool".to_string(),
1014 description: "A test tool".to_string(),
1015 input_schema: json!({"type": "object"}),
1016 };
1017
1018 let json_str = serde_json::to_string(&tool).unwrap();
1019 assert!(json_str.contains("test_tool"));
1020 assert!(json_str.contains("inputSchema"));
1021 }
1022
1023 #[test]
1024 fn test_handle_request_tools_call_with_scan() {
1025 let temp_dir = TempDir::new().unwrap();
1026 let test_file = temp_dir.path().join("SKILL.md");
1027 std::fs::write(&test_file, "safe content").unwrap();
1028
1029 let server = McpServer::new();
1030 let request = JsonRpcRequest {
1031 jsonrpc: "2.0".to_string(),
1032 id: Some(json!(5)),
1033 method: "tools/call".to_string(),
1034 params: Some(json!({
1035 "name": "scan",
1036 "arguments": {
1037 "path": test_file.display().to_string()
1038 }
1039 })),
1040 };
1041
1042 let response = server.handle_request(request);
1043 assert!(response.result.is_some());
1044 }
1045}