Skip to main content

yapi_core/mcp/
log.rs

1//! Log messages servers send with `notifications/message`, appended to
2//! `mcp.log` in the agent directory and rotated to `mcp.log.1` past 5 MB.
3//! Port of `extensions/mcp/log.ts` in pi `v1.0.0`.
4
5use std::io::Write as _;
6use std::path::PathBuf;
7
8use serde_json::Value;
9
10const MAX_LOG_BYTES: u64 = 5 * 1024 * 1024;
11
12/// One log line: time, server, level, logger and data, with continuation
13/// lines indented.
14pub fn format_message(server: &str, params: &Value, time: &str) -> String {
15    let wrapped;
16    let message = if params.is_object() {
17        params
18    } else {
19        wrapped = serde_json::json!({ "data": params });
20        &wrapped
21    };
22    let level = message["level"].as_str().unwrap_or("info");
23    let logger = message["logger"]
24        .as_str()
25        .filter(|logger| !logger.is_empty())
26        .map(|logger| format!(" {logger}:"))
27        .unwrap_or_default();
28    let data = match &message["data"] {
29        Value::String(text) => text.clone(),
30        Value::Null if message.get("data").is_none() => "undefined".into(),
31        other => yapi_types::json::stringify(other),
32    };
33    let text = data.replace("\r\n", "\n").replace('\n', "\n    ");
34    format!("{time} [{server}] {level}{logger} {text}\n")
35}
36
37/// Appends server log messages to one file and ignores failures.
38#[derive(Clone)]
39pub struct Log {
40    path: PathBuf,
41}
42
43impl Log {
44    /// A log at `path`.
45    pub fn new(path: PathBuf) -> Log {
46        Log { path }
47    }
48
49    /// Appends one message from `server`.
50    pub fn write(&self, server: &str, params: &Value) {
51        let line = format_message(server, params, &crate::time::now_iso());
52        if let Some(dir) = self.path.parent() {
53            let _ = std::fs::create_dir_all(dir);
54        }
55        let size = std::fs::metadata(&self.path).map_or(0, |meta| meta.len());
56        if size > MAX_LOG_BYTES {
57            let _ = std::fs::rename(&self.path, self.path.with_extension("log.1"));
58        }
59        if let Ok(mut file) = std::fs::OpenOptions::new()
60            .create(true)
61            .append(true)
62            .open(&self.path)
63        {
64            let _ = file.write_all(line.as_bytes());
65        }
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use serde_json::json;
73
74    #[test]
75    fn formats_like_pi() {
76        assert_eq!(
77            format_message(
78                "docs",
79                &json!({"level": "error", "logger": "db", "data": "a\nb"}),
80                "T"
81            ),
82            "T [docs] error db: a\n    b\n"
83        );
84        assert_eq!(format_message("x", &json!(5), "T"), "T [x] info 5\n");
85    }
86}