agentmux_mcp/tools/
shell.rs1use super::*;
8
9pub(super) async fn call(name: &str, arguments: &Value, cx: &ToolCtx<'_>) -> Result<String> {
10 let ToolCtx { local_url, auth_key, block_id, client, .. } = *cx;
11 match name {
12 "Shell" => {
13 let cmd = arguments
14 .get("cmd")
15 .and_then(|v| v.as_str())
16 .ok_or_else(|| anyhow::anyhow!("missing required parameter: cmd"))?;
17
18 if local_url.is_empty() || auth_key.is_empty() {
19 anyhow::bail!(
20 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
21 Is this agent pane opened via AgentMux?"
22 );
23 }
24 if block_id.is_empty() {
25 anyhow::bail!(
26 "neither AGENTMUX_AGENT_BUS_ID nor AGENTMUX_BLOCKID is set \
27 — cannot associate the shell with a conversation pane. \
28 Is this agent pane opened via AgentMux?"
29 );
30 }
31
32 let title = arguments
33 .get("title")
34 .and_then(|v| v.as_str())
35 .unwrap_or(cmd);
36 let cwd = arguments.get("cwd").and_then(|v| v.as_str()).map(str::to_string);
37 let env = arguments
38 .get("env")
39 .and_then(|v| serde_json::from_value(v.clone()).ok());
40
41 let url = format!(
42 "{}/api/v1/shell/create",
43 local_url.trim_end_matches('/')
44 );
45 let capture_stdin = arguments.get("capture_stdin").and_then(|v| v.as_bool());
46 let connection = arguments.get("connection").and_then(|v| v.as_str()).map(str::to_string);
47 let req = ShellCreateRequest {
48 agent_block_id: block_id.to_string(),
49 cmd: cmd.to_string(),
50 title: Some(title.to_string()),
51 cwd,
52 env,
53 capture_stdin,
54 connection,
55 auth: sign_ui_automation_auth().ok(),
58 };
59
60 let resp = client
61 .post(&url)
62 .header(AUTH_KEY_HEADER, auth_key)
63 .timeout(crate::srv_http::SHELL_CREATE_TIMEOUT)
64 .json(&req)
65 .send()
66 .await
67 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
68
69 if !resp.status().is_success() {
70 let status = resp.status();
71 let text = resp.text().await.unwrap_or_default();
72 anyhow::bail!("shell/create failed: HTTP {status} — {text}");
73 }
74
75 let result: ShellCreateResponse = resp
76 .json()
77 .await
78 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
79
80 Ok(result.shell_id)
81 }
82 "ShellStop" => {
83 let shell_id = arguments
84 .get("shell_id")
85 .and_then(|v| v.as_str())
86 .ok_or_else(|| anyhow::anyhow!("missing required parameter: shell_id"))?;
87
88 if local_url.is_empty() || auth_key.is_empty() {
89 anyhow::bail!(
90 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
91 Is this agent pane opened via AgentMux?"
92 );
93 }
94
95 let url = format!("{}/api/v1/shell/stop", local_url.trim_end_matches('/'));
96 let resp = client
97 .post(&url)
98 .header(AUTH_KEY_HEADER, auth_key)
99 .json(&ShellStopRequest { shell_id: shell_id.to_string() })
100 .send()
101 .await
102 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
103
104 if !resp.status().is_success() {
105 let status = resp.status();
106 let text = resp.text().await.unwrap_or_default();
107 anyhow::bail!("shell/stop failed: HTTP {status} — {text}");
108 }
109
110 let result: ShellStopResponse = resp
111 .json()
112 .await
113 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
114 Ok(if result.stopped {
115 format!("stopped shell {shell_id}")
116 } else {
117 format!("shell {shell_id} was not running (unknown or already exited)")
118 })
119 }
120 "ShellInput" => {
121 let shell_id = arguments
122 .get("shell_id")
123 .and_then(|v| v.as_str())
124 .ok_or_else(|| anyhow::anyhow!("missing required parameter: shell_id"))?;
125 let text = arguments
126 .get("text")
127 .and_then(|v| v.as_str())
128 .ok_or_else(|| anyhow::anyhow!("missing required parameter: text"))?;
129
130 if local_url.is_empty() || auth_key.is_empty() {
131 anyhow::bail!(
132 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
133 Is this agent pane opened via AgentMux?"
134 );
135 }
136
137 let url = format!("{}/api/v1/shell/input", local_url.trim_end_matches('/'));
138 let resp = client
139 .post(&url)
140 .header(AUTH_KEY_HEADER, auth_key)
141 .json(&ShellInputRequest { shell_id: shell_id.to_string(), text: text.to_string() })
142 .send()
143 .await
144 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
145
146 if !resp.status().is_success() {
147 let status = resp.status();
148 let body = resp.text().await.unwrap_or_default();
149 anyhow::bail!("shell/input failed: HTTP {status} — {body}");
150 }
151
152 let result: ShellInputResponse = resp
153 .json()
154 .await
155 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
156 Ok(if result.written {
157 format!("wrote to shell {shell_id}")
158 } else {
159 match result.reason {
160 Some(ShellInputFailure::StdinNotCaptured) => format!(
161 "shell {shell_id} is running but was started without capture_stdin=true — \
162 its stdin is /dev/null. Recreate the shell with Shell(..., capture_stdin=true) \
163 to send input."
164 ),
165 Some(ShellInputFailure::WriteFailed) => format!(
166 "shell {shell_id} closed its stdin — input discarded"
167 ),
168 Some(ShellInputFailure::NotRunning) | None => format!(
169 "shell {shell_id} is not running — input discarded"
170 ),
171 }
172 })
173 }
174 "ShellStatus" => {
175 let shell_id = arguments
176 .get("shell_id")
177 .and_then(|v| v.as_str())
178 .ok_or_else(|| anyhow::anyhow!("missing required parameter: shell_id"))?;
179
180 if local_url.is_empty() || auth_key.is_empty() {
181 anyhow::bail!(
182 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
183 Is this agent pane opened via AgentMux?"
184 );
185 }
186
187 let url = format!("{}/api/v1/shell/status", local_url.trim_end_matches('/'));
188 let resp = client
189 .post(&url)
190 .header(AUTH_KEY_HEADER, auth_key)
191 .json(&ShellStatusRequest { shell_id: shell_id.to_string() })
192 .send()
193 .await
194 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
195
196 if !resp.status().is_success() {
197 let status = resp.status();
198 let body = resp.text().await.unwrap_or_default();
199 anyhow::bail!("shell/status failed: HTTP {status} — {body}");
200 }
201
202 let result: ShellStatusResponse = resp
203 .json()
204 .await
205 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
206 Ok(if result.running {
207 format!("shell {shell_id} is running ({} lines so far)", result.line_count)
208 } else {
209 let code = result.exit_code.map(|c| c.to_string()).unwrap_or_else(|| "unknown".to_string());
210 format!("shell {shell_id} has exited — exit_code: {code}, {} lines total", result.line_count)
211 })
212 }
213 "ConnList" => {
214 crate::srv_http::require_agent_env(local_url, auth_key, block_id)?;
215 let url = format!("{}/api/v1/conn/list", local_url.trim_end_matches('/'));
216 let result = crate::srv_http::srv_get_json(client, &url, auth_key, &[], "ConnList").await?;
217 Ok(serde_json::to_string_pretty(&result)?)
218 }
219 _ => Err(not_in_family()),
220 }
221}