mirror of https://github.com/openclaw/openclaw.git
190 lines
6.0 KiB
TypeScript
190 lines
6.0 KiB
TypeScript
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 {
|
|
defaultRuntime.error(message);
|
|
defaultRuntime.exit(1);
|
|
throw new Error(message);
|
|
}
|
|
|
|
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 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")
|
|
.description("List configured MCP servers")
|
|
.option("--json", "Print JSON")
|
|
.action(async (opts: { json?: boolean }) => {
|
|
const loaded = await listConfiguredMcpServers();
|
|
if (!loaded.ok) {
|
|
fail(loaded.error);
|
|
}
|
|
if (opts.json) {
|
|
printJson(loaded.mcpServers);
|
|
return;
|
|
}
|
|
const names = Object.keys(loaded.mcpServers).toSorted();
|
|
if (names.length === 0) {
|
|
defaultRuntime.log(`No MCP servers configured in ${loaded.path}.`);
|
|
return;
|
|
}
|
|
defaultRuntime.log(`MCP servers (${loaded.path}):`);
|
|
for (const name of names) {
|
|
defaultRuntime.log(`- ${name}`);
|
|
}
|
|
});
|
|
|
|
mcp
|
|
.command("show")
|
|
.description("Show one configured MCP server or the full MCP config")
|
|
.argument("[name]", "MCP server name")
|
|
.option("--json", "Print JSON")
|
|
.action(async (name: string | undefined, opts: { json?: boolean }) => {
|
|
const loaded = await listConfiguredMcpServers();
|
|
if (!loaded.ok) {
|
|
fail(loaded.error);
|
|
}
|
|
const value = name ? loaded.mcpServers[name] : loaded.mcpServers;
|
|
if (name && !value) {
|
|
fail(`No MCP server named "${name}" in ${loaded.path}.`);
|
|
}
|
|
if (opts.json) {
|
|
printJson(value ?? {});
|
|
return;
|
|
}
|
|
if (name) {
|
|
defaultRuntime.log(`MCP server "${name}" (${loaded.path}):`);
|
|
} else {
|
|
defaultRuntime.log(`MCP servers (${loaded.path}):`);
|
|
}
|
|
printJson(value ?? {});
|
|
});
|
|
|
|
mcp
|
|
.command("set")
|
|
.description("Set one configured MCP server from a JSON object")
|
|
.argument("<name>", "MCP server name")
|
|
.argument("<value>", 'JSON object, for example {"command":"uvx","args":["context7-mcp"]}')
|
|
.action(async (name: string, rawValue: string) => {
|
|
const parsed = parseConfigValue(rawValue);
|
|
if (parsed.error) {
|
|
fail(parsed.error);
|
|
}
|
|
const result = await setConfiguredMcpServer({ name, server: parsed.value });
|
|
if (!result.ok) {
|
|
fail(result.error);
|
|
}
|
|
defaultRuntime.log(`Saved MCP server "${name}" to ${result.path}.`);
|
|
});
|
|
|
|
mcp
|
|
.command("unset")
|
|
.description("Remove one configured MCP server")
|
|
.argument("<name>", "MCP server name")
|
|
.action(async (name: string) => {
|
|
const result = await unsetConfiguredMcpServer({ name });
|
|
if (!result.ok) {
|
|
fail(result.error);
|
|
}
|
|
if (!result.removed) {
|
|
fail(`No MCP server named "${name}" in ${result.path}.`);
|
|
}
|
|
defaultRuntime.log(`Removed MCP server "${name}" from ${result.path}.`);
|
|
});
|
|
}
|