use serde_json::Value;
use tokio::sync::OnceCell;
use super::DispatchError;
static REVIEW_STATE: OnceCell<trusty_review::mcp::ReviewAppState> = OnceCell::const_new();
pub(super) fn review_tool_descriptors() -> Vec<Value> {
vec![
serde_json::json!({
"name": "tr_review_pr",
"description": "LLM-backed review of a GitHub pull request via the embedded trusty-review pipeline. Fetches the PR diff, retrieves code context from trusty-search, augments with this analyzer daemon's static-analysis context (loopback), and returns a structured verdict (APPROVE / APPROVE* / REQUEST_CHANGES / BLOCK / UNKNOWN) with actionable findings. Requires GITHUB_TOKEN and AWS Bedrock credentials (or OPENROUTER_API_KEY). Always dry-run — never posts a GitHub comment.",
"inputSchema": {
"type": "object",
"required": ["owner", "repo", "pr"],
"properties": {
"owner": { "type": "string", "description": "GitHub organisation or user that owns the repository" },
"repo": { "type": "string", "description": "GitHub repository name" },
"pr": { "type": "integer", "description": "Pull request number" },
"reviewer_model": { "type": "string", "description": "Override the reviewer model slug (e.g. 'bedrock/us.anthropic.claude-sonnet-4-6')" }
}
}
}),
serde_json::json!({
"name": "tr_review_diff",
"description": "LLM-backed review of a raw unified diff string via the embedded trusty-review pipeline. No GitHub credentials required. Useful for reviewing local changes, staged diffs, or patches. Requires AWS Bedrock credentials (or OPENROUTER_API_KEY). trusty-search + this analyzer daemon supply code and static-analysis context.",
"inputSchema": {
"type": "object",
"required": ["diff"],
"properties": {
"diff": { "type": "string", "description": "Unified diff string (output of `git diff` or similar)" },
"context": { "type": "string", "description": "Optional human-readable context (PR title/description, ticket, intent)" },
"reviewer_model": { "type": "string", "description": "Override the reviewer model slug (same format as tr_review_pr)" }
}
}
}),
serde_json::json!({
"name": "tr_review_health",
"description": "Probe the embedded trusty-review pipeline's liveness and configuration (dry_run mode, reviewer model, dependency URLs). Safe to call without any credentials.",
"inputSchema": { "type": "object", "properties": {} }
}),
]
}
pub(super) async fn handle_tr_review(tool: &str, args: &Value) -> Result<Value, DispatchError> {
let inner = tool.strip_prefix("tr_").unwrap_or(tool);
let state = REVIEW_STATE
.get_or_try_init(trusty_review::mcp::build_review_state)
.await
.map_err(|e| {
DispatchError::Transport(format!("failed to build trusty-review state: {e}"))
})?;
match trusty_review::mcp::call_review_tool(inner, args, state).await {
Ok(value) => Ok(value),
Err(trusty_review::mcp::ReviewToolError::UnknownTool) => Err(DispatchError::UnknownTool),
Err(trusty_review::mcp::ReviewToolError::InvalidParams(msg)) => {
Err(DispatchError::InvalidParams(msg))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn review_descriptors_are_tr_prefixed_and_complete() {
let descs = review_tool_descriptors();
let names: Vec<&str> = descs
.iter()
.filter_map(|d| d.get("name").and_then(Value::as_str))
.collect();
assert_eq!(names.len(), 3, "expected 3 tr_ tools, got {names:?}");
for required in ["tr_review_pr", "tr_review_diff", "tr_review_health"] {
assert!(names.contains(&required), "missing {required} in {names:?}");
}
for d in &descs {
assert!(d.get("inputSchema").is_some(), "missing inputSchema: {d}");
}
}
}