Skip to content

Commit 1cbcfa4

Browse files
committed
chore: bump version to v0.1.53 for mcp-stdio-proxy and update environment handling in server builders
1 parent e9c2100 commit 1cbcfa4

File tree

7 files changed

+217
-88
lines changed

7 files changed

+217
-88
lines changed

CHANGELOG.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/).
7+
8+
## [0.1.52] - 2026-02-15
9+
10+
### Fixed
11+
12+
- **PATH 按段去重**: `ensure_runtime_path` 从整段 `starts_with` 改为按分隔符拆段去重,
13+
解决上层多次前置导致的 `node/bin` 重复条目问题
14+
- **config PATH 覆盖**: 用户在 MCP config env 中指定自定义 PATH 时,
15+
仍然确保应用内置运行时路径(`NUWAX_APP_RUNTIME_PATH`)在最前面
16+
- **env value 泄露**: `log_command_details` 不再打印 env 变量的 value,
17+
仅输出 key 列表,避免泄露敏感信息(如 token、secret)
18+
19+
### Added
20+
21+
- **`mcp-common::diagnostic` 模块**: 提取子进程启动诊断日志为独立模块,
22+
提供 `log_stdio_spawn_context``format_spawn_error``format_path_summary` 等公共函数,
23+
减少业务代码中的日志侵入
24+
- **启动阶段环境诊断**: `env_init` 结束后输出 PATH 摘要和镜像环境变量最终值
25+
- **spawn 失败上下文**: SSE/Stream 子进程 spawn 失败时输出完整错误上下文
26+
(command、args、PATH),便于快速定位可执行文件找不到的问题
27+
- **build 失败上下文**: `mcp_start_task` 中 SSE/Stream server build 失败时
28+
通过 `anyhow::Context` 附加 MCP ID 和服务类型
29+
- **`ensure_runtime_path` 单元测试**: 新增 5 个测试覆盖前置、部分去重、
30+
全部已存在、双重重复等场景
31+
32+
### Changed
33+
34+
- **server_builder PATH 逻辑简化**: 两侧 `connect_stdio` 从三分支
35+
(继承/config/缺失)统一为:取基础 PATH → `ensure_runtime_path` → 传递给子进程
36+
- **无镜像提示**: 未配置镜像源时输出提示行,而非静默跳过
37+
38+
## [0.1.51] - 2026-02-14
39+
40+
### Changed
41+
42+
- 版本号更新
43+
44+
## [0.1.49] - 2026-02-13
45+
46+
### Added
47+
48+
- **镜像源配置**: 支持通过 `config.yml` 配置 npm/PyPI 镜像源,
49+
环境变量优先级高于配置文件
50+
- **环境变量初始化**: `env_init` 模块统一管理子进程环境(镜像源 + 内置运行时 PATH)
51+
- **`UV_INSECURE_HOST` 支持**: HTTP 类型的 PyPI 镜像自动提取 host 并设置
52+
53+
## [0.1.48] - 2026-02-12
54+
55+
### Added
56+
57+
- **跨平台进程管理**: `process_compat` 模块提供 `wrap_process_v8` / `wrap_process_v9` 宏,
58+
统一 Unix(ProcessGroup)和 Windows(JobObject + CREATE_NO_WINDOW)的进程包装
59+
60+
### Fixed
61+
62+
- Windows 平台隐藏控制台窗口配置

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mcp-common/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub use client_config::McpClientConfig;
2323
pub use config::McpServiceConfig;
2424
pub use process_compat::check_windows_command;
2525
pub use process_compat::ensure_runtime_path;
26+
pub use process_compat::prepare_stdio_env;
2627
pub use tool_filter::ToolFilter;
2728

2829
// Re-export telemetry types when feature is enabled

mcp-common/src/process_compat.rs

