Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This server eliminates custom scripts and manual LocalStack management with dire
- 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.
- Replicate external AWS resources into LocalStack with [AWS Replicator](https://docs.localstack.cloud/aws/tooling/aws-replicator/) so IaC stacks can resolve shared dependencies locally.
- Connect AI assistants and dev tools for automated cloud testing workflows.

## Tools Reference
Expand All @@ -35,6 +36,7 @@ This server provides your AI with dedicated tools for managing your LocalStack e
| [`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 |
| [`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<br/>- List available ephemeral instances, fetch logs, and delete instances<br/>- Supports lifetime, extension preload, Cloud Pod preload, and custom env vars on create<br/>- 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<br/>- Sanitizes commands to block shell chaining<br/>- Auto-detects LocalStack coverage errors and links to docs |
| [`localstack-aws-replicator`](./src/tools/localstack-aws-replicator.ts) | Replicates external AWS resources into a running LocalStack instance | - Start single-resource replication jobs with a resource type and identifier or ARN<br/>- Start batch replication jobs, such as SSM parameters under a path prefix<br/>- Poll job status by job ID and list existing jobs<br/>- List resource types supported by the running Replicator extension<br/>- Reads source AWS credentials from the MCP server environment and supports optional target account or region overrides |
| [`localstack-docs`](./src/tools/localstack-docs.ts) | Searches LocalStack documentation through CrawlChat | - Queries LocalStack docs through a public CrawlChat collection<br/>- Returns focused snippets with source links only<br/>- Helps answer coverage, configuration, and setup questions without requiring LocalStack runtime |

## Installation
Expand Down Expand Up @@ -94,6 +96,9 @@ If you installed from source, change `command` and `args` to point to your local
| `LOCALSTACK_AUTH_TOKEN` (**required**) | The LocalStack Auth Token to use for the MCP server | None |
| `MAIN_CONTAINER_NAME` | The name of the LocalStack container to use for the MCP server | `localstack-main` |
| `MCP_ANALYTICS_DISABLED` | Disable MCP analytics when set to `1` | `0` |
| `AWS_ACCESS_KEY_ID` (**required for AWS Replicator tool**) | Source AWS access key used by AWS Replicator to read external AWS resources | None |
Comment thread
HarshCasper marked this conversation as resolved.
| `AWS_SECRET_ACCESS_KEY` (**required for AWS Replicator tool**) | Source AWS secret access key used by AWS Replicator to read external AWS resources | None |
| `AWS_DEFAULT_REGION` (**required for AWS Replicator tool**) | Source AWS region used by AWS Replicator | None |

## Contributing

Expand All @@ -119,7 +124,7 @@ This repository includes [MCP Server Tester](https://github.com/gleanwork/mcp-se
export GOOGLE_GENERATIVE_AI_API_KEY="<your-gemini-key>"
export LOCALSTACK_AUTH_TOKEN="<your-localstack-auth-token>"
yarn test:mcp:evals
```
```
- Open the latest MCP Server Tester HTML report:
```bash
npx mcp-server-tester open
Expand Down
20 changes: 20 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@
"name": "localstack-ephemeral-instances",
"description": "Manage cloud-hosted LocalStack Ephemeral Instances by creating, listing, viewing logs, and deleting instances"
},
{
"name": "localstack-aws-client",
"description": "Runs AWS CLI commands inside the running LocalStack container"
},
{
"name": "localstack-aws-replicator",
"description": "Replicates external AWS resources into a running LocalStack instance using the AWS Replicator HTTP API"
},
{
"name": "localstack-docs",
"description": "Search the LocalStack documentation to find guides, API references, and configuration details"
Expand Down Expand Up @@ -127,6 +135,18 @@
"arguments": ["description"],
"text": "Please query the LocalStack container for ${arguments.description}."
},
{
"name": "aws-replicator-start",
"description": "Start an AWS Replicator job",
"arguments": ["resource_type", "resource_identifier"],
"text": "Start an AWS Replicator job for ${arguments.resource_type} ${arguments.resource_identifier} into LocalStack using the AWS credentials configured in the MCP server environment."
},
Comment thread
HarshCasper marked this conversation as resolved.
{
"name": "aws-replicator-status",
"description": "Check AWS Replicator job status",
"arguments": ["job_id"],
"text": "Check the AWS Replicator job status for ${arguments.job_id}."
},
{
"name": "cloud-pods",
"description": "Manage LocalStack state snapshots (Cloud Pods) for saving, loading, deleting, and resetting",
Expand Down
28 changes: 28 additions & 0 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,34 @@
"format": "string",
"is_secret": true,
"name": "LOCALSTACK_AUTH_TOKEN"
},
{
"description": "Source AWS access key used by AWS Replicator to read external AWS resources",
"is_required": false,
"format": "string",
"is_secret": true,
"name": "AWS_ACCESS_KEY_ID"
},
{
"description": "Source AWS secret access key used by AWS Replicator to read external AWS resources",
"is_required": false,
"format": "string",
"is_secret": true,
"name": "AWS_SECRET_ACCESS_KEY"
},
{
"description": "Optional source AWS session token used by AWS Replicator for temporary credentials",
"is_required": false,
"format": "string",
"is_secret": true,
"name": "AWS_SESSION_TOKEN"
},
{
"description": "Source AWS region used by AWS Replicator",
"is_required": false,
"format": "string",
"is_secret": false,
"name": "AWS_DEFAULT_REGION"
}
]
}
Expand Down
7 changes: 7 additions & 0 deletions src/core/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ const SHUTDOWN_TIMEOUT_MS = 1000;

export const TOOL_ARG_ALLOWLIST: Record<string, string[]> = {
"localstack-aws-client": ["command"],
"localstack-aws-replicator": [
"action",
"replication_type",
"resource_type",
"has_resource_identifier",
"has_resource_arn",
],
"localstack-chaos-injector": ["action", "rules_count", "latency_ms"],
"localstack-cloud-pods": ["action", "pod_name"],
"localstack-deployer": [
Expand Down
140 changes: 140 additions & 0 deletions src/lib/localstack/aws-replicator.logic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {
buildStartReplicationJobRequest,
formatReplicationJob,
formatReplicationJobs,
formatSupportedResources,
} from "../../tools/localstack-aws-replicator";

describe("localstack-aws-replicator", () => {
const originalEnv = process.env;

beforeEach(() => {
process.env = {
...originalEnv,
AWS_ACCESS_KEY_ID: "AKIA...",
AWS_SECRET_ACCESS_KEY: "secret",
AWS_DEFAULT_REGION: "us-east-1",
};
});

afterEach(() => {
process.env = originalEnv;
});

describe("buildStartReplicationJobRequest", () => {
it("builds a single-resource request from type and identifier using env credentials", () => {
const request = buildStartReplicationJobRequest({
action: "start",
replication_type: "SINGLE_RESOURCE",
resource_type: "AWS::EC2::VPC",
resource_identifier: "vpc-123",
target_account_id: "111111111111",
target_region_name: "eu-central-1",
} as any);

expect(request).toEqual({
replication_type: "SINGLE_RESOURCE",
replication_job_config: {
resource_type: "AWS::EC2::VPC",
resource_identifier: "vpc-123",
},
source_aws_config: {
aws_access_key_id: "AKIA...",
aws_secret_access_key: "secret",
region_name: "us-east-1",
},
target_aws_config: {
aws_access_key_id: "111111111111",
aws_secret_access_key: "test",
region_name: "eu-central-1",
},
});
});

it("builds a resource ARN request without requiring resource type using env credentials", () => {
process.env.AWS_SESSION_TOKEN = "token";
process.env.AWS_ENDPOINT_URL = "https://example.com";

const request = buildStartReplicationJobRequest({
action: "start",
replication_type: "SINGLE_RESOURCE",
resource_arn: "arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret",
} as any);

expect(request).toEqual({
replication_type: "SINGLE_RESOURCE",
replication_job_config: {
resource_arn: "arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret",
},
source_aws_config: {
aws_access_key_id: "AKIA...",
aws_secret_access_key: "secret",
aws_session_token: "token",
region_name: "us-east-1",
endpoint_url: "https://example.com",
},
});
});
});

describe("formatReplicationJob", () => {
it("includes batch result details", () => {
const formatted = formatReplicationJob("AWS Replicator Job Status", {
job_id: "job-123",
state: "SUCCEEDED",
type: "BATCH",
replication_config: {
resource_type: "AWS::SSM::Parameter",
identifier: "/dev/",
},
result: {
resources_succeeded: 2,
resources_failed: 0,
},
});

expect(formatted).toContain("AWS Replicator Job Status");
expect(formatted).toContain("`job-123`");
expect(formatted).toContain("resources_succeeded");
expect(formatted).toContain("AWS::SSM::Parameter");
});
});

describe("formatReplicationJobs", () => {
it("summarizes listed jobs and includes the raw response", () => {
const formatted = formatReplicationJobs([
{
job_id: "job-123",
state: "SUCCEEDED",
type: "SINGLE_RESOURCE",
replication_config: {
resource_type: "AWS::EC2::VPC",
resource_identifier: "vpc-123",
},
},
]);

expect(formatted).toContain("AWS Replicator Jobs");
expect(formatted).toContain("job-123");
expect(formatted).toContain("SUCCEEDED");
expect(formatted).toContain("Raw Response");
});
});

describe("formatSupportedResources", () => {
it("summarizes supported resource types and identifiers", () => {
const formatted = formatSupportedResources([
{
resource_type: "AWS::SSM::Parameter",
service: "ssm",
identifier: "Name",
},
]);

expect(formatted).toContain("AWS Replicator Supported Resources");
expect(formatted).toContain("AWS::SSM::Parameter");
expect(formatted).toContain("identifier: `Name`");
expect(formatted).toContain("Raw Response");
});
});
});
1 change: 1 addition & 0 deletions src/lib/localstack/license-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export enum ProFeature {
CLOUD_PODS = "localstack.platform.plugin/pods",
CHAOS_ENGINEERING = "localstack.platform.plugin/chaos",
EXTENSIONS = "localstack.platform.plugin/extensions",
REPLICATOR = "localstack.platform.plugin/replicator",
SNOWFLAKE = "localstack.aws.provider/snowflake:pro",
}

Expand Down
88 changes: 88 additions & 0 deletions src/lib/localstack/localstack.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,47 @@ export type ApiResult<T> =
| { success: true; data: T }
| { success: false; message: string; statusCode?: number };

export interface AwsConfig {
aws_access_key_id?: string;
aws_secret_access_key?: string;
aws_session_token?: string;
region_name?: string;
endpoint_url?: string;
}

export interface ReplicationJobConfig {
resource_type?: string;
resource_identifier?: string;
resource_arn?: string;
}

export interface StartReplicationJobRequest {
replication_type: "SINGLE_RESOURCE" | "BATCH";
replication_job_config: ReplicationJobConfig;
source_aws_config: AwsConfig;
target_aws_config?: AwsConfig;
}

export interface ReplicationJobResponse {
job_id: string;
state: string;
error_message?: string | null;
type?: string;
replication_type?: string;
replication_config?: Record<string, unknown>;
replication_job_config?: Record<string, unknown>;
result?: Record<string, unknown>;
[key: string]: unknown;
}

export interface ReplicationSupportedResource {
resource_type?: string;
service?: string;
identifier?: string;
policy_statements?: unknown[];
[key: string]: unknown;
}

// Chaos API Client
export class ChaosApiClient {
private async makeRequest(
Expand Down Expand Up @@ -134,3 +175,50 @@ export class CloudPodsApiClient {
return this.makeRequest("/_localstack/state/reset", "POST", false, {});
}
}

// AWS Replicator API Client
export class AwsReplicatorApiClient {
private async makeRequest<T>(
endpoint: string,
method: "GET" | "POST",
body?: unknown
): Promise<ApiResult<T>> {
try {
const data = await httpClient.request<T>(`/_localstack/replicator${endpoint}`, {
method,
headers: { "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
timeout: 300000,
});
return { success: true, data };
} catch (error) {
if (error instanceof HttpError) {
return {
success: false,
message: `❌ **Error:** The LocalStack AWS Replicator API returned an error (Status ${error.status}):\n\`\`\`\n${error.body}\n\`\`\``,
statusCode: error.status,
};
}
return {
success: false,
message: `❌ **Error:** Failed to communicate with LocalStack AWS Replicator API: ${error instanceof Error ? error.message : "Unknown error"}`,
};
}
}

startJob(request: StartReplicationJobRequest) {
return this.makeRequest<ReplicationJobResponse>("/jobs", "POST", request);
}

listJobs() {
return this.makeRequest<ReplicationJobResponse[]>("/jobs", "GET");
}

getJobStatus(jobId: string) {
return this.makeRequest<ReplicationJobResponse>(`/jobs/${encodeURIComponent(jobId)}`, "GET");
}

listSupportedResources() {
return this.makeRequest<ReplicationSupportedResource[]>("/resources", "GET");
}
}
Loading
Loading