claude_code_acp/mcp/tools/
kill_shell.rs1use async_trait::async_trait;
11use serde::Deserialize;
12use serde_json::{Value, json};
13
14use super::base::Tool;
15use crate::mcp::registry::{ToolContext, ToolResult};
16use crate::session::{BackgroundTerminal, TerminalExitStatus};
17use crate::terminal::TerminalId;
18
19const TERMINAL_API_PREFIX: &str = "term-";
21
22#[derive(Debug, Default)]
24pub struct KillShellTool;
25
26#[derive(Debug, Deserialize)]
28struct KillShellInput {
29 shell_id: String,
31}
32
33#[async_trait]
34impl Tool for KillShellTool {
35 fn name(&self) -> &str {
36 "KillShell"
37 }
38
39 fn description(&self) -> &str {
40 "Kills a running background bash shell. Use this to terminate long-running \
41 commands that were started with run_in_background=true."
42 }
43
44 fn input_schema(&self) -> Value {
45 json!({
46 "type": "object",
47 "properties": {
48 "shell_id": {
49 "type": "string",
50 "description": "The ID of the background shell to kill"
51 }
52 },
53 "required": ["shell_id"]
54 })
55 }
56
57 async fn execute(&self, input: Value, context: &ToolContext) -> ToolResult {
58 let params: KillShellInput = match serde_json::from_value(input) {
60 Ok(p) => p,
61 Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
62 };
63
64 if let Some(terminal_id) = params.shell_id.strip_prefix(TERMINAL_API_PREFIX) {
66 return Self::kill_terminal(terminal_id, context).await;
67 }
68
69 Self::kill_background_process(¶ms.shell_id, context).await
71 }
72}
73
74impl KillShellTool {
75 async fn kill_terminal(terminal_id: &str, context: &ToolContext) -> ToolResult {
77 let Some(terminal_client) = context.terminal_client() else {
78 return ToolResult::error("Terminal API not available");
79 };
80
81 let tid = TerminalId::new(terminal_id.to_string());
82
83 match terminal_client.kill(tid.clone()).await {
85 Ok(_) => {
86 let output = match terminal_client.output(tid.clone()).await {
88 Ok(resp) => resp.output,
89 Err(_) => String::new(),
90 };
91
92 drop(terminal_client.release(tid).await);
94
95 ToolResult::success(format!(
96 "Terminal command killed successfully.\n\nFinal output:\n{}",
97 if output.is_empty() {
98 "(No output)".to_string()
99 } else {
100 output
101 }
102 ))
103 .with_metadata(json!({
104 "terminal_id": terminal_id,
105 "terminal_api": true
106 }))
107 }
108 Err(e) => ToolResult::error(format!("Failed to kill terminal: {}", e)),
109 }
110 }
111
112 async fn kill_background_process(shell_id: &str, context: &ToolContext) -> ToolResult {
114 let Some(manager) = context.background_processes() else {
116 return ToolResult::error("Background process manager not available");
117 };
118
119 let Some(terminal) = manager.get(shell_id) else {
123 return ToolResult::error(format!("Unknown shell ID: {}", shell_id));
124 };
125
126 match &*terminal {
128 BackgroundTerminal::Running {
129 child,
130 output_buffer,
131 ..
132 } => {
133 let mut child_handle = child.clone();
135 let output_buffer_clone = output_buffer.clone();
136 drop(terminal); let final_output = {
140 let buffer_guard = output_buffer_clone.lock().await;
141 buffer_guard.clone()
142 }; match child_handle.kill().await {
146 Ok(()) => {
147 manager
149 .finish_terminal(shell_id, TerminalExitStatus::Killed)
150 .await;
151
152 ToolResult::success(format!(
153 "Command killed successfully.\n\nFinal output:\n{}",
154 if final_output.is_empty() {
155 "(No output)".to_string()
156 } else {
157 final_output
158 }
159 ))
160 }
161 Err(e) => ToolResult::error(format!("Failed to kill process: {}", e)),
162 }
163 }
164 BackgroundTerminal::Finished {
165 status,
166 final_output,
167 } => {
168 let message = match status {
169 TerminalExitStatus::Exited(code) => {
170 format!("Command had already exited with code {}.", code)
171 }
172 TerminalExitStatus::Killed => "Command was already killed.".to_string(),
173 TerminalExitStatus::TimedOut => "Command was killed by timeout.".to_string(),
174 TerminalExitStatus::Aborted => "Command was aborted by user.".to_string(),
175 };
176
177 ToolResult::success(format!(
178 "{}\n\nFinal output:\n{}",
179 message,
180 if final_output.is_empty() {
181 "(No output)".to_string()
182 } else {
183 final_output.clone()
184 }
185 ))
186 }
187 }
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_kill_shell_tool_properties() {
197 let tool = KillShellTool;
198 assert_eq!(tool.name(), "KillShell");
199 assert!(tool.description().contains("Kill"));
200 }
201
202 #[test]
203 fn test_kill_shell_input_schema() {
204 let tool = KillShellTool;
205 let schema = tool.input_schema();
206
207 assert_eq!(schema["type"], "object");
208 assert!(schema["properties"]["shell_id"].is_object());
209 assert!(
210 schema["required"]
211 .as_array()
212 .unwrap()
213 .contains(&json!("shell_id"))
214 );
215 }
216}