Lines changed: 140 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,28 +97,94 @@ pub fn check_windows_command(_command: &str) {
9797
/// 此函数将这些路径插入到给定 PATH 的最前面,确保优先使用应用内置版本,
9898
/// 即使用户在 MCP 配置的 `env` 中指定了自定义 PATH。
9999
///
100+
/// **按段去重**:将 runtime_path 和现有 PATH 拆分为独立条目,
101+
/// 先放 runtime 段,再追加 PATH 中不在 runtime 里的段,彻底避免重复。
102+
///
100103
/// 如果 `NUWAX_APP_RUNTIME_PATH` 未设置或为空,直接返回原始 PATH。
101104
pub fn ensure_runtime_path(path: &str) -> String {
102105
if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") {
103106
let runtime_path = runtime_path.trim();
104107
if !runtime_path.is_empty() {
105108
let sep = if cfg!(windows) { ";" } else { ":" };
106-
// 避免重复:如果 PATH 已经以 runtime_path 开头则不重复添加
107-
if path == runtime_path
108-
|| path.starts_with(&format!("{}{}", runtime_path, sep))
109-
{
110-
return path.to_string();
109+
110+
// 将 runtime_path 拆成各段
111+
let runtime_segments: Vec<&str> =
112+
runtime_path.split(sep).filter(|s| !s.is_empty()).collect();
113+
114+
// 将现有 PATH 拆成各段,去掉已在 runtime 中的
115+
let existing_segments: Vec<&str> = path
116+
.split(sep)
117+
.filter(|s| !s.is_empty() && !runtime_segments.contains(s))
118+
.collect();
119+
120+
let merged: Vec<&str> = runtime_segments
121+
.iter()
122+
.copied()
123+
.chain(existing_segments)
124+
.collect();
125+
126+
let result = merged.join(sep);
127+
if result != path {
128+
tracing::info!(
129+
"[ProcessCompat] 前置应用内置运行时到 PATH: {}",
130+
runtime_path
131+
);
111132
}
112-
tracing::info!(
113-
"[ProcessCompat] 前置应用内置运行时到 PATH: {}",
114-
runtime_path
115-
);
116-
return format!("{}{}{}", runtime_path, sep, path);
133+
return result;
117134
}
118135
}
119136
path.to_string()
120137
}
121138

139+
/// 为 stdio 子进程准备最终的 PATH 和过滤后的环境变量。
140+
///
141+
/// 统一处理:
142+
/// 1. 从 config env 或父进程确定基础 PATH
143+
/// 2. Windows 上追加 npm 全局 bin 目录
144+
/// 3. 通过 `ensure_runtime_path` 按段去重前置应用内置运行时
145+
/// 4. 从 config env 中过滤掉 PATH(已单独处理)
146+
///
147+
/// 返回 `(Option<final_path>, filtered_env)`,调用方只需 apply 到 `cmd` 即可。
148+
pub fn prepare_stdio_env(
149+
env: &Option<std::collections::HashMap<String, String>>,
150+
) -> (Option<String>, Option<Vec<(String, String)>>) {
151+
// 1. 确定基础 PATH
152+
let base_path = if env.as_ref().map_or(true, |e| !e.contains_key("PATH")) {
153+
std::env::var("PATH").ok()
154+
} else {
155+
env.as_ref().and_then(|e| e.get("PATH").cloned())
156+
};
157+
158+
// 2. Windows: 追加 npm 全局 bin + 3. ensure_runtime_path
159+
let final_path = base_path.map(|path| {
160+
#[cfg(target_os = "windows")]
161+
let path = {
162+
if let Ok(appdata) = std::env::var("APPDATA") {
163+
let npm_path = format!(r"{}\npm", appdata);
164+
if !path.contains(&npm_path) {
165+
format!("{};{}", path, npm_path)
166+
} else {
167+
path
168+
}
169+
} else {
170+
tracing::warn!("Windows: APPDATA not found, skipping npm global bin");
171+
path
172+
}
173+
};
174+
ensure_runtime_path(&path)
175+
});
176+
177+
// 4. 过滤掉 PATH(已单独处理)
178+
let filtered_env = env.as_ref().map(|vars| {
179+
vars.iter()
180+
.filter(|(k, _)| k.as_str() != "PATH")
181+
.map(|(k, v)| (k.clone(), v.clone()))
182+
.collect()
183+
});
184+
185+
(final_path, filtered_env)
186+
}
187+
122188
/// 为 process-wrap 8.x 的 TokioCommandWrap 应用平台特定的包装
123189
///
124190
/// 此宏会根据目标平台自动应用正确的进程包装:
@@ -221,4 +287,68 @@ mod tests {
221287
check_windows_command("npx some-server");
222288
check_windows_command("test.cmd");
223289
}
290+
291+
#[test]
292+
fn test_ensure_runtime_path_no_env() {
293+
// NUWAX_APP_RUNTIME_PATH 未设置时,返回原始 PATH
294+
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
295+
let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
296+
assert_eq!(result, "/usr/bin:/usr/local/bin");
297+
}
298+
299+
#[test]
300+
fn test_ensure_runtime_path_prepend() {
301+
unsafe {
302+
std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
303+
}
304+
let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
305+
assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin:/usr/local/bin");
306+
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
307+
}
308+
309+
#[test]
310+
fn test_ensure_runtime_path_dedup() {
311+
// 模拟:PATH 中已有 runtime 的部分段 → 不应重复
312+
unsafe {
313+
std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
314+
}
315+
let result =
316+
ensure_runtime_path("/app/node/bin:/opt/homebrew/bin:/usr/bin");
317+
assert_eq!(
318+
result,
319+
"/app/node/bin:/app/uv/bin:/opt/homebrew/bin:/usr/bin"
320+
);
321+
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
322+
}
323+
324+
#[test]
325+
fn test_ensure_runtime_path_all_present() {
326+
// PATH 已含全部 runtime 段 → 仅调整顺序确保 runtime 在前
327+
unsafe {
328+
std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
329+
}
330+
let result =
331+
ensure_runtime_path("/app/uv/bin:/usr/bin:/app/node/bin");
332+
assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin");
333+
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
334+
}
335+
336+
#[test]
337+
fn test_ensure_runtime_path_double_node() {
338+
// 模拟日志中的问题:node/bin 出现两次
339+
unsafe {
340+
std::env::set_var(
341+
"NUWAX_APP_RUNTIME_PATH",
342+
"/app/node/bin:/app/uv/bin:/app/debug",
343+
);
344+
}
345+
let result = ensure_runtime_path(
346+
"/app/node/bin:/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin",
347+
);
348+
assert_eq!(
349+
result,
350+
"/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin"
351+
);
352+
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
353+
}
224354
}

