Files
fusion/docs/cursor-cli-contract.md
gsxdsm 5e5b0dbb8f FN-9098: bridge scoped Fusion tools into Cursor
Publish engine-owned Fusion tools to Cursor through a crash-safe, worktree-scoped MCP bridge.

- preserve operator MCP configuration with locking, journaling, quarantine, and lease reconciliation
- enforce identity-scoped fn_* provenance so injected custom and MCP tools are never exposed
- secure loopback dispatch with per-session tokens, heartbeats, cleanup, and normalized tool events
- document the Cursor contract and cover bridge lifecycle, config hygiene, and failure handling

Files changed:
 .changeset/fn-9098-cursor-mcp-bridge.md            |   7 +
 docs/cursor-cli-contract.md                        | 140 ++-------------
 docs/mcp.md                                        |   4 +
 .../src/__tests__/agent-session-helpers.test.ts    |  24 +++
 .../src/__tests__/step-session-executor.test.ts    |  16 ++
 .../src/__tests__/web-fetch-universal.test.ts      |   4 +-
 packages/engine/src/agent-heartbeat.ts             |   3 +-
 packages/engine/src/agents/agent-runtime.ts        |   9 +
 .../engine/src/agents/agent-session-helpers.ts     |  26 +--
 packages/engine/src/execution/reviewer.ts          |   1 +
 .../engine/src/execution/step-session-executor.ts  |  31 ++--
 .../engine/src/executor/execute-workflow-step.ts   |   4 +-
 packages/engine/src/merger.ts                      |   4 +-
 plugins/fusion-plugin-cursor-runtime/README.md     |  18 +-
 plugins/fusion-plugin-cursor-runtime/package.json  |   2 +-
 .../src/__tests__/cursor-mcp-config.test.ts        | 100 +++++++++++
 .../cursor-mcp-server-failure.stream.jsonl         |   3 +
 .../fixtures/cursor-mcp-tool-call.stream.jsonl     |   4 +
 .../src/__tests__/runtime-adapter.test.ts          |  57 +++++-
 .../src/__tests__/worktree-hygiene.test.ts         |  52 ++++++
 .../src/cursor-mcp-config.ts                       | 196 +++++++++++++++++++++
 .../src/mcp-schema-server.cjs                      | 155 ++++++++++++++++
 .../src/prompt-transport.ts                        |   4 +-
 .../src/runtime-adapter.ts                         |  67 +++++--
 .../src/tool-bridge.ts                             |  48 +++++
 .../src/tool-mapping.ts                            |  11 ++
 plugins/fusion-plugin-cursor-runtime/src/types.ts  |   6 +-
 .../src/worktree-hygiene.ts                        | 117 ++++++++++++
 28 files changed, 934 insertions(+), 179 deletions(-)

Fusion-Task-Id: FN-9098

Fusion-Task-Lineage: 11b6cb10-ce0e-4f33-9007-c83f2bbf82ea

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-15 16:51:54 -07:00

3.7 KiB

MCP staging and cleanup

Fusion creates a unique fusion-custom-tools-<uuid> server key per Cursor session. The .cursor/.fusion-mcp-state.json manifest retains the complete { command, args, env } entry for every lease, allowing one process to recompose a peer process's live entry. Operator content is taken from current bytes; Fusion content is taken from that manifest.

Before staging in a git worktree, Fusion writes its marker block to info/exclude, then creates .cursor/ and its lock directory. The marker covers mcp.json, the state record, and the lock so step-boundary git add -A cannot capture session files. The first stage persists the byte-exact baseline before it writes config bytes. Later changes journal the intended output before atomic config replacement, then promote the record, so a crash can resolve either the intended or previous byte sequence without mistaking Fusion output for an operator edit.

A tracked .cursor/mcp.json is refused. Byte-different operator edits are preserved rather than restored over. If an operator edit makes the file unparsable, Fusion quarantines the worktree: no process writes that config, the exclusion remains, and further staging is refused. Reconciliation clears the quarantine only after the config is deleted or has valid JSON with no fusion-custom-tools-* keys.

Worktree safety protocol

The lease manifest records each serverEntry as { command, args, env? }; entries are always recomposed from the durable manifest while non-Fusion content comes from the current on-disk JSON. A peer can therefore dispose without dropping another process's bridge. The first stage commits the raw baseline before its first config write. Every subsequent mutation writes pending { kind, raw, seq }, atomically replaces the config, then promotes the pending record. Recovery compares bytes against the pending result and last confirmed result: match pending promotes, match prior discards, and any other bytes latch an operator edit.

Bootstrap is deliberately outside the main lock because that lock lives in .cursor/: resolve git shape, serialize the info/exclude marker under the git-dir bootstrap lock, observe/create .cursor/ with an EEXIST-safe ownership observation, then acquire the main lock. On final cleanup the inverse is used: the exclusion marker is the last in-lock removal, the main lock is released immediately, and only then may Fusion make one non-recursive rmdir attempt. A failed rmdir is a benign peer/operator race and is retried only by a later reconciliation.

Lock owners persist PID, hostname, and acquisition time. Contenders retry briefly and may reclaim only a dead same-host owner or an expired critical-section TTL. Leases heartbeat independently for long turns. The synchronous process-exit backstop makes one free-lock attempt only; it never bootstraps, takes over a stale lock, or writes when a peer owns the lock. Reconciliation is the crash-recovery owner.

When quarantine is held, .fusion-mcp-state.json remains as the durable record and the Fusion info/exclude block is intentionally retained so leftover bridge entries cannot be swept by git add -A. To recover, repair the JSON and remove every fusion-custom-tools-* entry, or delete the config; a later Cursor session clears the record and exclusion automatically. A merely parseable file that still has a Fusion key remains quarantined.