From 5b5fd2ddb7d2bbc493bd4bcb7968c3f0f9f533a4 Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Fri, 20 Feb 2026 14:39:20 +0530 Subject: [PATCH] add localstack extensions tool --- README.md | 14 +- manifest.json | 26 +++ src/lib/localstack/license-checker.ts | 1 + src/tools/localstack-extensions.ts | 303 ++++++++++++++++++++++++++ 4 files changed, 338 insertions(+), 6 deletions(-) create mode 100644 src/tools/localstack-extensions.ts diff --git a/README.md b/README.md index 8fb6571..487ad00 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. - - LocalStack Server MCP server - - 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. @@ -16,6 +12,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 @@ -30,6 +27,7 @@ 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-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 | ## Installation @@ -43,7 +41,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 @@ -107,4 +105,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) + + + LocalStack Server MCP server + \ No newline at end of file diff --git a/manifest.json b/manifest.json index d2e7471..ea685e2 100644 --- a/manifest.json +++ b/manifest.json @@ -46,6 +46,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" } ], "prompts": [ @@ -116,6 +120,28 @@ "description": "Manage LocalStack state snapshots (Cloud Pods) for saving, loading, deleting, and resetting", "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}." } ], "keywords": ["localstack", "aws", "cloud", "cloud-dev", "cloud-testing"], 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); + } +}