electromcp
v2.0.1
Published
Drive any Electron app from your AI. MCP server with 93 tools, 4 recipe packages (Slack/Discord/Notion/Linear), security primitives, and a CLI mirror. The testing surface area Tauri structurally cannot match.
Maintainers
Readme
ElectroMCP
Drive any Electron app from your AI. One install.

What you can do Monday morning
- Bulk-invite a cohort to Slack and send welcome DMs in 60 seconds — instead of 50 minutes of manual clicking
- Ship Friday-afternoon Electron features without dread, because tests stop hanging on dialogs you forgot to mock
- Drive your already-running Slack / Discord / Notion / Linear / VS Code from Claude or Cursor — no API tokens, no OAuth, no code changes to those apps
- Snapshot every screen of your app with one prompt — instead of 30 manual screenshots
- Reproduce the IPC bug from yesterday's trace by replaying the recorded sequence
These aren't demos. They're the actual workflows ElectroMCP was built for.
Quick start (5 tools you'll use 80% of the time)
claude mcp add electromcp -- npx electromcpThen in any AI conversation:
| Tool | What it does | When to use it |
|---|---|---|
| app_launch { stubMode: 'auto' } | Zero-config launch + auto-stub every native dialog/menu/tray/notification | Start of every test session |
| app_attach_by_name { name: "Slack" } | Connect to a running app over CDP | Power-user / consumer-app flows |
| app_vision_read | Returns app summary + screenshot + a11y + main-state in one call | "What is my app showing right now?" |
| electron_main_state | Full nervous system of your app (windows, IPC, menu, tray) in one call | Debugging or pre-flight |
| slack_bulk_invite { csvPath, channels } | Onboard a cohort with welcome DMs | Mondays |
That's the 80%. The other 88 tools are there when you need them.
Magic moments
"I forgot to mock that dialog"
Old way: test hangs forever, you Ctrl-C, dig through stack trace, add mockDialog, re-run.
ElectroMCP: app_launch { stubMode: 'auto' } — every native dialog / menu / tray / notification / shell.openExternal is stubbed at boot. Tests never hang again.
"Which workspace am I even in"
Old way: open Slack, scroll the workspace switcher, count.
ElectroMCP: prompt Claude with slack_list_workspaces — back in 200ms.
"Is my main process actually ready"
Old way: add console.log, restart app, scroll terminal.
ElectroMCP: electron_main_state returns BrowserWindows + ipcMain handlers + menu tree + Tray instances + locale + theme in one call.
"What did the AI just do to my Slack"
Old way: scroll Slack manually, hope nothing weird happened.
ElectroMCP: every recipe call writes to ~/.electromcp/audit.log with timestamp, tool, args, return summary. Receipts.
Three things in one MCP server
Automate any Electron app you didn't write
app_attach_by_namefinds running Electron processes viaps(Slack,Discord,Cursor,Notion,Linear, …) and attaches over CDP- Multi-window, multi-instance, multi-session aware
- BrowserView shim that Playwright structurally cannot match
Pre-built recipes for popular Electron apps
@electromcp/recipes-slack,-discord,-notion,-linear(TradingView lands in v1.1; VS Code + Cursor in v1.2)- Security model every recipe inherits:
auth_scopeallowlist (default empty), PII redaction default, append-only audit log, readonly-by-default, token-leak regression test in CI
Deep dev tooling for your own Electron app
electron_main_state— full nervous system in one call- Native shell auto-stub at one toggle
- IPC observability (
listen,mock,replay,assert) - Auto-update simulation, deep-link harness, perf gating, per-OS visual regression
The testing surface area Tauri structurally cannot match.
Why now
- MCP just hit critical mass — Claude Desktop, Cursor, Codex, Gemini, Copilot all speak it
- Tauri is taking marketing oxygen from Electron — the testability moat needs a flagship demonstration
- The recipe pattern is unclaimed — first mover wins the convention
Install
claude mcp add electromcp -- npx electromcpFor Claude Desktop / Cursor / Windsurf / Copilot / Gemini configs, the command is the same (npx electromcp); only the config file path differs per AI tool.
Feature matrix
| Capability | ElectroMCP v2.0 | Tauri | WDIO Electron | Spectron | kanishka MCP | halilural MCP |
|---|---|---|---|---|---|---|
| Zero-config launch (6 archetypes) | ✓ | — | partial (Forge/builder only) | — | — | — |
| Native shell auto-stub (one toggle) | ✓ | — | — (per-call manual) | — | — | — |
| Mock substrate (vitest-spy ergonomics) | ✓ | — | ✓ | — | — | — |
| electron_main_state (one-call snapshot) | ✓ | — | — | partial | — | — |
| IPC live observability (listen/traffic/replay) | ✓ | — | mock only | — | — | — |
| Auto-update simulation (fake server + scenarios) | ✓ | — | — | — | — | — |
| Deep-link harness (warm + cold start) | ✓ | — | — | — | — | — |
| Perf gating + assertions | ✓ | — | — | — | — | — |
| Visual regression (per-OS baselines + auto-mask) | ✓ | partial | — | — | — | partial |
| Multi-session / partition introspection | ✓ | — | — | — | — | — |
| BrowserView shim (raw CDP) | ✓ | n/a | — | — | — | — |
| Process-name attach (Slack, Discord) | ✓ | — | — | — | — | — |
| Recipe packages (Slack/Discord/Notion/Linear) | ✓ | — | — | — | — | — |
| Security primitives (auth_scope + PII + audit + readonly) — recipes only [^sec] | ✓ | — | — | — | — | partial |
| macOS testing | ✓ | broken (no WKWebView WebDriver) | ✓ | ✓ (archived) | ✓ | ✓ |
| Codegen export | ✓ | — | — | — | ✓ | — |
[^sec]: Core tools do not auto-enforce auth_scope / PII redaction / audit log / readonly mode. These primitives are exposed via the electromcp/security subpath export and are wired automatically into every recipe via @electromcp/recipes-template. To apply them to core tool outputs, redact in your AI prompt or call redactObject in user-side post-processing.
Why not Tauri?
Tauri is a great choice for some apps. But the testing story is structurally broken in ways ElectroMCP exposes:
- No WKWebView WebDriver on macOS.
tauri-driverworks on Windows and Linux. macOS testing is third-party hacks against Apple's closed-source webview. ElectroMCP runs the same way on every platform. - No main-process introspection. Tauri's webview-only architecture means there is no equivalent to
electron_main_state— no live BrowserWindow registry, no ipcMain handler list, no menu tree, no Tray instance dump. Your testing tool can only see what's inside the webview. - No native shell stub surface. Tauri's native dialogs/menus/notifications are Rust-side. There's no construction-site interception model like ElectroMCP's
electron_stub_*. Every native call hangs your test or hits the real OS.
If you stay on Electron, you keep the moat. ElectroMCP is the moat.
Tools
App lifecycle (5)
| Tool | Purpose |
|---|---|
| app_launch | Launch with zero-config archetype detection. Optional stubMode: 'auto', preset, instanceId. |
| app_connect | Attach to a running app on port or full url. |
| app_disconnect | Disconnect without killing the app. |
| app_stop | Shut down and disconnect. |
| app_health | Ping; auto-reconnect if the page is dead. |
Visual + interaction (12)
| Tool | Purpose |
|---|---|
| ui_screenshot | Full screenshot of the active window. |
| ui_accessibility_snapshot | Full a11y tree — cheaper than a screenshot. |
| ui_get_dom | Raw HTML of the page or an element. |
| ui_observe | Visual state with context for AI reasoning. |
| ui_click | Click by selector or visible text. |
| ui_click_text | a11y-role+name first, text fallback — the semantic locator. |
| ui_fill | Set an input value instantly. |
| ui_type | Type character-by-character. |
| ui_press | Press a keyboard chord. |
| ui_hover | Hover (tooltips, hover states). |
| ui_scroll | Scroll page or element in any direction. |
| ui_wait_for | Wait for selector to appear / disappear / become visible. |
| ui_get_element | Attributes, text, bbox, visibility, enabled state. |
| ui_act | Returns a screenshot so the AI picks the next action. |
| ui_screenshot_match | Per-OS visual baseline diff. Auto-masks titlebar / traffic lights. |
| app_screenshot_match_docker | Optional Docker-normalized baseline (cross-platform-stable). |
Windows (3)
| Tool | Purpose |
|---|---|
| window_list | Every open window: title, URL, dimensions, focus. |
| window_switch | Switch by title substring or index. |
| window_get_by_title | First window matching a title substring. |
Debug (5)
| Tool | Purpose |
|---|---|
| app_console_logs | Renderer console output since launch. |
| app_evaluate_renderer | Run JS in the renderer; get the return. |
| app_evaluate_main | Run JS in main. Gated by ALLOW_MAIN_EVAL=1 (or allowMainEval: true settings). |
| app_crash_status | Renderer crash check. |
| app_crash_reset | Clear the crash flag. |
Test runner (4)
| Tool | Purpose |
|---|---|
| test_run | Shell out to any test command, parse pass/fail. |
| test_run_file | Run a single test file. |
| test_run_grep | Run tests matching a name pattern. |
| test_get_results | Parsed results from the last run. |
Recording + planning (5)
| Tool | Purpose |
|---|---|
| batch_run | Plan a multi-step sequence; AI executes one tool at a time. |
| app_record_har | Start HAR network recording. |
| app_stop_har | Flush + save HAR. |
| app_record_video | Start .webm video recording. |
| app_stop_video | Flush + save video. |
Electron sugar (10)
| Tool | Purpose |
|---|---|
| electron_ipc_send | Fire-and-forget IPC from renderer (contextBridge or nodeIntegration fallback). |
| electron_ipc_invoke_main | Invoke an ipcMain.handle handler from MCP and capture the return. |
| electron_ipc_invoke_renderer | Drive ipcRenderer.invoke and capture the return. |
| electron_main_state | One-call structured snapshot: BrowserWindows, ipcMain handlers, menu tree, Tray instances, app event log, locale, theme, isReady. |
| electron_app_get_path | app.getPath(name) — userData, logs, downloads, etc. |
| electron_app_get_metrics | getAppMetrics() + getProcessMemoryInfo() per process. |
| electron_clipboard_read | Read system clipboard. |
| electron_clipboard_write | Write system clipboard. |
| electron_shell_open_external | shell.openExternal with stub: 'capture' \| 'noop' \| 'real'. |
| electron_on_crash | Wait for renderer crash or timeout. |
Native shell auto-stub (8)
| Tool | Purpose |
|---|---|
| electron_stub_dialog | Stub native OS dialogs by type and matchers (title/buttons). |
| electron_stub_menu | Capture Menu.setApplicationMenu / buildFromTemplate template tree. |
| menu_click | Walk captured menu tree and synthesize a click on "File > Save". |
| electron_stub_tray | Capture every new Tray() instance + listeners. |
| tray_click | Synthesize click / right-click / double-click on a captured Tray. |
| electron_stub_notification | Intercept new Notification(); capture title/body/silent/icon. |
| notification_assert | Assert against the captured notification registry. |
| electron_stub_shell_open_external | Capture shell.openExternal URLs without spawning a browser. |
app_launch({ stubMode: 'auto' }) toggles all of the above with sane defaults at boot.
IPC observability (7)
| Tool | Purpose |
|---|---|
| electron_ipc_listen | Start recording all ipcMain.handle / ipcRenderer.send traffic. |
| electron_ipc_traffic | Return recorded events with timestamps + serialization diagnostics. |
| electron_ipc_mock | Vitest-style handler mock — override ipcMain.handle returns. |
| electron_ipc_assert | Pass/fail matcher: { channel, calledWith?, calledTimes? }. |
| electron_ipc_replay | Replay a recorded trace to reproduce a bug from prod. |
Auto-update harness (5)
| Tool | Purpose |
|---|---|
| app_update_server_start | Fake update server: serves latest.yml (electron-updater) and RELEASES (Squirrel.Windows). |
| app_update_server_stop | Stop the fake server. |
| app_update_set_scenario | Pre-arm: available / downloading / downloaded / corrupt / signature_mismatch / partial_download / rollback. |
| app_update_simulate | Fire the armed scenario into the running app's electron-updater listeners. |
| app_update_assert_called | Assert the updater hit your fake server with expected url/headers/channel/times. |
| app_squirrel_simulate | Relaunch with --squirrel-firstrun / install / update / uninstall argv. |
Deep-link harness (5)
| Tool | Purpose |
|---|---|
| app_deeplink_register | Work around setAsDefaultProtocolClient dev-mode broken-ness on macOS/Linux/Win. |
| app_deeplink_unregister | Reverse the registration. |
| app_deeplink_fire | Warm-start: inject via app.emit('open-url') / 'second-instance'. |
| app_deeplink_simulate_cold_start | Cold-start: relaunch with URL prepended to argv. |
| app_deeplink_assert | Assert scheme/path/params were observed by the app. |
Perf (4)
| Tool | Purpose |
|---|---|
| app_perf_snapshot | Startup time + per-process metrics + RSS/heap + recent FPS sample. |
| app_perf_trace_start | Wraps contentTracing.startRecording with named scenarios. |
| app_perf_trace_stop | Stop and return the trace JSON path. |
| app_perf_assert | expect(startup).toBeUnder(1500)-style matcher with stable-baseline hashing. |
Multi-session (4)
| Tool | Purpose |
|---|---|
| app_partitions | List named sessions with metadata. |
| partition_snapshot | Cookies + localStorage + IndexedDB + serviceWorker registrations. |
| partition_assert_isolated | Assert zero key overlap between two partitions. |
| partition_clear | Bulk reset a partition. |
BrowserView + process attach (4)
| Tool | Purpose |
|---|---|
| browserview_list | List BrowserViews via raw CDP (Playwright doesn't support them). |
| browserview_evaluate | Run JS inside a BrowserView. |
| browserview_screenshot | Screenshot a BrowserView. |
| app_attach_by_name | Find a running Electron process via ps (Slack, Discord, …) and attach over CDP. |
Environment overrides (4)
| Tool | Purpose |
|---|---|
| app_set_geolocation | Override geolocation. |
| app_set_locale | Override locale. |
| app_set_timezone | Override timezone. |
| app_set_color_scheme | Override prefers-color-scheme. |
| app_network_emulate | CDP Network.emulateNetworkConditions: offline / slow-3g / fast-3g / custom. |
Codegen export (3)
| Tool | Purpose |
|---|---|
| app_codegen_start | Begin recording every tool call with its args. |
| app_codegen_stop | Stop recording. |
| app_codegen_export | Output as runnable Playwright .spec.ts or replayable MCP transcript. |
Recipes
ElectroMCP ships four recipe packages — each one attaches to your real installed Electron app and exposes a small, scoped, secure tool surface:
@electromcp/recipes-slack—slack_read_channel,slack_send_message,slack_search,slack_react@electromcp/recipes-discord—discord_read_channel,discord_send,discord_dm@electromcp/recipes-notion—notion_search,notion_create_page,notion_update_db_row,notion_export_db@electromcp/recipes-linear—linear_create_issue,linear_comment,linear_move_issue,linear_triage_inbox
Security model (every recipe inherits)
auth_scopeallowlist — explicit list of channels / workspaces / accounts the agent may touch. Default empty → no access until granted.- PII redaction by default — email / phone / credit-card patterns are scrubbed from returned text unless
pii: 'allow'is set per-call. - Append-only audit log at
~/.electromcp/audit.log— every recipe call writes{ timestamp, tool, redacted args, return summary }. You can review what the agent saw and sent. - Read-only by default —
mode: 'readonly'ships on every recipe. Write actions (*_send_*,*_post_*,*_create_*) require explicitmode: 'write'. - Token-leak assertion — the test suite asserts no recipe tool ever returns a cookie / bearer / OAuth token. CI fails on regression.
Selectors are pinned per app version via @electromcp/selector-tools (capture, pin, validate). When the target app upgrades, the recipe loads the closest matching selector file and warns on fallback. Auto-PR'd selector regen is aspirational; manual capture is the v2.0 path.
Migration
- From WebdriverIO Electron service: see
docs/migration-from-wdio.md. - From Spectron (archived Feb 2022): see
docs/migration-from-spectron.md.
Recommended flow
1. app_health confirm connection
2. ui_accessibility_snapshot cheap structure
3. ui_screenshot only if visuals matter
4. ui_click / ui_fill / ... interact
5. electron_main_state verify main-side stateFor debugging:
1. app_console_logs renderer output
2. app_evaluate_renderer live DOM / variables
3. electron_ipc_traffic what crossed the bridgeEnvironment variables
| Variable | Default | Description |
|---|---|---|
| ALLOW_MAIN_EVAL | unset | Set to 1 to enable app_evaluate_main AND electron_ipc_mock's implementation parameter (both compile user-supplied JS into the Electron main process). |
Limitations
Surfaced honestly so you know what you're buying. Several of these are tracked for v2.1.
- PII redaction is recipe-side only. Core tools (
ui_accessibility_snapshot,electron_main_state,electron_ipc_traffic, etc.) return raw text and state. If your app's accessibility tree, IPC traffic, or main-state contains emails / phone numbers / tokens / credit-card patterns, those will surface unredacted in tool returns. Recipe packages (@electromcp/recipes-*) auto-redact via the recipe template. For core tools, redact in your AI prompt or callredactObjectfromelectromcp/securityin user-side post-processing. electron_ipc_sendsends from the renderer. If your app usescontextIsolation: truewithout a contextBridge-exposedipcRendererand withoutnodeIntegration: true, the call fails with a clear error.electron_ipc_listenis not "everything that crosses the bridge". Renderer → mainipcRenderer.sendtraffic is only observable when the app actually registered anipcMain.onhandler for that channel; sends to channels with no handler are silently dropped by Electron itself and we cannot see them. Main → rendererwebContents.sendis not observed at all in v2.0 — Electron offers no public hook in the main process to intercept outgoing sends without monkey-patching everywebContentsinstance. Tracked for v2.1.app_update_assert_calledblockmap is not real electron-builder wire format. The fake update server emits an uncompressed JSON description of block hashes, not the actual.blockmapbinary format. Strict real electron-updater clients will reject it. The harness is sufficient for testing your app's update event handlers; it is not a full blockmap fuzzer. Tracked for v2.1.app_deeplink_assertrequires fixture cooperation. It reads fromglobalThis.__deeplink__.eventswhich the target app must populate (typically via a small instrumentation snippet at boot). The cold-start variant is fully turnkey; warm-start assertions need the snippet. Tracked for v2.1.app_screenshot_match_dockerdoes not run Electron in Docker. It screenshots an HTTP URL inside a pinned Playwright Chromium container so baselines don't drift across host OS / GPU / fonts. Useful for comparing renderer output against a normalized reference; does not exercise main-process or native-shell behavior. The host-Electron path (ui_screenshot_match) is what you want for full-app regression. Tracked for v2.1 (Electron-in-Docker is non-trivial because Electron needs root + display + sandbox bypass).partition_snapshotis partial. Partitions not bound to an openBrowserWindoware invisible (Electron has no public partition-enum API).localStorageis only walked for partitions with at least one open window.IndexedDBreturns aggregate usage, not per-DB enumeration.serviceWorkersis[]on Electron <28. The exported helpers' JSDoc spells this out per-method.- HAR / video recording require launch mode (cannot be added to an existing CDP session).
app_evaluate_mainis launch mode only. It requires eitherALLOW_MAIN_EVAL=1in the environment OR a preset that grants it (preset: 'permissive' | 'dev', orsettings: { allowMainEval: true }on launch).electron_ipc_mock'simplementationparameter compiles arbitrary JS into the main process and is gated by the sameALLOW_MAIN_EVAL/allowMainEvalprimitive asapp_evaluate_main. The staticresponseparameter remains unrestricted. Set the env or preset before passingimplementationstrings, otherwise the call rejects witheval_blocked.- Recipe selectors rot. The 4 recipe packages pin selectors per app version. When the target app upgrades past your pinned version,
loadSelectorswarns at >30 days stale and escalates to "deprecated" at >90 days. The recipe packages'CONTRIBUTING.mddocuments the regen flow. CI auto-PR'd selector capture is aspirational for v2.0 — manual capture is the v2.0 path. - Headless is not officially supported by Electron. On Linux CI:
xvfb-run npm test(ElectroMCP auto-detects + auto-launches xvfb when DISPLAY is unset).
Development
git clone https://gitlab.com/therealseandonahoe/electromcp
cd electromcp
npm install
npm run build # tsc -> build/
npm run dev # tsx src/index.ts
npm test # full suiteContributing
Recipe selectors break weekly. PRs against packages/recipes-*/selectors.<version>.json auto-merge if CI is green. Each recipe has a CONTRIBUTING.md with the selector-regen flow.
License
MIT
