use anyhow::{anyhow, bail, Result};
use chrono::{DateTime, SecondsFormat, Utc};
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::fs;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
use walkdir::WalkDir;
use crate::{contract, domain, runtime};
pub struct SddMcpBackend {
pub root: PathBuf,
}
#[derive(Clone)]
struct CachedContextBundle {
fingerprint: String,
value: Value,
}
static CONTEXT_BUNDLE_SNAPSHOTS: OnceLock<Mutex<BTreeMap<String, CachedContextBundle>>> =
OnceLock::new();
fn context_bundle_snapshots() -> &'static Mutex<BTreeMap<String, CachedContextBundle>> {
CONTEXT_BUNDLE_SNAPSHOTS.get_or_init(|| Mutex::new(BTreeMap::new()))
}
impl runtime::mcp::McpBackend for SddMcpBackend {
fn call_tool(&self, name: &str, arguments: &Value) -> Result<Value> {
runtime::sdd_cache::with_refresh_scope(&self.root, || {
let 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);
}
if profile_arg(arguments)?.is_some()
|| string_arg(arguments, "cursor").is_some()
|| usize_arg(arguments, "limit").is_some()
{
let limit = usize_arg(arguments, "limit").unwrap_or(50).clamp(1, 50);
let offset = cursor_offset(arguments)?;
let has_more = events.len() > offset.saturating_add(limit);
let events = events
.into_iter()
.skip(offset)
.take(limit)
.collect::<Vec<_>>();
Ok(json!({
"events": events,
"next_cursor": has_more.then(|| (offset + limit).to_string()),
}))
} else {
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_artifact_history" => artifact_history_value(&self.root, arguments),
"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");
cached_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" => cached_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);
if profile_arg(arguments)?.is_some()
|| string_arg(arguments, "cursor").is_some()
{
runtime::sdd_cache::search_cached_or_direct_page(
&self.root,
&query,
limit,
string_arg(arguments, "cursor").as_deref(),
)
} else {
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");
cached_context_handoff_value(
&self.root,
&orchestration,
&stage,
task.as_deref(),
)
}
"sdd_readiness_summary" => {
let orchestration = string_arg(arguments, "orchestration");
let workflow = workflow_arg(arguments)?;
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}`"),
}?;
apply_response_profile(name, arguments, value)
})
}
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 mut stores =
crate::artifact_store::ArtifactLocator::new(&self.root).discover_paths()?;
let legacy_docs = self.root.join("docs");
if legacy_docs.is_dir() {
stores.extend(
fs::read_dir(legacy_docs)?
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| path.is_dir()),
);
stores.sort();
stores.dedup();
}
let mut listed_artifacts = std::collections::BTreeSet::new();
for store in &stores {
let Some(parent) = store.file_name().and_then(OsStr::to_str) else {
continue;
};
for entry in fs::read_dir(store)?.filter_map(|entry| entry.ok()) {
let path = entry.path();
if !path.is_file() {
continue;
}
let Some(stage) = path
.file_stem()
.and_then(OsStr::to_str)
.and_then(contract::stage_by_filename_stem)
else {
continue;
};
if !listed_artifacts.insert((parent.to_string(), stage.key.to_string())) {
continue;
}
let annotations = annotations_for_path(&path, 0.9);
resources.push(annotated_resource(
&format!("sdd://artifact/{parent}/{}", stage.key),
&format!("{parent}/{}", stage.filename),
&stage.label,
"Local SDD artifact",
"text/markdown",
annotations.clone(),
));
resources.push(annotated_resource(
&format!("sdd://artifact/{parent}/{}/history", stage.key),
&format!("{parent}/{}/history", stage.key),
"SDD artifact history",
"Read committed revisions for this artifact",
"application/json",
annotations.clone(),
));
resources.push(annotated_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",
annotations.clone(),
));
resources.push(annotated_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",
annotations.clone(),
));
resources.push(annotated_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",
annotations,
));
}
}
let trace_events = runtime::trace::read_events(&self.root)?;
for run_id in runtime::trace::summary(&trace_events).roots {
let last_modified = trace_events
.iter()
.filter(|event| event.run_id == run_id)
.filter_map(|event| result_timestamp(&json!({ "ts": event.ts })))
.max();
resources.push(annotated_resource(
&format!("sdd://trace/{run_id}"),
&run_id,
"SDD trace tree",
"Read an SDD trace tree as JSON",
"application/json",
resource_annotations(last_modified, 0.8),
));
}
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",
));
}
let mut seen_slugs = std::collections::BTreeSet::new();
for store in &stores {
if let Some(slug) = store.file_name().and_then(OsStr::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, suffix]
if kind == "artifact" && suffix == "history" =>
{
let stage = contract::stage_by_key(artifact)
.or_else(|| contract::stage_by_command(artifact))
.ok_or_else(|| anyhow!("unknown artifact `{artifact}`"))?;
let value = artifact_history_value(
&self.root,
&json!({ "orchestration": orchestration, "stage": stage.key }),
)?;
Ok(mcp_resource(
uri,
"application/json",
serde_json::to_string_pretty(&value)?,
))
}
[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 store = crate::artifact_store::ArtifactLocator::new(&self.root)
.locate(orchestration, orchestration)
.ok_or_else(|| anyhow!("artifact store `{orchestration}` not found"))?;
let path = store.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 } }]
}))
}
}
const MCP_SCHEMA_VERSION: &str = "2025-11-25";
const COMPACT_LIMIT_BYTES: usize = 16 * 1024;
const STANDARD_LIMIT_BYTES: usize = 64 * 1024;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ResponseProfile {
Compact,
Standard,
Full,
}
impl ResponseProfile {
fn parse(value: &str) -> Result<Self> {
match value {
"compact" => Ok(Self::Compact),
"standard" => Ok(Self::Standard),
"full" => Ok(Self::Full),
other => bail!("invalid profile `{other}`; expected compact|standard|full"),
}
}
fn as_str(self) -> &'static str {
match self {
Self::Compact => "compact",
Self::Standard => "standard",
Self::Full => "full",
}
}
fn byte_limit(self) -> Option<usize> {
match self {
Self::Compact => Some(COMPACT_LIMIT_BYTES),
Self::Standard => Some(STANDARD_LIMIT_BYTES),
Self::Full => None,
}
}
}
fn profile_arg(arguments: &Value) -> Result<Option<ResponseProfile>> {
arguments
.get("profile")
.and_then(Value::as_str)
.map(ResponseProfile::parse)
.transpose()
}
fn cursor_offset(arguments: &Value) -> Result<usize> {
string_arg(arguments, "cursor")
.map(|cursor| {
cursor
.parse::<usize>()
.map_err(|_| anyhow!("invalid cursor"))
})
.transpose()
.map(Option::unwrap_or_default)
}
fn compact_project_status_value(root: &std::path::Path) -> Result<Value> {
let cache = match runtime::sdd_cache::status(root) {
Ok(status) => json!({
"status": if status.exists && status.fresh { "pass" } else { "warn" },
"cache": status,
}),
Err(error) => json!({
"status": "warn",
"error": error.to_string(),
"cache": Value::Null,
}),
};
let clients = crate::sdd_mcp_clients_doctor_value(root);
let agent_contexts = crate::sdd_agent_context_status_value(root);
let codegraph_indexed = root.join(".codegraph").exists();
let codegraph = json!({
"status": if codegraph_indexed { "pass" } else { "warn" },
"indexed": codegraph_indexed,
"refresh": "not-requested",
});
let orchestrations = docs_orchestration_slugs(root);
let status = if clients["status"].as_str() == Some("fail") {
"fail"
} else if cache["status"].as_str() == Some("warn")
|| agent_contexts["status"].as_str() == Some("warn")
|| codegraph["status"].as_str() == Some("warn")
{
"warn"
} else {
"pass"
};
Ok(json!({
"status": status,
"root": root,
"cache": cache,
"clients": clients,
"agent_contexts": agent_contexts,
"codegraph": codegraph,
"orchestrations": orchestrations,
"canonical_sources": ["docs/<slug>/", "docs/<slug>/traceability-map.yaml", ".sdd/*.jsonl"],
"derived_cache": ".sdd/cache/sdd.sqlite",
"refresh": "not-requested",
}))
}
fn docs_orchestration_slugs(root: &std::path::Path) -> Vec<String> {
let docs = root.join("docs");
if !docs.exists() {
return Vec::new();
}
let mut slugs = WalkDir::new(docs)
.min_depth(1)
.max_depth(1)
.into_iter()
.filter_map(Result::ok)
.filter(|entry| entry.file_type().is_dir())
.filter_map(|entry| entry.file_name().to_str().map(str::to_string))
.collect::<Vec<_>>();
slugs.sort();
slugs
}
fn cached_context_bundle_value(
root: &std::path::Path,
orchestration: &str,
stage: &str,
task: Option<&str>,
query: Option<&str>,
) -> Result<Value> {
let fingerprint = runtime::sdd_cache::source_fingerprint(root)?;
let key = format!(
"bundle\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}",
root.display(),
orchestration,
stage,
task.unwrap_or_default(),
query.unwrap_or_default()
);
if let Some(snapshot) = context_bundle_snapshots()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.get(&key)
.filter(|snapshot| snapshot.fingerprint == fingerprint)
.cloned()
{
let mut value = snapshot.value;
value["source"] = json!("cache");
return Ok(value);
}
let mut value = crate::sdd_mcp_context_bundle_value(root, orchestration, stage, task, query)?;
value["source"] = json!("direct");
context_bundle_snapshots()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.insert(
key,
CachedContextBundle {
fingerprint,
value: value.clone(),
},
);
Ok(value)
}
fn cached_context_handoff_value(
root: &std::path::Path,
orchestration: &str,
stage: &str,
task: Option<&str>,
) -> Result<Value> {
let fingerprint = runtime::sdd_cache::source_fingerprint(root)?;
let key = format!(
"handoff\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}",
root.display(),
orchestration,
stage,
task.unwrap_or_default()
);
if let Some(snapshot) = context_bundle_snapshots()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.get(&key)
.filter(|snapshot| snapshot.fingerprint == fingerprint)
.cloned()
{
let mut value = snapshot.value;
value["source"] = json!("cache");
return Ok(value);
}
let mut value = crate::sdd_mcp_context_handoff_value(root, orchestration, stage, task)?;
value["source"] = json!("direct");
context_bundle_snapshots()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.insert(
key,
CachedContextBundle {
fingerprint,
value: value.clone(),
},
);
Ok(value)
}
fn cached_project_status_value(root: &std::path::Path) -> Result<Value> {
let fingerprint = runtime::sdd_cache::source_fingerprint(root)?;
let key = format!("status\u{1f}{}", root.display());
if let Some(snapshot) = context_bundle_snapshots()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.get(&key)
.filter(|snapshot| snapshot.fingerprint == fingerprint)
.cloned()
{
let mut value = snapshot.value;
value["source"] = json!("cache");
return Ok(value);
}
let mut value = compact_project_status_value(root)?;
value["source"] = json!("direct");
context_bundle_snapshots()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.insert(
key,
CachedContextBundle {
fingerprint,
value: value.clone(),
},
);
Ok(value)
}
fn apply_response_profile(name: &str, arguments: &Value, value: Value) -> Result<Value> {
let Some(profile) = profile_arg(arguments)? else {
return Ok(value);
};
let resources = response_resource_links(name, arguments, &value);
let next_cursor = value.get("next_cursor").cloned().unwrap_or(Value::Null);
let mut data = value;
if let Some(object) = data.as_object_mut() {
object.remove("next_cursor");
}
if name == "sdd_readiness_summary" {
data = compact_readiness_data(&data);
} else if name == "sdd_context_bundle" {
data = context_bundle_profile_data(&data, profile);
}
let mut envelope = json!({
"schema_version": MCP_SCHEMA_VERSION,
"profile": profile.as_str(),
"truncated": !next_cursor.is_null() || profile != ResponseProfile::Full,
"next_cursor": next_cursor,
"data": data,
"resources": resources,
});
enforce_profile_budget(&mut envelope, profile);
Ok(envelope)
}
fn context_bundle_profile_data(value: &Value, profile: ResponseProfile) -> Value {
match profile {
ResponseProfile::Full => value.clone(),
ResponseProfile::Compact => {
let context_pack = &value["context_pack"];
let handoff = &context_pack["handoff"];
let artifacts = value["artifacts"]["artifacts"]
.as_object()
.map(|items| {
items
.iter()
.map(|(stage, artifact)| {
json!({
"stage": stage,
"exists": artifact.get("exists").cloned().unwrap_or(Value::Bool(false)),
"state": artifact.get("state").cloned().unwrap_or(Value::Null),
"revision": artifact.get("revision").cloned().unwrap_or(Value::Null),
"validation_status": artifact.get("validation_status").cloned().unwrap_or(Value::Null),
})
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
json!({
"source": value.get("source").cloned().unwrap_or_else(|| json!("direct")),
"status": value.get("status").cloned().unwrap_or(Value::Null),
"orchestration": value.get("orchestration").cloned().unwrap_or(Value::Null),
"orchestration_slug": value.get("orchestration_slug").cloned().unwrap_or(Value::Null),
"stage": value.get("stage").cloned().unwrap_or(Value::Null),
"task": value.get("task").cloned().unwrap_or(Value::Null),
"artifacts": artifacts,
"context_pack": {
"sources_included": context_pack.get("sources_included").cloned().unwrap_or(Value::Null),
"conflicts": context_pack.get("conflicts").cloned().unwrap_or(Value::Null),
"used_chars": context_pack.get("used_chars").cloned().unwrap_or(Value::Null),
"stale": take_array(context_pack.get("stale"), 5),
"decision_conflicts": take_array(context_pack.get("decision_conflicts"), 5),
"handoff": {
"strategy": handoff.get("strategy").cloned().unwrap_or(Value::Null),
"probable_files": take_array(handoff.get("probable_files"), 20),
"suggested_tests": take_array(handoff.get("suggested_tests"), 10),
"generated_surface_drift": take_array(handoff.get("generated_surface_drift"), 10),
},
},
"trace_summary": value.get("trace_summary").cloned().unwrap_or(Value::Null),
"search": compact_search(value.get("search"), 3),
"next_best_actions": take_array(value.get("next_best_actions"), 5),
"read_only": true,
})
}
ResponseProfile::Standard => {
let mut context_pack = value.get("context_pack").cloned().unwrap_or(Value::Null);
if let Some(object) = context_pack.as_object_mut() {
if let Some(content) = object.get("content").and_then(Value::as_str) {
let truncated = truncate_string(content, 32_000);
let was_truncated = truncated.chars().count() < content.chars().count();
object.insert("content".to_string(), Value::String(truncated));
object.insert("content_truncated".to_string(), Value::Bool(was_truncated));
}
}
json!({
"source": value.get("source").cloned().unwrap_or_else(|| json!("direct")),
"status": value.get("status").cloned().unwrap_or(Value::Null),
"generated_at": value.get("generated_at").cloned().unwrap_or(Value::Null),
"orchestration": value.get("orchestration").cloned().unwrap_or(Value::Null),
"orchestration_slug": value.get("orchestration_slug").cloned().unwrap_or(Value::Null),
"stage": value.get("stage").cloned().unwrap_or(Value::Null),
"task": value.get("task").cloned().unwrap_or(Value::Null),
"artifacts": value.get("artifacts").cloned().unwrap_or(Value::Null),
"context_pack": context_pack,
"trace_summary": value.get("trace_summary").cloned().unwrap_or(Value::Null),
"search": compact_search(value.get("search"), 10),
"optimization": {
"codegraph": value["optimization"].get("codegraph").cloned().unwrap_or(Value::Null),
"ignored_paths": value["optimization"].get("ignored_paths").cloned().unwrap_or(Value::Null),
},
"runtime_adapters": value.get("runtime_adapters").cloned().unwrap_or(Value::Null),
"next_best_actions": take_array(value.get("next_best_actions"), 10),
"read_only": true,
})
}
}
}
fn compact_search(value: Option<&Value>, limit: usize) -> Value {
let Some(value) = value else {
return Value::Null;
};
let mut compact = value.clone();
if let Some(object) = compact.as_object_mut() {
if let Some(results) = object.get("results").and_then(Value::as_array) {
object.insert(
"results".to_string(),
Value::Array(results.iter().take(limit).cloned().collect()),
);
}
}
compact
}
fn take_array(value: Option<&Value>, limit: usize) -> Value {
Value::Array(
value
.and_then(Value::as_array)
.map(|items| items.iter().take(limit).cloned().collect())
.unwrap_or_default(),
)
}
fn truncate_string(value: &str, max_chars: usize) -> String {
if value.chars().count() <= max_chars {
value.to_string()
} else {
value.chars().take(max_chars).collect()
}
}
fn enforce_profile_budget(envelope: &mut Value, profile: ResponseProfile) {
let Some(limit) = profile.byte_limit() else {
return;
};
if serialized_len(envelope) <= limit {
return;
}
let summary = summarize_value(&envelope["data"]);
envelope["data"] = summary;
envelope["truncated"] = json!(true);
if serialized_len(envelope) > limit {
envelope["resources"] = json!(envelope["resources"]
.as_array()
.map(|resources| resources.iter().take(16).cloned().collect::<Vec<_>>())
.unwrap_or_default());
}
}
fn serialized_len(value: &Value) -> usize {
serde_json::to_vec(value)
.map(|bytes| bytes.len())
.unwrap_or(usize::MAX)
}
fn summarize_value(value: &Value) -> Value {
match value {
Value::Object(object) => {
let fields = object.keys().cloned().collect::<Vec<_>>();
let status = object
.get("status")
.or_else(|| object.get("verdict"))
.cloned()
.unwrap_or(Value::Null);
json!({ "status": status, "fields": fields, "summary": "Payload omitted by profile byte budget; follow resource links for full data." })
}
Value::Array(items) => {
json!({ "item_count": items.len(), "summary": "Array omitted by profile byte budget." })
}
scalar => scalar.clone(),
}
}
fn compact_readiness_data(value: &Value) -> Value {
let components = value
.get("components")
.and_then(Value::as_object)
.map(|components| {
components
.iter()
.map(|(name, component)| {
json!({
"name": name,
"status": component
.get("readiness_status")
.or_else(|| component.get("status"))
.cloned()
.unwrap_or_else(|| json!("warn")),
})
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
json!({
"status": value.get("status").cloned().unwrap_or(Value::Null),
"verdict": value.get("verdict").cloned().unwrap_or(Value::Null),
"generated_at": value.get("generated_at").cloned().unwrap_or(Value::Null),
"orchestration": value.get("orchestration").cloned().unwrap_or(Value::Null),
"workflow": value.get("workflow").cloned().unwrap_or(Value::Null),
"components": components,
"recommended_commands": value.get("recommended_commands").cloned().unwrap_or_else(|| json!([])),
})
}
fn response_resource_links(name: &str, arguments: &Value, value: &Value) -> Vec<Value> {
let mut links = Vec::new();
match name {
"sdd_artifact_history" => {
if let (Some(orchestration), Some(stage)) = (
string_arg(arguments, "orchestration"),
string_arg(arguments, "stage"),
) {
let slug = crate::artifact_slug(&orchestration);
links.push(resource_link_with_metadata(
&format!("sdd://artifact/{slug}/{stage}/history"),
&format!("{slug}/{stage}/history"),
"SDD artifact history",
"application/json",
None,
0.8,
));
}
}
"sdd_trace_list" => {
if let Some(events) = value.get("events").and_then(Value::as_array) {
for event in events {
if let Some(run_id) = event.get("run_id").and_then(Value::as_str) {
links.push(resource_link_with_metadata(
&format!("sdd://trace/{run_id}"),
run_id,
"SDD trace tree",
"application/json",
result_timestamp(event),
0.8,
));
}
}
}
}
"sdd_search" => {
if let Some(results) = value.get("results").and_then(Value::as_array) {
for result in results {
if let Some(run_id) = result.get("run_id").and_then(Value::as_str) {
links.push(resource_link_with_metadata(
&format!("sdd://trace/{run_id}"),
run_id,
"SDD trace tree",
"application/json",
result_timestamp(result),
0.8,
));
} else if let (Some(slug), Some(stage)) = (
result.get("slug").and_then(Value::as_str),
result.get("stage").and_then(Value::as_str),
) {
links.push(resource_link_with_metadata(
&format!("sdd://artifact/{slug}/{stage}"),
&format!("{slug}/{stage}"),
"SDD artifact",
"text/markdown",
result_timestamp(result),
0.9,
));
}
}
}
}
"sdd_readiness_summary" => {
links.push(resource_link(
"sdd://auto/status",
"auto/status",
"SDD autonomous engine status",
));
links.push(resource_link(
"sdd://capabilities/catalog",
"capabilities/catalog",
"SDD capability catalog",
));
links.push(resource_link(
"sdd://runtime/adapters",
"runtime/adapters",
"SDD runtime adapters",
));
if let Some(workflow) = string_arg(arguments, "workflow") {
links.push(resource_link(
&format!("sdd://workflow/{workflow}/status"),
&format!("workflow/{workflow}/status"),
"SDD workflow status",
));
}
if let Some(orchestration) = string_arg(arguments, "orchestration") {
let orchestration = crate::artifact_slug(&orchestration);
links.push(resource_link(
&format!("sdd://quality/{orchestration}"),
&format!("quality/{orchestration}"),
"SDD quality report",
));
}
}
"sdd_context_bundle" | "sdd_context_build" | "sdd_context_handoff" => {
if let (Some(orchestration), Some(stage)) = (
string_arg(arguments, "orchestration"),
string_arg(arguments, "stage"),
) {
let orchestration = crate::artifact_slug(&orchestration);
links.push(resource_link(
&format!("sdd://context-bundle/{orchestration}/{stage}"),
&format!("{orchestration}/{stage}"),
"SDD context bundle",
));
}
}
_ => {}
}
links.truncate(32);
links
}
fn resource_link(uri: &str, name: &str, title: &str) -> Value {
resource_link_with_metadata(uri, name, title, "application/json", None, 0.7)
}
fn resource_link_with_metadata(
uri: &str,
name: &str,
title: &str,
mime_type: &str,
last_modified: Option<String>,
priority: f64,
) -> Value {
json!({
"type": "resource_link",
"uri": uri,
"name": name,
"title": title,
"mimeType": mime_type,
"annotations": resource_annotations(last_modified, priority),
})
}
fn resource_annotations(last_modified: Option<String>, priority: f64) -> Value {
let mut annotations = json!({
"audience": ["assistant"],
"priority": priority,
});
if let Some(last_modified) = last_modified {
annotations["lastModified"] = json!(last_modified);
}
annotations
}
fn annotations_for_path(path: &std::path::Path, priority: f64) -> Value {
let last_modified = fs::metadata(path)
.and_then(|metadata| metadata.modified())
.ok()
.map(DateTime::<Utc>::from)
.map(|timestamp| timestamp.to_rfc3339_opts(SecondsFormat::Millis, true));
resource_annotations(last_modified, priority)
}
fn result_timestamp(value: &Value) -> Option<String> {
if let Some(timestamp) = value.get("ts").and_then(Value::as_str) {
return chrono::DateTime::parse_from_rfc3339(timestamp)
.ok()
.map(|timestamp| timestamp.to_rfc3339_opts(SecondsFormat::Millis, true));
}
value
.get("modified_ms")
.and_then(Value::as_i64)
.filter(|timestamp| *timestamp > 0)
.and_then(DateTime::<Utc>::from_timestamp_millis)
.map(|timestamp| timestamp.to_rfc3339_opts(SecondsFormat::Millis, true))
}
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 annotated_resource(
uri: &str,
name: &str,
title: &str,
description: &str,
mime_type: &str,
annotations: Value,
) -> Value {
let mut value = resource(uri, name, title, description, mime_type);
value["annotations"] = annotations;
value
}
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 artifact_history_value(root: &std::path::Path, arguments: &Value) -> Result<Value> {
let orchestration = required_string_arg(arguments, "orchestration")?;
let stage = string_arg(arguments, "stage")
.map(|stage| {
contract::stage_by_key(&stage)
.or_else(|| contract::stage_by_command(&stage))
.map(|definition| definition.key.clone())
.ok_or_else(|| anyhow!("unknown artifact `{stage}`"))
})
.transpose()?;
let slug = crate::artifact_slug(&orchestration);
let store = crate::artifact_store::ArtifactLocator::new(root)
.locate(&slug, &slug)
.ok_or_else(|| anyhow!("artifact store `{orchestration}` not found"))?;
let service = crate::artifact_store::ArtifactStoreService::new(root, &store, &orchestration);
let mut history = service
.history()?
.into_iter()
.map(serde_json::to_value)
.collect::<std::result::Result<Vec<_>, _>>()?;
if let Some(stage) = stage.as_deref() {
history.retain(|entry| entry.get("stage").and_then(Value::as_str) == Some(stage));
}
let limit = usize_arg(arguments, "limit").unwrap_or(50).clamp(1, 50);
let offset = cursor_offset(arguments)?;
if offset > history.len() {
bail!("cursor is out of range");
}
let has_more = history.len() > offset.saturating_add(limit);
let history = history
.into_iter()
.skip(offset)
.take(limit)
.collect::<Vec<_>>();
Ok(json!({
"orchestration": orchestration,
"stage": stage,
"history": history,
"next_cursor": has_more.then(|| (offset + limit).to_string()),
}))
}
fn workflow_arg(arguments: &Value) -> Result<Option<String>> {
let Some(workflow) = string_arg(arguments, "workflow") else {
return Ok(None);
};
let valid = workflow.len() <= 128
&& workflow
.chars()
.all(|character| character.is_ascii_alphanumeric() || matches!(character, '-' | '_'));
if !valid {
bail!("invalid workflow `{workflow}`; expected 1-128 ASCII letters, digits, `-` or `_`");
}
Ok(Some(workflow))
}
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!(!root.path().join(".sdd/cache/sdd.sqlite").exists());
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 profiled_search_is_versioned_bounded_and_paginated() {
let root = tempdir().unwrap();
for index in 0..4 {
let docs = root.path().join(format!("docs/demo-{index}"));
fs::create_dir_all(&docs).unwrap();
fs::write(
docs.join("02-prd.md"),
format!("# PRD {index}\n\n{}", "contexto MCP ".repeat(2_000)),
)
.unwrap();
}
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let first = backend
.call_tool(
"sdd_search",
&json!({"query": "MCP", "limit": 1, "profile": "compact"}),
)
.unwrap();
assert_eq!(first["schema_version"], "2025-11-25");
assert_eq!(first["profile"], "compact");
assert!(first["next_cursor"].is_string());
assert!(first["resources"].is_array());
assert!(serde_json::to_vec(&first).unwrap().len() <= 16 * 1024);
let second = backend
.call_tool(
"sdd_search",
&json!({
"query": "MCP",
"limit": 1,
"profile": "standard",
"cursor": first["next_cursor"]
}),
)
.unwrap();
assert_eq!(second["profile"], "standard");
assert!(serde_json::to_vec(&second).unwrap().len() <= 64 * 1024);
assert_ne!(
first["data"]["results"][0]["path"],
second["data"]["results"][0]["path"]
);
}
#[test]
fn profiled_trace_list_pages_and_emits_resource_links() {
let root = tempdir().unwrap();
let sdd = root.path().join(".sdd");
fs::create_dir_all(&sdd).unwrap();
fs::write(
sdd.join("events.jsonl"),
concat!(
"{\"run_id\":\"run-1\",\"kind\":\"stage\",\"status\":\"ok\",\"ts\":\"2026-07-18T00:00:00Z\"}\n",
"{\"run_id\":\"run-2\",\"kind\":\"stage\",\"status\":\"ok\",\"ts\":\"2026-07-18T00:00:01Z\"}\n"
),
)
.unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let value = backend
.call_tool("sdd_trace_list", &json!({"limit": 1, "profile": "compact"}))
.unwrap();
assert!(value["next_cursor"].is_string());
assert_eq!(value["resources"][0]["uri"], "sdd://trace/run-1");
assert_eq!(
value["resources"][0]["annotations"]["audience"][0],
"assistant"
);
assert!(value["resources"][0]["annotations"]["lastModified"].is_string());
}
#[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")));
let compact = backend
.call_tool(
"sdd_readiness_summary",
&json!({"workflow": "agentic-sdd-loop", "profile": "compact"}),
)
.unwrap();
assert_eq!(compact["profile"], "compact");
assert!(compact["data"]["components"].is_array());
assert!(compact["resources"]
.as_array()
.unwrap()
.iter()
.any(|item| item["uri"] == "sdd://auto/status"));
assert!(serde_json::to_vec(&compact).unwrap().len() <= 16 * 1024);
}
#[test]
fn readiness_rejects_workflow_path_traversal() {
let root = tempdir().unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let error = backend
.call_tool(
"sdd_readiness_summary",
&json!({"workflow": "../../../outside"}),
)
.unwrap_err();
assert!(error.to_string().contains("invalid workflow"));
}
#[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 persistent_backend_reuses_context_bundle_until_sources_change() {
let root = tempdir().unwrap();
let docs = root.path().join("docs/demo");
fs::create_dir_all(&docs).unwrap();
fs::write(
docs.join("traceability-map.yaml"),
"orchestration:\n name: Demo\n slug: demo\nartifacts:\n prd:\n file: 02-prd.md\n state: approved\nrelated_orchestrations: []\n",
)
.unwrap();
fs::write(docs.join("02-prd.md"), "# PRD\n\nContexto inicial.\n").unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let arguments =
json!({"orchestration": "Demo", "stage": "execution", "profile": "standard"});
let first = backend.call_tool("sdd_context_bundle", &arguments).unwrap();
let second = backend.call_tool("sdd_context_bundle", &arguments).unwrap();
fs::write(
docs.join("02-prd.md"),
"# PRD\n\nContexto alterado e maior.\n",
)
.unwrap();
let after_edit = backend.call_tool("sdd_context_bundle", &arguments).unwrap();
assert_eq!(first["data"]["source"], "direct");
assert_eq!(second["data"]["source"], "cache");
assert_eq!(after_edit["data"]["source"], "direct");
assert!(after_edit["data"]["context_pack"]["content"]
.as_str()
.unwrap()
.contains("Contexto alterado"));
let handoff_arguments =
json!({"orchestration": "Demo", "stage": "execution", "profile": "compact"});
let handoff_first = backend
.call_tool("sdd_context_handoff", &handoff_arguments)
.unwrap();
let handoff_second = backend
.call_tool("sdd_context_handoff", &handoff_arguments)
.unwrap();
assert_eq!(handoff_first["data"]["source"], "direct");
assert_eq!(handoff_second["data"]["source"], "cache");
let status_arguments = json!({"profile": "compact"});
let status_first = backend
.call_tool("sdd_project_status", &status_arguments)
.unwrap();
let status_second = backend
.call_tool("sdd_project_status", &status_arguments)
.unwrap();
assert_eq!(status_first["data"]["source"], "direct");
assert_eq!(status_second["data"]["source"], "cache");
}
#[test]
fn artifact_resources_use_the_worktree_aware_locator() {
let root = tempdir().unwrap();
let store = root.path().join(".worktree/feature/docs/only-in-worktree");
fs::create_dir_all(&store).unwrap();
fs::write(
store.join("traceability-map.yaml"),
"orchestration:\n slug: only-in-worktree\nartifacts:\n prd:\n file: 02-prd.md\n state: approved\n",
)
.unwrap();
fs::write(store.join("02-prd.md"), "# PRD\n\nWorktree canonical.\n").unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let resources = backend.list_resources().unwrap();
assert!(resources.iter().any(|resource| {
resource["uri"].as_str() == Some("sdd://artifact/only-in-worktree/prd")
}));
let artifact = resources
.iter()
.find(|resource| {
resource["uri"].as_str() == Some("sdd://artifact/only-in-worktree/prd")
})
.unwrap();
assert_eq!(artifact["annotations"]["audience"][0], "assistant");
assert!(artifact["annotations"]["priority"].is_number());
assert!(artifact["annotations"]["lastModified"].is_string());
for kind in ["context", "handoff", "context-bundle"] {
let uri = format!("sdd://{kind}/only-in-worktree/prd");
let derived = resources
.iter()
.find(|resource| resource["uri"].as_str() == Some(uri.as_str()))
.unwrap();
assert_eq!(derived["annotations"]["audience"][0], "assistant");
assert!(derived["annotations"]["lastModified"].is_string());
}
let resource = backend
.read_resource("sdd://artifact/only-in-worktree/prd")
.unwrap();
assert!(resource.text.contains("Worktree canonical"));
}
#[test]
fn artifact_history_is_paged_and_linked_as_a_read_only_resource() {
let root = tempdir().unwrap();
let store = root.path().join("docs/demo");
fs::create_dir_all(&store).unwrap();
fs::write(
store.join("traceability-map.yaml"),
"orchestration:\n name: Demo\n slug: demo\nartifacts:\n idea:\n file: 01-idea.md\n state: pending\nrelated_orchestrations: []\n",
)
.unwrap();
let service = crate::artifact_store::ArtifactStoreService::new(root.path(), &store, "Demo");
let first = service
.save(crate::artifact_store::SaveRequest {
stage: "idea",
filename: "01-idea.md",
content: b"# Ideia\n\nPrimeira.\n",
state: "recorded",
validation_status: "valid",
if_match: None,
})
.unwrap();
service
.save(crate::artifact_store::SaveRequest {
stage: "idea",
filename: "01-idea.md",
content: b"# Ideia\n\nSegunda.\n",
state: "recorded",
validation_status: "valid",
if_match: Some(&first.sha256),
})
.unwrap();
let backend = SddMcpBackend {
root: root.path().to_path_buf(),
};
let page = backend
.call_tool(
"sdd_artifact_history",
&json!({
"orchestration": "Demo",
"stage": "idea",
"limit": 1,
"profile": "compact"
}),
)
.unwrap();
assert_eq!(page["data"]["history"].as_array().unwrap().len(), 1);
assert_eq!(page["next_cursor"], "1");
assert!(page["resources"]
.as_array()
.unwrap()
.iter()
.any(|link| { link["uri"].as_str() == Some("sdd://artifact/demo/idea/history") }));
let resource = backend
.read_resource("sdd://artifact/demo/idea/history")
.unwrap();
let history: Value = serde_json::from_str(&resource.text).unwrap();
assert_eq!(history["history"].as_array().unwrap().len(), 2);
}
#[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");
}
#[test]
fn context_profiles_keep_actionable_core_instead_of_generic_field_lists() {
let value = json!({
"status": "pass",
"orchestration": "flow",
"orchestration_slug": "flow",
"stage": "execution",
"task": "T-01",
"artifacts": {"artifacts": {"idea": {"exists": true, "state": "approved"}}},
"context_pack": {
"sources_included": 3,
"conflicts": 0,
"used_chars": 12_000,
"handoff": {"probable_files": ["src/main.rs"]},
"content": "contexto".repeat(3_000),
},
"trace_summary": {"total": 4},
"search": {"results": [{"path": "src/main.rs"}]},
"next_best_actions": ["Rode os testes focados."],
"project": {"large": "x".repeat(70_000)},
"capabilities": {"large": "x".repeat(70_000)},
});
let compact = apply_response_profile(
"sdd_context_bundle",
&json!({"orchestration": "flow", "stage": "execution", "profile": "compact"}),
value.clone(),
)
.unwrap();
assert_eq!(compact["data"]["orchestration"], "flow");
assert_eq!(compact["data"]["context_pack"]["sources_included"], 3);
assert_eq!(
compact["data"]["next_best_actions"][0],
"Rode os testes focados."
);
assert!(compact["data"].get("fields").is_none(), "{compact}");
assert!(serialized_len(&compact) <= COMPACT_LIMIT_BYTES);
let standard = apply_response_profile(
"sdd_context_bundle",
&json!({"orchestration": "flow", "stage": "execution", "profile": "standard"}),
value,
)
.unwrap();
assert!(standard["data"]["context_pack"]["content"]
.as_str()
.is_some_and(|content| content.contains("contexto")));
assert_eq!(
standard["data"]["context_pack"]["handoff"]["probable_files"][0],
"src/main.rs"
);
assert!(serialized_len(&standard) <= STANDARD_LIMIT_BYTES);
}
#[test]
fn search_artifact_links_use_markdown_mime_and_annotations() {
let value = json!({
"results": [{
"slug": "flow",
"stage": "prd",
"modified_ms": 1_700_000_000_000_i64,
}]
});
let links = response_resource_links("sdd_search", &json!({}), &value);
assert_eq!(links[0]["mimeType"], "text/markdown");
assert_eq!(links[0]["annotations"]["audience"][0], "assistant");
assert_eq!(links[0]["annotations"]["priority"], 0.9);
assert!(links[0]["annotations"]["lastModified"].is_string());
}
}