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