·

Terraform MCP With OpenCode

Set up Terraform MCP in OpenCode so your AI agent can plan, review, and manage infrastructure as code right from your editor.

OpenCode's MCP support is solid but younger than Claude Code's — no per-tool permission granularity yet, and its plan-mode-vs-agent-mode split doesn't map onto Terraform's own plan/apply distinction as cleanly as you'd hope, so you have to build that separation yourself through server choice and prompting discipline. This topic covers connecting the server, running the core validate/plan loop, a practical module-refactoring example, and where OpenCode's Terraform support currently falls short.

Installing and Connecting Terraform MCP to OpenCode

OpenCode reads MCP server definitions from opencode.json (project-local) or ~/.config/opencode/opencode.json (global). Add both the registry and CLI-execution servers under mcp:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "terraform-registry": {
      "type": "local",
      "command": ["docker", "run", "-i", "--rm", "hashicorp/terraform-mcp-server"],
      "enabled": true
    },
    "terraform-cli": {
      "type": "local",
      "command": [
        "terraform-mcp-server",
        "--allowed-dirs", "./infra",
        "--deny-glob", "**/*.tfstate"
      ],
      "environment": {
        "TF_IN_AUTOMATION": "true",
        "TF_WORKSPACE": "staging"
      },
      "enabled": true
    }
  }
}

Confirm both loaded from inside a session:

opencode
> /mcp

Because OpenCode doesn't yet support fine-grained per-tool allow/ask rules the way Claude Code does, the practical control point is which server you connect at all, and whether you leave terraform-cli's apply tool (if your server exposes one) out of scope by omitting it from the command's enabled toolset — some server implementations let you pass a --tools allowlist at launch:

terraform-mcp-server --allowed-dirs ./infra --tools init,validate,plan,state_list,state_show

That flag, when supported, is the single most important line in this whole setup — it removes apply from what the agent can even attempt to call, rather than relying on OpenCode to ask you every time.

Tips
- If your terraform-mcp-server build supports a --tools allowlist, use it to physically remove apply from the exposed toolset rather than trusting a runtime confirmation prompt that OpenCode may not surface consistently.
- Run /mcp at the start of every session, not just once at setup — a background docker daemon restart or a config typo silently drops a server and OpenCode won't always surface a big warning.
- Keep the registry and CLI servers as two separate entries; it makes /mcp output legible when you're debugging which tool call went where.


Running Validate and Plan from OpenCode

The everyday loop is unglamorous and that's the point — validate cheaply, plan deliberately, read before touching apply:

Validate the HCL under infra/environments/staging, then run a plan
against staging.tfvars. Don't apply anything. Summarize the plan
by resource and action.

OpenCode calls terraform_validate, then terraform_plan, and should hand back something close to:

terraform_validate: Success! The configuration is valid.

terraform_plan (staging, staging.tfvars):
  Plan: 2 to add, 0 to change, 1 to destroy.

  + aws_instance.worker[2]        create
  + aws_instance.worker[3]        create
  - aws_instance.worker_legacy    destroy (removed from config)

Where OpenCode's terseness actually helps: it won't editorialize much beyond what you ask for, so if you want risk classification you need to ask explicitly, same as any agent:

For the destroy action on aws_instance.worker_legacy, check whether
it's referenced by any other resource (security group associations,
load balancer target group attachments) before I approve removing it.
terraform state list | grep worker_legacy
terraform state show aws_lb_target_group_attachment.worker_legacy_attach

If the target group attachment resource still exists in state and references the instance, removing it first (or in the same apply) matters — an orphaned attachment referencing a destroyed instance ID is a common source of "why is this instance still receiving traffic" confusion after a supposedly clean destroy.

Tips
- Ask explicitly for reference checks before approving any destroy — OpenCode won't proactively cross-reference dependent resources unless you name the check you want.
- Keep var_file and working_dir explicit in every prompt; OpenCode sessions don't always retain enough working-directory context across a long conversation to infer it correctly on turn 12.
- Re-run terraform_validate after any HCL edit the agent makes mid-session — it's cheap enough to run on every turn and catches syntax slips before a wasted plan cycle.


Practical Example: Refactoring Duplicated Resources into a Module

A common real task: three nearly-identical aws_instance blocks for worker nodes, copy-pasted with minor differences, that should become one module with a count or for_each. Starting state:

resource "aws_instance" "worker_1" {
  ami           = "ami-0abc123"
  instance_type = "m6i.large"
  subnet_id     = "subnet-0a1b2c"
  tags          = { Name = "worker-1", Role = "worker" }
}

resource "aws_instance" "worker_2" {
  ami           = "ami-0abc123"
  instance_type = "m6i.large"
  subnet_id     = "subnet-0d4e5f"
  tags          = { Name = "worker-2", Role = "worker" }
}

