Skip to content

Commit cdc2163

Browse files
committed
New Prompt: infrastructure-tester
1 parent 631613f commit cdc2163

6 files changed

Lines changed: 260 additions & 1 deletion

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This server eliminates custom scripts and manual LocalStack management with dire
1818
- Launch and manage [Ephemeral Instances](https://docs.localstack.cloud/aws/capabilities/cloud-sandbox/ephemeral-instances/) for remote LocalStack testing workflows.
1919
- 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.
2020
- Inspect LocalStack application flows with [App Inspector](https://docs.localstack.cloud/aws/capabilities/web-app/app-inspector/) traces, spans, events, payload metadata, and IAM policy evaluations.
21+
- Start repeatable LocalStack workflows from ready-made MCP prompts, including infrastructure validation and integration test generation.
2122
- Connect AI assistants and dev tools for automated cloud testing workflows.
2223

2324
## Tools Reference
@@ -43,6 +44,14 @@ This server provides your AI with dedicated tools for managing your LocalStack e
4344
| [`localstack-app-inspector`](./src/tools/localstack-app-inspector.ts) | Inspects LocalStack application traces, spans, events, and IAM evaluations | - Enable or disable App Inspector for the running LocalStack instance<br/>- List and inspect traces to understand AWS service-to-service flows<br/>- Drill into spans, events, payload metadata, and IAM policy evaluation events<br/>- Filter by service, region, operation, resource, ARN, status, and time range<br/>- Requires a valid LocalStack Auth Token and the App Inspector feature in the connected LocalStack license |
4445
| [`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 |
4546

47+
## Prompts
48+
49+
Prompts are user-selected workflow templates exposed by MCP clients as slash commands or quick actions. They frame multi-step LocalStack tasks so the assistant follows the same phases, evidence requirements, and reporting format every time.
50+
51+
| Prompt Name | Description | Arguments |
52+
| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------- |
53+
| `infrastructure-tester` | Deploys an IaC project to LocalStack, validates declared resources with live AWS probes and App Inspector evidence, then writes and runs deterministic integration tests. | `iac_path` (required), `iac_type`, `test_language`, `test_framework`, `mode`, `services_focus` |
54+
4655
## Installation
4756

4857
| Editor | Installation |

manifest.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@
8181
}
8282
],
8383
"prompts": [
84+
{
85+
"name": "infrastructure-tester",
86+
"description": "Deploy IaC to LocalStack, validate resources with live evidence, then write and run deterministic integration tests",
87+
"arguments": [
88+
"iac_path",
89+
"iac_type",
90+
"test_language",
91+
"test_framework",
92+
"mode",
93+
"services_focus"
94+
],
95+
"text": "Run the Infrastructure Tester workflow for ${arguments.iac_path}. Detect or use ${arguments.iac_type}, validate deployed resources, and if mode is ${arguments.mode}, generate and run ${arguments.test_language}/${arguments.test_framework} integration tests focused on ${arguments.services_focus}."
96+
},
8497
{
8598
"name": "localstack-start",
8699
"description": "Start LocalStack",

src/core/analytics.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ type UnknownRecord = Record<string, unknown>;
88

99
const ANALYTICS_EVENT_TOOL = "mcp_tool_executed";
1010
const ANALYTICS_EVENT_ERROR = "mcp_tool_error";
11+
const ANALYTICS_EVENT_PROMPT = "mcp_prompt_invoked";
12+
const ANALYTICS_EVENT_PROMPT_ERROR = "mcp_prompt_error";
1113
const DEFAULT_POSTHOG_API_KEY = "phc_avw42FXoCcfAZUS67wftg93WOBeftfJuAhGHMAubGDB";
1214
const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";
1315
const ANALYTICS_ID_DIR = path.join(os.homedir(), ".localstack", "mcp");
@@ -314,3 +316,71 @@ export async function withToolAnalytics<T>(
314316

315317
return result as T;
316318
}
319+
320+
function sanitizePromptArgs(args: unknown): UnknownRecord {
321+
const argsKeys =
322+
args && typeof args === "object"
323+
? Object.keys(args as UnknownRecord)
324+
.filter((key) => Object.prototype.hasOwnProperty.call(args, key))
325+
.sort()
326+
: [];
327+
328+
return { keys: argsKeys };
329+
}
330+
331+
export async function withPromptAnalytics<T>(
332+
promptName: string,
333+
args: unknown,
334+
handler: () => Promise<T>
335+
): Promise<T> {
336+
const eventId = crypto.randomUUID();
337+
const startedAt = Date.now();
338+
const sanitizedArgs = sanitizePromptArgs(args);
339+
let result: T | undefined;
340+
let hasCaughtError = false;
341+
let caughtError: unknown;
342+
let success = false;
343+
let errorName: string | null = null;
344+
let errorMessage: string | null = null;
345+
346+
try {
347+
result = await handler();
348+
success = true;
349+
} catch (error) {
350+
hasCaughtError = true;
351+
caughtError = error;
352+
success = false;
353+
const err = error instanceof Error ? error : new Error(String(error));
354+
errorName = err.name;
355+
errorMessage = truncateValue(err.message || "Unknown error");
356+
} finally {
357+
const durationMs = Date.now() - startedAt;
358+
359+
await captureToolEvent(ANALYTICS_EVENT_PROMPT, {
360+
event_id: eventId,
361+
prompt_name: promptName,
362+
duration_ms: durationMs,
363+
success,
364+
error_name: errorName,
365+
error_message: errorMessage,
366+
args: sanitizedArgs,
367+
});
368+
369+
if (!success) {
370+
await captureToolEvent(ANALYTICS_EVENT_PROMPT_ERROR, {
371+
event_id: eventId,
372+
prompt_name: promptName,
373+
duration_ms: durationMs,
374+
error_name: errorName,
375+
error_message: errorMessage,
376+
args: sanitizedArgs,
377+
});
378+
}
379+
}
380+
381+
if (hasCaughtError) {
382+
throw caughtError;
383+
}
384+
385+
return result as T;
386+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { z } from "zod";
2+
import { type InferSchema, type PromptMetadata } from "xmcp";
3+
import { withPromptAnalytics } from "../core/analytics";
4+
5+
export const schema = {
6+
iac_path: z
7+
.string()
8+
.min(1)
9+
.describe("Path to the IaC root directory, e.g. ./infra, ./cdk, or ./terraform."),
10+
iac_type: z
11+
.string()
12+
.optional()
13+
.describe("IaC framework: cdk, terraform, sam, cloudformation, or auto. Defaults to auto."),
14+
test_language: z
15+
.string()
16+
.optional()
17+
.describe("Language for generated integration tests. Defaults to typescript."),
18+
test_framework: z
19+
.string()
20+
.optional()
21+
.describe("Test framework. Defaults from the test language, e.g. jest or pytest."),
22+
mode: z
23+
.string()
24+
.optional()
25+
.describe("validate-only runs deployment validation only; full also writes and runs tests."),
26+
services_focus: z
27+
.string()
28+
.optional()
29+
.describe("Comma-separated AWS services to focus on. Empty means all discovered services."),
30+
};
31+
32+
export const metadata: PromptMetadata = {
33+
name: "infrastructure-tester",
34+
title: "Infrastructure Tester",
35+
description:
36+
"Deploy IaC to LocalStack, validate every resource, then write and run integration tests with trace-backed debugging.",
37+
role: "user",
38+
};
39+
40+
type PromptArgs = InferSchema<typeof schema>;
41+
42+
export default async function infrastructureTester(args: PromptArgs): Promise<string> {
43+
return withPromptAnalytics(metadata.name, args, async () => {
44+
const values = {
45+
iac_path: args.iac_path,
46+
iac_type: normalize(args.iac_type, "auto"),
47+
test_language: normalize(args.test_language, "typescript"),
48+
test_framework: normalize(args.test_framework, defaultFrameworkFor(args.test_language)),
49+
mode: normalize(args.mode, "full"),
50+
services_focus: normalize(args.services_focus, "all discovered services"),
51+
};
52+
53+
return renderInfrastructureTesterPrompt(values);
54+
});
55+
}
56+
57+
function normalize(value: string | undefined, fallback: string): string {
58+
const normalized = value?.trim();
59+
return normalized && normalized.length > 0 ? normalized : fallback;
60+
}
61+
62+
function defaultFrameworkFor(language: string | undefined): string {
63+
switch (language?.trim().toLowerCase()) {
64+
case "python":
65+
return "pytest";
66+
case "java":
67+
return "junit";
68+
case "go":
69+
return "go-test";
70+
case "javascript":
71+
case "typescript":
72+
default:
73+
return "jest";
74+
}
75+
}
76+
77+
function renderInfrastructureTesterPrompt(values: {
78+
iac_path: string;
79+
iac_type: string;
80+
test_language: string;
81+
test_framework: string;
82+
mode: string;
83+
services_focus: string;
84+
}): string {
85+
return `# Infrastructure Tester (LocalStack)
86+
87+
You are an Infrastructure Tester operating against one running LocalStack instance. Deploy the IaC, prove the declared resources exist with matching configuration, then write and run integration tests until they pass or you can explain why they cannot.
88+
89+
## Inputs
90+
91+
- IaC path: \`${values.iac_path}\`
92+
- IaC framework: \`${values.iac_type}\`
93+
- Test language: \`${values.test_language}\`
94+
- Test framework: \`${values.test_framework}\`
95+
- Mode: \`${values.mode}\`
96+
- Services in focus: \`${values.services_focus}\`
97+
98+
## Tool Discipline
99+
100+
Use the LocalStack MCP tools instead of guessing:
101+
- \`localstack-management\` for runtime status and start/restart.
102+
- \`localstack-deployer\` for CDK, Terraform, SAM, or CloudFormation deploy/destroy.
103+
- \`localstack-aws-client\` for live \`awslocal\` resource probes.
104+
- \`localstack-app-inspector\` for traces, spans, events, and IAM evaluation evidence.
105+
- \`localstack-logs-analysis\` for container errors around deploy or test windows.
106+
- \`localstack-docs\` for service coverage and LocalStack-specific limitations.
107+
108+
## Phase 0: Preflight
109+
110+
1. Check LocalStack status. Start it if it is not running; do not start a second container.
111+
2. Detect the IaC framework if \`${values.iac_type}\` is \`auto\`: \`cdk.json\` means CDK, \`*.tf\` means Terraform, \`template.yaml\` plus SAM config means SAM, and CloudFormation templates mean CloudFormation.
112+
3. Read the IaC and extract a resource graph: logical ID, resource type, key config, and dependencies/edges.
113+
114+
Report a short preflight summary before continuing.
115+
116+
## Phase 1: Deploy and Validate
117+
118+
1. Deploy \`${values.iac_path}\` with \`localstack-deployer\`.
119+
2. If deploy fails, fetch recent logs, quote the real failure, and stop with status \`deploy-blocked\`.
120+
3. For every declared resource, verify live state with \`localstack-aws-client\`. Compare the deployed configuration to the IaC declaration.
121+
4. Use App Inspector traces for deployment API calls when available. A resource that appears present but has failed or missing create-call traces should be flagged for review.
122+
123+
Return this table:
124+
125+
| Resource | Type | Status | Evidence | Remediation |
126+
| --- | --- | --- | --- | --- |
127+
| \`Example\` | \`AWS::S3::Bucket\` | ready / partial / failed / unsupported | tool-backed proof | next action |
128+
129+
After the table, summarize whether Phase 2 should proceed. If mode is \`validate-only\`, stop after Phase 1.
130+
131+
## Phase 2: Write and Run Integration Tests
132+
133+
1. Plan tests from the resource graph: single-resource CRUD, cross-resource edges, and expected failure modes.
134+
2. Generate deterministic tests in \`${values.test_language}\` using \`${values.test_framework}\`. Put them under \`tests/integration/\`.
135+
3. Bake in LocalStack settings: endpoint \`http://localhost.localstack.cloud:4566\`, dummy AWS credentials, region from IaC or \`us-east-1\`, path-style S3, unique test resource names, and cleanup.
136+
4. Run tests. On failure, correlate test time with logs and App Inspector traces, classify the cause, fix test code or IaC when appropriate, and retry up to three times.
137+
138+
## Final Report
139+
140+
Return:
141+
- Readiness table from Phase 1.
142+
- Per-test table with status, iterations, last error, and remediation.
143+
- Headline counts: resources ready/partial/failed/unsupported, tests written, passed, failed, skipped.
144+
145+
Never hide real failures. If IaC is wrong, say so and propose the smallest fix. Ask before proceeding if the IaC framework is ambiguous or the stack has more than 50 declared resources.`;
146+
}

tests/mcp/direct.spec.mjs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const EXPECTED_TOOLS = [
1717
"localstack-app-inspector",
1818
];
1919

20+
const EXPECTED_PROMPT = "infrastructure-tester";
21+
2022
function requireEnv(name) {
2123
const value = process.env[name];
2224
if (!value || !value.trim()) {
@@ -34,6 +36,26 @@ test("exposes all expected LocalStack MCP tools", async ({ mcp }) => {
3436
}
3537
});
3638

39+
test("smoke tests the infrastructure tester prompt", async ({ mcp }) => {
40+
const prompts = await mcp.client.listPrompts();
41+
const prompt = prompts.prompts.find((entry) => entry.name === EXPECTED_PROMPT);
42+
43+
expect(prompt).toBeDefined();
44+
45+
const result = await mcp.client.getPrompt({
46+
name: EXPECTED_PROMPT,
47+
arguments: {
48+
iac_path: "./infra",
49+
},
50+
});
51+
52+
expect(result.messages).toHaveLength(1);
53+
expect(result.messages[0].role).toBe("user");
54+
expect(result.messages[0].content.type).toBe("text");
55+
expect(result.messages[0].content.text).toContain("# Infrastructure Tester (LocalStack)");
56+
expect(result.messages[0].content.text).toContain("`./infra`");
57+
});
58+
3759
test("docs tool returns useful documentation snippets", async ({ mcp }) => {
3860
requireEnv("LOCALSTACK_AUTH_TOKEN");
3961

xmcp.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { type XmcpConfig } from "xmcp";
33
const config: XmcpConfig = {
44
stdio: true,
55
paths: {
6-
prompts: false,
76
resources: false,
87
},
98
typescript: {

0 commit comments

Comments
 (0)