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}