Skip to main content

agentmux_mcp/
main.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! agentmux-mcp — MCP stdio server that exposes AgentMux's MuxAPI tools
5//! (`Shell`, `SendMessage`, memory, panes, …) to Claude.
6//!
7//! Claude Code launches this binary as an MCP server (via `.mcp.json`'s
8//! `"command": "agentmux-mcp"` entry, auto-injected by agent_config.rs).
9//! Communication is JSON-RPC 2.0 over stdin/stdout.
10//!
11//! The `Shell` tool starts a persistent shell process on agentmux-srv and
12//! returns immediately with a `shell_id`. Output streams live in the
13//! conversation as a `ShellNode` row without blocking the agent.
14//!
15//! Env vars (inherited from agentmux-srv's agent env injection):
16//!   AGENTMUX_LOCAL_URL    — sidecar HTTP base URL
17//!   AGENTMUX_AUTH_KEY     — X-AuthKey header secret
18//!   AGENTMUX_BLOCKID      — block UUID for shell event scoping (preferred).
19//!                           Injected by agent_handlers/input.rs into every persistent
20//!                           subprocess env; inherited by this MCP subprocess.
21//!   AGENTMUX_AGENT_BUS_ID — MuxBus routing identifier (fallback only).
22//!                           Often set to the agent type string (e.g. "claude")
23//!                           in .mcp.json, NOT the block UUID — do not use it
24//!                           as the shell scope unless AGENTMUX_BLOCKID is absent.
25
26use 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
52/// Metadata kept alongside each running loop's task handle.
53struct 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
63/// In-process registry of running loops, keyed by loop_id. Lives for this MCP
64/// process's lifetime (== the agent session), so loops are reaped automatically
65/// when the agent pane closes.
66type 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    // `agentmux-mcp doctor [--heal]`, from the agent's shell: why its AgentMux
78    // tools aren't connected, with no MCP session (doctor.rs).
79    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    // AGENTMUX_BLOCKID is the canonical block UUID injected by agent_handlers/input.rs
85    // into the persistent subprocess env. It is what the frontend subscribes to
86    // for shell_node_create events (`block:<uuid>`), so it MUST be used here.
87    //
88    // AGENTMUX_AGENT_BUS_ID is the MuxBus routing identifier — a different
89    // concept. In existing .mcp.json files it is often set to "claude" (the
90    // agent type, not the block UUID), which was accidentally being used as the
91    // shell scope and caused shell_node_create to publish under `block:claude`
92    // instead of the real pane UUID — making the ActivityDock never receive
93    // shell events. Prefer AGENTMUX_BLOCKID; fall back to AGENTMUX_AGENT_BUS_ID
94    // only for older deployments that pre-date AGENTMUX_BLOCKID injection.
95    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    // Identity M4a (spec §6.5.3): every request carries this agent's token,
105    // inherited from the spawn environment (never from `.mcp.json`), so srv
106    // can attribute it to the agent's UID. Attribution only — srv never
107    // refuses a request for lacking it. Absent for a tokenless spawn.
108    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    // Identity M4d-3: fetch this agent's signing keys from srv now, so the
125    // first signed send doesn't wait on it (self_keys.rs).
126    {
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    // Running loops, keyed by loop_id. Lives for this MCP process's lifetime
132    // (== the agent session), so loops are reaped when the agent pane closes.
133    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        // Notifications have no id — no response expected.
164        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(&params, &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    // Identity M4d-3: every tool may sign (a jekt, a UI-automation proof), so
378    // the fetched keys are checked here, once, before any of them runs.
379    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;