Make executor mcp ensure a durable daemon and bridge to it#1196
Make executor mcp ensure a durable daemon and bridge to it#1196RhysSullivan wants to merge 1 commit into
Conversation
executor mcp no longer starts a server in-process. It ensures a durable detached daemon and bridges stdio JSON-RPC to that owner over HTTP. Concurrent cold starts run a race-safe election: one process becomes the owner and the rest wait for its manifest and attach instead of failing. The owner's lifetime is independent of any MCP client, so many clients, the web UI, and the desktop app share one local server. This builds on the merged start-lock primitive so no client ever owns the database, replacing the earlier approach where the first mcp process started a server in-process and bridged to itself. Adds a cold-start election probe across the cli VM targets plus a local attach stress test; the one-winner, rest-attach behavior is verified on macOS, Linux, and Windows.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-marketing | ee75f4a | Commit Preview URL Branch Preview URL |
Jun 28 2026, 11:24 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
executor-cloud | ee75f4a | Jun 28 2026, 11:24 PM |
Cloudflare preview
Sign-in is Cloudflare Access (one-time PIN to an allowed email). The preview has its own database and encryption key; it is destroyed when this PR closes. |
Greptile SummaryThis PR replaces the in-process MCP server in
Confidence Score: 4/5The production path in The bridge and election logic in e2e/local/cli-mcp-daemon-attach-stress.test.ts — missing import breaks the daemon cleanup finalizer Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant C1 as executor mcp (client 1)
participant C2 as executor mcp (client 2..N)
participant FS as Filesystem (start-lock + manifest)
participant D as Daemon (detached)
par Cold-start race
C1->>FS: acquireDaemonStartLock()
C2->>FS: acquireDaemonStartLock()
end
FS-->>C1: lock acquired (winner)
FS-->>C2: contention error (loser)
C1->>D: spawnDetached()
D-->>FS: write server manifest
D-->>C1: health reachable
C2->>FS: waitForDaemonStartupTarget (polls manifest)
FS-->>C2: manifest found
C1->>FS: releaseDaemonStartLock()
C1->>FS: readActiveLocalServerManifest()
FS-->>C1: manifest (URL + auth token)
C2->>FS: readActiveLocalServerManifest()
FS-->>C2: manifest (URL + auth token)
par Bridge stdio to HTTP
C1->>D: StreamableHTTPClientTransport /mcp
C2->>D: StreamableHTTPClientTransport /mcp
end
Note over C1,D: MCP client stdin/stdout bridged to daemon over HTTP
Note over C1,C2: Daemon lifetime independent of any MCP client
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant C1 as executor mcp (client 1)
participant C2 as executor mcp (client 2..N)
participant FS as Filesystem (start-lock + manifest)
participant D as Daemon (detached)
par Cold-start race
C1->>FS: acquireDaemonStartLock()
C2->>FS: acquireDaemonStartLock()
end
FS-->>C1: lock acquired (winner)
FS-->>C2: contention error (loser)
C1->>D: spawnDetached()
D-->>FS: write server manifest
D-->>C1: health reachable
C2->>FS: waitForDaemonStartupTarget (polls manifest)
FS-->>C2: manifest found
C1->>FS: releaseDaemonStartLock()
C1->>FS: readActiveLocalServerManifest()
FS-->>C1: manifest (URL + auth token)
C2->>FS: readActiveLocalServerManifest()
FS-->>C2: manifest (URL + auth token)
par Bridge stdio to HTTP
C1->>D: StreamableHTTPClientTransport /mcp
C2->>D: StreamableHTTPClientTransport /mcp
end
Note over C1,D: MCP client stdin/stdout bridged to daemon over HTTP
Note over C1,C2: Daemon lifetime independent of any MCP client
Reviews (1): Last reviewed commit: "feat(cli): make executor mcp ensure a du..." | Re-trigger Greptile |
| import { Client } from "@modelcontextprotocol/sdk/client/index.js"; | ||
| import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; | ||
| import { Effect } from "effect"; | ||
| import { mkdtempSync, readdirSync, rmSync } from "node:fs"; |
There was a problem hiding this comment.
readFileSync is used in stopAutoSpawnedDaemon but is not present in the node:fs import. At runtime this throws a ReferenceError, which the surrounding try/catch silently swallows, so the auto-spawned daemon is never sent SIGTERM. Subsequent rmSync still removes the data directory, leaving an orphan daemon process behind — accumulating across repeated test runs.
| import { mkdtempSync, readdirSync, rmSync } from "node:fs"; | |
| import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"; |
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
What
executor mcpno longer owns the local database. It ensures a durable, detached daemon and bridges stdio JSON-RPC to it over HTTP. Concurrent cold starts run a race-safe election: exactly one process becomes the owner, and the rest wait for its manifest and attach rather than failing. The owner's lifetime is independent of any MCP client, so multiple MCP clients, the web UI, and the desktop app all share one local server.Supersedes #1033, which had the first
executor mcpprocess start a server in-process and bridge to itself. That tied the shared owner's lifetime to a transient client: when it exited, the server everyone else attached to went down. This builds on the merged start-lock primitive instead, so no client ever owns the database.Tests
e2e/local/cli-mcp-daemon-attach-stress.test.ts: cold-start race, attach storm, and kill-under-load against the local dev server.e2e/cli/election-cold-start.test.ts: fires N simultaneous clients at one cold data dir on the cli VM targets and asserts exactly one daemon is elected and every client attaches.e2e/cli/election-cold-start.win.ps1: the same election proven on real Windows.Verified the one-winner, rest-attach behavior on macOS, Linux, and Windows:
(cold: one client spawns the daemon, the other five attach, all round-trip; warm: all six attach, none spawn.)