Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
25 changes: 25 additions & 0 deletions src/lib/docker/docker.client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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"] }]);
Expand Down
42 changes: 32 additions & 10 deletions src/lib/docker/docker.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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(
Expand Down