1use serde::{Deserialize, Deserializer, 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)]
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
412impl<'de> Deserialize<'de> for McpServerConfig {
413 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
414 use serde::de::Error;
415 use serde_json::Value;
416
417 let mut map = serde_json::Map::deserialize(deserializer)?;
418
419 let transport = if let Some(t) = map.remove("transport") {
422 match &t {
423 Value::String(kind) => {
424 match kind.as_str() {
426 "stdio" => {
427 let command = map
428 .remove("command")
429 .and_then(|v| v.as_str().map(String::from))
430 .ok_or_else(|| D::Error::missing_field("command"))?;
431 let args = map
432 .remove("args")
433 .and_then(|v| serde_json::from_value(v).ok())
434 .unwrap_or_default();
435 McpTransportConfig::Stdio { command, args }
436 }
437 "http" => {
438 let url = map
439 .remove("url")
440 .and_then(|v| v.as_str().map(String::from))
441 .ok_or_else(|| D::Error::missing_field("url"))?;
442 let headers = map
443 .remove("headers")
444 .and_then(|v| serde_json::from_value(v).ok())
445 .unwrap_or_default();
446 McpTransportConfig::Http { url, headers }
447 }
448 "streamable-http" | "streamable_http" => {
449 let url = map
450 .remove("url")
451 .and_then(|v| v.as_str().map(String::from))
452 .ok_or_else(|| D::Error::missing_field("url"))?;
453 let headers = map
454 .remove("headers")
455 .and_then(|v| serde_json::from_value(v).ok())
456 .unwrap_or_default();
457 McpTransportConfig::StreamableHttp { url, headers }
458 }
459 other => {
460 return Err(D::Error::unknown_variant(
461 other,
462 &["stdio", "http", "streamable-http"],
463 ));
464 }
465 }
466 }
467 Value::Object(_) => serde_json::from_value(t).map_err(D::Error::custom)?,
469 _ => return Err(D::Error::custom("transport must be a string or object")),
470 }
471 } else {
472 return Err(D::Error::missing_field("transport"));
473 };
474
475 let name = map
476 .remove("name")
477 .and_then(|v| v.as_str().map(String::from))
478 .ok_or_else(|| D::Error::missing_field("name"))?;
479 let enabled = map
480 .remove("enabled")
481 .and_then(|v| v.as_bool())
482 .unwrap_or(true);
483 let env = map
484 .remove("env")
485 .and_then(|v| serde_json::from_value(v).ok())
486 .unwrap_or_default();
487 let oauth = map
488 .remove("oauth")
489 .and_then(|v| serde_json::from_value(v).ok());
490 let tool_timeout_secs = map
491 .remove("tool_timeout_secs")
492 .or_else(|| map.remove("toolTimeoutSecs"))
493 .and_then(|v| v.as_u64())
494 .unwrap_or(60);
495
496 Ok(McpServerConfig {
497 name,
498 transport,
499 enabled,
500 env,
501 oauth,
502 tool_timeout_secs,
503 })
504 }
505}
506
507#[allow(dead_code)] fn default_tool_timeout() -> u64 {
509 60
510}
511
512#[allow(dead_code)]
513fn default_true() -> bool {
514 true
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize)]
519#[serde(tag = "type", rename_all = "kebab-case")]
520pub enum McpTransportConfig {
521 Stdio {
523 command: String,
524 #[serde(default)]
525 args: Vec<String>,
526 },
527 Http {
529 url: String,
530 #[serde(default)]
531 headers: HashMap<String, String>,
532 },
533 StreamableHttp {
538 url: String,
539 #[serde(default)]
540 headers: HashMap<String, String>,
541 },
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct OAuthConfig {
547 pub auth_url: String,
548 pub token_url: String,
549 pub client_id: String,
550 #[serde(skip_serializing_if = "Option::is_none")]
551 pub client_secret: Option<String>,
552 #[serde(default)]
553 pub scopes: Vec<String>,
554 pub redirect_uri: String,
555 #[serde(skip_serializing_if = "Option::is_none")]
558 pub access_token: Option<String>,
559}
560
561#[cfg(test)]
566mod tests {
567 use super::*;
568
569 #[test]
570 fn test_json_rpc_request_serialize() {
571 let req = JsonRpcRequest::new(1, "initialize", Some(serde_json::json!({"test": true})));
572 let json = serde_json::to_string(&req).unwrap();
573 assert!(json.contains("\"jsonrpc\":\"2.0\""));
574 assert!(json.contains("\"id\":1"));
575 assert!(json.contains("\"method\":\"initialize\""));
576 }
577
578 #[test]
579 fn test_json_rpc_response_deserialize() {
580 let json = r#"{"jsonrpc":"2.0","id":1,"result":{"success":true}}"#;
581 let resp: JsonRpcResponse = serde_json::from_str(json).unwrap();
582 assert_eq!(resp.id, Some(1));
583 assert!(resp.result.is_some());
584 assert!(resp.error.is_none());
585 }
586
587 #[test]
588 fn test_json_rpc_error_deserialize() {
589 let json =
590 r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid Request"}}"#;
591 let resp: JsonRpcResponse = serde_json::from_str(json).unwrap();
592 assert!(resp.error.is_some());
593 let err = resp.error.unwrap();
594 assert_eq!(err.code, -32600);
595 }
596
597 #[test]
598 fn test_mcp_tool_deserialize() {
599 let json = r#"{
600 "name": "create_issue",
601 "description": "Create a GitHub issue",
602 "inputSchema": {
603 "type": "object",
604 "properties": {
605 "title": {"type": "string"},
606 "body": {"type": "string"}
607 },
608 "required": ["title"]
609 }
610 }"#;
611 let tool: McpTool = serde_json::from_str(json).unwrap();
612 assert_eq!(tool.name, "create_issue");
613 assert!(tool.description.is_some());
614 }
615
616 #[test]
617 fn test_tool_content_text() {
618 let content = ToolContent::Text {
619 text: "Hello".to_string(),
620 };
621 let json = serde_json::to_string(&content).unwrap();
622 assert!(json.contains("\"type\":\"text\""));
623 assert!(json.contains("\"text\":\"Hello\""));
624 }
625
626 #[test]
627 fn test_mcp_transport_config_stdio() {
628 let json = r#"{
629 "type": "stdio",
630 "command": "npx",
631 "args": ["-y", "@modelcontextprotocol/server-github"]
632 }"#;
633 let config: McpTransportConfig = serde_json::from_str(json).unwrap();
634 match config {
635 McpTransportConfig::Stdio { command, args } => {
636 assert_eq!(command, "npx");
637 assert_eq!(args.len(), 2);
638 }
639 _ => panic!("Expected Stdio transport"),
640 }
641 }
642
643 #[test]
644 fn test_mcp_transport_config_http() {
645 let json = r#"{
646 "type": "http",
647 "url": "https://mcp.example.com/api",
648 "headers": {"Authorization": "Bearer token"}
649 }"#;
650 let config: McpTransportConfig = serde_json::from_str(json).unwrap();
651 match config {
652 McpTransportConfig::Http { url, headers } => {
653 assert_eq!(url, "https://mcp.example.com/api");
654 assert!(headers.contains_key("Authorization"));
655 }
656 _ => panic!("Expected Http transport"),
657 }
658 }
659
660 #[test]
661 fn test_mcp_notification_parse() {
662 let notification = JsonRpcNotification::new("notifications/tools/list_changed", None);
663 let mcp_notif = McpNotification::from_json_rpc(¬ification);
664 match mcp_notif {
665 McpNotification::ToolsListChanged => {}
666 _ => panic!("Expected ToolsListChanged"),
667 }
668 }
669 #[test]
670 fn test_json_rpc_request_new_with_params() {
671 let req = JsonRpcRequest::new(1, "initialize", Some(serde_json::json!({"test": true})));
672 assert_eq!(req.jsonrpc, "2.0");
673 assert_eq!(req.id, 1);
674 assert_eq!(req.method, "initialize");
675 assert!(req.params.is_some());
676 }
677
678 #[test]
679 fn test_json_rpc_request_new_without_params() {
680 let req = JsonRpcRequest::new(2, "ping", None);
681 assert_eq!(req.jsonrpc, "2.0");
682 assert_eq!(req.id, 2);
683 assert_eq!(req.method, "ping");
684 assert!(req.params.is_none());
685 }
686
687 #[test]
688 fn test_json_rpc_request_serialization() {
689 let req = JsonRpcRequest::new(1, "test_method", Some(serde_json::json!({"key": "value"})));
690 let json = serde_json::to_string(&req).unwrap();
691 assert!(json.contains("\"jsonrpc\":\"2.0\""));
692 assert!(json.contains("\"id\":1"));
693 assert!(json.contains("\"method\":\"test_method\""));
694 assert!(json.contains("\"params\""));
695 }
696
697 #[test]
698 fn test_json_rpc_response_with_result() {
699 let resp = JsonRpcResponse {
700 jsonrpc: "2.0".to_string(),
701 id: Some(1),
702 result: Some(serde_json::json!({"success": true})),
703 error: None,
704 };
705 assert!(resp.result.is_some());
706 assert!(resp.error.is_none());
707 }
708
709 #[test]
710 fn test_json_rpc_response_with_error() {
711 let resp = JsonRpcResponse {
712 jsonrpc: "2.0".to_string(),
713 id: Some(1),
714 result: None,
715 error: Some(JsonRpcError {
716 code: -32600,
717 message: "Invalid Request".to_string(),
718 data: None,
719 }),
720 };
721 assert!(resp.result.is_none());
722 assert!(resp.error.is_some());
723 }
724
725 #[test]
726 fn test_json_rpc_response_both_none() {
727 let resp = JsonRpcResponse {
728 jsonrpc: "2.0".to_string(),
729 id: Some(1),
730 result: None,
731 error: None,
732 };
733 assert!(resp.result.is_none());
734 assert!(resp.error.is_none());
735 }
736
737 #[test]
738 fn test_json_rpc_response_serialization() {
739 let resp = JsonRpcResponse {
740 jsonrpc: "2.0".to_string(),
741 id: Some(1),
742 result: Some(serde_json::json!({"data": "test"})),
743 error: None,
744 };
745 let json = serde_json::to_string(&resp).unwrap();
746 assert!(json.contains("\"jsonrpc\":\"2.0\""));
747 assert!(json.contains("\"id\":1"));
748 assert!(json.contains("\"result\""));
749 }
750
751 #[test]
752 fn test_json_rpc_notification_new_with_params() {
753 let notif =
754 JsonRpcNotification::new("notification", Some(serde_json::json!({"msg": "hello"})));
755 assert_eq!(notif.jsonrpc, "2.0");
756 assert_eq!(notif.method, "notification");
757 assert!(notif.params.is_some());
758 }
759
760 #[test]
761 fn test_json_rpc_notification_new_without_params() {
762 let notif = JsonRpcNotification::new("ping", None);
763 assert_eq!(notif.jsonrpc, "2.0");
764 assert_eq!(notif.method, "ping");
765 assert!(notif.params.is_none());
766 }
767
768 #[test]
769 fn test_json_rpc_notification_serialization() {
770 let notif = JsonRpcNotification::new(
771 "test_notification",
772 Some(serde_json::json!({"key": "value"})),
773 );
774 let json = serde_json::to_string(¬if).unwrap();
775 assert!(json.contains("\"jsonrpc\":\"2.0\""));
776 assert!(json.contains("\"method\":\"test_notification\""));
777 assert!(!json.contains("\"id\""));
778 }
779
780 #[test]
781 fn test_mcp_tool_serialize() {
782 let tool = McpTool {
783 name: "test_tool".to_string(),
784 description: Some("A test tool".to_string()),
785 input_schema: serde_json::json!({"type": "object"}),
786 };
787 let json = serde_json::to_string(&tool).unwrap();
788 assert!(json.contains("\"name\":\"test_tool\""));
789 assert!(json.contains("\"description\":\"A test tool\""));
790 }
791
792 #[test]
793 fn test_mcp_tool_without_description() {
794 let json = r#"{"name":"tool","inputSchema":{"type":"object"}}"#;
795 let tool: McpTool = serde_json::from_str(json).unwrap();
796 assert_eq!(tool.name, "tool");
797 assert!(tool.description.is_none());
798 }
799
800 #[test]
801 fn test_mcp_resource_serialize() {
802 let resource = McpResource {
803 uri: "file:///test.txt".to_string(),
804 name: "test.txt".to_string(),
805 description: Some("Test file".to_string()),
806 mime_type: Some("text/plain".to_string()),
807 };
808 let json = serde_json::to_string(&resource).unwrap();
809 assert!(json.contains("\"uri\":\"file:///test.txt\""));
810 assert!(json.contains("\"name\":\"test.txt\""));
811 }
812
813 #[test]
814 fn test_mcp_resource_deserialize() {
815 let json = r#"{"uri":"file:///doc.md","name":"doc.md","mimeType":"text/markdown"}"#;
816 let resource: McpResource = serde_json::from_str(json).unwrap();
817 assert_eq!(resource.uri, "file:///doc.md");
818 assert_eq!(resource.name, "doc.md");
819 assert_eq!(resource.mime_type, Some("text/markdown".to_string()));
820 }
821
822 #[test]
823 fn test_initialize_params_serialization() {
824 let params = InitializeParams {
825 protocol_version: PROTOCOL_VERSION.to_string(),
826 capabilities: ClientCapabilities::default(),
827 client_info: ClientInfo {
828 name: "test-client".to_string(),
829 version: "1.0.0".to_string(),
830 },
831 };
832 let json = serde_json::to_string(¶ms).unwrap();
833 assert!(json.contains("\"protocolVersion\""));
834 assert!(json.contains("\"clientInfo\""));
835 }
836
837 #[test]
838 fn test_initialize_result_serialization() {
839 let result = InitializeResult {
840 protocol_version: PROTOCOL_VERSION.to_string(),
841 capabilities: ServerCapabilities::default(),
842 server_info: ServerInfo {
843 name: "test-server".to_string(),
844 version: "1.0.0".to_string(),
845 },
846 };
847 let json = serde_json::to_string(&result).unwrap();
848 assert!(json.contains("\"protocolVersion\""));
849 assert!(json.contains("\"serverInfo\""));
850 }
851
852 #[test]
853 fn test_call_tool_params_serialization() {
854 let params = CallToolParams {
855 name: "test_tool".to_string(),
856 arguments: Some(serde_json::json!({"arg1": "value1"})),
857 };
858 let json = serde_json::to_string(¶ms).unwrap();
859 assert!(json.contains("\"name\":\"test_tool\""));
860 assert!(json.contains("\"arguments\""));
861 }
862
863 #[test]
864 fn test_call_tool_params_without_arguments() {
865 let params = CallToolParams {
866 name: "simple_tool".to_string(),
867 arguments: None,
868 };
869 let json = serde_json::to_string(¶ms).unwrap();
870 assert!(json.contains("\"name\":\"simple_tool\""));
871 assert!(!json.contains("\"arguments\""));
872 }
873
874 #[test]
875 fn test_call_tool_result_serialization() {
876 let result = CallToolResult {
877 content: vec![ToolContent::Text {
878 text: "Result".to_string(),
879 }],
880 is_error: false,
881 };
882 let json = serde_json::to_string(&result).unwrap();
883 assert!(json.contains("\"content\""));
884 assert!(json.contains("\"isError\":false"));
885 }
886
887 #[test]
888 fn test_call_tool_result_error_flag() {
889 let result = CallToolResult {
890 content: vec![ToolContent::Text {
891 text: "Error occurred".to_string(),
892 }],
893 is_error: true,
894 };
895 assert!(result.is_error);
896 }
897
898 #[test]
899 fn test_call_tool_result_default() {
900 let json = r#"{"content":[]}"#;
901 let result: CallToolResult = serde_json::from_str(json).unwrap();
902 assert!(!result.is_error);
903 }
904
905 #[test]
906 fn test_read_resource_params_serialization() {
907 let params = ReadResourceParams {
908 uri: "file:///test.txt".to_string(),
909 };
910 let json = serde_json::to_string(¶ms).unwrap();
911 assert!(json.contains("\"uri\":\"file:///test.txt\""));
912 }
913
914 #[test]
915 fn test_read_resource_result_serialization() {
916 let result = ReadResourceResult {
917 contents: vec![ResourceContent {
918 uri: "file:///test.txt".to_string(),
919 mime_type: Some("text/plain".to_string()),
920 text: Some("Hello".to_string()),
921 blob: None,
922 }],
923 };
924 let json = serde_json::to_string(&result).unwrap();
925 assert!(json.contains("\"contents\""));
926 assert!(json.contains("\"uri\""));
927 }
928
929 #[test]
930 fn test_list_tools_result_serialization() {
931 let result = ListToolsResult {
932 tools: vec![McpTool {
933 name: "tool1".to_string(),
934 description: None,
935 input_schema: serde_json::json!({"type": "object"}),
936 }],
937 };
938 let json = serde_json::to_string(&result).unwrap();
939 assert!(json.contains("\"tools\""));
940 }
941
942 #[test]
943 fn test_list_resources_result_serialization() {
944 let result = ListResourcesResult {
945 resources: vec![McpResource {
946 uri: "file:///test.txt".to_string(),
947 name: "test.txt".to_string(),
948 description: None,
949 mime_type: None,
950 }],
951 };
952 let json = serde_json::to_string(&result).unwrap();
953 assert!(json.contains("\"resources\""));
954 }
955
956 #[test]
957 fn test_server_capabilities_default() {
958 let caps = ServerCapabilities::default();
959 assert!(caps.tools.is_none());
960 assert!(caps.resources.is_none());
961 assert!(caps.prompts.is_none());
962 assert!(caps.logging.is_none());
963 }
964
965 #[test]
966 fn test_server_capabilities_all_fields() {
967 let caps = ServerCapabilities {
968 tools: Some(ToolsCapability { list_changed: true }),
969 resources: Some(ResourcesCapability {
970 subscribe: true,
971 list_changed: true,
972 }),
973 prompts: Some(PromptsCapability { list_changed: true }),
974 logging: Some(LoggingCapability {}),
975 };
976 assert!(caps.tools.is_some());
977 assert!(caps.resources.is_some());
978 assert!(caps.prompts.is_some());
979 assert!(caps.logging.is_some());
980 }
981
982 #[test]
983 fn test_client_capabilities_default() {
984 let caps = ClientCapabilities::default();
985 assert!(caps.roots.is_none());
986 assert!(caps.sampling.is_none());
987 }
988
989 #[test]
990 fn test_client_capabilities_all_fields() {
991 let caps = ClientCapabilities {
992 roots: Some(RootsCapability { list_changed: true }),
993 sampling: Some(SamplingCapability {}),
994 };
995 assert!(caps.roots.is_some());
996 assert!(caps.sampling.is_some());
997 }
998
999 #[test]
1000 fn test_mcp_notification_tools_list_changed() {
1001 let notif = JsonRpcNotification::new("notifications/tools/list_changed", None);
1002 let mcp_notif = McpNotification::from_json_rpc(¬if);
1003 match mcp_notif {
1004 McpNotification::ToolsListChanged => {}
1005 _ => panic!("Expected ToolsListChanged"),
1006 }
1007 }
1008
1009 #[test]
1010 fn test_mcp_notification_resources_list_changed() {
1011 let notif = JsonRpcNotification::new("notifications/resources/list_changed", None);
1012 let mcp_notif = McpNotification::from_json_rpc(¬if);
1013 match mcp_notif {
1014 McpNotification::ResourcesListChanged => {}
1015 _ => panic!("Expected ResourcesListChanged"),
1016 }
1017 }
1018
1019 #[test]
1020 fn test_mcp_notification_prompts_list_changed() {
1021 let notif = JsonRpcNotification::new("notifications/prompts/list_changed", None);
1022 let mcp_notif = McpNotification::from_json_rpc(¬if);
1023 match mcp_notif {
1024 McpNotification::PromptsListChanged => {}
1025 _ => panic!("Expected PromptsListChanged"),
1026 }
1027 }
1028
1029 #[test]
1030 fn test_mcp_notification_progress() {
1031 let notif = JsonRpcNotification::new(
1032 "notifications/progress",
1033 Some(serde_json::json!({
1034 "progressToken": "token-123",
1035 "progress": 50.0,
1036 "total": 100.0
1037 })),
1038 );
1039 let mcp_notif = McpNotification::from_json_rpc(¬if);
1040 match mcp_notif {
1041 McpNotification::Progress {
1042 progress_token,
1043 progress,
1044 total,
1045 } => {
1046 assert_eq!(progress_token, "token-123");
1047 assert_eq!(progress, 50.0);
1048 assert_eq!(total, Some(100.0));
1049 }
1050 _ => panic!("Expected Progress"),
1051 }
1052 }
1053
1054 #[test]
1055 fn test_mcp_notification_log() {
1056 let notif = JsonRpcNotification::new(
1057 "notifications/message",
1058 Some(serde_json::json!({
1059 "level": "error",
1060 "logger": "test-logger",
1061 "data": {"message": "test"}
1062 })),
1063 );
1064 let mcp_notif = McpNotification::from_json_rpc(¬if);
1065 match mcp_notif {
1066 McpNotification::Log {
1067 level,
1068 logger,
1069 data,
1070 } => {
1071 assert_eq!(level, "error");
1072 assert_eq!(logger, Some("test-logger".to_string()));
1073 assert!(data.is_object());
1074 }
1075 _ => panic!("Expected Log"),
1076 }
1077 }
1078
1079 #[test]
1080 fn test_mcp_notification_log_edge_case_no_logger() {
1081 let notif = JsonRpcNotification::new(
1082 "notifications/message",
1083 Some(serde_json::json!({
1084 "level": "info",
1085 "data": "simple message"
1086 })),
1087 );
1088 let mcp_notif = McpNotification::from_json_rpc(¬if);
1089 match mcp_notif {
1090 McpNotification::Log { level, logger, .. } => {
1091 assert_eq!(level, "info");
1092 assert!(logger.is_none());
1093 }
1094 _ => panic!("Expected Log"),
1095 }
1096 }
1097
1098 #[test]
1099 fn test_mcp_notification_log_edge_case_default_level() {
1100 let notif = JsonRpcNotification::new(
1101 "notifications/message",
1102 Some(serde_json::json!({
1103 "data": "message"
1104 })),
1105 );
1106 let mcp_notif = McpNotification::from_json_rpc(¬if);
1107 match mcp_notif {
1108 McpNotification::Log { level, .. } => {
1109 assert_eq!(level, "info");
1110 }
1111 _ => panic!("Expected Log"),
1112 }
1113 }
1114
1115 #[test]
1116 fn test_mcp_notification_unknown() {
1117 let notif = JsonRpcNotification::new(
1118 "unknown/notification",
1119 Some(serde_json::json!({"key": "value"})),
1120 );
1121 let mcp_notif = McpNotification::from_json_rpc(¬if);
1122 match mcp_notif {
1123 McpNotification::Unknown { method, params } => {
1124 assert_eq!(method, "unknown/notification");
1125 assert!(params.is_some());
1126 }
1127 _ => panic!("Expected Unknown"),
1128 }
1129 }
1130
1131 #[test]
1132 fn test_tool_content_image() {
1133 let content = ToolContent::Image {
1134 data: "base64data".to_string(),
1135 mime_type: "image/png".to_string(),
1136 };
1137 let json = serde_json::to_string(&content).unwrap();
1138 assert!(json.contains("\"type\":\"image\""));
1139 assert!(json.contains("\"data\":\"base64data\""));
1140 assert!(json.contains("\"mimeType\":\"image/png\""));
1141 }
1142
1143 #[test]
1144 fn test_tool_content_resource() {
1145 let content = ToolContent::Resource {
1146 resource: ResourceContent {
1147 uri: "file:///test.txt".to_string(),
1148 mime_type: Some("text/plain".to_string()),
1149 text: Some("content".to_string()),
1150 blob: None,
1151 },
1152 };
1153 let json = serde_json::to_string(&content).unwrap();
1154 assert!(json.contains("\"type\":\"resource\""));
1155 assert!(json.contains("\"uri\":\"file:///test.txt\""));
1156 }
1157
1158 #[test]
1159 fn test_mcp_server_config_default() {
1160 let config = McpServerConfig {
1161 name: "test-server".to_string(),
1162 transport: McpTransportConfig::Stdio {
1163 command: "node".to_string(),
1164 args: vec!["server.js".to_string()],
1165 },
1166 enabled: true,
1167 env: HashMap::new(),
1168 oauth: None,
1169 tool_timeout_secs: 60,
1170 };
1171 assert!(config.enabled);
1172 assert!(config.oauth.is_none());
1173 }
1174
1175 #[test]
1176 fn test_mcp_server_config_with_env() {
1177 let mut env = HashMap::new();
1178 env.insert("API_KEY".to_string(), "secret".to_string());
1179 let config = McpServerConfig {
1180 name: "test-server".to_string(),
1181 transport: McpTransportConfig::Stdio {
1182 command: "node".to_string(),
1183 args: vec![],
1184 },
1185 enabled: true,
1186 env,
1187 oauth: None,
1188 tool_timeout_secs: 60,
1189 };
1190 assert!(config.env.contains_key("API_KEY"));
1191 }
1192
1193 #[test]
1194 fn test_mcp_server_config_with_oauth() {
1195 let config = McpServerConfig {
1196 name: "test-server".to_string(),
1197 transport: McpTransportConfig::Http {
1198 url: "https://api.example.com".to_string(),
1199 headers: HashMap::new(),
1200 },
1201 enabled: true,
1202 env: HashMap::new(),
1203 oauth: Some(OAuthConfig {
1204 auth_url: "https://auth.example.com".to_string(),
1205 token_url: "https://token.example.com".to_string(),
1206 client_id: "client-123".to_string(),
1207 client_secret: Some("secret".to_string()),
1208 scopes: vec!["read".to_string(), "write".to_string()],
1209 redirect_uri: "http://localhost:8080/callback".to_string(),
1210 access_token: None,
1211 }),
1212 tool_timeout_secs: 60,
1213 };
1214 assert!(config.oauth.is_some());
1215 }
1216
1217 #[test]
1218 fn test_mcp_transport_config_stdio_variant() {
1219 let transport = McpTransportConfig::Stdio {
1220 command: "python".to_string(),
1221 args: vec!["-m".to_string(), "server".to_string()],
1222 };
1223 match transport {
1224 McpTransportConfig::Stdio { command, args } => {
1225 assert_eq!(command, "python");
1226 assert_eq!(args.len(), 2);
1227 }
1228 _ => panic!("Expected Stdio"),
1229 }
1230 }
1231
1232 #[test]
1233 fn test_mcp_transport_config_http_variant() {
1234 let mut headers = HashMap::new();
1235 headers.insert("Authorization".to_string(), "Bearer token".to_string());
1236 let transport = McpTransportConfig::Http {
1237 url: "https://mcp.example.com".to_string(),
1238 headers,
1239 };
1240 match transport {
1241 McpTransportConfig::Http { url, headers } => {
1242 assert_eq!(url, "https://mcp.example.com");
1243 assert!(headers.contains_key("Authorization"));
1244 }
1245 _ => panic!("Expected Http"),
1246 }
1247 }
1248
1249 #[test]
1250 fn test_mcp_prompt_serialize() {
1251 let prompt = McpPrompt {
1252 name: "test_prompt".to_string(),
1253 description: Some("A test prompt".to_string()),
1254 arguments: Some(vec![PromptArgument {
1255 name: "arg1".to_string(),
1256 description: Some("First argument".to_string()),
1257 required: true,
1258 }]),
1259 };
1260 let json = serde_json::to_string(&prompt).unwrap();
1261 assert!(json.contains("\"name\":\"test_prompt\""));
1262 assert!(json.contains("\"arguments\""));
1263 }
1264
1265 #[test]
1266 fn test_prompt_argument_default() {
1267 let json = r#"{"name":"arg"}"#;
1268 let arg: PromptArgument = serde_json::from_str(json).unwrap();
1269 assert_eq!(arg.name, "arg");
1270 assert!(!arg.required);
1271 }
1272
1273 #[test]
1274 fn test_oauth_config_with_static_token() {
1275 let json = r#"{
1276 "auth_url": "https://auth.example.com/authorize",
1277 "token_url": "https://auth.example.com/token",
1278 "client_id": "my-client",
1279 "scopes": ["read", "write"],
1280 "redirect_uri": "http://localhost/callback",
1281 "access_token": "static-token-abc123"
1282 }"#;
1283 let config: OAuthConfig = serde_json::from_str(json).unwrap();
1284 assert_eq!(config.client_id, "my-client");
1285 assert_eq!(config.access_token, Some("static-token-abc123".to_string()));
1286 }
1287
1288 #[test]
1289 fn test_oauth_config_without_static_token() {
1290 let json = r#"{
1291 "auth_url": "https://auth.example.com/authorize",
1292 "token_url": "https://auth.example.com/token",
1293 "client_id": "my-client",
1294 "scopes": [],
1295 "redirect_uri": "http://localhost/callback"
1296 }"#;
1297 let config: OAuthConfig = serde_json::from_str(json).unwrap();
1298 assert!(config.access_token.is_none());
1299 }
1300
1301 #[test]
1302 fn test_oauth_config_static_token_not_serialized_when_absent() {
1303 let config = OAuthConfig {
1304 auth_url: "https://example.com/auth".to_string(),
1305 token_url: "https://example.com/token".to_string(),
1306 client_id: "client".to_string(),
1307 client_secret: None,
1308 scopes: vec![],
1309 redirect_uri: "http://localhost/cb".to_string(),
1310 access_token: None,
1311 };
1312 let json = serde_json::to_string(&config).unwrap();
1313 assert!(!json.contains("access_token"));
1314 }
1315}