|
| 1 | +# MCP server |
| 2 | + |
| 3 | +## Purpose |
| 4 | + |
| 5 | +Expose Cmdr functionality to AI agents via the Model Context Protocol (MCP). Agents can control the app using the same capabilities available to users—no more, no less. |
| 6 | + |
| 7 | +## Architecture |
| 8 | + |
| 9 | +### Server (`server.rs`) |
| 10 | + |
| 11 | +- Runs in a background tokio task spawned at app startup |
| 12 | +- Binds to `127.0.0.1:9224` (localhost only for security) |
| 13 | +- Streamable HTTP transport (MCP spec 2025-11-25) |
| 14 | +- Endpoints: `POST /mcp` (JSON-RPC), `GET /mcp/sse` (optional SSE), `GET /mcp/health` |
| 15 | + |
| 16 | +### Protocol (`protocol.rs`) |
| 17 | + |
| 18 | +- JSON-RPC 2.0 message parsing |
| 19 | +- Routes to `initialize`, `tools/list`, `tools/call`, `resources/list`, `resources/read` |
| 20 | +- Session management (though most clients don't use sessions) |
| 21 | + |
| 22 | +### Tools (`tools.rs`) |
| 23 | + |
| 24 | +18 semantic tools grouped by category: |
| 25 | +- Navigation (6): `select_volume`, `nav_to_path`, `move_cursor`, etc. |
| 26 | +- Cursor/Selection (3): `move_cursor`, `open_under_cursor`, `select` |
| 27 | +- File operations (3): `copy`, `mkdir`, `refresh` |
| 28 | +- View (3): `sort`, `toggle_hidden`, `set_view_mode` |
| 29 | +- Dialogs (1): `dialog` (unified open/focus/close) |
| 30 | +- App (2): `switch_pane`, `quit` |
| 31 | + |
| 32 | +### Resources (`resources.rs`) |
| 33 | + |
| 34 | +- `cmdr://state`: Complete app state in YAML (both panes, volumes, dialogs) |
| 35 | +- `cmdr://dialogs/available`: Static metadata about available dialogs |
| 36 | + |
| 37 | +### Executor (`executor.rs`) |
| 38 | + |
| 39 | +Routes tool calls to implementations. Most tools emit Tauri events that trigger the same code paths as keyboard shortcuts or menu clicks. |
| 40 | + |
| 41 | +### State stores |
| 42 | + |
| 43 | +- `PaneStateStore`: Current state of left/right panes (path, files, cursor, selection) |
| 44 | +- `SoftDialogTracker`: Which dialogs MCP thinks are open |
| 45 | +- `SettingsStateStore`: Current settings window state (section, settings, shortcuts) |
| 46 | + |
| 47 | +Frontend syncs state to these stores via Tauri commands (`update_left_pane_state`, `mcp_update_settings_sections`, etc.). |
| 48 | + |
| 49 | +## Key decisions |
| 50 | + |
| 51 | +### Why agent-centric API? |
| 52 | + |
| 53 | +The original design mirrored keyboard shortcuts (43 tools like `nav_up`, `nav_down`). This forced agents to make dozens of calls to find a file. The agent-centric redesign (Jan 2026) consolidated to 18 semantic tools (`move_cursor(index=42)`, `nav_to_path("/Users")`). This reduced round-trips from 6+ reads to 1 (`cmdr://state` resource). |
| 54 | + |
| 55 | +### Why YAML over JSON for resources? |
| 56 | + |
| 57 | +LLMs consume resources, not machines. YAML is 30-40% smaller and more readable. The `cmdr://state` resource is optimized for LLM token usage, not parsing speed. |
| 58 | + |
| 59 | +### Why plain text responses? |
| 60 | + |
| 61 | +Tool results are plain text (`"OK: Navigated to /Users"`, `"ERROR: Path not found"`), not JSON objects. This reduces token usage and is easier for LLMs to parse. Errors are still JSON-RPC error objects, but the `content` field is plain text. |
| 62 | + |
| 63 | +### Why stateful architecture? |
| 64 | + |
| 65 | +Without state, resources would need to query the frontend on every read (slow, async). Storing state in Rust allows synchronous reads. The frontend syncs state after meaningful changes (file load, cursor move, selection). |
| 66 | + |
| 67 | +### Why no file system access? |
| 68 | + |
| 69 | +Security via parity: agents can only do what users can do. Giving agents `fs.read`/`fs.write` would violate this. Agents navigate the UI just like users, using `move_cursor`, `open_under_cursor`, etc. |
| 70 | + |
| 71 | +### Why localhost only? |
| 72 | + |
| 73 | +Binding to `0.0.0.0` would expose the server to the network. An attacker could quit the app, change settings, or navigate to sensitive directories. Localhost binding ensures only local processes can connect. |
| 74 | + |
| 75 | +### Why separate state stores? |
| 76 | + |
| 77 | +`PaneStateStore` is always synced (file pane changes frequently). `SettingsStateStore` is only synced when settings window is open (rare). `SoftDialogTracker` is updated by MCP tools themselves. Separating concerns keeps each store simple. |
| 78 | + |
| 79 | +## Gotchas |
| 80 | + |
| 81 | +### Server starts in background task |
| 82 | + |
| 83 | +`start_mcp_server()` spawns a tokio task and returns immediately. If the server crashes, the app continues but MCP stops working. Check logs for "MCP server crashed" errors. |
| 84 | + |
| 85 | +### State sync is best-effort |
| 86 | + |
| 87 | +Frontend calls `update_left_pane_state()` after loading files, but there's no guarantee it completes before an MCP resource read. In practice, updates are fast and this isn't an issue. If stale data is a concern, add explicit sync waits. |
| 88 | + |
| 89 | +### Dialog state is "soft" |
| 90 | + |
| 91 | +`SoftDialogTracker` stores which dialogs MCP thinks are open, but if a dialog is closed manually (not via MCP), the tracker isn't updated. The `cmdr://state` resource double-checks reality by querying Tauri windows. |
| 92 | + |
| 93 | +### View mode affects resource detail |
| 94 | + |
| 95 | +`cmdr://state` shows file details differently based on view mode: |
| 96 | +- Full mode: all file info inline (`i:42 f package.json 1183b lm:2025-01-10`) |
| 97 | +- Brief mode: only cursor file gets details, rest are just names (`i:42 f package.json`) |
| 98 | + |
| 99 | +This prevents overwhelming agents with data they can't see in the UI. |
| 100 | + |
| 101 | +### Pane state includes pagination |
| 102 | + |
| 103 | +Large directories (50k+ files) are paginated. The `totalFiles`, `loadedStart`, `loadedEnd` fields indicate what's currently loaded. Agents must use `scroll_to(index)` to load different regions. |
| 104 | + |
| 105 | +### Resources don't require initialization |
| 106 | + |
| 107 | +Unlike tools (which need a session via `initialize`), resources can be read immediately after server start. This is by design for debugging with curl. |
| 108 | + |
| 109 | +### Settings state sync is window-specific |
| 110 | + |
| 111 | +The settings window calls `syncSettingsState()` on mount and section changes. The main window doesn't sync settings state (it doesn't need to). This means `cmdr://state` only includes settings when the settings window is open. |
| 112 | + |
| 113 | +### MCP-settings bridge vs MCP-shortcuts listener |
| 114 | + |
| 115 | +Settings window: full bridge (`mcp-settings-bridge.ts`) syncs all state and handles all MCP events. |
| 116 | +Main window: lightweight listener (`mcp-shortcuts-listener.ts`) only handles shortcut changes. |
| 117 | +This separation keeps main window overhead minimal. |
| 118 | + |
| 119 | +### Tool execution is synchronous |
| 120 | + |
| 121 | +`execute_tool()` is a synchronous function. Tools that trigger async operations (like `copy`, `mkdir`) return immediately after emitting the event. The tool result doesn't wait for the operation to complete. This is intentional—tools return "OK: Copy dialog opened" not "OK: Files copied". |
| 122 | + |
| 123 | +### Error codes are JSON-RPC standard |
| 124 | + |
| 125 | +`INVALID_PARAMS = -32602`, `INTERNAL_ERROR = -32603`, etc. These are defined by the JSON-RPC spec, not MCP. Don't change them. |
| 126 | + |
| 127 | +### Schema version doesn't apply to MCP state |
| 128 | + |
| 129 | +MCP state stores don't have `_schemaVersion` fields. They're runtime-only, not persisted. If the state format changes, just restart the app. |
0 commit comments