sdd-layer 0.16.0

Spec-Driven Development CLI and agent harness
use anyhow::{anyhow, bail, Result};
use serde_json::{json, Value};
use std::ffi::OsStr;
use std::fs;
use std::path::PathBuf;
use walkdir::WalkDir;

use crate::{contract, runtime};

pub struct SddMcpBackend {
    pub root: PathBuf,
}

impl runtime::mcp::McpBackend for SddMcpBackend {
    fn call_tool(&self, name: &str, arguments: &Value) -> Result<Value> {
        match name {
            "sdd_trace_list" => {
                let mut events = runtime::trace::read_events(&self.root)?;
                if let Some(orchestration) = string_arg(arguments, "orchestration") {
                    events = runtime::trace::filter_by_orchestration(events, &orchestration);
                }
                Ok(json!({ "events": events }))
            }
            "sdd_trace_show" => {
                let run_id = required_string_arg(arguments, "run_id")?;
                Ok(serde_json::to_value(runtime::trace::trace_tree(
                    &self.root, &run_id,
                )?)?)
            }
            "sdd_trace_summary" => {
                let mut events = runtime::trace::read_events(&self.root)?;
                if let Some(orchestration) = string_arg(arguments, "orchestration") {
                    events = runtime::trace::filter_by_orchestration(events, &orchestration);
                }
                Ok(serde_json::to_value(runtime::trace::summary(&events))?)
            }
            "sdd_artifact_status" => {
                let orchestration = string_arg(arguments, "orchestration")
                    .unwrap_or_else(|| "sdd-orchestration".to_string());
                crate::artifact_status_value(&self.root, &orchestration)
            }
            "sdd_context_build" => {
                let orchestration = required_string_arg(arguments, "orchestration")?;
                let stage = required_string_arg(arguments, "stage")?;
                let task = string_arg(arguments, "task");
                crate::sdd_mcp_context_build_value(
                    &self.root,
                    &orchestration,
                    &stage,
                    task.as_deref(),
                )
            }
            "sdd_clients_doctor" => Ok(crate::sdd_mcp_clients_doctor_value(&self.root)),
            "sdd_runtime_adapters" => runtime_adapters_value(),
            "sdd_optimize_status" => crate::sdd_mcp_optimize_status_value(&self.root),
            "sdd_context_handoff" => {
                let orchestration = required_string_arg(arguments, "orchestration")?;
                let stage = required_string_arg(arguments, "stage")?;
                let task = string_arg(arguments, "task");
                crate::sdd_mcp_context_handoff_value(
                    &self.root,
                    &orchestration,
                    &stage,
                    task.as_deref(),
                )
            }
            "sdd_capabilities_status" => crate::sdd_mcp_capabilities_status_value(&self.root),
            "sdd_agents_manifest" => {
                let agent_id =
                    string_arg(arguments, "agent_id").unwrap_or_else(|| "sdd-orchestrator".into());
                crate::sdd_mcp_agents_manifest_value(&agent_id)
            }
            other => bail!("unknown SDD MCP tool `{other}`"),
        }
    }

    fn list_resources(&self) -> Result<Vec<Value>> {
        let mut resources = vec![
            resource(
                "sdd://runtime/adapters",
                "runtime/adapters",
                "SDD runtime adapters",
                "Optional MCP/provider adapter metadata for this sdd binary",
                "application/json",
            ),
            resource(
                "sdd://optimization/status",
                "optimization/status",
                "SDD optimization status",
                "Read optimization wrapper status for CodeGraph, RTK and Caveman",
                "application/json",
            ),
            resource(
                "sdd://capabilities/catalog",
                "capabilities/catalog",
                "SDD capability catalog",
                "Read capability catalog and doctor status",
                "application/json",
            ),
            resource(
                "sdd://agents/sdd-orchestrator",
                "agents/sdd-orchestrator",
                "SDD orchestrator agent manifest",
                "Read the canonical SDD agent manifest",
                "application/json",
            ),
        ];
        let docs = self.root.join("docs");
        if docs.exists() {
            for entry in WalkDir::new(&docs)
                .max_depth(2)
                .into_iter()
                .filter_map(|entry| entry.ok())
                .filter(|entry| entry.file_type().is_file())
            {
                let path = entry.path();
                let Some(parent) = path
                    .parent()
                    .and_then(|path| path.file_name())
                    .and_then(OsStr::to_str)
                else {
                    continue;
                };
                let Some(stage) = path
                    .file_stem()
                    .and_then(OsStr::to_str)
                    .and_then(contract::stage_by_filename_stem)
                else {
                    continue;
                };
                resources.push(resource(
                    &format!("sdd://artifact/{parent}/{}", stage.key),
                    &format!("{parent}/{}", stage.filename),
                    &stage.label,
                    "Local SDD artifact",
                    "text/markdown",
                ));
                resources.push(resource(
                    &format!("sdd://handoff/{parent}/{}", stage.key),
                    &format!("{parent}/handoff/{}", stage.key),
                    "SDD context handoff",
                    "Read a context handoff derived from optimization and artifacts",
                    "application/json",
                ));
            }
        }
        for run_id in runtime::trace::summary(&runtime::trace::read_events(&self.root)?).roots {
            resources.push(resource(
                &format!("sdd://trace/{run_id}"),
                &run_id,
                "SDD trace tree",
                "Read an SDD trace tree as JSON",
                "application/json",
            ));
        }
        Ok(resources)
    }

