turbomcp_client/plugins/
examples.rs

1//! Example plugin implementations
2//!
3//! Provides plugin implementations for common use cases:
4//! - MetricsPlugin: Request/response metrics collection
5//! - RetryPlugin: Automatic retry with exponential backoff
6//! - CachePlugin: Response caching with TTL
7
8use crate::plugins::core::{
9    ClientPlugin, PluginConfig, PluginContext, PluginResult, RequestContext, ResponseContext,
10};
11use async_trait::async_trait;
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use serde_json::{Value, json};
15use std::collections::HashMap;
16use std::sync::{Arc, Mutex};
17use std::time::{Duration, Instant};
18use tracing::{debug, info, warn};
19
20// ============================================================================
21// METRICS PLUGIN
22// ============================================================================
23
24/// Request/response metrics data
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct MetricsData {
27    /// Total number of requests
28    pub total_requests: u64,
29
30    /// Total number of successful responses
31    pub successful_responses: u64,
32
33    /// Total number of failed responses
34    pub failed_responses: u64,
35
36    /// Average response time in milliseconds
37    pub avg_response_time_ms: f64,
38
39    /// Minimum response time in milliseconds
40    pub min_response_time_ms: u64,
41
42    /// Maximum response time in milliseconds
43    pub max_response_time_ms: u64,
44
45    /// Requests per minute (last minute)
46    pub requests_per_minute: f64,
47
48    /// Method-specific metrics
49    pub method_metrics: HashMap<String, MethodMetrics>,
50
51    /// Start time for metrics collection
52    pub start_time: DateTime<Utc>,
53
54    /// Last reset time
55    pub last_reset: DateTime<Utc>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct MethodMetrics {
60    pub count: u64,
61    pub avg_duration_ms: f64,
62    pub success_count: u64,
63    pub error_count: u64,
64}
65
66impl Default for MetricsData {
67    fn default() -> Self {
68        let now = Utc::now();
69        Self {
70            total_requests: 0,
71            successful_responses: 0,
72            failed_responses: 0,
73            avg_response_time_ms: 0.0,
74            min_response_time_ms: u64::MAX,
75            max_response_time_ms: 0,
76            requests_per_minute: 0.0,
77            method_metrics: HashMap::new(),
78            start_time: now,
79            last_reset: now,
80        }
81    }
82}
83
84/// Plugin for collecting request/response metrics
85#[derive(Debug)]
86pub struct MetricsPlugin {
87    /// Thread-safe metrics storage
88    metrics: Arc<Mutex<MetricsData>>,
89
90    /// Request start times for duration calculation
91    request_times: Arc<Mutex<HashMap<String, Instant>>>,
92
93    /// Recent request timestamps for rate calculation
94    recent_requests: Arc<Mutex<Vec<Instant>>>,
95}
96
97impl MetricsPlugin {
98    /// Create a new metrics plugin
99    #[must_use]
100    pub fn new(_config: PluginConfig) -> Self {
101        Self {
102            metrics: Arc::new(Mutex::new(MetricsData::default())),
103            request_times: Arc::new(Mutex::new(HashMap::new())),
104            recent_requests: Arc::new(Mutex::new(Vec::new())),
105        }
106    }
107
108    /// Get current metrics data
109    #[must_use]
110    pub fn get_metrics(&self) -> MetricsData {
111        self.metrics.lock().unwrap().clone()
112    }
113
114    /// Reset all metrics
115    pub fn reset_metrics(&self) {
116        let mut metrics = self.metrics.lock().unwrap();
117        let now = Utc::now();
118        *metrics = MetricsData {
119            start_time: metrics.start_time,
120            last_reset: now,
121            ..MetricsData::default()
122        };
123        self.request_times.lock().unwrap().clear();
124        self.recent_requests.lock().unwrap().clear();
125    }
126
127    fn update_request_rate(&self) {
128        let mut recent = self.recent_requests.lock().unwrap();
129        let now = Instant::now();
130
131        // Remove requests older than 1 minute
132        recent.retain(|&timestamp| now.duration_since(timestamp).as_secs() < 60);
133
134        // Add current request
135        recent.push(now);
136
137        // Update rate
138        let mut metrics = self.metrics.lock().unwrap();
139        metrics.requests_per_minute = recent.len() as f64;
140    }
141
142    fn update_method_metrics(&self, method: &str, duration: Duration, is_success: bool) {
143        let mut metrics = self.metrics.lock().unwrap();
144        let entry = metrics
145            .method_metrics
146            .entry(method.to_string())
147            .or_insert(MethodMetrics {
148                count: 0,
149                avg_duration_ms: 0.0,
150                success_count: 0,
151                error_count: 0,
152            });
153
154        entry.count += 1;
155        if is_success {
156            entry.success_count += 1;
157        } else {
158            entry.error_count += 1;
159        }
160
161        // Update running average
162        let duration_ms = duration.as_millis() as f64;
163        entry.avg_duration_ms =
164            (entry.avg_duration_ms * (entry.count - 1) as f64 + duration_ms) / entry.count as f64;
165    }
166}
167
168#[async_trait]
169impl ClientPlugin for MetricsPlugin {
170    fn name(&self) -> &str {
171        "metrics"
172    }
173
174    fn version(&self) -> &str {
175        "1.0.0"
176    }
177
178    fn description(&self) -> Option<&str> {
179        Some("Collects request/response metrics and performance data")
180    }
181
182    async fn initialize(&self, context: &PluginContext) -> PluginResult<()> {
183        info!(
184            "Metrics plugin initialized for client: {}",
185            context.client_name
186        );
187        Ok(())
188    }
189
190    async fn before_request(&self, context: &mut RequestContext) -> PluginResult<()> {
191        let request_id = context.request.id.to_string();
192
193        // Record request start time
194        self.request_times
195            .lock()
196            .unwrap()
197            .insert(request_id.clone(), Instant::now());
198
199        // Update metrics
200        {
201            let mut metrics = self.metrics.lock().unwrap();
202            metrics.total_requests += 1;
203        }
204
205        self.update_request_rate();
206
207        // Add metrics metadata
208        context.add_metadata("metrics.request_id".to_string(), json!(request_id));
209        context.add_metadata(
210            "metrics.start_time".to_string(),
211            json!(Utc::now().to_rfc3339()),
212        );
213
214        debug!(
215            "Metrics: Recorded request start for method: {}",
216            context.method()
217        );
218        Ok(())
219    }
220
221    async fn after_response(&self, context: &mut ResponseContext) -> PluginResult<()> {
222        let request_id = context.request_context.request.id.to_string();
223
224        // Calculate duration
225        let duration =
226            if let Some(start_time) = self.request_times.lock().unwrap().remove(&request_id) {
227                start_time.elapsed()
228            } else {
229                context.duration
230            };
231
232        let is_success = context.is_success();
233        let method = context.method().to_string();
234        let duration_ms = duration.as_millis();
235
236        // Update metrics
237        {
238            let mut metrics = self.metrics.lock().unwrap();
239
240            if is_success {
241                metrics.successful_responses += 1;
242            } else {
243                metrics.failed_responses += 1;
244            }
245
246            let duration_ms_u64 = duration_ms as u64;
247
248            // Update min/max
249            if duration_ms_u64 < metrics.min_response_time_ms {
250                metrics.min_response_time_ms = duration_ms_u64;
251            }
252            if duration_ms_u64 > metrics.max_response_time_ms {
253                metrics.max_response_time_ms = duration_ms_u64;
254            }
255
256            // Update running average
257            let total_responses = metrics.successful_responses + metrics.failed_responses;
258            metrics.avg_response_time_ms =
259                (metrics.avg_response_time_ms * (total_responses - 1) as f64 + duration_ms as f64)
260                    / total_responses as f64;
261        }
262
263        // Update method-specific metrics
264        self.update_method_metrics(&method, duration, is_success);
265
266        // Add metrics metadata
267        context.add_metadata("metrics.duration_ms".to_string(), json!(duration_ms));
268        context.add_metadata("metrics.success".to_string(), json!(is_success));
269
270        debug!(
271            "Metrics: Recorded response for method: {} ({}ms, success: {})",
272            method, duration_ms, is_success
273        );
274
275        Ok(())
276    }
277
278    async fn handle_custom(
279        &self,
280        method: &str,
281        params: Option<Value>,
282    ) -> PluginResult<Option<Value>> {
283        match method {
284            "metrics.get_stats" => {
285                let metrics = self.get_metrics();
286                Ok(Some(serde_json::to_value(metrics).unwrap()))
287            }
288            "metrics.reset" => {
289                self.reset_metrics();
290                info!("Metrics reset");
291                Ok(Some(json!({"status": "reset"})))
292            }
293            "metrics.get_method_stats" => {
294                if let Some(params) = params {
295                    if let Some(method_name) = params.get("method").and_then(|v| v.as_str()) {
296                        let metrics = self.metrics.lock().unwrap();
297                        if let Some(method_metrics) = metrics.method_metrics.get(method_name) {
298                            Ok(Some(serde_json::to_value(method_metrics).unwrap()))
299                        } else {
300                            Ok(Some(json!({"error": "Method not found"})))
301                        }
302                    } else {
303                        Ok(Some(json!({"error": "Method parameter required"})))
304                    }
305                } else {
306                    Ok(Some(json!({"error": "Parameters required"})))
307                }
308            }
309            _ => Ok(None),
310        }
311    }
312}
313
314// ============================================================================
315// RETRY PLUGIN
316// ============================================================================
317
318/// Configuration for retry behavior
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct RetryConfig {
321    /// Maximum number of retry attempts
322    pub max_retries: u32,
323
324    /// Base delay between retries in milliseconds
325    pub base_delay_ms: u64,
326
327    /// Maximum delay between retries in milliseconds
328    pub max_delay_ms: u64,
329
330    /// Multiplier for exponential backoff
331    pub backoff_multiplier: f64,
332
333    /// Whether to retry on timeout errors
334    pub retry_on_timeout: bool,
335
336    /// Whether to retry on connection errors
337    pub retry_on_connection_error: bool,
338}
339
340impl Default for RetryConfig {
341    fn default() -> Self {
342        Self {
343            max_retries: 3,
344            base_delay_ms: 100,
345            max_delay_ms: 5000,
346            backoff_multiplier: 2.0,
347            retry_on_timeout: true,
348            retry_on_connection_error: true,
349        }
350    }
351}
352
353/// Plugin for automatic retry with exponential backoff
354#[derive(Debug)]
355pub struct RetryPlugin {
356    config: RetryConfig,
357    retry_stats: Arc<Mutex<HashMap<String, u32>>>,
358}
359
360impl RetryPlugin {
361    /// Create a new retry plugin
362    #[must_use]
363    pub fn new(config: PluginConfig) -> Self {
364        let retry_config = match config {
365            PluginConfig::Retry(config) => config,
366            _ => RetryConfig::default(),
367        };
368
369        Self {
370            config: retry_config,
371            retry_stats: Arc::new(Mutex::new(HashMap::new())),
372        }
373    }
374
375    fn should_retry(&self, error: &turbomcp_protocol::Error) -> bool {
376        let error_string = error.to_string().to_lowercase();
377
378        if self.config.retry_on_connection_error
379            && (error_string.contains("transport") || error_string.contains("connection"))
380        {
381            return true;
382        }
383
384        if self.config.retry_on_timeout && error_string.contains("timeout") {
385            return true;
386        }
387
388        false
389    }
390
391    fn calculate_delay(&self, attempt: u32) -> Duration {
392        let delay_ms = (self.config.base_delay_ms as f64
393            * self.config.backoff_multiplier.powi(attempt as i32)) as u64;
394        Duration::from_millis(delay_ms.min(self.config.max_delay_ms))
395    }
396
397    fn get_retry_count(&self, request_id: &str) -> u32 {
398        self.retry_stats
399            .lock()
400            .unwrap()
401            .get(request_id)
402            .copied()
403            .unwrap_or(0)
404    }
405
406    fn increment_retry_count(&self, request_id: &str) {
407        let mut stats = self.retry_stats.lock().unwrap();
408        let count = stats.entry(request_id.to_string()).or_insert(0);
409        *count += 1;
410    }
411
412    fn clear_retry_count(&self, request_id: &str) {
413        self.retry_stats.lock().unwrap().remove(request_id);
414    }
415}
416
417#[async_trait]
418impl ClientPlugin for RetryPlugin {
419    fn name(&self) -> &str {
420        "retry"
421    }
422
423    fn version(&self) -> &str {
424        "1.0.0"
425    }
426
427    fn description(&self) -> Option<&str> {
428        Some("Automatic retry with exponential backoff for failed requests")
429    }
430
431    async fn initialize(&self, context: &PluginContext) -> PluginResult<()> {
432        info!(
433            "Retry plugin initialized for client: {} (max_retries: {}, base_delay: {}ms)",
434            context.client_name, self.config.max_retries, self.config.base_delay_ms
435        );
436        Ok(())
437    }
438
439    async fn before_request(&self, context: &mut RequestContext) -> PluginResult<()> {
440        let request_id = context.request.id.to_string();
441        let retry_count = self.get_retry_count(&request_id);
442
443        // Add retry metadata
444        context.add_metadata("retry.attempt".to_string(), json!(retry_count + 1));
445        context.add_metadata(
446            "retry.max_attempts".to_string(),
447            json!(self.config.max_retries + 1),
448        );
449
450        if retry_count > 0 {
451            debug!(
452                "Retry: Attempt {} for request {} (method: {})",
453                retry_count + 1,
454                request_id,
455                context.method()
456            );
457        }
458
459        Ok(())
460    }
461
462    async fn after_response(&self, context: &mut ResponseContext) -> PluginResult<()> {
463        let request_id = context.request_context.request.id.to_string();
464
465        if context.is_success() {
466            // Clear retry count on success
467            self.clear_retry_count(&request_id);
468            debug!("Retry: Request {} succeeded", request_id);
469        } else if let Some(error) = &context.error {
470            let retry_count = self.get_retry_count(&request_id);
471
472            if self.should_retry(error) && retry_count < self.config.max_retries {
473                // Increment retry count and schedule retry
474                self.increment_retry_count(&request_id);
475
476                let delay = self.calculate_delay(retry_count);
477                warn!(
478                    "Retry: Request {} failed (attempt {}), will retry after {:?}",
479                    request_id,
480                    retry_count + 1,
481                    delay
482                );
483
484                // Add retry metadata
485                context.add_metadata("retry.will_retry".to_string(), json!(true));
486                context.add_metadata("retry.delay_ms".to_string(), json!(delay.as_millis()));
487                context.add_metadata("retry.next_attempt".to_string(), json!(retry_count + 2));
488
489                // Schedule retry by modifying the response to indicate retry needed
490                // The client can check for retry metadata and handle accordingly
491                context.add_metadata("retry.should_retry".to_string(), json!(true));
492                context.add_metadata(
493                    "retry.recommended_action".to_string(),
494                    json!("retry_request"),
495                );
496            } else {
497                // Max retries reached or error not retryable
498                self.clear_retry_count(&request_id);
499                if retry_count >= self.config.max_retries {
500                    warn!("Retry: Request {} exhausted all retry attempts", request_id);
501                } else {
502                    debug!("Retry: Error not retryable for request {}", request_id);
503                }
504                context.add_metadata("retry.will_retry".to_string(), json!(false));
505                context.add_metadata(
506                    "retry.reason".to_string(),
507                    json!(if retry_count >= self.config.max_retries {
508                        "max_retries_reached"
509                    } else {
510                        "error_not_retryable"
511                    }),
512                );
513            }
514        }
515
516        Ok(())
517    }
518
519    async fn handle_custom(
520        &self,
521        method: &str,
522        _params: Option<Value>,
523    ) -> PluginResult<Option<Value>> {
524        match method {
525            "retry.get_config" => Ok(Some(serde_json::to_value(&self.config).unwrap())),
526            "retry.get_stats" => {
527                let stats = self.retry_stats.lock().unwrap();
528                Ok(Some(json!({
529                    "active_retries": stats.len(),
530                    "retry_counts": stats.clone()
531                })))
532            }
533            "retry.clear_stats" => {
534                self.retry_stats.lock().unwrap().clear();
535                info!("Retry stats cleared");
536                Ok(Some(json!({"status": "cleared"})))
537            }
538            _ => Ok(None),
539        }
540    }
541}
542
543// ============================================================================
544// CACHE PLUGIN
545// ============================================================================
546
547/// Configuration for caching behavior
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct CacheConfig {
550    /// Maximum number of cached entries
551    pub max_entries: usize,
552
553    /// Time-to-live for cached entries in seconds
554    pub ttl_seconds: u64,
555
556    /// Whether to cache successful responses
557    pub cache_responses: bool,
558
559    /// Whether to cache resource content
560    pub cache_resources: bool,
561
562    /// Whether to cache tool results
563    pub cache_tools: bool,
564}
565
566impl Default for CacheConfig {
567    fn default() -> Self {
568        Self {
569            max_entries: 1000,
570            ttl_seconds: 300, // 5 minutes
571            cache_responses: true,
572            cache_resources: true,
573            cache_tools: true,
574        }
575    }
576}
577
578#[derive(Debug, Clone)]
579struct CacheEntry {
580    data: Value,
581    timestamp: Instant,
582    access_count: u64,
583}
584
585impl CacheEntry {
586    fn new(data: Value) -> Self {
587        Self {
588            data,
589            timestamp: Instant::now(),
590            access_count: 0,
591        }
592    }
593
594    fn is_expired(&self, ttl: Duration) -> bool {
595        self.timestamp.elapsed() > ttl
596    }
597
598    fn access(&mut self) -> &Value {
599        self.access_count += 1;
600        &self.data
601    }
602}
603
604/// Plugin for caching responses with TTL
605#[derive(Debug)]
606pub struct CachePlugin {
607    config: CacheConfig,
608    cache: Arc<Mutex<HashMap<String, CacheEntry>>>,
609    stats: Arc<Mutex<CacheStats>>,
610}
611
612#[derive(Debug, Default)]
613struct CacheStats {
614    hits: u64,
615    misses: u64,
616    evictions: u64,
617    total_entries: u64,
618}
619
620impl CachePlugin {
621    /// Create a new cache plugin
622    #[must_use]
623    pub fn new(config: PluginConfig) -> Self {
624        let cache_config = match config {
625            PluginConfig::Cache(config) => config,
626            _ => CacheConfig::default(),
627        };
628
629        Self {
630            config: cache_config,
631            cache: Arc::new(Mutex::new(HashMap::new())),
632            stats: Arc::new(Mutex::new(CacheStats::default())),
633        }
634    }
635
636    fn should_cache_method(&self, method: &str) -> bool {
637        match method {
638            m if m.starts_with("tools/") && self.config.cache_tools => true,
639            m if m.starts_with("resources/") && self.config.cache_resources => true,
640            _ if self.config.cache_responses => true,
641            _ => false,
642        }
643    }
644
645    fn generate_cache_key(&self, context: &RequestContext) -> String {
646        // Simple cache key based on method and parameters
647        let params_hash = if let Some(params) = &context.request.params {
648            format!("{:x}", fxhash::hash64(params))
649        } else {
650            "no_params".to_string()
651        };
652        format!("{}:{}", context.method(), params_hash)
653    }
654
655    fn get_cached(&self, key: &str) -> Option<Value> {
656        let mut cache = self.cache.lock().unwrap();
657        let ttl = Duration::from_secs(self.config.ttl_seconds);
658
659        if let Some(entry) = cache.get_mut(key) {
660            if !entry.is_expired(ttl) {
661                let mut stats = self.stats.lock().unwrap();
662                stats.hits += 1;
663                return Some(entry.access().clone());
664            } else {
665                // Remove expired entry
666                cache.remove(key);
667                let mut stats = self.stats.lock().unwrap();
668                stats.evictions += 1;
669            }
670        }
671
672        let mut stats = self.stats.lock().unwrap();
673        stats.misses += 1;
674        None
675    }
676
677    fn store_cached(&self, key: String, data: Value) {
678        let mut cache = self.cache.lock().unwrap();
679
680        // Evict oldest entries if cache is full
681        if cache.len() >= self.config.max_entries {
682            // Simple LRU: remove oldest entries
683            let evict_keys: Vec<_> = {
684                let mut entries: Vec<_> = cache
685                    .iter()
686                    .map(|(k, v)| (k.clone(), v.timestamp))
687                    .collect();
688                entries.sort_by_key(|(_, timestamp)| *timestamp);
689
690                let evict_count = (cache.len() - self.config.max_entries + 1).min(cache.len() / 2);
691                entries
692                    .into_iter()
693                    .take(evict_count)
694                    .map(|(key, _)| key)
695                    .collect()
696            };
697
698            let evict_count = evict_keys.len();
699            for key in evict_keys {
700                cache.remove(&key);
701            }
702
703            let mut stats = self.stats.lock().unwrap();
704            stats.evictions += evict_count as u64;
705        }
706
707        cache.insert(key, CacheEntry::new(data));
708        let mut stats = self.stats.lock().unwrap();
709        stats.total_entries += 1;
710    }
711
712    fn cleanup_expired(&self) {
713        let mut cache = self.cache.lock().unwrap();
714        let ttl = Duration::from_secs(self.config.ttl_seconds);
715
716        let expired_keys: Vec<_> = cache
717            .iter()
718            .filter(|(_, entry)| entry.is_expired(ttl))
719            .map(|(key, _)| key.clone())
720            .collect();
721
722        let eviction_count = expired_keys.len();
723        for key in expired_keys {
724            cache.remove(&key);
725        }
726
727        if eviction_count > 0 {
728            let mut stats = self.stats.lock().unwrap();
729            stats.evictions += eviction_count as u64;
730            debug!("Cache: Cleaned up {} expired entries", eviction_count);
731        }
732    }
733}
734
735#[async_trait]
736impl ClientPlugin for CachePlugin {
737    fn name(&self) -> &str {
738        "cache"
739    }
740
741    fn version(&self) -> &str {
742        "1.0.0"
743    }
744
745    fn description(&self) -> Option<&str> {
746        Some("Response caching with TTL and LRU eviction")
747    }
748
749    async fn initialize(&self, context: &PluginContext) -> PluginResult<()> {
750        info!(
751            "Cache plugin initialized for client: {} (max_entries: {}, ttl: {}s)",
752            context.client_name, self.config.max_entries, self.config.ttl_seconds
753        );
754        Ok(())
755    }
756
757    async fn before_request(&self, context: &mut RequestContext) -> PluginResult<()> {
758        if !self.should_cache_method(context.method()) {
759            return Ok(());
760        }
761
762        let cache_key = self.generate_cache_key(context);
763
764        if let Some(cached_data) = self.get_cached(&cache_key) {
765            debug!(
766                "Cache: Hit for method {} (key: {})",
767                context.method(),
768                cache_key
769            );
770            context.add_metadata("cache.hit".to_string(), json!(true));
771            context.add_metadata("cache.key".to_string(), json!(cache_key));
772            context.add_metadata("cache.response_source".to_string(), json!("cache"));
773            // Store cached response for retrieval after protocol call is skipped
774            context.add_metadata("cache.response_data".to_string(), cached_data.clone());
775            context.add_metadata("cache.should_skip_request".to_string(), json!(true));
776        } else {
777            debug!(
778                "Cache: Miss for method {} (key: {})",
779                context.method(),
780                cache_key
781            );
782            context.add_metadata("cache.hit".to_string(), json!(false));
783            context.add_metadata("cache.key".to_string(), json!(cache_key));
784            context.add_metadata("cache.should_skip_request".to_string(), json!(false));
785        }
786
787        Ok(())
788    }
789
790    async fn after_response(&self, context: &mut ResponseContext) -> PluginResult<()> {
791        // Handle cache hits - if we have cached response data, use it
792        if let Some(cached_response_data) =
793            context.request_context.get_metadata("cache.response_data")
794        {
795            context.response = Some(cached_response_data.clone());
796            debug!(
797                "Cache: Used cached response for method {}",
798                context.method()
799            );
800            return Ok(());
801        }
802
803        if !self.should_cache_method(context.method()) || !context.is_success() {
804            return Ok(());
805        }
806
807        if let Some(cache_key) = context
808            .request_context
809            .get_metadata("cache.key")
810            .and_then(|v| v.as_str())
811            && let Some(response_data) = &context.response
812        {
813            self.store_cached(cache_key.to_string(), response_data.clone());
814            debug!(
815                "Cache: Stored response for method {} (key: {})",
816                context.method(),
817                cache_key
818            );
819            context.add_metadata("cache.stored".to_string(), json!(true));
820        }
821
822        // Periodic cleanup of expired entries
823        self.cleanup_expired();
824
825        Ok(())
826    }
827
828    async fn handle_custom(
829        &self,
830        method: &str,
831        params: Option<Value>,
832    ) -> PluginResult<Option<Value>> {
833        match method {
834            "cache.get_stats" => {
835                let stats = self.stats.lock().unwrap();
836                let cache_size = self.cache.lock().unwrap().len();
837
838                Ok(Some(json!({
839                    "hits": stats.hits,
840                    "misses": stats.misses,
841                    "evictions": stats.evictions,
842                    "total_entries": stats.total_entries,
843                    "current_size": cache_size,
844                    "hit_rate": if stats.hits + stats.misses > 0 {
845                        stats.hits as f64 / (stats.hits + stats.misses) as f64
846                    } else {
847                        0.0
848                    }
849                })))
850            }
851            "cache.clear" => {
852                let mut cache = self.cache.lock().unwrap();
853                let cleared_count = cache.len();
854                cache.clear();
855                info!("Cache: Cleared {} entries", cleared_count);
856                Ok(Some(json!({"cleared_entries": cleared_count})))
857            }
858            "cache.get_config" => Ok(Some(serde_json::to_value(&self.config).unwrap())),
859            "cache.cleanup" => {
860                self.cleanup_expired();
861                let cache_size = self.cache.lock().unwrap().len();
862                Ok(Some(json!({"remaining_entries": cache_size})))
863            }
864            "cache.get" => {
865                if let Some(params) = params {
866                    if let Some(key) = params.get("key").and_then(|v| v.as_str()) {
867                        if let Some(data) = self.get_cached(key) {
868                            Ok(Some(json!({"found": true, "data": data})))
869                        } else {
870                            Ok(Some(json!({"found": false})))
871                        }
872                    } else {
873                        Ok(Some(json!({"error": "Key parameter required"})))
874                    }
875                } else {
876                    Ok(Some(json!({"error": "Parameters required"})))
877                }
878            }
879            _ => Ok(None),
880        }
881    }
882}
883
884// Helper function for hashing (using a fast hash function)
885mod fxhash {
886    use serde_json::Value;
887    use std::collections::hash_map::DefaultHasher;
888    use std::hash::{Hash, Hasher};
889
890    pub fn hash64(value: &Value) -> u64 {
891        let mut hasher = DefaultHasher::new();
892
893        // Simple hash of JSON string representation
894        let json_str = value.to_string();
895        json_str.hash(&mut hasher);
896
897        hasher.finish()
898    }
899}
900
901#[cfg(test)]
902mod tests {
903    use super::*;
904
905    #[tokio::test]
906    async fn test_metrics_plugin_creation() {
907        let plugin = MetricsPlugin::new(PluginConfig::Metrics);
908        assert_eq!(plugin.name(), "metrics");
909
910        let metrics = plugin.get_metrics();
911        assert_eq!(metrics.total_requests, 0);
912        assert_eq!(metrics.successful_responses, 0);
913    }
914
915    #[tokio::test]
916    async fn test_retry_plugin_creation() {
917        let config = RetryConfig {
918            max_retries: 5,
919            base_delay_ms: 200,
920            max_delay_ms: 2000,
921            backoff_multiplier: 1.5,
922            retry_on_timeout: true,
923            retry_on_connection_error: false,
924        };
925
926        let plugin = RetryPlugin::new(PluginConfig::Retry(config.clone()));
927        assert_eq!(plugin.name(), "retry");
928        assert_eq!(plugin.config.max_retries, 5);
929        assert_eq!(plugin.config.base_delay_ms, 200);
930    }
931
932    #[tokio::test]
933    async fn test_cache_plugin_creation() {
934        let config = CacheConfig {
935            max_entries: 500,
936            ttl_seconds: 600,
937            cache_responses: true,
938            cache_resources: false,
939            cache_tools: true,
940        };
941
942        let plugin = CachePlugin::new(PluginConfig::Cache(config.clone()));
943        assert_eq!(plugin.name(), "cache");
944        assert_eq!(plugin.config.max_entries, 500);
945        assert_eq!(plugin.config.ttl_seconds, 600);
946    }
947
948    #[test]
949    fn test_retry_delay_calculation() {
950        let config = RetryConfig {
951            max_retries: 3,
952            base_delay_ms: 100,
953            max_delay_ms: 1000,
954            backoff_multiplier: 2.0,
955            retry_on_timeout: true,
956            retry_on_connection_error: true,
957        };
958
959        let plugin = RetryPlugin::new(PluginConfig::Retry(config));
960
961        assert_eq!(plugin.calculate_delay(0), Duration::from_millis(100));
962        assert_eq!(plugin.calculate_delay(1), Duration::from_millis(200));
963        assert_eq!(plugin.calculate_delay(2), Duration::from_millis(400));
964        assert_eq!(plugin.calculate_delay(3), Duration::from_millis(800));
965        assert_eq!(plugin.calculate_delay(4), Duration::from_millis(1000)); // Capped at max
966    }
967
968    #[test]
969    fn test_cache_entry_expiration() {
970        let entry = CacheEntry::new(json!({"test": "data"}));
971        assert!(!entry.is_expired(Duration::from_secs(1)));
972
973        // Can't easily test actual expiration without sleeping or mocking time
974    }
975}