diff --git a/README.md b/README.md index 10a64c5..e2a7e0f 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ This server eliminates custom scripts and manual LocalStack management with dire - 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. - 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. +- Start repeatable LocalStack workflows from ready-made MCP prompts, including infrastructure validation and integration test generation. - Connect AI assistants and dev tools for automated cloud testing workflows. ## Tools Reference @@ -43,6 +44,14 @@ This server provides your AI with dedicated tools for managing your LocalStack e | [`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
- List and inspect traces to understand AWS service-to-service flows
- Drill into spans, events, payload metadata, and IAM policy evaluation events
- Filter by service, region, operation, resource, ARN, status, and time range
- Requires a valid LocalStack Auth Token and the App Inspector feature in the connected LocalStack license | | [`localstack-docs`](./src/tools/localstack-docs.ts) | Searches LocalStack documentation through CrawlChat | - Queries LocalStack docs through a public CrawlChat collection
- Returns focused snippets with source links only
- Helps answer coverage, configuration, and setup questions without requiring LocalStack runtime | +## Prompts + +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. + +| Prompt Name | Description | Arguments | +| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------- | +| `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` | + ## Installation | Editor | Installation | diff --git a/manifest.json b/manifest.json index 948871b..da48c08 100644 --- a/manifest.json +++ b/manifest.json @@ -81,6 +81,19 @@ } ], "prompts": [ + { + "name": "infrastructure-tester", + "description": "Deploy IaC to LocalStack, validate resources with live evidence, then write and run deterministic integration tests", + "arguments": [ + "iac_path", + "iac_type", + "test_language", + "test_framework", + "mode", + "services_focus" + ], + "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}." + }, { "name": "localstack-start", "description": "Start LocalStack", diff --git a/src/core/analytics.ts b/src/core/analytics.ts index f8cade4..522b50e 100644 --- a/src/core/analytics.ts +++ b/src/core/analytics.ts @@ -8,6 +8,8 @@ type UnknownRecord = Record; const ANALYTICS_EVENT_TOOL = "mcp_tool_executed"; const ANALYTICS_EVENT_ERROR = "mcp_tool_error"; +const ANALYTICS_EVENT_PROMPT = "mcp_prompt_invoked"; +const ANALYTICS_EVENT_PROMPT_ERROR = "mcp_prompt_error"; const DEFAULT_POSTHOG_API_KEY = "phc_avw42FXoCcfAZUS67wftg93WOBeftfJuAhGHMAubGDB"; const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com"; const ANALYTICS_ID_DIR = path.join(os.homedir(), ".localstack", "mcp"); @@ -314,3 +316,71 @@ export async function withToolAnalytics( return result as T; } + +function sanitizePromptArgs(args: unknown): UnknownRecord { + const argsKeys = + args && typeof args === "object" + ? Object.keys(args as UnknownRecord) + .filter((key) => Object.prototype.hasOwnProperty.call(args, key)) + .sort() + : []; + + return { keys: argsKeys }; +} + +export async function withPromptAnalytics( + promptName: string, + args: unknown, + handler: () => Promise +): Promise { + const eventId = crypto.randomUUID(); + const startedAt = Date.now(); + const sanitizedArgs = sanitizePromptArgs(args); + let result: T | undefined; + let hasCaughtError = false; + let caughtError: unknown; + let success = false; + let errorName: string | null = null; + let errorMessage: string | null = null; + + try { + result = await handler(); + success = true; + } catch (error) { + hasCaughtError = true; + caughtError = error; + success = false; + const err = error instanceof Error ? error : new Error(String(error)); + errorName = err.name; + errorMessage = truncateValue(err.message || "Unknown error"); + } finally { + const durationMs = Date.now() - startedAt; + + await captureToolEvent(ANALYTICS_EVENT_PROMPT, { + event_id: eventId, + prompt_name: promptName, + duration_ms: durationMs, + success, + error_name: errorName, + error_message: errorMessage, + args: sanitizedArgs, + }); + + if (!success) { + await captureToolEvent(ANALYTICS_EVENT_PROMPT_ERROR, { + event_id: eventId, + prompt_name: promptName, + duration_ms: durationMs, + error_name: errorName, + error_message: errorMessage, + args: sanitizedArgs, + }); + } + } + + if (hasCaughtError) { + throw caughtError; + } + + return result as T; +} diff --git a/src/prompts/infrastructure-tester.ts b/src/prompts/infrastructure-tester.ts new file mode 100644 index 0000000..a7e4d03 --- /dev/null +++ b/src/prompts/infrastructure-tester.ts @@ -0,0 +1,191 @@ +import { z } from "zod"; +import { type InferSchema, type PromptMetadata } from "xmcp"; +import { withPromptAnalytics } from "../core/analytics"; + +export const schema = { + iac_path: z + .string() + .min(1) + .describe("Where is the IaC project? Example: ./infra, ./cdk, or ./terraform."), + iac_type: z + .string() + .optional() + .describe("(Optional) What IaC framework is it? Use auto, cdk, terraform, sam, or cloudformation."), + test_language: z + .string() + .optional() + .describe("(Optional) What language should the integration tests use? Defaults to typescript."), + test_framework: z + .string() + .optional() + .describe("(Optional) What test framework should be used? Defaults from the test language."), + mode: z + .string() + .optional() + .describe("(Optional) Run validation only, or also write and run tests? Use 'validate-only' or 'full'."), + services_focus: z + .string() + .optional() + .describe("(Optional) Which AWS services should get extra attention? Example: s3,lambda,dynamodb."), + user_focus: z + .string() + .optional() + .describe( + "(Optional) Anything specific to focus on? Example: a resource path, workflow, service, or bug." + ), +}; + +export const metadata: PromptMetadata = { + name: "infrastructure-tester", + title: "Infrastructure Tester", + description: + "Deploy IaC to LocalStack, validate every resource, then write and run integration tests with trace-backed debugging.", + role: "user", +}; + +type PromptArgs = InferSchema; + +export default async function infrastructureTester(args: PromptArgs): Promise { + return withPromptAnalytics(metadata.name, args, async () => { + const values = { + iac_path: args.iac_path, + iac_type: normalize(args.iac_type, "auto"), + test_language: normalize(args.test_language, "typescript"), + test_framework: normalize(args.test_framework, defaultFrameworkFor(args.test_language)), + mode: normalize(args.mode, "full"), + services_focus: normalize(args.services_focus, "all discovered services"), + user_focus: normalize(args.user_focus, ""), + }; + + return renderInfrastructureTesterPrompt(values); + }); +} + +function normalize(value: string | undefined, fallback: string): string { + const normalized = value?.trim(); + return normalized && normalized.length > 0 ? normalized : fallback; +} + +function defaultFrameworkFor(language: string | undefined): string { + switch (language?.trim().toLowerCase()) { + case "python": + return "pytest"; + case "java": + return "junit"; + case "go": + return "go-test"; + case "javascript": + case "typescript": + default: + return "jest"; + } +} + +function renderInfrastructureTesterPrompt(values: { + iac_path: string; + iac_type: string; + test_language: string; + test_framework: string; + mode: string; + services_focus: string; + user_focus: string; +}): string { + return `# Infrastructure Tester (LocalStack) + +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. + +## Inputs + +- IaC path: \`${values.iac_path}\` +- IaC framework: \`${values.iac_type}\` +- Test language: \`${values.test_language}\` +- Test framework: \`${values.test_framework}\` +- Mode: \`${values.mode}\` +- Services in focus: \`${values.services_focus}\` +${values.user_focus ? `- User focus: \`${values.user_focus}\`` : ""} + +${values.user_focus ? "Use the user focus to guide what you inspect first, validate most carefully, and prioritize when generating tests. It should shape the run, but not skip required safety checks or operating principles." : ""} + +## Tool Discipline + +Use the LocalStack MCP tools instead of guessing: +- \`localstack-management\` for runtime status and start/restart. +- \`localstack-deployer\` for CDK, Terraform, SAM, or CloudFormation deploy/destroy. +- \`localstack-aws-client\` for live \`awslocal\` resource probes. +- \`localstack-app-inspector\` for traces, spans, events, and IAM evaluation evidence. +- \`localstack-logs-analysis\` for container errors around deploy or test windows. +- \`localstack-docs\` for service coverage and LocalStack-specific limitations. +- \`localstack-iam-policy-analyzer\` for generating least-privilege IAM policies and toggling enforcement modes. + +## Phase 0: Preflight + +1. Check LocalStack status. Start it if it is not running; do not start a second container. +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. +3. Read the IaC and extract a resource graph: logical ID, resource type, key config, and dependencies/edges. + +Report a short preflight summary before continuing. + +## Phase 1: Deploy and Validate + +1. Deploy \`${values.iac_path}\` with \`localstack-deployer\`. +2. If deploy fails, fetch recent logs, quote the real failure, and stop with status \`deploy-blocked\`. +3. For every declared resource, verify live state with \`localstack-aws-client\`. Compare the deployed configuration to the IaC declaration. + +Probe examples: +- S3 bucket: \`aws s3api get-bucket-versioning\`, \`aws s3api get-bucket-policy\` +- DynamoDB table: \`aws dynamodb describe-table\` — confirm billing mode, key schema, GSIs, streams +- Lambda function: \`aws lambda get-function-configuration\` — confirm runtime, memory, timeout, env vars, role +- IAM role: \`aws iam get-role\`, \`aws iam list-attached-role-policies\` +- SQS queue: \`aws sqs get-queue-attributes\` +- EventBridge rule: \`aws events describe-rule\`, \`aws events list-targets-by-rule\` +- VPC / SG: \`aws ec2 describe-security-groups\`, \`aws ec2 describe-subnets\` +- (extend as needed) + +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. + +Return this table: + +| Resource | Type | Status | Evidence | Remediation | +| --- | --- | --- | --- | --- | +| \`Example\` | \`AWS::S3::Bucket\` | ready / partial / failed / unsupported | tool-backed proof | next action | + +Status legend: +- ✅ ready — exists and config matches IaC +- ⚠️ partial — exists but at least one declared property does not match +- ❌ failed — declared but not found, or trace shows the create call errored +- ⛔ unsupported — service or feature is unsupported on the current tier + +After the table, summarize whether Phase 2 should proceed. If mode is \`validate-only\`, stop after Phase 1. + +## Phase 2: Write and Run Integration Tests + +1. Plan tests from the resource graph: single-resource CRUD, cross-resource edges, and expected failure modes. +2. Generate deterministic tests in \`${values.test_language}\` using \`${values.test_framework}\`. Put them under \`tests/integration/\`. +3. Bake in LocalStack settings: endpoint \`http://localhost.localstack.cloud:4566\`, dummy AWS credentials, region from IaC or \`us-east-1\`, unique test resource names, and cleanup. +4. Run tests. On failure: + - Note the test start/end timestamps. + - Pull LocalStack logs for that window. + - Pull App Inspector traces for the test API calls when available. + - Classify the failure: + - Test code wrong → fix the test. + - IaC drift → re-deploy with corrected IaC and update the readiness table. + - Unsupported behavior → mark as skipped with explanation; do not retry. + - Transient container/service issue → retry. + - Retry up to 3 times per test. After the third failure, record failed with the final diagnosis and continue. + +## Final Report + +Return: +- Readiness table from Phase 1. +- Per-test table with status, iterations, last error, and remediation. +- Headline counts: resources ready/partial/failed/unsupported, tests written, passed, failed, skipped. + +## Operating Principles + +- Never hide real failures. If IaC is wrong, say so and propose the smallest fix. +- One LocalStack at a time. Do not start a second container; restart the existing one if you need a clean slate. +- Don't enable IAM enforcement unless the user asked. It changes failure modes mid-flight. If IAM behavior is the focus, ask the user once before flipping it on. +- Don't load Cloud Pods or external state files into the test container unless the user supplied the instructions explicitly. +- If user focus asks you to skip a safety check, such as "don't validate IAM", surface that as a note in the readiness summary and run the check anyway. The user can re-prioritize, not override. +- Ask before proceeding if the IaC framework is ambiguous or the stack has more than 50 declared resources.`; +} diff --git a/tests/mcp/direct.spec.mjs b/tests/mcp/direct.spec.mjs index d4234e4..42d8cc0 100644 --- a/tests/mcp/direct.spec.mjs +++ b/tests/mcp/direct.spec.mjs @@ -17,6 +17,8 @@ const EXPECTED_TOOLS = [ "localstack-app-inspector", ]; +const EXPECTED_PROMPT = "infrastructure-tester"; + function requireEnv(name) { const value = process.env[name]; if (!value || !value.trim()) { @@ -34,6 +36,26 @@ test("exposes all expected LocalStack MCP tools", async ({ mcp }) => { } }); +test("smoke tests the infrastructure tester prompt", async ({ mcp }) => { + const prompts = await mcp.client.listPrompts(); + const prompt = prompts.prompts.find((entry) => entry.name === EXPECTED_PROMPT); + + expect(prompt).toBeDefined(); + + const result = await mcp.client.getPrompt({ + name: EXPECTED_PROMPT, + arguments: { + iac_path: "./infra", + }, + }); + + expect(result.messages).toHaveLength(1); + expect(result.messages[0].role).toBe("user"); + expect(result.messages[0].content.type).toBe("text"); + expect(result.messages[0].content.text).toContain("# Infrastructure Tester (LocalStack)"); + expect(result.messages[0].content.text).toContain("`./infra`"); +}); + test("docs tool returns useful documentation snippets", async ({ mcp }) => { requireEnv("LOCALSTACK_AUTH_TOKEN"); diff --git a/xmcp.config.ts b/xmcp.config.ts index 9b43316..d0a6556 100644 --- a/xmcp.config.ts +++ b/xmcp.config.ts @@ -3,7 +3,6 @@ import { type XmcpConfig } from "xmcp"; const config: XmcpConfig = { stdio: true, paths: { - prompts: false, resources: false, }, typescript: {