Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
5 changes: 5 additions & 0 deletions .changeset/initial-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nebula-agents/electron-mcp": minor
---

Initial prerelease of the embedded Electron MCP server.
24 changes: 24 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Bug Report
description: Report a reproducible problem.
title: "bug: "
labels: ["bug"]
body:
- type: textarea
id: summary
attributes:
label: Summary
validations:
required: true
- type: textarea
id: repro
attributes:
label: Reproduction
description: Include a minimal Electron setup or failing test when possible.
validations:
required: true
- type: input
id: version
attributes:
label: Version
validations:
required: true
17 changes: 17 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Feature Request
description: Propose a focused change.
title: "feat: "
labels: ["enhancement"]
body:
- type: textarea
id: problem
attributes:
label: Problem
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposal
validations:
required: true
6 changes: 6 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Summary

## Verification

- [ ] `pnpm check`
- [ ] `pnpm test:electron` if Electron/CDP behavior changed
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm check

electron-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps chromium
- run: xvfb-run -a pnpm test:electron
31 changes: 31 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Release

on:
push:
branches: [main]

concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
cache: pnpm
registry-url: https://registry.npmjs.org
- run: pnpm install --frozen-lockfile
- run: pnpm check
- uses: changesets/action@ce079ea084e08a340947ed4d6ecedb2433c8f293 # v1
with:
publish: pnpm changeset publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ pnpm-debug.log*
coverage/
.nyc_output/
test-results/
playwright-report/

# Misc
.cache/
Expand Down
6 changes: 6 additions & 0 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Code of Conduct

This project follows the Contributor Covenant Code of Conduct, version 2.1:
https://www.contributor-covenant.org/version/2/1/code_of_conduct/

Report conduct concerns to `security@agent-labs.dev`.
39 changes: 39 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Contributing

Thanks for considering a contribution. This package is maintained best-effort;
small, focused PRs are easiest to review.

## Setup

```sh
pnpm install
pnpm check
```

## Development

- Source lives in `src/`.
- Unit tests are colocated as `*.test.ts` and are the only files vitest
picks up (see `vitest.config.ts`).
- The canonical Electron smoke test is `test/electron/smoke.spec.ts`,
driven by `pnpm test:electron`. CI runs this on Linux.
- `tests/smoke.electron.test.ts` is an alternate exploratory fixture
that talks to a CJS Electron main via stdout and is **not** wired
into vitest, playwright, or CI; it's kept for local experimentation
only and may be removed in a follow-up.
- Run `pnpm test` for unit tests.
- Run `pnpm test:electron` for the Electron smoke test.
Comment thread
jnsdls marked this conversation as resolved.

## Changesets

User-visible changes need a changeset:

```sh
pnpm changeset
```

Pick `patch`, `minor`, or `major` according to semver. For `0.x`, breaking API
changes generally use `minor`.

Releases are handled by the Changesets release workflow. Do not publish from a
local machine unless a maintainer explicitly coordinates it.
134 changes: 120 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,139 @@
# `@nebula-agents/electron-mcp`

> Embedded MCP (Model Context Protocol) server for Electron apps — drive your renderers via Chrome DevTools Protocol.
Embedded MCP server for Electron apps. It runs inside your Electron main
process, exposes the `BrowserWindow` surfaces you choose, and lets MCP clients
drive those renderers through Chrome DevTools Protocol.