resource "aws_instance" "worker_3" {
  ami           = "ami-0abc123"
  instance_type = "m6i.xlarge"
  subnet_id     = "subnet-0d4e5f"
  tags          = { Name = "worker-3", Role = "worker" }
}

Prompt for the refactor with an explicit constraint on state safety — this is the part people skip and then wonder why the plan wants to destroy and recreate everything:

Refactor worker_1/2/3 into a for_each-based resource. Preserve the
existing instance IDs — write the terraform state mv commands needed
so the refactor doesn't destroy and recreate the instances.

Resulting HCL:

locals {
  workers = {
    worker_1 = { instance_type = "m6i.large",  subnet_id = "subnet-0a1b2c" }
    worker_2 = { instance_type = "m6i.large",  subnet_id = "subnet-0d4e5f" }
    worker_3 = { instance_type = "m6i.xlarge", subnet_id = "subnet-0d4e5f" }
  }
}

resource "aws_instance" "worker" {
  for_each      = local.workers
  ami           = "ami-0abc123"
  instance_type = each.value.instance_type
  subnet_id     = each.value.subnet_id
  tags          = { Name = each.key, Role = "worker" }
}

And the required state surgery, which must run before the first plan against the new HCL or Terraform sees three deletes and three creates instead of zero-diff renames:

terraform state mv aws_instance.worker_1 'aws_instance.worker["worker_1"]'
terraform state mv aws_instance.worker_2 'aws_instance.worker["worker_2"]'
terraform state mv aws_instance.worker_3 'aws_instance.worker["worker_3"]'

Then confirm the refactor is genuinely a no-op against real infrastructure:

terraform plan

That last line — "No changes" — is the actual success criterion for a resource-to-module refactor. If the agent hands you new HCL without the accompanying state mv commands, or skips verifying the resulting plan is empty, treat the refactor as incomplete regardless of how clean the HCL looks.

Tips
- Never accept a resource-shape refactor (static blocks to for_each/count, or splitting/merging modules) without the matching terraform state mv commands generated alongside it.
- Run the plan after the state moves and require "No changes" as the acceptance bar — a non-empty diff means either the mv addresses were wrong or the HCL doesn't match what's actually in state.
- Do state mv operations on a copy of state first if your backend doesn't version state natively (S3 with versioning does; some setups don't) — a botched address string can orphan a resource from state entirely.


Known Limitations for Terraform MCP in OpenCode

Worth being direct about where this combination is weaker than Claude Code or Cursor, as of the versions available in mid-2026:

  • No per-tool permission rules. OpenCode's config doesn't let you allow terraform_plan while requiring confirmation on terraform_apply at the OpenCode level — you're relying entirely on the MCP server's own tool exposure (the --tools allowlist trick above) to enforce that split.
  • Session working-directory drift on long conversations. In sessions that span many turns and multiple cd-equivalent context switches, OpenCode occasionally loses track of which working_dir it last used for a Terraform tool call, silently reusing a stale one. Always pass working_dir explicitly rather than relying on inferred context.
  • No native plan-file diffing across turns. If you ask "what changed in the plan since we last ran it," OpenCode has no built-in memory of the prior plan JSON beyond whatever's still in the conversation context — for long sessions, that context can get truncated, and the comparison silently degrades to "guessing from what's still visible" rather than an exact diff.
  • Docker-based registry server startup latency. The docker run invocation cold-starts on every new OpenCode session (no persistent daemon mode by default), adding a few hundred milliseconds to a second before the first registry lookup responds — noticeable but not disqualifying.

None of these are dealbreakers for solo or small-team use, but they're reasons a security-sensitive team standardizes on Claude Code's tool-level permission model for the apply-adjacent boundary and uses OpenCode more for authoring and read-only review.

Tips
- Compensate for the missing per-tool permission model by physically restricting the MCP server's exposed toolset (--tools flag or a read-only server binary) rather than trusting OpenCode's prompt-level confirmations alone.
- Save plan JSON to a file (-out=tfplan && terraform show -json tfplan > plan.json) if you need to diff plans across a long session — don't rely on OpenCode's conversation memory holding the prior plan verbatim.
- Pre-pull the Docker image (docker pull hashicorp/terraform-mcp-server) before a demo or time-boxed session — cold pulls on a fresh machine add much more latency than a warm cold-start.


Tips

Tips
- Use a server-side --tools allowlist to remove apply from OpenCode's reach, since OpenCode itself has no per-tool permission model yet.
- Always pass explicit working_dir and var_file in prompts — don't trust inferred session context over a long conversation.
- Verify resource-to-module refactors with the "No changes" plan output after the accompanying state mv commands, not by eyeballing the HCL diff alone.