Daedalus, MCP server
How the MCP server inside Daedalus works
Daedalus embeds an MCP server at 127.0.0.1:14210. This page documents how it's wired up internally, useful when inspecting or extending it.
Why it's there
The MCP server lets Claude (or any MCP-aware agent) drive Daedalus the same way a human does: click buttons, type into form fields, read screenshots, execute terminal commands. The use case is agent-driven deployments, Claude walks through the wizard end-to-end while a human supervises.
See ADR 0022, MCP Localhost Only for the security rationale.
Where the code lives
| File | Purpose |
|---|---|
daedalus/src-tauri/src/mcp/mod.rs | Module root. Sets up the HTTP listener at 127.0.0.1:14210. |
daedalus/src-tauri/src/mcp/protocol.rs | JSON-RPC 2.0 envelope types: Request, Response, Error, ToolDefinition, ToolCallResult, ContentBlock. |
daedalus/src-tauri/src/mcp/tools.rs | Tool implementations. The canonical list of MCP tools. |
daedalus/src-tauri/src/commands/screenshot.rs | Native macOS screenshot via ScreenCaptureKit. |
daedalus/src-tauri/src/commands/pty.rs | Embedded PTY for terminal_exec / terminal_read. |
daedalus/src-tauri/src/commands/context.rs | Deployment context (daedalus.json) read/write. |
Protocol shape
The server speaks JSON-RPC 2.0 over HTTP POST at the single endpoint /. Standard MCP methods:
| Method | Purpose |
|---|---|
initialize | MCP handshake. Returns server name, version, capabilities. |
tools/list | Return the list of available tools (the eight from tools.rs). |
tools/call | Invoke a tool with arguments. Returns ToolCallResult. |
The 8 tools (see Reference, MCP):
screenshot, capture the Daedalus UI as PNG, return asimagecontent block.get_page, return current webview path + extracted text.navigate, change the wizard route.click, click an element by text or selector.form_input, set a form field value by label or selector.read_context, read the deployment context (daedalus.json).terminal_exec, execute a command in the embedded PTY.terminal_read, read recent PTY output.
The web-Rust bridge
Daedalus uses Tauri. The wizard UI is React running inside the webview; the MCP server runs in the Rust process.
When an MCP tool needs to interact with the UI (e.g. click):
- The MCP server invokes a Tauri command (
navigate_to_path,click_element_by_text, etc.) declared inlib.rs. - The Tauri command emits a custom event to the webview (
tauri::Manager::emit). - A React effect inside the webview listens for the event and performs the DOM action.
- The result (or an error) is reported back to Rust via a Tauri callback.
- Rust returns the result to the MCP caller.
Synchronous-feeling round-trip; ~10-100ms typical latency.
The screenshot pipeline
screenshot is the most platform-specific tool:
Manager::get_webview_window("main")obtains the Tauri window handle.- On macOS,
commands/screenshot.rsusesScreenCaptureKitvia thescreencapturekitcrate to capture the window contents. - Image is encoded as PNG.
- The PNG bytes are base64-encoded and returned as an MCP
imagecontent block.
Each capture is ~50-200ms depending on window size. The screenshot includes the wizard UI and any visible secrets, see ADR 0022 for the security framing.
The PTY pipeline
terminal_exec and terminal_read:
commands/pty.rsmaintains a single persistentportable-ptyinstance.- The PTY runs
bash(or the user's$SHELL). - Stdout/stderr is captured into a ring buffer (last 100KB).
terminal_execwrites the command to the PTY's stdin.terminal_readreturns the ring buffer content.
The buffer is sanitized before return, see Internals, Daedalus Secrets Sanitizer for the regex patterns that scrub bearer tokens, API keys, and DSN passwords from the output.
Initialization
On Daedalus startup, the MCP server is spawned as a Tokio task in mcp/mod.rs::start():
let listener = TcpListener::bind("127.0.0.1:14210").await?;
loop {
let (socket, _) = listener.accept().await?;
let app_handle = app_handle.clone();
tokio::spawn(async move {
handle_connection(socket, app_handle).await;
});
}If port 14210 is already in use (a stale Daedalus instance), the server logs the error and continues without MCP, the wizard still works for human users.
Tool registration
tools.rs::list_tools() returns a static Vec<ToolDefinition>. To add a new tool:
- Add the
ToolDefinitionliteral tolist_tools()with name, description, input schema. - Add the dispatch branch in
tools.rs::call_tool()to handle the new tool name. - Implement the actual logic (often as a new Tauri command).
- Rebuild Daedalus.
The generated MCP reference is parsed from tools.rs at build time, so a new tool automatically appears in the docs without manual updates.
Error handling
JSON-RPC errors use standard codes plus MCP-specific codes:
-32700Parse error-32600Invalid Request-32601Method not found (e.g. asking for a tool that doesn't exist)-32602Invalid params (e.g. missing required field in tool arguments)-32603Internal error-32000..-32099Implementation-defined (Daedalus uses these for "wizard not in expected state" errors)
Testing
The MCP server itself doesn't have unit tests in the Rust source, testing happens via integration with an MCP client. The CI workflow (daedalus/.github/workflows/ci.yml) spawns Daedalus in headless mode and runs a script that exercises every tool.
Where next
- Reference, Daedalus MCP, generated per-tool reference.
- ADR 0022, MCP Localhost Only.
- Integrate, MCP with Daedalus, driving Daedalus from an MCP client.
- Internals, Daedalus secrets sanitizer.