From be90867caf4421987ede551b10f3527aecd63d68 Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Sun, 15 Mar 2026 23:09:53 +0530 Subject: [PATCH] Gate all LocalStack MCP tools behind LOCALSTACK_AUTH_TOKEN --- .github/workflows/ci.yml | 3 ++ .gitignore | 1 + README.md | 37 ++++++++------------- server.json | 4 +-- src/core/preflight.ts | 6 ++-- src/tools/localstack-aws-client.ts | 4 +-- src/tools/localstack-chaos-injector.ts | 13 ++++++-- src/tools/localstack-cloud-pods.ts | 10 +++++- src/tools/localstack-deployer.ts | 18 ++++------ src/tools/localstack-docs.ts | 4 +++ src/tools/localstack-extensions.ts | 18 ++-------- src/tools/localstack-iam-policy-analyzer.ts | 10 +++++- src/tools/localstack-logs-analysis.ts | 14 ++++++-- src/tools/localstack-management.ts | 5 +-- tests/mcp/direct.spec.mjs | 10 ++++++ 15 files changed, 89 insertions(+), 68 deletions(-) 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,