openclaw/src/auto-reply/reply/commands-mcp.ts

135 lines
3.8 KiB
TypeScript

import {
listConfiguredMcpServers,
setConfiguredMcpServer,
unsetConfiguredMcpServer,
} from "../../config/mcp-config.js";
import { isInternalMessageChannel } from "../../utils/message-channel.js";
import {
rejectNonOwnerCommand,
rejectUnauthorizedCommand,
requireCommandFlagEnabled,
requireGatewayClientScopeForInternalChannel,
} from "./command-gates.js";
import type { CommandHandler } from "./commands-types.js";
import { parseMcpCommand } from "./mcp-commands.js";
function renderJsonBlock(label: string, value: unknown): string {
return `${label}\n\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
}
export const handleMcpCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) {
return null;
}
const mcpCommand = parseMcpCommand(params.command.commandBodyNormalized);
if (!mcpCommand) {
return null;
}
const unauthorized = rejectUnauthorizedCommand(params, "/mcp");
if (unauthorized) {
return unauthorized;
}
const allowInternalReadOnlyShow =
mcpCommand.action === "show" && isInternalMessageChannel(params.command.channel);
const nonOwner = allowInternalReadOnlyShow ? null : rejectNonOwnerCommand(params, "/mcp");
if (nonOwner) {
return nonOwner;
}
const disabled = requireCommandFlagEnabled(params.cfg, {
label: "/mcp",
configKey: "mcp",
});
if (disabled) {
return disabled;
}
if (mcpCommand.action === "error") {
return {
shouldContinue: false,
reply: { text: `⚠️ ${mcpCommand.message}` },
};
}
if (mcpCommand.action === "show") {
const loaded = await listConfiguredMcpServers();
if (!loaded.ok) {
return {
shouldContinue: false,
reply: { text: `⚠️ ${loaded.error}` },
};
}
if (mcpCommand.name) {
const server = loaded.mcpServers[mcpCommand.name];
if (!server) {
return {
shouldContinue: false,
reply: { text: `🔌 No MCP server named "${mcpCommand.name}" in ${loaded.path}.` },
};
}
return {
shouldContinue: false,
reply: {
text: renderJsonBlock(`🔌 MCP server "${mcpCommand.name}" (${loaded.path})`, server),
},
};
}
if (Object.keys(loaded.mcpServers).length === 0) {
return {
shouldContinue: false,
reply: { text: `🔌 No MCP servers configured in ${loaded.path}.` },
};
}
return {
shouldContinue: false,
reply: {
text: renderJsonBlock(`🔌 MCP servers (${loaded.path})`, loaded.mcpServers),
},
};
}
const missingAdminScope = requireGatewayClientScopeForInternalChannel(params, {
label: "/mcp write",
allowedScopes: ["operator.admin"],
missingText: "❌ /mcp set|unset requires operator.admin for gateway clients.",
});
if (missingAdminScope) {
return missingAdminScope;
}
if (mcpCommand.action === "set") {
const result = await setConfiguredMcpServer({
name: mcpCommand.name,
server: mcpCommand.value,
});
if (!result.ok) {
return {
shouldContinue: false,
reply: { text: `⚠️ ${result.error}` },
};
}
return {
shouldContinue: false,
reply: {
text: `🔌 MCP server "${mcpCommand.name}" saved to ${result.path}.`,
},
};
}
const result = await unsetConfiguredMcpServer({ name: mcpCommand.name });
if (!result.ok) {
return {
shouldContinue: false,
reply: { text: `⚠️ ${result.error}` },
};
}
if (!result.removed) {
return {
shouldContinue: false,
reply: { text: `🔌 No MCP server named "${mcpCommand.name}" in ${result.path}.` },
};
}
return {
shouldContinue: false,
reply: { text: `🔌 MCP server "${mcpCommand.name}" removed from ${result.path}.` },
};
};