Skip to content

Commit 5b5fd2d

Browse files
committed
add localstack extensions tool
1 parent e988658 commit 5b5fd2d

4 files changed

Lines changed: 338 additions & 6 deletions

File tree

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,14 @@
55
66
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.
77

8-
<a href="https://glama.ai/mcp/servers/@localstack/localstack-mcp-server">
9-
<img width="380" height="200" src="https://glama.ai/mcp/servers/@localstack/localstack-mcp-server/badge" alt="LocalStack Server MCP server" />
10-
</a>
11-
128
This server eliminates custom scripts and manual LocalStack management with direct access to:
139

1410
- Start, stop, restart, and monitor LocalStack for AWS container status with built-in auth.
1511
- Deploy CDK and Terraform projects with automatic configuration detection.
1612
- Parse logs, catch errors, and auto-generate IAM policies from violations. (requires active license)
1713
- Inject chaos faults and network effects into LocalStack to test system resilience. (requires active license)
1814
- Manage LocalStack state snapshots via [Cloud Pods](https://docs.localstack.cloud/aws/capabilities/state-management/cloud-pods/) for development workflows. (requires active license)
15+
- Install, remove, list, and discover [LocalStack Extensions](https://docs.localstack.cloud/aws/capabilities/extensions/) from the marketplace. (requires active license)
1916
- Connect AI assistants and dev tools for automated cloud testing workflows.
2017

2118
## Tools Reference
@@ -30,6 +27,7 @@ This server provides your AI with dedicated tools for managing your LocalStack e
3027
| [`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<br/>- Search logs for permission-related violations<br/>- Generate IAM policies automatically from detected access failures<br/>- Requires a valid LocalStack Auth Token |
3128
| [`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<br/>- Configure network latency effects<br/>- Comprehensive fault targeting by service, region, and operation<br/>- Built-in workflow guidance for chaos experiments<br/>- Requires a valid LocalStack Auth Token |
3229
| [`localstack-cloud-pods`](./src/tools/localstack-cloud-pods.ts) | Manages LocalStack state snapshots for development workflows | - Save current state as Cloud Pods<br/>- Load previously saved Cloud Pods instantly<br/>- Delete Cloud Pods or reset to a clean state<br/>- Requires a valid LocalStack Auth Token |
30+
| [`localstack-extensions`](./src/tools/localstack-extensions.ts) | Installs, uninstalls, lists, and discovers LocalStack Extensions | - Manage installed extensions via CLI actions (`list`, `install`, `uninstall`)<br/>- Browse the LocalStack Extensions marketplace (`available`)<br/>- Requires a valid LocalStack Auth Token support |
3331
| [`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<br/>- Sanitizes commands to block shell chaining<br/>- Auto-detects LocalStack coverage errors and links to docs |
3432

3533
## Installation
@@ -43,7 +41,7 @@ For other MCP Clients, refer to the [configuration guide](#configuration).
4341

4442
- [LocalStack CLI](https://docs.localstack.cloud/getting-started/installation/#localstack-cli) and Docker installed in your system path
4543
- [`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
46-
- 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**)
44+
- 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**)
4745
- [Node.js v22.x](https://nodejs.org/en/download/) installed in your system path
4846

4947
### Configuration
@@ -107,4 +105,8 @@ Built on the [XMCP](https://github.com/basementstudio/xmcp) framework, you can a
107105

108106
## License
109107

110-
[Apache License 2.0](./LICENSE)
108+
[Apache License 2.0](./LICENSE)
109+
110+
<a href="https://glama.ai/mcp/servers/@localstack/localstack-mcp-server">
111+
<img width="380" height="200" src="https://glama.ai/mcp/servers/@localstack/localstack-mcp-server/badge" alt="LocalStack Server MCP server" />
112+
</a>

manifest.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
{
4747
"name": "localstack-cloud-pods",
4848
"description": "Manages LocalStack state snapshots (Cloud Pods) for saving, loading, deleting, and resetting"
49+
},
50+
{
51+
"name": "localstack-extensions",
52+
"description": "Install, uninstall, list, and discover LocalStack Extensions from the marketplace"
4953
}
5054
],
5155
"prompts": [
@@ -116,6 +120,28 @@
116120
"description": "Manage LocalStack state snapshots (Cloud Pods) for saving, loading, deleting, and resetting",
117121
"arguments": ["action", "pod_name"],
118122
"text": "Please ${arguments.action} Cloud Pod in the LocalStack container with the pod name ${arguments.pod_name}."
123+
},
124+
{
125+
"name": "extensions-list",
126+
"description": "List installed LocalStack extensions",
127+
"text": "List all LocalStack extensions currently installed on my machine."
128+
},
129+
{
130+
"name": "extensions-available",
131+
"description": "Browse available LocalStack extensions",
132+
"text": "Show me the available extensions in the LocalStack marketplace."
133+
},
134+
{
135+
"name": "extensions-install",
136+
"description": "Install a LocalStack extension",
137+
"arguments": ["extension_name"],
138+
"text": "Install the LocalStack extension ${arguments.extension_name}."
139+
},
140+
{
141+
"name": "extensions-uninstall",
142+
"description": "Uninstall a LocalStack extension",
143+
"arguments": ["extension_name"],
144+
"text": "Uninstall the LocalStack extension ${arguments.extension_name}."
119145
}
120146
],
121147
"keywords": ["localstack", "aws", "cloud", "cloud-dev", "cloud-testing"],

src/lib/localstack/license-checker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export enum ProFeature {
77
IAM_ENFORCEMENT = "localstack.platform.plugin/iam-enforcement",
88
CLOUD_PODS = "localstack.platform.plugin/pods",
99
CHAOS_ENGINEERING = "localstack.platform.plugin/chaos",
10+
EXTENSIONS = "localstack.platform.plugin/extensions",
1011
}
1112

1213
export interface LicenseCheckResult {

src/tools/localstack-extensions.ts

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
import { z } from "zod";
2+
import { type ToolMetadata, type InferSchema } from "xmcp";
3+
import { HttpClient, HttpError } from "../core/http-client";
4+
import { runCommand, stripAnsiCodes } from "../core/command-runner";
5+
import {
6+
runPreflights,
7+
requireLocalStackCli,
8+
requireLocalStackRunning,
9+
requireProFeature,
10+
} from "../core/preflight";
11+
import { ResponseBuilder } from "../core/response-builder";
12+
import { ProFeature } from "../lib/localstack/license-checker";
13+
14+
export const schema = {
15+
action: z
16+
.enum(["list", "install", "uninstall", "available"])
17+
.describe(
18+
"list = installed extensions; install = install an extension; uninstall = remove an extension; available = browse the marketplace/extensions library"
19+
),
20+
name: z
21+
.string()
22+
.optional()
23+
.describe(
24+
"Extension package name (e.g. 'localstack-extension-typedb' or 'localstack-extension-typedb==1.0.0'). Required for install and uninstall actions."
25+
),
26+
source: z
27+
.string()
28+
.optional()
29+
.describe(
30+
"Git URL to install from (e.g. 'git+https://github.com/org/repo.git'). Use this instead of name when installing from a repository."
31+
),
32+
};
33+
34+
export const metadata: ToolMetadata = {
35+
name: "localstack-extensions",
36+
description: "Install, uninstall, list, and discover LocalStack Extensions from the marketplace",
37+
annotations: {
38+
title: "LocalStack Extensions",
39+
readOnlyHint: false,
40+
destructiveHint: false,
41+
idempotentHint: false,
42+
},
43+
};
44+
45+
interface MarketplaceExtension {
46+
name?: string;
47+
summary?: string;
48+
description?: string;
49+
author?: string;
50+
version?: string;
51+
}
52+
53+
const AUTH_TOKEN_REQUIRED_MESSAGE =
54+
"LOCALSTACK_AUTH_TOKEN is not set in your environment. LocalStack Extensions require a valid Auth Token. Please set it and try again.";
55+
56+
export default async function localstackExtensions({
57+
action,
58+
name,
59+
source,
60+
}: InferSchema<typeof schema>) {
61+
const checks = [
62+
requireLocalStackCli(),
63+
requireLocalStackRunning(),
64+
requireProFeature(ProFeature.EXTENSIONS),
65+
];
66+
67+
const preflightError = await runPreflights(checks);
68+
if (preflightError) return preflightError;
69+
70+
switch (action) {
71+
case "list":
72+
return await handleList();
73+
case "install":
74+
return await handleInstall(name, source);
75+
case "uninstall":
76+
return await handleUninstall(name);
77+
case "available":
78+
return await handleAvailable();
79+
default:
80+
return ResponseBuilder.error("Unknown action", `Unsupported action: ${action}`);
81+
}
82+
}
83+
84+
function requireAuthTokenForCli() {
85+
if (!process.env.LOCALSTACK_AUTH_TOKEN) {
86+
return ResponseBuilder.error("Auth Token Required", AUTH_TOKEN_REQUIRED_MESSAGE);
87+
}
88+
return null;
89+
}
90+
91+
function cleanOutput(stdout: string, stderr: string) {
92+
return {
93+
stdout: stripAnsiCodes(stdout || "").trim(),
94+
stderr: stripAnsiCodes(stderr || "").trim(),
95+
};
96+
}
97+
98+
function combineOutput(stdout: string, stderr: string): string {
99+
return [stdout, stderr].filter((part) => part.trim().length > 0).join("\n").trim();
100+
}
101+
102+
async function handleList() {
103+
const authError = requireAuthTokenForCli();
104+
if (authError) return authError;
105+
106+
const cmd = await runCommand("localstack", ["extensions", "list"], {
107+
env: { ...process.env },
108+
});
109+
const cleaned = cleanOutput(cmd.stdout, cmd.stderr);
110+
const combined = combineOutput(cleaned.stdout, cleaned.stderr);
111+
const combinedLower = combined.toLowerCase();
112+
113+
if (cmd.exitCode !== 0 && !combined) {
114+
return ResponseBuilder.error("List Failed", cleaned.stderr || "Failed to list installed extensions.");
115+
}
116+
117+
const looksEmpty =
118+
!combined ||
119+
combinedLower.includes("no extensions installed") ||
120+
combinedLower.includes("no extension installed");
121+
if (looksEmpty) {
122+
return ResponseBuilder.markdown(
123+
"No LocalStack extensions are currently installed.\n\nUse the `available` action to browse the marketplace."
124+
);
125+
}
126+
127+
return ResponseBuilder.markdown(`## Installed LocalStack Extensions\n\n\`\`\`\n${combined}\n\`\`\``);
128+
}
129+
130+
async function handleInstall(name?: string, source?: string) {
131+
const authError = requireAuthTokenForCli();
132+
if (authError) return authError;
133+
134+
const hasName = !!name;
135+
const hasSource = !!source;
136+
if ((hasName && hasSource) || (!hasName && !hasSource)) {
137+
return ResponseBuilder.error(
138+
"Invalid Parameters",
139+
"Provide either `name` or `source` for install, but not both."
140+
);
141+
}
142+
143+
const target = source || name!;
144+
const cmd = await runCommand("localstack", ["extensions", "install", target], {
145+
env: { ...process.env },
146+
timeout: 120000,
147+
});
148+
const cleaned = cleanOutput(cmd.stdout, cmd.stderr);
149+
const combined = combineOutput(cleaned.stdout, cleaned.stderr);
150+
const combinedLower = combined.toLowerCase();
151+
152+
if (combinedLower.includes("could not resolve package")) {
153+
return ResponseBuilder.error(
154+
"Extension Not Found",
155+
`Could not resolve the extension package '${name || target}'. Please verify it exists on PyPI, or provide a git repository URL using the source parameter.`
156+
);
157+
}
158+
159+
if (combinedLower.includes("no module named 'localstack.pro'")) {
160+
return ResponseBuilder.error(
161+
"Auth Token Required",
162+
"LocalStack Pro modules are not available. Ensure LOCALSTACK_AUTH_TOKEN is set correctly and LocalStack is running with a valid license."
163+
);
164+
}
165+
166+
if (
167+
combinedLower.includes("non-zero exit status") ||
168+
combinedLower.includes("returned non-zero")
169+
) {
170+
return ResponseBuilder.error(
171+
"Install Failed",
172+
"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."
173+
);
174+
}
175+
176+
const hasSuccessPattern = combinedLower.includes("extension successfully installed");
177+
if (cmd.exitCode !== 0 && !hasSuccessPattern) {
178+
return ResponseBuilder.error("Install Failed", cleaned.stderr || "Extension installation failed.");
179+
}
180+
181+
if (hasSuccessPattern || cmd.exitCode === 0) {
182+
const restartCmd = await runCommand("localstack", ["restart"], { timeout: 60000 });
183+
const restartCleaned = cleanOutput(restartCmd.stdout, restartCmd.stderr);
184+
const restartCombined = combineOutput(restartCleaned.stdout, restartCleaned.stderr);
185+
186+
let response = `## Extension Installation Result\n\n\`\`\`\n${combined || "Extension successfully installed."}\n\`\`\`\n\n`;
187+
response += "LocalStack was restarted to activate the extension.";
188+
if (restartCombined) {
189+
response += `\n\n### Restart Output\n\n\`\`\`\n${restartCombined}\n\`\`\``;
190+
}
191+
if (restartCmd.exitCode !== 0) {
192+
response += "\n\n⚠️ Restart command reported an issue. Please verify LocalStack status.";
193+
}
194+
return ResponseBuilder.markdown(response);
195+
}
196+
197+
return ResponseBuilder.error("Install Failed", cleaned.stderr || "Extension installation failed.");
198+
}
199+
200+
async function handleUninstall(name?: string) {
201+
const authError = requireAuthTokenForCli();
202+
if (authError) return authError;
203+
204+
if (!name) {
205+
return ResponseBuilder.error(
206+
"Missing Required Parameter",
207+
"The `uninstall` action requires the `name` parameter to be specified."
208+
);
209+
}
210+
211+
const cmd = await runCommand("localstack", ["extensions", "uninstall", name], {
212+
env: { ...process.env },
213+
timeout: 60000,
214+
});
215+
const cleaned = cleanOutput(cmd.stdout, cmd.stderr);
216+
const combined = combineOutput(cleaned.stdout, cleaned.stderr);
217+
const combinedLower = combined.toLowerCase();
218+
219+
if (combinedLower.includes("no module named 'localstack.pro'")) {
220+
return ResponseBuilder.error(
221+
"Auth Token Required",
222+
"LocalStack Pro modules are not available. Ensure LOCALSTACK_AUTH_TOKEN is set correctly and LocalStack is running with a valid license."
223+
);
224+
}
225+
226+
const hasSuccessPattern = combinedLower.includes("extension successfully uninstalled");
227+
if (cmd.exitCode !== 0 && !hasSuccessPattern) {
228+
return ResponseBuilder.error("Uninstall Failed", cleaned.stderr || "Extension uninstallation failed.");
229+
}
230+
231+
if (hasSuccessPattern || cmd.exitCode === 0) {
232+
const restartCmd = await runCommand("localstack", ["restart"], { timeout: 60000 });
233+
const restartCleaned = cleanOutput(restartCmd.stdout, restartCmd.stderr);
234+
const restartCombined = combineOutput(restartCleaned.stdout, restartCleaned.stderr);
235+
236+
let response = `## Extension Uninstall Result\n\n\`\`\`\n${combined || "Extension successfully uninstalled."}\n\`\`\`\n\n`;
237+
response += "LocalStack was restarted to apply extension removal.";
238+
if (restartCombined) {
239+
response += `\n\n### Restart Output\n\n\`\`\`\n${restartCombined}\n\`\`\``;
240+
}
241+
if (restartCmd.exitCode !== 0) {
242+
response += "\n\n⚠️ Restart command reported an issue. Please verify LocalStack status.";
243+
}
244+
return ResponseBuilder.markdown(response);
245+
}
246+
247+
return ResponseBuilder.error("Uninstall Failed", cleaned.stderr || "Extension uninstallation failed.");
248+
}
249+
250+
async function handleAvailable() {
251+
const token = process.env.LOCALSTACK_AUTH_TOKEN;
252+
if (!token) {
253+
return ResponseBuilder.error(
254+
"Authentication Failed",
255+
"Could not fetch the marketplace. Ensure LOCALSTACK_AUTH_TOKEN is set correctly."
256+
);
257+
}
258+
259+
const encoded = Buffer.from(`:${token}`).toString("base64");
260+
const client = new HttpClient();
261+
262+
try {
263+
const marketplace = await client.request<MarketplaceExtension[]>(
264+
"https://api.localstack.cloud/v1/extensions/marketplace",
265+
{
266+
method: "GET",
267+
baseUrl: "",
268+
headers: {
269+
Authorization: `Basic ${encoded}`,
270+
Accept: "application/json",
271+
},
272+
}
273+
);
274+
275+
if (!Array.isArray(marketplace)) {
276+
return ResponseBuilder.error("Marketplace Fetch Failed", "Unexpected marketplace response format.");
277+
}
278+
279+
const simplified = marketplace.map((item) => ({
280+
name: item.name || "unknown-extension",
281+
summary: item.summary || item.description || "No summary provided.",
282+
author: item.author || "Unknown",
283+
version: item.version || "Unknown",
284+
}));
285+
286+
let markdown = `# LocalStack Extensions Marketplace\n\n${simplified.length} extensions available. Install any with the \`install\` action.\n\n---`;
287+
for (const extension of simplified) {
288+
markdown += `\n\n### ${extension.name}\n**Author:** ${extension.author} | **Version:** ${extension.version}\n${extension.summary}\n\n---`;
289+
}
290+
291+
return ResponseBuilder.markdown(markdown);
292+
} catch (error) {
293+
if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
294+
return ResponseBuilder.error(
295+
"Authentication Failed",
296+
"Could not fetch the marketplace. Ensure LOCALSTACK_AUTH_TOKEN is set correctly."
297+
);
298+
}
299+
300+
const message = error instanceof Error ? error.message : String(error);
301+
return ResponseBuilder.error("Marketplace Fetch Failed", message);
302+
}
303+
}

0 commit comments

Comments
 (0)