diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bd2bcef..10a2b64 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -27,7 +27,10 @@ jobs:
run: yarn build
mcp-direct-tests:
+ if: ${{ secrets.LOCALSTACK_AUTH_TOKEN != '' }}
runs-on: ubuntu-latest
+ env:
+ LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}
steps:
- name: Checkout code
diff --git a/.gitignore b/.gitignore
index 1143204..1e01783 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ terraform.tfstate*
.terraform/
.terraform.lock.hcl
.mcp-test-results/
+.DS_Store
diff --git a/README.md b/README.md
index f236113..b370c7f 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,9 @@ This server eliminates custom scripts and manual LocalStack management with dire
This server provides your AI with dedicated tools for managing your LocalStack environment:
+> [!NOTE]
+> All tools in this MCP server require `LOCALSTACK_AUTH_TOKEN`.
+
| Tool Name | Description | Key Features |
| :-------------------------------------------------------------------------------- | :------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`localstack-management`](./src/tools/localstack-management.ts) | Manages LocalStack runtime operations for AWS and Snowflake stacks | - Execute start, stop, restart, and status checks
- Integrate LocalStack authentication tokens
- Inject custom environment variables
- Verify real-time status and perform health monitoring |
@@ -43,7 +46,7 @@ For other MCP Clients, refer to the [configuration guide](#configuration).
- [LocalStack CLI](https://docs.localstack.cloud/getting-started/installation/#localstack-cli) and Docker installed in your system path
- [`cdklocal`](https://github.com/localstack/aws-cdk-local), [`tflocal`](https://github.com/localstack/terraform-local), or [`samlocal`](https://github.com/localstack/aws-sam-cli-local) installed in your system path for running infrastructure deployment tooling
-- A [valid LocalStack Auth Token](https://docs.localstack.cloud/aws/getting-started/auth-token/) to enable Pro services, IAM Policy Analyzer, Cloud Pods, Chaos Injector, and Extensions tools (**optional**)
+- A [valid LocalStack Auth Token](https://docs.localstack.cloud/aws/getting-started/auth-token/) configured as `LOCALSTACK_AUTH_TOKEN` (**required for all MCP tools**)
- [Node.js v22.x](https://nodejs.org/en/download/) or higher installed in your system path
### Configuration
@@ -55,12 +58,17 @@ Add the following to your MCP client's configuration file (e.g., `~/.cursor/mcp.
"mcpServers": {
"localstack-mcp-server": {
"command": "npx",
- "args": ["-y", "@localstack/localstack-mcp-server"]
+ "args": ["-y", "@localstack/localstack-mcp-server"],
+ "env": {
+ "LOCALSTACK_AUTH_TOKEN": ""
+ }
}
}
}
```
+All LocalStack MCP tools require `LOCALSTACK_AUTH_TOKEN` to be set. You can get your LocalStack Auth Token by following the official [documentation](https://docs.localstack.cloud/aws/getting-started/auth-token/).
+
If you installed from source, change `command` and `args` to point to your local build:
```json
@@ -68,27 +76,10 @@ If you installed from source, change `command` and `args` to point to your local
"mcpServers": {
"localstack-mcp-server": {
"command": "node",
- "args": ["/path/to/your/localstack-mcp-server/dist/stdio.js"]
- }
- }
-}
-```
-
-#### Enabling Licensed Features
-
-To activate LocalStack licensed features, you need to add your LocalStack Auth Token to the environment variables. You can get your LocalStack Auth Token by following the official [documentation](https://docs.localstack.cloud/aws/getting-started/auth-token/).
-
-Here's how to add your LocalStack Auth Token to the environment variables:
-
-```json
-{
- "mcpServers": {
- "localstack-mcp-server": {
- "command": "npx",
- "args": ["-y", "@localstack/localstack-mcp-server"],
+ "args": ["/path/to/your/localstack-mcp-server/dist/stdio.js"],
"env": {
"LOCALSTACK_AUTH_TOKEN": ""
- }
+ }
}
}
}
@@ -98,7 +89,7 @@ Here's how to add your LocalStack Auth Token to the environment variables:
| Variable Name | Description | Default Value |
| ------------- | ----------- | ------------- |
-| `LOCALSTACK_AUTH_TOKEN` | The LocalStack Auth Token to use for the MCP server | None |
+| `LOCALSTACK_AUTH_TOKEN` (**required**) | 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` |
| `MCP_ANALYTICS_DISABLED` | Disable MCP analytics when set to `1` | `0` |
@@ -139,7 +130,7 @@ This repository includes [MCP Server Tester](https://github.com/gleanwork/mcp-se
Notes:
- MCP tests target the local STDIO server command `node dist/stdio.js` by default.
-- `LOCALSTACK_AUTH_TOKEN` is required for the comprehensive Gemini eval suite.
+- `LOCALSTACK_AUTH_TOKEN` is required for all MCP tool usage and test suites.
- You can override the target command with:
- `MCP_TEST_COMMAND`
- `MCP_TEST_ARGS` (space-separated arguments)
diff --git a/server.json b/server.json
index 630aa91..d77d1ed 100644
--- a/server.json
+++ b/server.json
@@ -19,8 +19,8 @@
},
"environment_variables": [
{
- "description": "LocalStack Auth Token (optional for Pro features)",
- "is_required": false,
+ "description": "LocalStack Auth Token (required for all LocalStack MCP tools)",
+ "is_required": true,
"format": "string",
"is_secret": true,
"name": "LOCALSTACK_AUTH_TOKEN"
diff --git a/src/core/preflight.ts b/src/core/preflight.ts
index 6c8fd13..ab6bc12 100644
--- a/src/core/preflight.ts
+++ b/src/core/preflight.ts
@@ -17,7 +17,7 @@ export const requireProFeature = async (feature: ProFeature): Promise {
- if (!process.env.LOCALSTACK_AUTH_TOKEN) {
+ if (!process.env.LOCALSTACK_AUTH_TOKEN?.trim()) {
return ResponseBuilder.error(
"Auth Token Required",
"LOCALSTACK_AUTH_TOKEN is required for this operation."
@@ -27,9 +27,9 @@ export const requireAuthToken = (): ToolResponse | null => {
};
export const runPreflights = async (
- checks: Array>
+ checks: Array>
): Promise => {
- const results = await Promise.all(checks);
+ const results = await Promise.all(checks.map((check) => Promise.resolve(check)));
return results.find((r) => r !== null) || null;
};
diff --git a/src/tools/localstack-aws-client.ts b/src/tools/localstack-aws-client.ts
index c3965f2..17f1e7f 100644
--- a/src/tools/localstack-aws-client.ts
+++ b/src/tools/localstack-aws-client.ts
@@ -1,6 +1,6 @@
import { z } from "zod";
import { type ToolMetadata, type InferSchema } from "xmcp";
-import { runPreflights, requireLocalStackRunning } from "../core/preflight";
+import { runPreflights, requireLocalStackRunning, requireAuthToken } from "../core/preflight";
import { ResponseBuilder } from "../core/response-builder";
import { withToolAnalytics } from "../core/analytics";
import { DockerApiClient } from "../lib/docker/docker.client";
@@ -29,7 +29,7 @@ export const metadata: ToolMetadata = {
export default async function localstackAwsClient({ command }: InferSchema) {
return withToolAnalytics("localstack-aws-client", { command }, async () => {
- const preflightError = await runPreflights([requireLocalStackRunning()]);
+ const preflightError = await runPreflights([requireAuthToken(), requireLocalStackRunning()]);
if (preflightError) return preflightError;
try {
diff --git a/src/tools/localstack-chaos-injector.ts b/src/tools/localstack-chaos-injector.ts
index 4fdd1e4..52e35be 100644
--- a/src/tools/localstack-chaos-injector.ts
+++ b/src/tools/localstack-chaos-injector.ts
@@ -3,7 +3,12 @@ import { type ToolMetadata, type InferSchema } from "xmcp";
import { ProFeature } from "../lib/localstack/license-checker";
import { ChaosApiClient } from "../lib/localstack/localstack.client";
import { ResponseBuilder } from "../core/response-builder";
-import { runPreflights, requireProFeature } from "../core/preflight";
+import {
+ runPreflights,
+ requireAuthToken,
+ requireLocalStackRunning,
+ requireProFeature,
+} from "../core/preflight";
import { withToolAnalytics } from "../core/analytics";
// Define the fault rule schema
@@ -130,7 +135,11 @@ export default async function localstackChaosInjector({
"localstack-chaos-injector",
{ action, rules_count: rules?.length, latency_ms },
async () => {
- const preflightError = await runPreflights([requireProFeature(ProFeature.CHAOS_ENGINEERING)]);
+ const preflightError = await runPreflights([
+ requireAuthToken(),
+ requireLocalStackRunning(),
+ requireProFeature(ProFeature.CHAOS_ENGINEERING),
+ ]);
if (preflightError) return preflightError;
const client = new ChaosApiClient();
diff --git a/src/tools/localstack-cloud-pods.ts b/src/tools/localstack-cloud-pods.ts
index ee2a855..12e0be4 100644
--- a/src/tools/localstack-cloud-pods.ts
+++ b/src/tools/localstack-cloud-pods.ts
@@ -3,7 +3,13 @@ import { type ToolMetadata, type InferSchema } from "xmcp";
import { ProFeature } from "../lib/localstack/license-checker";
import { CloudPodsApiClient } from "../lib/localstack/localstack.client";
import { ResponseBuilder } from "../core/response-builder";
-import { runPreflights, requireLocalStackCli, requireProFeature } from "../core/preflight";
+import {
+ runPreflights,
+ requireAuthToken,
+ requireLocalStackRunning,
+ requireLocalStackCli,
+ requireProFeature,
+} from "../core/preflight";
import { withToolAnalytics } from "../core/analytics";
// Define the schema for tool parameters
@@ -43,6 +49,8 @@ export default async function localstackCloudPods({
}: InferSchema) {
return withToolAnalytics("localstack-cloud-pods", { action, pod_name }, async () => {
const preflightError = await runPreflights([
+ requireAuthToken(),
+ requireLocalStackRunning(),
requireLocalStackCli(),
requireProFeature(ProFeature.CLOUD_PODS),
]);
diff --git a/src/tools/localstack-deployer.ts b/src/tools/localstack-deployer.ts
index f287fa9..21739ab 100644
--- a/src/tools/localstack-deployer.ts
+++ b/src/tools/localstack-deployer.ts
@@ -3,8 +3,11 @@ import { type ToolMetadata, type InferSchema } from "xmcp";
import { runCommand, stripAnsiCodes } from "../core/command-runner";
import path from "path";
import fs from "fs";
-import { ensureLocalStackCli } from "../lib/localstack/localstack.utils";
-import { runPreflights, requireLocalStackRunning } from "../core/preflight";
+import {
+ runPreflights,
+ requireAuthToken,
+ requireLocalStackRunning,
+} from "../core/preflight";
import { DockerApiClient } from "../lib/docker/docker.client";
import {
checkDependencies,
@@ -100,15 +103,8 @@ export default async function localstackDeployer({
"localstack-deployer",
{ action, projectType, directory, stackName, templatePath, variables, s3Bucket, resolveS3, saveParams },
async () => {
- if (action === "deploy" || action === "destroy") {
- const preflightError = await runPreflights([requireLocalStackRunning()]);
- if (preflightError) return preflightError;
- const cliError = await ensureLocalStackCli();
- if (cliError) return cliError;
- } else {
- const preflightError = await runPreflights([requireLocalStackRunning()]);
- if (preflightError) return preflightError;
- }
+ const preflightError = await runPreflights([requireAuthToken(), requireLocalStackRunning()]);
+ if (preflightError) return preflightError;
if (action === "create-stack") {
if (!stackName) {
diff --git a/src/tools/localstack-docs.ts b/src/tools/localstack-docs.ts
index 82eb1eb..483314a 100644
--- a/src/tools/localstack-docs.ts
+++ b/src/tools/localstack-docs.ts
@@ -1,6 +1,7 @@
import { z } from "zod";
import { type ToolMetadata, type InferSchema } from "xmcp";
import { httpClient } from "../core/http-client";
+import { runPreflights, requireAuthToken } from "../core/preflight";
import { ResponseBuilder } from "../core/response-builder";
import { withToolAnalytics } from "../core/analytics";
@@ -38,6 +39,9 @@ export const metadata: ToolMetadata = {
export default async function localstackDocs({ query, limit }: InferSchema) {
return withToolAnalytics("localstack-docs", { query, limit }, async () => {
try {
+ const preflightError = await runPreflights([requireAuthToken()]);
+ if (preflightError) return preflightError;
+
const endpoint = `${CRAWLCHAT_DOCS_ENDPOINT}?query=${encodeURIComponent(query)}`;
const response = await httpClient.request(endpoint, {
method: "GET",
diff --git a/src/tools/localstack-extensions.ts b/src/tools/localstack-extensions.ts
index 002fba1..1dc9bc1 100644
--- a/src/tools/localstack-extensions.ts
+++ b/src/tools/localstack-extensions.ts
@@ -59,6 +59,7 @@ export default async function localstackExtensions({
}: InferSchema) {
return withToolAnalytics("localstack-extensions", { action, name, source }, async () => {
const checks = [
+ requireAuthToken(),
requireLocalStackCli(),
requireLocalStackRunning(),
requireProFeature(ProFeature.EXTENSIONS),
@@ -94,9 +95,6 @@ function combineOutput(stdout: string, stderr: string): string {
}
async function handleList() {
- const authError = requireAuthToken();
- if (authError) return authError;
-
const cmd = await runCommand("localstack", ["extensions", "list"], {
env: { ...process.env },
});
@@ -122,9 +120,6 @@ async function handleList() {
}
async function handleInstall(name?: string, source?: string) {
- const authError = requireAuthToken();
- if (authError) return authError;
-
const hasName = !!name;
const hasSource = !!source;
if ((hasName && hasSource) || (!hasName && !hasSource)) {
@@ -192,9 +187,6 @@ async function handleInstall(name?: string, source?: string) {
}
async function handleUninstall(name?: string) {
- const authError = requireAuthToken();
- if (authError) return authError;
-
if (!name) {
return ResponseBuilder.error(
"Missing Required Parameter",
@@ -242,13 +234,7 @@ async function handleUninstall(name?: string) {
}
async function handleAvailable() {
- const token = process.env.LOCALSTACK_AUTH_TOKEN;
- if (!token) {
- return ResponseBuilder.error(
- "Authentication Failed",
- "Could not fetch the marketplace. Ensure LOCALSTACK_AUTH_TOKEN is set correctly."
- );
- }
+ const token = process.env.LOCALSTACK_AUTH_TOKEN!;
const encoded = Buffer.from(`:${token}`).toString("base64");
const client = new HttpClient();
diff --git a/src/tools/localstack-iam-policy-analyzer.ts b/src/tools/localstack-iam-policy-analyzer.ts
index d17d5ab..8cbae0f 100644
--- a/src/tools/localstack-iam-policy-analyzer.ts
+++ b/src/tools/localstack-iam-policy-analyzer.ts
@@ -10,7 +10,13 @@ import {
generateIamPolicy,
formatPolicyReport,
} from "../lib/iam/iam-policy.logic";
-import { runPreflights, requireLocalStackCli, requireProFeature } from "../core/preflight";
+import {
+ runPreflights,
+ requireAuthToken,
+ requireLocalStackCli,
+ requireLocalStackRunning,
+ requireProFeature,
+} from "../core/preflight";
import { ResponseBuilder } from "../core/response-builder";
import { withToolAnalytics } from "../core/analytics";
@@ -51,7 +57,9 @@ export default async function localstackIamPolicyAnalyzer({
}: InferSchema) {
return withToolAnalytics("localstack-iam-policy-analyzer", { action, mode }, async () => {
const preflightError = await runPreflights([
+ requireAuthToken(),
requireLocalStackCli(),
+ requireLocalStackRunning(),
requireProFeature(ProFeature.IAM_ENFORCEMENT),
]);
if (preflightError) return preflightError;
diff --git a/src/tools/localstack-logs-analysis.ts b/src/tools/localstack-logs-analysis.ts
index 40de61b..672b8a0 100644
--- a/src/tools/localstack-logs-analysis.ts
+++ b/src/tools/localstack-logs-analysis.ts
@@ -1,8 +1,12 @@
import { z } from "zod";
import { type ToolMetadata, type InferSchema } from "xmcp";
-import { ensureLocalStackCli } from "../lib/localstack/localstack.utils";
import { LocalStackLogRetriever, type LogEntry } from "../lib/logs/log-retriever";
-import { runPreflights, requireLocalStackCli } from "../core/preflight";
+import {
+ runPreflights,
+ requireAuthToken,
+ requireLocalStackCli,
+ requireLocalStackRunning,
+} from "../core/preflight";
import { ResponseBuilder } from "../core/response-builder";
import { withToolAnalytics } from "../core/analytics";
@@ -57,7 +61,11 @@ export default async function localstackLogsAnalysis({
"localstack-logs-analysis",
{ analysisType, lines, service, operation, filter },
async () => {
- const preflightError = await runPreflights([requireLocalStackCli()]);
+ const preflightError = await runPreflights([
+ requireAuthToken(),
+ requireLocalStackCli(),
+ requireLocalStackRunning(),
+ ]);
if (preflightError) return preflightError;
const retriever = new LocalStackLogRetriever();
diff --git a/src/tools/localstack-management.ts b/src/tools/localstack-management.ts
index 171e5ae..07c615c 100644
--- a/src/tools/localstack-management.ts
+++ b/src/tools/localstack-management.ts
@@ -44,12 +44,9 @@ export default async function localstackManagement({
envVars,
}: InferSchema) {
return withToolAnalytics("localstack-management", { action, service, envVars }, async () => {
- const checks = [requireLocalStackCli()];
+ const checks = [requireAuthToken(), requireLocalStackCli()];
if (service === "snowflake") {
- const authTokenError = requireAuthToken();
- if (authTokenError) return authTokenError;
-
// `start` can run when no LocalStack runtime is currently up; validate feature after startup.
if (action !== "start") checks.push(requireProFeature(ProFeature.SNOWFLAKE));
}
diff --git a/tests/mcp/direct.spec.mjs b/tests/mcp/direct.spec.mjs
index 6b52fd9..5d792d5 100644
--- a/tests/mcp/direct.spec.mjs
+++ b/tests/mcp/direct.spec.mjs
@@ -12,6 +12,14 @@ const EXPECTED_TOOLS = [
"localstack-docs",
];
+function requireEnv(name) {
+ const value = process.env[name];
+ if (!value || !value.trim()) {
+ throw new Error(`Missing required environment variable: ${name}`);
+ }
+ return value;
+}
+
test("exposes all expected LocalStack MCP tools", async ({ mcp }) => {
const tools = await mcp.listTools();
const toolNames = tools.map((tool) => tool.name);
@@ -22,6 +30,8 @@ test("exposes all expected LocalStack MCP tools", async ({ mcp }) => {
});
test("docs tool returns useful documentation snippets", async ({ mcp }) => {
+ requireEnv("LOCALSTACK_AUTH_TOKEN");
+
const result = await mcp.callTool("localstack-docs", {
query: "How to start LocalStack and configure auth token",
limit: 2,