1use std::collections::HashMap;
27use std::sync::atomic::{AtomicU64, Ordering};
28use std::sync::{Arc, Mutex};
29use std::time::Duration;
30
31use agentmux_common::api_types::{
32 InjectRequest, PaneOpenRequest, PaneOpenResponse, PtyShellCreateRequest, PtyShellCreateResponse,
33 PtyShellInputRequest, PtyShellInputResponse, PtyShellReadRequest, PtyShellReadResponse,
34 PtyShellResizeRequest, PtyShellResizeResponse,
35 PtyShellStatusRequest, PtyShellStatusResponse, PtyShellStopRequest, PtyShellStopResponse,
36 ShellCreateRequest, ShellCreateResponse, ShellInputFailure, ShellInputRequest,
37 ShellInputResponse, ShellStatusRequest, ShellStatusResponse, ShellStopRequest,
38 ShellStopResponse, TabActivateRequest, TabNameRequest, TabNewRequest,
39 UiBrowserDispatchKeyRequest, UiBrowserEvalRequest, UiBrowserFocusElementRequest,
40 UiBrowserFocusInfoRequest, UiBrowserHistoryRequest, UiBrowserNavigateRequest,
41 UiBrowserOpenRequest, UiClickRequest, UiPressKeysRequest, UiQueryRequest, UiRunCommandRequest,
42 UiScreenshotRequest, UiScreenshotResponse, UiShortcutsListRequest, WindowFocusRequest,
43 WindowNameRequest, WorkspaceNameRequest, PaneTitleRequest, ClosePaneRequest, QuitSelfRequest,
44 RegisterDevServerRequest, RegisterDevServerResponse,
45};
46use agentmux_common::AUTH_KEY_HEADER;
47use anyhow::Result;
48use serde_json::{json, Value};
49use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
50use tokio::task::JoinHandle;
51
52struct LoopEntry {
54 handle: JoinHandle<()>,
55 prompt: String,
56 target: String,
57 interval_secs: u64,
58 max_iterations: Option<u64>,
59 fire_count: Arc<AtomicU64>,
60 started_at: u64,
61}
62
63type LoopRegistry = Mutex<HashMap<String, LoopEntry>>;
67
68mod doctor;
69mod self_keys;
70mod tool_schemas;
71use tool_schemas::*;
72mod window_capture;
73use window_capture::*;
74
75#[tokio::main]
76async fn main() {
77 if std::env::args().nth(1).as_deref() == Some("doctor") {
80 std::process::exit(doctor::run(std::env::args().skip(2).collect()).await);
81 }
82 let local_url = std::env::var("AGENTMUX_LOCAL_URL").unwrap_or_default();
83 let auth_key = std::env::var("AGENTMUX_AUTH_KEY").unwrap_or_default();
84 let block_id = {
96 let blockid = std::env::var("AGENTMUX_BLOCKID").unwrap_or_default();
97 if blockid.is_empty() {
98 std::env::var("AGENTMUX_AGENT_BUS_ID").unwrap_or_default()
99 } else {
100 blockid
101 }
102 };
103
104 let mut default_headers = reqwest::header::HeaderMap::new();
109 if let Some(token) = std::env::var("AGENTMUX_AGENT_TOKEN")
110 .ok()
111 .filter(|t| !t.trim().is_empty())
112 .and_then(|t| reqwest::header::HeaderValue::from_str(t.trim()).ok())
113 {
114 let mut token = token;
115 token.set_sensitive(true);
116 default_headers.insert("X-Agent-Token", token);
117 }
118 let client = reqwest::Client::builder()
119 .timeout(std::time::Duration::from_secs(10))
120 .default_headers(default_headers)
121 .build()
122 .expect("http client");
123
124 {
127 let (client, local_url, auth_key) = (client.clone(), local_url.clone(), auth_key.clone());
128 tokio::spawn(async move { self_keys::refresh_if_stale(&client, &local_url, &auth_key).await });
129 }
130
131 let loops: LoopRegistry = Mutex::new(HashMap::new());
134 let loop_counter = AtomicU64::new(0);
135
136 let stdin = tokio::io::stdin();
137 let mut stdout = tokio::io::stdout();
138 let mut reader = BufReader::new(stdin);
139 let mut line = String::new();
140
141 loop {
142 line.clear();
143 match reader.read_line(&mut line).await {
144 Ok(0) => break,
145 Ok(_) => {}
146 Err(_) => break,
147 }
148
149 let trimmed = line.trim();
150 if trimmed.is_empty() {
151 continue;
152 }
153
154 let msg: Value = match serde_json::from_str(trimmed) {
155 Ok(v) => v,
156 Err(_) => continue,
157 };
158
159 let method = msg.get("method").and_then(|v| v.as_str()).unwrap_or("");
160 let id = msg.get("id").cloned().unwrap_or(Value::Null);
161 let params = msg.get("params").cloned().unwrap_or(Value::Null);
162
163 if id.is_null() {
165 continue;
166 }
167
168 let response = match method {
169 "initialize" => {
170 json!({
171 "jsonrpc": "2.0",
172 "id": id,
173 "result": {
174 "protocolVersion": "2024-11-05",
175 "capabilities": { "tools": {} },
176 "serverInfo": { "name": "agentmux-mcp", "version": env!("CARGO_PKG_VERSION") }
177 }
178 })
179 }
180 "tools/list" => {
181 let shell: Value = serde_json::from_str(SHELL_TOOL).expect("static json");
182 let shell_stop: Value = serde_json::from_str(SHELL_STOP_TOOL).expect("static json");
183 let shell_input: Value = serde_json::from_str(SHELL_INPUT_TOOL).expect("static json");
184 let shell_status: Value = serde_json::from_str(SHELL_STATUS_TOOL).expect("static json");
185 let pty_shell: Value = serde_json::from_str(PTY_SHELL_TOOL).expect("static json");
186 let pty_shell_input: Value =
187 serde_json::from_str(PTY_SHELL_INPUT_TOOL).expect("static json");
188 let pty_shell_resize: Value =
189 serde_json::from_str(PTY_SHELL_RESIZE_TOOL).expect("static json");
190 let pty_shell_read: Value =
191 serde_json::from_str(PTY_SHELL_READ_TOOL).expect("static json");
192 let pty_shell_status: Value =
193 serde_json::from_str(PTY_SHELL_STATUS_TOOL).expect("static json");
194 let pty_shell_stop: Value =
195 serde_json::from_str(PTY_SHELL_STOP_TOOL).expect("static json");
196 let open_editor: Value = serde_json::from_str(OPEN_EDITOR_TOOL).expect("static json");
197 let open_media: Value = serde_json::from_str(OPEN_MEDIA_TOOL).expect("static json");
198 let open_files: Value = serde_json::from_str(OPEN_FILES_TOOL).expect("static json");
199 let widget_list: Value = serde_json::from_str(WIDGET_LIST_TOOL).expect("static json");
200 let widget_install: Value = serde_json::from_str(WIDGET_INSTALL_TOOL).expect("static json");
201 let open_widget: Value = serde_json::from_str(OPEN_WIDGET_TOOL).expect("static json");
202 let send_message: Value = serde_json::from_str(SEND_MESSAGE_TOOL).expect("static json");
203 let discover_agents: Value =
204 serde_json::from_str(DISCOVER_AGENTS_TOOL).expect("static json");
205 let get_agent_transcript: Value =
206 serde_json::from_str(GET_AGENT_TRANSCRIPT_TOOL).expect("static json");
207 let list_conversations: Value =
208 serde_json::from_str(LIST_CONVERSATIONS_TOOL).expect("static json");
209 let who_is_working_on: Value =
210 serde_json::from_str(WHO_IS_WORKING_ON_TOOL).expect("static json");
211 let claim_work: Value = serde_json::from_str(CLAIM_WORK_TOOL).expect("static json");
212 let release_work: Value = serde_json::from_str(RELEASE_WORK_TOOL).expect("static json");
213 let search_history: Value =
214 serde_json::from_str(SEARCH_HISTORY_TOOL).expect("static json");
215 let supervisor_nudge: Value =
216 serde_json::from_str(SUPERVISOR_NUDGE_TOOL).expect("static json");
217 let whoami: Value = serde_json::from_str(WHOAMI_TOOL).expect("static json");
218 let conn_list: Value = serde_json::from_str(CONN_LIST_TOOL).expect("static json");
219 let layout: Value = serde_json::from_str(LAYOUT_TOOL).expect("static json");
220 let set_name: Value = serde_json::from_str(SET_NAME_TOOL).expect("static json");
221 let set_active_tab: Value =
222 serde_json::from_str(SET_ACTIVE_TAB_TOOL).expect("static json");
223 let new_tab: Value = serde_json::from_str(NEW_TAB_TOOL).expect("static json");
224 let focus_window: Value =
225 serde_json::from_str(FOCUS_WINDOW_TOOL).expect("static json");
226 let ui_screenshot: Value =
227 serde_json::from_str(UI_SCREENSHOT_TOOL).expect("static json");
228 let ui_click: Value = serde_json::from_str(UI_CLICK_TOOL).expect("static json");
229 let ui_query: Value = serde_json::from_str(UI_QUERY_TOOL).expect("static json");
230 let list_shortcuts: Value =
231 serde_json::from_str(LIST_SHORTCUTS_TOOL).expect("static json");
232 let run_command: Value = serde_json::from_str(RUN_COMMAND_TOOL).expect("static json");
233 let press_keys: Value = serde_json::from_str(PRESS_KEYS_TOOL).expect("static json");
234 let close_pane: Value =
235 serde_json::from_str(CLOSE_PANE_TOOL).expect("static json");
236 let quit_self: Value = serde_json::from_str(QUIT_SELF_TOOL).expect("static json");
237 let register_dev_server: Value =
238 serde_json::from_str(REGISTER_DEV_SERVER_TOOL).expect("static json");
239 let open_browser: Value =
240 serde_json::from_str(OPEN_BROWSER_TOOL).expect("static json");
241 let browser_snapshot: Value =
242 serde_json::from_str(BROWSER_SNAPSHOT_TOOL).expect("static json");
243 let browser_click: Value =
244 serde_json::from_str(BROWSER_CLICK_TOOL).expect("static json");
245 let browser_fill: Value =
246 serde_json::from_str(BROWSER_FILL_TOOL).expect("static json");
247 let browser_select: Value =
248 serde_json::from_str(BROWSER_SELECT_TOOL).expect("static json");
249 let browser_check: Value =
250 serde_json::from_str(BROWSER_CHECK_TOOL).expect("static json");
251 let browser_set_files: Value =
252 serde_json::from_str(BROWSER_SET_FILES_TOOL).expect("static json");
253 let browser_wait_for: Value =
254 serde_json::from_str(BROWSER_WAIT_FOR_TOOL).expect("static json");
255 let browser_handoff: Value =
256 serde_json::from_str(BROWSER_HANDOFF_TOOL).expect("static json");
257 let browser_navigate: Value =
258 serde_json::from_str(BROWSER_NAVIGATE_TOOL).expect("static json");
259 let browser_back: Value =
260 serde_json::from_str(BROWSER_BACK_TOOL).expect("static json");
261 let browser_forward: Value =
262 serde_json::from_str(BROWSER_FORWARD_TOOL).expect("static json");
263 let browser_reload: Value =
264 serde_json::from_str(BROWSER_RELOAD_TOOL).expect("static json");
265 let browser_eval: Value =
266 serde_json::from_str(BROWSER_EVAL_TOOL).expect("static json");
267 let browser_dispatch_key: Value =
268 serde_json::from_str(BROWSER_DISPATCH_KEY_TOOL).expect("static json");
269 let browser_focus_element: Value =
270 serde_json::from_str(BROWSER_FOCUS_ELEMENT_TOOL).expect("static json");
271 let browser_focus_info: Value =
272 serde_json::from_str(BROWSER_FOCUS_INFO_TOOL).expect("static json");
273 let capture_window: Value =
274 serde_json::from_str(CAPTURE_WINDOW_TOOL).expect("static json");
275 let discover_windows: Value =
276 serde_json::from_str(DISCOVER_WINDOWS_TOOL).expect("static json");
277 let fleet_list: Value = serde_json::from_str(FLEET_LIST_TOOL).expect("static json");
278 let fleet_broadcast: Value =
279 serde_json::from_str(FLEET_BROADCAST_TOOL).expect("static json");
280 let fleet_bulk_stop: Value =
281 serde_json::from_str(FLEET_BULK_STOP_TOOL).expect("static json");
282 let open_agent: Value =
283 serde_json::from_str(OPEN_AGENT_TOOL).expect("static json");
284 let loop_tool: Value = serde_json::from_str(LOOP_TOOL).expect("static json");
285 let loop_stop: Value = serde_json::from_str(LOOP_STOP_TOOL).expect("static json");
286 let loop_list: Value = serde_json::from_str(LOOP_LIST_TOOL).expect("static json");
287 let work_enqueue: Value = serde_json::from_str(WORK_ENQUEUE_TOOL).expect("static json");
288 let work_claim: Value = serde_json::from_str(WORK_CLAIM_TOOL).expect("static json");
289 let work_heartbeat: Value = serde_json::from_str(WORK_HEARTBEAT_TOOL).expect("static json");
290 let work_complete: Value = serde_json::from_str(WORK_COMPLETE_TOOL).expect("static json");
291 let work_release: Value = serde_json::from_str(WORK_RELEASE_TOOL).expect("static json");
292 let work_list: Value = serde_json::from_str(WORK_LIST_TOOL).expect("static json");
293 let cron_create: Value = serde_json::from_str(CRON_CREATE_TOOL).expect("static json");
294 let cron_delete: Value = serde_json::from_str(CRON_DELETE_TOOL).expect("static json");
295 let cron_list: Value = serde_json::from_str(CRON_LIST_TOOL).expect("static json");
296 let cron_pause: Value = serde_json::from_str(CRON_PAUSE_TOOL).expect("static json");
297 let cron_resume: Value = serde_json::from_str(CRON_RESUME_TOOL).expect("static json");
298 let memory_list: Value = serde_json::from_str(MEMORY_LIST_TOOL).expect("static json");
299 let memory_read: Value = serde_json::from_str(MEMORY_READ_TOOL).expect("static json");
300 let memory_write: Value = serde_json::from_str(MEMORY_WRITE_TOOL).expect("static json");
301 let memory_history: Value = serde_json::from_str(MEMORY_HISTORY_TOOL).expect("static json");
302 let memory_diff: Value = serde_json::from_str(MEMORY_DIFF_TOOL).expect("static json");
303 let memory_revert: Value = serde_json::from_str(MEMORY_REVERT_TOOL).expect("static json");
304 let global_memory_list: Value = serde_json::from_str(GLOBAL_MEMORY_LIST_TOOL).expect("static json");
305 let global_memory_read: Value = serde_json::from_str(GLOBAL_MEMORY_READ_TOOL).expect("static json");
306 let global_memory_write: Value = serde_json::from_str(GLOBAL_MEMORY_WRITE_TOOL).expect("static json");
307 let global_memory_remove: Value = serde_json::from_str(GLOBAL_MEMORY_REMOVE_TOOL).expect("static json");
308 let global_memory_history: Value = serde_json::from_str(GLOBAL_MEMORY_HISTORY_TOOL).expect("static json");
309 let global_memory_diff: Value = serde_json::from_str(GLOBAL_MEMORY_DIFF_TOOL).expect("static json");
310 let global_memory_revert: Value = serde_json::from_str(GLOBAL_MEMORY_REVERT_TOOL).expect("static json");
311 let preset_list: Value = serde_json::from_str(PRESET_LIST_TOOL).expect("static json");
312 let preset_get: Value = serde_json::from_str(PRESET_GET_TOOL).expect("static json");
313 let identity_accounts: Value =
314 serde_json::from_str(IDENTITY_ACCOUNTS_TOOL).expect("static json");
315 let identity_validate: Value =
316 serde_json::from_str(IDENTITY_VALIDATE_TOOL).expect("static json");
317 json!({
318 "jsonrpc": "2.0",
319 "id": id,
320 "result": { "tools": [shell, shell_stop, shell_input, shell_status, pty_shell, pty_shell_input, pty_shell_resize, pty_shell_read, pty_shell_status, pty_shell_stop, conn_list, open_editor, open_media, open_files, widget_list, widget_install, open_widget, send_message, discover_agents, get_agent_transcript, list_conversations, who_is_working_on, claim_work, release_work, search_history, supervisor_nudge, whoami, layout, set_name, set_active_tab, new_tab, focus_window, ui_screenshot, ui_click, ui_query, list_shortcuts, run_command, press_keys, close_pane, quit_self, register_dev_server, open_browser, browser_snapshot, browser_click, browser_fill, browser_select, browser_check, browser_set_files, browser_wait_for, browser_handoff, browser_navigate, browser_back, browser_forward, browser_reload, browser_eval, browser_dispatch_key, browser_focus_element, browser_focus_info, capture_window, discover_windows, fleet_list, fleet_broadcast, fleet_bulk_stop, open_agent, loop_tool, loop_stop, loop_list, cron_create, cron_delete, cron_list, cron_pause, cron_resume, work_enqueue, work_claim, work_heartbeat, work_complete, work_release, work_list, memory_list, memory_read, memory_write, memory_history, memory_diff, memory_revert, global_memory_list, global_memory_read, global_memory_write, global_memory_remove, global_memory_history, global_memory_diff, global_memory_revert, preset_list, preset_get, identity_accounts, identity_validate] }
321 })
322 }
323 "tools/call" => {
324 match call_tool(¶ms, &local_url, &auth_key, &block_id, &client, &loops, &loop_counter).await {
325 Ok(content) => json!({
326 "jsonrpc": "2.0",
327 "id": id,
328 "result": { "content": [{"type": "text", "text": content}], "isError": false }
329 }),
330 Err(e) => json!({
331 "jsonrpc": "2.0",
332 "id": id,
333 "result": { "content": [{"type": "text", "text": e.to_string()}], "isError": true }
334 }),
335 }
336 }
337 _ => json!({
338 "jsonrpc": "2.0",
339 "id": id,
340 "error": { "code": -32601, "message": "method not found" }
341 }),
342 };
343
344 let resp_str = serde_json::to_string(&response).unwrap_or_default();
345 let _ = stdout.write_all(resp_str.as_bytes()).await;
346 let _ = stdout.write_all(b"\n").await;
347 let _ = stdout.flush().await;
348 }
349}
350
351mod srv_http;
352mod identity;
353mod tool_helpers;
354mod tools;
355pub(crate) use identity::*;
356use srv_http::*;
357use tool_helpers::*;
358
359async fn call_tool(
360 params: &Value,
361 local_url: &str,
362 auth_key: &str,
363 block_id: &str,
364 client: &reqwest::Client,
365 loops: &LoopRegistry,
366 loop_counter: &AtomicU64,
367) -> Result<String> {
368 let name = params
369 .get("name")
370 .and_then(|v| v.as_str())
371 .unwrap_or("");
372 let arguments = params
373 .get("arguments")
374 .cloned()
375 .unwrap_or(json!({}));
376
377 self_keys::refresh_if_stale(client, local_url, auth_key).await;
380 tools::call(
381 name,
382 &arguments,
383 &tools::ToolCtx {
384 local_url,
385 auth_key,
386 block_id,
387 client,
388 loops,
389 loop_counter,
390 },
391 )
392 .await
393}
394
395#[cfg(test)]
396#[path = "tests/main_tests.rs"]
397mod tests;