deploymentHowTo

Managing Infrastructure with a Terraform MCP Server

Let an AI agent propose Terraform plans for review, with apply gated behind explicit human approval and a working-directory allowlist.

Quick Answer / TL;DR

A Terraform MCP server should expose a read-only plan tool freely, but gate apply behind an explicit approval flag and a working-directory allowlist, since an AI agent proposing infrastructure changes is very different from one being allowed to apply them unsupervised.

Key Takeaways

  • Never let a tool call both plan and apply in one turn - the plan output needs a human to actually read it first.
  • Wrap the Terraform CLI via a child process; there is no official Terraform Node.js SDK, so this is the standard integration pattern.
  • Restrict which directories the server can operate in with an explicit allowlist, so a model can't be steered into running terraform against an unintended state file.

A plan-then-apply tool pair

Terraform has no first-party SDK - the CLI itself is the interface, so the server wraps it with a child process. Keep plan and apply as two separate tools: plan is safe to call freely since it's read-only, while apply requires an explicit approved: true argument the model can only set after showing the plan output to the user and getting confirmation.

typescript
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import path from "node:path";

const execFileAsync = promisify(execFile);
const ALLOWED_DIRS = (process.env.TERRAFORM_ALLOWED_DIRS ?? "").split(",").filter(Boolean);

function assertAllowedDir(workingDir: string) {
  const resolved = path.resolve(workingDir);
  const ok = ALLOWED_DIRS.some((dir) => resolved.startsWith(path.resolve(dir)));
  if (!ok) throw new Error(`${workingDir} is not in TERRAFORM_ALLOWED_DIRS`);
}

async function terraformPlan(workingDir: string) {
  assertAllowedDir(workingDir);
  const { stdout } = await execFileAsync("terraform", ["plan", "-no-color", "-input=false"], {
    cwd: workingDir,
    timeout: 120_000,
  });
  return stdout;
}

async function terraformApply(workingDir: string, approved: boolean) {
  assertAllowedDir(workingDir);
  if (!approved) {
    throw new Error("apply requires approved: true - show the plan to the user and get confirmation first.");
  }
  const { stdout } = await execFileAsync("terraform", ["apply", "-auto-approve", "-no-color", "-input=false"], {
    cwd: workingDir,
    timeout: 600_000,
  });
  return stdout;
}

State locking and blast radius

Use a remote backend (S3 with a DynamoDB lock table, or Terraform Cloud) so concurrent applies fail loudly with a lock error instead of corrupting state. For an AI-proposed change, prefer -target to scope an apply to the specific resource under discussion rather than the whole configuration, keeping blast radius small if the model's reasoning about the change was wrong.

Managing Infrastructure with a Terraform MCP Server FAQs

Direct answers for developers, operators, and Indian teams evaluating MCP.

M
MCPserver Team

MCP documentation and protocol implementation team

Published: 2026-07-21
Updated: 2026-07-21