//! `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, }; use crate::http::context::AppContext; use crate::slog_warn; // ── Tool dispatch ───────────────────────────────────────────────── pub(super) async fn handle_tools_call( id: Option, params: &Value, ctx: &AppContext, ) -> JsonRpcResponse { 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), // Read-only peer mesh diagnostics (story 720) "mesh_status" => diagnostics::tool_mesh_status(&args), // 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") ); } }