diff --git a/README.md b/README.md
index 75a3d04..10a64c5 100644
--- a/README.md
+++ b/README.md
@@ -10,13 +10,14 @@ This server eliminates custom scripts and manual LocalStack management with dire
- Start, stop, restart, and monitor LocalStack for AWS container status with built-in auth.
- Deploy CDK, Terraform, and SAM projects with automatic configuration detection.
- Search LocalStack documentation for guides, API references, and configuration details.
-- Parse logs, catch errors, and auto-generate IAM policies from violations. (requires active license)
-- Inject chaos faults and network effects into LocalStack to test system resilience. (requires active license)
-- Manage LocalStack state snapshots via [Cloud Pods](https://docs.localstack.cloud/aws/capabilities/state-management/cloud-pods/) for development workflows. (requires active license)
-- Install, remove, list, and discover [LocalStack Extensions](https://docs.localstack.cloud/aws/capabilities/extensions/) from the marketplace. (requires active license)
+- Parse logs, catch errors, and auto-generate IAM policies from violations.
+- Inject chaos faults and network effects into LocalStack to test system resilience.
+- Manage LocalStack state snapshots via [Cloud Pods](https://docs.localstack.cloud/aws/capabilities/state-management/cloud-pods/) for development workflows.
+- Export, import, inspect, and reset LocalStack state locally with [Export & Import State](https://docs.localstack.cloud/aws/capabilities/state-management/export-import-state/) file-based workflows.
+- Install, remove, list, and discover [LocalStack Extensions](https://docs.localstack.cloud/aws/capabilities/extensions/) from the marketplace.
- Launch and manage [Ephemeral Instances](https://docs.localstack.cloud/aws/capabilities/cloud-sandbox/ephemeral-instances/) for remote LocalStack testing workflows.
- Replicate external AWS resources into LocalStack with [AWS Replicator](https://docs.localstack.cloud/aws/tooling/aws-replicator/) so IaC stacks can resolve shared dependencies locally.
-- Inspect LocalStack application flows with [App Inspector](https://docs.localstack.cloud/aws/capabilities/web-app/app-inspector/) traces, spans, events, payload metadata, and IAM policy evaluations. (requires active license)
+- Inspect LocalStack application flows with [App Inspector](https://docs.localstack.cloud/aws/capabilities/web-app/app-inspector/) traces, spans, events, payload metadata, and IAM policy evaluations.
- Connect AI assistants and dev tools for automated cloud testing workflows.
## Tools Reference
@@ -26,20 +27,21 @@ This server provides your AI with dedicated tools for managing your LocalStack e
> [!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 |
-| [`localstack-deployer`](./src/tools/localstack-deployer.ts) | Handles infrastructure deployment to LocalStack for AWS environments | - Automatically run CDK, Terraform, and SAM 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), [`tflocal`](https://github.com/localstack/terraform-local), or [`samlocal`](https://github.com/localstack/aws-sam-cli-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 |
-| [`localstack-chaos-injector`](./src/tools/localstack-chaos-injector.ts) | Injects and manages chaos experiment faults for system resilience testing | - Inject, add, remove, and clear service fault rules
- Configure network latency effects
- Comprehensive fault targeting by service, region, and operation
- Built-in workflow guidance for chaos experiments
- Requires a valid LocalStack Auth Token |
-| [`localstack-cloud-pods`](./src/tools/localstack-cloud-pods.ts) | Manages LocalStack state snapshots for development workflows | - Save current state as Cloud Pods
- Load previously saved Cloud Pods instantly
- Delete Cloud Pods or reset to a clean state
- Requires a valid LocalStack Auth Token |
-| [`localstack-extensions`](./src/tools/localstack-extensions.ts) | Installs, uninstalls, lists, and discovers LocalStack Extensions | - Manage installed extensions via CLI actions (`list`, `install`, `uninstall`)
- Browse the LocalStack Extensions marketplace (`available`)
- Requires a valid LocalStack Auth Token support |
-| [`localstack-ephemeral-instances`](./src/tools/localstack-ephemeral-instances.ts) | Manages cloud-hosted LocalStack Ephemeral Instances | - Create temporary cloud-hosted LocalStack instances and get an endpoint URL
- List available ephemeral instances, fetch logs, and delete instances
- Supports lifetime, extension preload, Cloud Pod preload, and custom env vars on create
- Requires a valid LocalStack Auth Token and LocalStack CLI |
-| [`localstack-aws-client`](./src/tools/localstack-aws-client.ts) | Runs AWS CLI commands inside the LocalStack for AWS container | - Executes commands via `awslocal` inside the running container
- Sanitizes commands to block shell chaining
- Auto-detects LocalStack coverage errors and links to docs |
-| [`localstack-aws-replicator`](./src/tools/localstack-aws-replicator.ts) | Replicates external AWS resources into a running LocalStack instance | - Start single-resource replication jobs with a resource type and identifier or ARN
- Start batch replication jobs, such as SSM parameters under a path prefix
- Poll job status by job ID and list existing jobs
- List resource types supported by the running Replicator extension
- Reads source AWS credentials from the MCP server environment and supports optional target account or region overrides |
-| [`localstack-app-inspector`](./src/tools/localstack-app-inspector.ts) | Inspects LocalStack application traces, spans, events, and IAM evaluations | - Enable or disable App Inspector for the running LocalStack instance
- List and inspect traces to understand AWS service-to-service flows
- Drill into spans, events, payload metadata, and IAM policy evaluation events
- Filter by service, region, operation, resource, ARN, status, and time range
- Requires a valid LocalStack Auth Token and the App Inspector feature in the connected LocalStack license |
-| [`localstack-docs`](./src/tools/localstack-docs.ts) | Searches LocalStack documentation through CrawlChat | - Queries LocalStack docs through a public CrawlChat collection
- Returns focused snippets with source links only
- Helps answer coverage, configuration, and setup questions without requiring LocalStack runtime |
+| 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 |
+| [`localstack-deployer`](./src/tools/localstack-deployer.ts) | Handles infrastructure deployment to LocalStack for AWS environments | - Automatically run CDK, Terraform, and SAM 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), [`tflocal`](https://github.com/localstack/terraform-local), or [`samlocal`](https://github.com/localstack/aws-sam-cli-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 |
+| [`localstack-chaos-injector`](./src/tools/localstack-chaos-injector.ts) | Injects and manages chaos experiment faults for system resilience testing | - Inject, add, remove, and clear service fault rules
- Configure network latency effects
- Comprehensive fault targeting by service, region, and operation
- Built-in workflow guidance for chaos experiments
- Requires a valid LocalStack Auth Token |
+| [`localstack-cloud-pods`](./src/tools/localstack-cloud-pods.ts) | Manages remote LocalStack Cloud Pods for development workflows | - Save current state as a Cloud Pod
- Load previously saved Cloud Pods instantly
- Delete Cloud Pods from remote cloud-backed storage
- Use this for managed remote state snapshots, not local export/import files
- Requires a valid LocalStack Auth Token |
+| [`localstack-state-management`](./src/tools/localstack-state-management.ts) | Manages local file-based LocalStack state export/import workflows | - Export LocalStack state to a local file on disk through the LocalStack State REST API
- Import LocalStack state from a local file
- Inspect current LocalStack state as JSON metamodel data
- Reset all state or only selected services
- Supports service-level granularity for export, reset, and inspect
- Use this for local disk workflows; use Cloud Pods for remote cloud-backed snapshots
- Requires a valid LocalStack Auth Token |
+| [`localstack-extensions`](./src/tools/localstack-extensions.ts) | Installs, uninstalls, lists, and discovers LocalStack Extensions | - Manage installed extensions via CLI actions (`list`, `install`, `uninstall`)
- Browse the LocalStack Extensions marketplace (`available`)
- Requires a valid LocalStack Auth Token support |
+| [`localstack-ephemeral-instances`](./src/tools/localstack-ephemeral-instances.ts) | Manages cloud-hosted LocalStack Ephemeral Instances | - Create temporary cloud-hosted LocalStack instances and get an endpoint URL
- List available ephemeral instances, fetch logs, and delete instances
- Supports lifetime, extension preload, Cloud Pod preload, and custom env vars on create
- Requires a valid LocalStack Auth Token and LocalStack CLI |
+| [`localstack-aws-client`](./src/tools/localstack-aws-client.ts) | Runs AWS CLI commands inside the LocalStack for AWS container | - Executes commands via `awslocal` inside the running container
- Sanitizes commands to block shell chaining
- Auto-detects LocalStack coverage errors and links to docs |
+| [`localstack-aws-replicator`](./src/tools/localstack-aws-replicator.ts) | Replicates external AWS resources into a running LocalStack instance | - Start single-resource replication jobs with a resource type and identifier or ARN
- Start batch replication jobs, such as SSM parameters under a path prefix
- Poll job status by job ID and list existing jobs
- List resource types supported by the running Replicator extension
- Reads source AWS credentials from the MCP server environment and supports optional target account or region overrides |
+| [`localstack-app-inspector`](./src/tools/localstack-app-inspector.ts) | Inspects LocalStack application traces, spans, events, and IAM evaluations | - Enable or disable App Inspector for the running LocalStack instance
- List and inspect traces to understand AWS service-to-service flows
- Drill into spans, events, payload metadata, and IAM policy evaluation events
- Filter by service, region, operation, resource, ARN, status, and time range
- Requires a valid LocalStack Auth Token and the App Inspector feature in the connected LocalStack license |
+| [`localstack-docs`](./src/tools/localstack-docs.ts) | Searches LocalStack documentation through CrawlChat | - Queries LocalStack docs through a public CrawlChat collection
- Returns focused snippets with source links only
- Helps answer coverage, configuration, and setup questions without requiring LocalStack runtime |
## Installation
diff --git a/manifest.json b/manifest.json
index 192d2b5..948871b 100644
--- a/manifest.json
+++ b/manifest.json
@@ -45,7 +45,11 @@
},
{
"name": "localstack-cloud-pods",
- "description": "Manages LocalStack state snapshots (Cloud Pods) for saving, loading, deleting, and resetting"
+ "description": "Manages remote LocalStack Cloud Pods for saving, loading, and deleting cloud-backed state snapshots"
+ },
+ {
+ "name": "localstack-state-management",
+ "description": "Exports, imports, resets, and inspects LocalStack state with local file-based workflows on disk"
},
{
"name": "localstack-extensions",
@@ -176,9 +180,33 @@
},
{
"name": "cloud-pods",
- "description": "Manage LocalStack state snapshots (Cloud Pods) for saving, loading, deleting, and resetting",
+ "description": "Manage remote LocalStack Cloud Pods for saving, loading, and deleting cloud-backed snapshots",
"arguments": ["action", "pod_name"],
- "text": "Please ${arguments.action} Cloud Pod in the LocalStack container with the pod name ${arguments.pod_name}."
+ "text": "Please ${arguments.action} Cloud Pod in the LocalStack container with the pod name ${arguments.pod_name}. Use Cloud Pods for remote cloud-backed snapshots; use LocalStack state management for local export/import files on disk."
+ },
+ {
+ "name": "localstack-state-export",
+ "description": "Export LocalStack state to a local file on disk",
+ "arguments": ["file_path", "services"],
+ "text": "Export my running LocalStack state to the local file ${arguments.file_path}. If services are provided (${arguments.services}), export only those services. Use this local file workflow instead of Cloud Pods because I want a disk file."
+ },
+ {
+ "name": "localstack-state-import",
+ "description": "Import LocalStack state from a local file on disk",
+ "arguments": ["file_path"],
+ "text": "Import LocalStack state from the local file ${arguments.file_path}. This is for local file-based restore; use Cloud Pods instead when I want remote cloud-backed snapshots."
+ },
+ {
+ "name": "localstack-state-inspect",
+ "description": "Inspect current LocalStack state locally",
+ "arguments": ["services"],
+ "text": "Inspect the current LocalStack state in JSON format. If services are provided (${arguments.services}), show only those services. Explain that this is local runtime state inspection and not a Cloud Pods remote snapshot operation."
+ },
+ {
+ "name": "localstack-state-reset",
+ "description": "Reset LocalStack state locally",
+ "arguments": ["services"],
+ "text": "Reset LocalStack state. If services are provided (${arguments.services}), reset only those services; otherwise reset all service state. Warn me that this is destructive and separate from deleting Cloud Pods."
},
{
"name": "extensions-list",
diff --git a/src/core/analytics.ts b/src/core/analytics.ts
index 72698e4..f8cade4 100644
--- a/src/core/analytics.ts
+++ b/src/core/analytics.ts
@@ -40,6 +40,7 @@ export const TOOL_ARG_ALLOWLIST: Record = {
],
"localstack-chaos-injector": ["action", "rules_count", "latency_ms"],
"localstack-cloud-pods": ["action", "pod_name"],
+ "localstack-state-management": ["action", "has_file_path", "services_count"],
"localstack-deployer": [
"action",
"projectType",
diff --git a/src/lib/localstack/license-checker.ts b/src/lib/localstack/license-checker.ts
index c732b84..33b34ae 100644
--- a/src/lib/localstack/license-checker.ts
+++ b/src/lib/localstack/license-checker.ts
@@ -11,6 +11,7 @@ export enum ProFeature {
EXTENSIONS = "localstack.platform.plugin/extensions",
REPLICATOR = "localstack.platform.plugin/replicator",
SNOWFLAKE = "localstack.aws.provider/snowflake:pro",
+ STATE_MANAGEMENT = "localstack.platform.plugin/snapshot",
}
export interface LicenseCheckResult {
diff --git a/src/lib/localstack/localstack.client.ts b/src/lib/localstack/localstack.client.ts
index 830d5ae..eb9e91a 100644
--- a/src/lib/localstack/localstack.client.ts
+++ b/src/lib/localstack/localstack.client.ts
@@ -1,3 +1,4 @@
+import { LOCALSTACK_BASE_URL } from "../../core/config";
import { httpClient, HttpError } from "../../core/http-client";
export type ApiResult =
@@ -58,6 +59,13 @@ export interface AppInspectorSetStatusResponse {
export type AppInspectorStatus = "enabled" | "disabled";
export type AppInspectorQuery = Record;
+export interface StateExportResult {
+ content: Buffer;
+ services: string[];
+ size: number;
+ contentLength?: number;
+}
+
// Chaos API Client
export class ChaosApiClient {
private async makeRequest(
@@ -184,8 +192,116 @@ export class CloudPodsApiClient {
deletePod(podName: string) {
return this.makeRequest(`/_localstack/pods/${encodeURIComponent(podName)}`, "DELETE", true, {});
}
- resetState() {
- return this.makeRequest("/_localstack/state/reset", "POST", false, {});
+}
+
+// Local file-based State Management API Client
+export class StateManagementApiClient {
+ private async requestResponse(
+ endpoint: string,
+ options: RequestInit = {}
+ ): Promise> {
+ try {
+ const response = await fetch(`${LOCALSTACK_BASE_URL}${endpoint}`, options);
+ if (!response.ok) {
+ return {
+ success: false,
+ message: await response.text(),
+ statusCode: response.status,
+ };
+ }
+ return { success: true, data: response };
+ } catch (error) {
+ return {
+ success: false,
+ message: `Failed to communicate with LocalStack State Management API: ${error instanceof Error ? error.message : "Unknown error"}`,
+ };
+ }
+ }
+
+ private serviceQuery(services?: string[]) {
+ if (!services || services.length === 0) return "";
+ return `?services=${encodeURIComponent(services.join(","))}`;
+ }
+
+ async exportState(services?: string[]): Promise> {
+ const result = await this.requestResponse(
+ `/_localstack/pods/state${this.serviceQuery(services)}`,
+ {
+ method: "GET",
+ }
+ );
+ if (!result.success) return result;
+
+ const response = result.data;
+ const content = Buffer.from(await response.arrayBuffer());
+ const exportedServices = (response.headers.get("x-localstack-pod-services") ?? "")
+ .split(",")
+ .map((service) => service.trim())
+ .filter(Boolean);
+ const size = Number(response.headers.get("x-localstack-pod-size") ?? content.length);
+ const contentLength = Number(response.headers.get("content-length") ?? content.length);
+
+ return {
+ success: true,
+ data: {
+ content,
+ services: exportedServices,
+ size: Number.isNaN(size) ? content.length : size,
+ contentLength: Number.isNaN(contentLength) ? undefined : contentLength,
+ },
+ };
+ }
+
+ async importState(content: Buffer): Promise> {
+ const body = content.buffer.slice(
+ content.byteOffset,
+ content.byteOffset + content.byteLength
+ ) as ArrayBuffer;
+ const result = await this.requestResponse("/_localstack/pods", {
+ method: "POST",
+ headers: { "Content-Type": "application/octet-stream" },
+ body,
+ });
+ if (!result.success) return result;
+ return { success: true, data: await result.data.text() };
+ }
+
+ async resetState(services?: string[]): Promise> {
+ if (!services || services.length === 0) {
+ const result = await this.requestResponse("/_localstack/state/reset", { method: "POST" });
+ return result.success ? { success: true, data: undefined } : result;
+ }
+
+ for (const service of services) {
+ const result = await this.requestResponse(
+ `/_localstack/state/${encodeURIComponent(service)}/reset`,
+ { method: "POST" }
+ );
+ if (!result.success) return result;
+ }
+
+ return { success: true, data: undefined };
+ }
+
+ async inspectState(): Promise> {
+ try {
+ const data = await httpClient.request("/_localstack/pods/state/metamodel", {
+ method: "GET",
+ });
+ return { success: true, data };
+ } catch (error) {
+ if (error instanceof HttpError) {
+ return {
+ success: false,
+ message: error.body || error.message,
+ statusCode: error.status,
+ };
+ }
+ return {
+ success: false,
+ message: `Failed to communicate with LocalStack State Management API: ${error instanceof Error ? error.message : "Unknown error"}`,
+ };
+ }
}
}
diff --git a/src/lib/localstack/state-management.logic.test.ts b/src/lib/localstack/state-management.logic.test.ts
new file mode 100644
index 0000000..21aebe4
--- /dev/null
+++ b/src/lib/localstack/state-management.logic.test.ts
@@ -0,0 +1,128 @@
+import fs from "fs";
+import os from "os";
+import path from "path";
+import {
+ buildStateAnalyticsArgs,
+ filterInspectServices,
+ formatInspectResult,
+ normalizeServices,
+ validateStateManagementArgs,
+} from "../../tools/localstack-state-management";
+
+describe("localstack-state-management", () => {
+ describe("normalizeServices", () => {
+ it("accepts comma-delimited and array service inputs", () => {
+ expect(normalizeServices("s3, lambda, s3")).toEqual(["s3", "lambda"]);
+ expect(normalizeServices(["sqs", "sns", "sqs"])).toEqual(["sqs", "sns"]);
+ });
+ });
+
+ describe("validateStateManagementArgs", () => {
+ it("validates export with file path and services", () => {
+ const filePath = path.join(os.tmpdir(), "ls-state-export-test.zip");
+ const result = validateStateManagementArgs({
+ action: "export",
+ file_path: filePath,
+ services: ["s3", "lambda"],
+ } as any);
+
+ expect(result.error).toBeUndefined();
+ expect(result.outputPath).toBe(filePath);
+ expect(result.serviceList).toEqual(["s3", "lambda"]);
+ });
+
+ it("requires an existing file for import and rejects service filters", () => {
+ const filePath = path.join(os.tmpdir(), "ls-state-import-test.zip");
+ fs.writeFileSync(filePath, "state");
+
+ try {
+ const result = validateStateManagementArgs({
+ action: "import",
+ file_path: filePath,
+ services: "s3",
+ } as any);
+
+ expect(result.error?.content[0].text).toContain("Unsupported Service Filter");
+ } finally {
+ fs.unlinkSync(filePath);
+ }
+ });
+
+ it("validates service-level reset", () => {
+ const result = validateStateManagementArgs({
+ action: "reset",
+ services: ["s3", "sqs"],
+ } as any);
+
+ expect(result.error).toBeUndefined();
+ expect(result.serviceList).toEqual(["s3", "sqs"]);
+ });
+
+ it("validates inspect without requiring a file path", () => {
+ const result = validateStateManagementArgs({
+ action: "inspect",
+ } as any);
+
+ expect(result.error).toBeUndefined();
+ expect(result.serviceList).toEqual([]);
+ });
+ });
+
+ describe("buildStateAnalyticsArgs", () => {
+ it("does not include raw file paths or service names", () => {
+ const analyticsArgs = buildStateAnalyticsArgs({
+ action: "export",
+ file_path: "/tmp/customer-state.zip",
+ services: ["s3", "lambda"],
+ } as any);
+
+ expect(analyticsArgs).toEqual({
+ action: "export",
+ has_file_path: true,
+ services_count: 2,
+ });
+ expect(JSON.stringify(analyticsArgs)).not.toContain("/tmp/customer-state.zip");
+ expect(JSON.stringify(analyticsArgs)).not.toContain("lambda");
+ });
+ });
+
+ describe("filterInspectServices", () => {
+ it("filters account-scoped inspect data to selected services", () => {
+ const filtered = filterInspectServices(
+ {
+ "000000000000": {
+ s3: { buckets: ["test"] },
+ lambda: { functions: ["fn"] },
+ sqs: { queues: ["q"] },
+ },
+ },
+ ["s3", "sqs"]
+ );
+
+ expect(filtered).toEqual({
+ "000000000000": {
+ s3: { buckets: ["test"] },
+ sqs: { queues: ["q"] },
+ },
+ });
+ });
+ });
+
+ describe("formatInspectResult", () => {
+ it("returns filtered JSON markdown for selected services", () => {
+ const result = formatInspectResult(
+ {
+ "000000000000": {
+ s3: { buckets: ["test"] },
+ lambda: { functions: ["fn"] },
+ },
+ },
+ ["s3"]
+ );
+
+ expect(result.content[0].text).toContain("LocalStack State Inspect");
+ expect(result.content[0].text).toContain('"s3"');
+ expect(result.content[0].text).not.toContain('"lambda"');
+ });
+ });
+});
diff --git a/src/tools/localstack-cloud-pods.ts b/src/tools/localstack-cloud-pods.ts
index 12e0be4..4206173 100644
--- a/src/tools/localstack-cloud-pods.ts
+++ b/src/tools/localstack-cloud-pods.ts
@@ -14,7 +14,11 @@ import { withToolAnalytics } from "../core/analytics";
// Define the schema for tool parameters
export const schema = {
- action: z.enum(["save", "load", "delete", "reset"]).describe("The Cloud Pods action to perform."),
+ action: z
+ .enum(["save", "load", "delete"])
+ .describe(
+ "The Cloud Pods action to perform."
+ ),
pod_name: z
.string()
@@ -34,7 +38,7 @@ export const schema = {
// Define tool metadata
export const metadata: ToolMetadata = {
name: "localstack-cloud-pods",
- description: "Manages LocalStack Cloud Pods with following actions: save, load, delete, reset",
+ description: "Manages remote LocalStack Cloud Pods with following actions: save, load, delete",
annotations: {
title: "LocalStack Cloud Pods",
readOnlyHint: false,
@@ -127,17 +131,6 @@ export default async function localstackCloudPods({
return ResponseBuilder.success(`Cloud Pod '**${pod_name}**' has been permanently deleted.`);
}
- case "reset": {
- const result = await client.resetState();
- if (!result.success) {
- return ResponseBuilder.error("Cloud Pods Error", result.message);
- }
-
- return ResponseBuilder.markdown(
- "⚠️ LocalStack state has been reset successfully. **All unsaved state has been permanently lost.**"
- );
- }
-
default:
return ResponseBuilder.error("Unknown action", `Unsupported action: ${action}`);
}
diff --git a/src/tools/localstack-state-management.ts b/src/tools/localstack-state-management.ts
new file mode 100644
index 0000000..24554fd
--- /dev/null
+++ b/src/tools/localstack-state-management.ts
@@ -0,0 +1,284 @@
+import fs from "fs";
+import path from "path";
+import { z } from "zod";
+import { type ToolMetadata, type InferSchema } from "xmcp";
+import { ResponseBuilder } from "../core/response-builder";
+import {
+ runPreflights,
+ requireAuthToken,
+ requireLocalStackRunning,
+ requireProFeature,
+} from "../core/preflight";
+import { withToolAnalytics } from "../core/analytics";
+import { ProFeature } from "../lib/localstack/license-checker";
+import {
+ StateManagementApiClient,
+ type ApiResult,
+ type StateExportResult,
+} from "../lib/localstack/localstack.client";
+
+const DEFAULT_EXPORT_PATH = "ls-state-export";
+
+export const schema = {
+ action: z
+ .enum(["export", "import", "reset", "inspect"])
+ .describe(
+ "The local LocalStack state action to perform through the LocalStack State REST API. Use this tool for file-based state export/import workflows on disk. Use Cloud Pods instead when the user wants remote cloud-backed state snapshots."
+ ),
+ file_path: z
+ .string()
+ .trim()
+ .optional()
+ .describe(
+ "Local file path for state export or import. Required for import. For export, defaults to ls-state-export in the MCP server working directory if omitted."
+ ),
+ services: z
+ .union([z.array(z.string().trim().min(1)), z.string().trim().min(1)])
+ .optional()
+ .describe(
+ "Optional AWS service names for service-level granularity, such as ['s3', 'lambda'] or 's3,lambda'. Supported for export, reset, and inspect. Import restores the services contained in the state file."
+ ),
+};
+
+export const metadata: ToolMetadata = {
+ name: "localstack-state-management",
+ description:
+ "Export, import, reset, and inspect LocalStack state using local file-based workflows on disk.",
+ annotations: {
+ title: "LocalStack State Management",
+ readOnlyHint: false,
+ destructiveHint: true,
+ idempotentHint: false,
+ },
+};
+
+export type StateManagementArgs = InferSchema;
+
+interface ValidationResult {
+ error?: ReturnType;
+ serviceList?: string[];
+ outputPath?: string;
+}
+
+export default async function localstackStateManagement(args: StateManagementArgs) {
+ return withToolAnalytics(
+ "localstack-state-management",
+ buildStateAnalyticsArgs(args),
+ async () => {
+ const preflightError = await runPreflights([
+ requireAuthToken(),
+ requireLocalStackRunning(),
+ requireProFeature(ProFeature.STATE_MANAGEMENT),
+ ]);
+ if (preflightError) return preflightError;
+
+ const validation = validateStateManagementArgs(args);
+ if (validation.error) return validation.error;
+
+ const client = new StateManagementApiClient();
+
+ switch (args.action) {
+ case "export":
+ return await handleExport(client, validation);
+ case "import":
+ return await handleImport(client, validation);
+ case "reset":
+ return await handleReset(client, validation);
+ case "inspect":
+ return await handleInspect(client, validation);
+ default:
+ return ResponseBuilder.error("Unknown Action", `Unsupported action: ${args.action}`);
+ }
+ }
+ );
+}
+
+export function buildStateAnalyticsArgs(args: StateManagementArgs) {
+ const services = normalizeServices(args.services);
+ return {
+ action: args.action,
+ has_file_path: Boolean(args.file_path),
+ services_count: services.length || undefined,
+ };
+}
+
+export function normalizeServices(services: StateManagementArgs["services"]): string[] {
+ const raw = Array.isArray(services) ? services : services?.split(",");
+ return Array.from(new Set((raw ?? []).map((service) => service.trim()).filter(Boolean)));
+}
+
+export function validateStateManagementArgs(args: StateManagementArgs): ValidationResult {
+ const services = normalizeServices(args.services);
+ const filePath = args.file_path?.trim();
+
+ switch (args.action) {
+ case "export": {
+ const destination = filePath || DEFAULT_EXPORT_PATH;
+ const parent = path.dirname(path.resolve(destination));
+ if (!fs.existsSync(parent)) {
+ return {
+ error: ResponseBuilder.error(
+ "Export Path Not Found",
+ `The parent directory for \`${destination}\` does not exist: \`${parent}\`.`
+ ),
+ };
+ }
+
+ return { serviceList: services, outputPath: destination };
+ }
+
+ case "import": {
+ if (!filePath) {
+ return {
+ error: ResponseBuilder.error(
+ "Missing File Path",
+ "The `import` action requires `file_path` pointing to a file previously created by `localstack state export`."
+ ),
+ };
+ }
+ if (!fs.existsSync(filePath)) {
+ return {
+ error: ResponseBuilder.error(
+ "Import File Not Found",
+ `The state file \`${filePath}\` does not exist.`
+ ),
+ };
+ }
+ if (services.length > 0) {
+ return {
+ error: ResponseBuilder.error(
+ "Unsupported Service Filter",
+ "`localstack state import` restores the services contained in the exported state file. Service-level filtering is supported for export, reset, and inspect."
+ ),
+ };
+ }
+ return { outputPath: filePath };
+ }
+
+ case "reset": {
+ if (filePath) {
+ return {
+ error: ResponseBuilder.error(
+ "Unsupported File Path",
+ "The `reset` action does not use `file_path`."
+ ),
+ };
+ }
+ return { serviceList: services };
+ }
+
+ case "inspect": {
+ if (filePath) {
+ return {
+ error: ResponseBuilder.error(
+ "Unsupported File Path",
+ "The `inspect` action reads state from the running LocalStack instance and does not use `file_path`."
+ ),
+ };
+ }
+ return { serviceList: services };
+ }
+
+ default:
+ return {
+ error: ResponseBuilder.error("Unknown Action", `Unsupported action: ${args.action}`),
+ };
+ }
+}
+
+async function handleExport(client: StateManagementApiClient, validation: ValidationResult) {
+ const result = await client.exportState(validation.serviceList);
+ if (!result.success) return formatStateApiError("export", result);
+
+ const outputPath = validation.outputPath ?? DEFAULT_EXPORT_PATH;
+ fs.writeFileSync(outputPath, result.data.content);
+ return formatExportSuccess(outputPath, result.data, validation.serviceList ?? []);
+}
+
+async function handleImport(client: StateManagementApiClient, validation: ValidationResult) {
+ const outputPath = validation.outputPath;
+ if (!outputPath) {
+ return ResponseBuilder.error("Missing File Path", "The `import` action requires `file_path`.");
+ }
+
+ const content = fs.readFileSync(outputPath);
+ const result = await client.importState(content);
+ if (!result.success) return formatStateApiError("import", result);
+
+ const details = result.data?.trim();
+ return ResponseBuilder.markdown(
+ `## LocalStack State Imported\n\n**File:** \`${outputPath}\`\n\nImported local state from disk using the LocalStack State REST API. Use Cloud Pods when you need remote cloud-backed state snapshots.${details ? `\n\n${details}` : ""}`
+ );
+}
+
+async function handleReset(client: StateManagementApiClient, validation: ValidationResult) {
+ const result = await client.resetState(validation.serviceList);
+ if (!result.success) return formatStateApiError("reset", result);
+
+ const services = formatServiceList(validation.serviceList ?? []);
+ return ResponseBuilder.markdown(
+ `## LocalStack State Reset\n\n${validation.serviceList?.length ? "Selected service state was reset." : "All LocalStack service state was reset."}${services}`
+ );
+}
+
+async function handleInspect(client: StateManagementApiClient, validation: ValidationResult) {
+ const result = await client.inspectState();
+ if (!result.success) return formatStateApiError("inspect", result);
+
+ return formatInspectResult(result.data, validation.serviceList ?? []);
+}
+
+function formatExportSuccess(path: string, data: StateExportResult, services: string[]) {
+ const exportedServices = data.services.length ? data.services : services;
+ return ResponseBuilder.markdown(
+ `## LocalStack State Exported\n\n**File:** \`${path}\`\n\n**Bytes written:** ${data.content.length}${formatServiceList(exportedServices)}`
+ );
+}
+
+function formatServiceList(services: string[]) {
+ return services.length
+ ? `\n\n**Services:** ${services.map((service) => `\`${service}\``).join(", ")}`
+ : "";
+}
+
+export function formatInspectResult(data: unknown, services: string[]) {
+ if (!data) {
+ return ResponseBuilder.markdown("## LocalStack State Inspect\n\nNo state data returned.");
+ }
+
+ const filtered = services.length > 0 ? filterInspectServices(data, services) : data;
+ const servicesSummary = formatServiceList(services);
+ return ResponseBuilder.markdown(
+ `## LocalStack State Inspect${servicesSummary}\n\n\`\`\`json\n${JSON.stringify(filtered, null, 2)}\n\`\`\``
+ );
+}
+
+export function filterInspectServices(data: unknown, services: string[]) {
+ const serviceSet = new Set(services);
+ if (!data || typeof data !== "object" || Array.isArray(data)) return data;
+
+ return Object.fromEntries(
+ Object.entries(data as Record).map(([account, details]) => {
+ if (!details || typeof details !== "object" || Array.isArray(details)) {
+ return [account, details];
+ }
+ return [
+ account,
+ Object.fromEntries(
+ Object.entries(details as Record).filter(([service]) =>
+ serviceSet.has(service)
+ )
+ ),
+ ];
+ })
+ );
+}
+
+function formatStateApiError(action: StateManagementArgs["action"], result: ApiResult) {
+ if (result.success) return ResponseBuilder.error("Unexpected State Management API Result");
+
+ return ResponseBuilder.error(
+ "State Management API Error",
+ `The \`${action}\` action failed${result.statusCode ? ` with HTTP ${result.statusCode}` : ""}.${result.message ? `\n\n${result.message}` : ""}`
+ );
+}
diff --git a/tests/mcp/direct.spec.mjs b/tests/mcp/direct.spec.mjs
index 57bb5ca..d4234e4 100644
--- a/tests/mcp/direct.spec.mjs
+++ b/tests/mcp/direct.spec.mjs
@@ -7,6 +7,7 @@ const EXPECTED_TOOLS = [
"localstack-iam-policy-analyzer",
"localstack-chaos-injector",
"localstack-cloud-pods",
+ "localstack-state-management",
"localstack-extensions",
"localstack-snowflake-client",
"localstack-ephemeral-instances",