1- use std:: collections:: BTreeMap ;
2- use std:: path:: { Path , PathBuf } ;
1+ use crate :: cli:: McpCommand ;
32
4- use anyhow :: { anyhow , Context } ;
5- use serde_json :: Value ;
6-
7- use super :: mcp_sanitize :: sanitize_mcp_config ;
8- use crate :: { cli :: McpCommand , runtime_env } ;
3+ mod diagnose ;
4+ mod json ;
5+ # [ cfg ( test ) ]
6+ mod tests ;
7+ mod tools ;
98
109pub ( super ) async fn run ( command : McpCommand ) -> anyhow:: Result < ( ) > {
1110 match command {
@@ -16,253 +15,6 @@ pub(super) async fn run(command: McpCommand) -> anyhow::Result<()> {
1615 resources,
1716 prompts,
1817 json,
19- } => diagnose ( workspace, session, config, resources, prompts, json) . await ,
20- }
21- }
22-
23- async fn diagnose (
24- workspace : PathBuf ,
25- session : Option < String > ,
26- config : Option < PathBuf > ,
27- resources : bool ,
28- prompts : bool ,
29- json : bool ,
30- ) -> anyhow:: Result < ( ) > {
31- let ( _paths, agent) = runtime_env:: load_agent_runtime ( ) ?;
32- let ( _paths, cfg) = runtime_env:: load_runtime_config ( ) ?;
33-
34- std:: fs:: create_dir_all ( & workspace)
35- . with_context ( || format ! ( "create workspace: {}" , workspace. display( ) ) ) ?;
36-
37- let session_id = match session {
38- Some ( id) => id,
39- None => rexos:: harness:: resolve_session_id ( & workspace) ?,
40- } ;
41-
42- let raw_config = match config. as_ref ( ) {
43- Some ( path) => {
44- let raw = std:: fs:: read_to_string ( path)
45- . with_context ( || format ! ( "read mcp config: {}" , path. display( ) ) ) ?;
46- normalize_json_string ( & raw ) . context ( "normalize mcp config json" ) ?
47- }
48- None => {
49- let snapshot = agent
50- . load_session_policy_snapshot ( & session_id)
51- . with_context ( || format ! ( "load session policy snapshot: {session_id}" ) ) ?;
52- snapshot
53- . mcp_config_json
54- . ok_or_else ( || anyhow ! ( "no MCP config is set for session {session_id}" ) ) ?
55- }
56- } ;
57-
58- let parsed_config: Value =
59- serde_json:: from_str ( & raw_config) . context ( "parse mcp config JSON" ) ?;
60- let sanitized_config = sanitize_mcp_config ( & parsed_config) ;
61-
62- let mut tools =
63- rexos:: tools:: Toolset :: new_with_security_config ( workspace. clone ( ) , cfg. security . clone ( ) ) ?;
64- tools
65- . enable_mcp_from_json ( & raw_config)
66- . await
67- . context ( "connect mcp servers" ) ?;
68-
69- let servers_raw = tools. call ( "mcp_servers_list" , r#"{}"# ) . await ?;
70- let servers_json: Value =
71- serde_json:: from_str ( & servers_raw) . context ( "decode mcp_servers_list output" ) ?;
72- let tool_names = list_remote_tool_names ( & tools) ;
73-
74- let resources_json = if resources {
75- let out = tools. call ( "mcp_resources_list" , r#"{}"# ) . await ?;
76- Some ( serde_json:: from_str ( & out) . context ( "decode mcp_resources_list output" ) ?)
77- } else {
78- None
79- } ;
80-
81- let prompts_json = if prompts {
82- let out = tools. call ( "mcp_prompts_list" , r#"{}"# ) . await ?;
83- Some ( serde_json:: from_str ( & out) . context ( "decode mcp_prompts_list output" ) ?)
84- } else {
85- None
86- } ;
87-
88- if json {
89- let out = build_mcp_diagnose_json (
90- & workspace,
91- & session_id,
92- sanitized_config,
93- servers_json,
94- tool_names,
95- resources_json,
96- prompts_json,
97- ) ?;
98- println ! ( "{}" , serde_json:: to_string_pretty( & out) ?) ;
99- return Ok ( ( ) ) ;
100- }
101-
102- println ! ( "MCP diagnose" ) ;
103- println ! ( ) ;
104- println ! ( "workspace: {}" , workspace. display( ) ) ;
105- println ! ( "session_id: {session_id}" ) ;
106- if let Some ( path) = config. as_ref ( ) {
107- println ! ( "config_source: file {}" , path. display( ) ) ;
108- } else {
109- println ! ( "config_source: session" ) ;
110- }
111- if let Some ( servers) = sanitized_config
112- . get ( "servers" )
113- . and_then ( |v| v. as_object ( ) )
114- . map ( |obj| obj. keys ( ) . cloned ( ) . collect :: < Vec < _ > > ( ) )
115- {
116- if !servers. is_empty ( ) {
117- println ! ( "config_servers: {}" , servers. join( ", " ) ) ;
118- }
119- }
120- println ! ( ) ;
121- println ! ( "servers: {}" , servers_raw. trim( ) ) ;
122- println ! ( "remote_tools: {} tool(s)" , tool_names. len( ) ) ;
123- for name in tool_names {
124- println ! ( "- {name}" ) ;
125- }
126- if let Some ( v) = resources_json {
127- println ! ( ) ;
128- println ! ( "resources_list: {}" , serde_json:: to_string_pretty( & v) ?) ;
129- }
130- if let Some ( v) = prompts_json {
131- println ! ( ) ;
132- println ! ( "prompts_list: {}" , serde_json:: to_string_pretty( & v) ?) ;
133- }
134- Ok ( ( ) )
135- }
136-
137- fn build_mcp_diagnose_json (
138- workspace : & Path ,
139- session_id : & str ,
140- sanitized_config : Value ,
141- servers_json : Value ,
142- tool_names : Vec < String > ,
143- resources_json : Option < Value > ,
144- prompts_json : Option < Value > ,
145- ) -> anyhow:: Result < Value > {
146- let mut out = BTreeMap :: new ( ) ;
147- out. insert (
148- "workspace" . to_string ( ) ,
149- Value :: String ( workspace. display ( ) . to_string ( ) ) ,
150- ) ;
151- out. insert (
152- "session_id" . to_string ( ) ,
153- Value :: String ( session_id. to_string ( ) ) ,
154- ) ;
155- out. insert ( "config" . to_string ( ) , sanitized_config) ;
156- out. insert ( "servers" . to_string ( ) , servers_json) ;
157- out. insert ( "tool_names" . to_string ( ) , serde_json:: to_value ( tool_names) ?) ;
158- if let Some ( v) = resources_json {
159- out. insert ( "resources" . to_string ( ) , v) ;
160- }
161- if let Some ( v) = prompts_json {
162- out. insert ( "prompts" . to_string ( ) , v) ;
163- }
164- Ok ( Value :: Object ( out. into_iter ( ) . collect ( ) ) )
165- }
166-
167- fn normalize_json_string ( raw : & str ) -> anyhow:: Result < String > {
168- let json: Value = serde_json:: from_str ( raw) . context ( "parse JSON" ) ?;
169- Ok ( serde_json:: to_string ( & json) ?)
170- }
171-
172- fn list_remote_tool_names ( tools : & rexos:: tools:: Toolset ) -> Vec < String > {
173- let mut names: Vec < String > = tools
174- . definitions ( )
175- . into_iter ( )
176- . filter_map ( |def| {
177- let name = def. function . name ;
178- if name. starts_with ( "mcp_" ) && name. contains ( "__" ) {
179- Some ( name)
180- } else {
181- None
182- }
183- } )
184- . collect ( ) ;
185- names. sort ( ) ;
186- names
187- }
188-
189- #[ cfg( test) ]
190- mod tests {
191- use super :: * ;
192-
193- #[ test]
194- fn build_mcp_diagnose_json_includes_expected_keys_and_redaction ( ) {
195- let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
196- let workspace = tmp. path ( ) . join ( "workspace" ) ;
197-
198- let config = serde_json:: json!( {
199- "servers" : {
200- "s1" : {
201- "command" : "python" ,
202- "env" : { "API_KEY" : "secret" } ,
203- }
204- }
205- } ) ;
206- let sanitized = sanitize_mcp_config ( & config) ;
207-
208- let out = build_mcp_diagnose_json (
209- & workspace,
210- "s-test" ,
211- sanitized,
212- serde_json:: json!( [ "s1" ] ) ,
213- vec ! [ "mcp_s1__echo" . to_string( ) ] ,
214- None ,
215- None ,
216- )
217- . unwrap ( ) ;
218-
219- assert_eq ! (
220- out[ "workspace" ] . as_str( ) ,
221- Some ( workspace. display( ) . to_string( ) . as_str( ) )
222- ) ;
223- assert_eq ! ( out[ "session_id" ] . as_str( ) , Some ( "s-test" ) ) ;
224- assert ! ( out. get( "servers" ) . is_some( ) ) ;
225- assert ! ( out. get( "tool_names" ) . is_some( ) ) ;
226- assert_eq ! (
227- out[ "config" ] [ "servers" ] [ "s1" ] [ "env" ] [ "API_KEY" ] . as_str( ) ,
228- Some ( "[redacted]" )
229- ) ;
230-
231- let tool_names: Vec < String > = out[ "tool_names" ]
232- . as_array ( )
233- . unwrap ( )
234- . iter ( )
235- . filter_map ( |v| v. as_str ( ) . map ( |s| s. to_string ( ) ) )
236- . collect ( ) ;
237- assert_eq ! ( tool_names, vec![ "mcp_s1__echo" . to_string( ) ] ) ;
238- assert ! ( out. get( "resources" ) . is_none( ) ) ;
239- assert ! ( out. get( "prompts" ) . is_none( ) ) ;
240- }
241-
242- #[ test]
243- fn normalize_json_string_round_trips ( ) {
244- let out = normalize_json_string ( " {\" servers\" :{}} " ) . unwrap ( ) ;
245- assert_eq ! ( out, "{\" servers\" :{}}" ) ;
246- }
247-
248- #[ test]
249- fn sanitize_mcp_config_redacts_env_values ( ) {
250- let input = serde_json:: json!( {
251- "servers" : {
252- "s1" : {
253- "env" : { "API_KEY" : "secret" } ,
254- "command" : "python"
255- }
256- }
257- } ) ;
258- let sanitized = sanitize_mcp_config ( & input) ;
259- assert_eq ! (
260- sanitized[ "servers" ] [ "s1" ] [ "env" ] [ "API_KEY" ] . as_str( ) ,
261- Some ( "[redacted]" )
262- ) ;
263- assert_eq ! (
264- sanitized[ "servers" ] [ "s1" ] [ "command" ] . as_str( ) ,
265- Some ( "python" )
266- ) ;
18+ } => diagnose:: run_diagnose ( workspace, session, config, resources, prompts, json) . await ,
26719 }
26820}
0 commit comments