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:
5
.changeset/fn-3717-openclaw-tool-bridge.md
Normal file
5
.changeset/fn-3717-openclaw-tool-bridge.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Enable Fusion tool-control support for the OpenClaw runtime plugin. OpenClaw sessions now derive custom tools from runtime session options, filter out built-in tools (`read`, `write`, `edit`, `bash`, `grep`, `find`), configure an MCP server via supported `openclaw mcp set` profile-based CLI flow, and pass that profile into `openclaw agent` calls while preserving default embedded `--local` behavior.
|
||||||
@@ -621,29 +621,37 @@ fn plugin install ./plugins/fusion-plugin-openclaw-runtime
|
|||||||
|
|
||||||
For more details, see the [Paperclip Runtime Plugin documentation](../plugins/fusion-plugin-paperclip-runtime/README.md), [Hermes Runtime Plugin documentation](../plugins/fusion-plugin-hermes-runtime/README.md), and [OpenClaw Runtime Plugin documentation](../plugins/fusion-plugin-openclaw-runtime/README.md).
|
For more details, see the [Paperclip Runtime Plugin documentation](../plugins/fusion-plugin-paperclip-runtime/README.md), [Hermes Runtime Plugin documentation](../plugins/fusion-plugin-hermes-runtime/README.md), and [OpenClaw Runtime Plugin documentation](../plugins/fusion-plugin-openclaw-runtime/README.md).
|
||||||
|
|
||||||
### OpenClaw Gateway Configuration
|
### OpenClaw Runtime Configuration
|
||||||
|
|
||||||
The OpenClaw runtime plugin connects to a running OpenClaw gateway instance through its OpenAI-compatible HTTP API. You can configure the gateway connection using plugin settings or environment variables.
|
The OpenClaw runtime plugin is CLI-first. Fusion invokes `openclaw agent --json` directly and defaults to embedded local mode (`--local`). Gateway mode is optional via `useGateway: true`.
|
||||||
|
|
||||||
| Setting | Type | Default | Description |
|
| Setting | Type | Default | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `gatewayUrl` | `string` | `http://127.0.0.1:18789` | URL of the OpenClaw gateway instance |
|
| `binaryPath` | `string` | `openclaw` | Path to the OpenClaw binary. |
|
||||||
| `gatewayToken` | `string` | (none) | Authentication token for the gateway |
|
| `agentId` | `string` | `"main"` | OpenClaw agent ID used for `--agent`. |
|
||||||
| `agentId` | `string` | `"main"` | OpenClaw agent ID to use for sessions |
|
| `model` | `string` | (OpenClaw default) | Optional model override passed as `--model`. |
|
||||||
|
| `thinking` | `string` | `"off"` | Thinking level passed as `--thinking`. |
|
||||||
|
| `cliTimeoutSec` | `number` | `0` | OpenClaw-side timeout (`--timeout`, 0 = no OpenClaw timeout). |
|
||||||
|
| `cliTimeoutMs` | `number` | `300000` | Fusion-side hard kill timeout for each subprocess turn. |
|
||||||
|
| `useGateway` | `boolean` | `false` | When true, omit `--local` and allow OpenClaw's gateway path. |
|
||||||
|
|
||||||
| Setting | Environment Variable | Default if Unset |
|
| Setting | Environment Variable | Default if Unset |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `gatewayUrl` | `OPENCLAW_GATEWAY_URL` | `http://127.0.0.1:18789` |
|
| `binaryPath` | `OPENCLAW_BIN` | `openclaw` |
|
||||||
| `gatewayToken` | `OPENCLAW_GATEWAY_TOKEN` | (none — unauthenticated) |
|
|
||||||
| `agentId` | `OPENCLAW_AGENT_ID` | `main` |
|
| `agentId` | `OPENCLAW_AGENT_ID` | `main` |
|
||||||
|
| `model` | `OPENCLAW_MODEL` | (OpenClaw default) |
|
||||||
|
| `thinking` | `OPENCLAW_THINKING` | `off` |
|
||||||
|
| `cliTimeoutSec` | `OPENCLAW_TIMEOUT_SEC` | `0` |
|
||||||
|
| `cliTimeoutMs` | `OPENCLAW_CLI_TIMEOUT_MS` | `300000` |
|
||||||
|
| `useGateway` | `OPENCLAW_USE_GATEWAY` | `false` |
|
||||||
|
|
||||||
Resolution priority is: plugin settings (`PluginContext.settings`) → environment variables → built-in defaults.
|
Resolution priority is: plugin settings (`PluginContext.settings`) → environment variables → built-in defaults.
|
||||||
|
|
||||||
> ℹ️ These are **plugin-level** settings configured when the OpenClaw runtime plugin is installed/enabled (for example in the dashboard Plugin Manager or plugin config). They are not agent-level `runtimeConfig` fields. Agents only need `runtimeConfig.runtimeHint: "openclaw"`; gateway connection details are handled by the plugin.
|
> ℹ️ These are **plugin-level** settings configured when the OpenClaw runtime plugin is installed/enabled. They are not agent-level `runtimeConfig` fields. Agents only need `runtimeConfig.runtimeHint: "openclaw"`.
|
||||||
|
|
||||||
> ⚠️ `gatewayToken` is a secret. Never log it or commit it to version control. For production, prefer setting `OPENCLAW_GATEWAY_TOKEN` in the environment.
|
OpenClaw tool-control uses the supported MCP CLI surface (`openclaw mcp set` + profile-scoped `--profile` runs) when custom Fusion tools are present; built-ins (`read`, `write`, `edit`, `bash`, `grep`, `find`) remain filtered from that MCP bridge.
|
||||||
|
|
||||||
For additional gateway/runtime details, see the [OpenClaw Runtime Plugin documentation](../plugins/fusion-plugin-openclaw-runtime/README.md).
|
For runtime details, see the [OpenClaw Runtime Plugin documentation](../plugins/fusion-plugin-openclaw-runtime/README.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -180,6 +180,15 @@ describe("CLI bundle output", () => {
|
|||||||
expect(manifest.name?.length).toBeGreaterThan(0);
|
expect(manifest.name?.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("dist/plugins/fusion-plugin-openclaw-runtime/ is staged with required bridge assets", () => {
|
||||||
|
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-openclaw-runtime");
|
||||||
|
const manifestPath = join(stagedRoot, "manifest.json");
|
||||||
|
|
||||||
|
expect(existsSync(manifestPath)).toBe(true);
|
||||||
|
expect(existsSync(join(stagedRoot, "bundled.js"))).toBe(true);
|
||||||
|
expect(existsSync(join(stagedRoot, "mcp-schema-server.cjs"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("dist/plugins/fusion-plugin-cursor-runtime/ is staged with a valid manifest", () => {
|
it("dist/plugins/fusion-plugin-cursor-runtime/ is staged with a valid manifest", () => {
|
||||||
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-cursor-runtime");
|
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-cursor-runtime");
|
||||||
const manifestPath = join(stagedRoot, "manifest.json");
|
const manifestPath = join(stagedRoot, "manifest.json");
|
||||||
|
|||||||
@@ -214,6 +214,13 @@ export default defineConfig({
|
|||||||
logLevel: "warning",
|
logLevel: "warning",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (pluginId === "fusion-plugin-openclaw-runtime") {
|
||||||
|
const mcpServerAsset = join(pluginSrcDir, "src", "mcp-schema-server.cjs");
|
||||||
|
if (existsSync(mcpServerAsset)) {
|
||||||
|
cpSync(mcpServerAsset, join(pluginDestDir, "mcp-schema-server.cjs"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Bundled runtime plugin ${pluginId} to dist/plugins/${pluginId}/bundled.js`);
|
console.log(`Bundled runtime plugin ${pluginId} to dist/plugins/${pluginId}/bundled.js`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ describe("OpenClaw runtime E2E pipeline", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.includes("mcp") && args.includes("set")) {
|
||||||
|
child.emit("close", 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.includes("agent") && args.includes("--json")) {
|
if (args.includes("agent") && args.includes("--json")) {
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
payloads: [{ text: "OpenClaw response" }],
|
payloads: [{ text: "OpenClaw response" }],
|
||||||
@@ -197,6 +202,13 @@ describe("OpenClaw runtime E2E pipeline", () => {
|
|||||||
cwd: testRoot,
|
cwd: testRoot,
|
||||||
systemPrompt: "You are helpful",
|
systemPrompt: "You are helpful",
|
||||||
tools: "coding",
|
tools: "coding",
|
||||||
|
customTools: [{
|
||||||
|
name: "fn_task_list",
|
||||||
|
label: "fn_task_list",
|
||||||
|
description: "list",
|
||||||
|
parameters: { type: "object" },
|
||||||
|
execute: vi.fn(),
|
||||||
|
} as any],
|
||||||
skills: ["bash"],
|
skills: ["bash"],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,6 +221,22 @@ describe("OpenClaw runtime E2E pipeline", () => {
|
|||||||
"openclaw/openclaw-agent/openclaw/openclaw-agent",
|
"openclaw/openclaw-agent/openclaw/openclaw-agent",
|
||||||
);
|
);
|
||||||
expect(mockCreateFnAgent).not.toHaveBeenCalled();
|
expect(mockCreateFnAgent).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
const spawnArgs = mockSpawn.mock.calls.map(([, args]) => args as string[]);
|
||||||
|
expect(spawnArgs.some((args) => args.includes("mcp") && args.includes("set"))).toBe(true);
|
||||||
|
expect(spawnArgs.some((args) => args.includes("agent") && args.includes("--profile"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps agent argv unchanged when no custom tools are provided", async () => {
|
||||||
|
const adapterModule = await import(pathToFileURL(openClawPluginModulePath()).href);
|
||||||
|
const adapter = new adapterModule.OpenClawRuntimeAdapter({ agentId: "openclaw-agent" });
|
||||||
|
const { session } = await adapter.createSession({ cwd: testRoot, systemPrompt: "sys" });
|
||||||
|
await adapter.promptWithFallback(session, "hello");
|
||||||
|
|
||||||
|
const agentCall = mockSpawn.mock.calls.find(([, args]) => (args as string[]).includes("agent"));
|
||||||
|
expect(agentCall).toBeTruthy();
|
||||||
|
const agentArgs = agentCall?.[1] as string[];
|
||||||
|
expect(agentArgs.includes("--profile")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to default pi runtime when OpenClaw plugin is not installed", async () => {
|
it("falls back to default pi runtime when OpenClaw plugin is not installed", async () => {
|
||||||
|
|||||||
@@ -145,6 +145,14 @@ describe("OpenClaw runtime integration via engine resolution pipeline", () => {
|
|||||||
getRuntimeById: vi.fn().mockReturnValue(registration),
|
getRuntimeById: vi.fn().mockReturnValue(registration),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const customTool = {
|
||||||
|
name: "fn_task_show",
|
||||||
|
label: "fn_task_show",
|
||||||
|
description: "show",
|
||||||
|
parameters: { type: "object" },
|
||||||
|
execute: vi.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
const result = await createResolvedAgentSession({
|
const result = await createResolvedAgentSession({
|
||||||
sessionPurpose: "executor",
|
sessionPurpose: "executor",
|
||||||
runtimeHint: "openclaw",
|
runtimeHint: "openclaw",
|
||||||
@@ -152,6 +160,7 @@ describe("OpenClaw runtime integration via engine resolution pipeline", () => {
|
|||||||
cwd: "/tmp/project",
|
cwd: "/tmp/project",
|
||||||
systemPrompt: "You are helpful",
|
systemPrompt: "You are helpful",
|
||||||
tools: "coding",
|
tools: "coding",
|
||||||
|
customTools: [customTool],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.runtimeId).toBe("openclaw");
|
expect(result.runtimeId).toBe("openclaw");
|
||||||
@@ -162,6 +171,7 @@ describe("OpenClaw runtime integration via engine resolution pipeline", () => {
|
|||||||
cwd: "/tmp/project",
|
cwd: "/tmp/project",
|
||||||
systemPrompt: "You are helpful",
|
systemPrompt: "You are helpful",
|
||||||
tools: "coding",
|
tools: "coding",
|
||||||
|
customTools: [customTool],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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
|
## Prerequisites
|
||||||
|
|
||||||
```bash
|
```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.
|
- **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.
|
- **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.
|
- **AbortSignal sends SIGTERM.** If the CLI ignores it (e.g. during a long model download), the hard-kill timer (`cliTimeoutMs`) eventually fires.
|
||||||
|
|
||||||
## Public API
|
## Public API
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
extractStderrError,
|
extractStderrError,
|
||||||
promptCli,
|
promptCli,
|
||||||
resolveCliConfig,
|
resolveCliConfig,
|
||||||
|
configureOpenClawMcpServer,
|
||||||
} from "../pi-module.js";
|
} from "../pi-module.js";
|
||||||
import type { CliConfig, GatewaySession } from "../types.js";
|
import type { CliConfig, GatewaySession } from "../types.js";
|
||||||
|
|
||||||
@@ -169,23 +170,36 @@ describe("buildOpenClawArgs", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it("puts --no-color first, then agent subcommand", () => {
|
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[0]).toBe("--no-color");
|
||||||
expect(args[1]).toBe("agent");
|
expect(args[1]).toBe("agent");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes --local when useGateway is false", () => {
|
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");
|
expect(args).toContain("--local");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("omits --local when useGateway is true", () => {
|
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");
|
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", () => {
|
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("--json");
|
||||||
expect(args).toContain("--session-id");
|
expect(args).toContain("--session-id");
|
||||||
expect(args[args.indexOf("--session-id") + 1]).toBe("my-uuid");
|
expect(args[args.indexOf("--session-id") + 1]).toBe("my-uuid");
|
||||||
@@ -196,24 +210,24 @@ describe("buildOpenClawArgs", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("includes --model when configured", () => {
|
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).toContain("--model");
|
||||||
expect(args[args.indexOf("--model") + 1]).toBe("anthropic/claude-opus-4-5");
|
expect(args[args.indexOf("--model") + 1]).toBe("anthropic/claude-opus-4-5");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("omits --model when not configured", () => {
|
it("omits --model when not configured", () => {
|
||||||
const args = buildOpenClawArgs(baseConfig, "u", "p");
|
const args = buildOpenClawArgs(baseConfig, { sessionId: "u" }, "p");
|
||||||
expect(args).not.toContain("--model");
|
expect(args).not.toContain("--model");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes --thinking with the configured level", () => {
|
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).toContain("--thinking");
|
||||||
expect(args[args.indexOf("--thinking") + 1]).toBe("high");
|
expect(args[args.indexOf("--thinking") + 1]).toBe("high");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes --timeout with cliTimeoutSec", () => {
|
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).toContain("--timeout");
|
||||||
expect(args[args.indexOf("--timeout") + 1]).toBe("120");
|
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", () => {
|
describe("promptCli", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
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,
|
mockCreateCliSession,
|
||||||
mockPromptCli,
|
mockPromptCli,
|
||||||
mockDescribeCliModel,
|
mockDescribeCliModel,
|
||||||
|
mockConfigureOpenClawMcpServer,
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
mockResolveCliConfig: vi.fn(),
|
mockResolveCliConfig: vi.fn(),
|
||||||
mockCreateCliSession: vi.fn(),
|
mockCreateCliSession: vi.fn(),
|
||||||
mockPromptCli: vi.fn(),
|
mockPromptCli: vi.fn(),
|
||||||
mockDescribeCliModel: vi.fn(),
|
mockDescribeCliModel: vi.fn(),
|
||||||
|
mockConfigureOpenClawMcpServer: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../pi-module.js", () => ({
|
vi.mock("../pi-module.js", () => ({
|
||||||
@@ -19,6 +21,7 @@ vi.mock("../pi-module.js", () => ({
|
|||||||
createCliSession: mockCreateCliSession,
|
createCliSession: mockCreateCliSession,
|
||||||
promptCli: mockPromptCli,
|
promptCli: mockPromptCli,
|
||||||
describeCliModel: mockDescribeCliModel,
|
describeCliModel: mockDescribeCliModel,
|
||||||
|
configureOpenClawMcpServer: mockConfigureOpenClawMcpServer,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -41,6 +44,7 @@ beforeEach(() => {
|
|||||||
callbacks,
|
callbacks,
|
||||||
}));
|
}));
|
||||||
mockDescribeCliModel.mockReturnValue("openclaw/main");
|
mockDescribeCliModel.mockReturnValue("openclaw/main");
|
||||||
|
mockConfigureOpenClawMcpServer.mockResolvedValue(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("OpenClawRuntimeAdapter — identity", () => {
|
describe("OpenClawRuntimeAdapter — identity", () => {
|
||||||
@@ -58,6 +62,48 @@ describe("OpenClawRuntimeAdapter — identity", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("OpenClawRuntimeAdapter — createSession", () => {
|
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 () => {
|
it("delegates to createCliSession with systemPrompt + agentId + callbacks", async () => {
|
||||||
const adapter = new OpenClawRuntimeAdapter({ agentId: "ops" });
|
const adapter = new OpenClawRuntimeAdapter({ agentId: "ops" });
|
||||||
const onText = vi.fn();
|
const onText = vi.fn();
|
||||||
|
|||||||
@@ -82,8 +82,13 @@ export {
|
|||||||
promptCli,
|
promptCli,
|
||||||
describeCliModel,
|
describeCliModel,
|
||||||
extractStderrError,
|
extractStderrError,
|
||||||
|
configureOpenClawMcpServer,
|
||||||
} from "./pi-module.js";
|
} from "./pi-module.js";
|
||||||
export type { CliConfig, GatewaySession, OpenClawAgentJson } from "./types.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.
|
// Probe re-export for the dashboard's runtime-provider-probes façade.
|
||||||
export { probeOpenClawBinary } from "./probe.js";
|
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 { spawn } from "node:child_process";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
import type {
|
import type {
|
||||||
CliConfig,
|
CliConfig,
|
||||||
GatewayCallbacks,
|
GatewayCallbacks,
|
||||||
@@ -101,15 +102,21 @@ export function resolveCliConfig(settings?: Record<string, unknown>): CliConfig
|
|||||||
*/
|
*/
|
||||||
export function buildOpenClawArgs(
|
export function buildOpenClawArgs(
|
||||||
config: CliConfig,
|
config: CliConfig,
|
||||||
sessionId: string,
|
session: Pick<GatewaySession, "sessionId" | "mcpProfile">,
|
||||||
message: string,
|
message: string,
|
||||||
): 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");
|
if (!config.useGateway) args.push("--local");
|
||||||
|
|
||||||
args.push("--json");
|
args.push("--json");
|
||||||
args.push("--session-id", sessionId);
|
args.push("--session-id", session.sessionId);
|
||||||
args.push("--message", message);
|
args.push("--message", message);
|
||||||
args.push("--agent", config.agentId);
|
args.push("--agent", config.agentId);
|
||||||
|
|
||||||
@@ -152,6 +159,8 @@ export function createCliSession(opts: {
|
|||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
callbacks?: GatewayCallbacks;
|
callbacks?: GatewayCallbacks;
|
||||||
|
mcpProfile?: string;
|
||||||
|
mcpConfigPath?: string;
|
||||||
}): GatewaySession {
|
}): GatewaySession {
|
||||||
return {
|
return {
|
||||||
sessionId: randomUUID(),
|
sessionId: randomUUID(),
|
||||||
@@ -161,9 +170,44 @@ export function createCliSession(opts: {
|
|||||||
lastModelDescription: `openclaw/${opts.agentId ?? DEFAULT_AGENT_ID}`,
|
lastModelDescription: `openclaw/${opts.agentId ?? DEFAULT_AGENT_ID}`,
|
||||||
lastUsage: undefined,
|
lastUsage: undefined,
|
||||||
callbacks: opts.callbacks,
|
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
|
// promptCli — spawns openclaw, parses the JSON, fires callbacks
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -175,7 +219,7 @@ export async function promptCli(
|
|||||||
callbacks?: GatewayCallbacks,
|
callbacks?: GatewayCallbacks,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const args = buildOpenClawArgs(config, session.sessionId, message);
|
const args = buildOpenClawArgs(config, session, message);
|
||||||
const cb: GatewayCallbacks = { ...session.callbacks, ...callbacks };
|
const cb: GatewayCallbacks = { ...session.callbacks, ...callbacks };
|
||||||
|
|
||||||
cb.onToolStart?.("openclaw.agent", { sessionId: session.sessionId });
|
cb.onToolStart?.("openclaw.agent", { sessionId: session.sessionId });
|
||||||
|
|||||||
@@ -20,11 +20,14 @@ import type {
|
|||||||
GatewaySession,
|
GatewaySession,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
import {
|
import {
|
||||||
|
configureOpenClawMcpServer,
|
||||||
createCliSession,
|
createCliSession,
|
||||||
describeCliModel,
|
describeCliModel,
|
||||||
promptCli,
|
promptCli,
|
||||||
resolveCliConfig,
|
resolveCliConfig,
|
||||||
} from "./pi-module.js";
|
} from "./pi-module.js";
|
||||||
|
import { toolsToMcpToolDefs, writeOpenClawMcpBridgeFiles, type ToolLike } from "./mcp-config.js";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
export class OpenClawRuntimeAdapter implements AgentRuntime {
|
export class OpenClawRuntimeAdapter implements AgentRuntime {
|
||||||
readonly id = "openclaw";
|
readonly id = "openclaw";
|
||||||
@@ -37,9 +40,32 @@ export class OpenClawRuntimeAdapter implements AgentRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createSession(options: AgentRuntimeOptions): Promise<AgentSessionResult> {
|
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({
|
const session = createCliSession({
|
||||||
systemPrompt: options.systemPrompt,
|
systemPrompt: options.systemPrompt,
|
||||||
agentId: this.config.agentId,
|
agentId: this.config.agentId,
|
||||||
|
mcpProfile,
|
||||||
|
mcpConfigPath,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
onText: options.onText,
|
onText: options.onText,
|
||||||
onThinking: options.onThinking,
|
onThinking: options.onThinking,
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ export interface GatewaySession {
|
|||||||
lastModelDescription: string;
|
lastModelDescription: string;
|
||||||
/** Last-known token usage from the CLI JSON's `meta.agentMeta.usage`. */
|
/** Last-known token usage from the CLI JSON's `meta.agentMeta.usage`. */
|
||||||
lastUsage?: Record<string, number>;
|
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;
|
callbacks?: GatewayCallbacks;
|
||||||
dispose?: () => Promise<void> | void;
|
dispose?: () => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user