mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 19:32:27 +00:00
feat: add openclaw channel mcp bridge
This commit is contained in:
@@ -9,11 +9,16 @@ import { createCliRuntimeCapture } from "./test-runtime-capture.js";
|
||||
const { defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
|
||||
const mockLog = defaultRuntime.log;
|
||||
const mockError = defaultRuntime.error;
|
||||
const serveOpenClawChannelMcp = vi.fn();
|
||||
|
||||
vi.mock("../runtime.js", () => ({
|
||||
defaultRuntime,
|
||||
}));
|
||||
|
||||
vi.mock("../mcp/channel-server.js", () => ({
|
||||
serveOpenClawChannelMcp,
|
||||
}));
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function createWorkspace(): Promise<string> {
|
||||
@@ -74,4 +79,33 @@ describe("mcp cli", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("starts the channel bridge with parsed serve options", async () => {
|
||||
await withTempHome("openclaw-cli-mcp-home-", async () => {
|
||||
const workspaceDir = await createWorkspace();
|
||||
const tokenFile = path.join(workspaceDir, "gateway.token");
|
||||
vi.spyOn(process, "cwd").mockReturnValue(workspaceDir);
|
||||
await fs.writeFile(tokenFile, "secret-token\n", "utf-8");
|
||||
|
||||
await runMcpCommand([
|
||||
"mcp",
|
||||
"serve",
|
||||
"--url",
|
||||
"ws://127.0.0.1:18789",
|
||||
"--token-file",
|
||||
tokenFile,
|
||||
"--claude-channel-mode",
|
||||
"on",
|
||||
"--verbose",
|
||||
]);
|
||||
|
||||
expect(serveOpenClawChannelMcp).toHaveBeenCalledWith({
|
||||
gatewayUrl: "ws://127.0.0.1:18789",
|
||||
gatewayToken: "secret-token",
|
||||
gatewayPassword: undefined,
|
||||
claudeChannelMode: "on",
|
||||
verbose: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Command } from "commander";
|
||||
import { readSecretFromFile } from "../acp/secret-file.js";
|
||||
import { parseConfigValue } from "../auto-reply/reply/config-value.js";
|
||||
import {
|
||||
listConfiguredMcpServers,
|
||||
setConfiguredMcpServer,
|
||||
unsetConfiguredMcpServer,
|
||||
} from "../config/mcp-config.js";
|
||||
import { serveOpenClawChannelMcp } from "../mcp/channel-server.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
|
||||
function fail(message: string): never {
|
||||
@@ -17,8 +19,91 @@ function printJson(value: unknown): void {
|
||||
defaultRuntime.writeJson(value);
|
||||
}
|
||||
|
||||
function resolveSecretOption(params: {
|
||||
direct?: string;
|
||||
file?: string;
|
||||
directFlag: string;
|
||||
fileFlag: string;
|
||||
label: string;
|
||||
}) {
|
||||
const direct = params.direct?.trim();
|
||||
const file = params.file?.trim();
|
||||
if (direct && file) {
|
||||
throw new Error(`Use either ${params.directFlag} or ${params.fileFlag} for ${params.label}.`);
|
||||
}
|
||||
if (file) {
|
||||
return readSecretFromFile(file, params.label);
|
||||
}
|
||||
return direct || undefined;
|
||||
}
|
||||
|
||||
function warnSecretCliFlag(flag: "--token" | "--password") {
|
||||
defaultRuntime.error(
|
||||
`Warning: ${flag} can be exposed via process listings. Prefer ${flag}-file or environment variables.`,
|
||||
);
|
||||
}
|
||||
|
||||
export function registerMcpCli(program: Command) {
|
||||
const mcp = program.command("mcp").description("Manage OpenClaw MCP server config");
|
||||
const mcp = program.command("mcp").description("Manage OpenClaw MCP config and channel bridge");
|
||||
|
||||
mcp
|
||||
.command("serve")
|
||||
.description("Expose OpenClaw channels over MCP stdio")
|
||||
.option("--url <url>", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)")
|
||||
.option("--token <token>", "Gateway token (if required)")
|
||||
.option("--token-file <path>", "Read gateway token from file")
|
||||
.option("--password <password>", "Gateway password (if required)")
|
||||
.option("--password-file <path>", "Read gateway password from file")
|
||||
.option(
|
||||
"--claude-channel-mode <mode>",
|
||||
"Claude channel notification mode: auto, on, or off",
|
||||
"auto",
|
||||
)
|
||||
.option("-v, --verbose", "Verbose logging to stderr", false)
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
const gatewayToken = resolveSecretOption({
|
||||
direct: opts.token as string | undefined,
|
||||
file: opts.tokenFile as string | undefined,
|
||||
directFlag: "--token",
|
||||
fileFlag: "--token-file",
|
||||
label: "Gateway token",
|
||||
});
|
||||
const gatewayPassword = resolveSecretOption({
|
||||
direct: opts.password as string | undefined,
|
||||
file: opts.passwordFile as string | undefined,
|
||||
directFlag: "--password",
|
||||
fileFlag: "--password-file",
|
||||
label: "Gateway password",
|
||||
});
|
||||
if (opts.token) {
|
||||
warnSecretCliFlag("--token");
|
||||
}
|
||||
if (opts.password) {
|
||||
warnSecretCliFlag("--password");
|
||||
}
|
||||
const claudeChannelMode = String(opts.claudeChannelMode ?? "auto")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (
|
||||
claudeChannelMode !== "auto" &&
|
||||
claudeChannelMode !== "on" &&
|
||||
claudeChannelMode !== "off"
|
||||
) {
|
||||
throw new Error("Invalid --claude-channel-mode value. Use auto, on, or off.");
|
||||
}
|
||||
await serveOpenClawChannelMcp({
|
||||
gatewayUrl: opts.url as string | undefined,
|
||||
gatewayToken,
|
||||
gatewayPassword,
|
||||
claudeChannelMode,
|
||||
verbose: Boolean(opts.verbose),
|
||||
});
|
||||
} catch (err) {
|
||||
defaultRuntime.error(String(err));
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
mcp
|
||||
.command("list")
|
||||
|
||||
@@ -151,7 +151,7 @@ const coreEntries: CoreCliEntry[] = [
|
||||
commands: [
|
||||
{
|
||||
name: "mcp",
|
||||
description: "Manage embedded Pi MCP servers",
|
||||
description: "Manage OpenClaw MCP config and channel bridge",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user