diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index a828bdaa59d..6d97bc8bace 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -170,6 +170,14 @@ export namespace MCP { return typeof entry === "object" && entry !== null && "type" in entry } + // kilocode_change — exported for testing + export function ensureDockerRm(cmd: string, args: string[]): string[] { + if (cmd !== "docker" || args[0] !== "run") return args + // Always inject --rm right after "run". Docker treats duplicate --rm as + // a no-op, so this is safe even when the user already specified it. + return ["run", "--rm", ...args.slice(1)] + } + async function descendants(pid: number): Promise { if (process.platform === "win32") return [] const pids: number[] = [] @@ -456,11 +464,14 @@ export namespace MCP { if (mcp.type === "local") { const [cmd, ...args] = mcp.command + // kilocode_change — inject --rm for Docker containers to prevent stopped + // containers from accumulating when MCP servers are toggled on/off. + const finalArgs = ensureDockerRm(cmd, args) const cwd = Instance.directory const transport = new StdioClientTransport({ stderr: "pipe", command: cmd, - args, + args: finalArgs, cwd, env: { ...process.env, @@ -590,7 +601,6 @@ export namespace MCP { const s = await state() s.status[name] = result.status if (result.mcpClient) { - // Close existing client if present to prevent memory leaks const existingClient = s.clients[name] if (existingClient) { await existingClient.close().catch((error) => { diff --git a/packages/opencode/test/kilocode/mcp-docker-rm.test.ts b/packages/opencode/test/kilocode/mcp-docker-rm.test.ts new file mode 100644 index 00000000000..b5a0c3e2d4c --- /dev/null +++ b/packages/opencode/test/kilocode/mcp-docker-rm.test.ts @@ -0,0 +1,31 @@ +import { test, expect, describe } from "bun:test" +import { MCP } from "../../src/mcp" + +describe("ensureDockerRm", () => { + test("injects --rm after 'run' for docker run commands", () => { + const result = MCP.ensureDockerRm("docker", ["run", "-i", "my-image"]) + expect(result).toEqual(["run", "--rm", "-i", "my-image"]) + }) + + test("keeps existing --rm and adds another (Docker treats duplicates as no-op)", () => { + const result = MCP.ensureDockerRm("docker", ["run", "--rm", "-i", "my-image"]) + expect(result).toEqual(["run", "--rm", "--rm", "-i", "my-image"]) + }) + + test("does not modify non-docker commands", () => { + const args = ["-y", "@modelcontextprotocol/server-filesystem"] + const result = MCP.ensureDockerRm("npx", args) + expect(result).toBe(args) + }) + + test("does not modify docker commands that are not 'run'", () => { + const args = ["build", "-t", "my-image", "."] + const result = MCP.ensureDockerRm("docker", args) + expect(result).toBe(args) + }) + + test("handles docker run with no additional args", () => { + const result = MCP.ensureDockerRm("docker", ["run"]) + expect(result).toEqual(["run", "--rm"]) + }) +})