diff --git a/README.md b/README.md
index f236113..7bfeab1 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ This server eliminates custom scripts and manual LocalStack management with dire
- 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)
+- Launch and manage [Ephemeral Instances](https://docs.localstack.cloud/aws/capabilities/cloud-sandbox/ephemeral-instances/) for remote LocalStack testing workflows.
- Connect AI assistants and dev tools for automated cloud testing workflows.
## Tools Reference
@@ -29,6 +30,7 @@ This server provides your AI with dedicated tools for managing your LocalStack e
| [`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-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 |
diff --git a/manifest.json b/manifest.json
index ce07292..727930e 100644
--- a/manifest.json
+++ b/manifest.json
@@ -51,6 +51,10 @@
"name": "localstack-extensions",
"description": "Install, uninstall, list, and discover LocalStack Extensions from the marketplace"
},
+ {
+ "name": "localstack-ephemeral-instances",
+ "description": "Manage cloud-hosted LocalStack Ephemeral Instances by creating, listing, viewing logs, and deleting instances"
+ },
{
"name": "localstack-docs",
"description": "Search the LocalStack documentation to find guides, API references, and configuration details"
diff --git a/src/core/analytics.ts b/src/core/analytics.ts
index c6c8aa0..a3fb203 100644
--- a/src/core/analytics.ts
+++ b/src/core/analytics.ts
@@ -30,6 +30,14 @@ export const TOOL_ARG_ALLOWLIST: Record = {
"saveParams",
],
"localstack-docs": ["query", "limit"],
+ "localstack-ephemeral-instances": [
+ "action",
+ "name",
+ "lifetime",
+ "extension",
+ "cloudPod",
+ "envVarKeys",
+ ],
"localstack-extensions": ["action", "name", "source"],
"localstack-iam-policy-analyzer": ["action", "mode"],
"localstack-logs-analysis": ["analysisType", "lines", "service", "operation", "filter"],
diff --git a/src/tools/localstack-ephemeral-instances.ts b/src/tools/localstack-ephemeral-instances.ts
new file mode 100644
index 0000000..0862338
--- /dev/null
+++ b/src/tools/localstack-ephemeral-instances.ts
@@ -0,0 +1,296 @@
+import { z } from "zod";
+import { type ToolMetadata, type InferSchema } from "xmcp";
+import { runCommand, stripAnsiCodes } from "../core/command-runner";
+import { runPreflights, requireLocalStackCli, requireAuthToken } from "../core/preflight";
+import { ResponseBuilder } from "../core/response-builder";
+import { withToolAnalytics } from "../core/analytics";
+
+export const schema = {
+ action: z
+ .enum(["create", "list", "logs", "delete"])
+ .describe("The Ephemeral Instances action to perform."),
+ name: z
+ .string()
+ .optional()
+ .describe("Instance name. Required for create, logs, and delete actions."),
+ lifetime: z
+ .number()
+ .int()
+ .positive()
+ .optional()
+ .describe("Lifetime in minutes for create action. Defaults to CLI default when omitted."),
+ extension: z
+ .string()
+ .optional()
+ .describe(
+ "Optional extension package to preload for create action. This is passed as EXTENSION_AUTO_INSTALL."
+ ),
+ cloudPod: z
+ .string()
+ .optional()
+ .describe(
+ "Optional Cloud Pod name to initialize state for create action. This is passed as CLOUD_POD_NAME."
+ ),
+ envVars: z
+ .record(z.string(), z.string())
+ .optional()
+ .describe(
+ "Additional environment variables to pass to the ephemeral instance (create action only), translated to repeated --env KEY=VALUE flags."
+ ),
+};
+
+export const metadata: ToolMetadata = {
+ name: "localstack-ephemeral-instances",
+ description:
+ "Manage cloud-hosted LocalStack Ephemeral Instances: create, list, fetch logs, and delete.",
+ annotations: {
+ title: "LocalStack Ephemeral Instances",
+ readOnlyHint: false,
+ destructiveHint: true,
+ idempotentHint: false,
+ },
+};
+
+export default async function localstackEphemeralInstances({
+ action,
+ name,
+ lifetime,
+ extension,
+ cloudPod,
+ envVars,
+}: InferSchema) {
+ return withToolAnalytics(
+ "localstack-ephemeral-instances",
+ {
+ action,
+ name,
+ lifetime,
+ extension,
+ cloudPod,
+ envVarKeys: envVars ? Object.keys(envVars) : [],
+ },
+ async () => {
+ const authError = requireAuthToken();
+ if (authError) return authError;
+
+ const preflightError = await runPreflights([requireLocalStackCli()]);
+ if (preflightError) return preflightError;
+
+ switch (action) {
+ case "create":
+ return await handleCreate({ name, lifetime, extension, cloudPod, envVars });
+ case "list":
+ return await handleList();
+ case "logs":
+ return await handleLogs({ name });
+ case "delete":
+ return await handleDelete({ name });
+ default:
+ return ResponseBuilder.error("Unknown action", `Unsupported action: ${action}`);
+ }
+ }
+ );
+}
+
+function cleanOutput(stdout: string, stderr: string): { stdout: string; stderr: string; combined: string } {
+ const cleanStdout = stripAnsiCodes(stdout || "").trim();
+ const cleanStderr = stripAnsiCodes(stderr || "").trim();
+ const combined = [cleanStdout, cleanStderr].filter((part) => part.length > 0).join("\n").trim();
+ return { stdout: cleanStdout, stderr: cleanStderr, combined };
+}
+
+function parseJsonFromText(text: string): unknown {
+ const trimmed = text.trim();
+ if (!trimmed) return null;
+ try {
+ return JSON.parse(trimmed);
+ } catch {
+ const startObject = trimmed.indexOf("{");
+ const endObject = trimmed.lastIndexOf("}");
+ if (startObject !== -1 && endObject > startObject) {
+ const candidate = trimmed.slice(startObject, endObject + 1);
+ try {
+ return JSON.parse(candidate);
+ } catch {
+ // continue
+ }
+ }
+ const startArray = trimmed.indexOf("[");
+ const endArray = trimmed.lastIndexOf("]");
+ if (startArray !== -1 && endArray > startArray) {
+ const candidate = trimmed.slice(startArray, endArray + 1);
+ try {
+ return JSON.parse(candidate);
+ } catch {
+ // continue
+ }
+ }
+ return null;
+ }
+}
+
+function formatCreateResponse(payload: Record): string {
+ const endpoint = String(payload.endpoint_url ?? "N/A");
+ const id = String(payload.id ?? "N/A");
+ const status = String(payload.status ?? "unknown");
+ const creationTime = String(payload.creation_time ?? "N/A");
+ const expiryTime = String(payload.expiry_time ?? "N/A");
+
+ return `## Ephemeral Instance Created
+
+- **ID:** ${id}
+- **Status:** ${status}
+- **Endpoint URL:** ${endpoint}
+- **Creation Time:** ${creationTime}
+- **Expiry Time:** ${expiryTime}
+
+\`\`\`json
+${JSON.stringify(payload, null, 2)}
+\`\`\`
+
+Use this endpoint with your tools, for example:
+\`aws --endpoint-url=${endpoint} s3 ls\``;
+}
+
+async function handleCreate({
+ name,
+ lifetime,
+ extension,
+ cloudPod,
+ envVars,
+}: {
+ name?: string;
+ lifetime?: number;
+ extension?: string;
+ cloudPod?: string;
+ envVars?: Record;
+}) {
+ if (!name?.trim()) {
+ return ResponseBuilder.error(
+ "Missing Required Parameter",
+ "The `create` action requires the `name` parameter."
+ );
+ }
+
+ const args = ["ephemeral", "create", "--name", name.trim()];
+ if (lifetime !== undefined) {
+ args.push("--lifetime", String(lifetime));
+ }
+
+ const mergedEnvVars: Record = { ...(envVars || {}) };
+ if (extension) {
+ mergedEnvVars.EXTENSION_AUTO_INSTALL = extension;
+ }
+ if (cloudPod) {
+ mergedEnvVars.CLOUD_POD_NAME = cloudPod;
+ }
+
+ for (const [key, value] of Object.entries(mergedEnvVars)) {
+ if (!key || key.includes("=")) {
+ return ResponseBuilder.error(
+ "Invalid Environment Variable Key",
+ `Invalid env var key '${key}'. Keys must be non-empty and cannot contain '='.`
+ );
+ }
+ args.push("--env", `${key}=${value}`);
+ }
+
+ const result = await runCommand("localstack", args, {
+ env: { ...process.env },
+ timeout: 180000,
+ });
+ const cleaned = cleanOutput(result.stdout, result.stderr);
+
+ if (result.exitCode !== 0) {
+ return ResponseBuilder.error(
+ "Create Failed",
+ cleaned.combined || "Failed to create ephemeral instance."
+ );
+ }
+
+ const parsed = parseJsonFromText(cleaned.stdout) || parseJsonFromText(cleaned.combined);
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
+ return ResponseBuilder.markdown(formatCreateResponse(parsed as Record));
+ }
+
+ return ResponseBuilder.markdown(
+ `## Ephemeral Instance Created\n\n${cleaned.combined || "Instance created successfully."}`
+ );
+}
+
+async function handleList() {
+ const result = await runCommand("localstack", ["ephemeral", "list"], {
+ env: { ...process.env },
+ timeout: 120000,
+ });
+ const cleaned = cleanOutput(result.stdout, result.stderr);
+
+ if (result.exitCode !== 0) {
+ return ResponseBuilder.error("List Failed", cleaned.combined || "Failed to list ephemeral instances.");
+ }
+
+ const parsed = parseJsonFromText(cleaned.stdout) || parseJsonFromText(cleaned.combined);
+ if (parsed === null) {
+ return ResponseBuilder.markdown(
+ `## Ephemeral Instances\n\n\`\`\`\n${cleaned.combined || "No instances found."}\n\`\`\``
+ );
+ }
+
+ return ResponseBuilder.markdown(
+ `## Ephemeral Instances\n\n\`\`\`json\n${JSON.stringify(parsed, null, 2)}\n\`\`\``
+ );
+}
+
+async function handleLogs({ name }: { name?: string }) {
+ if (!name?.trim()) {
+ return ResponseBuilder.error(
+ "Missing Required Parameter",
+ "The `logs` action requires the `name` parameter."
+ );
+ }
+
+ const result = await runCommand("localstack", ["ephemeral", "logs", "--name", name.trim()], {
+ env: { ...process.env },
+ timeout: 180000,
+ });
+ const cleaned = cleanOutput(result.stdout, result.stderr);
+
+ if (result.exitCode !== 0) {
+ return ResponseBuilder.error(
+ "Logs Failed",
+ cleaned.combined || `Failed to fetch logs for instance '${name}'.`
+ );
+ }
+
+ if (!cleaned.combined) {
+ return ResponseBuilder.markdown(`No logs available for ephemeral instance '${name}'.`);
+ }
+
+ return ResponseBuilder.markdown(
+ `## Ephemeral Instance Logs: ${name}\n\n\`\`\`\n${cleaned.combined}\n\`\`\``
+ );
+}
+
+async function handleDelete({ name }: { name?: string }) {
+ if (!name?.trim()) {
+ return ResponseBuilder.error(
+ "Missing Required Parameter",
+ "The `delete` action requires the `name` parameter."
+ );
+ }
+
+ const result = await runCommand("localstack", ["ephemeral", "delete", "--name", name.trim()], {
+ env: { ...process.env },
+ timeout: 120000,
+ });
+ const cleaned = cleanOutput(result.stdout, result.stderr);
+
+ if (result.exitCode !== 0) {
+ return ResponseBuilder.error(
+ "Delete Failed",
+ cleaned.combined || `Failed to delete ephemeral instance '${name}'.`
+ );
+ }
+
+ return ResponseBuilder.markdown(cleaned.combined || `Successfully deleted instance: ${name} ✅`);
+}
diff --git a/tests/mcp/direct.spec.mjs b/tests/mcp/direct.spec.mjs
index 6b52fd9..2530169 100644
--- a/tests/mcp/direct.spec.mjs
+++ b/tests/mcp/direct.spec.mjs
@@ -8,6 +8,7 @@ const EXPECTED_TOOLS = [
"localstack-chaos-injector",
"localstack-cloud-pods",
"localstack-extensions",
+ "localstack-ephemeral-instances",
"localstack-aws-client",
"localstack-docs",
];