Skip to main content

zagens_runtime_adapters/mcp/
observability.rs

1//! MCP call observability: in-memory recent-call ring buffer + helpers for tracing.
2
3use std::collections::VecDeque;
4use std::sync::{Mutex, OnceLock};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7const MAX_RECENT_CALLS: usize = 64;
8
9/// One completed MCP RPC or tool invocation (sanitized for UI).
10#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
11pub struct McpCallRecord {
12    pub timestamp_ms: u128,
13    pub server: String,
14    pub method: String,
15    pub duration_ms: u64,
16    pub success: bool,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub error: Option<String>,
19    pub result_bytes: usize,
20}
21
22struct CallLog {
23    entries: VecDeque<McpCallRecord>,
24}
25
26impl CallLog {
27    fn push(&mut self, record: McpCallRecord) {
28        if self.entries.len() >= MAX_RECENT_CALLS {
29            self.entries.pop_front();
30        }
31        self.entries.push_back(record);
32    }
33}
34
35static CALL_LOG: OnceLock<Mutex<CallLog>> = OnceLock::new();
36
37fn call_log() -> &'static Mutex<CallLog> {
38    CALL_LOG.get_or_init(|| {
39        Mutex::new(CallLog {
40            entries: VecDeque::new(),
41        })
42    })
43}
44
45fn now_ms() -> u128 {
46    SystemTime::now()
47        .duration_since(UNIX_EPOCH)
48        .map_or(0, |d| d.as_millis())
49}
50
51/// Record a completed MCP operation for the desktop diagnostics panel.
52pub fn record_mcp_call(
53    server: impl Into<String>,
54    method: impl Into<String>,
55    duration_ms: u64,
56    success: bool,
57    error: Option<String>,
58    result_bytes: usize,
59) {
60    let record = McpCallRecord {
61        timestamp_ms: now_ms(),
62        server: server.into(),
63        method: method.into(),
64        duration_ms,
65        success,
66        error: error.map(|e| super::diagnostics::redact_body_preview(&e)),
67        result_bytes,
68    };
69    if let Ok(mut log) = call_log().lock() {
70        log.push(record);
71    }
72}
73
74/// Recent MCP calls, newest last (up to [`MAX_RECENT_CALLS`]).
75#[must_use]
76pub fn recent_mcp_calls() -> Vec<McpCallRecord> {
77    call_log()
78        .lock()
79        .ok()
80        .map(|log| log.entries.iter().cloned().collect())
81        .unwrap_or_default()
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn recent_calls_ring_buffer() {
90        for i in 0..70 {
91            record_mcp_call("srv", format!("tool_{i}"), 1, true, None, 10);
92        }
93        let calls = recent_mcp_calls();
94        assert!(calls.len() <= MAX_RECENT_CALLS);
95        assert_eq!(calls.last().map(|c| c.method.as_str()), Some("tool_69"));
96    }
97}