Electron Stagewrightdocs

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 }
    }
  ]
}

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

  1. If electron_plugins is absent, no plugin is loaded. Confirm the package is installed, add the explicit --plugin flag, and restart.
  2. If the plugin is listed but a tool is disabled, apply its disabledReason.target with --allow-eval=main or --allow-eval=renderer, then restart with that narrow grant.
  3. If an enabled tool returns *.UNSUPPORTED, call electron_info after launch or attach and choose a transport whose capability matrix satisfies the plugin requirement.
  4. 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".