diff --git a/README.md b/README.md
index 845834d..39f7f13 100644
--- a/README.md
+++ b/README.md
@@ -5,10 +5,6 @@
A [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro) (MCP) server that provides tools to manage and interact with your [LocalStack for AWS](https://www.localstack.cloud/localstack-for-aws) container for simplified local cloud development and testing. The LocalStack MCP Server provides simplified integration between MCP-compatible apps and your local LocalStack for AWS development environment, enabling secure and direct communication with LocalStack's emulated services and additional developer experience features.
-
-
-
-
This server eliminates custom scripts and manual LocalStack management with direct access to:
- Start, stop, restart, and monitor LocalStack for AWS container status with built-in auth.
@@ -17,6 +13,7 @@ This server eliminates custom scripts and manual LocalStack management with dire
- 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)
- Connect AI assistants and dev tools for automated cloud testing workflows.
## Tools Reference
@@ -31,7 +28,8 @@ This server provides your AI with dedicated tools for managing your LocalStack e
| [`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-aws-client`](./src/tools/localstack-aws-client.ts) | Runs AWS CLI commands inside the LocalStack 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-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-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 |
## Installation
@@ -45,7 +43,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) or [`tflocal`](https://github.com/localstack/terraform-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, and Chaos Injector tools (**optional**)
+- 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**)
- [Node.js v22.x](https://nodejs.org/en/download/) installed in your system path
### Configuration
@@ -109,4 +107,8 @@ Built on the [XMCP](https://github.com/basementstudio/xmcp) framework, you can a
## License
-[Apache License 2.0](./LICENSE)
\ No newline at end of file
+[Apache License 2.0](./LICENSE)
+
+
+
+
\ No newline at end of file
diff --git a/manifest.json b/manifest.json
index 0d3b659..ac77dd9 100644
--- a/manifest.json
+++ b/manifest.json
@@ -47,6 +47,10 @@
"name": "localstack-cloud-pods",
"description": "Manages LocalStack state snapshots (Cloud Pods) for saving, loading, deleting, and resetting"
},
+ {
+ "name": "localstack-extensions",
+ "description": "Install, uninstall, list, and discover LocalStack Extensions from the marketplace"
+ },
{
"name": "localstack-docs",
"description": "Search the LocalStack documentation to find guides, API references, and configuration details"
@@ -121,6 +125,28 @@
"arguments": ["action", "pod_name"],
"text": "Please ${arguments.action} Cloud Pod in the LocalStack container with the pod name ${arguments.pod_name}."
},
+ {
+ "name": "extensions-list",
+ "description": "List installed LocalStack extensions",
+ "text": "List all LocalStack extensions currently installed on my machine."
+ },
+ {
+ "name": "extensions-available",
+ "description": "Browse available LocalStack extensions",
+ "text": "Show me the available extensions in the LocalStack marketplace."
+ },
+ {
+ "name": "extensions-install",
+ "description": "Install a LocalStack extension",
+ "arguments": ["extension_name"],
+ "text": "Install the LocalStack extension ${arguments.extension_name}."
+ },
+ {
+ "name": "extensions-uninstall",
+ "description": "Uninstall a LocalStack extension",
+ "arguments": ["extension_name"],
+ "text": "Uninstall the LocalStack extension ${arguments.extension_name}."
+ },
{
"name": "localstack-docs-search",
"description": "Search LocalStack documentation",
diff --git a/src/lib/localstack/license-checker.ts b/src/lib/localstack/license-checker.ts
index 4f3bf3a..af7cf9e 100644
--- a/src/lib/localstack/license-checker.ts
+++ b/src/lib/localstack/license-checker.ts
@@ -7,6 +7,7 @@ export enum ProFeature {
IAM_ENFORCEMENT = "localstack.platform.plugin/iam-enforcement",
CLOUD_PODS = "localstack.platform.plugin/pods",
CHAOS_ENGINEERING = "localstack.platform.plugin/chaos",
+ EXTENSIONS = "localstack.platform.plugin/extensions",
}
export interface LicenseCheckResult {
diff --git a/src/tools/localstack-extensions.ts b/src/tools/localstack-extensions.ts
new file mode 100644
index 0000000..977899e
--- /dev/null
+++ b/src/tools/localstack-extensions.ts
@@ -0,0 +1,303 @@
+import { z } from "zod";
+import { type ToolMetadata, type InferSchema } from "xmcp";
+import { HttpClient, HttpError } from "../core/http-client";
+import { runCommand, stripAnsiCodes } from "../core/command-runner";
+import {
+ runPreflights,
+ requireLocalStackCli,
+ requireLocalStackRunning,
+ requireProFeature,
+} from "../core/preflight";
+import { ResponseBuilder } from "../core/response-builder";
+import { ProFeature } from "../lib/localstack/license-checker";
+
+export const schema = {
+ action: z
+ .enum(["list", "install", "uninstall", "available"])
+ .describe(
+ "list = installed extensions; install = install an extension; uninstall = remove an extension; available = browse the marketplace/extensions library"
+ ),
+ name: z
+ .string()
+ .optional()
+ .describe(
+ "Extension package name (e.g. 'localstack-extension-typedb' or 'localstack-extension-typedb==1.0.0'). Required for install and uninstall actions."
+ ),
+ source: z
+ .string()
+ .optional()
+ .describe(
+ "Git URL to install from (e.g. 'git+https://github.com/org/repo.git'). Use this instead of name when installing from a repository."
+ ),
+};
+
+export const metadata: ToolMetadata = {
+ name: "localstack-extensions",
+ description: "Install, uninstall, list, and discover LocalStack Extensions from the marketplace",
+ annotations: {
+ title: "LocalStack Extensions",
+ readOnlyHint: false,
+ destructiveHint: false,
+ idempotentHint: false,
+ },
+};
+
+interface MarketplaceExtension {
+ name?: string;
+ summary?: string;
+ description?: string;
+ author?: string;
+ 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,
+ source,
+}: InferSchema) {
+ const checks = [
+ requireLocalStackCli(),
+ requireLocalStackRunning(),
+ requireProFeature(ProFeature.EXTENSIONS),
+ ];
+
+ const preflightError = await runPreflights(checks);
+ if (preflightError) return preflightError;
+
+ switch (action) {
+ case "list":
+ return await handleList();
+ case "install":
+ return await handleInstall(name, source);
+ case "uninstall":
+ return await handleUninstall(name);
+ case "available":
+ return await handleAvailable();
+ default:
+ return ResponseBuilder.error("Unknown action", `Unsupported action: ${action}`);
+ }
+}
+
+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(),
+ stderr: stripAnsiCodes(stderr || "").trim(),
+ };
+}
+
+function combineOutput(stdout: string, stderr: string): string {
+ return [stdout, stderr].filter((part) => part.trim().length > 0).join("\n").trim();
+}
+
+async function handleList() {
+ const authError = requireAuthTokenForCli();
+ if (authError) return authError;
+
+ const cmd = await runCommand("localstack", ["extensions", "list"], {
+ env: { ...process.env },
+ });
+ const cleaned = cleanOutput(cmd.stdout, cmd.stderr);
+ const combined = combineOutput(cleaned.stdout, cleaned.stderr);
+ const combinedLower = combined.toLowerCase();
+
+ if (cmd.exitCode !== 0 && !combined) {
+ return ResponseBuilder.error("List Failed", cleaned.stderr || "Failed to list installed extensions.");
+ }
+
+ const looksEmpty =
+ !combined ||
+ combinedLower.includes("no extensions installed") ||
+ combinedLower.includes("no extension installed");
+ if (looksEmpty) {
+ return ResponseBuilder.markdown(
+ "No LocalStack extensions are currently installed.\n\nUse the `available` action to browse the marketplace."
+ );
+ }
+
+ return ResponseBuilder.markdown(`## Installed LocalStack Extensions\n\n\`\`\`\n${combined}\n\`\`\``);
+}
+
+async function handleInstall(name?: string, source?: string) {
+ const authError = requireAuthTokenForCli();
+ if (authError) return authError;
+
+ const hasName = !!name;
+ const hasSource = !!source;
+ if ((hasName && hasSource) || (!hasName && !hasSource)) {
+ return ResponseBuilder.error(
+ "Invalid Parameters",
+ "Provide either `name` or `source` for install, but not both."
+ );
+ }
+
+ const target = source || name!;
+ const cmd = await runCommand("localstack", ["extensions", "install", target], {
+ env: { ...process.env },
+ timeout: 120000,
+ });
+ const cleaned = cleanOutput(cmd.stdout, cmd.stderr);
+ const combined = combineOutput(cleaned.stdout, cleaned.stderr);
+ const combinedLower = combined.toLowerCase();
+
+ if (combinedLower.includes("could not resolve package")) {
+ return ResponseBuilder.error(
+ "Extension Not Found",
+ `Could not resolve the extension package '${name || target}'. Please verify it exists on PyPI, or provide a git repository URL using the source parameter.`
+ );
+ }
+
+ if (combinedLower.includes("no module named 'localstack.pro'")) {
+ return ResponseBuilder.error(
+ "Auth Token Required",
+ "LocalStack Pro modules are not available. Ensure LOCALSTACK_AUTH_TOKEN is set correctly and LocalStack is running with a valid license."
+ );
+ }
+
+ if (
+ combinedLower.includes("non-zero exit status") ||
+ combinedLower.includes("returned non-zero")
+ ) {
+ return ResponseBuilder.error(
+ "Install Failed",
+ "The extension could not be installed from the provided source. The repository may not contain valid LocalStack extension code. Run the command again with --verbose for more details, or check that the repository contains a proper LocalStack extension."
+ );
+ }
+
+ const hasSuccessPattern = combinedLower.includes("extension successfully installed");
+ if (cmd.exitCode !== 0 && !hasSuccessPattern) {
+ return ResponseBuilder.error("Install Failed", cleaned.stderr || "Extension installation failed.");
+ }
+
+ if (hasSuccessPattern || cmd.exitCode === 0) {
+ const restartCmd = await runCommand("localstack", ["restart"], { timeout: 60000 });
+ const restartCleaned = cleanOutput(restartCmd.stdout, restartCmd.stderr);
+ const restartCombined = combineOutput(restartCleaned.stdout, restartCleaned.stderr);
+
+ let response = `## Extension Installation Result\n\n\`\`\`\n${combined || "Extension successfully installed."}\n\`\`\`\n\n`;
+ response += "LocalStack was restarted to activate the extension.";
+ if (restartCombined) {
+ response += `\n\n### Restart Output\n\n\`\`\`\n${restartCombined}\n\`\`\``;
+ }
+ if (restartCmd.exitCode !== 0) {
+ response += "\n\n⚠️ Restart command reported an issue. Please verify LocalStack status.";
+ }
+ return ResponseBuilder.markdown(response);
+ }
+
+ return ResponseBuilder.error("Install Failed", cleaned.stderr || "Extension installation failed.");
+}
+
+async function handleUninstall(name?: string) {
+ const authError = requireAuthTokenForCli();
+ if (authError) return authError;
+
+ if (!name) {
+ return ResponseBuilder.error(
+ "Missing Required Parameter",
+ "The `uninstall` action requires the `name` parameter to be specified."
+ );
+ }
+
+ const cmd = await runCommand("localstack", ["extensions", "uninstall", name], {
+ env: { ...process.env },
+ timeout: 60000,
+ });
+ const cleaned = cleanOutput(cmd.stdout, cmd.stderr);
+ const combined = combineOutput(cleaned.stdout, cleaned.stderr);
+ const combinedLower = combined.toLowerCase();
+
+ if (combinedLower.includes("no module named 'localstack.pro'")) {
+ return ResponseBuilder.error(
+ "Auth Token Required",
+ "LocalStack Pro modules are not available. Ensure LOCALSTACK_AUTH_TOKEN is set correctly and LocalStack is running with a valid license."
+ );
+ }
+
+ const hasSuccessPattern = combinedLower.includes("extension successfully uninstalled");
+ if (cmd.exitCode !== 0 && !hasSuccessPattern) {
+ return ResponseBuilder.error("Uninstall Failed", cleaned.stderr || "Extension uninstallation failed.");
+ }
+
+ if (hasSuccessPattern || cmd.exitCode === 0) {
+ const restartCmd = await runCommand("localstack", ["restart"], { timeout: 60000 });
+ const restartCleaned = cleanOutput(restartCmd.stdout, restartCmd.stderr);
+ const restartCombined = combineOutput(restartCleaned.stdout, restartCleaned.stderr);
+
+ let response = `## Extension Uninstall Result\n\n\`\`\`\n${combined || "Extension successfully uninstalled."}\n\`\`\`\n\n`;
+ response += "LocalStack was restarted to apply extension removal.";
+ if (restartCombined) {
+ response += `\n\n### Restart Output\n\n\`\`\`\n${restartCombined}\n\`\`\``;
+ }
+ if (restartCmd.exitCode !== 0) {
+ response += "\n\n⚠️ Restart command reported an issue. Please verify LocalStack status.";
+ }
+ return ResponseBuilder.markdown(response);
+ }
+
+ return ResponseBuilder.error("Uninstall Failed", cleaned.stderr || "Extension uninstallation failed.");
+}
+
+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 encoded = Buffer.from(`:${token}`).toString("base64");
+ const client = new HttpClient();
+
+ try {
+ const marketplace = await client.request(
+ "https://api.localstack.cloud/v1/extensions/marketplace",
+ {
+ method: "GET",
+ baseUrl: "",
+ headers: {
+ Authorization: `Basic ${encoded}`,
+ Accept: "application/json",
+ },
+ }
+ );
+
+ if (!Array.isArray(marketplace)) {
+ return ResponseBuilder.error("Marketplace Fetch Failed", "Unexpected marketplace response format.");
+ }
+
+ const simplified = marketplace.map((item) => ({
+ name: item.name || "unknown-extension",
+ summary: item.summary || item.description || "No summary provided.",
+ author: item.author || "Unknown",
+ version: item.version || "Unknown",
+ }));
+
+ let markdown = `# LocalStack Extensions Marketplace\n\n${simplified.length} extensions available. Install any with the \`install\` action.\n\n---`;
+ for (const extension of simplified) {
+ markdown += `\n\n### ${extension.name}\n**Author:** ${extension.author} | **Version:** ${extension.version}\n${extension.summary}\n\n---`;
+ }
+
+ return ResponseBuilder.markdown(markdown);
+ } catch (error) {
+ if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
+ return ResponseBuilder.error(
+ "Authentication Failed",
+ "Could not fetch the marketplace. Ensure LOCALSTACK_AUTH_TOKEN is set correctly."
+ );
+ }
+
+ const message = error instanceof Error ? error.message : String(error);
+ return ResponseBuilder.error("Marketplace Fetch Failed", message);
+ }
+}