mcp-proxy/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "mcp-stdio-proxy"
33
# 主入口版本,与 workspace 对外发布版本一致
4-
version = "0.1.52"
4+
version = "0.1.53"
55
edition = "2024"
66
authors = ["nuwax-ai"]
77
description = "MCP (Model Context Protocol) proxy server and CLI tool for protocol conversion and remote service access"

mcp-sse-proxy/src/server_builder.rs

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -297,49 +297,17 @@ impl SseServerBuilder {
297297
// process-wrap 会自动处理进程组(Unix)或 Job Object(Windows)
298298
// 并且在 Drop 时自动清理子进程树
299299
let mut wrapped_cmd = TokioCommandWrap::with_new(command, |cmd| {
300-
// 继承父进程的 PATH 环境变量(如果配置中未指定)
301-
if env.as_ref().map_or(true, |e| !e.contains_key("PATH")) {
302-
if let Ok(path) = std::env::var("PATH") {
303-
info!("[SseServerBuilder] Inheriting PATH from parent process");
304-
// Windows: 添加 npm 全局 bin 目录到 PATH
305-
#[cfg(target_os = "windows")]
306-
let path = {
307-
if let Ok(appdata) = std::env::var("APPDATA") {
308-
let npm_path = format!(r"{}\npm", appdata);
309-
if !path.contains(&npm_path) {
310-
info!(
311-
"[SseServerBuilder] Windows: Adding npm global bin to PATH: {}",
312-
npm_path
313-
);
314-
format!("{};{}", path, npm_path)
315-
} else {
316-
info!(
317-
"[SseServerBuilder] Windows: npm global bin already in PATH: {}",
318-
npm_path
319-
);
320-
path
321-
}
322-
} else {
323-
warn!(
324-
"[SseServerBuilder] Windows: APPDATA environment variable not found, using original PATH"
325-
);
326-
path
327-
}
328-
};
329-
cmd.env("PATH", path);
330-
} else {
331-
warn!(
332-
"[SseServerBuilder] Failed to read PATH environment variable from parent process"
333-
);
334-
}
300+
let (final_path, filtered_env) = mcp_common::prepare_stdio_env(env);
301+
if let Some(path) = final_path {
302+
cmd.env("PATH", path);
335303
} else {
336-
info!("[SseServerBuilder] Using PATH from MCP service configuration");
304+
warn!("[SseServerBuilder] PATH not available from parent process or config");
337305
}
338306
if let Some(cmd_args) = args {
339307
cmd.args(cmd_args);
340308
}
341-
if let Some(env_vars) = env {
342-
for (k, v) in env_vars {
309+
if let Some(vars) = filtered_env {
310+
for (k, v) in vars {
343311
cmd.env(k, v);
344312
}
345313
}

mcp-streamable-proxy/src/server_builder.rs

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -181,51 +181,19 @@ impl StreamServerBuilder {
181181
) -> Result<rmcp::service::RunningService<rmcp::RoleClient, ClientInfo>> {
182182
let mut cmd = Command::new(command);
183183

184-
// 继承父进程的 PATH 环境变量(如果配置中未指定)
185-
if env.as_ref().map_or(true, |e| !e.contains_key("PATH")) {
186-
if let Ok(path) = std::env::var("PATH") {
187-
info!("[StreamServerBuilder] Inheriting PATH from parent process");
188-
// Windows: 添加 npm 全局 bin 目录到 PATH
189-
#[cfg(target_os = "windows")]
190-
let path = {
191-
if let Ok(appdata) = std::env::var("APPDATA") {
192-
let npm_path = format!(r"{}\npm", appdata);
193-
if !path.contains(&npm_path) {
194-
info!(
195-
"[StreamServerBuilder] Windows: Adding npm global bin to PATH: {}",
196-
npm_path
197-
);
198-
format!("{};{}", path, npm_path)
199-
} else {
200-
info!(
201-
"[StreamServerBuilder] Windows: npm global bin already in PATH: {}",
202-
npm_path
203-
);
204-
path
205-
}
206-
} else {
207-
warn!(
208-
"[StreamServerBuilder] Windows: APPDATA environment variable not found, using original PATH"
209-
);
210-
path
211-
}
212-
};
213-
cmd.env("PATH", path);
214-
} else {
215-
warn!(
216-
"[StreamServerBuilder] Failed to read PATH environment variable from parent process"
217-
);
218-
}
184+
let (final_path, filtered_env) = mcp_common::prepare_stdio_env(env);
185+
if let Some(path) = final_path {
186+
cmd.env("PATH", path);
219187
} else {
220-
info!("[StreamServerBuilder] Using PATH from MCP service configuration");
188+
warn!("[StreamServerBuilder] PATH not available from parent process or config");
221189
}
222190

223191
if let Some(cmd_args) = args {
224192
cmd.args(cmd_args);
225193
}
226194

227-
if let Some(env_vars) = env {
228-
for (k, v) in env_vars {
195+
if let Some(vars) = filtered_env {
196+
for (k, v) in vars {
229197
cmd.env(k, v);
230198
}
231199
}

0 commit comments

Comments
 (0)