Architecture overview
How Murage fits together: the driver SPI in server/contracts.ts, provider drivers, the harness registry, the fan-in event bus, the HTTP and SSE API, the React app and the Electron shell.
Murage is deliberately small and direct: plain Node on the server with no server framework, one store, one event bus. This page walks through the pieces in the order a message travels through them.
The big picture
There are three processes you care about:
- The harness server (
server/). Portable Node. It owns bots, conversations, memory, routines, approvals and the engines. It serves an HTTP and SSE API on127.0.0.1only. - The app (
src/). A React chat app built with Vite. It has no transport of its own: it sends HTTP commands out and reads one SSE stream in. - The desktop shell (
electron/). Electron. It starts the server, shows the app, and owns everything that needs the operating system: encrypted credentials, dictation, screen capture, local computer control, the built-in browser, the tray and updates.
Types shared by the app and server live in shared/. The phone web app lives in companion/. Data lives in ~/.murage/.
Start here: server/contracts.ts
server/contracts.ts holds the driver SPI and the canonical runtime event types. The repository calls it "the whole architecture in one file", and it's the right first read. The shapes and names follow the upstream harness they were ported from, with Promises and listener callbacks instead of Effect streams.
The key types:
ProviderDriver: a kind of engine. It has adriverKind, displaymetadata, an optionalinstallrecipe (per-platform install command, docs link, sign-in command), amodelscatalog,decodeConfig,defaultConfigandcreate.ProviderInstance: one configured, live engine made by a driver. It exposes itsmodels, anadapter, asnapshot()of its state, and optional helpers such asgenerateText(cheap titles and summaries), memory extraction and isolated permission review.ProviderAdapter: the conversation surface:sendTurn,interruptTurn,respondToRequest, optionalsteerandresetSession,hasSession,stopAll, andonEventfor the event stream.ProviderSnapshot:availableorunavailablewith a reason, plus sign-in state, version and whether billing is metered or a subscription.SendTurnInput: everything a turn carries: the thread, the text, the model and effort level, images, a memory bundle, the working folder, folder-trust decisions, integrations and the stop-line flag.
Runtime events
Every engine, whatever its native protocol, is translated into one set of canonical RuntimeEvents. Each event carries its provider (driver kind), optional instance id, thread, turn and item ids. The types include:
session.started,session.exited;turn.started,turn.retrying,turn.completed(with ok, stop reason, cost and token usage);content.deltafor streaming text;item.started,item.updated,item.completedfor tools, reasoning and assistant text;request.opened,request.resolvedfor approvals and questions;plan.updated,engine.commands,thread.token-usage.updated;runtime.error, with structured diagnostics.
Because the app only ever sees canonical events, adding an engine never touches the UI.
Drivers
server/drivers/ holds one file per engine family. Built-in drivers are registered in a static array in server/drivers/builtIn.ts: Fuigo (the bundled engine) first, then Grok, Gemini, Kimi, Droid, Cursor, OpenCode, Qwen, Hermes, custom ACP, Pi, the OpenAI-compatible endpoint, Claude, Codex, Antigravity, the Box cloud agent and MiniMax. Engines that speak the Agent Client Protocol share a core in server/drivers/acp/.
Other files in server/drivers/ are tool proxies that give engines Murage's own tools (bots, memory, browser, computer, phone) over MCP.
The harness: registry and bus
server/harness/ holds two pieces.
The registry turns the instance config map (from ~/.murage/config.json) into live instances. An unknown driver, or a config that fails to decode, becomes an unavailable shadow instance with a reason instead of crashing startup. That's what keeps configs forward and backward compatible, and it's a rule contributors must not work around. Disposing one instance never touches its siblings.
The bus fans every adapter's event stream into one. Each event is stamped with its instance id, written to a per-thread canonical NDJSON log under ~/.murage/events/, and delivered to subscribers: the SSE endpoint and the server-side message folder that turns events into stored messages. The bus drops events whose driver kind doesn't match the adapter that sent them.
The HTTP and SSE API
server/index.ts is the API the app talks to. Commands go in as HTTP requests; state changes come back on one SSE stream. The server binds to 127.0.0.1 only. Requests from the desktop window carry a per-launch secret, and routes that only the owner's desktop may use are refused from other surfaces such as a paired phone. The inspector routes read a thread's runtime events and native protocol log back from disk.
API keys are write-only through the API: they're saved, and the API only reports whether each one is configured.
Permissions
Engines run real CLIs with your user's privileges. The permission broker is the consent layer between an engine and anything risky: it turns an engine's permission request into a request.opened event, an approval card, and a respondToRequest back to the engine. The stop line, approval levels, folder trust and peer-contact approval all hang off this path.
The Electron shell
The shell starts the server as a child process and serves the app in a window. It keeps credentials encrypted with the operating system's secure storage and passes them to the server as environment variables at start, so config.json doesn't hold them in plain text in packaged builds. It owns the platform-specific pieces: the macOS speech helper for dictation, screen capture, the bundled Cua Driver for local computer control, the sandboxed built-in browser, the tray, notifications, backups and the updater.
macOS-only code lives in electron/ behind platform checks. The server must stay portable Node.
Other entry points
- MCP server: a local stdio MCP server lets other MCP clients list bots and channels, read transcripts, send work and switch models. It can't approve requests, delete data, import teams or change credentials. From source it runs with
pnpm mcp. - Webhooks: a separate receiver on
127.0.0.1:8800starts tasks from authenticated HTTP calls. - Companion: the phone web app talks to the same server over your own network or Tailscale.