agentmux_mcp/tools/
pty_shell.rs1use super::*;
7
8pub(super) async fn call(name: &str, arguments: &Value, cx: &ToolCtx<'_>) -> Result<String> {
9 let ToolCtx { local_url, auth_key, block_id, client, .. } = *cx;
10 match name {
11 "PtyShell" => {
12 if local_url.is_empty() || auth_key.is_empty() {
13 anyhow::bail!(
14 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
15 Is this agent pane opened via AgentMux?"
16 );
17 }
18 if block_id.is_empty() {
19 anyhow::bail!(
20 "neither AGENTMUX_AGENT_BUS_ID nor AGENTMUX_BLOCKID is set \
21 — cannot associate the shell with a conversation pane. \
22 Is this agent pane opened via AgentMux?"
23 );
24 }
25
26 let cwd = arguments.get("cwd").and_then(|v| v.as_str()).map(str::to_string);
27 let rows = arguments.get("rows").and_then(|v| v.as_u64()).map(|n| n as u16);
28 let cols = arguments.get("cols").and_then(|v| v.as_u64()).map(|n| n as u16);
29
30 let url = format!("{}/api/v1/ptyshell/create", local_url.trim_end_matches('/'));
31 let connection = arguments.get("connection").and_then(|v| v.as_str()).map(str::to_string);
32 let req = PtyShellCreateRequest {
33 agent_block_id: block_id.to_string(),
34 cwd,
35 rows,
36 cols,
37 connection,
38 auth: sign_ui_automation_auth().ok(),
39 };
40 let resp = client
41 .post(&url)
42 .header(AUTH_KEY_HEADER, auth_key)
43 .timeout(crate::srv_http::SHELL_CREATE_TIMEOUT)
44 .json(&req)
45 .send()
46 .await
47 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
48
49 if !resp.status().is_success() {
50 let status = resp.status();
51 let text = resp.text().await.unwrap_or_default();
52 anyhow::bail!("ptyshell/create failed: HTTP {status} — {text}");
53 }
54
55 let result: PtyShellCreateResponse = resp
56 .json()
57 .await
58 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
59 Ok(result.shell_id)
60 }
61 "PtyShellInput" => {
62 let shell_id = arguments
63 .get("shell_id")
64 .and_then(|v| v.as_str())
65 .ok_or_else(|| anyhow::anyhow!("missing required parameter: shell_id"))?;
66 let text = arguments
67 .get("text")
68 .and_then(|v| v.as_str())
69 .ok_or_else(|| anyhow::anyhow!("missing required parameter: text"))?;
70
71 if local_url.is_empty() || auth_key.is_empty() {
72 anyhow::bail!(
73 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
74 Is this agent pane opened via AgentMux?"
75 );
76 }
77
78 let url = format!("{}/api/v1/ptyshell/input", local_url.trim_end_matches('/'));
79 let resp = client
80 .post(&url)
81 .header(AUTH_KEY_HEADER, auth_key)
82 .json(&PtyShellInputRequest {
83 shell_id: shell_id.to_string(),
84 agent_block_id: block_id.to_string(),
85 text: text.to_string(),
86 })
87 .send()
88 .await
89 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
90
91 if !resp.status().is_success() {
92 let status = resp.status();
93 let body = resp.text().await.unwrap_or_default();
94 anyhow::bail!("ptyshell/input failed: HTTP {status} — {body}");
95 }
96
97 let result: PtyShellInputResponse = resp
98 .json()
99 .await
100 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
101 Ok(if result.written {
102 format!("wrote to shell {shell_id}")
103 } else {
104 format!(
105 "shell {shell_id}: write failed — {}",
106 result.error.unwrap_or_else(|| "unknown reason".to_string())
107 )
108 })
109 }
110 "PtyShellResize" => {
111 let shell_id = arguments
112 .get("shell_id")
113 .and_then(|v| v.as_str())
114 .ok_or_else(|| anyhow::anyhow!("missing required parameter: shell_id"))?;
115 let rows = arguments
116 .get("rows")
117 .and_then(|v| v.as_u64())
118 .ok_or_else(|| anyhow::anyhow!("missing required parameter: rows"))? as u16;
119 let cols = arguments
120 .get("cols")
121 .and_then(|v| v.as_u64())
122 .ok_or_else(|| anyhow::anyhow!("missing required parameter: cols"))? as u16;
123
124 if local_url.is_empty() || auth_key.is_empty() {
125 anyhow::bail!(
126 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
127 Is this agent pane opened via AgentMux?"
128 );
129 }
130
131 let url = format!("{}/api/v1/ptyshell/resize", local_url.trim_end_matches('/'));
132 let resp = client
133 .post(&url)
134 .header(AUTH_KEY_HEADER, auth_key)
135 .json(&PtyShellResizeRequest {
136 shell_id: shell_id.to_string(),
137 agent_block_id: block_id.to_string(),
138 rows,
139 cols,
140 })
141 .send()
142 .await
143 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
144
145 if !resp.status().is_success() {
146 let status = resp.status();
147 let body = resp.text().await.unwrap_or_default();
148 anyhow::bail!("ptyshell/resize failed: HTTP {status} — {body}");
149 }
150
151 let result: PtyShellResizeResponse = resp
152 .json()
153 .await
154 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
155 Ok(if result.resized {
156 format!("resized shell {shell_id} to {rows}x{cols}")
157 } else {
158 format!(
159 "shell {shell_id}: resize failed — {}",
160 result.error.unwrap_or_else(|| "unknown reason".to_string())
161 )
162 })
163 }
164 "PtyShellRead" => {
165 let shell_id = arguments
166 .get("shell_id")
167 .and_then(|v| v.as_str())
168 .ok_or_else(|| anyhow::anyhow!("missing required parameter: shell_id"))?;
169 let tail_lines = arguments.get("tail_lines").and_then(|v| v.as_u64()).map(|n| n as u32);
170
171 if local_url.is_empty() || auth_key.is_empty() {
172 anyhow::bail!(
173 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
174 Is this agent pane opened via AgentMux?"
175 );
176 }
177
178 let url = format!("{}/api/v1/ptyshell/read", local_url.trim_end_matches('/'));
179 let resp = client
180 .post(&url)
181 .header(AUTH_KEY_HEADER, auth_key)
182 .json(&PtyShellReadRequest {
183 shell_id: shell_id.to_string(),
184 agent_block_id: block_id.to_string(),
185 tail_lines,
186 })
187 .send()
188 .await
189 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
190
191 if !resp.status().is_success() {
192 let status = resp.status();
193 let body = resp.text().await.unwrap_or_default();
194 anyhow::bail!("ptyshell/read failed: HTTP {status} — {body}");
195 }
196
197 let result: PtyShellReadResponse = resp
198 .json()
199 .await
200 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
201 let prefix = if result.truncated { "[...output truncated...]\n" } else { "" };
202 Ok(format!("{prefix}{}", result.content))
203 }
204 "PtyShellStatus" => {
205 let shell_id = arguments
206 .get("shell_id")
207 .and_then(|v| v.as_str())
208 .ok_or_else(|| anyhow::anyhow!("missing required parameter: shell_id"))?;
209
210 if local_url.is_empty() || auth_key.is_empty() {
211 anyhow::bail!(
212 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
213 Is this agent pane opened via AgentMux?"
214 );
215 }
216
217 let url = format!("{}/api/v1/ptyshell/status", local_url.trim_end_matches('/'));
218 let resp = client
219 .post(&url)
220 .header(AUTH_KEY_HEADER, auth_key)
221 .json(&PtyShellStatusRequest {
222 shell_id: shell_id.to_string(),
223 agent_block_id: block_id.to_string(),
224 })
225 .send()
226 .await
227 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
228
229 if !resp.status().is_success() {
230 let status = resp.status();
231 let body = resp.text().await.unwrap_or_default();
232 anyhow::bail!("ptyshell/status failed: HTTP {status} — {body}");
233 }
234
235 let result: PtyShellStatusResponse = resp
236 .json()
237 .await
238 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
239 Ok(if result.running {
240 format!("shell {shell_id} is running")
241 } else {
242 let code = result.exit_code.map(|c| c.to_string()).unwrap_or_else(|| "unknown".to_string());
243 format!("shell {shell_id} is not running — exit_code: {code}")
244 })
245 }
246 "PtyShellStop" => {
247 let shell_id = arguments
248 .get("shell_id")
249 .and_then(|v| v.as_str())
250 .ok_or_else(|| anyhow::anyhow!("missing required parameter: shell_id"))?;
251
252 if local_url.is_empty() || auth_key.is_empty() {
253 anyhow::bail!(
254 "AGENTMUX_LOCAL_URL and AGENTMUX_AUTH_KEY must be set. \
255 Is this agent pane opened via AgentMux?"
256 );
257 }
258
259 let url = format!("{}/api/v1/ptyshell/stop", local_url.trim_end_matches('/'));
260 let resp = client
261 .post(&url)
262 .header(AUTH_KEY_HEADER, auth_key)
263 .json(&PtyShellStopRequest {
264 shell_id: shell_id.to_string(),
265 agent_block_id: block_id.to_string(),
266 })
267 .send()
268 .await
269 .map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
270
271 if !resp.status().is_success() {
272 let status = resp.status();
273 let body = resp.text().await.unwrap_or_default();
274 anyhow::bail!("ptyshell/stop failed: HTTP {status} — {body}");
275 }
276
277 let result: PtyShellStopResponse = resp
278 .json()
279 .await
280 .map_err(|e| anyhow::anyhow!("response parse failed: {e}"))?;
281 Ok(if result.released {
282 format!("released the agent lock on shell {shell_id} (the shell itself keeps running)")
283 } else {
284 format!("shell {shell_id}: no active agent lock to release (unrecognized id, not yours, or already unlocked)")
285 })
286 }
287 _ => Err(not_in_family()),
288 }
289}