    fn read_resource(&self, uri: &str) -> Result<runtime::mcp::McpResource> {
        let segments = runtime::trace::validate_sdd_uri(uri)?;
        match segments.as_slice() {
            [kind, orchestration, artifact] if kind == "artifact" => {
                let stage = contract::stage_by_key(artifact)
                    .or_else(|| contract::stage_by_command(artifact))
                    .ok_or_else(|| anyhow!("unknown artifact `{artifact}`"))?;
                let path = crate::artifact_dir(&self.root, orchestration).join(&stage.filename);
                let text = fs::read_to_string(&path)?;
                Ok(mcp_resource(uri, "text/markdown", text))
            }
            [kind, run_id] if kind == "trace" => {
                let tree = runtime::trace::trace_tree(&self.root, run_id)?;
                Ok(mcp_resource(
                    uri,
                    "application/json",
                    serde_json::to_string_pretty(&tree)?,
                ))
            }
            [kind, orchestration, stage] if kind == "context" => {
                let text = crate::sdd_mcp_context_resource_text(&self.root, orchestration, stage)?;
                Ok(mcp_resource(uri, "text/markdown", text))
            }
            [kind, orchestration, stage] if kind == "handoff" => Ok(mcp_resource(
                uri,
                "application/json",
                serde_json::to_string_pretty(&crate::sdd_mcp_context_handoff_value(
                    &self.root,
                    orchestration,
                    stage,
                    None,
                )?)?,
            )),
            [kind, resource] if kind == "runtime" && resource == "adapters" => Ok(mcp_resource(
                uri,
                "application/json",
                serde_json::to_string_pretty(&runtime_adapters_value()?)?,
            )),
            [kind, resource] if kind == "optimization" && resource == "status" => Ok(mcp_resource(
                uri,
                "application/json",
                serde_json::to_string_pretty(&crate::sdd_mcp_optimize_status_value(&self.root)?)?,
            )),
            [kind, resource] if kind == "capabilities" && resource == "catalog" => {
                Ok(mcp_resource(
                    uri,
                    "application/json",
                    serde_json::to_string_pretty(&crate::sdd_mcp_capabilities_status_value(
                        &self.root,
                    )?)?,
                ))
            }
            [kind, agent_id] if kind == "agents" => Ok(mcp_resource(
                uri,
                "application/json",
                serde_json::to_string_pretty(&crate::sdd_mcp_agents_manifest_value(agent_id)?)?,
            )),
            _ => bail!("unsupported SDD resource URI `{uri}`"),
        }
    }

    fn get_prompt(&self, name: &str, arguments: &Value) -> Result<Value> {
        let text = match name {
            "sdd_orchestration" => {
                let idea = required_string_arg(arguments, "idea")?;
                format!(
                    "Conduza a orquestração SDD para a ideia abaixo. Use `sdd orchestration`, preserve checkpoints humanos, salve artefatos em docs/<slug>/ e consulte `sdd_trace_summary` quando precisar de observabilidade.\n\nIdeia:\n{idea}"
                )
            }
            "sdd_stage_handoff" => {
                let orchestration = required_string_arg(arguments, "orchestration")?;
                let stage = required_string_arg(arguments, "stage")?;
                format!(
                    "Prepare o handoff da etapa `{stage}` da orquestração `{orchestration}`. Inclua origem, estado, evidências, riscos, próximo artefato e comandos `sdd trace` úteis."
                )
            }
            "sdd_trace_review" => {
                let run_id = required_string_arg(arguments, "run_id")?;
                format!(
                    "Revise a árvore de trace `{run_id}`. Procure falhas, falta de evidência, eventos sem parent_run_id quando deveriam ser filhos e riscos de segredo/transcript bruto."
                )
            }
            other => bail!("unknown SDD prompt `{other}`"),
        };
        Ok(json!({
            "description": name,
            "messages": [{ "role": "user", "content": { "type": "text", "text": text } }]
        }))
    }
}

fn runtime_adapters_value() -> Result<Value> {
    Ok(json!({
        "adapters": runtime::mcp::runtime_adapters(),
        "rmcp_server_info": runtime::mcp::rmcp_server_info_json()?,
    }))
}

fn resource(uri: &str, name: &str, title: &str, description: &str, mime_type: &str) -> Value {
    json!({
        "uri": uri,
        "name": name,
        "title": title,
        "description": description,
        "mimeType": mime_type
    })
}

fn mcp_resource(uri: &str, mime_type: &str, text: String) -> runtime::mcp::McpResource {
    runtime::mcp::McpResource {
        uri: uri.to_string(),
        mime_type: mime_type.to_string(),
        text,
    }
}

fn string_arg(arguments: &Value, key: &str) -> Option<String> {
    arguments
        .get(key)
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_string)
}

fn required_string_arg(arguments: &Value, key: &str) -> Result<String> {
    string_arg(arguments, key).ok_or_else(|| anyhow!("missing required argument `{key}`"))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::runtime::mcp::McpBackend;
    use tempfile::tempdir;

    #[test]
    fn exposes_agent_manifest_tool_and_resource() {
        let root = tempdir().unwrap();
        let backend = SddMcpBackend {
            root: root.path().to_path_buf(),
        };

        let value = backend
            .call_tool("sdd_agents_manifest", &json!({}))
            .expect("agent manifest tool should be readable");
        assert_eq!(value["manifest"]["id"], "sdd-orchestrator");
        assert_eq!(
            value["canonical_source"],
            ".agents/agents/sdd-orchestrator/AGENT.md"
        );

        let resource = backend
            .read_resource("sdd://agents/sdd-orchestrator")
            .expect("agent manifest resource should be readable");
        assert_eq!(resource.mime_type, "application/json");
        assert!(resource.text.contains("sdd-orchestrator"));
    }
}