Skip to main content

bamboo_agent/agent/mcp/
types.rs

1//! MCP Types and Data Structures
2//!
3//! This module defines the core types used throughout the MCP (Model Context Protocol)
4//! implementation, including tool definitions, execution results, server status tracking,
5//! and event notifications.
6//!
7//! # Core Types
8//!
9//! - [`McpTool`]: Tool metadata received from MCP servers
10//! - [`McpCallResult`]: Result of tool execution
11//! - [`McpContentItem`]: Content returned by tools (text, images, resources)
12//! - [`ServerStatus`]: Runtime status of MCP servers
13//! - [`RuntimeInfo`]: Detailed server health and statistics
14//! - [`McpEvent`]: Events for server state changes
15//!
16//! # Architecture
17//!
18//! The MCP client manages multiple server connections, each providing tools.
19//! Tools can be invoked and return structured content. The manager tracks
20//! server health and emits events for monitoring.
21
22use chrono::{DateTime, Utc};
23use serde::{Deserialize, Serialize};
24
25/// MCP tool metadata received from a server.
26///
27/// Represents a tool discovered from an MCP server during tool listing.
28/// Contains the tool's identity, description, and parameter schema.
29///
30/// # Fields
31///
32/// * `name` - Unique tool identifier within the server
33/// * `description` - Human-readable description of the tool's purpose
34/// * `parameters` - JSON Schema describing expected input parameters
35///
36/// # Example
37///
38/// ```ignore
39/// let tool = McpTool {
40///     name: "read_file".to_string(),
41///     description: "Read file contents from the filesystem".to_string(),
42///     parameters: json!({
43///         "type": "object",
44///         "properties": {
45///             "path": {"type": "string", "description": "File path"}
46///         },
47///         "required": ["path"]
48///     }),
49/// };
50/// ```
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct McpTool {
53    /// Unique tool identifier within the server
54    pub name: String,
55    /// Human-readable description of what the tool does
56    pub description: String,
57    /// JSON Schema describing the tool's input parameters
58    pub parameters: serde_json::Value,
59}
60
61/// Result of calling an MCP tool.
62///
63/// Contains the content returned by the tool execution and a flag
64/// indicating whether an error occurred.
65///
66/// # Fields
67///
68/// * `content` - List of content items (text, images, or resources)
69/// * `is_error` - Whether the tool execution failed
70///
71/// # Error Handling
72///
73/// When `is_error` is true, the content typically contains error messages
74/// or diagnostic information. Clients should check this flag before
75/// processing the content.
76///
77/// # Example
78///
79/// ```ignore
80/// let result = McpCallResult {
81///     content: vec![McpContentItem::Text {
82///         text: "File contents here".to_string(),
83///     }],
84///     is_error: false,
85/// };
86///
87/// if !result.is_error {
88///     for item in result.content {
89///         // Process content
90///     }
91/// }
92/// ```
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct McpCallResult {
95    /// Content items returned by the tool
96    pub content: Vec<McpContentItem>,
97    /// Whether the tool execution encountered an error
98    #[serde(default)]
99    pub is_error: bool,
100}
101
102/// Content item returned by MCP tools.
103///
104/// Tools can return different types of content: text, images, or resources.
105/// This enum provides a tagged union for all content types.
106///
107/// # Variants
108///
109/// * `Text` - Plain text content
110/// * `Image` - Image data with MIME type (base64-encoded)
111/// * `Resource` - Reference to a resource (file, URL, etc.)
112///
113/// # Example
114///
115/// ```ignore
116/// // Text content
117/// let text = McpContentItem::Text {
118///     text: "Hello, world!".to_string(),
119/// };
120///
121/// // Image content
122/// let image = McpContentItem::Image {
123///     data: base64_encoded_data,
124///     mime_type: "image/png".to_string(),
125/// };
126///
127/// // Resource reference
128/// let resource = McpContentItem::Resource {
129///     resource: McpResource {
130///         uri: "file:///path/to/file.txt".to_string(),
131///         mime_type: Some("text/plain".to_string()),
132///         text: Some("file contents".to_string()),
133///         blob: None,
134///     },
135/// };
136/// ```
137#[derive(Debug, Clone, Serialize, Deserialize)]
138#[serde(tag = "type")]
139pub enum McpContentItem {
140    /// Plain text content
141    #[serde(rename = "text")]
142    Text {
143        /// The text content
144        text: String,
145    },
146    /// Image content (base64-encoded)
147    #[serde(rename = "image")]
148    Image {
149        /// Base64-encoded image data
150        data: String,
151        /// MIME type of the image (e.g., "image/png")
152        mime_type: String,
153    },
154    /// Resource reference
155    #[serde(rename = "resource")]
156    Resource {
157        /// The resource being referenced
158        resource: McpResource,
159    },
160}
161
162/// Resource reference in MCP.
163///
164/// Represents a resource that can be accessed through MCP, such as a file,
165/// URL, or other data source. Resources can contain either text or binary data.
166///
167/// # Fields
168///
169/// * `uri` - Unique identifier for the resource (e.g., "file:///path/to/file")
170/// * `mime_type` - Optional MIME type of the resource content
171/// * `text` - Text content (if the resource is text-based)
172/// * `blob` - Binary content as base64 (if the resource is binary)
173///
174/// # Note
175///
176/// Either `text` or `blob` should be present, but not both.
177///
178/// # Example
179///
180/// ```ignore
181/// // Text file resource
182/// let text_resource = McpResource {
183///     uri: "file:///docs/readme.txt".to_string(),
184///     mime_type: Some("text/plain".to_string()),
185///     text: Some("File contents here".to_string()),
186///     blob: None,
187/// };
188///
189/// // Binary file resource
190/// let binary_resource = McpResource {
191///     uri: "file:///images/photo.png".to_string(),
192///     mime_type: Some("image/png".to_string()),
193///     text: None,
194///     blob: Some(base64_encoded_data),
195/// };
196/// ```
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct McpResource {
199    /// Unique resource identifier (URI format)
200    pub uri: String,
201    /// MIME type of the resource content
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub mime_type: Option<String>,
204    /// Text content (for text-based resources)
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub text: Option<String>,
207    /// Binary content as base64 (for binary resources)
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub blob: Option<String>,
210}
211
212/// Server runtime status indicator.
213///
214/// Represents the current operational state of an MCP server connection.
215/// Used for health monitoring and connection management.
216///
217/// # States
218///
219/// * `Connecting` - Server is being initialized
220/// * `Ready` - Server is operational and accepting requests
221/// * `Degraded` - Server is running but with limited functionality
222/// * `Stopped` - Server has been shut down
223/// * `Error` - Server encountered a critical error
224///
225/// # Example
226///
227/// ```ignore
228/// match server_status {
229///     ServerStatus::Ready => println!("Server is healthy"),
230///     ServerStatus::Error => eprintln!("Server failed"),
231///     _ => println!("Server is transitioning"),
232/// }
233/// ```
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
235#[serde(rename_all = "lowercase")]
236pub enum ServerStatus {
237    /// Server is being initialized
238    Connecting,
239    /// Server is operational and ready for requests
240    Ready,
241    /// Server is running but with degraded functionality
242    Degraded,
243    /// Server has been shut down
244    Stopped,
245    /// Server encountered a critical error
246    Error,
247}
248
249impl std::fmt::Display for ServerStatus {
250    /// Formats the status as a lowercase string.
251    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252        match self {
253            ServerStatus::Connecting => write!(f, "connecting"),
254            ServerStatus::Ready => write!(f, "ready"),
255            ServerStatus::Degraded => write!(f, "degraded"),
256            ServerStatus::Stopped => write!(f, "stopped"),
257            ServerStatus::Error => write!(f, "error"),
258        }
259    }
260}
261
262/// Runtime information for an MCP server.
263///
264/// Contains comprehensive health and statistics data for a server connection,
265/// including status, timestamps, tool counts, and error information.
266///
267/// # Fields
268///
269/// * `status` - Current operational status of the server
270/// * `last_error` - Most recent error message (if any)
271/// * `connected_at` - Timestamp when the server connected
272/// * `disconnected_at` - Timestamp when the server disconnected
273/// * `tool_count` - Number of tools provided by this server
274/// * `restart_count` - Number of times the server has been restarted
275/// * `last_ping_at` - Timestamp of the last successful ping
276///
277/// # Monitoring
278///
279/// This structure is used for health monitoring and diagnostics,
280/// providing visibility into server connection lifecycle and performance.
281///
282/// # Example
283///
284/// ```ignore
285/// let info = RuntimeInfo {
286///     status: ServerStatus::Ready,
287///     connected_at: Some(Utc::now()),
288///     tool_count: 5,
289///     ..Default::default()
290/// };
291///
292/// if info.status == ServerStatus::Ready {
293///     println!("Server has {} tools available", info.tool_count);
294/// }
295/// ```
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct RuntimeInfo {
298    /// Current operational status
299    pub status: ServerStatus,
300    /// Most recent error message (if any)
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub last_error: Option<String>,
303    /// Timestamp when the server connected
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub connected_at: Option<DateTime<Utc>>,
306    /// Timestamp when the server disconnected
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub disconnected_at: Option<DateTime<Utc>>,
309    /// Number of tools provided by this server
310    pub tool_count: usize,
311    /// Number of times the server has been restarted
312    pub restart_count: u32,
313    /// Timestamp of the last successful ping
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub last_ping_at: Option<DateTime<Utc>>,
316}
317
318impl Default for RuntimeInfo {
319    /// Creates default runtime info for a stopped server.
320    ///
321    /// Initializes with `Stopped` status and zero counters.
322    fn default() -> Self {
323        Self {
324            status: ServerStatus::Stopped,
325            last_error: None,
326            connected_at: None,
327            disconnected_at: None,
328            tool_count: 0,
329            restart_count: 0,
330            last_ping_at: None,
331        }
332    }
333}
334
335/// Tool alias mapping for namespaced tool access.
336///
337/// Maps a fully-qualified tool name (with server prefix) to the original
338/// tool name on a specific server. This enables multiple servers to provide
339/// tools with the same name without conflicts.
340///
341/// # Format
342///
343/// The alias format is: `mcp__<server_id>__<original_name>`
344///
345/// # Fields
346///
347/// * `alias` - Fully-qualified tool name with server prefix
348/// * `server_id` - Identifier of the server providing this tool
349/// * `original_name` - Original tool name on the server
350///
351/// # Example
352///
353/// ```ignore
354/// let alias = ToolAlias {
355///     alias: "mcp__filesystem__read_file".to_string(),
356///     server_id: "filesystem".to_string(),
357///     original_name: "read_file".to_string(),
358/// };
359///
360/// // When the user calls "mcp__filesystem__read_file",
361/// // it maps to the "read_file" tool on the "filesystem" server
362/// ```
363#[derive(Debug, Clone)]
364pub struct ToolAlias {
365    /// Fully-qualified tool name (mcp__<server>__<tool>)
366    pub alias: String,
367    /// Server providing this tool
368    pub server_id: String,
369    /// Original tool name on the server
370    pub original_name: String,
371}
372
373/// Event emitted by the MCP manager.
374///
375/// Represents state changes and operations in the MCP system.
376/// Events are used for monitoring, logging, and reactive updates.
377///
378/// # Variants
379///
380/// * `ServerStatusChanged` - Server connection status changed
381/// * `ToolsChanged` - Server's tool list was updated
382/// * `ToolExecuted` - A tool was invoked (success or failure)
383///
384/// # Monitoring
385///
386/// Subscribe to these events to monitor server health, track tool usage,
387/// or implement reactive UI updates.
388///
389/// # Example
390///
391/// ```ignore
392/// match event {
393///     McpEvent::ServerStatusChanged { server_id, status, error } => {
394///         println!("Server {} is now {:?}", server_id, status);
395///     }
396///     McpEvent::ToolExecuted { server_id, tool_name, success } => {
397///         println!("Tool {} on {} - success: {}", tool_name, server_id, success);
398///     }
399///     _ => {}
400/// }
401/// ```
402#[derive(Debug, Clone, Serialize)]
403#[serde(tag = "type")]
404pub enum McpEvent {
405    /// Server connection status changed
406    ServerStatusChanged {
407        /// Server identifier
408        server_id: String,
409        /// New status
410        status: ServerStatus,
411        /// Error message (if status is Error)
412        #[serde(skip_serializing_if = "Option::is_none")]
413        error: Option<String>,
414    },
415    /// Server's available tools changed
416    ToolsChanged {
417        /// Server identifier
418        server_id: String,
419        /// List of available tool names
420        tools: Vec<String>,
421    },
422    /// Tool was executed
423    ToolExecuted {
424        /// Server identifier
425        server_id: String,
426        /// Name of the executed tool
427        tool_name: String,
428        /// Whether execution succeeded
429        success: bool,
430    },
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn test_mcp_tool() {
439        let tool = McpTool {
440            name: "read_file".to_string(),
441            description: "Read a file".to_string(),
442            parameters: serde_json::json!({"type": "object"}),
443        };
444        assert_eq!(tool.name, "read_file");
445        assert_eq!(tool.description, "Read a file");
446    }
447
448    #[test]
449    fn test_mcp_call_result() {
450        let result = McpCallResult {
451            content: vec![McpContentItem::Text {
452                text: "success".to_string(),
453            }],
454            is_error: false,
455        };
456        assert!(!result.is_error);
457        assert_eq!(result.content.len(), 1);
458    }
459
460    #[test]
461    fn test_mcp_call_result_error() {
462        let result = McpCallResult {
463            content: vec![McpContentItem::Text {
464                text: "error occurred".to_string(),
465            }],
466            is_error: true,
467        };
468        assert!(result.is_error);
469    }
470
471    #[test]
472    fn test_mcp_content_item_text() {
473        let item = McpContentItem::Text {
474            text: "hello".to_string(),
475        };
476        match item {
477            McpContentItem::Text { text } => assert_eq!(text, "hello"),
478            _ => panic!("Expected Text variant"),
479        }
480    }
481
482    #[test]
483    fn test_mcp_content_item_image() {
484        let item = McpContentItem::Image {
485            data: "base64data".to_string(),
486            mime_type: "image/png".to_string(),
487        };
488        match item {
489            McpContentItem::Image { data, mime_type } => {
490                assert_eq!(data, "base64data");
491                assert_eq!(mime_type, "image/png");
492            }
493            _ => panic!("Expected Image variant"),
494        }
495    }
496
497    #[test]
498    fn test_mcp_content_item_resource() {
499        let item = McpContentItem::Resource {
500            resource: McpResource {
501                uri: "file:///test.txt".to_string(),
502                mime_type: Some("text/plain".to_string()),
503                text: Some("content".to_string()),
504                blob: None,
505            },
506        };
507        match item {
508            McpContentItem::Resource { resource } => {
509                assert_eq!(resource.uri, "file:///test.txt");
510            }
511            _ => panic!("Expected Resource variant"),
512        }
513    }
514
515    #[test]
516    fn test_mcp_resource() {
517        let resource = McpResource {
518            uri: "file:///test.txt".to_string(),
519            mime_type: Some("text/plain".to_string()),
520            text: Some("file content".to_string()),
521            blob: None,
522        };
523        assert_eq!(resource.uri, "file:///test.txt");
524        assert_eq!(resource.mime_type, Some("text/plain".to_string()));
525        assert_eq!(resource.text, Some("file content".to_string()));
526    }
527
528    #[test]
529    fn test_server_status_variants() {
530        assert_eq!(ServerStatus::Connecting, ServerStatus::Connecting);
531        assert_eq!(ServerStatus::Ready, ServerStatus::Ready);
532        assert_eq!(ServerStatus::Degraded, ServerStatus::Degraded);
533        assert_eq!(ServerStatus::Stopped, ServerStatus::Stopped);
534        assert_eq!(ServerStatus::Error, ServerStatus::Error);
535    }
536
537    #[test]
538    fn test_server_status_display() {
539        assert_eq!(format!("{}", ServerStatus::Connecting), "connecting");
540        assert_eq!(format!("{}", ServerStatus::Ready), "ready");
541        assert_eq!(format!("{}", ServerStatus::Degraded), "degraded");
542        assert_eq!(format!("{}", ServerStatus::Stopped), "stopped");
543        assert_eq!(format!("{}", ServerStatus::Error), "error");
544    }
545
546    #[test]
547    fn test_runtime_info_default() {
548        let info = RuntimeInfo::default();
549        assert_eq!(info.status, ServerStatus::Stopped);
550        assert!(info.last_error.is_none());
551        assert!(info.connected_at.is_none());
552        assert!(info.disconnected_at.is_none());
553        assert_eq!(info.tool_count, 0);
554        assert_eq!(info.restart_count, 0);
555        assert!(info.last_ping_at.is_none());
556    }
557
558    #[test]
559    fn test_runtime_info_custom() {
560        let info = RuntimeInfo {
561            status: ServerStatus::Ready,
562            last_error: None,
563            connected_at: Some(Utc::now()),
564            disconnected_at: None,
565            tool_count: 5,
566            restart_count: 0,
567            last_ping_at: Some(Utc::now()),
568        };
569        assert_eq!(info.status, ServerStatus::Ready);
570        assert_eq!(info.tool_count, 5);
571    }
572
573    #[test]
574    fn test_tool_alias() {
575        let alias = ToolAlias {
576            alias: "mcp__server__tool".to_string(),
577            server_id: "server".to_string(),
578            original_name: "tool".to_string(),
579        };
580        assert_eq!(alias.alias, "mcp__server__tool");
581        assert_eq!(alias.server_id, "server");
582        assert_eq!(alias.original_name, "tool");
583    }
584
585    #[test]
586    fn test_mcp_event_server_status_changed() {
587        let event = McpEvent::ServerStatusChanged {
588            server_id: "test-server".to_string(),
589            status: ServerStatus::Ready,
590            error: None,
591        };
592        match event {
593            McpEvent::ServerStatusChanged {
594                server_id,
595                status,
596                error,
597            } => {
598                assert_eq!(server_id, "test-server");
599                assert_eq!(status, ServerStatus::Ready);
600                assert!(error.is_none());
601            }
602            _ => panic!("Expected ServerStatusChanged variant"),
603        }
604    }
605
606    #[test]
607    fn test_mcp_event_tools_changed() {
608        let event = McpEvent::ToolsChanged {
609            server_id: "test-server".to_string(),
610            tools: vec!["tool1".to_string(), "tool2".to_string()],
611        };
612        match event {
613            McpEvent::ToolsChanged { server_id, tools } => {
614                assert_eq!(server_id, "test-server");
615                assert_eq!(tools.len(), 2);
616            }
617            _ => panic!("Expected ToolsChanged variant"),
618        }
619    }
620
621    #[test]
622    fn test_mcp_event_tool_executed() {
623        let event = McpEvent::ToolExecuted {
624            server_id: "test-server".to_string(),
625            tool_name: "test-tool".to_string(),
626            success: true,
627        };
628        match event {
629            McpEvent::ToolExecuted {
630                server_id,
631                tool_name,
632                success,
633            } => {
634                assert_eq!(server_id, "test-server");
635                assert_eq!(tool_name, "test-tool");
636                assert!(success);
637            }
638            _ => panic!("Expected ToolExecuted variant"),
639        }
640    }
641
642    #[test]
643    fn test_mcp_event_serialization() {
644        let event = McpEvent::ServerStatusChanged {
645            server_id: "test".to_string(),
646            status: ServerStatus::Ready,
647            error: None,
648        };
649        let json = serde_json::to_string(&event).unwrap();
650        assert!(json.contains("ServerStatusChanged"));
651        assert!(json.contains("test"));
652        assert!(json.contains("ready"));
653    }
654
655    #[test]
656    fn test_server_status_serialization() {
657        let status = ServerStatus::Ready;
658        let json = serde_json::to_string(&status).unwrap();
659        assert_eq!(json, "\"ready\"");
660    }
661
662    #[test]
663    fn test_runtime_info_serialization() {
664        let info = RuntimeInfo::default();
665        let json = serde_json::to_string(&info).unwrap();
666        assert!(json.contains("stopped"));
667        assert!(json.contains("tool_count"));
668    }
669}