**Status:** 🚧 Pre-release. The implementation lives in [`agent-labs-dev/nebula-desktop`](https://github.com/agent-labs-dev/nebula-desktop) and is being extracted here. See [PRD #149](https://github.com/agent-labs-dev/nebula-desktop/issues/149) for the migration plan.
## Embedded vs External Attach

## What this is
This package is for apps that want MCP automation as an opt-in feature of the
app itself. Your main process decides when the server starts, which windows are
reachable, and which custom tools are registered.

A library for Electron app developers who want **first-class MCP integration as an opt-in feature of their own app**. Unlike external-attach approaches (which speak to Electron over its remote debugging port), this package runs the MCP server _inside_ your main process, giving you:
External-attach servers connect to an already-running Electron app through a
remote debugging port. That can be useful for local debugging, but it does not
let the app own surface naming, gating, or app-specific tools. If you want that
external model, this package is probably not the right fit.

- Full control over which surfaces (windows) are exposed
- Custom tool registration via `addTool()` — expose your app's domain commands alongside the bundled Electron primitives (screenshot, click, type, eval, DOM/AX queries, …)
- Explicit `start()` / `stop()` lifecycle so you decide when the server runs

## Planned API
## Quickstart

```ts
import { createElectronMcpServer } from '@nebula-agents/electron-mcp';
import { app, BrowserWindow } from "electron";
import { createElectronMcpServer } from "@nebula-agents/electron-mcp";

let mainWindow: BrowserWindow | null = null;

const mcp = createElectronMcpServer({
getSurfaces: () => ({ main: mainWindow }),
});

app.whenReady().then(async () => {
mainWindow = new BrowserWindow();

if (recommendedGuards()) {
await mcp.start();
console.log(`MCP listening at ${mcp.url}`);
}
});

app.on("before-quit", () => {
void mcp.stop();
});

function recommendedGuards(): boolean {
return !app.isPackaged && process.env.MY_APP_MCP === "1";
}
```

Connect an MCP client to the logged HTTP URL. The default is
`http://127.0.0.1:9229/mcp`.

## API

```ts
const mcp = createElectronMcpServer({
getSurfaces: () => ({ main: mainWindow, settings: settingsWindow }),
port: 9229,
host: "127.0.0.1",
path: "/mcp",
instructions: "Optional client-facing instructions.",
});
```

mcp.addTool(myCustomTool);
`createElectronMcpServer(config)` returns a synchronous handle:

if (!app.isPackaged && process.env.MY_APP_MCP === '1') {
await mcp.start();
- `addTool(toolDef)` registers a custom tool. Call it before `start()`.
- `start()` starts the loopback HTTP server.
- `stop()` stops the HTTP server and detaches CDP sessions.
- `isRunning` reports whether the server is active.
- `url` is the bound MCP endpoint once running.

Bundled tools:

- `list_surfaces`
- `show_surface`
- `hide_surface`
- `focus_surface`
- `reload_surface`
- `screenshot`
- `evaluate`
- `click`
- `type_text`
- `press_key`
- `hover`
- `query_dom`
- `ax_snapshot`
- `fill_form`
- `wait_for_load`

Public types are available from the root export and from
`@nebula-agents/electron-mcp/types`.

## Custom Tools

```ts
import { z } from "zod";
import type { ToolDef } from "@nebula-agents/electron-mcp/types";

const resetStateTool: ToolDef = {
name: "reset_state",
config: {
title: "Reset State",
description: "Reset the app's local demo state.",
inputSchema: { profile: z.enum(["empty", "demo"]) },
},
handler: async ({ profile }) => {
await resetLocalState(profile);
return { content: [{ type: "text", text: "ok" }] };
},
};

mcp.addTool(resetStateTool);
```

Tools must be registered before `start()`. Dynamic tool registration and
`tools/list_changed` are intentionally out of scope for `0.x`.

## Security Model

The server binds to loopback only by default: `127.0.0.1`. Attempts to bind a
non-loopback host throw. There is no authentication layer in `0.1.0`; if your
threat model requires more than loopback isolation, wrap this package in your
own gate or do not start it.

Recommended production guard:

```ts
function recommendedGuards(): boolean {
return !app.isPackaged && process.env.MY_APP_MCP === "1";
}
```

## Maintenance

This is open code with no support SLA. Agent Labs reviews issues and PRs on a
best-effort basis. `0.x` versions may change API shape based on real usage.

## License

MIT — see [`LICENSE`](./LICENSE).
MIT.
25 changes: 25 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Security

## Supported Versions

Only the latest published `0.x` version receives security consideration.

## Reporting a Vulnerability

Do not open a public issue for a vulnerability. Email `security@agent-labs.dev`
with:

- affected version or commit
- reproduction steps
- impact
- any known mitigations

We review reports best-effort and will coordinate disclosure when a fix is
available.

## Model

The MCP server binds to loopback by default and rejects non-loopback hosts. It
does not implement authentication in `0.1.0`. Host apps are responsible for
their own start gates and must not start the server in production unless they
have intentionally accepted that risk.
30 changes: 30 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.13/schema.json",
"files": {
"includes": [
"**",
"!dist",
"!node_modules",
"!test-results",
"!test/electron/fixture/dist"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
Loading
Loading