claude_code_acp/mcp/tools/
bash_output.rs

1//! BashOutput tool for retrieving output from background shell processes
2//!
3//! This tool retrieves incremental output from a running or completed background
4//! shell process started with `run_in_background=true`.
5//!
6//! Supports two execution modes:
7//! - Direct process execution: shell IDs starting with "shell-"
8//! - Terminal API: shell IDs starting with "term-" (Client-side PTY)
9
10use 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
18/// Prefix for Terminal API shell IDs
19const TERMINAL_API_PREFIX: &str = "term-";
20
21/// BashOutput tool implementation
22#[derive(Debug, Default)]
23pub struct BashOutputTool;
24
25/// Input parameters for BashOutput
26#[derive(Debug, Deserialize)]
27struct BashOutputInput {
28    /// The ID of the background shell to get output from
29    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        // Parse input
58        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        // Check if this is a Terminal API shell
64        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        // Fall back to background process manager
69        self.get_background_output(&params.bash_id, context).await
70    }
71}
72
73impl BashOutputTool {
74    /// Get output from Terminal API
75    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        // Get output from terminal
83        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    /// Get output from background process manager
120    async fn get_background_output(&self, bash_id: &str, context: &ToolContext) -> ToolResult {
121        // Get the background process manager from context
122        let Some(manager) = context.background_processes() else {
123            return ToolResult::error("Background process manager not available");
124        };
125
126        // Get the terminal
127        let Some(terminal) = manager.get(bash_id) else {
128            return ToolResult::error(format!("Unknown shell ID: {}", bash_id));
129        };
130
131        // Get incremental output
132        let output = terminal.get_incremental_output().await;
133        let status = terminal.status_str();
134
135        // Format response
136        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}