1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9pub const PROTOCOL_VERSION: &str = "2024-11-05";
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct JsonRpcRequest {
15 pub jsonrpc: String,
16 pub id: u64,
17 pub method: String,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub params: Option<serde_json::Value>,
20}
21
22impl JsonRpcRequest {
23 pub fn new(id: u64, method: &str, params: Option<serde_json::Value>) -> Self {
24 Self {
25 jsonrpc: "2.0".to_string(),
26 id,
27 method: method.to_string(),
28 params,
29 }
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct JsonRpcResponse {
36 pub jsonrpc: String,
37 pub id: Option<u64>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub result: Option<serde_json::Value>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub error: Option<JsonRpcError>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct JsonRpcError {
47 pub code: i32,
48 pub message: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub data: Option<serde_json::Value>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct JsonRpcNotification {
56 pub jsonrpc: String,
57 pub method: String,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub params: Option<serde_json::Value>,
60}
61
62impl JsonRpcNotification {
63 pub fn new(method: &str, params: Option<serde_json::Value>) -> Self {
64 Self {
65 jsonrpc: "2.0".to_string(),
66 method: method.to_string(),
67 params,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78pub struct ClientCapabilities {
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub roots: Option<RootsCapability>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub sampling: Option<SamplingCapability>,
83}
84
85#[derive(Debug, Clone, Default, Serialize, Deserialize)]
86pub struct RootsCapability {
87 #[serde(default)]
88 pub list_changed: bool,
89}
90
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct SamplingCapability {}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ClientInfo {
97 pub name: String,
98 pub version: String,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct InitializeParams {
105 pub protocol_version: String,
106 pub capabilities: ClientCapabilities,
107 pub client_info: ClientInfo,
108}
109
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct ServerCapabilities {
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub tools: Option<ToolsCapability>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub resources: Option<ResourcesCapability>,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub prompts: Option<PromptsCapability>,
119 #[serde(skip_serializing_if = "Option::is_none")]
120 pub logging: Option<LoggingCapability>,
121}
122
123#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct ToolsCapability {
126 #[serde(default)]
127 pub list_changed: bool,
128}
129
130#[derive(Debug, Clone, Default, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct ResourcesCapability {
133 #[serde(default)]
134 pub subscribe: bool,
135 #[serde(default)]
136 pub list_changed: bool,
137}
138
139#[derive(Debug, Clone, Default, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct PromptsCapability {
142 #[serde(default)]
143 pub list_changed: bool,
144}
145
146#[derive(Debug, Clone, Default, Serialize, Deserialize)]
147pub struct LoggingCapability {}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ServerInfo {
152 pub name: String,
153 pub version: String,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct InitializeResult {
160 pub protocol_version: String,
161 pub capabilities: ServerCapabilities,
162 pub server_info: ServerInfo,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct McpTool {
173 pub name: String,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub description: Option<String>,
176 pub input_schema: serde_json::Value,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct ListToolsResult {
182 pub tools: Vec<McpTool>,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct CallToolParams {
188 pub name: String,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub arguments: Option<serde_json::Value>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195#[serde(tag = "type", rename_all = "lowercase")]
196pub enum ToolContent {
197 Text {
198 text: String,
199 },
200 Image {
201 data: String,
202 #[serde(rename = "mimeType")]
203 mime_type: String,
204 },
205 Resource {
206 resource: ResourceContent,
207 },
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct ResourceContent {
214 pub uri: String,
215 #[serde(skip_serializing_if = "Option::is_none")]
216 pub mime_type: Option<String>,
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub text: Option<String>,
219 #[serde(skip_serializing_if = "Option::is_none")]
220 pub blob: Option<String>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225#[serde(rename_all = "camelCase")]
226pub struct CallToolResult {
227 pub content: Vec<ToolContent>,
228 #[serde(default)]
229 pub is_error: bool,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct McpResource {
240 pub uri: String,
241 pub name: String,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub description: Option<String>,
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub mime_type: Option<String>,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ListResourcesResult {
251 pub resources: Vec<McpResource>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct ReadResourceParams {
257 pub uri: String,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct ReadResourceResult {
263 pub contents: Vec<ResourceContent>,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct McpPrompt {
273 pub name: String,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub description: Option<String>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub arguments: Option<Vec<PromptArgument>>,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct PromptArgument {
283 pub name: String,
284 #[serde(skip_serializing_if = "Option::is_none")]
285 pub description: Option<String>,
286 #[serde(default)]
287 pub required: bool,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ListPromptsResult {
293 pub prompts: Vec<McpPrompt>,
294}
295
296#[derive(Debug, Clone)]
302pub enum McpNotification {
303 ToolsListChanged,
304 ResourcesListChanged,
305 PromptsListChanged,
306 Progress {
307 progress_token: String,
308 progress: f64,
309 total: Option<f64>,
310 },
311 Log {
312 level: String,
313 logger: Option<String>,
314 data: serde_json::Value,
315 },
316 Unknown {
317 method: String,
318 params: Option<serde_json::Value>,
319 },
320}
321
322impl McpNotification {
323 pub fn from_json_rpc(notification: &JsonRpcNotification) -> Self {
324 match notification.method.as_str() {
325 "notifications/tools/list_changed" => McpNotification::ToolsListChanged,
326 "notifications/resources/list_changed" => McpNotification::ResourcesListChanged,
327 "notifications/prompts/list_changed" => McpNotification::PromptsListChanged,
328 "notifications/progress" => {
329 if let Some(params) = ¬ification.params {
330 let progress_token = params
331 .get("progressToken")
332 .and_then(|v| v.as_str())
333 .unwrap_or("")
334 .to_string();
335 let progress = params
336 .get("progress")
337 .and_then(|v| v.as_f64())
338 .unwrap_or(0.0);
339 let total = params.get("total").and_then(|v| v.as_f64());
340 McpNotification::Progress {
341 progress_token,
342 progress,
343 total,
344 }
345 } else {
346 McpNotification::Unknown {
347 method: notification.method.clone(),
348 params: notification.params.clone(),
349 }
350 }
351 }
352 "notifications/message" => {
353 if let Some(params) = ¬ification.params {
354 let level = params
355 .get("level")
356 .and_then(|v| v.as_str())
357 .unwrap_or("info")
358 .to_string();
359 let logger = params
360 .get("logger")
361 .and_then(|v| v.as_str())
362 .map(|s| s.to_string());
363 let data = params
364 .get("data")
365 .cloned()
366 .unwrap_or(serde_json::Value::Null);
367 McpNotification::Log {
368 level,
369 logger,
370 data,
371 }
372 } else {
373 McpNotification::Unknown {
374 method: notification.method.clone(),
375 params: notification.params.clone(),
376 }
377 }
378 }
379 _ => McpNotification::Unknown {
380 method: notification.method.clone(),
381 params: notification.params.clone(),
382 },
383 }
384 }
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct McpServerConfig {
394 pub name: String,
396 pub transport: McpTransportConfig,
398 #[serde(default = "default_true")]
400 pub enabled: bool,
401 #[serde(default)]
403 pub env: HashMap<String, String>,
404 #[serde(skip_serializing_if = "Option::is_none")]
406 pub oauth: Option<OAuthConfig>,
407 #[serde(default = "default_tool_timeout")]
409 pub tool_timeout_secs: u64,
410}
411
412fn default_tool_timeout() -> u64 {
413 60
414}
415
416fn default_true() -> bool {
417 true
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize)]
422#[serde(tag = "type", rename_all = "lowercase")]
423pub enum McpTransportConfig {
424 Stdio {
426 command: String,
427 #[serde(default)]
428 args: Vec<String>,
429 },
430 Http {
432 url: String,
433 #[serde(default)]
434 headers: HashMap<String, String>,
435 },
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct OAuthConfig {
441 pub auth_url: String,
442 pub token_url: String,
443 pub client_id: String,
444 #[serde(skip_serializing_if = "Option::is_none")]
445 pub client_secret: Option<String>,
446 #[serde(default)]
447 pub scopes: Vec<String>,
448 pub redirect_uri: String,
449}
450
451#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn test_json_rpc_request_serialize() {
461 let req = JsonRpcRequest::new(1, "initialize", Some(serde_json::json!({"test": true})));
462 let json = serde_json::to_string(&req).unwrap();
463 assert!(json.contains("\"jsonrpc\":\"2.0\""));
464 assert!(json.contains("\"id\":1"));
465 assert!(json.contains("\"method\":\"initialize\""));
466 }
467
468 #[test]
469 fn test_json_rpc_response_deserialize() {
470 let json = r#"{"jsonrpc":"2.0","id":1,"result":{"success":true}}"#;
471 let resp: JsonRpcResponse = serde_json::from_str(json).unwrap();
472 assert_eq!(resp.id, Some(1));
473 assert!(resp.result.is_some());
474 assert!(resp.error.is_none());
475 }
476
477 #[test]
478 fn test_json_rpc_error_deserialize() {
479 let json =
480 r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid Request"}}"#;
481 let resp: JsonRpcResponse = serde_json::from_str(json).unwrap();
482 assert!(resp.error.is_some());
483 let err = resp.error.unwrap();
484 assert_eq!(err.code, -32600);
485 }
486
487 #[test]
488 fn test_mcp_tool_deserialize() {
489 let json = r#"{
490 "name": "create_issue",
491 "description": "Create a GitHub issue",
492 "inputSchema": {
493 "type": "object",
494 "properties": {
495 "title": {"type": "string"},
496 "body": {"type": "string"}
497 },
498 "required": ["title"]
499 }
500 }"#;
501 let tool: McpTool = serde_json::from_str(json).unwrap();
502 assert_eq!(tool.name, "create_issue");
503 assert!(tool.description.is_some());
504 }
505
506 #[test]
507 fn test_tool_content_text() {
508 let content = ToolContent::Text {
509 text: "Hello".to_string(),
510 };
511 let json = serde_json::to_string(&content).unwrap();
512 assert!(json.contains("\"type\":\"text\""));
513 assert!(json.contains("\"text\":\"Hello\""));
514 }
515
516 #[test]
517 fn test_mcp_transport_config_stdio() {
518 let json = r#"{
519 "type": "stdio",
520 "command": "npx",
521 "args": ["-y", "@modelcontextprotocol/server-github"]
522 }"#;
523 let config: McpTransportConfig = serde_json::from_str(json).unwrap();
524 match config {
525 McpTransportConfig::Stdio { command, args } => {
526 assert_eq!(command, "npx");
527 assert_eq!(args.len(), 2);
528 }
529 _ => panic!("Expected Stdio transport"),
530 }
531 }
532
533 #[test]
534 fn test_mcp_transport_config_http() {
535 let json = r#"{
536 "type": "http",
537 "url": "https://mcp.example.com/api",
538 "headers": {"Authorization": "Bearer token"}
539 }"#;
540 let config: McpTransportConfig = serde_json::from_str(json).unwrap();
541 match config {
542 McpTransportConfig::Http { url, headers } => {
543 assert_eq!(url, "https://mcp.example.com/api");
544 assert!(headers.contains_key("Authorization"));
545 }
546 _ => panic!("Expected Http transport"),
547 }
548 }
549
550 #[test]
551 fn test_mcp_notification_parse() {
552 let notification = JsonRpcNotification::new("notifications/tools/list_changed", None);
553 let mcp_notif = McpNotification::from_json_rpc(¬ification);
554 match mcp_notif {
555 McpNotification::ToolsListChanged => {}
556 _ => panic!("Expected ToolsListChanged"),
557 }
558 }
559 #[test]
560 fn test_json_rpc_request_new_with_params() {
561 let req = JsonRpcRequest::new(1, "initialize", Some(serde_json::json!({"test": true})));
562 assert_eq!(req.jsonrpc, "2.0");
563 assert_eq!(req.id, 1);
564 assert_eq!(req.method, "initialize");
565 assert!(req.params.is_some());
566 }
567
568 #[test]
569 fn test_json_rpc_request_new_without_params() {
570 let req = JsonRpcRequest::new(2, "ping", None);
571 assert_eq!(req.jsonrpc, "2.0");
572 assert_eq!(req.id, 2);
573 assert_eq!(req.method, "ping");
574 assert!(req.params.is_none());
575 }
576
577 #[test]
578 fn test_json_rpc_request_serialization() {
579 let req = JsonRpcRequest::new(1, "test_method", Some(serde_json::json!({"key": "value"})));
580 let json = serde_json::to_string(&req).unwrap();
581 assert!(json.contains("\"jsonrpc\":\"2.0\""));
582 assert!(json.contains("\"id\":1"));
583 assert!(json.contains("\"method\":\"test_method\""));
584 assert!(json.contains("\"params\""));
585 }
586
587 #[test]
588 fn test_json_rpc_response_with_result() {
589 let resp = JsonRpcResponse {
590 jsonrpc: "2.0".to_string(),
591 id: Some(1),
592 result: Some(serde_json::json!({"success": true})),
593 error: None,
594 };
595 assert!(resp.result.is_some());
596 assert!(resp.error.is_none());
597 }
598
599 #[test]
600 fn test_json_rpc_response_with_error() {
601 let resp = JsonRpcResponse {
602 jsonrpc: "2.0".to_string(),
603 id: Some(1),
604 result: None,
605 error: Some(JsonRpcError {
606 code: -32600,
607 message: "Invalid Request".to_string(),
608 data: None,
609 }),
610 };
611 assert!(resp.result.is_none());
612 assert!(resp.error.is_some());
613 }
614
615 #[test]
616 fn test_json_rpc_response_both_none() {
617 let resp = JsonRpcResponse {
618 jsonrpc: "2.0".to_string(),
619 id: Some(1),
620 result: None,
621 error: None,
622 };
623 assert!(resp.result.is_none());
624 assert!(resp.error.is_none());
625 }
626
627 #[test]
628 fn test_json_rpc_response_serialization() {
629 let resp = JsonRpcResponse {
630 jsonrpc: "2.0".to_string(),
631 id: Some(1),
632 result: Some(serde_json::json!({"data": "test"})),
633 error: None,
634 };
635 let json = serde_json::to_string(&resp).unwrap();
636 assert!(json.contains("\"jsonrpc\":\"2.0\""));
637 assert!(json.contains("\"id\":1"));
638 assert!(json.contains("\"result\""));
639 }
640
641 #[test]
642 fn test_json_rpc_notification_new_with_params() {
643 let notif =
644 JsonRpcNotification::new("notification", Some(serde_json::json!({"msg": "hello"})));
645 assert_eq!(notif.jsonrpc, "2.0");
646 assert_eq!(notif.method, "notification");
647 assert!(notif.params.is_some());
648 }
649
650 #[test]
651 fn test_json_rpc_notification_new_without_params() {
652 let notif = JsonRpcNotification::new("ping", None);
653 assert_eq!(notif.jsonrpc, "2.0");
654 assert_eq!(notif.method, "ping");
655 assert!(notif.params.is_none());
656 }
657
658 #[test]
659 fn test_json_rpc_notification_serialization() {
660 let notif = JsonRpcNotification::new(
661 "test_notification",
662 Some(serde_json::json!({"key": "value"})),
663 );
664 let json = serde_json::to_string(¬if).unwrap();
665 assert!(json.contains("\"jsonrpc\":\"2.0\""));
666 assert!(json.contains("\"method\":\"test_notification\""));
667 assert!(!json.contains("\"id\""));
668 }
669
670 #[test]
671 fn test_mcp_tool_serialize() {
672 let tool = McpTool {
673 name: "test_tool".to_string(),
674 description: Some("A test tool".to_string()),
675 input_schema: serde_json::json!({"type": "object"}),
676 };
677 let json = serde_json::to_string(&tool).unwrap();
678 assert!(json.contains("\"name\":\"test_tool\""));
679 assert!(json.contains("\"description\":\"A test tool\""));
680 }
681
682 #[test]
683 fn test_mcp_tool_without_description() {
684 let json = r#"{"name":"tool","inputSchema":{"type":"object"}}"#;
685 let tool: McpTool = serde_json::from_str(json).unwrap();
686 assert_eq!(tool.name, "tool");
687 assert!(tool.description.is_none());
688 }
689
690 #[test]
691 fn test_mcp_resource_serialize() {
692 let resource = McpResource {
693 uri: "file:///test.txt".to_string(),
694 name: "test.txt".to_string(),
695 description: Some("Test file".to_string()),
696 mime_type: Some("text/plain".to_string()),
697 };
698 let json = serde_json::to_string(&resource).unwrap();
699 assert!(json.contains("\"uri\":\"file:///test.txt\""));
700 assert!(json.contains("\"name\":\"test.txt\""));
701 }
702
703 #[test]
704 fn test_mcp_resource_deserialize() {
705 let json = r#"{"uri":"file:///doc.md","name":"doc.md","mimeType":"text/markdown"}"#;
706 let resource: McpResource = serde_json::from_str(json).unwrap();
707 assert_eq!(resource.uri, "file:///doc.md");
708 assert_eq!(resource.name, "doc.md");
709 assert_eq!(resource.mime_type, Some("text/markdown".to_string()));
710 }
711
712 #[test]
713 fn test_initialize_params_serialization() {
714 let params = InitializeParams {
715 protocol_version: PROTOCOL_VERSION.to_string(),
716 capabilities: ClientCapabilities::default(),
717 client_info: ClientInfo {
718 name: "test-client".to_string(),
719 version: "1.0.0".to_string(),
720 },
721 };
722 let json = serde_json::to_string(¶ms).unwrap();
723 assert!(json.contains("\"protocolVersion\""));
724 assert!(json.contains("\"clientInfo\""));
725 }
726
727 #[test]
728 fn test_initialize_result_serialization() {
729 let result = InitializeResult {
730 protocol_version: PROTOCOL_VERSION.to_string(),
731 capabilities: ServerCapabilities::default(),
732 server_info: ServerInfo {
733 name: "test-server".to_string(),
734 version: "1.0.0".to_string(),
735 },
736 };
737 let json = serde_json::to_string(&result).unwrap();
738 assert!(json.contains("\"protocolVersion\""));
739 assert!(json.contains("\"serverInfo\""));
740 }
741
742 #[test]
743 fn test_call_tool_params_serialization() {
744 let params = CallToolParams {
745 name: "test_tool".to_string(),
746 arguments: Some(serde_json::json!({"arg1": "value1"})),
747 };
748 let json = serde_json::to_string(¶ms).unwrap();
749 assert!(json.contains("\"name\":\"test_tool\""));
750 assert!(json.contains("\"arguments\""));
751 }
752
753 #[test]
754 fn test_call_tool_params_without_arguments() {
755 let params = CallToolParams {
756 name: "simple_tool".to_string(),
757 arguments: None,
758 };
759 let json = serde_json::to_string(¶ms).unwrap();
760 assert!(json.contains("\"name\":\"simple_tool\""));
761 assert!(!json.contains("\"arguments\""));
762 }
763
764 #[test]
765 fn test_call_tool_result_serialization() {
766 let result = CallToolResult {
767 content: vec![ToolContent::Text {
768 text: "Result".to_string(),
769 }],
770 is_error: false,
771 };
772 let json = serde_json::to_string(&result).unwrap();
773 assert!(json.contains("\"content\""));
774 assert!(json.contains("\"isError\":false"));
775 }
776
777 #[test]
778 fn test_call_tool_result_error_flag() {
779 let result = CallToolResult {
780 content: vec![ToolContent::Text {
781 text: "Error occurred".to_string(),
782 }],
783 is_error: true,
784 };
785 assert!(result.is_error);
786 }
787
788 #[test]
789 fn test_call_tool_result_default() {
790 let json = r#"{"content":[]}"#;
791 let result: CallToolResult = serde_json::from_str(json).unwrap();
792 assert!(!result.is_error);
793 }
794
795 #[test]
796 fn test_read_resource_params_serialization() {
797 let params = ReadResourceParams {
798 uri: "file:///test.txt".to_string(),
799 };
800 let json = serde_json::to_string(¶ms).unwrap();
801 assert!(json.contains("\"uri\":\"file:///test.txt\""));
802 }
803
804 #[test]
805 fn test_read_resource_result_serialization() {
806 let result = ReadResourceResult {
807 contents: vec![ResourceContent {
808 uri: "file:///test.txt".to_string(),
809 mime_type: Some("text/plain".to_string()),
810 text: Some("Hello".to_string()),
811 blob: None,
812 }],
813 };
814 let json = serde_json::to_string(&result).unwrap();
815 assert!(json.contains("\"contents\""));
816 assert!(json.contains("\"uri\""));
817 }
818
819 #[test]
820 fn test_list_tools_result_serialization() {
821 let result = ListToolsResult {
822 tools: vec![McpTool {
823 name: "tool1".to_string(),
824 description: None,
825 input_schema: serde_json::json!({"type": "object"}),
826 }],
827 };
828 let json = serde_json::to_string(&result).unwrap();
829 assert!(json.contains("\"tools\""));
830 }
831
832 #[test]
833 fn test_list_resources_result_serialization() {
834 let result = ListResourcesResult {
835 resources: vec![McpResource {
836 uri: "file:///test.txt".to_string(),
837 name: "test.txt".to_string(),
838 description: None,
839 mime_type: None,
840 }],
841 };
842 let json = serde_json::to_string(&result).unwrap();
843 assert!(json.contains("\"resources\""));
844 }
845
846 #[test]
847 fn test_server_capabilities_default() {
848 let caps = ServerCapabilities::default();
849 assert!(caps.tools.is_none());
850 assert!(caps.resources.is_none());
851 assert!(caps.prompts.is_none());
852 assert!(caps.logging.is_none());
853 }
854
855 #[test]
856 fn test_server_capabilities_all_fields() {
857 let caps = ServerCapabilities {
858 tools: Some(ToolsCapability { list_changed: true }),
859 resources: Some(ResourcesCapability {
860 subscribe: true,
861 list_changed: true,
862 }),
863 prompts: Some(PromptsCapability { list_changed: true }),
864 logging: Some(LoggingCapability {}),
865 };
866 assert!(caps.tools.is_some());
867 assert!(caps.resources.is_some());
868 assert!(caps.prompts.is_some());
869 assert!(caps.logging.is_some());
870 }
871
872 #[test]
873 fn test_client_capabilities_default() {
874 let caps = ClientCapabilities::default();
875 assert!(caps.roots.is_none());
876 assert!(caps.sampling.is_none());
877 }
878
879 #[test]
880 fn test_client_capabilities_all_fields() {
881 let caps = ClientCapabilities {
882 roots: Some(RootsCapability { list_changed: true }),
883 sampling: Some(SamplingCapability {}),
884 };
885 assert!(caps.roots.is_some());
886 assert!(caps.sampling.is_some());
887 }
888
889 #[test]
890 fn test_mcp_notification_tools_list_changed() {
891 let notif = JsonRpcNotification::new("notifications/tools/list_changed", None);
892 let mcp_notif = McpNotification::from_json_rpc(¬if);
893 match mcp_notif {
894 McpNotification::ToolsListChanged => {}
895 _ => panic!("Expected ToolsListChanged"),
896 }
897 }
898
899 #[test]
900 fn test_mcp_notification_resources_list_changed() {
901 let notif = JsonRpcNotification::new("notifications/resources/list_changed", None);
902 let mcp_notif = McpNotification::from_json_rpc(¬if);
903 match mcp_notif {
904 McpNotification::ResourcesListChanged => {}
905 _ => panic!("Expected ResourcesListChanged"),
906 }
907 }
908
909 #[test]
910 fn test_mcp_notification_prompts_list_changed() {
911 let notif = JsonRpcNotification::new("notifications/prompts/list_changed", None);
912 let mcp_notif = McpNotification::from_json_rpc(¬if);
913 match mcp_notif {
914 McpNotification::PromptsListChanged => {}
915 _ => panic!("Expected PromptsListChanged"),
916 }
917 }
918
919 #[test]
920 fn test_mcp_notification_progress() {
921 let notif = JsonRpcNotification::new(
922 "notifications/progress",
923 Some(serde_json::json!({
924 "progressToken": "token-123",
925 "progress": 50.0,
926 "total": 100.0
927 })),
928 );
929 let mcp_notif = McpNotification::from_json_rpc(¬if);
930 match mcp_notif {
931 McpNotification::Progress {
932 progress_token,
933 progress,
934 total,
935 } => {
936 assert_eq!(progress_token, "token-123");
937 assert_eq!(progress, 50.0);
938 assert_eq!(total, Some(100.0));
939 }
940 _ => panic!("Expected Progress"),
941 }
942 }
943
944 #[test]
945 fn test_mcp_notification_log() {
946 let notif = JsonRpcNotification::new(
947 "notifications/message",
948 Some(serde_json::json!({
949 "level": "error",
950 "logger": "test-logger",
951 "data": {"message": "test"}
952 })),
953 );
954 let mcp_notif = McpNotification::from_json_rpc(¬if);
955 match mcp_notif {
956 McpNotification::Log {
957 level,
958 logger,
959 data,
960 } => {
961 assert_eq!(level, "error");
962 assert_eq!(logger, Some("test-logger".to_string()));
963 assert!(data.is_object());
964 }
965 _ => panic!("Expected Log"),
966 }
967 }
968
969 #[test]
970 fn test_mcp_notification_log_edge_case_no_logger() {
971 let notif = JsonRpcNotification::new(
972 "notifications/message",
973 Some(serde_json::json!({
974 "level": "info",
975 "data": "simple message"
976 })),
977 );
978 let mcp_notif = McpNotification::from_json_rpc(¬if);
979 match mcp_notif {
980 McpNotification::Log { level, logger, .. } => {
981 assert_eq!(level, "info");
982 assert!(logger.is_none());
983 }
984 _ => panic!("Expected Log"),
985 }
986 }
987
988 #[test]
989 fn test_mcp_notification_log_edge_case_default_level() {
990 let notif = JsonRpcNotification::new(
991 "notifications/message",
992 Some(serde_json::json!({
993 "data": "message"
994 })),
995 );
996 let mcp_notif = McpNotification::from_json_rpc(¬if);
997 match mcp_notif {
998 McpNotification::Log { level, .. } => {
999 assert_eq!(level, "info");
1000 }
1001 _ => panic!("Expected Log"),
1002 }
1003 }
1004
1005 #[test]
1006 fn test_mcp_notification_unknown() {
1007 let notif = JsonRpcNotification::new(
1008 "unknown/notification",
1009 Some(serde_json::json!({"key": "value"})),
1010 );
1011 let mcp_notif = McpNotification::from_json_rpc(¬if);
1012 match mcp_notif {
1013 McpNotification::Unknown { method, params } => {
1014 assert_eq!(method, "unknown/notification");
1015 assert!(params.is_some());
1016 }
1017 _ => panic!("Expected Unknown"),
1018 }
1019 }
1020
1021 #[test]
1022 fn test_tool_content_image() {
1023 let content = ToolContent::Image {
1024 data: "base64data".to_string(),
1025 mime_type: "image/png".to_string(),
1026 };
1027 let json = serde_json::to_string(&content).unwrap();
1028 assert!(json.contains("\"type\":\"image\""));
1029 assert!(json.contains("\"data\":\"base64data\""));
1030 assert!(json.contains("\"mimeType\":\"image/png\""));
1031 }
1032
1033 #[test]
1034 fn test_tool_content_resource() {
1035 let content = ToolContent::Resource {
1036 resource: ResourceContent {
1037 uri: "file:///test.txt".to_string(),
1038 mime_type: Some("text/plain".to_string()),
1039 text: Some("content".to_string()),
1040 blob: None,
1041 },
1042 };
1043 let json = serde_json::to_string(&content).unwrap();
1044 assert!(json.contains("\"type\":\"resource\""));
1045 assert!(json.contains("\"uri\":\"file:///test.txt\""));
1046 }
1047
1048 #[test]
1049 fn test_mcp_server_config_default() {
1050 let config = McpServerConfig {
1051 name: "test-server".to_string(),
1052 transport: McpTransportConfig::Stdio {
1053 command: "node".to_string(),
1054 args: vec!["server.js".to_string()],
1055 },
1056 enabled: true,
1057 env: HashMap::new(),
1058 oauth: None,
1059 tool_timeout_secs: 60,
1060 };
1061 assert!(config.enabled);
1062 assert!(config.oauth.is_none());
1063 }
1064
1065 #[test]
1066 fn test_mcp_server_config_with_env() {
1067 let mut env = HashMap::new();
1068 env.insert("API_KEY".to_string(), "secret".to_string());
1069 let config = McpServerConfig {
1070 name: "test-server".to_string(),
1071 transport: McpTransportConfig::Stdio {
1072 command: "node".to_string(),
1073 args: vec![],
1074 },
1075 enabled: true,
1076 env,
1077 oauth: None,
1078 tool_timeout_secs: 60,
1079 };
1080 assert!(config.env.contains_key("API_KEY"));
1081 }
1082
1083 #[test]
1084 fn test_mcp_server_config_with_oauth() {
1085 let config = McpServerConfig {
1086 name: "test-server".to_string(),
1087 transport: McpTransportConfig::Http {
1088 url: "https://api.example.com".to_string(),
1089 headers: HashMap::new(),
1090 },
1091 enabled: true,
1092 env: HashMap::new(),
1093 oauth: Some(OAuthConfig {
1094 auth_url: "https://auth.example.com".to_string(),
1095 token_url: "https://token.example.com".to_string(),
1096 client_id: "client-123".to_string(),
1097 client_secret: Some("secret".to_string()),
1098 scopes: vec!["read".to_string(), "write".to_string()],
1099 redirect_uri: "http://localhost:8080/callback".to_string(),
1100 }),
1101 tool_timeout_secs: 60,
1102 };
1103 assert!(config.oauth.is_some());
1104 }
1105
1106 #[test]
1107 fn test_mcp_transport_config_stdio_variant() {
1108 let transport = McpTransportConfig::Stdio {
1109 command: "python".to_string(),
1110 args: vec!["-m".to_string(), "server".to_string()],
1111 };
1112 match transport {
1113 McpTransportConfig::Stdio { command, args } => {
1114 assert_eq!(command, "python");
1115 assert_eq!(args.len(), 2);
1116 }
1117 _ => panic!("Expected Stdio"),
1118 }
1119 }
1120
1121 #[test]
1122 fn test_mcp_transport_config_http_variant() {
1123 let mut headers = HashMap::new();
1124 headers.insert("Authorization".to_string(), "Bearer token".to_string());
1125 let transport = McpTransportConfig::Http {
1126 url: "https://mcp.example.com".to_string(),
1127 headers,
1128 };
1129 match transport {
1130 McpTransportConfig::Http { url, headers } => {
1131 assert_eq!(url, "https://mcp.example.com");
1132 assert!(headers.contains_key("Authorization"));
1133 }
1134 _ => panic!("Expected Http"),
1135 }
1136 }
1137
1138 #[test]
1139 fn test_mcp_prompt_serialize() {
1140 let prompt = McpPrompt {
1141 name: "test_prompt".to_string(),
1142 description: Some("A test prompt".to_string()),
1143 arguments: Some(vec![PromptArgument {
1144 name: "arg1".to_string(),
1145 description: Some("First argument".to_string()),
1146 required: true,
1147 }]),
1148 };
1149 let json = serde_json::to_string(&prompt).unwrap();
1150 assert!(json.contains("\"name\":\"test_prompt\""));
1151 assert!(json.contains("\"arguments\""));
1152 }
1153
1154 #[test]
1155 fn test_prompt_argument_default() {
1156 let json = r#"{"name":"arg"}"#;
1157 let arg: PromptArgument = serde_json::from_str(json).unwrap();
1158 assert_eq!(arg.name, "arg");
1159 assert!(!arg.required);
1160 }
1161}