diff --git a/README.md b/README.md
index b1840fa..9bbd1de 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ A [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/
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.
-- Deploy CDK and Terraform projects with automatic configuration detection.
+- 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)
@@ -23,7 +23,7 @@ This server provides your AI with dedicated tools for managing your LocalStack e
| 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 and Terraform 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) or [`tflocal`](https://github.com/localstack/terraform-local) installed in your system path |
+| [`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 |
@@ -42,7 +42,7 @@ For other MCP Clients, refer to the [configuration guide](#configuration).
### Prerequisites
- [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
+- [`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 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, Chaos Injector, and Extensions tools (**optional**)
- [Node.js v22.x](https://nodejs.org/en/download/) installed in your system path
diff --git a/src/core/analytics.ts b/src/core/analytics.ts
index d740ed5..c6c8aa0 100644
--- a/src/core/analytics.ts
+++ b/src/core/analytics.ts
@@ -19,7 +19,16 @@ export const TOOL_ARG_ALLOWLIST: Record = {
"localstack-aws-client": ["command"],
"localstack-chaos-injector": ["action", "rules_count", "latency_ms"],
"localstack-cloud-pods": ["action", "pod_name"],
- "localstack-deployer": ["action", "projectType", "directory", "stackName", "templatePath"],
+ "localstack-deployer": [
+ "action",
+ "projectType",
+ "directory",
+ "stackName",
+ "templatePath",
+ "s3Bucket",
+ "resolveS3",
+ "saveParams",
+ ],
"localstack-docs": ["query", "limit"],
"localstack-extensions": ["action", "name", "source"],
"localstack-iam-policy-analyzer": ["action", "mode"],
diff --git a/src/lib/deployment/deployment-utils.test.ts b/src/lib/deployment/deployment-utils.test.ts
index dacbcd7..d1da989 100644
--- a/src/lib/deployment/deployment-utils.test.ts
+++ b/src/lib/deployment/deployment-utils.test.ts
@@ -1,4 +1,12 @@
-import { parseCdkOutputs, parseTerraformOutputs, validateVariables } from "./deployment-utils";
+import fs from "fs";
+import os from "os";
+import path from "path";
+import {
+ inferProjectType,
+ parseCdkOutputs,
+ parseTerraformOutputs,
+ validateVariables,
+} from "./deployment-utils";
describe("deployment-utils", () => {
describe("validateVariables", () => {
@@ -35,4 +43,25 @@ describe("deployment-utils", () => {
expect(result).toContain("No outputs defined");
});
});
+
+ describe("inferProjectType", () => {
+ it("detects SAM projects via samconfig.toml", async () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ls-mcp-samcfg-"));
+ fs.writeFileSync(path.join(dir, "samconfig.toml"), "version = 0.1");
+
+ await expect(inferProjectType(dir)).resolves.toBe("sam");
+ fs.rmSync(dir, { recursive: true, force: true });
+ });
+
+ it("detects SAM projects via template with AWS::Serverless resources", async () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ls-mcp-samtpl-"));
+ fs.writeFileSync(
+ path.join(dir, "template.yaml"),
+ "Resources:\n MyFunction:\n Type: AWS::Serverless::Function\n"
+ );
+
+ await expect(inferProjectType(dir)).resolves.toBe("sam");
+ fs.rmSync(dir, { recursive: true, force: true });
+ });
+ });
});
diff --git a/src/lib/deployment/deployment-utils.ts b/src/lib/deployment/deployment-utils.ts
index c91a516..7e7b0df 100644
--- a/src/lib/deployment/deployment-utils.ts
+++ b/src/lib/deployment/deployment-utils.ts
@@ -9,7 +9,13 @@ export interface DependencyCheckResult {
errorMessage?: string;
}
-export type ProjectType = "cdk" | "terraform" | "cloudformation" | "ambiguous" | "unknown";
+export type ProjectType =
+ | "cdk"
+ | "terraform"
+ | "sam"
+ | "cloudformation"
+ | "ambiguous"
+ | "unknown";
/**
* Check if the required deployment tool (cdklocal or tflocal) is available in the system PATH
@@ -17,9 +23,10 @@ export type ProjectType = "cdk" | "terraform" | "cloudformation" | "ambiguous" |
* @returns Promise with availability status and tool information
*/
export async function checkDependencies(
- projectType: "cdk" | "terraform"
+ projectType: "cdk" | "terraform" | "sam"
): Promise {
- const tool = projectType === "cdk" ? "cdklocal" : "tflocal";
+ const tool =
+ projectType === "cdk" ? "cdklocal" : projectType === "terraform" ? "tflocal" : "samlocal";
try {
const { stdout, error } = await runCommand(tool, ["--version"], { timeout: 10000 });
@@ -42,7 +49,8 @@ Installation:
npm install -g aws-cdk-local aws-cdk
After installation, make sure the 'cdklocal' command is available in your PATH.`
- : `❌ tflocal is not installed or not available in PATH.
+ : projectType === "terraform"
+ ? `❌ tflocal is not installed or not available in PATH.
Please install terraform-local by following the official documentation:
https://github.com/localstack/terraform-local
@@ -50,7 +58,16 @@ https://github.com/localstack/terraform-local
Installation:
pip install terraform-local
-After installation, make sure the 'tflocal' command is available in your PATH.`;
+After installation, make sure the 'tflocal' command is available in your PATH.`
+ : `❌ samlocal is not installed or not available in PATH.
+
+Please install aws-sam-cli-local by following the official documentation:
+https://github.com/localstack/aws-sam-cli-local
+
+Installation:
+pip install aws-sam-cli-local
+
+After installation, make sure the 'samlocal' command is available in your PATH.`;
return {
isAvailable: false,
@@ -84,22 +101,37 @@ export async function inferProjectType(directory: string): Promise
(file) => file.endsWith(".tf") || file.endsWith(".tf.json")
);
- const hasCloudFormationTemplates = files.some(
- (file) => file.endsWith(".yaml") || file.endsWith(".yml")
- );
+ const hasTemplateYaml = files.includes("template.yaml") || files.includes("template.yml");
+ const hasSamConfig = files.includes("samconfig.toml");
+
+ let hasServerlessResources = false;
+ if (hasTemplateYaml) {
+ const samTemplateFile = files.includes("template.yaml") ? "template.yaml" : "template.yml";
+ try {
+ const templateContent = await fs.promises.readFile(path.join(directory, samTemplateFile), "utf-8");
+ hasServerlessResources = /AWS::Serverless::[A-Za-z]+/.test(templateContent);
+ } catch {
+ hasServerlessResources = false;
+ }
+ }
+
+ const hasCloudFormationTemplates = files.some((file) => file.endsWith(".yaml") || file.endsWith(".yml"));
const isCdk = hasCdkJson || hasCdkFiles;
const isTerraform = hasTerraformFiles;
- const isCloudFormation = hasCloudFormationTemplates;
+ const isSam = hasSamConfig || hasServerlessResources;
+ const isCloudFormation = hasCloudFormationTemplates && !isSam;
if (
- [isCdk, isTerraform, isCloudFormation].filter(Boolean).length > 1
+ [isCdk, isTerraform, isSam, isCloudFormation].filter(Boolean).length > 1
) {
return "ambiguous";
} else if (isCdk) {
return "cdk";
} else if (isTerraform) {
return "terraform";
+ } else if (isSam) {
+ return "sam";
} else if (isCloudFormation) {
return "cloudformation";
} else {
diff --git a/src/tools/localstack-deployer.ts b/src/tools/localstack-deployer.ts
index a38db5e..f287fa9 100644
--- a/src/tools/localstack-deployer.ts
+++ b/src/tools/localstack-deployer.ts
@@ -27,10 +27,10 @@ export const schema = {
"The action to perform: 'deploy'/'destroy' for CDK/Terraform, or 'create-stack'/'delete-stack' for CloudFormation."
),
projectType: z
- .enum(["cdk", "terraform", "auto"])
+ .enum(["cdk", "terraform", "sam", "auto"])
.default("auto")
.describe(
- "The type of project. 'auto' (default) infers from files. Specify 'cdk' or 'terraform' to override."
+ "The type of project. 'auto' (default) infers from files. Specify 'cdk', 'terraform', 'cloudformation', or 'sam' to override."
),
directory: z
.string()
@@ -47,17 +47,35 @@ export const schema = {
stackName: z
.string()
.optional()
- .describe("The name of the CloudFormation stack. Required for 'create-stack' and 'delete-stack'."),
+ .describe(
+ "The stack name used by CloudFormation and SAM. Required for 'create-stack'/'delete-stack' actions, and optional for SAM deploy/destroy (defaults can be inferred)."
+ ),
templatePath: z
.string()
.optional()
- .describe("The local file path to the CloudFormation template. Required for 'create-stack' if not discoverable from 'directory'."),
+ .describe(
+ "The local template file path used by CloudFormation and SAM. Required for 'create-stack' if not discoverable from 'directory', and optional for SAM as a template override."
+ ),
+ s3Bucket: z
+ .string()
+ .optional()
+ .describe("S3 bucket name used by SAM deployments. If omitted, SAM can use --resolve-s3."),
+ resolveS3: z
+ .boolean()
+ .optional()
+ .describe("For SAM deployments, whether to use --resolve-s3 when no s3Bucket is provided."),
+ saveParams: z
+ .boolean()
+ .optional()
+ .describe(
+ "For SAM deployments, whether to persist resolved parameters to samconfig.toml using --save-params."
+ ),
};
// Define tool metadata
export const metadata: ToolMetadata = {
name: "localstack-deployer",
- description: "Deploys or destroys AWS infrastructure on LocalStack using CDK or Terraform.",
+ description: "Deploys or destroys AWS infrastructure on LocalStack using CDK, Terraform, or SAM.",
annotations: {
title: "LocalStack Deployer",
readOnlyHint: false,
@@ -74,12 +92,17 @@ export default async function localstackDeployer({
variables,
stackName,
templatePath,
+ s3Bucket,
+ resolveS3,
+ saveParams,
}: InferSchema) {
return withToolAnalytics(
"localstack-deployer",
- { action, projectType, directory, stackName, templatePath, variables },
+ { action, projectType, directory, stackName, templatePath, variables, s3Bucket, resolveS3, saveParams },
async () => {
if (action === "deploy" || action === "destroy") {
+ const preflightError = await runPreflights([requireLocalStackRunning()]);
+ if (preflightError) return preflightError;
const cliError = await ensureLocalStackCli();
if (cliError) return cliError;
} else {
@@ -230,7 +253,7 @@ export default async function localstackDeployer({
}
}
- let resolvedProjectType: "cdk" | "terraform";
+ let resolvedProjectType: "cdk" | "terraform" | "sam";
try {
if (!directory) {
@@ -248,10 +271,11 @@ export default async function localstackDeployer({
if (inferredType === "ambiguous") {
return ResponseBuilder.error(
"Ambiguous Project Type",
- `The directory "${directory}" contains both CDK and Terraform files. Please specify the project type explicitly:
+ `The directory "${directory}" contains multiple infrastructure project types. Please specify the project type explicitly:
- Use \`projectType: 'cdk'\` to deploy as a CDK project
-- Use \`projectType: 'terraform'\` to deploy as a Terraform project`
+- Use \`projectType: 'terraform'\` to deploy as a Terraform project
+- Use \`projectType: 'sam'\` to deploy as a SAM project`
);
}
@@ -263,14 +287,15 @@ export default async function localstackDeployer({
Expected files:
- **CDK**: \`cdk.json\`, \`app.py\`, \`app.js\`, or \`app.ts\`
- **Terraform**: \`*.tf\` or \`*.tf.json\` files
+- **SAM**: \`samconfig.toml\` or \`template.yaml/.yml\` with \`AWS::Serverless::*\` resources
Please check the directory path or specify the project type explicitly.`
);
}
- resolvedProjectType = inferredType as "cdk" | "terraform";
+ resolvedProjectType = inferredType as "cdk" | "terraform" | "sam";
} else {
- resolvedProjectType = projectType as "cdk" | "terraform";
+ resolvedProjectType = projectType as "cdk" | "terraform" | "sam";
}
// Check Dependencies
@@ -299,7 +324,8 @@ Please review your variables and ensure they don't contain shell metacharacters
resolvedProjectType,
action,
nonNullDirectory,
- variables
+ variables,
+ { stackName, templatePath, s3Bucket, resolveS3, saveParams }
);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
@@ -318,17 +344,26 @@ Please check the directory path and ensure all prerequisites are met.`
* Execute the deployment commands based on project type and action
*/
async function executeDeploymentCommands(
- projectType: "cdk" | "terraform",
+ projectType: "cdk" | "terraform" | "sam",
action: "deploy" | "destroy",
directory: string,
- variables?: Record
+ variables?: Record,
+ samOptions?: {
+ stackName?: string;
+ templatePath?: string;
+ s3Bucket?: string;
+ resolveS3?: boolean;
+ saveParams?: boolean;
+ }
) {
const absoluteDirectory = path.resolve(directory);
const baseTitle = `🚀 LocalStack ${projectType.toUpperCase()} ${action === "deploy" ? "Deployment" : "Destruction"}`;
const events =
projectType === "terraform"
? await executeTerraformCommands(action, absoluteDirectory, variables)
- : await executeCdkCommands(action, absoluteDirectory, variables);
+ : projectType === "sam"
+ ? await executeSamCommands(action, absoluteDirectory, variables, samOptions)
+ : await executeCdkCommands(action, absoluteDirectory, variables);
const report = formatDeploymentReport(baseTitle, events);
return ResponseBuilder.markdown(report);
}
@@ -404,6 +439,148 @@ async function executeTerraformCommands(
return events;
}
+interface SamConfigDefaults {
+ stackName?: string;
+ s3Bucket?: string;
+ resolveS3?: boolean;
+ saveParams?: boolean;
+ templatePath?: string;
+}
+
+async function loadSamConfigDefaults(directory: string): Promise {
+ const configPath = path.join(directory, "samconfig.toml");
+ try {
+ const content = await fs.promises.readFile(configPath, "utf-8");
+
+ const readString = (key: string): string | undefined => {
+ const match = content.match(new RegExp(`${key}\\s*=\\s*"([^"]+)"`));
+ return match?.[1];
+ };
+ const readBoolean = (key: string): boolean | undefined => {
+ const match = content.match(new RegExp(`${key}\\s*=\\s*(true|false)`));
+ return match ? match[1] === "true" : undefined;
+ };
+
+ return {
+ stackName: readString("stack_name"),
+ s3Bucket: readString("s3_bucket"),
+ resolveS3: readBoolean("resolve_s3"),
+ saveParams: readBoolean("save_params"),
+ templatePath: readString("template_file"),
+ };
+ } catch {
+ return {};
+ }
+}
+
+async function executeSamCommands(
+ action: "deploy" | "destroy",
+ directory: string,
+ variables?: Record,
+ samOptions?: {
+ stackName?: string;
+ templatePath?: string;
+ s3Bucket?: string;
+ resolveS3?: boolean;
+ saveParams?: boolean;
+ }
+): Promise {
+ const events: DeploymentEvent[] = [];
+ const baseCommand = "samlocal";
+ const defaults = await loadSamConfigDefaults(directory);
+ const inferredStackName = `${path.basename(directory).replace(/[^a-zA-Z0-9-]/g, "-")}-stack`;
+
+ const resolvedStackName = samOptions?.stackName || defaults.stackName || inferredStackName;
+ const resolvedRegion = "us-east-1"; // Not configurable at the moment
+ const resolvedTemplatePath = samOptions?.templatePath || defaults.templatePath;
+ const resolvedS3Bucket = samOptions?.s3Bucket || defaults.s3Bucket;
+ const shouldResolveS3 = samOptions?.resolveS3 ?? defaults.resolveS3 ?? !resolvedS3Bucket;
+ const shouldSaveParams = samOptions?.saveParams ?? defaults.saveParams ?? false;
+
+ if (action === "deploy") {
+ events.push({ type: "header", title: "🏗️ Building SAM Application", content: "" });
+ const buildArgs = ["build", "--cached"];
+ if (resolvedTemplatePath) {
+ buildArgs.push("--template-file", resolvedTemplatePath);
+ }
+ events.push({ type: "command", content: `${baseCommand} ${buildArgs.join(" ")}` });
+ const buildRes = await runCommand(baseCommand, buildArgs, { cwd: directory });
+ events.push({ type: "output", content: stripAnsiCodes(buildRes.stdout) });
+ if (buildRes.stderr) events.push({ type: "warning", content: stripAnsiCodes(buildRes.stderr) });
+ if (buildRes.error) {
+ events.push({
+ type: "error",
+ title: "Error during `samlocal build`",
+ content: buildRes.error.message,
+ });
+ return events;
+ }
+
+ events.push({ type: "header", title: "🚀 Deploying SAM Application", content: "" });
+ const deployArgs = [
+ "deploy",
+ "--no-confirm-changeset",
+ "--no-fail-on-empty-changeset",
+ "--region",
+ resolvedRegion,
+ "--stack-name",
+ resolvedStackName,
+ "--capabilities",
+ "CAPABILITY_IAM",
+ "CAPABILITY_NAMED_IAM",
+ ];
+ if (resolvedTemplatePath) {
+ deployArgs.push("--template-file", resolvedTemplatePath);
+ }
+ if (resolvedS3Bucket) {
+ deployArgs.push("--s3-bucket", resolvedS3Bucket);
+ } else if (shouldResolveS3) {
+ deployArgs.push("--resolve-s3");
+ }
+ if (shouldSaveParams) {
+ deployArgs.push("--save-params");
+ }
+ if (variables && Object.keys(variables).length > 0) {
+ deployArgs.push("--parameter-overrides", ...Object.entries(variables).map(([k, v]) => `${k}=${v}`));
+ }
+
+ events.push({ type: "command", content: `${baseCommand} ${deployArgs.join(" ")}` });
+ const deployRes = await runCommand(baseCommand, deployArgs, {
+ cwd: directory,
+ env: { ...process.env, CI: "true" },
+ });
+ events.push({ type: "output", content: stripAnsiCodes(deployRes.stdout) });
+ if (deployRes.stderr) events.push({ type: "warning", content: stripAnsiCodes(deployRes.stderr) });
+ if (deployRes.error) {
+ events.push({
+ type: "error",
+ title: "Error during `samlocal deploy`",
+ content: deployRes.error.message,
+ });
+ return events;
+ }
+ events.push({ type: "success", content: "SAM application deployed successfully!" });
+ } else {
+ events.push({ type: "header", title: "💥 Deleting SAM Application", content: "" });
+ const deleteArgs = ["delete", "--no-prompts", "--region", resolvedRegion, "--stack-name", resolvedStackName];
+ events.push({ type: "command", content: `${baseCommand} ${deleteArgs.join(" ")}` });
+ const deleteRes = await runCommand(baseCommand, deleteArgs, { cwd: directory });
+ events.push({ type: "output", content: stripAnsiCodes(deleteRes.stdout) });
+ if (deleteRes.stderr) events.push({ type: "warning", content: stripAnsiCodes(deleteRes.stderr) });
+ if (deleteRes.error) {
+ events.push({
+ type: "error",
+ title: "Error during `samlocal delete`",
+ content: deleteRes.error.message,
+ });
+ return events;
+ }
+ events.push({ type: "success", content: `SAM application in ${directory} has been deleted.` });
+ }
+
+ return events;
+}
+
/**
* Execute CDK commands
*/