Skip to content

Commit b69b6ec

Browse files
committed
refactor: split session and mcp dispatch modules
1 parent cc173e6 commit b69b6ec

10 files changed

Lines changed: 563 additions & 523 deletions

File tree

Lines changed: 7 additions & 255 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
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

109
pub(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
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use std::path::PathBuf;
2+
3+
use anyhow::{anyhow, Context};
4+
use serde_json::Value;
5+
6+
use super::super::mcp_sanitize::sanitize_mcp_config;
7+
use super::json::{build_mcp_diagnose_json, normalize_json_string};
8+
use super::tools::list_remote_tool_names;
9+
use crate::runtime_env;
10+
11+
pub(super) async fn run_diagnose(
12+
workspace: PathBuf,
13+
session: Option<String>,
14+
config: Option<PathBuf>,
15+
resources: bool,
16+
prompts: bool,
17+
json: bool,
18+
) -> anyhow::Result<()> {
19+
let (_paths, agent) = runtime_env::load_agent_runtime()?;
20+
let (_paths, cfg) = runtime_env::load_runtime_config()?;
21+
22+
std::fs::create_dir_all(&workspace)
23+
.with_context(|| format!("create workspace: {}", workspace.display()))?;
24+
25+
let session_id = match session {
26+
Some(id) => id,
27+
None => rexos::harness::resolve_session_id(&workspace)?,
28+
};
29+
30+
let raw_config = match config.as_ref() {
31+
Some(path) => {
32+
let raw = std::fs::read_to_string(path)
33+
.with_context(|| format!("read mcp config: {}", path.display()))?;
34+
normalize_json_string(&raw).context("normalize mcp config json")?
35+
}
36+
None => {
37+
let snapshot = agent
38+
.load_session_policy_snapshot(&session_id)
39+
.with_context(|| format!("load session policy snapshot: {session_id}"))?;
40+
snapshot
41+
.mcp_config_json
42+
.ok_or_else(|| anyhow!("no MCP config is set for session {session_id}"))?
43+
}
44+
};
45+
46+
let parsed_config: Value =
47+
serde_json::from_str(&raw_config).context("parse mcp config JSON")?;
48+
let sanitized_config = sanitize_mcp_config(&parsed_config);
49+
50+
let mut tools =
51+
rexos::tools::Toolset::new_with_security_config(workspace.clone(), cfg.security.clone())?;
52+
tools
53+
.enable_mcp_from_json(&raw_config)
54+
.await
55+
.context("connect mcp servers")?;
56+
57+
let servers_raw = tools.call("mcp_servers_list", r#"{}"#).await?;
58+
let servers_json: Value =
59+
serde_json::from_str(&servers_raw).context("decode mcp_servers_list output")?;
60+
let tool_names = list_remote_tool_names(&tools);
61+
62+
let resources_json = if resources {
63+
let out = tools.call("mcp_resources_list", r#"{}"#).await?;
64+
Some(serde_json::from_str(&out).context("decode mcp_resources_list output")?)
65+
} else {
66+
None
67+
};
68+
69+
let prompts_json = if prompts {
70+
let out = tools.call("mcp_prompts_list", r#"{}"#).await?;
71+
Some(serde_json::from_str(&out).context("decode mcp_prompts_list output")?)
72+
} else {
73+
None
74+
};
75+
76+
if json {
77+
let out = build_mcp_diagnose_json(
78+
&workspace,
79+
&session_id,
80+
sanitized_config,
81+
servers_json,
82+
tool_names,
83+
resources_json,
84+
prompts_json,
85+
)?;
86+
println!("{}", serde_json::to_string_pretty(&out)?);
87+
return Ok(());
88+
}
89+
90+
println!("MCP diagnose");
91+
println!();
92+
println!("workspace: {}", workspace.display());
93+
println!("session_id: {session_id}");
94+
if let Some(path) = config.as_ref() {
95+
println!("config_source: file {}", path.display());
96+
} else {
97+
println!("config_source: session");
98+
}
99+
if let Some(servers) = sanitized_config
100+
.get("servers")
101+
.and_then(|v| v.as_object())
102+
.map(|obj| obj.keys().cloned().collect::<Vec<_>>())
103+
{
104+
if !servers.is_empty() {
105+
println!("config_servers: {}", servers.join(", "));
106+
}
107+
}
108+
println!();
109+
println!("servers: {}", servers_raw.trim());
110+
println!("remote_tools: {} tool(s)", tool_names.len());
111+
for name in tool_names {
112+
println!("- {name}");
113+
}
114+
if let Some(v) = resources_json {
115+
println!();
116+
println!("resources_list: {}", serde_json::to_string_pretty(&v)?);
117+
}
118+
if let Some(v) = prompts_json {
119+
println!();
120+
println!("prompts_list: {}", serde_json::to_string_pretty(&v)?);
121+
}
122+
Ok(())
123+
}

0 commit comments

Comments
 (0)