feat(FN-3717): add OpenClaw MCP bridge (+5 more)
Commits merged: - docs(FN-3717): update openclaw runtime and settings docs - test(FN-3717): fix openclaw engine test typing - test(FN-3717): strengthen openclaw bridge coverage - feat(FN-3717): complete Step 2 — wire engine and bundle delivery - fix(FN-3717): address openclaw bridge review feedback - feat(FN-3717): complete Step 1 — add OpenClaw MCP bridge Files changed: .changeset/fn-3717-openclaw-tool-bridge.md | 5 ++ docs/settings-reference.md | 28 +++++--- packages/cli/src/__tests__/bundle-output.test.ts | 9 +++ packages/cli/tsup.config.ts | 7 ++ .../src/__tests__/openclaw-runtime-e2e.test.ts | 28 ++++++++ .../__tests__/openclaw-runtime-integration.test.ts | 10 +++ plugins/fusion-plugin-openclaw-runtime/README.md | 13 ++++ .../src/__tests__/cli-spawn.test.ts | 82 +++++++++++++++++++--- .../src/__tests__/mcp-config.test.ts | 45 ++++++++++++ .../src/__tests__/runtime-adapter.test.ts | 46 ++++++++++++ .../fusion-plugin-openclaw-runtime/src/index.ts | 5 ++ .../src/mcp-config.ts | 60 ++++++++++++++++ .../src/mcp-schema-server.cjs | 59 ++++++++++++++++ .../src/pi-module.ts | 52 ++++++++++++-- .../src/runtime-adapter.ts | 26 +++++++ .../fusion-plugin-openclaw-runtime/src/types.ts | 4 ++ 16 files changed, 457 insertions(+), 22 deletions(-) Fusion-Task-Id: FN-3717
This commit is contained in:
@@ -16,6 +16,18 @@ For each `promptWithFallback(session, prompt)`:
|
||||
|
||||
The previous HTTP `/v1/chat/completions` integration has been removed — that endpoint required a separate gateway daemon and was an OpenAI-compat shim. The CLI surface is the canonical OpenClaw API.
|
||||
|
||||
## Fusion tool-control (MCP bridge)
|
||||
|
||||
When a Fusion OpenClaw session includes custom tools, the runtime plugin now enables tool-control through OpenClaw's supported MCP configuration flow:
|
||||
|
||||
1. Collect session tools and filter out built-ins: `read`, `write`, `edit`, `bash`, `grep`, `find`.
|
||||
2. Convert remaining tools into MCP-compatible schemas.
|
||||
3. Write a temporary schema file and MCP server config (`node mcp-schema-server.cjs <schema.json>`).
|
||||
4. Configure a profile-scoped MCP server using `openclaw --profile <id> mcp set fusion-custom-tools <json>`.
|
||||
5. Spawn the agent turn with `openclaw --profile <id> agent ...` so OpenClaw can see the configured MCP server.
|
||||
|
||||
No private protocol is used — this is the verified OpenClaw CLI contract (`mcp set` + `--profile`).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
@@ -46,6 +58,7 @@ Settings precedence: plugin settings → env var → default.
|
||||
|
||||
- **No per-token streaming.** `--json` emits a single JSON document at process exit. `onText` is called exactly once.
|
||||
- **Default ignores the gateway.** With `useGateway: false` (default) we always pass `--local`, skipping the WebSocket connect attempt entirely. Most users want this.
|
||||
- **Built-in tools are intentionally excluded from MCP bridge.** `read`, `write`, `edit`, `bash`, `grep`, and `find` stay native to Fusion and are not duplicated through OpenClaw MCP.
|
||||
- **AbortSignal sends SIGTERM.** If the CLI ignores it (e.g. during a long model download), the hard-kill timer (`cliTimeoutMs`) eventually fires.
|
||||
|
||||
## Public API
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
extractStderrError,
|
||||
promptCli,
|
||||
resolveCliConfig,
|
||||
configureOpenClawMcpServer,
|
||||
} from "../pi-module.js";
|
||||
import type { CliConfig, GatewaySession } from "../types.js";
|
||||
|
||||
@@ -169,23 +170,36 @@ describe("buildOpenClawArgs", () => {
|
||||
};
|
||||
|
||||
it("puts --no-color first, then agent subcommand", () => {
|
||||
const args = buildOpenClawArgs(baseConfig, "uuid-1", "hello");
|
||||
const args = buildOpenClawArgs(baseConfig, { sessionId: "uuid-1" }, "hello");
|
||||
expect(args[0]).toBe("--no-color");
|
||||
expect(args[1]).toBe("agent");
|
||||
});
|
||||
|
||||
it("includes --local when useGateway is false", () => {
|
||||
const args = buildOpenClawArgs(baseConfig, "uuid-1", "hello");
|
||||
const args = buildOpenClawArgs(baseConfig, { sessionId: "uuid-1" }, "hello");
|
||||
expect(args).toContain("--local");
|
||||
});
|
||||
|
||||
it("omits --local when useGateway is true", () => {
|
||||
const args = buildOpenClawArgs({ ...baseConfig, useGateway: true }, "uuid-1", "hello");
|
||||
const args = buildOpenClawArgs({ ...baseConfig, useGateway: true }, { sessionId: "uuid-1" }, "hello");
|
||||
expect(args).not.toContain("--local");
|
||||
});
|
||||
|
||||
it("includes --profile before agent when MCP profile is configured", () => {
|
||||
const args = buildOpenClawArgs(baseConfig, { sessionId: "my-uuid", mcpProfile: "fusion-profile" }, "test prompt");
|
||||
expect(args[0]).toBe("--no-color");
|
||||
expect(args).toContain("--profile");
|
||||
expect(args[args.indexOf("--profile") + 1]).toBe("fusion-profile");
|
||||
expect(args.indexOf("--profile")).toBeLessThan(args.indexOf("agent"));
|
||||
});
|
||||
|
||||
it("omits --profile when no MCP profile is configured", () => {
|
||||
const args = buildOpenClawArgs(baseConfig, { sessionId: "my-uuid" }, "test prompt");
|
||||
expect(args).not.toContain("--profile");
|
||||
});
|
||||
|
||||
it("includes --json, --session-id, --message, --agent", () => {
|
||||
const args = buildOpenClawArgs(baseConfig, "my-uuid", "test prompt");
|
||||
const args = buildOpenClawArgs(baseConfig, { sessionId: "my-uuid" }, "test prompt");
|
||||
expect(args).toContain("--json");
|
||||
expect(args).toContain("--session-id");
|
||||
expect(args[args.indexOf("--session-id") + 1]).toBe("my-uuid");
|
||||
@@ -196,24 +210,24 @@ describe("buildOpenClawArgs", () => {
|
||||
});
|
||||
|
||||
it("includes --model when configured", () => {
|
||||
const args = buildOpenClawArgs({ ...baseConfig, model: "anthropic/claude-opus-4-5" }, "u", "p");
|
||||
const args = buildOpenClawArgs({ ...baseConfig, model: "anthropic/claude-opus-4-5" }, { sessionId: "u" }, "p");
|
||||
expect(args).toContain("--model");
|
||||
expect(args[args.indexOf("--model") + 1]).toBe("anthropic/claude-opus-4-5");
|
||||
});
|
||||
|
||||
it("omits --model when not configured", () => {
|
||||
const args = buildOpenClawArgs(baseConfig, "u", "p");
|
||||
const args = buildOpenClawArgs(baseConfig, { sessionId: "u" }, "p");
|
||||
expect(args).not.toContain("--model");
|
||||
});
|
||||
|
||||
it("includes --thinking with the configured level", () => {
|
||||
const args = buildOpenClawArgs({ ...baseConfig, thinking: "high" }, "u", "p");
|
||||
const args = buildOpenClawArgs({ ...baseConfig, thinking: "high" }, { sessionId: "u" }, "p");
|
||||
expect(args).toContain("--thinking");
|
||||
expect(args[args.indexOf("--thinking") + 1]).toBe("high");
|
||||
});
|
||||
|
||||
it("includes --timeout with cliTimeoutSec", () => {
|
||||
const args = buildOpenClawArgs({ ...baseConfig, cliTimeoutSec: 120 }, "u", "p");
|
||||
const args = buildOpenClawArgs({ ...baseConfig, cliTimeoutSec: 120 }, { sessionId: "u" }, "p");
|
||||
expect(args).toContain("--timeout");
|
||||
expect(args[args.indexOf("--timeout") + 1]).toBe("120");
|
||||
});
|
||||
@@ -241,6 +255,58 @@ describe("extractStderrError", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("configureOpenClawMcpServer", () => {
|
||||
it("spawns `openclaw mcp set` with profile", async () => {
|
||||
const child = makeFakeChild();
|
||||
spawnMock.mockImplementation(() => {
|
||||
setTimeout(() => child.emit("close", 0), 0);
|
||||
return child;
|
||||
});
|
||||
|
||||
const fs = await import("node:fs/promises");
|
||||
const path = await import("node:path");
|
||||
const os = await import("node:os");
|
||||
const filePath = path.join(os.tmpdir(), `openclaw-mcp-config-${Date.now()}.json`);
|
||||
await fs.writeFile(filePath, JSON.stringify({ command: "node", args: ["server.cjs", "schema.json"] }));
|
||||
|
||||
await configureOpenClawMcpServer({
|
||||
binaryPath: "openclaw",
|
||||
profile: "fusion-profile",
|
||||
serverName: "fusion-custom-tools",
|
||||
serverConfigPath: filePath,
|
||||
});
|
||||
|
||||
const [, args] = spawnMock.mock.calls[0] as [string, string[]];
|
||||
expect(args).toEqual(expect.arrayContaining(["--profile", "fusion-profile", "mcp", "set", "fusion-custom-tools"]));
|
||||
});
|
||||
|
||||
it("throws when mcp set exits non-zero", async () => {
|
||||
const child = makeFakeChild();
|
||||
spawnMock.mockImplementation(() => {
|
||||
setTimeout(() => {
|
||||
child.stderr.emit("data", Buffer.from("bad config"));
|
||||
child.emit("close", 1);
|
||||
}, 0);
|
||||
return child;
|
||||
});
|
||||
|
||||
const fs = await import("node:fs/promises");
|
||||
const path = await import("node:path");
|
||||
const os = await import("node:os");
|
||||
const filePath = path.join(os.tmpdir(), `openclaw-mcp-config-${Date.now()}-err.json`);
|
||||
await fs.writeFile(filePath, JSON.stringify({ command: "node", args: ["server.cjs", "schema.json"] }));
|
||||
|
||||
await expect(
|
||||
configureOpenClawMcpServer({
|
||||
binaryPath: "openclaw",
|
||||
profile: "fusion-profile",
|
||||
serverName: "fusion-custom-tools",
|
||||
serverConfigPath: filePath,
|
||||
}),
|
||||
).rejects.toThrow(/mcp set failed/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("promptCli", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { readFileSync, unlinkSync } from "node:fs";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { toolsToMcpToolDefs, writeOpenClawMcpBridgeFiles } from "../mcp-config.js";
|
||||
|
||||
const cleanupPaths: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const path of cleanupPaths.splice(0)) {
|
||||
try {
|
||||
unlinkSync(path);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("toolsToMcpToolDefs", () => {
|
||||
it("filters out built-in tools", () => {
|
||||
const defs = toolsToMcpToolDefs([
|
||||
{ name: "read", description: "builtin", parameters: { type: "object" } },
|
||||
{ name: "fn_task_list", description: "list", parameters: { type: "object", properties: {} } },
|
||||
]);
|
||||
expect(defs).toHaveLength(1);
|
||||
expect(defs[0]?.name).toBe("fn_task_list");
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeOpenClawMcpBridgeFiles", () => {
|
||||
it("writes schema and server config files", () => {
|
||||
const bridge = writeOpenClawMcpBridgeFiles([
|
||||
{ name: "fn_task_list", description: "list", inputSchema: { type: "object", properties: {} } },
|
||||
], "test");
|
||||
|
||||
cleanupPaths.push(bridge.schemaPath, bridge.serverConfigPath);
|
||||
|
||||
const schema = JSON.parse(readFileSync(bridge.schemaPath, "utf-8"));
|
||||
expect(Array.isArray(schema)).toBe(true);
|
||||
expect(schema[0]?.name).toBe("fn_task_list");
|
||||
|
||||
const server = JSON.parse(readFileSync(bridge.serverConfigPath, "utf-8"));
|
||||
expect(server.command).toBe("node");
|
||||
expect(server.args[0]).toContain("mcp-schema-server.cjs");
|
||||
expect(server.args[1]).toBe(bridge.schemaPath);
|
||||
});
|
||||
});
|
||||
@@ -7,11 +7,13 @@ const {
|
||||
mockCreateCliSession,
|
||||
mockPromptCli,
|
||||
mockDescribeCliModel,
|
||||
mockConfigureOpenClawMcpServer,
|
||||
} = vi.hoisted(() => ({
|
||||
mockResolveCliConfig: vi.fn(),
|
||||
mockCreateCliSession: vi.fn(),
|
||||
mockPromptCli: vi.fn(),
|
||||
mockDescribeCliModel: vi.fn(),
|
||||
mockConfigureOpenClawMcpServer: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../pi-module.js", () => ({
|
||||
@@ -19,6 +21,7 @@ vi.mock("../pi-module.js", () => ({
|
||||
createCliSession: mockCreateCliSession,
|
||||
promptCli: mockPromptCli,
|
||||
describeCliModel: mockDescribeCliModel,
|
||||
configureOpenClawMcpServer: mockConfigureOpenClawMcpServer,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -41,6 +44,7 @@ beforeEach(() => {
|
||||
callbacks,
|
||||
}));
|
||||
mockDescribeCliModel.mockReturnValue("openclaw/main");
|
||||
mockConfigureOpenClawMcpServer.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
describe("OpenClawRuntimeAdapter — identity", () => {
|
||||
@@ -58,6 +62,48 @@ describe("OpenClawRuntimeAdapter — identity", () => {
|
||||
});
|
||||
|
||||
describe("OpenClawRuntimeAdapter — createSession", () => {
|
||||
it("configures MCP bridge when custom tools are present", async () => {
|
||||
const adapter = new OpenClawRuntimeAdapter({ agentId: "ops" });
|
||||
await adapter.createSession({
|
||||
cwd: "/repo",
|
||||
systemPrompt: "You are helpful",
|
||||
tools: [
|
||||
{ name: "read", description: "builtin", parameters: { type: "object" } },
|
||||
{ name: "fn_task_list", description: "list", parameters: { type: "object", properties: {} } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockConfigureOpenClawMcpServer).toHaveBeenCalledOnce();
|
||||
expect(mockCreateCliSession).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ mcpProfile: expect.stringContaining("fusion-") }),
|
||||
);
|
||||
});
|
||||
|
||||
it("configures MCP bridge when tools arrive through customTools", async () => {
|
||||
const adapter = new OpenClawRuntimeAdapter({ agentId: "ops" });
|
||||
await adapter.createSession({
|
||||
cwd: "/repo",
|
||||
systemPrompt: "You are helpful",
|
||||
customTools: [{ name: "fn_task_show", description: "custom", parameters: { type: "object" } }],
|
||||
});
|
||||
|
||||
expect(mockConfigureOpenClawMcpServer).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not configure MCP bridge when only built-in tools are provided", async () => {
|
||||
const adapter = new OpenClawRuntimeAdapter({ agentId: "ops" });
|
||||
await adapter.createSession({
|
||||
cwd: "/repo",
|
||||
systemPrompt: "You are helpful",
|
||||
tools: [{ name: "read", description: "builtin", parameters: { type: "object" } }],
|
||||
});
|
||||
|
||||
expect(mockConfigureOpenClawMcpServer).not.toHaveBeenCalled();
|
||||
expect(mockCreateCliSession).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ mcpProfile: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it("delegates to createCliSession with systemPrompt + agentId + callbacks", async () => {
|
||||
const adapter = new OpenClawRuntimeAdapter({ agentId: "ops" });
|
||||
const onText = vi.fn();
|
||||
|
||||
@@ -82,8 +82,13 @@ export {
|
||||
promptCli,
|
||||
describeCliModel,
|
||||
extractStderrError,
|
||||
configureOpenClawMcpServer,
|
||||
} from "./pi-module.js";
|
||||
export type { CliConfig, GatewaySession, OpenClawAgentJson } from "./types.js";
|
||||
export {
|
||||
toolsToMcpToolDefs,
|
||||
writeOpenClawMcpBridgeFiles,
|
||||
} from "./mcp-config.js";
|
||||
|
||||
// Probe re-export for the dashboard's runtime-provider-probes façade.
|
||||
export { probeOpenClawBinary } from "./probe.js";
|
||||
|
||||
60
plugins/fusion-plugin-openclaw-runtime/src/mcp-config.ts
Normal file
60
plugins/fusion-plugin-openclaw-runtime/src/mcp-config.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
const BUILT_IN_TOOL_NAMES = new Set(["read", "write", "edit", "bash", "grep", "find"]);
|
||||
|
||||
export interface ToolLike {
|
||||
name: string;
|
||||
description?: string;
|
||||
parameters?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface McpToolDef {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface OpenClawMcpBridgeFiles {
|
||||
schemaPath: string;
|
||||
serverConfigPath: string;
|
||||
serverName: string;
|
||||
}
|
||||
|
||||
export function toolsToMcpToolDefs(tools: ReadonlyArray<ToolLike> | undefined): McpToolDef[] {
|
||||
if (!Array.isArray(tools)) return [];
|
||||
return tools
|
||||
.filter((tool) => tool && typeof tool.name === "string" && !BUILT_IN_TOOL_NAMES.has(tool.name))
|
||||
.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: typeof tool.description === "string" ? tool.description : "",
|
||||
inputSchema: tool.parameters ?? { type: "object", properties: {} },
|
||||
}));
|
||||
}
|
||||
|
||||
export function writeOpenClawMcpBridgeFiles(toolDefs: McpToolDef[], cacheKey?: string): OpenClawMcpBridgeFiles {
|
||||
const suffix = cacheKey ? `${process.pid}-${cacheKey}` : `${process.pid}`;
|
||||
const schemaPath = join(tmpdir(), `openclaw-runtime-mcp-schemas-${suffix}.json`);
|
||||
writeFileSync(schemaPath, JSON.stringify(toolDefs));
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const serverPath = join(__dirname, "mcp-schema-server.cjs");
|
||||
|
||||
const serverConfig = {
|
||||
command: "node",
|
||||
args: [serverPath, schemaPath],
|
||||
};
|
||||
|
||||
const serverConfigPath = join(tmpdir(), `openclaw-runtime-mcp-server-${suffix}.json`);
|
||||
writeFileSync(serverConfigPath, JSON.stringify(serverConfig));
|
||||
|
||||
return {
|
||||
schemaPath,
|
||||
serverConfigPath,
|
||||
serverName: "fusion-custom-tools",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const readline = require("readline");
|
||||
|
||||
const schemaPath = process.argv[2];
|
||||
if (!schemaPath) process.exit(1);
|
||||
|
||||
let tools = [];
|
||||
try {
|
||||
tools = JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
|
||||
} catch {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({ input: process.stdin });
|
||||
rl.on("line", (line) => {
|
||||
let msg;
|
||||
try {
|
||||
msg = JSON.parse(line);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.method === "initialize") {
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: msg.id,
|
||||
result: {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: "fusion-custom-tools", version: "1.0.0" },
|
||||
},
|
||||
}) + "\n",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.method === "tools/list") {
|
||||
process.stdout.write(
|
||||
JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: { tools } }) + "\n",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.method === "tools/call") {
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: msg.id,
|
||||
result: {
|
||||
content: [{ type: "text", text: "Tool execution is handled by Fusion runtime." }],
|
||||
isError: true,
|
||||
},
|
||||
}) + "\n",
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import type {
|
||||
CliConfig,
|
||||
GatewayCallbacks,
|
||||
@@ -101,15 +102,21 @@ export function resolveCliConfig(settings?: Record<string, unknown>): CliConfig
|
||||
*/
|
||||
export function buildOpenClawArgs(
|
||||
config: CliConfig,
|
||||
sessionId: string,
|
||||
session: Pick<GatewaySession, "sessionId" | "mcpProfile">,
|
||||
message: string,
|
||||
): string[] {
|
||||
const args: string[] = ["--no-color", "agent"];
|
||||
const args: string[] = ["--no-color"];
|
||||
|
||||
if (session.mcpProfile) {
|
||||
args.push("--profile", session.mcpProfile);
|
||||
}
|
||||
|
||||
args.push("agent");
|
||||
|
||||
if (!config.useGateway) args.push("--local");
|
||||
|
||||
args.push("--json");
|
||||
args.push("--session-id", sessionId);
|
||||
args.push("--session-id", session.sessionId);
|
||||
args.push("--message", message);
|
||||
args.push("--agent", config.agentId);
|
||||
|
||||
@@ -152,6 +159,8 @@ export function createCliSession(opts: {
|
||||
systemPrompt: string;
|
||||
agentId?: string;
|
||||
callbacks?: GatewayCallbacks;
|
||||
mcpProfile?: string;
|
||||
mcpConfigPath?: string;
|
||||
}): GatewaySession {
|
||||
return {
|
||||
sessionId: randomUUID(),
|
||||
@@ -161,9 +170,44 @@ export function createCliSession(opts: {
|
||||
lastModelDescription: `openclaw/${opts.agentId ?? DEFAULT_AGENT_ID}`,
|
||||
lastUsage: undefined,
|
||||
callbacks: opts.callbacks,
|
||||
mcpProfile: opts.mcpProfile,
|
||||
mcpConfigPath: opts.mcpConfigPath,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// configureOpenClawMcpServer — configure profile-scoped MCP server via CLI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function configureOpenClawMcpServer(opts: {
|
||||
binaryPath: string;
|
||||
profile: string;
|
||||
serverName: string;
|
||||
serverConfigPath: string;
|
||||
}): Promise<void> {
|
||||
const serverValue = await readFile(opts.serverConfigPath, "utf-8");
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const child = spawn(opts.binaryPath, ["--no-color", "--profile", opts.profile, "mcp", "set", opts.serverName, serverValue], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stderr = "";
|
||||
child.stderr?.on("data", (chunk: Buffer) => {
|
||||
stderr += chunk.toString("utf-8");
|
||||
});
|
||||
|
||||
child.on("error", (err) => reject(new Error(`openclaw: failed to configure MCP server — ${err.message}`)));
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
reject(new Error(`openclaw: mcp set failed (${String(code)}): ${extractStderrError(stderr)}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// promptCli — spawns openclaw, parses the JSON, fires callbacks
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -175,7 +219,7 @@ export async function promptCli(
|
||||
callbacks?: GatewayCallbacks,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
const args = buildOpenClawArgs(config, session.sessionId, message);
|
||||
const args = buildOpenClawArgs(config, session, message);
|
||||
const cb: GatewayCallbacks = { ...session.callbacks, ...callbacks };
|
||||
|
||||
cb.onToolStart?.("openclaw.agent", { sessionId: session.sessionId });
|
||||
|
||||
@@ -20,11 +20,14 @@ import type {
|
||||
GatewaySession,
|
||||
} from "./types.js";
|
||||
import {
|
||||
configureOpenClawMcpServer,
|
||||
createCliSession,
|
||||
describeCliModel,
|
||||
promptCli,
|
||||
resolveCliConfig,
|
||||
} from "./pi-module.js";
|
||||
import { toolsToMcpToolDefs, writeOpenClawMcpBridgeFiles, type ToolLike } from "./mcp-config.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
export class OpenClawRuntimeAdapter implements AgentRuntime {
|
||||
readonly id = "openclaw";
|
||||
@@ -37,9 +40,32 @@ export class OpenClawRuntimeAdapter implements AgentRuntime {
|
||||
}
|
||||
|
||||
async createSession(options: AgentRuntimeOptions): Promise<AgentSessionResult> {
|
||||
const contextTools = [
|
||||
...(Array.isArray(options.tools) ? (options.tools as ToolLike[]) : []),
|
||||
...(Array.isArray(options.customTools) ? (options.customTools as ToolLike[]) : []),
|
||||
];
|
||||
const toolDefs = toolsToMcpToolDefs(contextTools);
|
||||
|
||||
let mcpProfile: string | undefined;
|
||||
let mcpConfigPath: string | undefined;
|
||||
|
||||
if (toolDefs.length > 0) {
|
||||
const bridge = writeOpenClawMcpBridgeFiles(toolDefs, randomUUID());
|
||||
mcpProfile = `fusion-${randomUUID()}`;
|
||||
mcpConfigPath = bridge.serverConfigPath;
|
||||
await configureOpenClawMcpServer({
|
||||
binaryPath: this.config.binaryPath,
|
||||
profile: mcpProfile,
|
||||
serverName: bridge.serverName,
|
||||
serverConfigPath: bridge.serverConfigPath,
|
||||
});
|
||||
}
|
||||
|
||||
const session = createCliSession({
|
||||
systemPrompt: options.systemPrompt,
|
||||
agentId: this.config.agentId,
|
||||
mcpProfile,
|
||||
mcpConfigPath,
|
||||
callbacks: {
|
||||
onText: options.onText,
|
||||
onThinking: options.onThinking,
|
||||
|
||||
@@ -53,6 +53,10 @@ export interface GatewaySession {
|
||||
lastModelDescription: string;
|
||||
/** Last-known token usage from the CLI JSON's `meta.agentMeta.usage`. */
|
||||
lastUsage?: Record<string, number>;
|
||||
/** Optional profile that carries MCP server config for this session. */
|
||||
mcpProfile?: string;
|
||||
/** Optional path to the server JSON used to configure MCP for this session. */
|
||||
mcpConfigPath?: string;
|
||||
callbacks?: GatewayCallbacks;
|
||||
dispose?: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user