From 6ce6d0978a0d6c4f465d3b0648899241dd0f9387 Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Mon, 2 Mar 2026 17:02:16 +0530 Subject: [PATCH] fix(docker): make LocalStack container lookup explicitly configurable --- README.md | 7 +++++ src/lib/docker/docker.client.test.ts | 25 +++++++++++++++++ src/lib/docker/docker.client.ts | 42 +++++++++++++++++++++------- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 39f7f13..dbe4bc6 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,13 @@ Here's how to add your LocalStack Auth Token to the environment variables: } ``` +## LocalStack Configuration + +| Variable Name | Description | Default Value | +| ------------- | ----------- | ------------- | +| `LOCALSTACK_AUTH_TOKEN` | The LocalStack Auth Token to use for the MCP server | None | +| `MAIN_CONTAINER_NAME` | The name of the LocalStack container to use for the MCP server | `localstack-main` | + ## Contributing Pull requests are welcomed on GitHub! To get started: diff --git a/src/lib/docker/docker.client.test.ts b/src/lib/docker/docker.client.test.ts index 41df07a..8c135e9 100644 --- a/src/lib/docker/docker.client.test.ts +++ b/src/lib/docker/docker.client.test.ts @@ -57,6 +57,8 @@ describe("DockerApiClient", () => { mocks.getContainer.mockImplementation(() => ({ exec: mocks.exec })); mocks.exec.mockImplementation(async () => ({ start: mocks.start, inspect: mocks.execInspect })); mocks.__state.demuxTarget = "stdout"; + delete process.env.MAIN_CONTAINER_NAME; + delete process.env.LOCALSTACK_MAIN_CONTAINER_NAME; }); test("findLocalStackContainer throws when none found", async () => { @@ -77,6 +79,29 @@ describe("DockerApiClient", () => { await expect(client.findLocalStackContainer()).resolves.toBe("abc123"); }); + test("findLocalStackContainer matches MAIN_CONTAINER_NAME when configured", async () => { + process.env.MAIN_CONTAINER_NAME = "my-custom-localstack"; + + const mocks = getDockerMocks(); + mocks.listContainers.mockResolvedValueOnce([ + { Id: "not-this", Names: ["/localstack-main"] }, + { Id: "xyz999", Names: ["/my-custom-localstack"] }, + ]); + + const client = new DockerApiClient(); + await expect(client.findLocalStackContainer()).resolves.toBe("xyz999"); + }); + + test("findLocalStackContainer throws when only compose-prefixed name exists without config", async () => { + const mocks = getDockerMocks(); + mocks.listContainers.mockResolvedValueOnce([{ Id: "compose123", Names: ["/project-localstack-1"] }]); + + const client = new DockerApiClient(); + await expect(client.findLocalStackContainer()).rejects.toThrow( + /Could not find a running LocalStack container named "localstack-main"/i + ); + }); + test("executeInContainer returns stdout on success", async () => { const mocks = getDockerMocks(); mocks.listContainers.mockResolvedValueOnce([{ Id: "abc123", Names: ["/localstack-main"] }]); diff --git a/src/lib/docker/docker.client.ts b/src/lib/docker/docker.client.ts index 63d31b6..373bf12 100644 --- a/src/lib/docker/docker.client.ts +++ b/src/lib/docker/docker.client.ts @@ -14,21 +14,43 @@ export class DockerApiClient { this.docker = new DockerCtor(); } + private normalizeContainerName(name?: string): string { + if (!name) return ""; + return name.startsWith("/") ? name.slice(1) : name; + } + + private matchesConfiguredContainerName( + container: { Names?: string[] }, + configuredName: string + ): boolean { + return (container.Names || []).some( + (n) => this.normalizeContainerName(n) === configuredName + ); + } + async findLocalStackContainer(): Promise { const running = (await (this.docker.listContainers as any)({ filters: { status: ["running"] }, - })) as Array<{ Id: string; Names?: string[] }>; - - const match = (running || []).find((c) => - (c.Names || []).some((n) => { - const name = (n || "").startsWith("/") ? n.slice(1) : n; - return name === "localstack-main"; - }) + })) as Array<{ + Id: string; + Names?: string[]; + }>; + + const configuredName = ( + process.env.MAIN_CONTAINER_NAME || + process.env.LOCALSTACK_MAIN_CONTAINER_NAME || + "localstack-main" + ).trim(); + + const byConfiguredName = (running || []).find((c) => + this.matchesConfiguredContainerName(c, configuredName) ); + if (byConfiguredName) return byConfiguredName.Id as string; - if (match) return match.Id as string; - - throw new Error("Could not find a running LocalStack container named 'localstack-main'."); + throw new Error( + `Could not find a running LocalStack container named "${configuredName}". ` + + `Set MAIN_CONTAINER_NAME to your container name if it is custom.` + ); } async executeInContainer(