Files
fusion/plugins/fusion-plugin-cursor-runtime/src/mcp-schema-server.cjs
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

156 lines
4.0 KiB
JavaScript

#!/usr/bin/env node
/*
FNXC:GrokAcp 2026-07-11-14:00:
Executable MCP bridge for Fusion custom tools (fn_*) on the Grok ACP path.
tools/list is served from a schema file; tools/call POSTs to a localhost bridge
owned by GrokRuntimeAdapter so ToolDefinition.execute runs in-process with the
engine's closures. Unlike the Claude/Droid schema-only break-early servers,
Grok actually invokes MCP tools/call itself.
*/
"use strict";
const fs = require("fs");
const http = require("http");
const readline = require("readline");
// FNXC:GrokAcp 2026-07-11-18:30: CJS has no global URL under eslint no-undef; use node:url.
const { URL } = require("node:url");
const schemaPath = process.argv[2];
const bridgeUrl = process.env.FUSION_CURSOR_TOOL_BRIDGE_URL;
const bridgeToken = process.env.FUSION_CURSOR_TOOL_BRIDGE_TOKEN;
if (!schemaPath || !bridgeUrl || !bridgeToken) {
process.stderr.write("fusion-cursor-mcp-server: missing schema path or FUSION_CURSOR_TOOL_BRIDGE_URL\n");
process.exit(1);
}
let tools = [];
try {
tools = JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
if (!Array.isArray(tools)) tools = [];
} catch {
process.exit(1);
}
function write(msg) {
process.stdout.write(JSON.stringify(msg) + "\n");
}
function callBridge(toolName, args) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({ name: toolName, arguments: args ?? {} });
const url = new URL("/tool-call", bridgeUrl);
const req = http.request(
{
hostname: url.hostname,
port: url.port,
path: url.pathname,
method: "POST",
headers: {
"content-type": "application/json",
"content-length": Buffer.byteLength(body),
authorization: `Bearer ${bridgeToken}`,
},
timeout: 120_000,
},
(res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
try {
resolve(JSON.parse(data || "{}"));
} catch (err) {
reject(err);
}
});
},
);
req.on("error", reject);
req.on("timeout", () => {
req.destroy(new Error("tool bridge timeout"));
});
req.write(body);
req.end();
});
}
const rl = readline.createInterface({ input: process.stdin });
rl.on("line", (line) => {
let msg;
try {
msg = JSON.parse(line);
} catch {
return;
}
if (msg.method === "initialize") {
write({
jsonrpc: "2.0",
id: msg.id,
result: {
protocolVersion: "2024-11-05",
capabilities: { tools: {} },
serverInfo: { name: "fusion-custom-tools", version: "1.0.0" },
},
});
return;
}
if (msg.method === "notifications/initialized" || msg.method === "initialized") {
return;
}
if (msg.method === "tools/list") {
write({
jsonrpc: "2.0",
id: msg.id,
result: {
tools: tools.map((tool) => ({
name: tool.name,
description: tool.description ?? "",
inputSchema: tool.inputSchema ?? { type: "object", properties: {} },
})),
},
});
return;
}
if (msg.method === "tools/call") {
const toolName = msg.params?.name;
const args = msg.params?.arguments ?? {};
callBridge(toolName, args)
.then((result) => {
write({
jsonrpc: "2.0",
id: msg.id,
result: {
content: Array.isArray(result.content)
? result.content
: [{ type: "text", text: typeof result.text === "string" ? result.text : JSON.stringify(result) }],
isError: result.isError === true,
},
});
})
.catch((err) => {
write({
jsonrpc: "2.0",
id: msg.id,
result: {
content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
isError: true,
},
});
});
return;
}
if (msg.id !== undefined) {
write({
jsonrpc: "2.0",
id: msg.id,
error: { code: -32601, message: `Method not found: ${msg.method}` },
});
}
});