An MCP server dedicated to designing, driving and debugging Electron
applications from an agent (Claude Code, Codex, any MCP harness). It is the
Electron-only extraction of ghostchrome's
renderer engine (accessibility snapshots with refs, strict locators, mutation
diffs, console/network observers) plus an Electron host layer: launching a
project or a .app in isolation, the project's own dev loop with automatic
re-attach after restarts, process-tree teardown, and a Node inspector client
for the main process.
agent (MCP client)
│ stdio
▼
mcp-electron ── internal/mcp 24 tools (session, page, flows)
│
├── internal/electron launch / attach / dev loop / logs
│ ├── CDP ───────────► renderer windows (internal/engine)
│ └── Node inspector ─► main process (eval_main)
│
└── internal/flow Maestro-style YAML flows (run_flow, CLI)
As a Claude Code plugin (MCP server + skill, with the binary for macOS universal and Windows x64 bundled: nothing else to install):
/plugin install mcp-electron --marketplace dev-toolings/mcp-electron
On older Claude Code versions: /plugin marketplace add dev-toolings/mcp-electron,
then /plugin install mcp-electron@dev-toolings.
For other MCP hosts, download the archive of your platform from the
GitHub releases
(checksums in SHA256SUMS) and register mcp-electron mcp over stdio, or
build from source:
From source, with the CLI installer (also registers Codex and writes the skill
to ~/.claude/skills and ~/.codex/skills; do not combine it with the
plugin, or Claude Code gets the server and the skill twice):
go build -o bin/mcp-electron ./cmd/mcp-electron
./bin/mcp-electron install --dry-run # prints every step, writes nothing
./bin/mcp-electron install # copy to ~/.local/bin, register, install skills
./bin/mcp-electron install --uninstall # remove registrations, skills and the copyinstall copies the binary to ~/.local/bin/mcp-electron (--dest DIR, or
--bin PATH to register an executable in place), registers it with Claude
Code (claude mcp add --scope user) and with Codex when its CLI is on PATH
(--no-codex to skip), replacing an existing entry, and writes the skill to
~/.claude/skills/mcp-electron and ~/.codex/skills/mcp-electron.
--uninstall reverses all of it and verifies with claude mcp get /
codex mcp get that the entry is gone. --dry-run applies to both and never
writes, copies, removes or registers anything.
Manual registration: run mcp-electron mcp over stdio. Flags: --artifacts DIR for screenshots and flow outputs (default
~/Library/Caches/mcp-electron/sessions), --max-sessions N,
--idle-timeout D (see below). stop deletes the session's artifacts
(keep_artifacts: true keeps them), and a starting server removes session
directories left there for more than 24 hours by a server that died.
Every session is a whole Electron process tree (main, GPU, renderers, helpers), so the server keeps them bounded:
- Session cap: 3 open sessions by default (
--max-sessions);launchandattachrefuse beyond it and name the sessions to stop. - Idle stop: a launched app with no tool or flow access for 30 minutes
(
--idle-timeout,0disables) is stopped and the reason is logged. Attached apps are never stopped. - Teardown:
stop, the idle timer, and the server's own exit (stdin closed, SIGTERM) terminate the full process tree top-down (SIGTERM, then SIGKILL after 10 s), including helpers that re-parented themselves, and delete the private profile. - Measurement:
statusreports each session's live process count, resident memory and CPU share (onepsscan, at most once per second) and warns above 2 GB. - Bounded records: the descendant record forgets processes that exited.
- Bounded buffers: process output rings (3000 / 5000 lines, lines cut at
a fixed width), main-process console, per-window observer history (1000
events), per-window request table (2000 entries, oldest finished dropped),
logsanswers (2000 entries max). - Bounded captures: full-page screenshots refuse above 32 Mpx; use the viewport, an element, or scroll in steps.
- Light polling: process tree rescanned every 5 s, debugging endpoint probed every 3 s while healthy (every second while restarting), waits poll the DOM at the engine's intervals and stop as soon as the calling tool's context is cancelled.
- Tests: the engine suite launches one headless Chrome for the whole package; the Escape end-to-end test is opt-in.
| Group | Tools |
|---|---|
| Session | launch, attach, status, windows, logs, stop |
| Page | snapshot, click, type, fill_form, select, press, hover, drag, scroll, upload, wait_for, screenshot, eval, eval_main, navigate, reload, dialog |
| Flows | run_flow |
Full reference: plugin/skills/mcp-electron/references/tools.md. Agent-facing procedure: plugin/skills/mcp-electron/SKILL.md.
@N refs from the last snapshot, a unique CSS selector, or
getByRole("button", { name: "Save" }), getByText("Save"),
getByLabel("Email"). Names and texts may be regexes
(getByRole("button", { name: /^Mode : / })) or exact (exact: true).
getByText keeps the innermost matching element and prefers an exact match,
so a button wrapped in nested divs is still unique. Actions wait for the
target to be visible and enabled, then return a diff of the interactive
elements; click first hovers a per-row button that only shows on :hover
(hover_first). wait_for with state: hidden holds when no element
matching the target is visible.
- Project directory (
package.json+node_modules/electron): runs the local Electron binary with.as the app. The app gets a private--user-data-dirunder/tmpand--remote-debugging-port/--inspecton free loopback ports. .appbundle (must embedElectron Framework.framework) or a bare executable.- Dev command (
command: ["bun","run","dev"]): the server exportsMCP_ELECTRON_CDP_PORTandMCP_ELECTRON_INSPECT_PORT; the dev script must pass them to Electron, e.g.electron --remote-debugging-port=$MCP_ELECTRON_CDP_PORT --inspect=127.0.0.1:$MCP_ELECTRON_INSPECT_PORT .${MCP_ELECTRON_CDP_PORT}also expands insidecommandandenv. When the dev loop restarts Electron, the session reportsrestarting, then re-attaches on its own;statusshows the restart count. - Attach:
attachwithhttp://127.0.0.1:PORTof an app already started with--remote-debugging-port. Attached apps are never killed.
Apps that relocate their own data directory need their variable in env
(Escape: ESCAPE_TEST_DATA).
Maestro syntax, desktop subset: tapOn, doubleTapOn, inputText,
eraseText, pressKey, assertVisible, assertNotVisible, scroll,
scrollUntilVisible, waitForAnimationToEnd, extendedWaitUntil,
takeScreenshot, evalJs, assertJs, evalMain, assertMain,
repeat, runFlow. Selectors: text, role + text, label, id,
css, ref. See plugin/skills/mcp-electron/references/flows.md
and examples/flows/escape-create-agent.yaml.
CI:
mcp-electron run-flow examples/flows/escape-create-agent.yaml \
--target ~/Documents/lab/sandbox/escape --env ESCAPE_TEST_DATA=$(mktemp -d /tmp/esc-XXXX)go test -short ./... # unit tests (no browser)
go test ./... # engine tests launch a headless system Chrome
MCP_ELECTRON_E2E_TARGET=~/Documents/lab/sandbox/escape \
MCP_ELECTRON_E2E_ENV="ESCAPE_TEST_DATA=@tmp" \
MCP_ELECTRON_E2E_FLOW=$PWD/examples/flows/escape-create-agent.yaml \
go test -count=1 -v -run 'TestEndToEndElectron|TestE2E' ./internal/mcp/ ./internal/electron/ ./cmd/mcp-electron/The end-to-end scenarios open the app on screen and cover: the tool loop and
a flow (internal/mcp), attach leaving the app alive on stop, a hard
crash of the main process (state exited, helpers and profile cleaned), a
dev loop that restarts Electron (state restarting, automatic re-attach,
restart counted), a second window driven by its own index, and the server
stopping its apps when the host closes stdin (cmd/mcp-electron). @tmp
in the env gives each scenario a fresh data directory.
Layout: internal/engine (renderer engine from ghostchrome, attach-only
Browser), internal/electron (sessions, process tree, inspector),
internal/mcp (tools), internal/flow (YAML flows), cmd/mcp-electron
(CLI), plugin/ (Claude Code plugin: manifest, .mcp.json, and the skill,
also embedded in the binary by plugin/skills), .claude-plugin/ (the
marketplace), npm/mcp-electron (npm launcher, unpublished), scripts/release.ts.
The marketplace (.claude-plugin/marketplace.json on main) serves the
plugin from the dist branch, which the release workflow replaces on every
tag with a single commit: plugin/ (manifest, .mcp.json, skill) plus
bin/mcp-electron (universal macOS binary, assembled by the script since
Linux has no lipo) and bin/mcp-electron.exe (Windows x64, which Windows on
arm64 runs too). .mcp.json starts ${CLAUDE_PLUGIN_ROOT}/bin/mcp-electron
directly: no Node, no npx, no registry, and no secret beyond the workflow's
own GITHUB_TOKEN.
bun scripts/release.ts version 0.2.0 # plugin.json (+ the dormant npm package.json)
git commit -am "release 0.2.0" && git tag v0.2.0
git push origin main v0.2.0 # release workflow: test, build, GitHub release, dist branchThe dist branch is generated: never commit to it. It moves only once the
build and the GitHub release succeeded, and re-running a release replaces it
again. To try a plugin build locally: bun scripts/release.ts build 0.2.0 && bun scripts/release.ts plugin 0.2.0, then claude --plugin-dir dist/plugin.
An npm distribution (npm/mcp-electron: Node launcher plus one package per
platform) is ready but not published; bun scripts/release.ts stage and
publish (with NPM_CONFIG_TOKEN) would ship it.
CI runs on a single ubuntu-latest job (no macOS or Windows runners): the
binaries are pure Go and cross-compile in parallel in a few seconds,
setup-go caches modules and GOCACHE (test results included), and a newer
push cancels the running check. The end-to-end suites, which need a display
and Electron, stay local.