Skip to content

Commit 7a6ade2

Browse files
authored
New MCP Tool: AWS Replicator (#27)
* first implementation * add proper preflights * add new actions (list, list-resources) to the tool * final fixes * resolve @cloutierMat comments
1 parent 266ef54 commit 7a6ade2

9 files changed

Lines changed: 766 additions & 1 deletion

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ This server eliminates custom scripts and manual LocalStack management with dire
1515
- Manage LocalStack state snapshots via [Cloud Pods](https://docs.localstack.cloud/aws/capabilities/state-management/cloud-pods/) for development workflows. (requires active license)
1616
- Install, remove, list, and discover [LocalStack Extensions](https://docs.localstack.cloud/aws/capabilities/extensions/) from the marketplace. (requires active license)
1717
- Launch and manage [Ephemeral Instances](https://docs.localstack.cloud/aws/capabilities/cloud-sandbox/ephemeral-instances/) for remote LocalStack testing workflows.
18+
- 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.
1819
- Connect AI assistants and dev tools for automated cloud testing workflows.
1920

2021
## Tools Reference
@@ -35,6 +36,7 @@ This server provides your AI with dedicated tools for managing your LocalStack e
3536
| [`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 |
3637
| [`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 |
3738
| [`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 |
39+
| [`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 |
3840
| [`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 |
3941

4042
## Installation
@@ -94,6 +96,11 @@ If you installed from source, change `command` and `args` to point to your local
9496
| `LOCALSTACK_AUTH_TOKEN` (**required**) | The LocalStack Auth Token to use for the MCP server | None |
9597
| `MAIN_CONTAINER_NAME` | The name of the LocalStack container to use for the MCP server | `localstack-main` |
9698
| `MCP_ANALYTICS_DISABLED` | Disable MCP analytics when set to `1` | `0` |
99+
| `AWS_ACCESS_KEY_ID` (**required for AWS Replicator tool**) | Source AWS access key used by AWS Replicator to read external AWS resources | None |
100+
| `AWS_SECRET_ACCESS_KEY` (**required for AWS Replicator tool**) | Source AWS secret access key used by AWS Replicator to read external AWS resources | None |
101+
| `AWS_DEFAULT_REGION` (**required for AWS Replicator tool**) | Source AWS region used by AWS Replicator | None |
102+
103+
For AWS Replicator-specific source credentials, you can use the `AWS_REPLICATOR_SOURCE_` prefixed variants instead of the unprefixed variants. Do not mix the prefixed and unprefixed source credential groups; when any `AWS_REPLICATOR_SOURCE_` variable is set, the Replicator tool reads the source configuration only from that group.
97104

98105
## Contributing
99106

@@ -119,7 +126,7 @@ This repository includes [MCP Server Tester](https://github.com/gleanwork/mcp-se
119126
export GOOGLE_GENERATIVE_AI_API_KEY="<your-gemini-key>"
120127
export LOCALSTACK_AUTH_TOKEN="<your-localstack-auth-token>"
121128
yarn test:mcp:evals
122-
```
129+
```
123130
- Open the latest MCP Server Tester HTML report:
124131
```bash
125132
npx mcp-server-tester open

manifest.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@
5959
"name": "localstack-ephemeral-instances",
6060
"description": "Manage cloud-hosted LocalStack Ephemeral Instances by creating, listing, viewing logs, and deleting instances"
6161
},
62+
{
63+
"name": "localstack-aws-client",
64+
"description": "Runs AWS CLI commands inside the running LocalStack container"
65+
},
66+
{
67+
"name": "localstack-aws-replicator",
68+
"description": "Replicates external AWS resources into a running LocalStack instance using the AWS Replicator HTTP API"
69+
},
6270
{
6371
"name": "localstack-docs",
6472
"description": "Search the LocalStack documentation to find guides, API references, and configuration details"
@@ -127,6 +135,24 @@
127135
"arguments": ["description"],
128136
"text": "Please query the LocalStack container for ${arguments.description}."
129137
},
138+
{
139+
"name": "aws-replicator-start",
140+
"description": "Start an AWS Replicator job",
141+
"arguments": ["resource_type", "resource_identifier"],
142+
"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."
143+
},
144+
{
145+
"name": "aws-replicator-start-by-arn",
146+
"description": "Start an AWS Replicator job from a resource ARN",
147+
"arguments": ["resource_arn"],
148+
"text": "Start an AWS Replicator job for the resource ARN ${arguments.resource_arn} into LocalStack using the AWS credentials configured in the MCP server environment."
149+
},
150+
{
151+
"name": "aws-replicator-status",
152+
"description": "Check AWS Replicator job status",
153+
"arguments": ["job_id"],
154+
"text": "Check the AWS Replicator job status for ${arguments.job_id}."
155+
},
130156
{
131157
"name": "cloud-pods",
132158
"description": "Manage LocalStack state snapshots (Cloud Pods) for saving, loading, deleting, and resetting",

server.json

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,69 @@
2424
"format": "string",
2525
"is_secret": true,
2626
"name": "LOCALSTACK_AUTH_TOKEN"
27+
},
28+
{
29+
"description": "Source AWS access key used by AWS Replicator to read external AWS resources",
30+
"is_required": false,
31+
"format": "string",
32+
"is_secret": true,
33+
"name": "AWS_ACCESS_KEY_ID"
34+
},
35+
{
36+
"description": "Source AWS secret access key used by AWS Replicator to read external AWS resources",
37+
"is_required": false,
38+
"format": "string",
39+
"is_secret": true,
40+
"name": "AWS_SECRET_ACCESS_KEY"
41+
},
42+
{
43+
"description": "Optional source AWS session token used by AWS Replicator for temporary credentials",
44+
"is_required": false,
45+
"format": "string",
46+
"is_secret": true,
47+
"name": "AWS_SESSION_TOKEN"
48+
},
49+
{
50+
"description": "Source AWS region used by AWS Replicator",
51+
"is_required": false,
52+
"format": "string",
53+
"is_secret": false,
54+
"name": "AWS_DEFAULT_REGION"
55+
},
56+
{
57+
"description": "Optional Replicator-specific source AWS access key. Use this instead of AWS_ACCESS_KEY_ID when the Replicator source account should be isolated from generic AWS config.",
58+
"is_required": false,
59+
"format": "string",
60+
"is_secret": true,
61+
"name": "AWS_REPLICATOR_SOURCE_AWS_ACCESS_KEY_ID"
62+
},
63+
{
64+
"description": "Optional Replicator-specific source AWS secret access key. Use with AWS_REPLICATOR_SOURCE_AWS_ACCESS_KEY_ID.",
65+
"is_required": false,
66+
"format": "string",
67+
"is_secret": true,
68+
"name": "AWS_REPLICATOR_SOURCE_AWS_SECRET_ACCESS_KEY"
69+
},
70+
{
71+
"description": "Optional Replicator-specific source AWS session token for temporary credentials.",
72+
"is_required": false,
73+
"format": "string",
74+
"is_secret": true,
75+
"name": "AWS_REPLICATOR_SOURCE_AWS_SESSION_TOKEN"
76+
},
77+
{
78+
"description": "Optional Replicator-specific source AWS region. Use with the AWS_REPLICATOR_SOURCE_* credential group.",
79+
"is_required": false,
80+
"format": "string",
81+
"is_secret": false,
82+
"name": "AWS_REPLICATOR_SOURCE_REGION_NAME"
83+
},
84+
{
85+
"description": "Optional Replicator-specific source AWS endpoint URL for advanced source-account scenarios.",
86+
"is_required": false,
87+
"format": "string",
88+
"is_secret": false,
89+
"name": "AWS_REPLICATOR_SOURCE_ENDPOINT_URL"
2790
}
2891
]
2992
}

src/core/analytics.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ const SHUTDOWN_TIMEOUT_MS = 1000;
1717

1818
export const TOOL_ARG_ALLOWLIST: Record<string, string[]> = {
1919
"localstack-aws-client": ["command"],
20+
"localstack-aws-replicator": [
21+
"action",
22+
"replication_type",
23+
"resource_target_kind",
24+
"resource_type",
25+
"resource_arn_service",
26+
"has_resource_identifier",
27+
"has_resource_arn",
28+
],
2029
"localstack-chaos-injector": ["action", "rules_count", "latency_ms"],
2130
"localstack-cloud-pods": ["action", "pod_name"],
2231
"localstack-deployer": [
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import {
2+
buildStartReplicationJobRequest,
3+
formatReplicationJob,
4+
formatReplicationJobs,
5+
formatSupportedResources,
6+
} from "../../tools/localstack-aws-replicator";
7+
8+
describe("localstack-aws-replicator", () => {
9+
const originalEnv = process.env;
10+
11+
beforeEach(() => {
12+
process.env = {
13+
...originalEnv,
14+
AWS_ACCESS_KEY_ID: "AKIA...",
15+
AWS_SECRET_ACCESS_KEY: "secret",
16+
AWS_DEFAULT_REGION: "us-east-1",
17+
};
18+
});
19+
20+
afterEach(() => {
21+
process.env = originalEnv;
22+
});
23+
24+
describe("buildStartReplicationJobRequest", () => {
25+
it("builds a single-resource request from type and identifier using env credentials", () => {
26+
const request = buildStartReplicationJobRequest({
27+
action: "start",
28+
replication_type: "SINGLE_RESOURCE",
29+
resource_type: "AWS::EC2::VPC",
30+
resource_identifier: "vpc-123",
31+
target_account_id: "111111111111",
32+
target_region_name: "eu-central-1",
33+
} as any);
34+
35+
expect(request).toEqual({
36+
replication_type: "SINGLE_RESOURCE",
37+
replication_job_config: {
38+
resource_type: "AWS::EC2::VPC",
39+
resource_identifier: "vpc-123",
40+
},
41+
source_aws_config: {
42+
aws_access_key_id: "AKIA...",
43+
aws_secret_access_key: "secret",
44+
region_name: "us-east-1",
45+
},
46+
target_aws_config: {
47+
aws_access_key_id: "111111111111",
48+
aws_secret_access_key: "test",
49+
region_name: "eu-central-1",
50+
},
51+
});
52+
});
53+
54+
it("builds a resource ARN request without requiring resource type using env credentials", () => {
55+
process.env.AWS_SESSION_TOKEN = "token";
56+
process.env.AWS_ENDPOINT_URL = "https://example.com";
57+
58+
const request = buildStartReplicationJobRequest({
59+
action: "start",
60+
replication_type: "SINGLE_RESOURCE",
61+
resource_arn: "arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret",
62+
} as any);
63+
64+
expect(request).toEqual({
65+
replication_type: "SINGLE_RESOURCE",
66+
replication_job_config: {
67+
resource_arn: "arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret",
68+
},
69+
source_aws_config: {
70+
aws_access_key_id: "AKIA...",
71+
aws_secret_access_key: "secret",
72+
aws_session_token: "token",
73+
region_name: "us-east-1",
74+
endpoint_url: "https://example.com",
75+
},
76+
});
77+
});
78+
79+
it("does not mix prefixed source credentials with generic AWS session values", () => {
80+
process.env.AWS_REPLICATOR_SOURCE_AWS_ACCESS_KEY_ID = "replicator-key";
81+
process.env.AWS_REPLICATOR_SOURCE_AWS_SECRET_ACCESS_KEY = "replicator-secret";
82+
process.env.AWS_REPLICATOR_SOURCE_REGION_NAME = "eu-west-1";
83+
process.env.AWS_SESSION_TOKEN = "generic-token";
84+
85+
const request = buildStartReplicationJobRequest({
86+
action: "start",
87+
replication_type: "SINGLE_RESOURCE",
88+
resource_type: "AWS::SSM::Parameter",
89+
resource_identifier: "my-param",
90+
} as any);
91+
92+
expect(request.source_aws_config).toEqual({
93+
aws_access_key_id: "replicator-key",
94+
aws_secret_access_key: "replicator-secret",
95+
region_name: "eu-west-1",
96+
});
97+
});
98+
99+
it("does not allow a target endpoint URL override", () => {
100+
process.env.AWS_REPLICATOR_TARGET_ENDPOINT_URL = "https://not-localstack.example.com";
101+
102+
const request = buildStartReplicationJobRequest({
103+
action: "start",
104+
replication_type: "SINGLE_RESOURCE",
105+
resource_type: "AWS::EC2::VPC",
106+
resource_identifier: "vpc-123",
107+
target_region_name: "eu-central-1",
108+
} as any);
109+
110+
expect(request.target_aws_config).toEqual({
111+
aws_access_key_id: "test",
112+
aws_secret_access_key: "test",
113+
region_name: "eu-central-1",
114+
});
115+
});
116+
});
117+
118+
describe("formatReplicationJob", () => {
119+
it("includes batch result details", () => {
120+
const formatted = formatReplicationJob("AWS Replicator Job Status", {
121+
job_id: "job-123",
122+
state: "SUCCEEDED",
123+
type: "BATCH",
124+
replication_config: {
125+
resource_type: "AWS::SSM::Parameter",
126+
identifier: "/dev/",
127+
},
128+
result: {
129+
resources_succeeded: 2,
130+
resources_failed: 0,
131+
},
132+
});
133+
134+
expect(formatted).toContain("AWS Replicator Job Status");
135+
expect(formatted).toContain("`job-123`");
136+
expect(formatted).toContain("resources_succeeded");
137+
expect(formatted).toContain("AWS::SSM::Parameter");
138+
});
139+
});
140+
141+
describe("formatReplicationJobs", () => {
142+
it("summarizes listed jobs and includes the raw response", () => {
143+
const formatted = formatReplicationJobs([
144+
{
145+
job_id: "job-123",
146+
state: "SUCCEEDED",
147+
type: "SINGLE_RESOURCE",
148+
replication_config: {
149+
resource_type: "AWS::EC2::VPC",
150+
resource_identifier: "vpc-123",
151+
},
152+
},
153+
]);
154+
155+
expect(formatted).toContain("AWS Replicator Jobs");
156+
expect(formatted).toContain("job-123");
157+
expect(formatted).toContain("SUCCEEDED");
158+
expect(formatted).toContain("Raw Response");
159+
});
160+
});
161+
162+
describe("formatSupportedResources", () => {
163+
it("summarizes supported resource types and identifiers", () => {
164+
const formatted = formatSupportedResources([
165+
{
166+
resource_type: "AWS::SSM::Parameter",
167+
service: "ssm",
168+
identifier: "Name",
169+
},
170+
]);
171+
172+
expect(formatted).toContain("AWS Replicator Supported Resources");
173+
expect(formatted).toContain("AWS::SSM::Parameter");
174+
expect(formatted).toContain("identifier: `Name`");
175+
expect(formatted).toContain("Raw Response");
176+
});
177+
});
178+
});

src/lib/localstack/license-checker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export enum ProFeature {
88
CLOUD_PODS = "localstack.platform.plugin/pods",
99
CHAOS_ENGINEERING = "localstack.platform.plugin/chaos",
1010
EXTENSIONS = "localstack.platform.plugin/extensions",
11+
REPLICATOR = "localstack.platform.plugin/replicator",
1112
SNOWFLAKE = "localstack.aws.provider/snowflake:pro",
1213
}
1314

0 commit comments

Comments
 (0)