mcp_protocol_sdk/core/
tool_metadata.rs

1//! Enhanced Tool Metadata System
2//!
3//! This module provides advanced metadata features for MCP tools including:
4//! - Tool behavior hints (readOnly, destructive, idempotent)
5//! - Tool categorization and tagging
6//! - Discovery and filtering capabilities
7//! - Performance metrics and tracking
8//! - Deprecation warnings and versioning
9
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use std::collections::{HashMap, HashSet};
13use std::sync::{Arc, RwLock};
14use std::time::Duration;
15
16/// Tool behavior hints for clients to understand tool characteristics
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
18pub struct ToolBehaviorHints {
19    /// Tool only reads data without making changes
20    #[serde(rename = "readOnlyHint", skip_serializing_if = "Option::is_none")]
21    pub read_only: Option<bool>,
22
23    /// Tool makes destructive changes that cannot be easily undone
24    #[serde(rename = "destructiveHint", skip_serializing_if = "Option::is_none")]
25    pub destructive: Option<bool>,
26
27    /// Tool produces the same output for the same input (no side effects)
28    #[serde(rename = "idempotentHint", skip_serializing_if = "Option::is_none")]
29    pub idempotent: Option<bool>,
30
31    /// Tool requires authentication or special permissions
32    #[serde(rename = "requiresAuthHint", skip_serializing_if = "Option::is_none")]
33    pub requires_auth: Option<bool>,
34
35    /// Tool may take a long time to execute
36    #[serde(rename = "longRunningHint", skip_serializing_if = "Option::is_none")]
37    pub long_running: Option<bool>,
38
39    /// Tool may consume significant system resources
40    #[serde(
41        rename = "resourceIntensiveHint",
42        skip_serializing_if = "Option::is_none"
43    )]
44    pub resource_intensive: Option<bool>,
45
46    /// Tool provides cacheable results
47    #[serde(rename = "cacheableHint", skip_serializing_if = "Option::is_none")]
48    pub cacheable: Option<bool>,
49}
50
51impl ToolBehaviorHints {
52    /// Create a new empty set of behavior hints
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// Mark tool as read-only (no destructive changes)
58    pub fn read_only(mut self) -> Self {
59        self.read_only = Some(true);
60        self
61    }
62
63    /// Mark tool as destructive (makes changes that cannot be easily undone)
64    pub fn destructive(mut self) -> Self {
65        self.destructive = Some(true);
66        self
67    }
68
69    /// Mark tool as idempotent (same input produces same output)
70    pub fn idempotent(mut self) -> Self {
71        self.idempotent = Some(true);
72        self
73    }
74
75    /// Mark tool as requiring authentication
76    pub fn requires_auth(mut self) -> Self {
77        self.requires_auth = Some(true);
78        self
79    }
80
81    /// Mark tool as potentially long-running
82    pub fn long_running(mut self) -> Self {
83        self.long_running = Some(true);
84        self
85    }
86
87    /// Mark tool as resource-intensive
88    pub fn resource_intensive(mut self) -> Self {
89        self.resource_intensive = Some(true);
90        self
91    }
92
93    /// Mark tool results as cacheable
94    pub fn cacheable(mut self) -> Self {
95        self.cacheable = Some(true);
96        self
97    }
98}
99
100/// Tool categorization for organization and discovery
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
102pub struct ToolCategory {
103    /// Primary category (e.g., "file", "network", "data", "ai")
104    pub primary: String,
105    /// Secondary category (e.g., "read", "write", "analyze", "transform")
106    pub secondary: Option<String>,
107    /// Custom tags for flexible categorization
108    pub tags: HashSet<String>,
109}
110
111impl ToolCategory {
112    /// Create a new tool category
113    pub fn new(primary: String) -> Self {
114        Self {
115            primary,
116            secondary: None,
117            tags: HashSet::new(),
118        }
119    }
120
121    /// Set secondary category
122    pub fn with_secondary(mut self, secondary: String) -> Self {
123        self.secondary = Some(secondary);
124        self
125    }
126
127    /// Add a tag
128    pub fn with_tag(mut self, tag: String) -> Self {
129        self.tags.insert(tag);
130        self
131    }
132
133    /// Add multiple tags
134    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
135        self.tags.extend(tags);
136        self
137    }
138
139    /// Check if category matches a filter
140    pub fn matches_filter(&self, filter: &CategoryFilter) -> bool {
141        // Check primary category
142        if let Some(ref primary) = filter.primary {
143            if !self.primary.contains(primary) {
144                return false;
145            }
146        }
147
148        // Check secondary category
149        if let Some(ref secondary) = filter.secondary {
150            match &self.secondary {
151                Some(s) => {
152                    if !s.contains(secondary) {
153                        return false;
154                    }
155                }
156                None => return false,
157            }
158        }
159
160        // Check tags (any match is sufficient)
161        if !filter.tags.is_empty() && !filter.tags.iter().any(|tag| self.tags.contains(tag)) {
162            return false;
163        }
164
165        true
166    }
167}
168
169/// Filter for tool discovery based on categories
170#[derive(Debug, Clone, Default)]
171pub struct CategoryFilter {
172    /// Filter by primary category (substring match)
173    pub primary: Option<String>,
174    /// Filter by secondary category (substring match)
175    pub secondary: Option<String>,
176    /// Filter by tags (any match)
177    pub tags: HashSet<String>,
178}
179
180impl CategoryFilter {
181    /// Create a new empty filter
182    pub fn new() -> Self {
183        Self::default()
184    }
185
186    /// Filter by primary category
187    pub fn with_primary(mut self, primary: String) -> Self {
188        self.primary = Some(primary);
189        self
190    }
191
192    /// Filter by secondary category
193    pub fn with_secondary(mut self, secondary: String) -> Self {
194        self.secondary = Some(secondary);
195        self
196    }
197
198    /// Filter by tag
199    pub fn with_tag(mut self, tag: String) -> Self {
200        self.tags.insert(tag);
201        self
202    }
203
204    /// Filter by multiple tags
205    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
206        self.tags.extend(tags);
207        self
208    }
209}
210
211/// Performance metrics for tool execution tracking
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct ToolPerformanceMetrics {
214    /// Total number of executions
215    pub execution_count: u64,
216    /// Total execution time across all calls
217    pub total_execution_time: Duration,
218    /// Average execution time
219    pub average_execution_time: Duration,
220    /// Minimum execution time recorded
221    pub min_execution_time: Duration,
222    /// Maximum execution time recorded
223    pub max_execution_time: Duration,
224    /// Number of successful executions
225    pub success_count: u64,
226    /// Number of failed executions
227    pub error_count: u64,
228    /// Success rate as percentage (0.0 to 100.0)
229    pub success_rate: f64,
230    /// Last execution timestamp
231    pub last_execution: Option<DateTime<Utc>>,
232    /// Recent execution times (last 10 executions)
233    pub recent_execution_times: Vec<Duration>,
234}
235
236impl Default for ToolPerformanceMetrics {
237    fn default() -> Self {
238        Self {
239            execution_count: 0,
240            total_execution_time: Duration::from_secs(0),
241            average_execution_time: Duration::from_secs(0),
242            min_execution_time: Duration::from_secs(u64::MAX),
243            max_execution_time: Duration::from_secs(0),
244            success_count: 0,
245            error_count: 0,
246            success_rate: 0.0,
247            last_execution: None,
248            recent_execution_times: Vec::new(),
249        }
250    }
251}
252
253impl ToolPerformanceMetrics {
254    /// Create new empty metrics
255    pub fn new() -> Self {
256        Self::default()
257    }
258
259    /// Record a successful execution
260    pub fn record_success(&mut self, execution_time: Duration) {
261        self.execution_count += 1;
262        self.success_count += 1;
263        self.record_execution_time(execution_time);
264        self.update_success_rate();
265        self.last_execution = Some(Utc::now());
266    }
267
268    /// Record a failed execution
269    pub fn record_error(&mut self, execution_time: Duration) {
270        self.execution_count += 1;
271        self.error_count += 1;
272        self.record_execution_time(execution_time);
273        self.update_success_rate();
274        self.last_execution = Some(Utc::now());
275    }
276
277    /// Record execution time and update statistics
278    fn record_execution_time(&mut self, execution_time: Duration) {
279        self.total_execution_time += execution_time;
280
281        // Update min/max
282        if execution_time < self.min_execution_time {
283            self.min_execution_time = execution_time;
284        }
285        if execution_time > self.max_execution_time {
286            self.max_execution_time = execution_time;
287        }
288
289        // Update average
290        if self.execution_count > 0 {
291            self.average_execution_time = self.total_execution_time / self.execution_count as u32;
292        }
293
294        // Update recent execution times (keep last 10)
295        self.recent_execution_times.push(execution_time);
296        if self.recent_execution_times.len() > 10 {
297            self.recent_execution_times.remove(0);
298        }
299    }
300
301    /// Update success rate percentage
302    fn update_success_rate(&mut self) {
303        if self.execution_count > 0 {
304            self.success_rate = (self.success_count as f64 / self.execution_count as f64) * 100.0;
305        }
306    }
307
308    /// Get recent average execution time (last 10 executions)
309    pub fn recent_average_execution_time(&self) -> Duration {
310        if self.recent_execution_times.is_empty() {
311            Duration::from_secs(0)
312        } else {
313            let total: Duration = self.recent_execution_times.iter().sum();
314            total / self.recent_execution_times.len() as u32
315        }
316    }
317}
318
319/// Tool deprecation information and versioning
320#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
321pub struct ToolDeprecation {
322    /// Whether the tool is deprecated
323    pub deprecated: bool,
324    /// Deprecation reason/message
325    pub reason: Option<String>,
326    /// Recommended replacement tool
327    pub replacement: Option<String>,
328    /// Date when tool was deprecated
329    pub deprecated_date: Option<DateTime<Utc>>,
330    /// Date when tool will be removed (if known)
331    pub removal_date: Option<DateTime<Utc>>,
332    /// Severity of deprecation warning
333    pub severity: DeprecationSeverity,
334}
335
336/// Severity levels for deprecation warnings
337#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
338pub enum DeprecationSeverity {
339    /// Tool is deprecated but still fully functional
340    #[default]
341    Low,
342    /// Tool may have reduced functionality or support
343    Medium,
344    /// Tool will be removed soon or has significant issues
345    High,
346    /// Tool is disabled or non-functional
347    Critical,
348}
349
350impl ToolDeprecation {
351    /// Create a new deprecation notice
352    pub fn new(reason: String) -> Self {
353        Self {
354            deprecated: true,
355            reason: Some(reason),
356            replacement: None,
357            deprecated_date: Some(Utc::now()),
358            removal_date: None,
359            severity: DeprecationSeverity::Low,
360        }
361    }
362
363    /// Set replacement tool
364    pub fn with_replacement(mut self, replacement: String) -> Self {
365        self.replacement = Some(replacement);
366        self
367    }
368
369    /// Set removal date
370    pub fn with_removal_date(mut self, removal_date: DateTime<Utc>) -> Self {
371        self.removal_date = Some(removal_date);
372        self
373    }
374
375    /// Set severity
376    pub fn with_severity(mut self, severity: DeprecationSeverity) -> Self {
377        self.severity = severity;
378        self
379    }
380}
381
382/// Comprehensive enhanced metadata for tools
383#[derive(Debug, Clone)]
384pub struct EnhancedToolMetadata {
385    /// Tool behavior hints for client understanding
386    pub behavior_hints: ToolBehaviorHints,
387    /// Tool categorization for organization
388    pub category: Option<ToolCategory>,
389    /// Performance tracking metrics (using thread-safe interior mutability)
390    pub performance: Arc<RwLock<ToolPerformanceMetrics>>,
391    /// Deprecation information
392    pub deprecation: Option<ToolDeprecation>,
393    /// Tool version information
394    pub version: Option<String>,
395    /// Author/maintainer information
396    pub author: Option<String>,
397    /// Custom metadata fields
398    pub custom: HashMap<String, serde_json::Value>,
399}
400
401impl Default for EnhancedToolMetadata {
402    fn default() -> Self {
403        Self {
404            behavior_hints: ToolBehaviorHints::default(),
405            category: None,
406            performance: Arc::new(RwLock::new(ToolPerformanceMetrics::default())),
407            deprecation: None,
408            version: None,
409            author: None,
410            custom: HashMap::new(),
411        }
412    }
413}
414
415impl EnhancedToolMetadata {
416    /// Create new enhanced metadata
417    pub fn new() -> Self {
418        Self::default()
419    }
420
421    /// Set behavior hints
422    pub fn with_behavior_hints(mut self, hints: ToolBehaviorHints) -> Self {
423        self.behavior_hints = hints;
424        self
425    }
426
427    /// Set category
428    pub fn with_category(mut self, category: ToolCategory) -> Self {
429        self.category = Some(category);
430        self
431    }
432
433    /// Set version
434    pub fn with_version(mut self, version: String) -> Self {
435        self.version = Some(version);
436        self
437    }
438
439    /// Set author
440    pub fn with_author(mut self, author: String) -> Self {
441        self.author = Some(author);
442        self
443    }
444
445    /// Add custom metadata field
446    pub fn with_custom_field(mut self, key: String, value: serde_json::Value) -> Self {
447        self.custom.insert(key, value);
448        self
449    }
450
451    /// Deprecate the tool
452    pub fn deprecated(mut self, deprecation: ToolDeprecation) -> Self {
453        self.deprecation = Some(deprecation);
454        self
455    }
456
457    /// Check if tool is deprecated
458    pub fn is_deprecated(&self) -> bool {
459        self.deprecation.as_ref().is_some_and(|d| d.deprecated)
460    }
461
462    /// Get deprecation warning message
463    pub fn deprecation_warning(&self) -> Option<String> {
464        self.deprecation.as_ref().and_then(|d| {
465            if d.deprecated {
466                let mut warning = "Tool is deprecated".to_string();
467                if let Some(ref reason) = d.reason {
468                    warning.push_str(&format!(": {reason}"));
469                }
470                if let Some(ref replacement) = d.replacement {
471                    warning.push_str(&format!(". Use '{replacement}' instead"));
472                }
473                Some(warning)
474            } else {
475                None
476            }
477        })
478    }
479
480    /// Record a successful execution (with thread-safe interior mutability)
481    pub fn record_success(&self, execution_time: Duration) {
482        if let Ok(mut perf) = self.performance.write() {
483            perf.record_success(execution_time);
484        }
485    }
486
487    /// Record a failed execution (with thread-safe interior mutability)
488    pub fn record_error(&self, execution_time: Duration) {
489        if let Ok(mut perf) = self.performance.write() {
490            perf.record_error(execution_time);
491        }
492    }
493
494    /// Get performance metrics snapshot
495    pub fn get_performance_snapshot(&self) -> ToolPerformanceMetrics {
496        self.performance
497            .read()
498            .map(|p| p.clone())
499            .unwrap_or_default()
500    }
501
502    /// Get execution count
503    pub fn execution_count(&self) -> u64 {
504        self.performance
505            .read()
506            .map(|p| p.execution_count)
507            .unwrap_or(0)
508    }
509
510    /// Get success rate
511    pub fn success_rate(&self) -> f64 {
512        self.performance
513            .read()
514            .map(|p| p.success_rate)
515            .unwrap_or(0.0)
516    }
517
518    /// Get average execution time
519    pub fn average_execution_time(&self) -> Duration {
520        self.performance
521            .read()
522            .map(|p| p.average_execution_time)
523            .unwrap_or_default()
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530    use std::time::Duration;
531
532    #[test]
533    fn test_behavior_hints() {
534        let hints = ToolBehaviorHints::new()
535            .read_only()
536            .idempotent()
537            .cacheable();
538
539        assert_eq!(hints.read_only, Some(true));
540        assert_eq!(hints.idempotent, Some(true));
541        assert_eq!(hints.cacheable, Some(true));
542        assert_eq!(hints.destructive, None);
543    }
544
545    #[test]
546    fn test_tool_category() {
547        let category = ToolCategory::new("file".to_string())
548            .with_secondary("read".to_string())
549            .with_tag("filesystem".to_string())
550            .with_tag("utility".to_string());
551
552        assert_eq!(category.primary, "file");
553        assert_eq!(category.secondary, Some("read".to_string()));
554        assert!(category.tags.contains("filesystem"));
555        assert!(category.tags.contains("utility"));
556    }
557
558    #[test]
559    fn test_category_filter() {
560        let category = ToolCategory::new("file".to_string())
561            .with_secondary("read".to_string())
562            .with_tag("filesystem".to_string());
563
564        let filter = CategoryFilter::new().with_primary("file".to_string());
565
566        assert!(category.matches_filter(&filter));
567
568        let filter = CategoryFilter::new().with_primary("network".to_string());
569
570        assert!(!category.matches_filter(&filter));
571
572        let filter = CategoryFilter::new().with_tag("filesystem".to_string());
573
574        assert!(category.matches_filter(&filter));
575    }
576
577    #[test]
578    fn test_performance_metrics() {
579        let mut metrics = ToolPerformanceMetrics::new();
580
581        metrics.record_success(Duration::from_millis(100));
582        metrics.record_success(Duration::from_millis(200));
583        metrics.record_error(Duration::from_millis(150));
584
585        assert_eq!(metrics.execution_count, 3);
586        assert_eq!(metrics.success_count, 2);
587        assert_eq!(metrics.error_count, 1);
588        assert!((metrics.success_rate - 66.66666666666667).abs() < 0.001);
589        assert_eq!(metrics.min_execution_time, Duration::from_millis(100));
590        assert_eq!(metrics.max_execution_time, Duration::from_millis(200));
591    }
592
593    #[test]
594    fn test_tool_deprecation() {
595        let deprecation = ToolDeprecation::new("Tool is no longer maintained".to_string())
596            .with_replacement("new_tool".to_string())
597            .with_severity(DeprecationSeverity::High);
598
599        assert!(deprecation.deprecated);
600        assert_eq!(
601            deprecation.reason,
602            Some("Tool is no longer maintained".to_string())
603        );
604        assert_eq!(deprecation.replacement, Some("new_tool".to_string()));
605        assert_eq!(deprecation.severity, DeprecationSeverity::High);
606    }
607
608    #[test]
609    fn test_enhanced_metadata() {
610        let hints = ToolBehaviorHints::new().read_only().cacheable();
611        let category = ToolCategory::new("data".to_string()).with_tag("analysis".to_string());
612
613        let metadata = EnhancedToolMetadata::new()
614            .with_behavior_hints(hints)
615            .with_category(category)
616            .with_version("1.0.0".to_string())
617            .with_author("Test Author".to_string());
618
619        assert_eq!(metadata.behavior_hints.read_only, Some(true));
620        assert_eq!(metadata.behavior_hints.cacheable, Some(true));
621        assert!(metadata.category.is_some());
622        assert_eq!(metadata.version, Some("1.0.0".to_string()));
623        assert_eq!(metadata.author, Some("Test Author".to_string()));
624        assert!(!metadata.is_deprecated());
625    }
626
627    #[test]
628    fn test_deprecation_warning() {
629        let deprecation = ToolDeprecation::new("Old implementation".to_string())
630            .with_replacement("better_tool".to_string());
631
632        let metadata = EnhancedToolMetadata::new().deprecated(deprecation);
633
634        assert!(metadata.is_deprecated());
635        let warning = metadata.deprecation_warning().unwrap();
636        assert!(warning.contains("deprecated"));
637        assert!(warning.contains("Old implementation"));
638        assert!(warning.contains("better_tool"));
639    }
640}