Skip to content

Local daemon: /mcp/toolkits/<slug> always 500s — toolkit resource re-opens the owned data dir and deadlocks on its own lock #1406

Description

@kristojorg

Summary

On the local daemon, every toolkit-scoped MCP endpoint (/mcp/toolkits/<slug>) returns a 500 (-32603 Internal server error), while the default /mcp endpoint works fine. The daemon logs:

[mcp] handleRequest error: Error: Failed to open local SQLite data. Close other Executor processes and retry, or run with --log-level debug for details.

This happens on a completely clean daemon with no other Executor processes running.

Repro

  1. executor daemon run (v1.5.32, macOS), create a toolkit (e.g. slug radial) via the web UI.
  2. POST /mcp with a valid bearer + initialize → 200.
  3. POST /mcp/toolkits/radial with the same bearer + initialize → 500, and the error above in daemon.error.log.

The slug resolves fine (GET /api/toolkits lists it); any slug fails the same way.

Root cause

The daemon's shared boot executor holds the data-dir ownership lock for its whole lifetime — a BEGIN EXCLUSIVE held open on data.db.owner-lock (apps/local/src/db/data-dir-ownership.ts, acquireDataDirOwnership).

The toolkit MCP path doesn't reuse that executor. createServerHandlers wires:

// apps/local/src/main.ts
createConfigForResource: async (resource) => {
  if (resource.kind === "default") return { config: { engine } };
  const handle = await createExecutorHandle({
    activeToolkitSlug: resource.slug,
  });
  ...

createExecutorHandlecreateLocalExecutorLayeropenOwnedLocalDatabaseacquireDataDirOwnership, which tries to take the same exclusive lock the daemon already holds (per-connection SQLite lock, busy_timeout = 0) → SQLITE_BUSYLocalExecutorCreateError with the "Close other Executor processes" message → 500.

So the default resource reuses the shared engine, but any toolkit resource attempts a second owned open of the same data dir from inside the process that owns it — it can never succeed while the daemon is running, which is always.

Expected

Toolkit resources should derive their engine from the already-open shared bundle (thread activeToolkitSlug into an engine/executor over the existing DB handle) rather than opening a second owned database.

Notes

  • e2e/local/toolkits-mcp.test.ts exercises exactly this path (/mcp/toolkits/<slug> against a spawned local server), so it presumably isn't running in CI — it should reproduce this.
  • Self-host is unaffected as far as I can tell; this is the local daemon wiring only.
  • This was drafted by a clanker. Hope that is ok. Feel free to close if not.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions