claude_code_acp/mcp/tools/
bash_output.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::terminal::TerminalId;
17
18const TERMINAL_API_PREFIX: &str = "term-";
20
21#[derive(Debug, Default)]
23pub struct BashOutputTool;
24
25#[derive(Debug, Deserialize)]
27struct BashOutputInput {
28 bash_id: String,
30}
31
32#[async_trait]
33impl Tool for BashOutputTool {
34 fn name(&self) -> &str {
35 "BashOutput"
36 }
37
38 fn description(&self) -> &str {
39 "Retrieves output from a running or completed background bash shell. \
40 Use this to check on the progress of commands started with run_in_background=true."
41 }
42
43 fn input_schema(&self) -> Value {
44 json!({
45 "type": "object",
46 "properties": {
47 "bash_id": {
48 "type": "string",
49 "description": "The ID of the background shell returned when the command was started"
50 }
51 },
52 "required": ["bash_id"]
53 })
54 }
55
56 async fn execute(&self, input: Value, context: &ToolContext) -> ToolResult {
57 let params: BashOutputInput = match serde_json::from_value(input) {
59 Ok(p) => p,
60 Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
61 };
62
63 if let Some(terminal_id) = params.bash_id.strip_prefix(TERMINAL_API_PREFIX) {
65 return self.get_terminal_output(terminal_id, context).await;
66 }
67
68 self.get_background_output(¶ms.bash_id, context).await
70 }
71}
72
73impl BashOutputTool {
74 async fn get_terminal_output(&self, terminal_id: &str, context: &ToolContext) -> ToolResult {
76 let Some(terminal_client) = context.terminal_client() else {
77 return ToolResult::error("Terminal API not available");
78 };
79
80 let tid = TerminalId::new(terminal_id.to_string());
81
82 match terminal_client.output(tid).await {
84 Ok(response) => {
85 let status = match &response.exit_status {
86 Some(exit_status) => {
87 if let Some(code) = exit_status.exit_code {
88 if code == 0 {
89 "completed (exit code 0)".to_string()
90 } else {
91 format!("completed (exit code {})", code)
92 }
93 } else if exit_status.signal.is_some() {
94 format!("killed (signal: {:?})", exit_status.signal)
95 } else {
96 "completed".to_string()
97 }
98 }
99 None => "running".to_string(),
100 };
101
102 let output = &response.output;
103 let response_text = if output.is_empty() {
104 format!("Status: {}\n\n(No output yet)", status)
105 } else {
106 format!("Status: {}\n\n{}", status, output)
107 };
108
109 ToolResult::success(response_text).with_metadata(json!({
110 "terminal_id": terminal_id,
111 "status": status,
112 "terminal_api": true
113 }))
114 }
115 Err(e) => ToolResult::error(format!("Failed to get terminal output: {}", e)),
116 }
117 }
118
119 async fn get_background_output(&self, bash_id: &str, context: &ToolContext) -> ToolResult {
121 let Some(manager) = context.background_processes() else {
123 return ToolResult::error("Background process manager not available");
124 };
125
126 let Some(terminal) = manager.get(bash_id) else {
128 return ToolResult::error(format!("Unknown shell ID: {}", bash_id));
129 };
130
131 let output = terminal.get_incremental_output().await;
133 let status = terminal.status_str();
134
135 let response = if output.is_empty() {
137 format!("Status: {}\n\n(No new output)", status)
138 } else {
139 format!("Status: {}\n\n{}", status, output)
140 };
141
142 ToolResult::success(response)
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn test_bash_output_tool_properties() {
152 let tool = BashOutputTool;
153 assert_eq!(tool.name(), "BashOutput");
154 assert!(tool.description().contains("background"));
155 }
156
157 #[test]
158 fn test_bash_output_input_schema() {
159 let tool = BashOutputTool;
160 let schema = tool.input_schema();
161
162 assert_eq!(schema["type"], "object");
163 assert!(schema["properties"]["bash_id"].is_object());
164 assert!(
165 schema["required"]
166 .as_array()
167 .unwrap()
168 .contains(&json!("bash_id"))
169 );
170 }
171}