Electron Driver
About
Drive Electron apps from AI agents via MCP - click, type, drag, screenshot, eval JS, and more.
Details
- Author
- mesomya
- Categories
- Developer Tools, Automation
Jump to
Setup
Install Electron Driver in your MCP client (Claude Desktop, Cursor, Windsurf, and others).
Repository: https://github.com/mesomya/electron-driver
Follow the installation instructions in the repository README, then restart your MCP client.
Drive Electron apps from AI agents via MCP - click, type, drag, screenshot, eval JS, and more.
Drive Electron apps from AI agents.Click, type, drag, screenshot, evaluate JavaScript in the renderer or main process, read console logs, handle multi-window apps, capture accessibility snapshots — all through an MCP (Model Context Protocol) server that plugs into Claude Code, Claude Desktop, Cursor, and any other MCP-compatible agent host.
https://github.com/user-attachments/assets/a95500d2-28d2-4ee1-9965-8f7e1ef54caa
Built on Playwright's experimental_electronAPI. Works with any Electron app — React, Vue, Svelte, vanilla — as long as you can point it at a compiled main-process entry.
Status:v0.3.0. First public release. 38 tools covering real workflows.
AI agents can reason about what a desktop app should do, but they can't see or interact with one on their own. Web browsers have plenty of agent-automation options; Electron has almost none. This package closes that gap: give an agent the path to your compiled Electron app and it can drive it the same way a human would.
- An agent verifies a feature it just implemented by actually running the app and checking the visible result
- Visual regression testing during a refactor
- Accessibility audits via ARIA tree snapshots
- Reproducing bugs from a natural-language description
- Teaching a subagent to iterate on UI until a spec passes
Requires Node 18+ and an Electron app you've already built.
You don't need to install Playwright browsers separately —_electrondrives your Electron binary directly.
{ "mcpServers": { "electron-driver": { "command": "npx", "args": ["electron-driver"] } } }
Claude Code (user scope — available in every project)
claude mcp add electron-driver --scope user -- npx electron-driver
Add to the host's MCP configuration, pointing atnpx electron-driveror the absolute path tonode_modules/electron-driver/index.mjs.
The server ownsexactly oneElectron session at a time.start_applaunches it, everything else drives it,stop_appcloses it. Screenshots go to a session directory that is wiped on everystart_app— no pileup, no stale artefacts. All tool calls are logged to<project>/.electron-driver/driver.logduring a session.
Errors carry a stablecodefield so callers can branch programmatically without regex-matching prose:
All 38 tools grouped by purpose. Every selector-based tool uses Playwright's full selector engine: CSS,text=,role=,[aria-label=],:has-text(), scoping (main >> button), etc.
start_app— launch the app. Takesmain(absolute path to the compiled main entry), optionalcwd,args,env,screenshotsDir,timeoutMs. Returns{ title, url, viewport, screenshotsDir, logFile }. Detects the single-instance-lock failure mode and gives a helpful hint instead of a raw disconnection error.
stop_app— close cleanly. Safe on an already-stopped session.
info—{ title, url, viewport: {width, height, devicePixelRatio}, uptimeMs }. Viewport is populated fromwindow.innerWidth/innerHeight.
screenshot— full-page PNG. Passname(without extension) to control the filename. Returns{ path }.
cleanup_screenshots— wipe the current session's screenshot directory.
console_logs— recent renderer console messages (log/info/warn/error/ debug/pageerror) and main-process stdout/stderr. Rolling 1000-entry buffer. Filter bysource(renderer/main/all),type, andlimit. Passclear: trueto drain after reading.
click— click an element. Options:timeoutMs,button(left/right/middle),clickCount,force(skip actionability checks),position(click at an offset inside the element).
type—filla text input, replacing existing content. Fast but only works on real inputs. For editors/CodeMirror/contenteditables, usekeyboard_type.
keyboard_type— type as real per-character keydown events. PassfocusSelectorto click an element first. Warns in the result if nothing has focus and no focus selector was passed.
press— press a key or chord:"Escape","Enter","Control+S","Shift+Tab","Control+Shift+P".
press_sequence— alias forkeyboard_typewith no focus selector.
hover— hover over an element. Options:timeoutMs,force.
drag— drag from one point to another using real Chromium input events (via Playwright's CDP mouse pipeline). Because these are trusted browser events, Chromium's pointer pipeline generates matchingPointerEvents as a side effect, so ReactonPointerDown, nativepointerdownlisteners,setPointerCapture, CSS:hover/:active, and every other pointer consumer see the drag exactly as if a real user had performed it. Coordinates are CSS pixels. PassdetectSelectorand the driver will measure the element before and after the drag and includedetect.movedin the result — the only reliable way to catch drags that silently hit a min/max clamp.
{ "from": { "x": 275, "y": 400 }, "to": { "x": 420, "y": 400 }, "detectSelector": ".sidebar-resize-handle" }
If the primary strategy does not move the detect target, the driver automatically falls back to invoking the React handler directly via fiber-prop access and dispatching move/up events on bothdocumentandwindow— covering all known React splitter patterns. Disable the fallback withfiberFallback: false. The result includesstrategy("pointer-capture"or"react-fiber").
clear_input— empty an input or textarea.
select_option— select from a<select>byvalue,label, orindex.
check— check a checkbox or radio. Options:timeoutMs,force.
scroll— scroll a container (passselector) or the window. Supports absolute (x,y) or delta (dx,dy).
scroll_into_view— ensure an element is visible. Safe if already is.
drop_file— simulate dropping a file onto a target via syntheticDragEvents and a reconstructedFilewithDataTransfer. Works for apps that read the File via web APIs (FileReader,File.text(), etc). Doesnotpopulatefile.path— apps relying onwebUtils.getPathForFile()must useeval_mainto invoke their own IPC handler directly.
set_input_files— the correct way to test file upload UI. Sets files on an<input type="file">without a native dialog. Much more reliable thandrop_filewhen the app uses real file inputs.
wait— fixed pause in milliseconds. Prefer the others.
wait_for_selector— wait until a selector reaches a state (attached/detached/visible/hidden). HonourstimeoutMs. Returnscount,box, andelapsedMson success; the error carrieselapsed vs requestedon timeout.
wait_for— poll a JavaScript predicate (function body, usereturn) until it returns truthy. Options:timeoutMs,pollMs.
exists—{ exists, count }fast check, no waiting. Accepts the full selector engine.
get_text— text content of the first match. Accepts the full selector engine. Returns{ exists, text }.
get_attribute— read an HTML attribute by name. Returns{ exists, value }.
get_value— read an input/textarea/select's current value.
get_bbox— bounding box as{ x, y, width, height }in CSS pixels. Use before dragging or clicking at an offset.
get_computed_style— read one or more computed CSS properties. Pass apropertiesarray.
elements_list— enumerate elements matching a selector with their tag, id, classes, text snippet, box, and key attributes. Great for "what buttons exist on this screen". Capped at 50 by default; tune vialimit.
focused_element— what currently has focus, with tag/id/classes/text and bounding box. Returns{ focused: false }if nothing meaningful has focus.
accessibility_snapshot— capture the ARIA tree as JSON. Useful for a11y audits and finding elements by role. PassinterestingOnly: falseto include every node. Passrootto snapshot a subtree.
windows_list— everyBrowserWindowthe app has open, with id, title, URL, focus/visibility/state flags.
switch_window— route subsequent tool calls to a different window. PassindexortitleMatch.
dialog_handler— install an auto-responder for JavaScript dialogs (alert/confirm/prompt/beforeunload). Passaction: "accept" | "dismiss", optionaltextforprompt(), andonce: true(default) to auto-uninstall after the first dialog.
Botheval_rendererandeval_mainuse thesame contract: pass a function body, usereturnto yield a value, supportsasync/await, and an optionalargpayload is available as the localargvariable.
eval_renderer— evaluate in the renderer (page) context.
{ "js": "return document.querySelectorAll(arg.selector).length", "arg": { "selector": ".item" } }
eval_main— evaluate in the Electron main process. The body receiveselectron(the full Electron module) andarg.
{ "js": "return electron.app.getName()" }
{ "js": "const w = electron.BrowserWindow.getAllWindows()[0]; w.webContents.send('open-file', arg.path); return true", "arg": { "path": "C:/docs/README.md" } }
Useeval_mainas the escape hatch for everything the DOM side can't reach: invoking IPC handlers, reading user paths, driving secondary windows, bypassing native dialogs for apps that use them.
text=Open File // exact text match text=/^Save$/ // regex [aria-label="Settings"] // ARIA attribute role=button[name="Close"] // ARIA role button:has-text("Save") // CSS with text predicate button.primary // plain CSS main >> text=Save // scoped
Every selector-based tool (click,hover,wait_for_selector,get_text,get_attribute,get_value,get_bbox,exists,elements_list,scroll_into_view,select_option,check,uncheck,set_input_files) goes through Playwright's full locator engine. Anything Playwright accepts, these tools accept — includingtext=,role=, and:has-text().
Tools that read low-level DOM viaeval_rendererunder the hood (get_computed_style,scroll,focused_element,drop_file) use nativedocument.querySelectorand only support CSS. This is documented on each tool's description where relevant.
"Electron process exited immediately after launch."Another copy of your app is already running and grabbed the single-instance lock — the second process quits viaapp.requestSingleInstanceLock(). Close the running instance (check your taskbar and background processes) and retry.
dragreturnedok:truebutdetect.movedisfalse.The most common cause is a min/max clamp on the target (e.g. a resizable sidebar at itsMAX_WIDTH). Try dragging in the opposite direction to confirm the drag pipeline is working. If it's really not the clamp, the React fiber fallback should catch it automatically — check thestrategyfield in the result. If even that returnsmoved:false, useeval_rendereroreval_mainto invoke the app's own drag API directly.
typethrows "Element is not an<input>..."You're hitting a button, a div, or a contenteditable. Usekeyboard_typewith afocusSelectorinstead.
clicktimes out on an element that's clearly there.Something is covering it — a modal backdrop, a tooltip, a toast. Useexistsfirst to confirm the count, then tryforce: true, or useeval_rendererto checkgetComputedStyle(el).pointerEvents.
Native dialogs are invisible.Playwright cannot see OS-level file pickers, save dialogs, or system alerts. Useeval_mainto invoke the same IPC handler your UI button uses. For JavaScript dialogs (alert/confirm/prompt), usedialog_handlerto auto-respond.
Development build vs compiled app.This drives thebuiltapp, not dev-server output. Run your build command beforestart_app, and rebuild
- restart the session after source changes.
One session at a time.Callingstart_appwhile a session is running returnsALREADY_RUNNING. Callstop_appfirst.
Logs.Every tool call is logged to<project>/.electron-driver/driver.logwhile a session is active. Useful when debugging why an agent got stuck.
Screenshots location.Defaults to<project>/.electron-driver/screenshots, where<project>is the nearest directory containing.gitorpackage.json. Override viascreenshotsDironstart_app.
This server gives the connected agent full control over an Electron app, including arbitrary code execution in the main process (viaeval_main).The Electron main process has unrestricted Node.js access — filesystem, network, child processes, everything. This is by design: it's what makes the driver powerful enough to drive real apps.
- Only use over stdio(the default). Never expose this server over HTTP, WebSocket, or any network transport. Stdio ties it to the process that spawned it — your local Claude Code or Claude Desktop session.
- Trust the agent.The agent calling these tools can do anything on your machine viaeval_main. Only connect agents you trust.
- Don't use in multi-tenant environments.This is a single-user, local-machine tool. It's not designed for shared servers, CI pipelines with untrusted input, or any context where the caller might be adversarial.
- drop_fileandset_input_filesread local filesand pass their contents to the renderer. The file paths should come from trusted sources.
If you're running this with Claude Code, the risk profile is the same as giving Claude Code terminal access (which you already have). The driver doesn't add new capabilities beyond whatevalin a terminal could do — it just makes them convenient for the agent to use.
- Playwright's_electronnamespace is officially experimental upstream. Occasional launch timeouts on slow machines; usually retrying fixes it.
- Developed primarily on Windows. Mac and Linux should work — Playwright handles them — but are less battle-tested. Bug reports welcome.
- switch_windowroutes subsequent calls to the selected window, but the console-log buffer is populated from the initial window. Multi-window console capture is a planned v0.4 item.
- drop_filedoes not populatefile.path. Apps usingwebUtils.getPathForFile()must useeval_mainwith their own IPC.
- No built-in network-request capture yet — planned for v0.4.
- Single Electron session, owned by the MCP server process.
- Screenshots wiped on everystart_app— intentional.
- Console logs captured into a rolling 1000-entry buffer.
- Every tool call is logged; errors carry a stablecodefield.
- Error messages are rewritten to be attributed to the driver tool, not the underlying Playwright method.
- Single-instance-lock detection keys on "process disconnected within 5s of launch", which is the actual shape of the failure.
- Evals are async-IIFE wrapped, soreturnworks andawaitworks.
- argpayloads are coerced server-side (JSON-parse on strings) to protect against MCP clients that stringify arg fields.
- stderr is used for status messages; stdout is reserved for MCP protocol frames.
Issues and PRs welcome. Run locally with:
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.


