diff --git a/README.md b/README.md
index 39f7f13..20fdb49 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ This server provides your AI with dedicated tools for managing your LocalStack e
| Tool Name | Description | Key Features |
| :-------------------------------------------------------------------------------- | :------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| [`localstack-management`](./src/tools/localstack-management.ts) | Manages LocalStack for AWS container operations and settings | - Execute start, stop, restart, and status checks
- Integrate LocalStack authentication tokens
- Inject custom environment variables
- Verify real-time status and perform health monitoring |
+| [`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 |
| [`localstack-deployer`](./src/tools/localstack-deployer.ts) | Handles infrastructure deployment to LocalStack for AWS environments | - Automatically run CDK and Terraform tooling to deploy infrastructure locally
- Enable parameterized deployments with variable support
- Process and present deployment results
- Requires you to have [`cdklocal`](https://github.com/localstack/aws-cdk-local) or [`tflocal`](https://github.com/localstack/terraform-local) installed in your system path |
| [`localstack-logs-analysis`](./src/tools/localstack-logs-analysis.ts) | Analyzes LocalStack for AWS logs for troubleshooting and insights | - Offer multiple analysis options including summaries, errors, requests, and raw data
- Filter by specific services and operations
- Generate API call metrics and failure breakdowns
- Group errors intelligently and identify patterns |
| [`localstack-iam-policy-analyzer`](./src/tools/localstack-iam-policy-analyzer.ts) | Handles IAM policy management and violation remediation | - Set IAM enforcement levels including `enforced`, `soft`, and `disabled` modes
- Search logs for permission-related violations
- Generate IAM policies automatically from detected access failures
- Requires a valid LocalStack Auth Token |
diff --git a/src/core/preflight.ts b/src/core/preflight.ts
index 0151738..6c8fd13 100644
--- a/src/core/preflight.ts
+++ b/src/core/preflight.ts
@@ -16,6 +16,16 @@ export const requireProFeature = async (feature: ProFeature): Promise {
+ if (!process.env.LOCALSTACK_AUTH_TOKEN) {
+ return ResponseBuilder.error(
+ "Auth Token Required",
+ "LOCALSTACK_AUTH_TOKEN is required for this operation."
+ );
+ }
+ return null;
+};
+
export const runPreflights = async (
checks: Array>
): Promise => {
diff --git a/src/lib/localstack/license-checker.ts b/src/lib/localstack/license-checker.ts
index af7cf9e..2ce823f 100644
--- a/src/lib/localstack/license-checker.ts
+++ b/src/lib/localstack/license-checker.ts
@@ -8,6 +8,7 @@ export enum ProFeature {
CLOUD_PODS = "localstack.platform.plugin/pods",
CHAOS_ENGINEERING = "localstack.platform.plugin/chaos",
EXTENSIONS = "localstack.platform.plugin/extensions",
+ SNOWFLAKE = "localstack.aws.provider/snowflake:pro",
}
export interface LicenseCheckResult {
diff --git a/src/lib/localstack/localstack.utils.test.ts b/src/lib/localstack/localstack.utils.test.ts
new file mode 100644
index 0000000..7703fb1
--- /dev/null
+++ b/src/lib/localstack/localstack.utils.test.ts
@@ -0,0 +1,51 @@
+import { getLocalStackStatus, getSnowflakeEmulatorStatus } from "./localstack.utils";
+import { runCommand } from "../../core/command-runner";
+
+jest.mock("../../core/command-runner", () => ({
+ runCommand: jest.fn(),
+}));
+
+const mockedRunCommand = runCommand as jest.MockedFunction;
+
+describe("localstack.utils", () => {
+ beforeEach(() => {
+ mockedRunCommand.mockReset();
+ });
+
+ test("getLocalStackStatus marks instance as running and ready", async () => {
+ mockedRunCommand.mockResolvedValueOnce({
+ stdout: "Runtime status: running (Ready)",
+ stderr: "",
+ exitCode: 0,
+ } as any);
+
+ const result = await getLocalStackStatus();
+ expect(result.isRunning).toBe(true);
+ expect(result.isReady).toBe(true);
+ });
+
+ test("getSnowflakeEmulatorStatus marks emulator healthy on success payload", async () => {
+ mockedRunCommand.mockResolvedValueOnce({
+ stdout: '{"success": true}',
+ stderr: "",
+ exitCode: 0,
+ } as any);
+
+ const result = await getSnowflakeEmulatorStatus();
+ expect(result.isRunning).toBe(true);
+ expect(result.isReady).toBe(true);
+ expect(result.statusOutput).toContain('"success": true');
+ });
+
+ test("getSnowflakeEmulatorStatus reports unhealthy response", async () => {
+ mockedRunCommand.mockResolvedValueOnce({
+ stdout: '{"success": false}',
+ stderr: "",
+ exitCode: 0,
+ } as any);
+
+ const result = await getSnowflakeEmulatorStatus();
+ expect(result.isRunning).toBe(false);
+ expect(result.isReady).toBe(false);
+ });
+});
diff --git a/src/lib/localstack/localstack.utils.ts b/src/lib/localstack/localstack.utils.ts
index 39656a0..bb2a0ea 100644
--- a/src/lib/localstack/localstack.utils.ts
+++ b/src/lib/localstack/localstack.utils.ts
@@ -1,4 +1,6 @@
+import { spawn } from "child_process";
import { runCommand } from "../../core/command-runner";
+import { ResponseBuilder } from "../../core/response-builder";
export interface LocalStackCliCheckResult {
isAvailable: boolean;
@@ -44,6 +46,19 @@ export interface LocalStackStatusResult {
isReady?: boolean;
}
+export interface SnowflakeStatusResult {
+ isRunning: boolean;
+ statusOutput?: string;
+ errorMessage?: string;
+ isReady?: boolean;
+}
+
+export interface RuntimeStatus {
+ isRunning: boolean;
+ isReady?: boolean;
+ statusOutput?: string;
+}
+
/**
* Get LocalStack status information
* @returns Promise with status details including running state and raw output
@@ -68,6 +83,144 @@ export async function getLocalStackStatus(): Promise {
}
}
+/**
+ * Get Snowflake emulator status information by checking the Snowflake session endpoint
+ * @returns Promise with status details including running state and raw output
+ */
+export async function getSnowflakeEmulatorStatus(): Promise {
+ try {
+ const { stdout, stderr, error, exitCode } = await runCommand("curl", [
+ "-sS",
+ "-X",
+ "POST",
+ "-H",
+ "Content-Type: application/json",
+ "-d",
+ "{}",
+ "snowflake.localhost.localstack.cloud:4566/session",
+ ]);
+
+ const output = (stdout || "").trim();
+ const isSuccess = /"success"\s*:\s*true/.test(output);
+
+ return {
+ isRunning: exitCode === 0 && isSuccess,
+ isReady: exitCode === 0 && isSuccess,
+ statusOutput: output || stderr.trim(),
+ ...(error
+ ? {
+ errorMessage: `Failed to reach Snowflake emulator endpoint: ${error.message}`,
+ }
+ : {}),
+ };
+ } catch (error) {
+ return {
+ isRunning: false,
+ errorMessage: `Failed to get Snowflake emulator status: ${
+ error instanceof Error ? error.message : String(error)
+ }`,
+ };
+ }
+}
+
+/**
+ * Start a LocalStack runtime flavor and poll until it becomes available.
+ * Supports custom startup args (e.g. default stack vs Snowflake stack), optional
+ * environment overrides, and optional post-start validation hooks.
+ */
+export async function startRuntime({
+ startArgs,
+ getStatus,
+ processLabel,
+ alreadyRunningMessage,
+ successTitle,
+ statusHeading,
+ timeoutMessage,
+ envVars,
+ onReady,
+}: {
+ startArgs: string[];
+ getStatus: () => Promise;
+ processLabel: string;
+ alreadyRunningMessage: string;
+ successTitle: string;
+ statusHeading: string;
+ timeoutMessage: string;
+ envVars?: Record;
+ onReady?: () => Promise | null>;
+}) {
+ const statusCheck = await getStatus();
+ if (statusCheck.isReady || statusCheck.isRunning) {
+ return ResponseBuilder.markdown(alreadyRunningMessage);
+ }
+
+ const environment = { ...process.env, ...(envVars || {}) } as Record;
+ if (process.env.LOCALSTACK_AUTH_TOKEN) {
+ environment.LOCALSTACK_AUTH_TOKEN = process.env.LOCALSTACK_AUTH_TOKEN;
+ }
+
+ return new Promise((resolve) => {
+ const child = spawn("localstack", startArgs, {
+ env: environment,
+ stdio: ["ignore", "ignore", "pipe"],
+ });
+
+ let stderr = "";
+ child.stderr.on("data", (data) => {
+ stderr += data.toString();
+ });
+
+ let earlyExit = false;
+ let poll: NodeJS.Timeout;
+ child.on("error", (err) => {
+ earlyExit = true;
+ if (poll) clearInterval(poll);
+ resolve(ResponseBuilder.markdown(`❌ Failed to start ${processLabel} process: ${err.message}`));
+ });
+
+ child.on("close", (code) => {
+ if (earlyExit) return;
+ if (poll) clearInterval(poll);
+ if (code !== 0) {
+ resolve(
+ ResponseBuilder.markdown(
+ `❌ ${processLabel} process exited unexpectedly with code ${code}.\n\nStderr:\n${stderr}`
+ )
+ );
+ }
+ });
+
+ const pollInterval = 5000;
+ const maxWaitTime = 120000;
+ let timeWaited = 0;
+
+ poll = setInterval(async () => {
+ timeWaited += pollInterval;
+ const status = await getStatus();
+ if (status.isReady || status.isRunning) {
+ if (onReady) {
+ const preflight = await onReady();
+ if (preflight) {
+ clearInterval(poll);
+ resolve(preflight);
+ return;
+ }
+ }
+
+ clearInterval(poll);
+ let resultMessage = `${successTitle}\n\n`;
+ if (envVars)
+ resultMessage += `✅ Custom environment variables applied: ${Object.keys(envVars).join(", ")}\n`;
+ if (status.statusOutput) resultMessage += `\n**${statusHeading}:**\n${status.statusOutput}`;
+ resolve(ResponseBuilder.markdown(resultMessage));
+ } else if (timeWaited >= maxWaitTime) {
+ clearInterval(poll);
+ resolve(ResponseBuilder.markdown(timeoutMessage));
+ }
+ }, pollInterval);
+ });
+}
+
/**
* Validate LocalStack CLI availability and return early if not available
* This is a helper function for tools that require LocalStack CLI
diff --git a/src/tools/localstack-extensions.ts b/src/tools/localstack-extensions.ts
index 977899e..ec50885 100644
--- a/src/tools/localstack-extensions.ts
+++ b/src/tools/localstack-extensions.ts
@@ -7,6 +7,7 @@ import {
requireLocalStackCli,
requireLocalStackRunning,
requireProFeature,
+ requireAuthToken,
} from "../core/preflight";
import { ResponseBuilder } from "../core/response-builder";
import { ProFeature } from "../lib/localstack/license-checker";
@@ -50,9 +51,6 @@ interface MarketplaceExtension {
version?: string;
}
-const AUTH_TOKEN_REQUIRED_MESSAGE =
- "LOCALSTACK_AUTH_TOKEN is not set in your environment. LocalStack Extensions require a valid Auth Token. Please set it and try again.";
-
export default async function localstackExtensions({
action,
name,
@@ -81,13 +79,6 @@ export default async function localstackExtensions({
}
}
-function requireAuthTokenForCli() {
- if (!process.env.LOCALSTACK_AUTH_TOKEN) {
- return ResponseBuilder.error("Auth Token Required", AUTH_TOKEN_REQUIRED_MESSAGE);
- }
- return null;
-}
-
function cleanOutput(stdout: string, stderr: string) {
return {
stdout: stripAnsiCodes(stdout || "").trim(),
@@ -100,7 +91,7 @@ function combineOutput(stdout: string, stderr: string): string {
}
async function handleList() {
- const authError = requireAuthTokenForCli();
+ const authError = requireAuthToken();
if (authError) return authError;
const cmd = await runCommand("localstack", ["extensions", "list"], {
@@ -128,7 +119,7 @@ async function handleList() {
}
async function handleInstall(name?: string, source?: string) {
- const authError = requireAuthTokenForCli();
+ const authError = requireAuthToken();
if (authError) return authError;
const hasName = !!name;
@@ -198,7 +189,7 @@ async function handleInstall(name?: string, source?: string) {
}
async function handleUninstall(name?: string) {
- const authError = requireAuthTokenForCli();
+ const authError = requireAuthToken();
if (authError) return authError;
if (!name) {
diff --git a/src/tools/localstack-management.ts b/src/tools/localstack-management.ts
index f65ed6e..a682b3b 100644
--- a/src/tools/localstack-management.ts
+++ b/src/tools/localstack-management.ts
@@ -1,15 +1,25 @@
import { z } from "zod";
import { type ToolMetadata, type InferSchema } from "xmcp";
-import { spawn } from "child_process";
-import { getLocalStackStatus } from "../lib/localstack/localstack.utils";
+import {
+ getLocalStackStatus,
+ getSnowflakeEmulatorStatus,
+ startRuntime,
+} from "../lib/localstack/localstack.utils";
import { runCommand } from "../core/command-runner";
-import { runPreflights, requireLocalStackCli } from "../core/preflight";
+import { runPreflights, requireLocalStackCli, requireProFeature, requireAuthToken } from "../core/preflight";
import { ResponseBuilder } from "../core/response-builder";
+import { ProFeature } from "../lib/localstack/license-checker";
export const schema = {
action: z
.enum(["start", "stop", "restart", "status"])
.describe("The LocalStack management action to perform"),
+ service: z
+ .enum(["aws", "snowflake"])
+ .default("aws")
+ .describe(
+ "The LocalStack stack/service to manage. Use 'aws' for the default AWS emulator, or 'snowflake' for the Snowflake emulator."
+ ),
envVars: z
.record(z.string())
.optional()
@@ -29,20 +39,31 @@ export const metadata: ToolMetadata = {
export default async function localstackManagement({
action,
+ service,
envVars,
}: InferSchema) {
- const preflightError = await runPreflights([requireLocalStackCli()]);
+ const checks = [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));
+ }
+
+ const preflightError = await runPreflights(checks);
if (preflightError) return preflightError;
switch (action) {
case "start":
- return await handleStart({ envVars });
+ return await handleStart({ envVars, service });
case "stop":
return await handleStop();
case "restart":
return await handleRestart();
case "status":
- return await handleStatus();
+ return await handleStatus({ service });
default:
return ResponseBuilder.error(
"Unknown action",
@@ -52,73 +73,44 @@ export default async function localstackManagement({
}
// Handle start action
-async function handleStart({ envVars }: { envVars?: Record }) {
- const statusCheck = await getLocalStackStatus();
- if (statusCheck.isRunning) {
- return ResponseBuilder.markdown(
- "⚠️ LocalStack is already running. Use 'restart' if you want to apply new configuration."
- );
+async function handleStart({
+ envVars,
+ service,
+}: {
+ envVars?: Record;
+ service: "aws" | "snowflake";
+}) {
+ if (service === "snowflake") {
+ return await handleSnowflakeStart({ envVars });
}
- const environment = { ...process.env, ...(envVars || {}) } as Record;
- if (process.env.LOCALSTACK_AUTH_TOKEN) {
- environment.LOCALSTACK_AUTH_TOKEN = process.env.LOCALSTACK_AUTH_TOKEN;
- }
+ return await startRuntime({
+ startArgs: ["start"],
+ getStatus: getLocalStackStatus,
+ processLabel: "LocalStack",
+ alreadyRunningMessage:
+ "⚠️ LocalStack is already running. Use 'restart' if you want to apply new configuration.",
+ successTitle: "🚀 LocalStack started successfully!",
+ statusHeading: "Status",
+ timeoutMessage:
+ "❌ LocalStack start timed out after 120 seconds. It may still be starting in the background.",
+ envVars,
+ });
+}
- return new Promise((resolve) => {
- const child = spawn("localstack", ["start"], {
- env: environment,
- stdio: ["ignore", "ignore", "pipe"],
- });
-
- let stderr = "";
- child.stderr.on("data", (data) => {
- stderr += data.toString();
- });
-
- let earlyExit = false;
- let poll: NodeJS.Timeout;
- child.on("error", (err) => {
- earlyExit = true;
- if (poll) clearInterval(poll);
- resolve(ResponseBuilder.markdown(`❌ Failed to start LocalStack process: ${err.message}`));
- });
-
- child.on("close", (code) => {
- if (earlyExit) return;
- if (poll) clearInterval(poll);
- if (code !== 0) {
- resolve(
- ResponseBuilder.markdown(
- `❌ LocalStack process exited unexpectedly with code ${code}.\n\nStderr:\n${stderr}`
- )
- );
- }
- });
-
- const pollInterval = 5000;
- const maxWaitTime = 120000;
- let timeWaited = 0;
-
- poll = setInterval(async () => {
- timeWaited += pollInterval;
- const status = await getLocalStackStatus();
- if (status.isReady || status.isRunning) {
- clearInterval(poll);
- let resultMessage = "🚀 LocalStack started successfully!\n\n";
- if (envVars)
- resultMessage += `✅ Custom environment variables applied: ${Object.keys(envVars).join(", ")}\n`;
- resultMessage += `\n**Status:**\n${status.statusOutput}`;
- resolve(ResponseBuilder.markdown(resultMessage));
- } else if (timeWaited >= maxWaitTime) {
- clearInterval(poll);
- resolve(
- ResponseBuilder.markdown(
- `❌ LocalStack start timed out after ${maxWaitTime / 1000} seconds. It may still be starting in the background.`
- )
- );
- }
- }, pollInterval);
+async function handleSnowflakeStart({ envVars }: { envVars?: Record }) {
+ return await startRuntime({
+ startArgs: ["start", "--stack", "snowflake"],
+ getStatus: getSnowflakeEmulatorStatus,
+ processLabel: "Snowflake emulator",
+ alreadyRunningMessage:
+ "⚠️ Snowflake emulator is already running. Use 'restart' if you want to apply new configuration.",
+ successTitle: "🚀 Snowflake emulator started successfully!",
+ statusHeading: "Health check",
+ timeoutMessage:
+ '❌ Snowflake emulator start timed out after 120 seconds. Health check endpoint did not return {"success": true}.',
+ envVars,
+ onReady: async () => await requireProFeature(ProFeature.SNOWFLAKE),
});
}
@@ -130,16 +122,16 @@ async function handleStop() {
if (cmd.stderr.trim()) result += `\nMessages:\n${cmd.stderr}`;
const statusResult = await getLocalStackStatus();
- if (!statusResult.isRunning) {
+
+ if (!statusResult.isRunning || statusResult.errorMessage) {
result += "\n\n✅ LocalStack has been stopped successfully.";
- } else if (statusResult.errorMessage) {
- result += "\n\n✅ LocalStack appears to be stopped.";
} else {
result += "\n\n⚠️ LocalStack may still be running. Check the status manually if needed.";
}
if (cmd.error) {
- result = `❌ Failed to stop LocalStack: ${cmd.error.message}\n\nThis could happen if:\n- LocalStack is not currently running\n- There was an error executing the stop command\n- Permission issues\n\nYou can try checking the LocalStack status first to see if it's running.`;
+ result =
+ `❌ Failed to stop LocalStack: ${cmd.error.message}\n\nThis could happen if:\n- LocalStack is not currently running\n- There was an error executing the stop command\n- Permission issues\n\nYou can try checking the LocalStack status first to see if it's running.`;
}
return ResponseBuilder.markdown(result);
@@ -157,8 +149,7 @@ async function handleRestart() {
if (statusResult.statusOutput) {
result += `\nStatus after restart:\n${statusResult.statusOutput}`;
if (statusResult.isRunning) {
- result +=
- "\n\n✅ LocalStack has been restarted successfully and is now running with a fresh state.";
+ result += "\n\n✅ LocalStack has been restarted successfully and is now running with a fresh state.";
} else {
result +=
"\n\n⚠️ LocalStack restart completed but may still be starting up. Check status again in a few moments.";
@@ -176,20 +167,36 @@ async function handleRestart() {
}
// Handle status action
-async function handleStatus() {
+async function handleStatus({ service }: { service: "aws" | "snowflake" }) {
const statusResult = await getLocalStackStatus();
if (statusResult.statusOutput) {
let result = "📊 LocalStack Status:\n\n";
result += statusResult.statusOutput;
- // Add helpful information based on the status
- if (statusResult.isRunning) {
- result += "\n\n✅ LocalStack is currently running and ready to accept requests.";
- } else {
+ if (!statusResult.isRunning) {
result += "\n\n⚠️ LocalStack is not currently running. Use the start action to start it.";
+ return ResponseBuilder.markdown(result);
+ }
+
+ if (service === "snowflake") {
+ const snowflakeStatus = await getSnowflakeEmulatorStatus();
+
+ if (snowflakeStatus.isReady || snowflakeStatus.isRunning) {
+ result += "\n\n✅ LocalStack is running and Snowflake emulator health check passed.";
+ } else {
+ const diagnostics = [snowflakeStatus.statusOutput, snowflakeStatus.errorMessage]
+ .filter(Boolean)
+ .join(" | ");
+ result +=
+ "\n\n⚠️ LocalStack is running, but Snowflake emulator health check did not pass." +
+ (diagnostics ? ` (${diagnostics})` : "");
+ }
+ return ResponseBuilder.markdown(result);
}
+ // Default aws service status check
+ result += "\n\n✅ LocalStack is currently running and ready to accept requests.";
return ResponseBuilder.markdown(result);
} else {
const result = `❌ ${statusResult.errorMessage}