Files
fusion/plugins/fusion-plugin-claude-runtime/src/mcp-forwarding.ts
gsxdsm adb3eb3a37 FN-8224: add Claude ACP runtime support
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>
2026-07-17 12:00:26 -07:00

115 lines
3.9 KiB
TypeScript

/*
FNXC:ClaudeAcp 2026-07-11-14:00:
Convert engine-resolved MCP server definitions (FN-7022 three-transport shape)
into ACP `session/new.mcpServers` entries so Claude agent stdio receives the same
operator-approved MCP set as other Fusion AI lanes. Env/header secrets are
already materialized by the engine; this module only reshapes them and never logs
server contents.
*/
export type AcpMcpServer =
| {
name: string;
command: string;
args: string[];
env: { name: string; value: string }[];
}
| {
type: "http";
name: string;
url: string;
headers: { name: string; value: string }[];
}
| {
type: "sse";
name: string;
url: string;
headers: { name: string; value: string }[];
};
function asRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function mapEntries(map: Record<string, string> | undefined): { name: string; value: string }[] {
if (!map) return [];
return Object.entries(map)
.filter((entry): entry is [string, string] => typeof entry[0] === "string" && typeof entry[1] === "string")
.map(([name, value]) => ({ name, value }));
}
/**
* Normalize engine `mcpServers` (ResolvedMcpServerDefinition or legacy ACP
* stdio shape) into the ACP wire format Claude accepts.
*/
export function toAcpMcpServers(servers: unknown): AcpMcpServer[] {
if (!Array.isArray(servers) || servers.length === 0) return [];
const out: AcpMcpServer[] = [];
for (const raw of servers) {
const server = asRecord(raw);
if (!server) continue;
const name = typeof server.name === "string" ? server.name.trim() : "";
if (!name || server.enabled === false) continue;
// Legacy ACP stdio shape: { name, command, args, env: [{name,value}] }
if (typeof server.command === "string" && server.command.trim() && !("transport" in server) && !("type" in server) && !("url" in server)) {
const envPairs = Array.isArray(server.env)
? server.env
.map((entry) => asRecord(entry))
.filter((entry): entry is Record<string, unknown> => Boolean(entry))
.filter((entry) => typeof entry.name === "string" && typeof entry.value === "string")
.map((entry) => ({ name: String(entry.name), value: String(entry.value) }))
: mapEntries(asRecord(server.env) as Record<string, string> | undefined);
out.push({
name,
command: server.command.trim(),
args: Array.isArray(server.args) ? server.args.filter((a): a is string => typeof a === "string") : [],
env: envPairs,
});
continue;
}
const transport = typeof server.transport === "string" ? server.transport : typeof server.type === "string" ? server.type : "stdio";
if (transport === "stdio") {
const command = typeof server.command === "string" ? server.command.trim() : "";
if (!command) continue;
out.push({
name,
command,
args: Array.isArray(server.args) ? server.args.filter((a): a is string => typeof a === "string") : [],
env: mapEntries(asRecord(server.env) as Record<string, string> | undefined),
});
continue;
}
if (transport === "http" || transport === "streamable-http") {
const url = typeof server.url === "string" ? server.url.trim() : "";
if (!url) continue;
out.push({
type: "http",
name,
url,
headers: mapEntries(asRecord(server.headers) as Record<string, string> | undefined),
});
continue;
}
if (transport === "sse") {
const url = typeof server.url === "string" ? server.url.trim() : "";
if (!url) continue;
out.push({
type: "sse",
name,
url,
headers: mapEntries(asRecord(server.headers) as Record<string, string> | undefined),
});
}
}
return out;
}