use crate::error::{Error, Result};
use crate::mcp::tools::{Tool, ToolHandler, ToolResult};
use async_trait::async_trait;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use tracing::{info, instrument};
pub struct ArchitectAgentHandler;
impl Default for ArchitectAgentHandler {
fn default() -> Self {
Self::new()
}
}
impl ArchitectAgentHandler {
pub fn new() -> Self {
Self
}
pub fn tool_definitions() -> Vec<Tool> {
vec![
Self::create_adr_tool(),
Self::list_decisions_tool(),
Self::review_design_tool(),
]
}
fn create_adr_tool() -> Tool {
Tool::with_schema(
"architect_create_adr",
"Creates a new Architecture Decision Record (ADR) file.",
json!({
"type": "object",
"properties": {
"title": { "type": "string", "description": "Title of the decision" },
"status": { "type": "string", "description": "Status (Proposed, Accepted)", "default": "Proposed" },
"context": { "type": "string", "description": "Context and problem statement" },
"decision": { "type": "string", "description": "The decision made" },
"consequences": { "type": "string", "description": "Consequences (positive/negative)" },
"directory": { "type": "string", "description": "Output directory", "default": "docs/adr" }
},
"required": ["title", "context", "decision"],
"additionalProperties": false
}),
)
}
fn list_decisions_tool() -> Tool {
Tool::with_schema(
"architect_list_decisions",
"Lists all Architecture Decision Records (ADRs) in the project.",
json!({
"type": "object",
"properties": {
"directory": { "type": "string", "description": "ADR directory", "default": "docs/adr" }
},
"additionalProperties": false
}),
)
}
fn review_design_tool() -> Tool {
Tool::with_schema(
"architect_review_design",
"Reviews a design description against architectural principles.",
json!({
"type": "object",
"properties": {
"content": { "type": "string", "description": "Design content or path to file" },
"focus": { "type": "string", "description": "Focus area (e.g., scalability)" }
},
"required": ["content"],
"additionalProperties": false
}),
)
}
#[instrument(skip(self, arguments), fields(tool = %name))]
pub async fn call_tool(
&self,
name: &str,
arguments: HashMap<String, Value>,
) -> Result<ToolResult> {
info!(tool = %name, "Executing Architect Agent Tool");
match name {
"architect_create_adr" => self.handle_create_adr(arguments).await,
"architect_list_decisions" => self.handle_list_decisions(arguments).await,
"architect_review_design" => self.handle_review_design(arguments).await,
_ => Ok(ToolResult::error(format!("Unknown tool: {}", name))),
}
}
async fn handle_create_adr(&self, args: HashMap<String, Value>) -> Result<ToolResult> {
let title = args
.get("title")
.and_then(|v| v.as_str())
.unwrap_or("Untitled");
let status = args
.get("status")
.and_then(|v| v.as_str())
.unwrap_or("Proposed");
let context = args.get("context").and_then(|v| v.as_str()).unwrap_or("");
let decision = args.get("decision").and_then(|v| v.as_str()).unwrap_or("");
let consequences = args
.get("consequences")
.and_then(|v| v.as_str())
.unwrap_or("");
let dir = args
.get("directory")
.and_then(|v| v.as_str())
.unwrap_or("docs/adr");
let slug = title.to_lowercase().replace(" ", "-");
let date = chrono::Local::now().format("%Y-%m-%d").to_string();
let filename = format!("{}-{}.md", date, slug); let path = Path::new(dir).join(&filename);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| Error::Mcp(format!("Failed to create dir: {}", e)))?;
}
let content = format!(
"# ADR: {}
**Status:** {}
**Date:** {}
## Context
{}
## Decision
{}
## Consequences
{}
",
title, status, date, context, decision, consequences
);
fs::write(&path, &content)
.map_err(|e| Error::Mcp(format!("Failed to write ADR: {}", e)))?;
Ok(ToolResult::text(format!(
"Created ADR at: {}",
path.display()
)))
}
async fn handle_list_decisions(&self, args: HashMap<String, Value>) -> Result<ToolResult> {
let dir_str = args
.get("directory")
.and_then(|v| v.as_str())
.unwrap_or("docs/adr");
let dir = Path::new(dir_str);
if !dir.exists() {
return Ok(ToolResult::text("No ADR directory found."));
}
let mut adrs = Vec::new();
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "md") {
let name = path.file_name().unwrap().to_string_lossy().to_string();
adrs.push(name);
}
}
}
adrs.sort();
Ok(ToolResult::text(json!({ "decisions": adrs }).to_string()))
}
async fn handle_review_design(&self, args: HashMap<String, Value>) -> Result<ToolResult> {
let content = args.get("content").and_then(|v| v.as_str()).unwrap_or("");
let mut findings = Vec::new();
if !content.contains("trade-off") && !content.contains("tradeoff") {
findings.push("Missing trade-off analysis.");
}
if content.len() < 100 {
findings.push("Description too short for architectural review.");
}
let result = json!({
"status": "reviewed",
"findings": findings,
"score": if findings.is_empty() { 90 } else { 70 }
});
Ok(ToolResult::text(serde_json::to_string_pretty(&result)?))
}
}
#[async_trait]
impl ToolHandler for ArchitectAgentHandler {
async fn call(&self, arguments: HashMap<String, Value>) -> Result<ToolResult> {
let tool_name = arguments
.get("_tool")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| Error::Mcp("Missing _tool identifier".into()))?;
self.call_tool(&tool_name, arguments).await
}
}