@downatthebottomofthemolehole/terraform-best-practices-mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/dist/index.js +79 -0
- package/dist/lib/exec.js +86 -0
- package/dist/lib/http.js +19 -0
- package/dist/lib/text.js +39 -0
- package/dist/tools/common.js +60 -0
- package/dist/tools/fetch_provider_best_practices.js +103 -0
- package/dist/tools/fetch_terraform_best_practices.js +77 -0
- package/dist/tools/fetch_terraform_registry_guidance.js +112 -0
- package/dist/tools/index.js +18 -0
- package/dist/tools/run_checkov.js +23 -0
- package/dist/tools/run_infracost.js +23 -0
- package/dist/tools/run_kics.js +23 -0
- package/dist/tools/run_tflint.js +24 -0
- package/dist/tools/run_trivy.js +23 -0
- package/dist/tools/types.js +1 -0
- package/mcp.json +20 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Carl Dawson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# terraform-best-practices-mcp
|
|
2
|
+
|
|
3
|
+
An MCP server that helps teams produce better Terraform by combining:
|
|
4
|
+
|
|
5
|
+
- CLI analysis: `tflint`, `checkov`, `trivy`, `kics`, `infracost`
|
|
6
|
+
- Best-practice retrieval from `terraform-best-practices.com`
|
|
7
|
+
- Cloud provider guidance for `azure`, `aws`, and `gcp`
|
|
8
|
+
- Terraform Registry guidance for providers/resources/modules
|
|
9
|
+
|
|
10
|
+
## Tool Catalog
|
|
11
|
+
|
|
12
|
+
- `run_tflint`
|
|
13
|
+
- `run_checkov`
|
|
14
|
+
- `run_trivy`
|
|
15
|
+
- `run_kics`
|
|
16
|
+
- `run_infracost`
|
|
17
|
+
- `fetch_terraform_best_practices`
|
|
18
|
+
- `fetch_provider_best_practices`
|
|
19
|
+
- `fetch_terraform_registry_guidance`
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
- Node.js 24+
|
|
24
|
+
- Optional CLIs available on `PATH` for command tools:
|
|
25
|
+
- `tflint`
|
|
26
|
+
- `checkov`
|
|
27
|
+
- `trivy`
|
|
28
|
+
- `kics`
|
|
29
|
+
- `infracost`
|
|
30
|
+
|
|
31
|
+
If a CLI is missing, the server returns installation guidance instead of failing silently.
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install
|
|
37
|
+
npm run build
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Run
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm run dev
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For production-style execution:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm run build
|
|
50
|
+
npm start
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Publish
|
|
54
|
+
|
|
55
|
+
This package is configured for npm as:
|
|
56
|
+
|
|
57
|
+
- `@downatthebottomofthemolehole/terraform-best-practices-mcp-server`
|
|
58
|
+
|
|
59
|
+
To publish:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm publish --access public
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## MCP Configuration
|
|
66
|
+
|
|
67
|
+
This project includes `.vscode/mcp.json`:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"servers": {
|
|
72
|
+
"terraform-best-practices-mcp": {
|
|
73
|
+
"type": "stdio",
|
|
74
|
+
"command": "npm",
|
|
75
|
+
"args": ["run", "dev"]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Registry metadata files used by your other MCP repos are also included:
|
|
82
|
+
|
|
83
|
+
- `mcp.json`
|
|
84
|
+
- `server.json`
|
|
85
|
+
|
|
86
|
+
## References
|
|
87
|
+
|
|
88
|
+
- [MCP org](https://github.com/modelcontextprotocol)
|
|
89
|
+
- [MCP docs](https://modelcontextprotocol.io/docs)
|
|
90
|
+
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
91
|
+
- [terraform-best-practices](https://www.terraform-best-practices.com/)
|
|
92
|
+
- [Terraform Registry](https://registry.terraform.io/)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { allTools } from "./tools/index.js";
|
|
5
|
+
const server = new Server({
|
|
6
|
+
name: "terraform-best-practices-mcp",
|
|
7
|
+
version: "0.1.0"
|
|
8
|
+
}, {
|
|
9
|
+
capabilities: {
|
|
10
|
+
tools: {}
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
const toolMap = new Map(allTools.map((tool) => [tool.name, tool]));
|
|
14
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
15
|
+
return {
|
|
16
|
+
tools: allTools.map((tool) => ({
|
|
17
|
+
name: tool.name,
|
|
18
|
+
description: tool.description,
|
|
19
|
+
inputSchema: tool.inputSchemaJson
|
|
20
|
+
}))
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
24
|
+
const tool = toolMap.get(request.params.name);
|
|
25
|
+
if (!tool) {
|
|
26
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
27
|
+
}
|
|
28
|
+
const parsed = tool.inputSchema.safeParse(request.params.arguments ?? {});
|
|
29
|
+
if (!parsed.success) {
|
|
30
|
+
const issues = parsed.error.issues
|
|
31
|
+
.map((issue) => {
|
|
32
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "input";
|
|
33
|
+
return `${path}: ${issue.message}`;
|
|
34
|
+
})
|
|
35
|
+
.join("; ");
|
|
36
|
+
return {
|
|
37
|
+
isError: true,
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: "text",
|
|
41
|
+
text: `Invalid arguments for ${tool.name}: ${issues}`
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const text = await tool.run(parsed.data);
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text",
|
|
52
|
+
text
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
59
|
+
return {
|
|
60
|
+
isError: true,
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: `Tool execution failed: ${message}`
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
async function main() {
|
|
71
|
+
const transport = new StdioServerTransport();
|
|
72
|
+
await server.connect(transport);
|
|
73
|
+
console.error("terraform-best-practices-mcp running on stdio");
|
|
74
|
+
}
|
|
75
|
+
main().catch((error) => {
|
|
76
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
77
|
+
console.error(`Fatal startup error: ${message}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
});
|
package/dist/lib/exec.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
3
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 2_000_000;
|
|
4
|
+
export class CommandNotFoundError extends Error {
|
|
5
|
+
constructor(command) {
|
|
6
|
+
super(`Command not found: ${command}`);
|
|
7
|
+
this.name = "CommandNotFoundError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export async function runCommand(command, args, options = {}) {
|
|
11
|
+
const cwd = options.cwd ?? process.cwd();
|
|
12
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
13
|
+
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
14
|
+
return await new Promise((resolve, reject) => {
|
|
15
|
+
const child = spawn(command, args, {
|
|
16
|
+
cwd,
|
|
17
|
+
env: options.env ?? process.env,
|
|
18
|
+
shell: false,
|
|
19
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
20
|
+
});
|
|
21
|
+
let stdout = "";
|
|
22
|
+
let stderr = "";
|
|
23
|
+
let timedOut = false;
|
|
24
|
+
let outputTruncated = false;
|
|
25
|
+
let killIssued = false;
|
|
26
|
+
let settled = false;
|
|
27
|
+
const finish = (done) => {
|
|
28
|
+
if (settled) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
settled = true;
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
done();
|
|
34
|
+
};
|
|
35
|
+
const stopProcess = () => {
|
|
36
|
+
if (killIssued || child.killed) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
killIssued = true;
|
|
40
|
+
child.kill("SIGTERM");
|
|
41
|
+
};
|
|
42
|
+
const appendOutput = (current, chunk) => {
|
|
43
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
44
|
+
const next = current + text;
|
|
45
|
+
if (Buffer.byteLength(next, "utf8") > maxOutputBytes) {
|
|
46
|
+
outputTruncated = true;
|
|
47
|
+
stopProcess();
|
|
48
|
+
return next.slice(0, maxOutputBytes);
|
|
49
|
+
}
|
|
50
|
+
return next;
|
|
51
|
+
};
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
timedOut = true;
|
|
54
|
+
stopProcess();
|
|
55
|
+
}, timeoutMs);
|
|
56
|
+
child.stdout?.on("data", (chunk) => {
|
|
57
|
+
stdout = appendOutput(stdout, chunk);
|
|
58
|
+
});
|
|
59
|
+
child.stderr?.on("data", (chunk) => {
|
|
60
|
+
stderr = appendOutput(stderr, chunk);
|
|
61
|
+
});
|
|
62
|
+
child.once("error", (error) => {
|
|
63
|
+
finish(() => {
|
|
64
|
+
if (error.code === "ENOENT") {
|
|
65
|
+
reject(new CommandNotFoundError(command));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
reject(error);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
child.once("close", (code) => {
|
|
72
|
+
finish(() => {
|
|
73
|
+
resolve({
|
|
74
|
+
command,
|
|
75
|
+
args,
|
|
76
|
+
cwd,
|
|
77
|
+
exitCode: code ?? -1,
|
|
78
|
+
stdout: stdout.trim(),
|
|
79
|
+
stderr: stderr.trim(),
|
|
80
|
+
timedOut,
|
|
81
|
+
outputTruncated
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
package/dist/lib/http.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export async function fetchText(url, timeoutMs = 15_000) {
|
|
2
|
+
const controller = new AbortController();
|
|
3
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
4
|
+
try {
|
|
5
|
+
const response = await fetch(url, {
|
|
6
|
+
signal: controller.signal,
|
|
7
|
+
headers: {
|
|
8
|
+
"user-agent": "terraform-best-practices-mcp/0.1.0"
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
if (!response.ok) {
|
|
12
|
+
throw new Error(`Request failed with status ${response.status}`);
|
|
13
|
+
}
|
|
14
|
+
return await response.text();
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
clearTimeout(timeout);
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/lib/text.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function toPlainText(value) {
|
|
2
|
+
return value
|
|
3
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
4
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
5
|
+
.replace(/<[^>]+>/g, " ")
|
|
6
|
+
.replace(/ /gi, " ")
|
|
7
|
+
.replace(/&/gi, "&")
|
|
8
|
+
.replace(/"/gi, '"')
|
|
9
|
+
.replace(/'/gi, "'")
|
|
10
|
+
.replace(/\s+/g, " ")
|
|
11
|
+
.trim();
|
|
12
|
+
}
|
|
13
|
+
export function keywordSnippet(text, keywords, maxChars = 2_000) {
|
|
14
|
+
const cleaned = text.trim();
|
|
15
|
+
if (!cleaned) {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
if (!keywords.length) {
|
|
19
|
+
return cleaned.slice(0, maxChars);
|
|
20
|
+
}
|
|
21
|
+
const loweredKeywords = keywords
|
|
22
|
+
.map((keyword) => keyword.trim().toLowerCase())
|
|
23
|
+
.filter((keyword) => keyword.length > 0);
|
|
24
|
+
const sentences = cleaned.split(/(?<=[.!?])\s+/);
|
|
25
|
+
const matches = sentences.filter((sentence) => {
|
|
26
|
+
const lowered = sentence.toLowerCase();
|
|
27
|
+
return loweredKeywords.some((keyword) => lowered.includes(keyword));
|
|
28
|
+
});
|
|
29
|
+
const source = matches.length > 0 ? matches : sentences;
|
|
30
|
+
let output = "";
|
|
31
|
+
for (const sentence of source) {
|
|
32
|
+
const candidate = output ? `${output} ${sentence}` : sentence;
|
|
33
|
+
if (candidate.length > maxChars) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
output = candidate;
|
|
37
|
+
}
|
|
38
|
+
return output || cleaned.slice(0, maxChars);
|
|
39
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const commandToolInputSchema = z.object({
|
|
3
|
+
path: z.string().min(1).default("."),
|
|
4
|
+
extraArgs: z.array(z.string().min(1)).max(50).default([]),
|
|
5
|
+
timeoutMs: z.number().int().positive().max(600_000).optional()
|
|
6
|
+
});
|
|
7
|
+
export const commandToolInputJsonSchema = {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
path: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Terraform project path to scan.",
|
|
13
|
+
default: "."
|
|
14
|
+
},
|
|
15
|
+
extraArgs: {
|
|
16
|
+
type: "array",
|
|
17
|
+
items: {
|
|
18
|
+
type: "string"
|
|
19
|
+
},
|
|
20
|
+
description: "Extra CLI arguments.",
|
|
21
|
+
default: []
|
|
22
|
+
},
|
|
23
|
+
timeoutMs: {
|
|
24
|
+
type: "integer",
|
|
25
|
+
description: "Optional command timeout in milliseconds.",
|
|
26
|
+
minimum: 1,
|
|
27
|
+
maximum: 600000
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
additionalProperties: false
|
|
31
|
+
};
|
|
32
|
+
const installHints = {
|
|
33
|
+
tflint: "Install tflint from https://github.com/terraform-linters/tflint and ensure `tflint` is on PATH.",
|
|
34
|
+
checkov: "Install checkov with `pip install checkov` and ensure `checkov` is on PATH.",
|
|
35
|
+
trivy: "Install trivy from https://github.com/aquasecurity/trivy and ensure `trivy` is on PATH.",
|
|
36
|
+
kics: "Install kics from https://github.com/Checkmarx/kics and ensure `kics` is on PATH.",
|
|
37
|
+
infracost: "Install infracost from https://www.infracost.io/docs/ and ensure `infracost` is on PATH."
|
|
38
|
+
};
|
|
39
|
+
export function missingCliMessage(command) {
|
|
40
|
+
return [`Required CLI not found: ${command}`, installHints[command]].join("\n");
|
|
41
|
+
}
|
|
42
|
+
export function formatCommandExecution(result) {
|
|
43
|
+
const lines = [
|
|
44
|
+
`Command: ${result.command} ${result.args.join(" ")}`.trim(),
|
|
45
|
+
`Working directory: ${result.cwd}`,
|
|
46
|
+
`Exit code: ${result.exitCode}`,
|
|
47
|
+
`Timed out: ${result.timedOut ? "yes" : "no"}`,
|
|
48
|
+
`Output truncated: ${result.outputTruncated ? "yes" : "no"}`
|
|
49
|
+
];
|
|
50
|
+
if (result.stdout) {
|
|
51
|
+
lines.push("", "stdout:", result.stdout);
|
|
52
|
+
}
|
|
53
|
+
if (result.stderr) {
|
|
54
|
+
lines.push("", "stderr:", result.stderr);
|
|
55
|
+
}
|
|
56
|
+
if (!result.stdout && !result.stderr) {
|
|
57
|
+
lines.push("", "No command output was produced.");
|
|
58
|
+
}
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fetchText } from "../lib/http.js";
|
|
3
|
+
import { keywordSnippet, toPlainText } from "../lib/text.js";
|
|
4
|
+
const providerSources = {
|
|
5
|
+
azure: {
|
|
6
|
+
url: "https://learn.microsoft.com/azure/developer/terraform/overview",
|
|
7
|
+
practices: [
|
|
8
|
+
"Use managed identity or workload identity instead of static credentials.",
|
|
9
|
+
"Keep state in Azure Storage with blob versioning and lease locking.",
|
|
10
|
+
"Model least-privilege RBAC for service principals and managed identities.",
|
|
11
|
+
"Prefer provider aliases for multi-subscription or multi-tenant deployments.",
|
|
12
|
+
"Use consistent resource tags for ownership, environment, and cost center.",
|
|
13
|
+
"Use Azure Verified Modules where possible for baseline guardrails."
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
aws: {
|
|
17
|
+
url: "https://docs.aws.amazon.com/prescriptive-guidance/latest/terraform-aws-provider-best-practices/introduction.html",
|
|
18
|
+
practices: [
|
|
19
|
+
"Use dedicated IAM roles for Terraform runs and avoid long-lived keys.",
|
|
20
|
+
"Separate state by account/environment with DynamoDB locking for S3 state.",
|
|
21
|
+
"Pin provider and module versions for reproducible plans.",
|
|
22
|
+
"Adopt tagging standards for cost allocation and inventory automation.",
|
|
23
|
+
"Use data sources carefully and avoid hidden cross-account dependencies.",
|
|
24
|
+
"Enforce security controls through policy and static analysis in CI."
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
gcp: {
|
|
28
|
+
url: "https://cloud.google.com/docs/terraform/best-practices",
|
|
29
|
+
practices: [
|
|
30
|
+
"Use dedicated service accounts with least privilege and short-lived auth.",
|
|
31
|
+
"Store state in versioned GCS buckets with access controls.",
|
|
32
|
+
"Organize modules around folders/projects to match GCP hierarchy.",
|
|
33
|
+
"Use explicit dependency modeling for APIs and IAM propagation-sensitive resources.",
|
|
34
|
+
"Standardize labels for environment, ownership, and compliance.",
|
|
35
|
+
"Run policy checks and drift detection as part of CI/CD workflows."
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
export const fetchProviderBestPracticesInputSchema = z.object({
|
|
40
|
+
provider: z.enum(["azure", "aws", "gcp"]),
|
|
41
|
+
topic: z.string().min(2).max(120).optional(),
|
|
42
|
+
liveFetch: z.boolean().default(true)
|
|
43
|
+
});
|
|
44
|
+
export const fetchProviderBestPracticesInputJsonSchema = {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
provider: {
|
|
48
|
+
type: "string",
|
|
49
|
+
enum: ["azure", "aws", "gcp"],
|
|
50
|
+
description: "Cloud provider to retrieve Terraform best practices for."
|
|
51
|
+
},
|
|
52
|
+
topic: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: "Optional focus area, for example state, IAM, modules, networking, or cost."
|
|
55
|
+
},
|
|
56
|
+
liveFetch: {
|
|
57
|
+
type: "boolean",
|
|
58
|
+
description: "When true, attempts to fetch and summarize the linked provider guidance page.",
|
|
59
|
+
default: true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
required: ["provider"],
|
|
63
|
+
additionalProperties: false
|
|
64
|
+
};
|
|
65
|
+
function filteredPractices(practices, topic) {
|
|
66
|
+
if (!topic) {
|
|
67
|
+
return practices;
|
|
68
|
+
}
|
|
69
|
+
const lowered = topic.toLowerCase();
|
|
70
|
+
const filtered = practices.filter((practice) => practice.toLowerCase().includes(lowered));
|
|
71
|
+
return filtered.length > 0 ? filtered : practices;
|
|
72
|
+
}
|
|
73
|
+
export const fetchProviderBestPracticesTool = {
|
|
74
|
+
name: "fetch_provider_best_practices",
|
|
75
|
+
description: "Fetch Terraform best-practice guidance for Azure, AWS, or GCP from curated checks and optional live provider docs summaries.",
|
|
76
|
+
inputSchema: fetchProviderBestPracticesInputSchema,
|
|
77
|
+
inputSchemaJson: fetchProviderBestPracticesInputJsonSchema,
|
|
78
|
+
run: async (input) => {
|
|
79
|
+
const source = providerSources[input.provider];
|
|
80
|
+
const topic = input.topic?.trim();
|
|
81
|
+
const practices = filteredPractices(source.practices, topic);
|
|
82
|
+
const lines = [
|
|
83
|
+
`Provider: ${input.provider}`,
|
|
84
|
+
`Primary source: ${source.url}`,
|
|
85
|
+
"Curated provider checklist:",
|
|
86
|
+
...practices.map((practice, index) => `${index + 1}. ${practice}`)
|
|
87
|
+
];
|
|
88
|
+
if (!input.liveFetch) {
|
|
89
|
+
lines.push("", "Live fetch disabled. Returning curated guidance only.");
|
|
90
|
+
return lines.join("\n");
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const html = await fetchText(source.url);
|
|
94
|
+
const snippet = keywordSnippet(toPlainText(html), topic ? [topic, input.provider] : [input.provider, "terraform", "state", "security", "module"], 1800);
|
|
95
|
+
lines.push("", "Live excerpt:", snippet || "No matching live excerpt found.");
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
99
|
+
lines.push("", `Live fetch unavailable: ${reason}`);
|
|
100
|
+
}
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fetchText } from "../lib/http.js";
|
|
3
|
+
import { keywordSnippet, toPlainText } from "../lib/text.js";
|
|
4
|
+
const sourceUrl = "https://www.terraform-best-practices.com/";
|
|
5
|
+
const curatedPractices = [
|
|
6
|
+
"Pin Terraform and provider versions to avoid unexpected upgrades.",
|
|
7
|
+
"Keep modules small and focused, with clear inputs and outputs.",
|
|
8
|
+
"Use remote state with locking and encryption enabled.",
|
|
9
|
+
"Adopt a consistent naming convention for resources, variables, and outputs.",
|
|
10
|
+
"Prefer `for_each` over `count` for stable resource addressing.",
|
|
11
|
+
"Use `validation` blocks for variables and preconditions on critical resources.",
|
|
12
|
+
"Separate environments by state/workspace boundaries to reduce blast radius.",
|
|
13
|
+
"Treat plans as review artifacts in CI before apply.",
|
|
14
|
+
"Store secrets in dedicated secret managers instead of plaintext variables.",
|
|
15
|
+
"Use policy-as-code and linting in CI (tflint/checkov/trivy/kics).",
|
|
16
|
+
"Document each module with usage examples and constraints.",
|
|
17
|
+
"Continuously track cost drift with tools such as infracost."
|
|
18
|
+
];
|
|
19
|
+
export const fetchTerraformBestPracticesInputSchema = z.object({
|
|
20
|
+
topic: z.string().min(2).max(120).optional(),
|
|
21
|
+
liveFetch: z.boolean().default(true)
|
|
22
|
+
});
|
|
23
|
+
export const fetchTerraformBestPracticesInputJsonSchema = {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
topic: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Optional topic filter such as state, modules, security, or naming."
|
|
29
|
+
},
|
|
30
|
+
liveFetch: {
|
|
31
|
+
type: "boolean",
|
|
32
|
+
description: "When true, attempts to fetch and summarize live content from terraform-best-practices.com.",
|
|
33
|
+
default: true
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
additionalProperties: false
|
|
37
|
+
};
|
|
38
|
+
function filterPractices(topic) {
|
|
39
|
+
if (!topic) {
|
|
40
|
+
return curatedPractices;
|
|
41
|
+
}
|
|
42
|
+
const loweredTopic = topic.toLowerCase();
|
|
43
|
+
const filtered = curatedPractices.filter((practice) => practice.toLowerCase().includes(loweredTopic));
|
|
44
|
+
return filtered.length > 0 ? filtered : curatedPractices;
|
|
45
|
+
}
|
|
46
|
+
export const fetchTerraformBestPracticesTool = {
|
|
47
|
+
name: "fetch_terraform_best_practices",
|
|
48
|
+
description: "Fetch Terraform best-practice guidance from curated checks and optional live summaries from terraform-best-practices.com.",
|
|
49
|
+
inputSchema: fetchTerraformBestPracticesInputSchema,
|
|
50
|
+
inputSchemaJson: fetchTerraformBestPracticesInputJsonSchema,
|
|
51
|
+
run: async (input) => {
|
|
52
|
+
const topic = input.topic?.trim();
|
|
53
|
+
const practices = filterPractices(topic);
|
|
54
|
+
const lines = [
|
|
55
|
+
`Source: ${sourceUrl}`,
|
|
56
|
+
"Curated Terraform best-practice checklist:",
|
|
57
|
+
...practices.map((practice, index) => `${index + 1}. ${practice}`)
|
|
58
|
+
];
|
|
59
|
+
if (!input.liveFetch) {
|
|
60
|
+
lines.push("", "Live fetch disabled. Returning curated guidance only.");
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const html = await fetchText(sourceUrl);
|
|
65
|
+
const text = toPlainText(html);
|
|
66
|
+
const snippet = keywordSnippet(text, topic
|
|
67
|
+
? [topic]
|
|
68
|
+
: ["state", "module", "provider", "security", "variable", "output", "version"], 1800);
|
|
69
|
+
lines.push("", "Live excerpt:", snippet || "No matching live excerpt found.");
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
73
|
+
lines.push("", `Live fetch unavailable: ${reason}`);
|
|
74
|
+
}
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fetchText } from "../lib/http.js";
|
|
3
|
+
import { keywordSnippet, toPlainText } from "../lib/text.js";
|
|
4
|
+
const defaultRegistryUrl = "https://registry.terraform.io/";
|
|
5
|
+
const registryPractices = [
|
|
6
|
+
"Pin provider and module versions instead of using unbounded constraints.",
|
|
7
|
+
"Prefer well-maintained modules with clear versioning and examples.",
|
|
8
|
+
"Read provider resource docs for force-recreate fields and lifecycle behavior.",
|
|
9
|
+
"Use module inputs/outputs intentionally and avoid overexposing internals.",
|
|
10
|
+
"Validate required providers and Terraform versions in every module.",
|
|
11
|
+
"Track breaking changes across provider major versions before upgrades."
|
|
12
|
+
];
|
|
13
|
+
export const fetchTerraformRegistryGuidanceInputSchema = z.object({
|
|
14
|
+
provider: z.string().min(1).max(120).optional(),
|
|
15
|
+
resource: z.string().min(1).max(120).optional(),
|
|
16
|
+
module: z.string().min(1).max(240).optional(),
|
|
17
|
+
topic: z.string().min(2).max(120).optional(),
|
|
18
|
+
liveFetch: z.boolean().default(true)
|
|
19
|
+
});
|
|
20
|
+
export const fetchTerraformRegistryGuidanceInputJsonSchema = {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
provider: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Optional provider name, for example aws, azurerm, or google."
|
|
26
|
+
},
|
|
27
|
+
resource: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Optional resource type used with provider, for example s3_bucket or resource_group."
|
|
30
|
+
},
|
|
31
|
+
module: {
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "Optional module path in the form namespace/name/provider."
|
|
34
|
+
},
|
|
35
|
+
topic: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "Optional topic filter for the summary output."
|
|
38
|
+
},
|
|
39
|
+
liveFetch: {
|
|
40
|
+
type: "boolean",
|
|
41
|
+
description: "When true, fetches and summarizes selected Terraform Registry pages.",
|
|
42
|
+
default: true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
additionalProperties: false
|
|
46
|
+
};
|
|
47
|
+
function selectPractices(topic) {
|
|
48
|
+
if (!topic) {
|
|
49
|
+
return registryPractices;
|
|
50
|
+
}
|
|
51
|
+
const lowered = topic.toLowerCase();
|
|
52
|
+
const filtered = registryPractices.filter((item) => item.toLowerCase().includes(lowered));
|
|
53
|
+
return filtered.length > 0 ? filtered : registryPractices;
|
|
54
|
+
}
|
|
55
|
+
function buildRegistryUrls(input) {
|
|
56
|
+
const urls = [];
|
|
57
|
+
const modulePath = input.module?.trim();
|
|
58
|
+
if (modulePath) {
|
|
59
|
+
urls.push(`https://registry.terraform.io/modules/${encodeURI(modulePath)}`);
|
|
60
|
+
}
|
|
61
|
+
const provider = input.provider?.trim();
|
|
62
|
+
const resource = input.resource?.trim();
|
|
63
|
+
if (provider && resource) {
|
|
64
|
+
urls.push(`https://registry.terraform.io/providers/hashicorp/${encodeURIComponent(provider)}/latest/docs/resources/${encodeURIComponent(resource)}`);
|
|
65
|
+
}
|
|
66
|
+
if (urls.length === 0) {
|
|
67
|
+
urls.push(defaultRegistryUrl);
|
|
68
|
+
}
|
|
69
|
+
return urls;
|
|
70
|
+
}
|
|
71
|
+
export const fetchTerraformRegistryGuidanceTool = {
|
|
72
|
+
name: "fetch_terraform_registry_guidance",
|
|
73
|
+
description: "Fetch Terraform Registry best-practice guidance with optional provider/resource/module context.",
|
|
74
|
+
inputSchema: fetchTerraformRegistryGuidanceInputSchema,
|
|
75
|
+
inputSchemaJson: fetchTerraformRegistryGuidanceInputJsonSchema,
|
|
76
|
+
run: async (input) => {
|
|
77
|
+
const topic = input.topic?.trim();
|
|
78
|
+
const practices = selectPractices(topic);
|
|
79
|
+
const urls = buildRegistryUrls(input);
|
|
80
|
+
const lines = [
|
|
81
|
+
`Registry source: ${defaultRegistryUrl}`,
|
|
82
|
+
"Curated Terraform Registry checklist:",
|
|
83
|
+
...practices.map((practice, index) => `${index + 1}. ${practice}`)
|
|
84
|
+
];
|
|
85
|
+
if (!input.liveFetch) {
|
|
86
|
+
lines.push("", "Live fetch disabled. Returning curated guidance only.");
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
const keywords = [
|
|
90
|
+
...(topic ? [topic] : []),
|
|
91
|
+
...(input.provider ? [input.provider] : []),
|
|
92
|
+
...(input.resource ? [input.resource] : []),
|
|
93
|
+
...(input.module ? [input.module] : []),
|
|
94
|
+
"terraform",
|
|
95
|
+
"version",
|
|
96
|
+
"provider",
|
|
97
|
+
"module"
|
|
98
|
+
];
|
|
99
|
+
for (const url of urls) {
|
|
100
|
+
try {
|
|
101
|
+
const html = await fetchText(url);
|
|
102
|
+
const snippet = keywordSnippet(toPlainText(html), keywords, 1400);
|
|
103
|
+
lines.push("", `Live excerpt (${url}):`, snippet || "No matching live excerpt found.");
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
107
|
+
lines.push("", `Live fetch unavailable for ${url}: ${reason}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
112
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { fetchProviderBestPracticesTool } from "./fetch_provider_best_practices.js";
|
|
2
|
+
import { fetchTerraformBestPracticesTool } from "./fetch_terraform_best_practices.js";
|
|
3
|
+
import { fetchTerraformRegistryGuidanceTool } from "./fetch_terraform_registry_guidance.js";
|
|
4
|
+
import { runCheckovTool } from "./run_checkov.js";
|
|
5
|
+
import { runInfracostTool } from "./run_infracost.js";
|
|
6
|
+
import { runKicsTool } from "./run_kics.js";
|
|
7
|
+
import { runTflintTool } from "./run_tflint.js";
|
|
8
|
+
import { runTrivyTool } from "./run_trivy.js";
|
|
9
|
+
export const allTools = [
|
|
10
|
+
runTflintTool,
|
|
11
|
+
runCheckovTool,
|
|
12
|
+
runTrivyTool,
|
|
13
|
+
runKicsTool,
|
|
14
|
+
runInfracostTool,
|
|
15
|
+
fetchTerraformBestPracticesTool,
|
|
16
|
+
fetchProviderBestPracticesTool,
|
|
17
|
+
fetchTerraformRegistryGuidanceTool
|
|
18
|
+
];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CommandNotFoundError, runCommand } from "../lib/exec.js";
|
|
2
|
+
import { commandToolInputJsonSchema, commandToolInputSchema, formatCommandExecution, missingCliMessage } from "./common.js";
|
|
3
|
+
export const runCheckovInputSchema = commandToolInputSchema;
|
|
4
|
+
export const runCheckovTool = {
|
|
5
|
+
name: "run_checkov",
|
|
6
|
+
description: "Run checkov over a Terraform directory.",
|
|
7
|
+
inputSchema: runCheckovInputSchema,
|
|
8
|
+
inputSchemaJson: commandToolInputJsonSchema,
|
|
9
|
+
run: async (input) => {
|
|
10
|
+
try {
|
|
11
|
+
const result = await runCommand("checkov", ["-d", input.path, ...input.extraArgs], {
|
|
12
|
+
timeoutMs: input.timeoutMs
|
|
13
|
+
});
|
|
14
|
+
return formatCommandExecution(result);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof CommandNotFoundError) {
|
|
18
|
+
return missingCliMessage("checkov");
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CommandNotFoundError, runCommand } from "../lib/exec.js";
|
|
2
|
+
import { commandToolInputJsonSchema, commandToolInputSchema, formatCommandExecution, missingCliMessage } from "./common.js";
|
|
3
|
+
export const runInfracostInputSchema = commandToolInputSchema;
|
|
4
|
+
export const runInfracostTool = {
|
|
5
|
+
name: "run_infracost",
|
|
6
|
+
description: "Run infracost breakdown for a Terraform directory.",
|
|
7
|
+
inputSchema: runInfracostInputSchema,
|
|
8
|
+
inputSchemaJson: commandToolInputJsonSchema,
|
|
9
|
+
run: async (input) => {
|
|
10
|
+
try {
|
|
11
|
+
const result = await runCommand("infracost", ["breakdown", "--path", input.path, "--format", "json", ...input.extraArgs], {
|
|
12
|
+
timeoutMs: input.timeoutMs
|
|
13
|
+
});
|
|
14
|
+
return formatCommandExecution(result);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof CommandNotFoundError) {
|
|
18
|
+
return missingCliMessage("infracost");
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CommandNotFoundError, runCommand } from "../lib/exec.js";
|
|
2
|
+
import { commandToolInputJsonSchema, commandToolInputSchema, formatCommandExecution, missingCliMessage } from "./common.js";
|
|
3
|
+
export const runKicsInputSchema = commandToolInputSchema;
|
|
4
|
+
export const runKicsTool = {
|
|
5
|
+
name: "run_kics",
|
|
6
|
+
description: "Run kics IaC scanning against Terraform code.",
|
|
7
|
+
inputSchema: runKicsInputSchema,
|
|
8
|
+
inputSchemaJson: commandToolInputJsonSchema,
|
|
9
|
+
run: async (input) => {
|
|
10
|
+
try {
|
|
11
|
+
const result = await runCommand("kics", ["scan", "-p", input.path, ...input.extraArgs], {
|
|
12
|
+
timeoutMs: input.timeoutMs
|
|
13
|
+
});
|
|
14
|
+
return formatCommandExecution(result);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof CommandNotFoundError) {
|
|
18
|
+
return missingCliMessage("kics");
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CommandNotFoundError, runCommand } from "../lib/exec.js";
|
|
2
|
+
import { commandToolInputJsonSchema, commandToolInputSchema, formatCommandExecution, missingCliMessage } from "./common.js";
|
|
3
|
+
export const runTflintInputSchema = commandToolInputSchema;
|
|
4
|
+
export const runTflintTool = {
|
|
5
|
+
name: "run_tflint",
|
|
6
|
+
description: "Run tflint against a Terraform project directory.",
|
|
7
|
+
inputSchema: runTflintInputSchema,
|
|
8
|
+
inputSchemaJson: commandToolInputJsonSchema,
|
|
9
|
+
run: async (input) => {
|
|
10
|
+
try {
|
|
11
|
+
const result = await runCommand("tflint", [...input.extraArgs], {
|
|
12
|
+
cwd: input.path,
|
|
13
|
+
timeoutMs: input.timeoutMs
|
|
14
|
+
});
|
|
15
|
+
return formatCommandExecution(result);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error instanceof CommandNotFoundError) {
|
|
19
|
+
return missingCliMessage("tflint");
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CommandNotFoundError, runCommand } from "../lib/exec.js";
|
|
2
|
+
import { commandToolInputJsonSchema, commandToolInputSchema, formatCommandExecution, missingCliMessage } from "./common.js";
|
|
3
|
+
export const runTrivyInputSchema = commandToolInputSchema;
|
|
4
|
+
export const runTrivyTool = {
|
|
5
|
+
name: "run_trivy",
|
|
6
|
+
description: "Run trivy config scanning against Terraform code.",
|
|
7
|
+
inputSchema: runTrivyInputSchema,
|
|
8
|
+
inputSchemaJson: commandToolInputJsonSchema,
|
|
9
|
+
run: async (input) => {
|
|
10
|
+
try {
|
|
11
|
+
const result = await runCommand("trivy", ["config", input.path, ...input.extraArgs], {
|
|
12
|
+
timeoutMs: input.timeoutMs
|
|
13
|
+
});
|
|
14
|
+
return formatCommandExecution(result);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof CommandNotFoundError) {
|
|
18
|
+
return missingCliMessage("trivy");
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/mcp.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.DownAtTheBottomOfTheMoleHole/terraform-best-practices",
|
|
4
|
+
"description": "MCP server for Terraform best-practice guidance, linting, security scanning, and cost insights",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/DownAtTheBottomOfTheMoleHole/terraform-best-practices-mcp",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "npm",
|
|
13
|
+
"identifier": "@downatthebottomofthemolehole/terraform-best-practices-mcp-server",
|
|
14
|
+
"version": "0.1.0",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@downatthebottomofthemolehole/terraform-best-practices-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"mcpName": "io.github.DownAtTheBottomOfTheMoleHole/terraform-best-practices",
|
|
5
|
+
"description": "MCP server for Terraform cost, lint, security, and cloud best-practice guidance.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"terraform-best-practices-mcp-server": "dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/**/*",
|
|
14
|
+
"mcp.json",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"mcp": {
|
|
19
|
+
"name": "terraform-best-practices",
|
|
20
|
+
"description": "Terraform best-practice, security, lint, and cost analysis via MCP"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"terraform",
|
|
25
|
+
"best-practices",
|
|
26
|
+
"infracost",
|
|
27
|
+
"tflint",
|
|
28
|
+
"checkov",
|
|
29
|
+
"trivy",
|
|
30
|
+
"kics",
|
|
31
|
+
"azure",
|
|
32
|
+
"aws",
|
|
33
|
+
"gcp"
|
|
34
|
+
],
|
|
35
|
+
"author": "Carl Dawson",
|
|
36
|
+
"repository": "https://github.com/DownAtTheBottomOfTheMoleHole/terraform-best-practices-mcp",
|
|
37
|
+
"homepage": "https://github.com/DownAtTheBottomOfTheMoleHole/terraform-best-practices-mcp",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/DownAtTheBottomOfTheMoleHole/terraform-best-practices-mcp/issues"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=24.0.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p tsconfig.json",
|
|
46
|
+
"dev": "tsx src/index.ts",
|
|
47
|
+
"start": "node dist/index.js",
|
|
48
|
+
"check": "tsc --noEmit -p tsconfig.json",
|
|
49
|
+
"lint": "eslint --ext .ts src tests",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:watch": "vitest"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@modelcontextprotocol/sdk": "1.27.1",
|
|
55
|
+
"zod": "^3.23.8"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "25.3.5",
|
|
59
|
+
"@typescript-eslint/eslint-plugin": "8.56.1",
|
|
60
|
+
"@typescript-eslint/parser": "8.56.1",
|
|
61
|
+
"eslint": "8.57.1",
|
|
62
|
+
"tsx": "4.21.0",
|
|
63
|
+
"typescript": "5.9.3",
|
|
64
|
+
"vitest": "4.0.18"
|
|
65
|
+
},
|
|
66
|
+
"license": "MIT"
|
|
67
|
+
}
|