Load, configure, and diagnose plugins
Use a plugin when the core Electron-driving workflow needs accessibility audits, visual baselines, traces and replay, IPC, network interception, virtual time, storage, native UI, or packaged-app validation. This how-to shows how to load one, grant its narrowest gate, and inspect what the server enabled.
The core never discovers plugins automatically. Installing a package is not enough: explicitly name
every plugin with --plugin (or pass it to createServer).
Install and load a plugin
Install the core, its launch peers, and the plugin together. For example, load trace support through
npx. First prepare the exact package list in a terminal: install-electron fetches Electron's
binary, whose download would otherwise print to stdout during the first MCP launch, and doctor
confirms the setup. --prefix ~/ makes npx install every listed package even inside a project
that already has one of them:
npx -y --prefix ~/ \
--package @electron-stagewright/core@0.8.1 \
--package @electron-stagewright/plugin-trace@0.5.2 \
--package playwright@1.63.0 \
--package electron@42.11.10 \
install-electron
npx -y --prefix ~/ \
--package @electron-stagewright/core@0.8.1 \
--package @electron-stagewright/plugin-trace@0.5.2 \
--package playwright@1.63.0 \
--package electron@42.11.10 \
electron-stagewright doctor --json --plugin trace
Then use the same packages with the plugin flag:
npx -y --prefix ~/ \
--package @electron-stagewright/core@0.8.1 \
--package @electron-stagewright/plugin-trace@0.5.2 \
--package playwright@1.63.0 \
--package electron@42.11.10 \
electron-stagewright --plugin trace
For an MCP client, preserve the same package and flag order in its args:
[
"-y",
"--prefix",
"~/",
"--package",
"@electron-stagewright/core@0.8.1",
"--package",
"@electron-stagewright/plugin-trace@0.5.2",
"--package",
"playwright@1.63.0",
"--package",
"electron@42.11.10",
"electron-stagewright",
"--plugin",
"trace"
]
Repeat --plugin to load more than one. A plugin may also be a trusted local package or file path;
the server imports only the values you specify and rejects a bad manifest before it exposes tools.
Shipped first-party suffixes are stable aliases, so --plugin production resolves to
@electron-stagewright/plugin-production; full scoped package names remain valid and third-party
specifiers are never rewritten.
Run doctor with the same configuration before serving:
electron-stagewright doctor --json \
--plugin network \
--plugin-config network='{"redactHeaders":["x-api-key"],"redactBodies":true}' \
--tool-profile testing \
--operation-timeout-ms 120000
The server_config check imports the explicitly selected trusted plugin, validates its manifest and
configuration, runs setup against an unconnected server, and immediately runs teardown. It reports
plugin names and option states but never raw config values. A plugin's top-level and setup code
therefore runs during this check under the same trust boundary as normal server startup.
From code, pass imported plugin objects directly:
import { createServer } from '@electron-stagewright/core'
import tracePlugin from '@electron-stagewright/plugin-trace'
const server = await createServer({ plugins: [tracePlugin] })
Configure safely
Each plugin owns a Zod config schema. The CLI receives JSON keyed by the plugin namespace:
electron-stagewright \
--plugin @electron-stagewright/plugin-network \
--plugin-config network='{"redactHeaders":["x-api-key"],"redactBodies":true}'
Visual baselines use two operator-owned absolute roots. Keep accepted baselines under version control and send diagnostic captures elsewhere:
electron-stagewright \
--plugin @electron-stagewright/plugin-visual \
--plugin-config visual='{"baselineDir":"/abs/project/visual-baselines","artifactsDir":"/abs/project/visual-artifacts","fontFingerprint":"linux-ci-fonts-v1"}'
The programmatic equivalent is pluginConfigs:
const server = await createServer({
plugins: [visualPlugin],
pluginConfigs: {
visual: {
baselineDir: '/abs/project/visual-baselines',
artifactsDir: '/abs/project/visual-artifacts',
fontFingerprint: 'linux-ci-fonts-v1',
},
},
})
Configuration is parsed once, cloned, and frozen for that server. Invalid values fail closed with
PLUGIN_CONFIG_INVALID; the details include a Zod path such as $.maxRecords and name the exact
--plugin-config <name>=<json> or pluginConfigs.<name> input to correct. A server with a partial
plugin set never starts.
electron_plugins never returns raw operator configuration. A plugin explicitly allowlists every
top-level field it considers safe. An absent field means only that it is not disclosed — never treat
it as proof that the field is unset or non-sensitive.
Inspect actual availability
After at least one plugin loads, call electron_plugins before launching an app. It needs no session
and returns a deterministic, per-server snapshot:
{
"ok": true,
"plugins": [
{
"name": "storage",
"version": "…",
"state": "enabled",
"tools": [
{ "name": "storage_cookies", "state": "enabled" },
{
"name": "storage_local_get",
"state": "disabled",
"disabledReason": { "kind": "eval_policy_disabled", "target": "renderer" }
}
],
"errorCodes": ["storage.EVAL_REQUIRED", "storage.UNSUPPORTED"],
"requirements": {
"evalTargets": ["renderer"],
"transportCapabilities": ["canAccessStorage", "supportsRendererEval"]
},
"effectiveConfig": { "redactValues": false, "revealValues": false }
}
]
}
state: "enabled"means the plugin passed validation and setup. A failed plugin is never listed.- A tool with
state: "disabled"was intentionally hidden by the eval policy. Restart with the narrow target indisabledReason, such as--allow-eval=renderer. requirementsis the conservative union of gates used by at least one plugin tool. It does not mean every tool requires every listed capability. Capability checks run against the launched or attached transport and return the plugin's documented error code when unsupported.errorCodesare stable namespaced codes the plugin can return. Branch on the code, not prose.effectiveConfigcontains only allowlisted safe fields after defaults and validation.
On a plugin-free server, electron_plugins itself is absent. This is intentional: the core keeps
that manifest lean. Load the required plugin and restart, then inspect this tool again.
Grant the narrowest gate
Loading a plugin does not grant eval or transport powers. Start with no eval and opt into only the target a tool needs:
# IPC instrumentation needs main-process eval.
electron-stagewright --plugin @electron-stagewright/plugin-ipc --allow-eval=main
# Per-key Web Storage and IndexedDB need renderer eval.
electron-stagewright --plugin @electron-stagewright/plugin-storage --allow-eval=renderer
| Plugin | Primary gate or scope |
|---|---|
@electron-stagewright/plugin-a11y |
supportsRendererEval + supportsSurfaceTargeting; fixed engine, no --allow-eval; audit one selected surface at a time. |
@electron-stagewright/plugin-visual |
supportsRendererEval + supportsSurfaceTargeting; fixed BrowserWindow capture preparation, no --allow-eval; configure baseline and artifact roots before use. CDP sessions report page roots as other, so capture needs a Playwright launch. |
@electron-stagewright/plugin-trace |
No eval; records, promotes, and replays sessions. |
@electron-stagewright/plugin-production |
No running session; validates a packaged macOS .app. |
@electron-stagewright/plugin-ipc |
--allow-eval=main and supportsMainEval. |
@electron-stagewright/plugin-network |
canIntercept; no eval. |
@electron-stagewright/plugin-clock |
canControlClock; no eval. |
@electron-stagewright/plugin-storage |
Cookies, snapshots and per-key Web Storage need canAccessStorage; IndexedDB additionally needs renderer eval and supportsRendererEval. |
@electron-stagewright/plugin-native-ui |
canAccessNativeUI; no eval. |
Read the security model before enabling eval or a plugin that reads sensitive
app state. See TOOL-REFERENCE.md for full namespaced tool schemas.
Troubleshoot a missing tool
- If
electron_pluginsis absent, no plugin is loaded. Confirm the package is installed, add the explicit--pluginflag, and restart. - If the plugin is listed but a tool is disabled, apply its
disabledReason.targetwith--allow-eval=mainor--allow-eval=renderer, then restart with that narrow grant. - If an enabled tool returns
*.UNSUPPORTED, callelectron_infoafter launch or attach and choose a transport whose capability matrix satisfies the plugin requirement. - If startup reports
PLUGIN_CONFIG_INVALID, correct the reported field path in the named config input. Do not remove a security setting merely to make startup succeed.
For trace workflows, continue with Capture diagnostics. For trust boundaries, read the security model.
Cancellation in plugin handlers
The dispatcher supplies ctx.signal for client cancellation and operation timeouts. Check
ctx.signal?.throwIfAborted() before starting another step after an await, and pass the signal
to APIs that support it. ctx.dispatch(...) inherits the current request's cancellation.
Register cleanup for resources acquired by the current call with ctx.onCancel?.(cleanup).
It returns an unregister function; successful dispatch completion discards pending callbacks.
A callback registered after cancellation runs immediately. Cleanup must be idempotent and must
preserve applications the request did not launch. Its failure is logged without delaying the
cancelled response. Do not rely on cancellation to reverse mutations or interrupt JavaScript
already sent to Electron; completed nested calls are not rolled back.
Generated evidence and output schemas
ctx.artifacts?.publish(bytes, mimeType, name) accepts generated bytes and returns additive
artifact metadata or an artifact_unavailable reason. It accepts PNG, JPEG, JSONL
(application/x-ndjson), JSON and HTML; no file paths or readers are accepted. Check cancellation
before publishing. maxArtifactBytes lets a streaming producer stop buffering a portable copy
while continuing its on-disk artifact. Use artifactOutputFields from the plugin SDK in the
success schema when returning publication metadata.
A tool's optional outputSchema: z.object({...}) describes its success payload without ok and
_meta. The dispatcher adds the standard success/error envelopes, validates the complete result,
and retains both structured content and legacy JSON text regardless of response size. Omit this
property until the tool's success contract is specified accurately; an invalid declared result
becomes INTERNAL_ERROR.
Results must be JSON-serialisable. The dispatcher validates the serialized representation before
recording completion; circular values, BigInt and throwing serialization hooks return
INTERNAL_ERROR. Additive JSON fields remain compatible. Dispatch observers receive completion
records for diagnostics and must treat those records as read-only.
The actual JSON must satisfy the advertised schema without coercion or field removal. Optional fields
may be omitted even when they have a default annotation; required fields must be constructed by the
handler. Explicitly allow nested additional fields with a loose object when those fields are part of
the contract. Custom tools that previously depended on Zod accepting a wrong wire type or stripping
forbidden nested fields now receive INTERNAL_ERROR for those invalid results.
Publication is owned by the current dispatch rather than by the URI fields in its result. A returned
error, thrown error, schema violation or serialization failure releases every portable snapshot
created by that dispatch. A valid success commits them. This never deletes local artifacts, another
request's snapshots or a completed nested call's snapshots. Do not retain a request's publisher for
background work: after completion it returns artifact_unavailable: "closed".