Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3344,7 +3344,8 @@ mod tests {
"arguments": {
"provider": "cursor",
"storage_scope": "hermes_profile",
"hermes_home": hermes_home
"hermes_home": hermes_home,
"format": "json"
}
}
}))
Expand Down
37 changes: 35 additions & 2 deletions src/mcp/tools/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,9 +439,42 @@ const FORMAT_CAPABLE_TOOL_NAMES: &[&str] = &[
"tracedecay_redundancy",
// memory
"tracedecay_memory_status",
"tracedecay_fact_store",
"tracedecay_fact_feedback",
// workflow
"tracedecay_diagnose",
"tracedecay_run_affected_tests",
// session / LCM
"tracedecay_message_search",
"tracedecay_lcm_status",
"tracedecay_lcm_doctor",
"tracedecay_lcm_load_session",
"tracedecay_lcm_grep",
"tracedecay_lcm_describe",
"tracedecay_lcm_expand",
"tracedecay_lcm_expand_query",
"tracedecay_lcm_session_boundary",
"tracedecay_lcm_preflight",
"tracedecay_lcm_compress",
// skills
"tracedecay_skill_list",
"tracedecay_skill_view",
"tracedecay_automation_run_artifact_view",
"tracedecay_hermes_skill_bridge",
// edit
"tracedecay_str_replace",
"tracedecay_multi_str_replace",
"tracedecay_insert_at",
"tracedecay_insert_at_symbol",
"tracedecay_replace_symbol",
"tracedecay_ast_grep_rewrite",
// git & info
"tracedecay_branch_list",
"tracedecay_active_project",
"tracedecay_storage_status",
// misc
"tracedecay_dashboard",
"tracedecay_retrieve",
];

pub fn format_capable_tool_names() -> &'static [&'static str] {
Expand Down Expand Up @@ -1823,8 +1856,8 @@ fn def_dsm() -> ToolDefinition {
},
"format": {
"type": "string",
"enum": ["stats", "clusters", "matrix"],
"description": "Output format (default: stats)"
"enum": ["stats", "clusters", "matrix", "json"],
"description": "Data shape rendered as markdown: stats, clusters, or matrix (default: stats). Pass 'json' for compact machine-readable JSON of the default stats shape."
},
"max_files": {
"type": "number",
Expand Down
14 changes: 9 additions & 5 deletions src/mcp/tools/handlers/dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use serde_json::{json, Value};
use crate::errors::{Result, TraceDecayError};
use crate::tracedecay::TraceDecay;

use super::super::render::truncated_json_envelope_with_handle;
use super::super::render;
use super::super::ToolResult;

use crate::dashboard::{bind_dashboard, build_state, router, DEFAULT_PORT};
Expand Down Expand Up @@ -43,11 +43,13 @@ fn validate_mcp_dashboard_host(host: &str) -> Result<&str> {
})
}

