use std::process::Stdio;
use rmcp::{
handler::server::wrapper::Parameters,
model::{ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router,
transport::stdio,
ErrorData as McpError, ServerHandler, ServiceExt,
};
use serde::Deserialize;
use serde_json::{Map, Value};
use tokio::process::Command;
const DEFAULT_BIN: &str = "rca_cli";
fn rca_cli_bin() -> String {
std::env::var("RCA_CLI_BIN").unwrap_or_else(|_| DEFAULT_BIN.to_string())
}
#[derive(Clone)]
struct RcaServer;
#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct AnalyzeParams {
archive_path: String,
#[serde(default)]
parser: Option<String>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct SummarizeParams {
archive_path: String,
}
async fn run_cli(args: &[String]) -> Result<String, McpError> {
let bin = rca_cli_bin();
let output = Command::new(&bin)
.args(args)
.stdin(Stdio::null())
.output()
.await
.map_err(|e| {
McpError::internal_error(
format!(
"failed to spawn rca_cli ('{bin}'): {e}. \
Set RCA_CLI_BIN to the compiled binary path."
),
None,
)
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(McpError::internal_error(
format!("rca_cli exited with {}: {}", output.status, stderr.trim()),
None,
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
fn fired(value: &Value) -> bool {
match value {
Value::Object(o) => {
o.get("found").and_then(Value::as_bool).unwrap_or(false)
|| o.get("count")
.and_then(Value::as_i64)
.map(|c| c > 0)
.unwrap_or(false)
}
Value::Array(a) => !a.is_empty(),
_ => false,
}
}
#[tool_router]
impl RcaServer {
#[tool(
description = "List all available RCA detectors (parser name + the file-path regex each runs on)."
)]
async fn list_parsers(&self) -> Result<String, McpError> {
let text = run_cli(&["--list-parsers".to_string()]).await?;
let mut parsers = Vec::new();
for line in text.lines() {
if let Some((name, pattern)) = line.split_once(" -> ") {
parsers.push(serde_json::json!({
"name": name.trim(),
"pattern": pattern.trim(),
}));
}
}
serde_json::to_string_pretty(&Value::Array(parsers))
.map_err(|e| McpError::internal_error(format!("encode error: {e}"), None))
}
#[tool(
description = "Analyze a support archive (sosreport/supportconfig) and return the full structured findings as JSON. Optionally restrict the run to a single parser by name."
)]
async fn analyze_archive(
&self,
Parameters(AnalyzeParams {
archive_path,
parser,
}): Parameters<AnalyzeParams>,
) -> Result<String, McpError> {
let mut args = vec![archive_path, "--json".to_string()];
if let Some(p) = parser {
args.push("--parser".to_string());
args.push(p);
}
run_cli(&args).await
}
#[tool(
description = "Analyze a support archive and return only the detectors that fired (found=true or count>0), as a compact JSON summary suitable for grounding an agent."
)]
async fn summarize_findings(
&self,
Parameters(SummarizeParams { archive_path }): Parameters<SummarizeParams>,
) -> Result<String, McpError> {
let raw = run_cli(&[archive_path, "--json".to_string()]).await?;
let parsed: Value = serde_json::from_str(&raw).map_err(|e| {
McpError::internal_error(format!("rca_cli did not return valid JSON: {e}"), None)
})?;
let obj = parsed
.as_object()
.ok_or_else(|| McpError::internal_error("expected a JSON object from rca_cli", None))?;
let mut findings = Map::new();
for (k, v) in obj {
if matches!(k.as_str(), "fileCount" | "matchedFiles" | "fileTypes") {
continue;
}
if fired(v) {
findings.insert(k.clone(), v.clone());
}
}
let summary = serde_json::json!({
"fileCount": obj.get("fileCount").cloned().unwrap_or(Value::Null),
"matchedFiles": obj.get("matchedFiles").cloned().unwrap_or(Value::Null),
"firedCount": findings.len(),
"findings": Value::Object(findings),
});
serde_json::to_string_pretty(&summary)
.map_err(|e| McpError::internal_error(format!("encode error: {e}"), None))
}
}
#[tool_handler]
impl ServerHandler for RcaServer {
fn get_info(&self) -> ServerInfo {
let mut info = ServerInfo::default();
info.instructions = Some(
"RCA tool MCP server. Use list_parsers to discover detectors, \
analyze_archive for the full JSON findings of a support archive, \
and summarize_findings for a compact view of only the detectors \
that fired."
.to_string(),
);
info.capabilities = ServerCapabilities::builder().enable_tools().build();
info
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let service = RcaServer.serve(stdio()).await?;
service.waiting().await?;
Ok(())
}