Add a bundled Claude Code ACP runtime with model discovery and CLI distribution integration. - Add the Claude ACP runtime plugin, bridge, tool forwarding, and tests. - Register Claude model discovery and cached picker support in the dashboard. - Stage the plugin in CLI and desktop packaging with its pinned dependency. - Add workspace, lockfile, and release metadata. Files changed: .changeset/fn-8224-claude-acp-runtime.md | 7 + packages/cli/package.json | 1 + packages/cli/src/__tests__/bundle-output.test.ts | 32 +- .../cli/src/plugins/staged-bundled-plugin-ids.ts | 1 + packages/cli/tsup.config.ts | 40 +- .../core/src/plugins/bundled-plugin-install.ts | 2 + packages/dashboard/package.json | 3 +- packages/dashboard/src/claude-model-cache.ts | 185 +++++++ packages/dashboard/src/routes.ts | 1 + .../dashboard/src/routes/register-model-routes.ts | 11 + packages/dashboard/src/runtime-provider-probes.ts | 15 + packages/dashboard/vitest.config.ts | 4 + packages/desktop/scripts/workspace-tools.ts | 1 + plugins/fusion-plugin-claude-runtime/README.md | 7 + plugins/fusion-plugin-claude-runtime/manifest.json | 1 + plugins/fusion-plugin-claude-runtime/package.json | 1 + .../src/__tests__/cli-spawn.test.ts | 3 + .../src/__tests__/index.test.ts | 3 + .../src/__tests__/provider.test.ts | 8 + .../src/__tests__/runtime-adapter.test.ts | 7 + .../src/__tests__/tool-bridge.test.ts | 79 +++ .../src/acp-settings.ts | 21 + .../src/acp/VENDORED.md | 30 ++ .../src/acp/cli-spawn.ts | 188 +++++++ .../src/acp/control-handler.ts | 302 ++++++++++++ .../src/acp/event-bridge.ts | 308 ++++++++++++ .../src/acp/fs-capabilities.ts | 263 ++++++++++ .../fusion-plugin-claude-runtime/src/acp/index.ts | 16 + .../src/acp/path-jail.ts | 229 +++++++++ .../src/acp/process-manager.ts | 177 +++++++ .../src/acp/prompt-builder.ts | 87 ++++ .../src/acp/provider.ts | 542 +++++++++++++++++++++ .../src/acp/runtime-adapter.ts | 190 ++++++++ .../src/acp/sanitize.ts | 81 +++ .../src/acp/tool-mapping.ts | 47 ++ .../fusion-plugin-claude-runtime/src/acp/types.ts | 189 +++++++ .../fusion-plugin-claude-runtime/src/cli-spawn.ts | 31 ++ plugins/fusion-plugin-claude-runtime/src/index.ts | 95 ++++ .../src/mcp-forwarding.ts | 114 +++++ .../src/mcp-schema-server.cjs | 155 ++++++ plugins/fusion-plugin-claude-runtime/src/probe.ts | 8 + .../fusion-plugin-claude-runtime/src/provider.ts | 8 + .../src/runtime-adapter.ts | 424 ++++++++++++++++ .../src/skill-loader.ts | 290 +++++++++++ .../src/tool-bridge.ts | 264 ++++++++++ plugins/fusion-plugin-claude-runtime/src/types.ts | 142 ++++++ plugins/fusion-plugin-claude-runtime/tsconfig.json | 10 + .../fusion-plugin-claude-runtime/vitest.config.ts | 22 + pnpm-lock.yaml | 131 ++++- pnpm-workspace.yaml | 1 + 50 files changed, 4753 insertions(+), 24 deletions(-) Fusion-Task-Id: FN-8224 Fusion-Task-Lineage: 4e626913-32bb-4baa-a142-816c7f4ee7c4 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
156 lines
4.0 KiB
JavaScript
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_GROK_TOOL_BRIDGE_URL;
|
|
const capabilityToken = process.env.FUSION_TOOL_BRIDGE_CAPABILITY;
|
|
if (!schemaPath || !bridgeUrl || !capabilityToken) {
|
|
process.stderr.write("fusion-tools-mcp-server: missing schema path, bridge URL, or capability\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 ${capabilityToken}`,
|
|
},
|
|
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}` },
|
|
});
|
|
}
|
|
});
|