fn dashboard_tool_result(cg: &TraceDecay, payload: &Value) -> ToolResult {
let formatted = serde_json::to_string(payload).unwrap_or_default();
fn dashboard_tool_result(cg: &TraceDecay, args: &Value, payload: &Value) -> ToolResult {
let text = render::finalize(Some(cg.project_root()), args, payload, || {
render::generic_md(payload)
});
ToolResult::new(
json!({
"content": [{ "type": "text", "text": truncated_json_envelope_with_handle(Some(cg.project_root()), &formatted) }]
"content": [{ "type": "text", "text": text }]
}),
vec![],
)
Expand All @@ -70,7 +72,7 @@ pub(super) async fn handle_dashboard(cg: &TraceDecay, args: Value) -> Result<Too
} else {
json!({ "status": "not_running" })
};
Ok(dashboard_tool_result(cg, &payload))
Ok(dashboard_tool_result(cg, &args, &payload))
}
"start" | "" => {
let host = args
Expand All @@ -93,6 +95,7 @@ pub(super) async fn handle_dashboard(cg: &TraceDecay, args: Value) -> Result<Too
// already running — idempotent return
return Ok(dashboard_tool_result(
cg,
&args,
&json!({
"status": "already_running",
"url": handle.url
Expand Down Expand Up @@ -127,6 +130,7 @@ pub(super) async fn handle_dashboard(cg: &TraceDecay, args: Value) -> Result<Too

Ok(dashboard_tool_result(
cg,
&args,
&json!({
"status": "started",
"url": url,
Expand Down
28 changes: 18 additions & 10 deletions src/mcp/tools/handlers/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use serde_json::{json, Value};
use crate::errors::{Result, TraceDecayError};
use crate::tracedecay::TraceDecay;

use super::super::render;
use super::super::ToolResult;

fn missing_required_param(name: &str) -> TraceDecayError {
Expand All @@ -28,11 +29,18 @@ fn required_array<'a>(args: &'a Value, name: &str) -> Result<&'a [Value]> {
.ok_or_else(|| missing_required_param(name))
}

fn text_tool_result<T: Serialize>(result: &T, touched_files: Vec<String>) -> ToolResult {
fn text_tool_result<T: Serialize>(
cg: &TraceDecay,
args: &Value,
result: &T,
touched_files: Vec<String>,
) -> ToolResult {
let value = serde_json::to_value(result).unwrap_or_default();
let text = render::finalize(Some(cg.project_root()), args, &value, || {
render::generic_md(&value)
});
ToolResult::new(
json!({
"content": [{ "type": "text", "text": serde_json::to_string(result).unwrap_or_default() }]
}),
json!({ "content": [{ "type": "text", "text": text }] }),
touched_files,
)
}
Expand All @@ -44,7 +52,7 @@ pub(super) async fn handle_str_replace(cg: &TraceDecay, args: Value) -> Result<T

let result = cg.str_replace(path, old_str, new_str).await?;
let touched_files = vec![result.file_path.clone()];
Ok(text_tool_result(&result, touched_files))
Ok(text_tool_result(cg, &args, &result, touched_files))
}

pub(super) async fn handle_multi_str_replace(cg: &TraceDecay, args: Value) -> Result<ToolResult> {
Expand Down Expand Up @@ -72,7 +80,7 @@ pub(super) async fn handle_multi_str_replace(cg: &TraceDecay, args: Value) -> Re

let result = cg.multi_str_replace(path, &parsed_replacements).await?;
let touched_files = vec![result.file_path.clone()];
Ok(text_tool_result(&result, touched_files))
Ok(text_tool_result(cg, &args, &result, touched_files))
}

pub(super) async fn handle_insert_at(cg: &TraceDecay, args: Value) -> Result<ToolResult> {
Expand All @@ -84,7 +92,7 @@ pub(super) async fn handle_insert_at(cg: &TraceDecay, args: Value) -> Result<Too

let result = cg.insert_at(path, anchor, content, before).await?;
let touched_files = vec![result.file_path.clone()];
Ok(text_tool_result(&result, touched_files))
Ok(text_tool_result(cg, &args, &result, touched_files))
}

pub(super) async fn handle_replace_symbol(cg: &TraceDecay, args: Value) -> Result<ToolResult> {
Expand All @@ -97,7 +105,7 @@ pub(super) async fn handle_replace_symbol(cg: &TraceDecay, args: Value) -> Resul
} else {
vec![]
};
Ok(text_tool_result(&result, touched_files))
Ok(text_tool_result(cg, &args, &result, touched_files))
}

pub(super) async fn handle_insert_at_symbol(cg: &TraceDecay, args: Value) -> Result<ToolResult> {
Expand All @@ -114,7 +122,7 @@ pub(super) async fn handle_insert_at_symbol(cg: &TraceDecay, args: Value) -> Res
} else {
vec![]
};
Ok(text_tool_result(&result, touched_files))
Ok(text_tool_result(cg, &args, &result, touched_files))
}

pub(super) async fn handle_ast_grep_rewrite(cg: &TraceDecay, args: Value) -> Result<ToolResult> {
Expand All @@ -128,5 +136,5 @@ pub(super) async fn handle_ast_grep_rewrite(cg: &TraceDecay, args: Value) -> Res
} else {
vec![]
};
Ok(text_tool_result(&result, touched_files))
Ok(text_tool_result(cg, &args, &result, touched_files))
}
28 changes: 14 additions & 14 deletions src/mcp/tools/handlers/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::collections::{HashMap, HashSet, VecDeque};

use serde_json::{json, Value};

use super::super::render::{self, truncated_json_envelope_with_handle};
use super::super::render;
use super::super::ToolResult;
use super::support::unique_file_paths;
use crate::errors::{Result, TraceDecayError};
Expand All @@ -18,22 +18,20 @@ struct GitFileChange {
status: &'static str,
}

fn project_response_text(cg: &TraceDecay, text: &str) -> String {
truncated_json_envelope_with_handle(Some(cg.project_root()), text)
}

fn git_error_result(cg: &TraceDecay, operation: &str, message: &str) -> ToolResult {
fn git_error_result(cg: &TraceDecay, args: &Value, operation: &str, message: &str) -> ToolResult {
let output = json!({
"error": {
"kind": "git",
"operation": operation,
"message": message,
}
});
let formatted = serde_json::to_string(&output).unwrap_or_default();
let text = render::finalize(Some(cg.project_root()), args, &output, || {
render::generic_md(&output)
});
ToolResult::new(
json!({
"content": [{ "type": "text", "text": project_response_text(cg, &formatted) }]
"content": [{ "type": "text", "text": text }]
}),
vec![],
)
Expand Down Expand Up @@ -598,7 +596,7 @@ pub(super) async fn handle_changelog(cg: &TraceDecay, args: Value) -> Result<Too
let changes = match git_diff_file_changes(cg.project_root(), from_ref, to_ref) {
Ok(files) => files,
Err(e) => {
return Ok(git_error_result(cg, "diff", &e));
return Ok(git_error_result(cg, &args, "diff", &e));
}
};
let changed_files: Vec<String> = changes.iter().map(|change| change.path.clone()).collect();
Expand Down Expand Up @@ -678,7 +676,7 @@ pub(super) async fn handle_commit_context(cg: &TraceDecay, args: Value) -> Resul
let changed_files = match git_changed_files(cg.project_root(), staged_only) {
Ok(files) => files,
Err(e) => {
return Ok(git_error_result(cg, "status", &e));
return Ok(git_error_result(cg, &args, "status", &e));
}
};

Expand Down Expand Up @@ -775,7 +773,7 @@ pub(super) async fn handle_pr_context(cg: &TraceDecay, args: Value) -> Result<To
let changes = match git_diff_file_changes(cg.project_root(), base, head) {
Ok(files) => files,
Err(e) => {
return Ok(git_error_result(cg, "diff", &e));
return Ok(git_error_result(cg, &args, "diff", &e));
}
};
let changed_files: Vec<String> = changes.iter().map(|change| change.path.clone()).collect();
Expand Down Expand Up @@ -901,7 +899,7 @@ pub(super) async fn handle_pr_context(cg: &TraceDecay, args: Value) -> Result<To
// ── Cross-branch tools ─────────────────────────────────────────────────

/// Handles `tracedecay_branch_list` tool calls.
pub(super) fn handle_branch_list(cg: &TraceDecay) -> ToolResult {
pub(super) fn handle_branch_list(cg: &TraceDecay, args: &Value) -> ToolResult {
let diagnostics = cg.branch_diagnostics();
let mut result = serde_json::to_value(&diagnostics).unwrap_or(json!({}));
if let Some(object) = result.as_object_mut() {
Expand All @@ -911,10 +909,12 @@ pub(super) fn handle_branch_list(cg: &TraceDecay) -> ToolResult {
);
}

let output = serde_json::to_string(&result).unwrap_or_default();
let text = render::finalize(Some(cg.project_root()), args, &result, || {
render::generic_md(&result)
});
ToolResult::new(
json!({
"content": [{ "type": "text", "text": project_response_text(cg, &output) }]
"content": [{ "type": "text", "text": text }]
}),
vec![],
)
Expand Down
14 changes: 8 additions & 6 deletions src/mcp/tools/handlers/health.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use crate::graph::queries::GraphQueryManager;
use crate::tracedecay::TraceDecay;
use crate::types::{EdgeKind, NodeKind};

use super::super::render::{self, truncated_json_envelope_with_handle};
use super::super::render;
use super::super::ToolResult;
use super::support::{effective_path, unique_file_paths};

Expand Down Expand Up @@ -621,13 +621,15 @@ pub(super) async fn handle_dsm(
}
};

// `dsm` owns its `format` argument (stats/clusters/list/matrix) for data
// shaping, so it stays compact JSON rather than routing through the
// markdown/json `render::finalize` selector.
let formatted = serde_json::to_string(&output).unwrap_or_default();
// `dsm` overloads `format`: stats/clusters/matrix pick the data shape and
// render as markdown; "json" falls through to the default (stats) shape
// and `render::finalize` emits it as compact JSON.
let text = render::finalize(Some(cg.project_root()), &args, &output, || {
render::generic_md(&output)
});
Ok(ToolResult::new(
json!({
"content": [{ "type": "text", "text": truncated_json_envelope_with_handle(Some(cg.project_root()), &formatted) }]
"content": [{ "type": "text", "text": text }]
}),
vec![],
))
Expand Down
17 changes: 8 additions & 9 deletions src/mcp/tools/handlers/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,10 @@ use crate::storage::{ProjectPath, StorageMode, StoreKind};
use crate::tracedecay::{BranchDiagnostics, TraceDecay};
use crate::types::{NodeKind, Visibility};

use super::super::render::{self, truncate_response, truncated_json_envelope_with_handle, Md};
use super::super::render::{self, Md};
use super::super::ToolResult;
use super::support::{effective_path, filter_by_scope, require_node_id, unique_file_paths};

fn project_response_text(cg: &TraceDecay, text: &str) -> String {
truncated_json_envelope_with_handle(Some(cg.project_root()), text)
}

/// Handles `tracedecay_status` tool calls.
pub(super) async fn handle_status(
cg: &TraceDecay,
Expand Down Expand Up @@ -254,15 +250,18 @@ fn store_kind_name(kind: &StoreKind) -> &'static str {
/// Handles `tracedecay_active_project` tool calls.
pub(super) fn handle_active_project(
cg: &TraceDecay,
args: &Value,
server_stats: Option<Value>,
scope_prefix: Option<&str>,
) -> ToolResult {
let branch = cg.branch_diagnostics();
let output = active_project_context(cg, &branch, server_stats, scope_prefix);
let formatted = serde_json::to_string(&output).unwrap_or_default();
let text = render::finalize(Some(cg.project_root()), args, &output, || {
render::generic_md(&output)
});
ToolResult::new(
json!({
"content": [{ "type": "text", "text": project_response_text(cg, &formatted) }]
"content": [{ "type": "text", "text": text }]
}),
vec![],
)
Expand Down Expand Up @@ -633,7 +632,7 @@ pub(super) async fn handle_files(

Ok(ToolResult::new(
json!({
"content": [{ "type": "text", "text": truncate_response(&output) }]
"content": [{ "type": "text", "text": render::truncate_text_with_handle(Some(cg.project_root()), &output) }]
}),
touched_files,
))
Expand Down Expand Up @@ -1536,7 +1535,7 @@ pub(super) async fn handle_type_hierarchy(cg: &TraceDecay, args: Value) -> Resul

let touched_files = unique_file_paths(all_files.iter().map(std::string::String::as_str));
Ok(ToolResult::new(
json!({"content": [{"type": "text", "text": truncate_response(&output)}]}),
json!({"content": [{"type": "text", "text": render::truncate_text_with_handle(Some(cg.project_root()), &output)}]}),
touched_files,
))
}
Expand Down
Loading
Loading