2026-04-26 21:05:07 +00:00
|
|
|
//! `tools/call` MCP method — dispatches a tool name to the appropriate `*_tools` module.
|
|
|
|
|
|
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
|
|
|
|
|
|
use super::JsonRpcResponse;
|
|
|
|
|
use super::{
|
|
|
|
|
agent_tools, diagnostics, git_tools, merge_tools, qa_tools, shell_tools, status_tools,
|
|
|
|
|
story_tools, wizard_tools,
|
|
|
|
|
};
|
2026-04-27 01:32:08 +00:00
|
|
|
use crate::http::context::AppContext;
|
2026-04-26 21:05:07 +00:00
|
|
|
use crate::slog_warn;
|
|
|
|
|
|
|
|
|
|
// ── Tool dispatch ─────────────────────────────────────────────────
|
|
|
|
|
|
2026-04-27 01:32:08 +00:00
|
|
|
pub(super) async fn handle_tools_call(
|
|
|
|
|
id: Option<Value>,
|
|
|
|
|
params: &Value,
|
|
|
|
|
ctx: &AppContext,
|
|
|
|
|
) -> JsonRpcResponse {
|
2026-04-26 21:05:07 +00:00
|
|
|
let tool_name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
|
|
|
|
let args = params.get("arguments").cloned().unwrap_or(json!({}));
|
|
|
|
|
|
|
|
|
|
let result = match tool_name {
|
|
|
|
|
// Workflow tools
|
|
|
|
|
"create_story" => story_tools::tool_create_story(&args, ctx),
|
|
|
|
|
"validate_stories" => story_tools::tool_validate_stories(ctx),
|
|
|
|
|
"list_upcoming" => story_tools::tool_list_upcoming(ctx),
|
|
|
|
|
"get_story_todos" => story_tools::tool_get_story_todos(&args, ctx),
|
|
|
|
|
"record_tests" => story_tools::tool_record_tests(&args, ctx),
|
|
|
|
|
"ensure_acceptance" => story_tools::tool_ensure_acceptance(&args, ctx),
|
|
|
|
|
// Agent tools (async)
|
|
|
|
|
"start_agent" => agent_tools::tool_start_agent(&args, ctx).await,
|
|
|
|
|
"stop_agent" => agent_tools::tool_stop_agent(&args, ctx).await,
|
|
|
|
|
"list_agents" => agent_tools::tool_list_agents(ctx),
|
|
|
|
|
"get_agent_config" => agent_tools::tool_get_agent_config(ctx),
|
|
|
|
|
"reload_agent_config" => agent_tools::tool_get_agent_config(ctx),
|
|
|
|
|
"get_agent_output" => agent_tools::tool_get_agent_output(&args, ctx).await,
|
|
|
|
|
"wait_for_agent" => agent_tools::tool_wait_for_agent(&args, ctx).await,
|
|
|
|
|
"get_agent_remaining_turns_and_budget" => {
|
|
|
|
|
agent_tools::tool_get_agent_remaining_turns_and_budget(&args, ctx)
|
|
|
|
|
}
|
|
|
|
|
// Worktree tools
|
|
|
|
|
"create_worktree" => agent_tools::tool_create_worktree(&args, ctx).await,
|
|
|
|
|
"list_worktrees" => agent_tools::tool_list_worktrees(ctx),
|
|
|
|
|
"remove_worktree" => agent_tools::tool_remove_worktree(&args, ctx).await,
|
|
|
|
|
// Editor tools
|
|
|
|
|
"get_editor_command" => agent_tools::tool_get_editor_command(&args, ctx),
|
|
|
|
|
// Lifecycle tools
|
|
|
|
|
"accept_story" => story_tools::tool_accept_story(&args, ctx),
|
|
|
|
|
// Story mutation tools (auto-commit to master)
|
|
|
|
|
"check_criterion" => story_tools::tool_check_criterion(&args, ctx),
|
|
|
|
|
"edit_criterion" => story_tools::tool_edit_criterion(&args, ctx),
|
|
|
|
|
"add_criterion" => story_tools::tool_add_criterion(&args, ctx),
|
|
|
|
|
"remove_criterion" => story_tools::tool_remove_criterion(&args, ctx),
|
|
|
|
|
"update_story" => story_tools::tool_update_story(&args, ctx),
|
|
|
|
|
// Spike lifecycle tools
|
|
|
|
|
"create_spike" => story_tools::tool_create_spike(&args, ctx),
|
|
|
|
|
// Bug lifecycle tools
|
|
|
|
|
"create_bug" => story_tools::tool_create_bug(&args, ctx),
|
|
|
|
|
"list_bugs" => story_tools::tool_list_bugs(ctx),
|
|
|
|
|
"close_bug" => story_tools::tool_close_bug(&args, ctx),
|
|
|
|
|
// Refactor lifecycle tools
|
|
|
|
|
"create_refactor" => story_tools::tool_create_refactor(&args, ctx),
|
|
|
|
|
"list_refactors" => story_tools::tool_list_refactors(ctx),
|
|
|
|
|
// Mergemaster tools
|
|
|
|
|
"merge_agent_work" => merge_tools::tool_merge_agent_work(&args, ctx).await,
|
|
|
|
|
"get_merge_status" => merge_tools::tool_get_merge_status(&args, ctx),
|
|
|
|
|
"move_story_to_merge" => merge_tools::tool_move_story_to_merge(&args, ctx).await,
|
|
|
|
|
"report_merge_failure" => merge_tools::tool_report_merge_failure(&args, ctx),
|
|
|
|
|
// QA tools
|
|
|
|
|
"request_qa" => qa_tools::tool_request_qa(&args, ctx).await,
|
|
|
|
|
"approve_qa" => qa_tools::tool_approve_qa(&args, ctx).await,
|
|
|
|
|
"reject_qa" => qa_tools::tool_reject_qa(&args, ctx).await,
|
|
|
|
|
"launch_qa_app" => qa_tools::tool_launch_qa_app(&args, ctx).await,
|
|
|
|
|
// Pipeline status
|
|
|
|
|
"get_pipeline_status" => story_tools::tool_get_pipeline_status(ctx),
|
|
|
|
|
// Diagnostics
|
|
|
|
|
"get_server_logs" => diagnostics::tool_get_server_logs(&args),
|
|
|
|
|
"get_version" => diagnostics::tool_get_version(ctx),
|
|
|
|
|
// Server lifecycle
|
|
|
|
|
"rebuild_and_restart" => diagnostics::tool_rebuild_and_restart(ctx).await,
|
|
|
|
|
// Permission bridge (Claude Code → frontend dialog)
|
|
|
|
|
"prompt_permission" => diagnostics::tool_prompt_permission(&args, ctx).await,
|
|
|
|
|
// Token usage
|
|
|
|
|
"get_token_usage" => diagnostics::tool_get_token_usage(&args, ctx),
|
|
|
|
|
// Delete story
|
|
|
|
|
"delete_story" => story_tools::tool_delete_story(&args, ctx).await,
|
|
|
|
|
// Purge story (CRDT tombstone — story 521)
|
|
|
|
|
"purge_story" => story_tools::tool_purge_story(&args, ctx),
|
|
|
|
|
// Debug CRDT dump (story 515)
|
|
|
|
|
"dump_crdt" => diagnostics::tool_dump_crdt(&args),
|
2026-04-27 18:13:45 +00:00
|
|
|
// Read-only peer mesh diagnostics (story 720)
|
|
|
|
|
"mesh_status" => diagnostics::tool_mesh_status(&args),
|
2026-04-26 21:05:07 +00:00
|
|
|
// Arbitrary pipeline movement
|
|
|
|
|
"move_story" => diagnostics::tool_move_story(&args, ctx),
|
|
|
|
|
// Unblock story
|
|
|
|
|
"unblock_story" => story_tools::tool_unblock_story(&args, ctx),
|
|
|
|
|
// Shell command execution
|
|
|
|
|
"run_command" => shell_tools::tool_run_command(&args, ctx).await,
|
|
|
|
|
"run_tests" => shell_tools::tool_run_tests(&args, ctx).await,
|
|
|
|
|
"get_test_result" => shell_tools::tool_get_test_result(&args, ctx).await,
|
|
|
|
|
"run_build" => shell_tools::tool_run_build(&args, ctx).await,
|
|
|
|
|
"run_lint" => shell_tools::tool_run_lint(&args, ctx).await,
|
|
|
|
|
// Git operations
|
|
|
|
|
"git_status" => git_tools::tool_git_status(&args, ctx).await,
|
|
|
|
|
"git_diff" => git_tools::tool_git_diff(&args, ctx).await,
|
|
|
|
|
"git_add" => git_tools::tool_git_add(&args, ctx).await,
|
|
|
|
|
"git_commit" => git_tools::tool_git_commit(&args, ctx).await,
|
|
|
|
|
"git_log" => git_tools::tool_git_log(&args, ctx).await,
|
|
|
|
|
// Story triage
|
|
|
|
|
"status" => status_tools::tool_status(&args, ctx).await,
|
|
|
|
|
// File line count
|
|
|
|
|
"loc_file" => diagnostics::tool_loc_file(&args, ctx),
|
|
|
|
|
// Setup wizard tools
|
|
|
|
|
"wizard_status" => wizard_tools::tool_wizard_status(ctx),
|
|
|
|
|
"wizard_generate" => wizard_tools::tool_wizard_generate(&args, ctx),
|
|
|
|
|
"wizard_confirm" => wizard_tools::tool_wizard_confirm(ctx),
|
|
|
|
|
"wizard_skip" => wizard_tools::tool_wizard_skip(ctx),
|
|
|
|
|
"wizard_retry" => wizard_tools::tool_wizard_retry(ctx),
|
|
|
|
|
_ => Err(format!("Unknown tool: {tool_name}")),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match result {
|
|
|
|
|
Ok(content) => JsonRpcResponse::success(
|
|
|
|
|
id,
|
|
|
|
|
json!({
|
|
|
|
|
"content": [{ "type": "text", "text": content }]
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
Err(msg) => {
|
|
|
|
|
slog_warn!("[mcp] Tool call failed: tool={tool_name} error={msg}");
|
|
|
|
|
JsonRpcResponse::success(
|
|
|
|
|
id,
|
|
|
|
|
json!({
|
|
|
|
|
"content": [{ "type": "text", "text": msg }],
|
|
|
|
|
"isError": true
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use crate::http::test_helpers::test_ctx;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn handle_tools_call_unknown_tool() {
|
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
|
let ctx = test_ctx(tmp.path());
|
|
|
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
|
|
|
let resp = rt.block_on(handle_tools_call(
|
|
|
|
|
Some(json!(1)),
|
|
|
|
|
&json!({"name": "bogus_tool", "arguments": {}}),
|
|
|
|
|
&ctx,
|
|
|
|
|
));
|
|
|
|
|
let result = resp.result.unwrap();
|
|
|
|
|
assert_eq!(result["isError"], true);
|
|
|
|
|
assert!(
|
|
|
|
|
result["content"][0]["text"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.contains("Unknown tool")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|