From ad5a26cf699afd7a387a187aee77bbc7d04793df Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:37:17 -0500 Subject: [PATCH] Require owner gate for MCP chat command --- src/auto-reply/reply/commands-mcp.test.ts | 43 +++++++++++++++++++++++ src/auto-reply/reply/commands-mcp.ts | 5 +++ 2 files changed, 48 insertions(+) diff --git a/src/auto-reply/reply/commands-mcp.test.ts b/src/auto-reply/reply/commands-mcp.test.ts index 3e876c4357b1..24a97f051e5f 100644 --- a/src/auto-reply/reply/commands-mcp.test.ts +++ b/src/auto-reply/reply/commands-mcp.test.ts @@ -84,6 +84,49 @@ describe("handleCommands /mcp", () => { }); }); + it("blocks authorized non-owner senders from writing MCP config", async () => { + await withTempHome("openclaw-command-mcp-home-", async () => { + const workspaceDir = await workspaceHarness.createWorkspace(); + mcpServers.set("existing", { command: "uvx", args: ["existing-mcp"] }); + const setParams = buildCommandTestParams( + '/mcp set evil={"command":"/bin/sh","args":["-c","id > /tmp/pwned"]}', + buildCfg(), + undefined, + { workspaceDir }, + ); + setParams.command.senderIsOwner = false; + + const setResult = expectMcpResult(await handleMcpCommand(setParams, true)); + expect(setResult).toEqual({ shouldContinue: false }); + expect(mcpServers.has("evil")).toBe(false); + + const unsetParams = buildCommandTestParams("/mcp unset existing", buildCfg(), undefined, { + workspaceDir, + }); + unsetParams.command.senderIsOwner = false; + const unsetResult = expectMcpResult(await handleMcpCommand(unsetParams, true)); + expect(unsetResult).toEqual({ shouldContinue: false }); + expect(mcpServers.has("existing")).toBe(true); + }); + }); + + it("blocks authorized non-owner senders from reading MCP config", async () => { + await withTempHome("openclaw-command-mcp-home-", async () => { + const workspaceDir = await workspaceHarness.createWorkspace(); + mcpServers.set("context7", { command: "uvx", args: ["context7-mcp"] }); + const showParams = buildCommandTestParams("/mcp show context7", buildCfg(), undefined, { + workspaceDir, + }); + showParams.command.senderIsOwner = false; + + const showResult = expectMcpResult(await handleMcpCommand(showParams, true)); + expect(showResult).toEqual({ shouldContinue: false }); + const replyText = showResult.reply?.text ?? ""; + expect(replyText).not.toContain('MCP server "context7"'); + expect(replyText).not.toContain('"command": "uvx"'); + }); + }); + it("rejects internal writes without operator.admin", async () => { await withTempHome("openclaw-command-mcp-home-", async () => { const workspaceDir = await workspaceHarness.createWorkspace(); diff --git a/src/auto-reply/reply/commands-mcp.ts b/src/auto-reply/reply/commands-mcp.ts index 2e7025a29658..bea9d307a77d 100644 --- a/src/auto-reply/reply/commands-mcp.ts +++ b/src/auto-reply/reply/commands-mcp.ts @@ -5,6 +5,7 @@ import { unsetConfiguredMcpServer, } from "../../config/mcp-config.js"; import { + rejectNonOwnerCommand, rejectUnauthorizedCommand, requireCommandFlagEnabled, requireGatewayClientScope, @@ -29,6 +30,10 @@ export const handleMcpCommand: CommandHandler = async (params, allowTextCommands if (unauthorized) { return unauthorized; } + const nonOwner = rejectNonOwnerCommand(params, "/mcp"); + if (nonOwner) { + return nonOwner; + } const disabled = requireCommandFlagEnabled(params.cfg, { label: "/mcp", configKey: "mcp",