fix(mcp): inject --rm flag for Docker MCP containers to prevent accumulation

Docker containers spawned by MCP servers configured with `docker run`
were not cleaned up after being stopped, causing exited containers to
accumulate and fill up Docker storage over time.

Closes #8103
This commit is contained in:
Johnny Amancio
2026-04-07 11:59:31 +02:00
committed by Johnny Eric Amancio
parent 66421421a7
commit def64b8913
2 changed files with 43 additions and 2 deletions
+12 -2
View File
@@ -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<number[]> {
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) => {
@@ -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"])
})
})