pub mod console_metrics;
pub mod tools;
use std::sync::Arc;
use anyhow::Result;
use serde_json::Value;
use tracing::{debug, info, warn};
use trusty_common::mcp::{Request, Response, error_codes, initialize_response, run_stdio_loop};
use crate::config::ReviewConfig;
use crate::integrations::{analyze_client::HttpAnalyzeClient, search_client::HttpSearchClient};
use crate::llm::build_provider;
use crate::mcp::tools::{ToolError, call_tool, tool_descriptors, wrap_tool_error};
use crate::service::AppState;
pub use crate::mcp::tools::{ToolError as ReviewToolError, call_tool as call_review_tool};
pub use crate::service::AppState as ReviewAppState;
pub async fn run(state: AppState) -> Result<()> {
let state = Arc::new(state);
run_stdio_loop(move |req| {
let state = Arc::clone(&state);
async move { dispatch(req, &state).await }
})
.await
}
pub async fn build_review_state() -> Result<AppState> {
let config = ReviewConfig::load(None);
let reviewer_model = config.role_models.reviewer.model.clone();
let default_provider = config.role_models.reviewer.provider.clone();
let llm = build_provider(
&reviewer_model,
&default_provider,
&config.openrouter_api_key,
)
.await
.map_err(|e| anyhow::anyhow!("failed to build reviewer LLM provider: {e}"))?;
let verifier = if config.verification.enabled {
let role = &config.role_models.verifier;
match build_provider(&role.model, &role.provider, &config.openrouter_api_key).await {
Ok(p) => Some(p),
Err(e) => {
warn!("failed to build verifier provider (continuing without verification): {e}");
None
}
}
} else {
None
};
let search = HttpSearchClient::from_config(&config)
.map_err(|e| anyhow::anyhow!("failed to build search HTTP client: {e}"))?;
let analyze = HttpAnalyzeClient::from_config(&config)
.map_err(|e| anyhow::anyhow!("failed to build analyze HTTP client: {e}"))?;
info!(
reviewer_model = %config.role_models.reviewer.model,
analyzer_url = %config.analyzer_url,
search_url = %config.search_url,
"trusty-review embedded AppState built"
);
Ok(AppState::with_verifier_and_dedup(
config,
llm,
verifier,
Arc::new(search),
Some(Arc::new(analyze)),
None,
))
}
pub async fn dispatch(req: Request, state: &AppState) -> Response {
let is_notification = req.id.is_none();
let id = req.id.clone();
if req.jsonrpc.as_deref() != Some("2.0") {
if is_notification {
return Response::suppressed();
}
return Response::err(id, error_codes::INVALID_REQUEST, "jsonrpc must be \"2.0\"");
}
match req.method.as_str() {
"initialize" => {
return Response::ok(
id,
initialize_response("trusty-review", env!("CARGO_PKG_VERSION"), None),
);
}
"notifications/initialized" | "initialized" => {
return Response::suppressed();
}
_ => {}
}
let params = req.params.clone().unwrap_or(Value::Null);
let (tool, arguments, via_tools_call) = match req.method.as_str() {
"tools/call" => {
let name = params
.get("name")
.and_then(Value::as_str)
.map(str::to_owned);
let args = params
.get("arguments")
.cloned()
.unwrap_or(Value::Object(Default::default()));
match name {
Some(n) => (n, args, true),
None => {
return Response::err(
id,
error_codes::INVALID_PARAMS,
"tools/call requires a 'name' field",
);
}
}
}
"tools/list" => {
return Response::ok(id, serde_json::json!({ "tools": tool_descriptors() }));
}
other => (other.to_string(), params, false),
};
debug!(tool, via_tools_call, "mcp dispatch");
let outcome = call_tool(&tool, &arguments, state).await;
if via_tools_call {
match outcome {
Ok(value) => Response::ok(id, value),
Err(ToolError::UnknownTool) => Response::err(
id,
error_codes::METHOD_NOT_FOUND,
format!("unknown tool: {tool}"),
),
Err(ToolError::InvalidParams(msg)) => Response::ok(id, wrap_tool_error(&msg)),
}
} else {
match outcome {
Ok(value) => Response::ok(id, value),
Err(ToolError::UnknownTool) => Response::err(
id,
error_codes::METHOD_NOT_FOUND,
format!("unknown tool: {tool}"),
),
Err(ToolError::InvalidParams(msg)) => {
Response::err(id, error_codes::INVALID_PARAMS, msg)
}
}
}
}
#[cfg(test)]
#[path = "mcp_tests.rs"]
mod tests;