1use crate::mcp::KaizenMcp;
5use rmcp::ServiceExt;
6use rmcp::model::{CallToolRequestParams, CallToolResult};
7use serde::Serialize;
8use serde_json::{Map, Value};
9
10pub const WEB_TOOL_NAMES: &[&str] = &["kaizen_sessions_list"];
11
12#[derive(Debug, Serialize)]
13#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
14pub enum ToolOutput {
15 Json(Value),
16 Text(String),
17}
18
19pub async fn call(name: &str, args: Value) -> Result<ToolOutput, String> {
20 if !WEB_TOOL_NAMES.contains(&name) {
21 return Err(format!("unknown web tool: {name}"));
22 }
23 reject_refresh_scan(&args)?;
24 call_mcp(name, args_map(args)?).await.and_then(output)
25}
26
27fn reject_refresh_scan(args: &Value) -> Result<(), String> {
28 match args.get("refresh").and_then(Value::as_bool) {
29 Some(true) => Err("web Observe does not allow refresh scans".into()),
30 _ => Ok(()),
31 }
32}
33
34async fn call_mcp(name: &str, args: Map<String, Value>) -> Result<CallToolResult, String> {
35 let (server_half, client_half) = tokio::io::duplex(1_048_576);
36 let server = tokio::spawn(async move {
37 let svc = KaizenMcp.serve(server_half).await?;
38 svc.waiting().await?;
39 Ok::<_, anyhow::Error>(())
40 });
41 let client = ().serve(client_half).await.map_err(|e| e.to_string())?;
42 let params = CallToolRequestParams::new(name.to_string()).with_arguments(args);
43 let result = client.call_tool(params).await.map_err(|e| e.to_string());
44 drop(client);
45 server.abort();
46 result
47}
48
49fn args_map(args: Value) -> Result<Map<String, Value>, String> {
50 match args {
51 Value::Null => Ok(Map::new()),
52 Value::Object(map) => Ok(map),
53 _ => Err("tool args must be a JSON object".into()),
54 }
55}
56
57fn output(result: CallToolResult) -> Result<ToolOutput, String> {
58 let text = result_text(&result);
59 if result.is_error == Some(true) {
60 return Err(text);
61 }
62 Ok(match result.structured_content {
63 Some(value) => ToolOutput::Json(value),
64 None => parsed_json(&text).map_or(ToolOutput::Text(text), ToolOutput::Json),
65 })
66}
67
68fn parsed_json(text: &str) -> Option<Value> {
69 let trimmed = text.trim();
70 if !(trimmed.starts_with('{') || trimmed.starts_with('[')) {
71 return None;
72 }
73 serde_json::from_str(trimmed).ok()
74}
75
76fn result_text(result: &CallToolResult) -> String {
77 result
78 .content
79 .iter()
80 .filter_map(|c| c.raw.as_text().map(|t| t.text.as_str()))
81 .collect::<Vec<_>>()
82 .join("\n")
83}