use anyhow::{anyhow, bail, Result};
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::fs;
use std::path::PathBuf;
use walkdir::WalkDir;
use crate::{contract, domain, 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_context_bundle" => {
let orchestration = required_string_arg(arguments, "orchestration")?;
let stage = required_string_arg(arguments, "stage")?;
let task = string_arg(arguments, "task");
let query = string_arg(arguments, "query");
crate::sdd_mcp_context_bundle_value(
&self.root,
&orchestration,
&stage,
task.as_deref(),
query.as_deref(),
)
}
"sdd_clients_doctor" => Ok(crate::sdd_mcp_clients_doctor_value(&self.root)),
"sdd_project_status" => crate::sdd_mcp_project_status_value(&self.root),
"sdd_search" => {
let query = string_arg(arguments, "query").unwrap_or_default();
let limit = usize_arg(arguments, "limit").unwrap_or(10);
crate::sdd_mcp_search_value(&self.root, &query, limit)
}
"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_readiness_summary" => {
let orchestration = string_arg(arguments, "orchestration");
let workflow = string_arg(arguments, "workflow");
crate::sdd_readiness_summary_value(
&self.root,
orchestration.as_deref(),
workflow.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://context/{parent}/{}", stage.key),
&format!("{parent}/context/{}", stage.key),
"SDD context pack",
"Read a generated context pack for one stage",
"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",
));
resources.push(resource(
&format!("sdd://context-bundle/{parent}/{}", stage.key),
&format!("{parent}/context-bundle/{}", stage.key),
"SDD context bundle",
"Read the primary MCP context bundle combining artifacts, handoff, traces, capabilities, runtime adapters and CodeGraph guidance",
"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",
));
}
resources.push(resource(
"sdd://auto/status",
"auto/status",
"SDD autonomous engine status",
"Read demand queue and engine states",
"application/json",
));
for id in domain::workflow::list_run_report_ids(&self.root) {
resources.push(resource(
&format!("sdd://workflow/{id}/status"),
&format!("workflow/{id}/status"),
"SDD workflow run status",
"Read a persisted workflow run report",
"application/json",
));
}
if docs.exists() {
let mut seen_slugs = std::collections::BTreeSet::new();
for entry in WalkDir::new(&docs)
.max_depth(1)
.min_depth(1)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_dir())
{
if let Some(slug) = entry.file_name().to_str() {
if seen_slugs.insert(slug.to_string()) {
resources.push(resource(
&format!("sdd://quality/{slug}"),
&format!("quality/{slug}"),
"SDD quality report",
"Read quality evaluation for an orchestration",
"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 == "context-bundle" => Ok(mcp_resource(
uri,
"application/json",
serde_json::to_string_pretty(&crate::sdd_mcp_context_bundle_value(
&self.root,
orchestration,
stage,
None,
None,
)?)?,
)),
[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)?)?,
)),
[kind, resource_name] if kind == "auto" && resource_name == "status" => {
Ok(mcp_resource(
uri,
"application/json",
serde_json::to_string_pretty(&auto_status_value(&self.root)?)?,
))
}
[kind, id, resource_name] if kind == "workflow" && resource_name == "status" => {
let report = domain::workflow::load_run_report(&self.root, id)?
.ok_or_else(|| anyhow!("workflow run report not found: {id}"))?;
Ok(mcp_resource(
uri,
"application/json",
serde_json::to_string_pretty(&report)?,
))
}
[kind, slug] if kind == "quality" => {
let report = domain::evaluation::quality_report(&self.root, slug)?;
Ok(mcp_resource(
uri,
"application/json",
serde_json::to_string_pretty(&report)?,
))
}
_ => 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 auto_status_value(root: &std::path::Path) -> Result<Value> {
let demands = domain::orchestrator::queue::list(root)?;
let engine_states = domain::orchestrator::state::list_states(root)?;
let demands_json: Vec<Value> = demands
.iter()
.map(|d| {
json!({
"id": d.id,
"slug": d.slug,
"title": d.title,
"status": d.status,
})
})
.collect();
let states_json: Vec<Value> = engine_states
.iter()
.map(|s| {
json!({
"slug": s.slug,
"cursor": s.cursor,
"status": s.status.to_repr(),
"locked": s.lock.is_some(),
"approvals": s.approvals.len(),
"last_error": s.last_error,
"last_tick": s.last_tick,
})
})
.collect();
let mut by_status: BTreeMap<String, usize> = BTreeMap::new();
for state in &engine_states {
*by_status.entry(state.status.to_repr()).or_insert(0) += 1;
}
let overall_status = if engine_states
.iter()
.any(|s| s.status == domain::orchestrator::state::EngineStatus::Error)
{
"warn"
} else {
"pass"
};
Ok(json!({
"status": overall_status,
"demands": demands_json,
"engine_states": states_json,
"summary": {
"total_demands": demands.len(),
"total_states": engine_states.len(),
"by_status": by_status,
}
}))
}
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}`"))
}
fn usize_arg(arguments: &Value, key: &str) -> Option<usize> {
arguments
.get(key)
.and_then(|value| {
value
.as_u64()
.map(|number| number as usize)
.or_else(|| value.as_str().and_then(|text| text.parse::<usize>().ok()))
})
.filter(|value| *value > 0)
}
#[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"));
}
#[test]
fn exposes_project_status_and_search_tools() {
let root = tempdir().unwrap();
let docs = root.path().join("docs/demo");
fs::create_dir_all(&docs).unwrap();
fs::write(docs.join("02-prd.md"), "# PRD\n\nMCP clean e leve").unwrap();
fs::write(
root.path().join("AGENTS.md"),
"# AGENTS\n\nRoteamento de Contexto SDD clean",
)
.unwrap();
fs::write(
root.path().join("sdd.config.yaml"),
"project:\n name: demo\nsystems: {}\npaths: {}\ncommands: {}\nquality: {}\nrisk_policy: {}\nadapters: {}\nartifact_store: {}\n",
)
.unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let status = backend
.call_tool("sdd_project_status", &json!({}))
.expect("project status should be readable");
assert_eq!(status["derived_cache"], ".sdd/cache/sdd.sqlite");
assert_eq!(status["agent_contexts"]["policy"], "root_only");
let search = backend
.call_tool("sdd_search", &json!({"query": "clean", "limit": "5"}))
.expect("search should use cache or fallback");
assert_eq!(search["query"], "clean");
assert!(search["results"].as_array().unwrap().iter().any(|item| {
item["kind"].as_str() == Some("context") && item["path"].as_str() == Some("AGENTS.md")
}));
}
#[test]
fn exposes_readiness_summary_tool() {
let root = tempdir().unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let value = backend
.call_tool(
"sdd_readiness_summary",
&json!({"workflow": "agentic-sdd-loop"}),
)
.expect("readiness summary should aggregate local signals");
let verdict = value["verdict"].as_str().unwrap();
assert!(matches!(verdict, "ready" | "warn" | "blocked"));
assert_eq!(value["status"], value["verdict"]);
assert!(value["components"]["mcp_doctor"].is_object());
assert!(value["components"]["health"].is_object());
assert!(value["components"]["auto_status"].is_object());
assert!(value["components"]["workflow_status"].is_object());
assert!(value["components"]["cache_freshness"].is_object());
assert!(value["components"]["capabilities"].is_object());
assert!(value["components"]["quality_report"].is_object());
assert!(value["recommended_commands"]
.as_array()
.unwrap()
.iter()
.any(|item| item["command"].as_str().unwrap().contains("sdd mcp doctor")));
}
#[test]
fn exposes_primary_context_bundle_tool_and_resource() {
let root = tempdir().unwrap();
let docs = root.path().join("docs/demo");
fs::create_dir_all(&docs).unwrap();
fs::write(
docs.join("traceability-map.yaml"),
"name: Demo\nstages:\n prd:\n state: approved\n techspec:\n state: approved\n",
)
.unwrap();
fs::write(
docs.join("02-prd.md"),
"# PRD\n\nMCP como contexto principal.",
)
.unwrap();
fs::write(
docs.join("03-techspec.md"),
"# Tech Spec\n\n## Decisões\n\n- Contexto MCP: bundle primário read-only.\n",
)
.unwrap();
fs::write(
root.path().join("sdd.config.yaml"),
"project:\n name: demo\nsystems: {}\npaths: {}\ncommands: {}\nquality: {}\nrisk_policy: {}\nadapters: {}\nartifact_store: {}\n",
)
.unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let value = backend
.call_tool(
"sdd_context_bundle",
&json!({"orchestration": "Demo", "stage": "execution", "task": "T-01"}),
)
.expect("context bundle should be readable");
assert_eq!(value["mcp_policy"]["role"], "primary_read_surface");
assert_eq!(value["mcp_policy"]["read_only"], true);
assert!(value["context_pack"]["content"]
.as_str()
.unwrap()
.contains("Context Pack"));
assert_eq!(
value["codegraph_companion"]["recommended_mcp_calls"][1]["tool"],
"codegraph_context"
);
let resource = backend
.read_resource("sdd://context-bundle/demo/execution")
.expect("context bundle resource should be readable");
assert_eq!(resource.mime_type, "application/json");
assert!(resource.text.contains("primary_read_surface"));
let resources = backend
.list_resources()
.expect("resources list should work");
assert!(resources
.iter()
.any(|item| item["uri"].as_str() == Some("sdd://context-bundle/demo/prd")));
assert!(resources
.iter()
.any(|item| item["uri"].as_str() == Some("sdd://context/demo/prd")));
}
#[test]
fn context_bundle_degrades_trace_errors_to_warning_status() {
let root = tempdir().unwrap();
let docs = root.path().join("docs/demo");
fs::create_dir_all(&docs).unwrap();
fs::write(
docs.join("traceability-map.yaml"),
"name: Demo\nstages:\n prd:\n state: approved\n techspec:\n state: approved\n",
)
.unwrap();
fs::write(docs.join("02-prd.md"), "# PRD\n\nContexto MCP.").unwrap();
fs::write(docs.join("03-techspec.md"), "# Tech Spec\n\nPlano técnico.").unwrap();
fs::write(
root.path().join("sdd.config.yaml"),
"project:\n name: demo\nsystems: {}\npaths: {}\ncommands: {}\nquality: {}\nrisk_policy: {}\nadapters: {}\nartifact_store: {}\n",
)
.unwrap();
fs::create_dir_all(root.path().join(".sdd/events.jsonl")).unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let value = backend
.call_tool(
"sdd_context_bundle",
&json!({"orchestration": "Demo", "stage": "execution"}),
)
.expect("trace read errors should not fail the context bundle");
assert_eq!(value["status"], "warn");
assert_eq!(value["trace_summary"]["status"], "warn");
assert!(value["trace_summary"]["error"]
.as_str()
.unwrap()
.contains("events.jsonl"));
}
#[test]
fn exposes_auto_status_resource() {
let root = tempdir().unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let resource = backend
.read_resource("sdd://auto/status")
.expect("auto/status resource should be readable");
assert_eq!(resource.mime_type, "application/json");
let value: serde_json::Value = serde_json::from_str(&resource.text).expect("valid JSON");
assert_eq!(value["status"], "pass");
assert!(value["demands"].as_array().unwrap().is_empty());
assert!(value["engine_states"].as_array().unwrap().is_empty());
assert_eq!(value["summary"]["total_demands"], 0);
}
#[test]
fn exposes_auto_status_with_data() {
let root = tempdir().unwrap();
let queue_dir = root.path().join(".sdd/queue");
fs::create_dir_all(&queue_dir).unwrap();
fs::write(
queue_dir.join("DEM-1.json"),
r#"{"schema_version":1,"id":"DEM-1","type":"story","title":"Test","description":"d","source":"cli","priority":"normal","created_at":"2026-07-04T00:00:00Z","slug":"test","status":"queued"}"#,
).unwrap();
let state_dir = root.path().join(".sdd/state");
fs::create_dir_all(&state_dir).unwrap();
fs::write(
state_dir.join("test.json"),
r#"{"schema_version":1,"slug":"test","cursor":"prd","status":"awaiting_approval","approvals":[],"attempts":{},"tasks":{}}"#,
).unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let resource = backend
.read_resource("sdd://auto/status")
.expect("auto/status should work with data");
let value: serde_json::Value = serde_json::from_str(&resource.text).unwrap();
assert_eq!(value["demands"].as_array().unwrap().len(), 1);
assert_eq!(value["engine_states"].as_array().unwrap().len(), 1);
assert_eq!(value["engine_states"][0]["status"], "awaiting_approval");
assert_eq!(value["summary"]["by_status"]["awaiting_approval"], 1);
}
#[test]
fn exposes_workflow_status_resource() {
let root = tempdir().unwrap();
let wdir = root.path().join(".sdd/workflows");
fs::create_dir_all(&wdir).unwrap();
fs::write(
wdir.join("test-wf.json"),
r#"{"workflow_id":"test-wf","status":"completed","iterations":3,"events":[],"produced":[],"paused":[],"ready":[],"errors":[]}"#,
).unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let resource = backend
.read_resource("sdd://workflow/test-wf/status")
.expect("workflow status should be readable");
assert_eq!(resource.mime_type, "application/json");
let value: serde_json::Value = serde_json::from_str(&resource.text).unwrap();
assert_eq!(value["workflow_id"], "test-wf");
assert_eq!(value["status"], "completed");
assert_eq!(value["iterations"], 3);
}
#[test]
fn workflow_status_returns_error_for_missing() {
let root = tempdir().unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let err = backend
.read_resource("sdd://workflow/nope/status")
.unwrap_err();
assert!(err.to_string().contains("not found"));
}
#[test]
fn exposes_quality_resource() {
let root = tempdir().unwrap();
let docs = root.path().join("docs/demo");
fs::create_dir_all(&docs).unwrap();
fs::write(
docs.join("traceability-map.yaml"),
"name: Demo\nstages:\n idea:\n state: recorded\n",
)
.unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let resource = backend
.read_resource("sdd://quality/demo")
.expect("quality resource should be readable");
assert_eq!(resource.mime_type, "application/json");
let value: serde_json::Value = serde_json::from_str(&resource.text).unwrap();
assert!(value["status"].as_str().is_some());
assert!(value["evaluation"].is_object());
assert!(value["tools"].as_array().is_some());
}
#[test]
fn list_resources_includes_observable_autonomy() {
let root = tempdir().unwrap();
let docs = root.path().join("docs/demo");
fs::create_dir_all(&docs).unwrap();
fs::write(docs.join("02-prd.md"), "# PRD\n").unwrap();
let wdir = root.path().join(".sdd/workflows");
fs::create_dir_all(&wdir).unwrap();
fs::write(
wdir.join("test-wf.json"),
r#"{"workflow_id":"test-wf","status":"completed","iterations":0,"events":[],"produced":[],"paused":[],"ready":[],"errors":[]}"#,
).unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let resources = backend.list_resources().expect("list should work");
let uris: Vec<&str> = resources.iter().filter_map(|r| r["uri"].as_str()).collect();
assert!(uris.contains(&"sdd://auto/status"), "missing auto/status");
assert!(
uris.contains(&"sdd://workflow/test-wf/status"),
"missing workflow status"
);
assert!(uris.contains(&"sdd://quality/demo"), "missing quality/demo");
}
}