Skip to content
Open
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ terraform.tfstate*
.terraform/
.terraform.lock.hcl
.mcp-test-results/
.DS_Store
37 changes: 14 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<br/>- Integrate LocalStack authentication tokens<br/>- Inject custom environment variables<br/>- Verify real-time status and perform health monitoring |
Expand All @@ -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
Expand All @@ -55,40 +58,28 @@ 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": "<YOUR_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
{
"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": "<YOUR_TOKEN>"
}
}
}
}
}
Expand All @@ -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` |

Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions src/core/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const requireProFeature = async (feature: ProFeature): Promise<ToolRespon
};

export const requireAuthToken = (): ToolResponse | null => {
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."
Expand All @@ -27,9 +27,9 @@ export const requireAuthToken = (): ToolResponse | null => {
};

export const runPreflights = async (
checks: Array<Promise<ToolResponse | null>>
checks: Array<ToolResponse | null | Promise<ToolResponse | null>>
): Promise<ToolResponse | null> => {
const results = await Promise.all(checks);
const results = await Promise.all(checks.map((check) => Promise.resolve(check)));
return results.find((r) => r !== null) || null;
};

Expand Down
4 changes: 2 additions & 2 deletions src/tools/localstack-aws-client.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -29,7 +29,7 @@ export const metadata: ToolMetadata = {

export default async function localstackAwsClient({ command }: InferSchema<typeof schema>) {
return withToolAnalytics("localstack-aws-client", { command }, async () => {
const preflightError = await runPreflights([requireLocalStackRunning()]);
const preflightError = await runPreflights([requireAuthToken(), requireLocalStackRunning()]);
if (preflightError) return preflightError;

try {
Expand Down
13 changes: 11 additions & 2 deletions src/tools/localstack-chaos-injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
10 changes: 9 additions & 1 deletion src/tools/localstack-cloud-pods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -43,6 +49,8 @@ export default async function localstackCloudPods({
}: InferSchema<typeof schema>) {
return withToolAnalytics("localstack-cloud-pods", { action, pod_name }, async () => {
const preflightError = await runPreflights([
requireAuthToken(),
requireLocalStackRunning(),
requireLocalStackCli(),
requireProFeature(ProFeature.CLOUD_PODS),
]);
Expand Down
18 changes: 7 additions & 11 deletions src/tools/localstack-deployer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions src/tools/localstack-docs.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -38,6 +39,9 @@ export const metadata: ToolMetadata = {
export default async function localstackDocs({ query, limit }: InferSchema<typeof schema>) {
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<CrawlChatDocsResult[]>(endpoint, {
method: "GET",
Expand Down
18 changes: 2 additions & 16 deletions src/tools/localstack-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default async function localstackExtensions({
}: InferSchema<typeof schema>) {
return withToolAnalytics("localstack-extensions", { action, name, source }, async () => {
const checks = [
requireAuthToken(),
requireLocalStackCli(),
requireLocalStackRunning(),
requireProFeature(ProFeature.EXTENSIONS),
Expand Down Expand Up @@ -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 },
});
Expand All @@ -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)) {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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();
Expand Down
10 changes: 9 additions & 1 deletion src/tools/localstack-iam-policy-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -51,7 +57,9 @@ export default async function localstackIamPolicyAnalyzer({
}: InferSchema<typeof schema>) {
return withToolAnalytics("localstack-iam-policy-analyzer", { action, mode }, async () => {
const preflightError = await runPreflights([
requireAuthToken(),
requireLocalStackCli(),
requireLocalStackRunning(),
requireProFeature(ProFeature.IAM_ENFORCEMENT),
]);
if (preflightError) return preflightError;
Expand Down
14 changes: 11 additions & 3 deletions src/tools/localstack-logs-analysis.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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();
Expand Down
5 changes: 1 addition & 4 deletions src/tools/localstack-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,9 @@ export default async function localstackManagement({
envVars,
}: InferSchema<typeof schema>) {
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));
}
Expand Down
10 changes: 10 additions & 0 deletions tests/mcp/direct.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down