Skip to main content

mcp_common/
diagnostic.rs

1//! 子进程启动诊断工具
2//!
3//! 提供 stdio 子进程启动时的环境诊断日志,供 mcp-proxy / mcp-sse-proxy /
4//! mcp-streamable-proxy 共用,避免诊断代码散落在业务逻辑中。
5
6use std::collections::HashMap;
7
8/// PATH 分隔符
9const PATH_SEP: char = if cfg!(windows) { ';' } else { ':' };
10
11/// 诊断时检查的镜像相关环境变量
12const MIRROR_ENV_KEYS: &[&str] = &[
13    "npm_config_registry",
14    "UV_INDEX_URL",
15    "UV_EXTRA_INDEX_URL",
16    "UV_INSECURE_HOST",
17    "PIP_INDEX_URL",
18];
19
20// ─── 纯数据格式化(不依赖日志框架) ───
21
22/// 返回 PATH 摘要字符串,只展示前 `max_segments` 段
23///
24/// 示例: `/usr/bin:/usr/local/bin ... (12 entries total)`
25pub fn format_path_summary(max_segments: usize) -> String {
26    match std::env::var("PATH") {
27        Ok(path) => {
28            let segments: Vec<&str> = path.split(PATH_SEP).collect();
29            let preview: String = segments
30                .iter()
31                .take(max_segments)
32                .copied()
33                .collect::<Vec<_>>()
34                .join(&PATH_SEP.to_string());
35            if segments.len() > max_segments {
36                format!("{} ... ({} entries total)", preview, segments.len())
37            } else {
38                preview
39            }
40        }
41        Err(_) => "(unset)".to_string(),
42    }
43}
44
45/// 收集当前进程中已设置的镜像相关环境变量
46///
47/// 返回 `Vec<(key, value)>`,仅包含已设置的条目。
48pub fn collect_mirror_env_vars() -> Vec<(&'static str, String)> {
49    MIRROR_ENV_KEYS
50        .iter()
51        .filter_map(|&key| std::env::var(key).ok().map(|val| (key, val)))
52        .collect()
53}
54
55/// 构造 spawn 失败时的完整错误信息
56pub fn format_spawn_error(
57    mcp_id: &str,
58    command: &str,
59    args: &Option<Vec<String>>,
60    inner: impl std::fmt::Display,
61) -> String {
62    let path_val = std::env::var("PATH").unwrap_or_else(|_| "(unset)".to_string());
63    format!(
64        "Failed to spawn child process - MCP ID: {}, command: {}, \
65         args: {:?}, PATH: {}, error: {}",
66        mcp_id,
67        command,
68        args.as_ref().unwrap_or(&Vec::new()),
69        path_val,
70        inner
71    )
72}
73
74// ─── 带 tracing 的便捷函数 ───
75
76/// 输出 stdio 子进程启动前的诊断日志(debug 级别)
77///
78/// 包含:PATH 摘要、镜像变量、config env keys 列表。
79/// 业务代码只需在 spawn 前调用此函数。
80pub fn log_stdio_spawn_context(tag: &str, mcp_id: &str, env: &Option<HashMap<String, String>>) {
81    tracing::debug!(
82        "[{}] MCP ID: {}, PATH: {}",
83        tag,
84        mcp_id,
85        format_path_summary(3),
86    );
87
88    for (key, val) in collect_mirror_env_vars() {
89        tracing::debug!("[{}] MCP ID: {}, {}={}", tag, mcp_id, key, val);
90    }
91
92    if let Some(env_vars) = env {
93        let keys: Vec<&String> = env_vars.keys().collect();
94        tracing::debug!("[{}] MCP ID: {}, config env keys: {:?}", tag, mcp_id, keys);
95    }
96}
97
98/// 启动阶段环境变量汇总(eprintln 输出,日志框架尚未初始化时使用)
99pub fn eprint_env_summary() {
100    eprintln!("  - PATH: {}", format_path_summary(3));
101
102    for (key, val) in collect_mirror_env_vars() {
103        eprintln!("  - {}={}", key, val);
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_format_path_summary_not_empty() {
113        let summary = format_path_summary(3);
114        assert!(!summary.is_empty());
115    }
116
117    #[test]
118    fn test_collect_mirror_env_vars() {
119        // 不应 panic
120        let _ = collect_mirror_env_vars();
121    }
122
123    #[test]
124    fn test_format_spawn_error() {
125        let msg = format_spawn_error(
126            "test-id",
127            "npx",
128            &Some(vec!["-y".into(), "server".into()]),
129            "file not found",
130        );
131        assert!(msg.contains("test-id"));
132        assert!(msg.contains("npx"));
133        assert!(msg.contains("file not found"));
134    }
135
136    #[test]
137    fn test_log_stdio_spawn_context_no_panic() {
138        let mut env = HashMap::new();
139        env.insert("FOO".to_string(), "bar".to_string());
140        log_stdio_spawn_context("Test", "test-id", &Some(env));
141        log_stdio_spawn_context("Test", "test-id", &None);
142    }
143}