reasonkit_web/mcp/
types.rs

1//! MCP protocol types
2//!
3//! This module defines the types used in the MCP JSON-RPC protocol.
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::time::{Duration, Instant};
8
9/// JSON-RPC 2.0 request
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct JsonRpcRequest {
12    /// JSON-RPC version (always "2.0")
13    pub jsonrpc: String,
14    /// Method name
15    pub method: String,
16    /// Optional parameters
17    #[serde(default)]
18    pub params: Option<Value>,
19    /// Request ID (None for notifications)
20    pub id: Option<Value>,
21}
22
23/// JSON-RPC 2.0 response
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct JsonRpcResponse {
26    /// JSON-RPC version (always "2.0")
27    pub jsonrpc: String,
28    /// Request ID
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub id: Option<Value>,
31    /// Success result
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub result: Option<Value>,
34    /// Error result
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub error: Option<JsonRpcError>,
37}
38
39impl JsonRpcResponse {
40    /// Create a success response
41    pub fn success(id: Option<Value>, result: Value) -> Self {
42        Self {
43            jsonrpc: "2.0".to_string(),
44            id,
45            result: Some(result),
46            error: None,
47        }
48    }
49
50    /// Create an error response
51    pub fn error(id: Option<Value>, code: i32, message: impl Into<String>) -> Self {
52        Self {
53            jsonrpc: "2.0".to_string(),
54            id,
55            result: None,
56            error: Some(JsonRpcError {
57                code,
58                message: message.into(),
59                data: None,
60            }),
61        }
62    }
63
64    /// Create a parse error response
65    pub fn parse_error() -> Self {
66        Self::error(None, -32700, "Parse error")
67    }
68
69    /// Create an invalid request error
70    pub fn invalid_request(id: Option<Value>) -> Self {
71        Self::error(id, -32600, "Invalid Request")
72    }
73
74    /// Create a method not found error
75    pub fn method_not_found(id: Option<Value>, method: &str) -> Self {
76        Self::error(id, -32601, format!("Method not found: {}", method))
77    }
78
79    /// Create an invalid params error
80    pub fn invalid_params(id: Option<Value>, msg: &str) -> Self {
81        Self::error(id, -32602, format!("Invalid params: {}", msg))
82    }
83
84    /// Create an internal error
85    pub fn internal_error(id: Option<Value>, msg: &str) -> Self {
86        Self::error(id, -32603, format!("Internal error: {}", msg))
87    }
88}
89
90/// JSON-RPC 2.0 error object
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct JsonRpcError {
93    /// Error code
94    pub code: i32,
95    /// Error message
96    pub message: String,
97    /// Optional additional data
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub data: Option<Value>,
100}
101
102/// MCP server capabilities
103#[derive(Debug, Clone, Default, Serialize, Deserialize)]
104pub struct McpCapabilities {
105    /// Tools capability
106    #[serde(default)]
107    pub tools: ToolsCapability,
108    /// Resources capability
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub resources: Option<ResourcesCapability>,
111    /// Prompts capability
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub prompts: Option<PromptsCapability>,
114}
115
116/// Tools capability
117#[derive(Debug, Clone, Default, Serialize, Deserialize)]
118pub struct ToolsCapability {
119    /// Whether tool list changes should be notified
120    #[serde(default, rename = "listChanged")]
121    pub list_changed: bool,
122}
123
124/// Resources capability (not implemented)
125#[derive(Debug, Clone, Default, Serialize, Deserialize)]
126pub struct ResourcesCapability {}
127
128/// Prompts capability (not implemented)
129#[derive(Debug, Clone, Default, Serialize, Deserialize)]
130pub struct PromptsCapability {}
131
132/// MCP server info
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct McpServerInfo {
135    /// Server name
136    pub name: String,
137    /// Server version
138    pub version: String,
139}
140
141impl Default for McpServerInfo {
142    fn default() -> Self {
143        Self {
144            name: "reasonkit-web".to_string(),
145            version: env!("CARGO_PKG_VERSION").to_string(),
146        }
147    }
148}
149
150/// MCP tool definition
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct McpToolDefinition {
153    /// Tool name
154    pub name: String,
155    /// Tool description
156    pub description: String,
157    /// Input JSON schema
158    #[serde(rename = "inputSchema")]
159    pub input_schema: Value,
160}
161
162/// Parameters for tools/call method
163#[derive(Debug, Clone, Deserialize)]
164pub struct ToolCallParams {
165    /// Tool name
166    pub name: String,
167    /// Tool arguments
168    #[serde(default)]
169    pub arguments: Value,
170}
171
172/// Result of a tool call
173#[derive(Debug, Clone, Serialize)]
174pub struct ToolCallResult {
175    /// Whether the call was an error
176    #[serde(rename = "isError", skip_serializing_if = "std::ops::Not::not")]
177    pub is_error: bool,
178    /// Content array
179    pub content: Vec<ToolContent>,
180}
181
182impl ToolCallResult {
183    /// Create a success result with text content
184    pub fn text(text: impl Into<String>) -> Self {
185        Self {
186            is_error: false,
187            content: vec![ToolContent::text(text)],
188        }
189    }
190
191    /// Create a success result with image content
192    pub fn image(data: String, mime_type: impl Into<String>) -> Self {
193        Self {
194            is_error: false,
195            content: vec![ToolContent::image(data, mime_type)],
196        }
197    }
198
199    /// Create an error result
200    pub fn error(message: impl Into<String>) -> Self {
201        Self {
202            is_error: true,
203            content: vec![ToolContent::text(message)],
204        }
205    }
206
207    /// Create a result with multiple content items
208    pub fn multi(content: Vec<ToolContent>) -> Self {
209        Self {
210            is_error: false,
211            content,
212        }
213    }
214}
215
216/// Content item in tool result
217#[derive(Debug, Clone, Serialize, Deserialize)]
218#[serde(tag = "type")]
219pub enum ToolContent {
220    /// Text content
221    #[serde(rename = "text")]
222    Text {
223        /// The text content
224        text: String,
225    },
226    /// Image content
227    #[serde(rename = "image")]
228    Image {
229        /// Base64 encoded image data
230        data: String,
231        /// MIME type
232        #[serde(rename = "mimeType")]
233        mime_type: String,
234    },
235    /// Resource content
236    #[serde(rename = "resource")]
237    Resource {
238        /// Resource URI
239        uri: String,
240        /// Resource content
241        resource: ResourceContent,
242    },
243}
244
245impl ToolContent {
246    /// Create text content
247    pub fn text(text: impl Into<String>) -> Self {
248        Self::Text { text: text.into() }
249    }
250
251    /// Create image content
252    pub fn image(data: String, mime_type: impl Into<String>) -> Self {
253        Self::Image {
254            data,
255            mime_type: mime_type.into(),
256        }
257    }
258}
259
260/// Resource content in tool result
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct ResourceContent {
263    /// MIME type
264    #[serde(rename = "mimeType")]
265    pub mime_type: String,
266    /// Text content (for text resources)
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub text: Option<String>,
269    /// Binary content as base64 (for binary resources)
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub blob: Option<String>,
272}
273
274/// Server status information
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct ServerStatus {
277    /// Server name
278    pub name: String,
279    /// Server version
280    pub version: String,
281    /// Uptime in seconds
282    pub uptime_secs: u64,
283    /// Whether server is healthy
284    pub healthy: bool,
285    /// Memory usage in bytes (if available)
286    pub memory_bytes: Option<u64>,
287    /// Number of active connections
288    pub active_connections: u32,
289    /// Total requests handled
290    pub total_requests: u64,
291}
292
293impl ServerStatus {
294    /// Create a new server status
295    pub fn new(start_time: Instant) -> Self {
296        Self {
297            name: "reasonkit-web".to_string(),
298            version: env!("CARGO_PKG_VERSION").to_string(),
299            uptime_secs: start_time.elapsed().as_secs(),
300            healthy: true,
301            memory_bytes: None,
302            active_connections: 0,
303            total_requests: 0,
304        }
305    }
306
307    /// Format uptime as human-readable string
308    pub fn uptime_formatted(&self) -> String {
309        let secs = self.uptime_secs;
310        if secs < 60 {
311            format!("{}s", secs)
312        } else if secs < 3600 {
313            format!("{}m {}s", secs / 60, secs % 60)
314        } else if secs < 86400 {
315            format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
316        } else {
317            format!("{}d {}h", secs / 86400, (secs % 86400) / 3600)
318        }
319    }
320
321    /// Format memory usage as human-readable string
322    pub fn memory_formatted(&self) -> Option<String> {
323        self.memory_bytes.map(|bytes| {
324            if bytes < 1024 {
325                format!("{} B", bytes)
326            } else if bytes < 1024 * 1024 {
327                format!("{:.1} KB", bytes as f64 / 1024.0)
328            } else if bytes < 1024 * 1024 * 1024 {
329                format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
330            } else {
331                format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
332            }
333        })
334    }
335}
336
337/// Feed event for server-sent events
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct FeedEvent {
340    /// Event type
341    #[serde(rename = "type")]
342    pub event_type: FeedEventType,
343    /// Event timestamp (Unix epoch seconds)
344    pub timestamp: u64,
345    /// Event data
346    pub data: Value,
347}
348
349/// Types of feed events
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
351#[serde(rename_all = "lowercase")]
352pub enum FeedEventType {
353    /// Heartbeat ping
354    Heartbeat,
355    /// Status update
356    Status,
357    /// Tool execution started
358    ToolStart,
359    /// Tool execution completed
360    ToolComplete,
361    /// Error occurred
362    Error,
363    /// Server shutdown
364    Shutdown,
365}
366
367impl FeedEvent {
368    /// Create a heartbeat event
369    pub fn heartbeat() -> Self {
370        Self {
371            event_type: FeedEventType::Heartbeat,
372            timestamp: std::time::SystemTime::now()
373                .duration_since(std::time::UNIX_EPOCH)
374                .unwrap_or(Duration::ZERO)
375                .as_secs(),
376            data: serde_json::json!({"status": "ok"}),
377        }
378    }
379
380    /// Create a status event
381    pub fn status(status: &ServerStatus) -> Self {
382        Self {
383            event_type: FeedEventType::Status,
384            timestamp: std::time::SystemTime::now()
385                .duration_since(std::time::UNIX_EPOCH)
386                .unwrap_or(Duration::ZERO)
387                .as_secs(),
388            data: serde_json::to_value(status).unwrap_or(Value::Null),
389        }
390    }
391
392    /// Create a tool start event
393    pub fn tool_start(tool_name: &str) -> Self {
394        Self {
395            event_type: FeedEventType::ToolStart,
396            timestamp: std::time::SystemTime::now()
397                .duration_since(std::time::UNIX_EPOCH)
398                .unwrap_or(Duration::ZERO)
399                .as_secs(),
400            data: serde_json::json!({"tool": tool_name}),
401        }
402    }
403
404    /// Create a tool complete event
405    pub fn tool_complete(tool_name: &str, success: bool, duration_ms: u64) -> Self {
406        Self {
407            event_type: FeedEventType::ToolComplete,
408            timestamp: std::time::SystemTime::now()
409                .duration_since(std::time::UNIX_EPOCH)
410                .unwrap_or(Duration::ZERO)
411                .as_secs(),
412            data: serde_json::json!({
413                "tool": tool_name,
414                "success": success,
415                "duration_ms": duration_ms
416            }),
417        }
418    }
419
420    /// Create an error event
421    pub fn error(message: &str) -> Self {
422        Self {
423            event_type: FeedEventType::Error,
424            timestamp: std::time::SystemTime::now()
425                .duration_since(std::time::UNIX_EPOCH)
426                .unwrap_or(Duration::ZERO)
427                .as_secs(),
428            data: serde_json::json!({"error": message}),
429        }
430    }
431}
432
433/// Heartbeat configuration
434pub struct HeartbeatConfig {
435    /// Interval between heartbeats
436    pub interval: Duration,
437    /// Maximum missed heartbeats before disconnect
438    pub max_missed: u32,
439}
440
441impl Default for HeartbeatConfig {
442    fn default() -> Self {
443        Self {
444            interval: Duration::from_secs(30),
445            max_missed: 3,
446        }
447    }
448}
449
450impl HeartbeatConfig {
451    /// Create a new heartbeat config with custom interval
452    pub fn with_interval(interval_secs: u64) -> Self {
453        Self {
454            interval: Duration::from_secs(interval_secs),
455            max_missed: 3,
456        }
457    }
458
459    /// Get interval in milliseconds
460    pub fn interval_ms(&self) -> u64 {
461        self.interval.as_millis() as u64
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    // ========================================================================
470    // JsonRpcRequest Tests
471    // ========================================================================
472
473    #[test]
474    fn test_jsonrpc_request_deserialize() {
475        let json = r#"{"jsonrpc":"2.0","method":"test","id":1}"#;
476        let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
477        assert_eq!(req.method, "test");
478        assert_eq!(req.id, Some(serde_json::json!(1)));
479    }
480
481    #[test]
482    fn test_jsonrpc_request_with_params() {
483        let json = r#"{"jsonrpc":"2.0","method":"test","params":{"foo":"bar"},"id":1}"#;
484        let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
485        assert!(req.params.is_some());
486        assert_eq!(req.params.unwrap()["foo"], "bar");
487    }
488
489    #[test]
490    fn test_jsonrpc_request_notification() {
491        let json = r#"{"jsonrpc":"2.0","method":"notify"}"#;
492        let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
493        assert!(req.id.is_none());
494        assert!(req.params.is_none());
495    }
496
497    // ========================================================================
498    // JsonRpcResponse Tests
499    // ========================================================================
500
501    #[test]
502    fn test_jsonrpc_response_success() {
503        let resp =
504            JsonRpcResponse::success(Some(serde_json::json!(1)), serde_json::json!({"ok": true}));
505        let json = serde_json::to_string(&resp).unwrap();
506        assert!(json.contains("\"result\""));
507        assert!(!json.contains("\"error\""));
508    }
509
510    #[test]
511    fn test_jsonrpc_response_error() {
512        let resp = JsonRpcResponse::error(Some(serde_json::json!(1)), -32600, "Invalid");
513        let json = serde_json::to_string(&resp).unwrap();
514        assert!(json.contains("\"error\""));
515        assert!(json.contains("-32600"));
516    }
517
518    #[test]
519    fn test_jsonrpc_response_parse_error() {
520        let resp = JsonRpcResponse::parse_error();
521        assert!(resp.error.is_some());
522        assert_eq!(resp.error.as_ref().unwrap().code, -32700);
523    }
524
525    #[test]
526    fn test_jsonrpc_response_method_not_found() {
527        let resp = JsonRpcResponse::method_not_found(Some(serde_json::json!(1)), "unknown");
528        assert!(resp.error.is_some());
529        assert_eq!(resp.error.as_ref().unwrap().code, -32601);
530        assert!(resp.error.as_ref().unwrap().message.contains("unknown"));
531    }
532
533    #[test]
534    fn test_jsonrpc_response_invalid_params() {
535        let resp = JsonRpcResponse::invalid_params(Some(serde_json::json!(1)), "missing url");
536        assert!(resp.error.is_some());
537        assert_eq!(resp.error.as_ref().unwrap().code, -32602);
538    }
539
540    #[test]
541    fn test_jsonrpc_response_internal_error() {
542        let resp = JsonRpcResponse::internal_error(Some(serde_json::json!(1)), "boom");
543        assert!(resp.error.is_some());
544        assert_eq!(resp.error.as_ref().unwrap().code, -32603);
545    }
546
547    // ========================================================================
548    // ToolCallResult Tests
549    // ========================================================================
550
551    #[test]
552    fn test_tool_call_result_text() {
553        let result = ToolCallResult::text("Hello, world!");
554        assert!(!result.is_error);
555        assert_eq!(result.content.len(), 1);
556    }
557
558    #[test]
559    fn test_tool_call_result_error() {
560        let result = ToolCallResult::error("Something went wrong");
561        assert!(result.is_error);
562    }
563
564    #[test]
565    fn test_tool_call_result_image() {
566        let result = ToolCallResult::image("base64data".to_string(), "image/png");
567        assert!(!result.is_error);
568        assert_eq!(result.content.len(), 1);
569    }
570
571    #[test]
572    fn test_tool_call_result_multi() {
573        let content = vec![ToolContent::text("Hello"), ToolContent::text("World")];
574        let result = ToolCallResult::multi(content);
575        assert!(!result.is_error);
576        assert_eq!(result.content.len(), 2);
577    }
578
579    // ========================================================================
580    // ToolContent Tests
581    // ========================================================================
582
583    #[test]
584    fn test_tool_content_serialize() {
585        let content = ToolContent::text("Hello");
586        let json = serde_json::to_string(&content).unwrap();
587        assert!(json.contains("\"type\":\"text\""));
588        assert!(json.contains("\"text\":\"Hello\""));
589    }
590
591    #[test]
592    fn test_tool_content_image() {
593        let content = ToolContent::image("data".to_string(), "image/png");
594        let json = serde_json::to_string(&content).unwrap();
595        assert!(json.contains("\"type\":\"image\""));
596        assert!(json.contains("\"mimeType\":\"image/png\""));
597    }
598
599    // ========================================================================
600    // MCP Capabilities Tests
601    // ========================================================================
602
603    #[test]
604    fn test_mcp_capabilities() {
605        let caps = McpCapabilities::default();
606        assert!(!caps.tools.list_changed);
607        assert!(caps.resources.is_none());
608    }
609
610    #[test]
611    fn test_mcp_server_info_default() {
612        let info = McpServerInfo::default();
613        assert_eq!(info.name, "reasonkit-web");
614        assert!(!info.version.is_empty());
615    }
616
617    // ========================================================================
618    // ServerStatus Tests
619    // ========================================================================
620
621    #[test]
622    fn test_uptime_calculation() {
623        let start = Instant::now();
624        std::thread::sleep(std::time::Duration::from_millis(10));
625        let status = ServerStatus::new(start);
626
627        // Touch value to ensure it's set
628        let _ = status.uptime_secs;
629        assert!(status.healthy);
630    }
631
632    #[test]
633    fn test_uptime_formatted_seconds() {
634        let mut status = ServerStatus::new(Instant::now());
635        status.uptime_secs = 45;
636        assert_eq!(status.uptime_formatted(), "45s");
637    }
638
639    #[test]
640    fn test_uptime_formatted_minutes() {
641        let mut status = ServerStatus::new(Instant::now());
642        status.uptime_secs = 125; // 2m 5s
643        assert_eq!(status.uptime_formatted(), "2m 5s");
644    }
645
646    #[test]
647    fn test_uptime_formatted_hours() {
648        let mut status = ServerStatus::new(Instant::now());
649        status.uptime_secs = 3725; // 1h 2m
650        assert_eq!(status.uptime_formatted(), "1h 2m");
651    }
652
653    #[test]
654    fn test_uptime_formatted_days() {
655        let mut status = ServerStatus::new(Instant::now());
656        status.uptime_secs = 90061; // 1d 1h
657        assert_eq!(status.uptime_formatted(), "1d 1h");
658    }
659
660    #[test]
661    fn test_memory_usage_format_bytes() {
662        let mut status = ServerStatus::new(Instant::now());
663        status.memory_bytes = Some(512);
664        assert_eq!(status.memory_formatted(), Some("512 B".to_string()));
665    }
666
667    #[test]
668    fn test_memory_usage_format_kilobytes() {
669        let mut status = ServerStatus::new(Instant::now());
670        status.memory_bytes = Some(2048);
671        assert_eq!(status.memory_formatted(), Some("2.0 KB".to_string()));
672    }
673
674    #[test]
675    fn test_memory_usage_format_megabytes() {
676        let mut status = ServerStatus::new(Instant::now());
677        status.memory_bytes = Some(52_428_800); // 50 MB
678        assert_eq!(status.memory_formatted(), Some("50.0 MB".to_string()));
679    }
680
681    #[test]
682    fn test_memory_usage_format_gigabytes() {
683        let mut status = ServerStatus::new(Instant::now());
684        status.memory_bytes = Some(2_147_483_648); // 2 GB
685        assert_eq!(status.memory_formatted(), Some("2.00 GB".to_string()));
686    }
687
688    #[test]
689    fn test_memory_usage_format_none() {
690        let status = ServerStatus::new(Instant::now());
691        assert!(status.memory_formatted().is_none());
692    }
693
694    #[test]
695    fn test_status_response_serialization() {
696        let status = ServerStatus {
697            name: "test-server".to_string(),
698            version: "1.0.0".to_string(),
699            uptime_secs: 3600,
700            healthy: true,
701            memory_bytes: Some(1048576),
702            active_connections: 5,
703            total_requests: 100,
704        };
705
706        let json = serde_json::to_string(&status).unwrap();
707        assert!(json.contains("\"name\":\"test-server\""));
708        assert!(json.contains("\"healthy\":true"));
709        assert!(json.contains("\"uptime_secs\":3600"));
710    }
711
712    // ========================================================================
713    // FeedEvent Tests
714    // ========================================================================
715
716    #[test]
717    fn test_feed_event_serialization() {
718        let event = FeedEvent::heartbeat();
719        let json = serde_json::to_string(&event).unwrap();
720        assert!(json.contains("\"type\":\"heartbeat\""));
721        assert!(json.contains("\"timestamp\""));
722    }
723
724    #[test]
725    fn test_feed_event_heartbeat() {
726        let event = FeedEvent::heartbeat();
727        assert_eq!(event.event_type, FeedEventType::Heartbeat);
728        assert!(event.timestamp > 0);
729    }
730
731    #[test]
732    fn test_feed_event_tool_start() {
733        let event = FeedEvent::tool_start("web_navigate");
734        assert_eq!(event.event_type, FeedEventType::ToolStart);
735        assert_eq!(event.data["tool"], "web_navigate");
736    }
737
738    #[test]
739    fn test_feed_event_tool_complete() {
740        let event = FeedEvent::tool_complete("web_screenshot", true, 500);
741        assert_eq!(event.event_type, FeedEventType::ToolComplete);
742        assert_eq!(event.data["tool"], "web_screenshot");
743        assert_eq!(event.data["success"], true);
744        assert_eq!(event.data["duration_ms"], 500);
745    }
746
747    #[test]
748    fn test_feed_event_error() {
749        let event = FeedEvent::error("Connection failed");
750        assert_eq!(event.event_type, FeedEventType::Error);
751        assert_eq!(event.data["error"], "Connection failed");
752    }
753
754    #[test]
755    fn test_feed_event_type_serialization() {
756        assert_eq!(
757            serde_json::to_string(&FeedEventType::Heartbeat).unwrap(),
758            "\"heartbeat\""
759        );
760        assert_eq!(
761            serde_json::to_string(&FeedEventType::Status).unwrap(),
762            "\"status\""
763        );
764        assert_eq!(
765            serde_json::to_string(&FeedEventType::ToolStart).unwrap(),
766            "\"toolstart\""
767        );
768        assert_eq!(
769            serde_json::to_string(&FeedEventType::ToolComplete).unwrap(),
770            "\"toolcomplete\""
771        );
772        assert_eq!(
773            serde_json::to_string(&FeedEventType::Error).unwrap(),
774            "\"error\""
775        );
776        assert_eq!(
777            serde_json::to_string(&FeedEventType::Shutdown).unwrap(),
778            "\"shutdown\""
779        );
780    }
781
782    // ========================================================================
783    // HeartbeatConfig Tests
784    // ========================================================================
785
786    #[test]
787    fn test_heartbeat_interval_default() {
788        let config = HeartbeatConfig::default();
789        assert_eq!(config.interval, Duration::from_secs(30));
790        assert_eq!(config.max_missed, 3);
791    }
792
793    #[test]
794    fn test_heartbeat_interval_custom() {
795        let config = HeartbeatConfig::with_interval(60);
796        assert_eq!(config.interval, Duration::from_secs(60));
797        assert_eq!(config.interval_ms(), 60000);
798    }
799
800    #[test]
801    fn test_heartbeat_interval_ms() {
802        let config = HeartbeatConfig::default();
803        assert_eq!(config.interval_ms(), 30000);
804    }
805
806    // ========================================================================
807    // McpToolDefinition Tests
808    // ========================================================================
809
810    #[test]
811    fn test_tool_definition_serialization() {
812        let tool = McpToolDefinition {
813            name: "test_tool".to_string(),
814            description: "A test tool".to_string(),
815            input_schema: serde_json::json!({
816                "type": "object",
817                "properties": {
818                    "url": {"type": "string"}
819                }
820            }),
821        };
822
823        let json = serde_json::to_string(&tool).unwrap();
824        assert!(json.contains("\"name\":\"test_tool\""));
825        assert!(json.contains("\"inputSchema\""));
826    }
827
828    // ========================================================================
829    // Edge Cases Tests
830    // ========================================================================
831
832    #[test]
833    fn test_jsonrpc_response_null_id() {
834        let resp = JsonRpcResponse::success(None, serde_json::json!("ok"));
835        let json = serde_json::to_string(&resp).unwrap();
836        // id should be omitted when None
837        assert!(!json.contains("\"id\""));
838    }
839
840    #[test]
841    fn test_server_status_zero_uptime() {
842        let mut status = ServerStatus::new(Instant::now());
843        status.uptime_secs = 0;
844        assert_eq!(status.uptime_formatted(), "0s");
845    }
846
847    #[test]
848    fn test_feed_event_status() {
849        let status = ServerStatus {
850            name: "test".to_string(),
851            version: "1.0".to_string(),
852            uptime_secs: 100,
853            healthy: true,
854            memory_bytes: None,
855            active_connections: 0,
856            total_requests: 50,
857        };
858        let event = FeedEvent::status(&status);
859        assert_eq!(event.event_type, FeedEventType::Status);
860    }
861}