@@ -7,6 +7,64 @@ use std::path::{Path, PathBuf};
77use crate :: config:: { atomic_write, get_claude_mcp_path, get_default_claude_mcp_path} ;
88use crate :: error:: AppError ;
99
10+ /// 需要在 Windows 上用 cmd /c 包装的命令
11+ /// 这些命令在 Windows 上实际是 .cmd 批处理文件,需要通过 cmd /c 来执行
12+ #[ cfg( windows) ]
13+ const WINDOWS_WRAP_COMMANDS : & [ & str ] = & [ "npx" , "npm" , "yarn" , "pnpm" , "node" , "bun" , "deno" ] ;
14+
15+ /// Windows 平台:将 `npx args...` 转换为 `cmd /c npx args...`
16+ /// 解决 Claude Code /doctor 报告的 "Windows requires 'cmd /c' wrapper to execute npx" 警告
17+ #[ cfg( windows) ]
18+ fn wrap_command_for_windows ( obj : & mut Map < String , Value > ) {
19+ // 只处理 stdio 类型(默认或显式)
20+ let server_type = obj. get ( "type" ) . and_then ( |v| v. as_str ( ) ) . unwrap_or ( "stdio" ) ;
21+ if server_type != "stdio" {
22+ return ;
23+ }
24+
25+ let Some ( cmd) = obj. get ( "command" ) . and_then ( |v| v. as_str ( ) ) else {
26+ return ;
27+ } ;
28+
29+ // 已经是 cmd 的不重复包装
30+ if cmd. eq_ignore_ascii_case ( "cmd" ) || cmd. eq_ignore_ascii_case ( "cmd.exe" ) {
31+ return ;
32+ }
33+
34+ // 提取命令名(去掉 .cmd 后缀和路径)
35+ let cmd_name = Path :: new ( cmd)
36+ . file_stem ( )
37+ . and_then ( |s| s. to_str ( ) )
38+ . unwrap_or ( cmd) ;
39+
40+ let needs_wrap = WINDOWS_WRAP_COMMANDS
41+ . iter ( )
42+ . any ( |& c| cmd_name. eq_ignore_ascii_case ( c) ) ;
43+
44+ if !needs_wrap {
45+ return ;
46+ }
47+
48+ // 构建新的 args: ["/c", "原命令", ...原args]
49+ let original_args = obj
50+ . get ( "args" )
51+ . and_then ( |v| v. as_array ( ) )
52+ . cloned ( )
53+ . unwrap_or_default ( ) ;
54+
55+ let mut new_args = vec ! [ Value :: String ( "/c" . into( ) ) , Value :: String ( cmd. into( ) ) ] ;
56+ new_args. extend ( original_args) ;
57+
58+ obj. insert ( "command" . into ( ) , Value :: String ( "cmd" . into ( ) ) ) ;
59+ obj. insert ( "args" . into ( ) , Value :: Array ( new_args) ) ;
60+ }
61+
62+ /// 非 Windows 平台无需处理
63+ #[ cfg( not( windows) ) ]
64+ fn wrap_command_for_windows ( _obj : & mut Map < String , Value > ) {
65+ // 非 Windows 平台不做任何处理
66+ }
67+
1068#[ derive( Debug , Clone , Serialize , Deserialize ) ]
1169#[ serde( rename_all = "camelCase" ) ]
1270pub struct McpStatus {
@@ -339,6 +397,9 @@ pub fn set_mcp_servers_map(
339397 obj. remove ( "homepage" ) ;
340398 obj. remove ( "docs" ) ;
341399
400+ // Windows 平台自动包装 npx/npm 等命令为 cmd /c 格式
401+ wrap_command_for_windows ( & mut obj) ;
402+
342403 out. insert ( id. clone ( ) , Value :: Object ( obj) ) ;
343404 }
344405
@@ -352,3 +413,136 @@ pub fn set_mcp_servers_map(
352413 write_json_value ( & path, & root) ?;
353414 Ok ( ( ) )
354415}
416+
417+ #[ cfg( test) ]
418+ mod tests {
419+ use super :: * ;
420+ use serde_json:: json;
421+
422+ /// 测试 Windows 命令包装功能
423+ /// 由于使用条件编译,在非 Windows 平台上测试的是空函数
424+ #[ test]
425+ fn test_wrap_command_for_windows_npx ( ) {
426+ let mut obj = json ! ( { "command" : "npx" , "args" : [ "-y" , "@upstash/context7-mcp" ] } )
427+ . as_object ( )
428+ . unwrap ( )
429+ . clone ( ) ;
430+ wrap_command_for_windows ( & mut obj) ;
431+
432+ #[ cfg( windows) ]
433+ {
434+ assert_eq ! ( obj[ "command" ] , "cmd" ) ;
435+ assert_eq ! (
436+ obj[ "args" ] ,
437+ json!( [ "/c" , "npx" , "-y" , "@upstash/context7-mcp" ] )
438+ ) ;
439+ }
440+
441+ #[ cfg( not( windows) ) ]
442+ {
443+ // 非 Windows 平台不做任何处理
444+ assert_eq ! ( obj[ "command" ] , "npx" ) ;
445+ }
446+ }
447+
448+ #[ test]
449+ fn test_wrap_command_for_windows_npm ( ) {
450+ let mut obj = json ! ( { "command" : "npm" , "args" : [ "run" , "start" ] } )
451+ . as_object ( )
452+ . unwrap ( )
453+ . clone ( ) ;
454+ wrap_command_for_windows ( & mut obj) ;
455+
456+ #[ cfg( windows) ]
457+ {
458+ assert_eq ! ( obj[ "command" ] , "cmd" ) ;
459+ assert_eq ! ( obj[ "args" ] , json!( [ "/c" , "npm" , "run" , "start" ] ) ) ;
460+ }
461+ }
462+
463+ #[ test]
464+ fn test_wrap_command_for_windows_already_cmd ( ) {
465+ // 已经是 cmd 的不应该重复包装
466+ let mut obj = json ! ( { "command" : "cmd" , "args" : [ "/c" , "npx" , "-y" , "foo" ] } )
467+ . as_object ( )
468+ . unwrap ( )
469+ . clone ( ) ;
470+ wrap_command_for_windows ( & mut obj) ;
471+
472+ assert_eq ! ( obj[ "command" ] , "cmd" ) ;
473+ // args 应该保持不变,不会变成 ["/c", "cmd", "/c", "npx", ...]
474+ assert_eq ! ( obj[ "args" ] , json!( [ "/c" , "npx" , "-y" , "foo" ] ) ) ;
475+ }
476+
477+ #[ test]
478+ fn test_wrap_command_for_windows_http_type_skipped ( ) {
479+ // http 类型不应该被处理
480+ let mut obj = json ! ( { "type" : "http" , "url" : "https://example.com/mcp" } )
481+ . as_object ( )
482+ . unwrap ( )
483+ . clone ( ) ;
484+ wrap_command_for_windows ( & mut obj) ;
485+
486+ assert ! ( !obj. contains_key( "command" ) ) ;
487+ assert_eq ! ( obj[ "url" ] , "https://example.com/mcp" ) ;
488+ }
489+
490+ #[ test]
491+ fn test_wrap_command_for_windows_other_command_skipped ( ) {
492+ // 非目标命令(如 python)不应该被包装
493+ let mut obj = json ! ( { "command" : "python" , "args" : [ "server.py" ] } )
494+ . as_object ( )
495+ . unwrap ( )
496+ . clone ( ) ;
497+ wrap_command_for_windows ( & mut obj) ;
498+
499+ // python 不在 WINDOWS_WRAP_COMMANDS 列表中,不应该被包装
500+ assert_eq ! ( obj[ "command" ] , "python" ) ;
501+ assert_eq ! ( obj[ "args" ] , json!( [ "server.py" ] ) ) ;
502+ }
503+
504+ #[ test]
505+ fn test_wrap_command_for_windows_no_args ( ) {
506+ // 没有 args 的情况
507+ let mut obj = json ! ( { "command" : "npx" } ) . as_object ( ) . unwrap ( ) . clone ( ) ;
508+ wrap_command_for_windows ( & mut obj) ;
509+
510+ #[ cfg( windows) ]
511+ {
512+ assert_eq ! ( obj[ "command" ] , "cmd" ) ;
513+ assert_eq ! ( obj[ "args" ] , json!( [ "/c" , "npx" ] ) ) ;
514+ }
515+ }
516+
517+ #[ test]
518+ fn test_wrap_command_for_windows_with_cmd_suffix ( ) {
519+ // 处理 npx.cmd 格式
520+ let mut obj = json ! ( { "command" : "npx.cmd" , "args" : [ "-y" , "foo" ] } )
521+ . as_object ( )
522+ . unwrap ( )
523+ . clone ( ) ;
524+ wrap_command_for_windows ( & mut obj) ;
525+
526+ #[ cfg( windows) ]
527+ {
528+ assert_eq ! ( obj[ "command" ] , "cmd" ) ;
529+ assert_eq ! ( obj[ "args" ] , json!( [ "/c" , "npx.cmd" , "-y" , "foo" ] ) ) ;
530+ }
531+ }
532+
533+ #[ test]
534+ fn test_wrap_command_for_windows_case_insensitive ( ) {
535+ // 大小写不敏感
536+ let mut obj = json ! ( { "command" : "NPX" , "args" : [ "-y" , "foo" ] } )
537+ . as_object ( )
538+ . unwrap ( )
539+ . clone ( ) ;
540+ wrap_command_for_windows ( & mut obj) ;
541+
542+ #[ cfg( windows) ]
543+ {
544+ assert_eq ! ( obj[ "command" ] , "cmd" ) ;
545+ assert_eq ! ( obj[ "args" ] , json!( [ "/c" , "NPX" , "-y" , "foo" ] ) ) ;
546+ }
547+ }
548+ }
0 commit comments