From 81d77074dbc97e795c711b4e919f06e4395bc208 Mon Sep 17 00:00:00 2001 From: Gautam Date: Wed, 22 Oct 2025 12:14:18 -0700 Subject: [PATCH 01/28] go mod fixes (#203) --- go.mod | 17 ++++++------ go.sum | 37 ++++++++++++++------------- pkg/tools/tfe/create_run.go | 1 + pkg/tools/tfe/list_workspaces_test.go | 3 +-- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index 60aa671a..6a65bb2a 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,9 @@ go 1.24.0 require ( github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-retryablehttp v0.7.8 - github.com/hashicorp/go-tfe v1.93.0 + github.com/hashicorp/go-tfe v1.95.0 github.com/hashicorp/jsonapi v1.5.0 - github.com/mark3labs/mcp-go v0.41.1 + github.com/mark3labs/mcp-go v0.42.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.10.1 github.com/spf13/viper v1.21.0 @@ -21,18 +21,16 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/go-slug v0.16.7 // indirect + github.com/hashicorp/go-slug v0.16.8 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect @@ -41,7 +39,8 @@ require ( github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9d9d4ad1..8f31f3e3 100644 --- a/go.sum +++ b/go.sum @@ -28,10 +28,10 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= -github.com/hashicorp/go-slug v0.16.7 h1:sBW8y1sX+JKOZKu9a+DQZuWDVaX+U9KFnk6+VDQvKcw= -github.com/hashicorp/go-slug v0.16.7/go.mod h1:X5fm++dL59cDOX8j48CqHr4KARTQau7isGh0ZVxJB5I= -github.com/hashicorp/go-tfe v1.93.0 h1:hJubwn1xNCo1iBO66iQkjyC+skR61cK1AQUj4O9vvuI= -github.com/hashicorp/go-tfe v1.93.0/go.mod h1:QwqgCD5seztgp76CP7F0POJPflQNSqjIvBpVohg9X50= +github.com/hashicorp/go-slug v0.16.8 h1:f4/sDZqRsxx006HrE6e9BE5xO9lWXydKhVoH6Kb0v1M= +github.com/hashicorp/go-slug v0.16.8/go.mod h1:hB4mUcVHl4RPu0205s0fwmB9i31MxQgeafGkko3FD+Y= +github.com/hashicorp/go-tfe v1.95.0 h1:vx1X6SlSHBnJzoy8Wy+0OyXcNmbdUt3wIk+Ll6mQLgo= +github.com/hashicorp/go-tfe v1.95.0/go.mod h1:hTnfAzkwOMvWL4sVKNPzUYTjrbwKIWnRWYSIC/Zh5SA= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= @@ -42,14 +42,17 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mark3labs/mcp-go v0.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA= -github.com/mark3labs/mcp-go v0.41.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mark3labs/mcp-go v0.42.0 h1:gk/8nYJh8t3yroCAOBhNbYsM9TCKvkM13I5t5Hfu6Ls= +github.com/mark3labs/mcp-go v0.42.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -62,12 +65,10 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= -github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -94,16 +95,16 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/tools/tfe/create_run.go b/pkg/tools/tfe/create_run.go index b8def14a..032dc9ae 100644 --- a/pkg/tools/tfe/create_run.go +++ b/pkg/tools/tfe/create_run.go @@ -40,6 +40,7 @@ func CreateRunSafe(logger *log.Logger) server.ServerTool { ), mcp.WithString("message", mcp.Description("Optional message for the run"), + mcp.DefaultString("Triggered via Terraform MCP Server"), ), ), Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { diff --git a/pkg/tools/tfe/list_workspaces_test.go b/pkg/tools/tfe/list_workspaces_test.go index ed5bf993..feeb75aa 100644 --- a/pkg/tools/tfe/list_workspaces_test.go +++ b/pkg/tools/tfe/list_workspaces_test.go @@ -95,8 +95,7 @@ func TestSearchWorkspaces(t *testing.T) { } mockWorkspaceList := &tfe.WorkspaceList{ - Items: mockWorkspaces, - Pagination: &tfe.Pagination{CurrentPage: 1, TotalCount: 2}, + Items: mockWorkspaces, } // Verify the mock workspace list structure From efe052b17c4ce6aaa121264a7cd2b88c96247196 Mon Sep 17 00:00:00 2001 From: Lee Fowler Date: Wed, 22 Oct 2025 22:09:38 -0400 Subject: [PATCH 02/28] CI workflow and associated dependencies for Anthropic MCP Registry publishing (#200) * ci: add anthropic mcp registry publishing gha workflow, add server.json spec for v0.3.0 of mcp server, update dockerfile with package provenance label * ci: update publish-registry workflow to use protected "publish-registry" environment * chore: update makefile with new target to update version specifiers in server.json from the versionfile * feat: update server.json mcp specification file with runtime argument specifications for k/v environment variables * chore: update version specifiers in server.json to latest released version of the mcp server package * docs: update changelog * fix: replace outdated "terraform cloud" moniker with updated "hcp terraform" name * docs: update description of mcp server in server.json --- .github/workflows/publish-registry.yml | 42 ++ CHANGELOG.md | 4 + Dockerfile | 1 + Makefile | 10 +- scripts/update-server-json-version.sh | 44 ++ server.json | 762 +++++++++++++++++++++++++ 6 files changed, 861 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/publish-registry.yml create mode 100755 scripts/update-server-json-version.sh create mode 100644 server.json diff --git a/.github/workflows/publish-registry.yml b/.github/workflows/publish-registry.yml new file mode 100644 index 00000000..9339dce8 --- /dev/null +++ b/.github/workflows/publish-registry.yml @@ -0,0 +1,42 @@ +name: publish-registry + +on: + workflow_dispatch: + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + environment: publish-registry + + steps: + - name: Checkout repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: .go-version + cache: true + cache-dependency-path: | + go.sum + + - name: Run unit tests + run: go test ./... + + - name: Build release artifacts + run: make crt-build + + - name: Install MCP Publisher CLI + run: | + curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname%20-s%20|%20tr'[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher + chmod +x mcp-publisher + + - name: Authenticate with MCP Registry + run: ./mcp-publisher login github-oidc + + - name: Publish server to MCP Registry + run: ./mcp-publisher publish diff --git a/CHANGELOG.md b/CHANGELOG.md index e020491c..acb3371f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ FIXES * Added a module id validator to fix issue [182](https://github.com/hashicorp/terraform-mcp-server/issues/182) * Fixes in readme for `TFE_HOSTNAME` v/s `TFE_ADDRESS` +IMPROVEMENTS + +* Added official MCP Registry Server JSON Specification file [server.json](server.json) to the repo. See [#200](https://github.com/hashicorp/terraform-mcp-server/pull/200) + ## 0.3.1 (Oct 3, 2025) FEATURES diff --git a/Dockerfile b/Dockerfile index 36224fda..c000e5ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,6 +61,7 @@ ARG PRODUCT_NAME=$BIN_NAME ARG TARGETOS TARGETARCH LABEL version=$PRODUCT_VERSION LABEL revision=$PRODUCT_REVISION +LABEL io.modelcontextprotocol.server.name="io.github.hashicorp/terraform-mcp-server" COPY dist/$TARGETOS/$TARGETARCH/$BIN_NAME /bin/terraform-mcp-server COPY --from=certbuild /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt # Command to run the server (mode determined by environment variables or defaults to stdio) diff --git a/Makefile b/Makefile index 1e7c3b61..463811f7 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ TARGET_DIR ?= $(CURDIR)/dist # Build flags LDFLAGS=-ldflags="-s -w -X terraform-mcp-server/version.GitCommit=$(shell git rev-parse HEAD) -X terraform-mcp-server/version.BuildDate=$(shell git show --no-show-signature -s --format=%cd --date=format:"%Y-%m-%dT%H:%M:%SZ" HEAD)" -.PHONY: all build crt-build test test-e2e test-security clean deps docker-build run-http run-http-secure docker-run-http test-http cleanup-test-containers help +.PHONY: all build crt-build test test-e2e test-security clean deps docker-build run-http run-http-secure docker-run-http test-http cleanup-test-containers update-server-json-version help # Default target all: build @@ -63,6 +63,12 @@ run-http-secure: docker-run-http: $(DOCKER) run -p 8080:8080 --rm $(BINARY_NAME):$(VERSION) http --transport-port 8080 --transport-host 0.0.0.0 +# Synchronise server.json version fields with version/VERSION +update-server-json-version: + @VERSION_FILE="$(CURDIR)/version/VERSION"; \ + SERVER_JSON="$(CURDIR)/server.json"; \ + "$(CURDIR)/scripts/update-server-json-version.sh" "$$VERSION_FILE" "$$SERVER_JSON" + # Test HTTP endpoint test-http: @echo "Testing StreamableHTTP server health endpoint..." @@ -100,5 +106,5 @@ help: @echo " docker-run-http - Run StreamableHTTP server in Docker on port 8080" @echo " test-http - Test StreamableHTTP health endpoint" @echo " cleanup-test-containers - Stop and remove all test containers" + @echo " update-server-json-version - Update server.json to match version/VERSION" @echo " help - Show this help message" - diff --git a/scripts/update-server-json-version.sh b/scripts/update-server-json-version.sh new file mode 100755 index 00000000..bda8044b --- /dev/null +++ b/scripts/update-server-json-version.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERSION_FILE="${1:-version/VERSION}" +SERVER_JSON="${2:-server.json}" + +if [[ ! -f "${VERSION_FILE}" ]]; then + echo "Version file not found: ${VERSION_FILE}" >&2 + exit 1 +fi + +if [[ ! -f "${SERVER_JSON}" ]]; then + echo "server.json not found: ${SERVER_JSON}" >&2 + exit 1 +fi + +VERSION_VALUE="$(<"${VERSION_FILE}")" + +if [[ -z "${VERSION_VALUE}" ]]; then + echo "Version file ${VERSION_FILE} is empty" >&2 + exit 1 +fi + +tmp_file="$(mktemp)" +trap 'rm -f "${tmp_file}"' EXIT + +jq --arg version "${VERSION_VALUE}" ' + .version = $version + | .packages = (.packages // []) + | (.packages[] |= ( + (if has("identifier") and (.identifier | type) == "string" then + .identifier = (.identifier | gsub("hashicorp/terraform-mcp-server:[^[:space:]]+"; "hashicorp/terraform-mcp-server:" + $version)) + else . end) + | .runtimeArguments = (.runtimeArguments // []) + | (.runtimeArguments[] |= ( + if has("value") and (.value | type) == "string" then + .value = (.value | gsub("hashicorp/terraform-mcp-server:[^[:space:]]+"; "hashicorp/terraform-mcp-server:" + $version)) + else . + end + )) + )) +' "${SERVER_JSON}" > "${tmp_file}" + +mv "${tmp_file}" "${SERVER_JSON}" diff --git a/server.json b/server.json new file mode 100644 index 00000000..24006835 --- /dev/null +++ b/server.json @@ -0,0 +1,762 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", + "name": "io.gitub.hashicorp/terraform-mcp-server", + "title": "Terraform MCP Server", + "description": "Generate more accurate Terraform and automate workflows for HCP Terraform and Terraform Enterprise", + "websiteUrl": "https://github.com/hashicorp/terraform-mcp-server", + "repository": { + "url": "https://github.com/hashicorp/terraform-mcp-server", + "source": "github" + }, + "version": "0.3.1", + "packages": [ + { + "registryType": "oci", + "identifier": "docker.io/hashicorp/terraform-mcp-server:0.3.1", + "transport": { + "type": "stdio" + }, + "runtimeHint": "docker", + "runtimeArguments": [ + { + "type": "positional", + "value": "run" + }, + { + "type": "named", + "name": "--rm" + }, + { + "type": "named", + "name": "-i" + }, + { + "type": "named", + "name": "-e", + "value": "TFE_ADDRESS={tfe_address}", + "description": "HCP Terraform or Terraform Enterprise base URL.", + "variables": { + "tfe_address": { + "description": "HCP Terraform or Terraform Enterprise base URL.", + "default": "https://app.terraform.io/", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "TFE_TOKEN={tfe_token}", + "description": "HCP Terraform or Terraform Enterprise API token used to authenticate requests.", + "variables": { + "tfe_token": { + "description": "HCP Terraform or Terraform Enterprise API token used to authenticate requests.", + "isRequired": false, + "isSecret": true + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "TFE_SKIP_TLS_VERIFY={tfe_skip_tls_verify}", + "description": "Set to true to disable TLS verification when connecting to HCP Terraform or Terraform Enterprise.", + "variables": { + "tfe_skip_tls_verify": { + "description": "Set to true to disable TLS verification when connecting to HCP Terraform or Terraform Enterprise.", + "default": "false", + "choices": [ + "true", + "false" + ], + "format": "boolean", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "TRANSPORT_MODE={transport_mode}", + "description": "Selects the active transport; use streamable-http to serve HTTP clients.", + "variables": { + "transport_mode": { + "description": "Selects the active transport; use streamable-http to serve HTTP clients.", + "default": "stdio", + "choices": [ + "stdio" + ], + "isRequired": true + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "TRANSPORT_HOST={transport_host}", + "description": "Interface to bind when running in streamable-http mode.", + "variables": { + "transport_host": { + "description": "Interface to bind when running in streamable-http mode.", + "default": "127.0.0.1", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "TRANSPORT_PORT={transport_port}", + "description": "Port to bind when running in streamable-http mode.", + "variables": { + "transport_port": { + "description": "Port to bind when running in streamable-http mode.", + "default": "8080", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_ENDPOINT={mcp_endpoint}", + "description": "HTTP endpoint path exposed when streamable-http transport is enabled.", + "variables": { + "mcp_endpoint": { + "description": "HTTP endpoint path exposed when streamable-http transport is enabled.", + "default": "/mcp", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_SESSION_MODE={mcp_session_mode}", + "description": "Session management mode for the MCP server.", + "variables": { + "mcp_session_mode": { + "description": "Session management mode for the MCP server.", + "default": "stateful", + "choices": [ + "stateful", + "stateless" + ], + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_ALLOWED_ORIGINS={mcp_allowed_origins}", + "description": "Comma-separated list of allowed origins for CORS when using streamable-http transport.", + "variables": { + "mcp_allowed_origins": { + "description": "Comma-separated list of allowed origins for CORS when using streamable-http transport.", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_CORS_MODE={mcp_cors_mode}", + "description": "CORS enforcement mode; strict enforces allowed origins while development trusts localhost.", + "variables": { + "mcp_cors_mode": { + "description": "CORS enforcement mode; strict enforces allowed origins while development trusts localhost.", + "default": "strict", + "choices": [ + "strict", + "development", + "disabled" + ], + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_TLS_CERT_FILE={mcp_tls_cert_file}", + "description": "Path to the TLS certificate file to enable HTTPS for streamable-http transport.", + "variables": { + "mcp_tls_cert_file": { + "description": "Path to the TLS certificate file to enable HTTPS for streamable-http transport.", + "format": "filepath", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_TLS_KEY_FILE={mcp_tls_key_file}", + "description": "Path to the TLS key file to enable HTTPS for streamable-http transport.", + "variables": { + "mcp_tls_key_file": { + "description": "Path to the TLS key file to enable HTTPS for streamable-http transport.", + "format": "filepath", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_RATE_LIMIT_GLOBAL={mcp_rate_limit_global}", + "description": "Global rate limit in requests-per-second:burst format applied across all sessions.", + "variables": { + "mcp_rate_limit_global": { + "description": "Global rate limit in requests-per-second:burst format applied across all sessions.", + "default": "10:20", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_RATE_LIMIT_SESSION={mcp_rate_limit_session}", + "description": "Per-session rate limit in requests-per-second:burst format.", + "variables": { + "mcp_rate_limit_session": { + "description": "Per-session rate limit in requests-per-second:burst format.", + "default": "5:10", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "ENABLE_TF_OPERATIONS={enable_tf_operations}", + "description": "Set to true to enable tools that execute Terraform operations requiring explicit approval.", + "variables": { + "enable_tf_operations": { + "description": "Set to true to enable tools that execute Terraform operations requiring explicit approval.", + "default": "false", + "choices": [ + "true", + "false" + ], + "format": "boolean", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "positional", + "value": "hashicorp/terraform-mcp-server:0.3.1" + } + ], + "environmentVariables": [ + { + "name": "TFE_ADDRESS", + "description": "HCP Terraform or Terraform Enterprise base URL.", + "default": "https://app.terraform.io/", + "isRequired": false + }, + { + "name": "TFE_TOKEN", + "description": "HCP Terraform or Terraform Enterprise API token used to authenticate requests.", + "isRequired": false, + "isSecret": true + }, + { + "name": "TFE_SKIP_TLS_VERIFY", + "description": "Set to true to disable TLS verification when connecting to HCP Terraform or Terraform Enterprise.", + "default": "false", + "format": "boolean", + "choices": [ + "true", + "false" + ], + "isRequired": false + }, + { + "name": "TRANSPORT_MODE", + "description": "Selects the active transport; use streamable-http to serve HTTP clients.", + "default": "stdio", + "choices": [ + "stdio" + ], + "isRequired": true + }, + { + "name": "TRANSPORT_HOST", + "description": "Interface to bind when running in streamable-http mode.", + "default": "127.0.0.1", + "isRequired": false + }, + { + "name": "TRANSPORT_PORT", + "description": "Port to bind when running in streamable-http mode.", + "default": "8080", + "isRequired": false + }, + { + "name": "MCP_ENDPOINT", + "description": "HTTP endpoint path exposed when streamable-http transport is enabled.", + "default": "/mcp", + "isRequired": false + }, + { + "name": "MCP_SESSION_MODE", + "description": "Session management mode for the MCP server.", + "default": "stateful", + "choices": [ + "stateful", + "stateless" + ], + "isRequired": false + }, + { + "name": "MCP_ALLOWED_ORIGINS", + "description": "Comma-separated list of allowed origins for CORS when using streamable-http transport.", + "isRequired": false + }, + { + "name": "MCP_CORS_MODE", + "description": "CORS enforcement mode; strict enforces allowed origins while development trusts localhost.", + "default": "strict", + "choices": [ + "strict", + "development", + "disabled" + ], + "isRequired": false + }, + { + "name": "MCP_TLS_CERT_FILE", + "description": "Path to the TLS certificate file to enable HTTPS for streamable-http transport.", + "format": "filepath", + "isRequired": false + }, + { + "name": "MCP_TLS_KEY_FILE", + "description": "Path to the TLS key file to enable HTTPS for streamable-http transport.", + "format": "filepath", + "isRequired": false + }, + { + "name": "MCP_RATE_LIMIT_GLOBAL", + "description": "Global rate limit in requests-per-second:burst format applied across all sessions.", + "default": "10:20", + "isRequired": false + }, + { + "name": "MCP_RATE_LIMIT_SESSION", + "description": "Per-session rate limit in requests-per-second:burst format.", + "default": "5:10", + "isRequired": false + }, + { + "name": "ENABLE_TF_OPERATIONS", + "description": "Set to true to enable tools that execute Terraform operations requiring explicit approval.", + "default": "false", + "format": "boolean", + "choices": [ + "true", + "false" + ], + "isRequired": false + } + ] + }, + { + "registryType": "oci", + "registryBaseUrl": "https://docker.io/", + "identifier": "docker.io/hashicorp/terraform-mcp-server:0.3.1", + "version": "0.3.1", + "transport": { + "type": "streamable-http", + "url": "http://127.0.0.1:8080/mcp" + }, + "runtimeHint": "docker", + "runtimeArguments": [ + { + "type": "positional", + "value": "run" + }, + { + "type": "named", + "name": "--rm" + }, + { + "type": "named", + "name": "-p", + "value": "8080:8080" + }, + { + "type": "named", + "name": "-i" + }, + { + "type": "named", + "name": "-e", + "value": "TFE_ADDRESS={tfe_address}", + "description": "HCP Terraform or Terraform Enterprise base URL.", + "variables": { + "tfe_address": { + "description": "HCP Terraform or Terraform Enterprise base URL.", + "default": "https://app.terraform.io/", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "TFE_TOKEN={tfe_token}", + "description": "HCP Terraform or Terraform Enterprise API token used to authenticate requests.", + "variables": { + "tfe_token": { + "description": "HCP Terraform or Terraform Enterprise API token used to authenticate requests.", + "isRequired": false, + "isSecret": true + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "TFE_SKIP_TLS_VERIFY={tfe_skip_tls_verify}", + "description": "Set to true to disable TLS verification when connecting to HCP Terraform or Terraform Enterprise.", + "variables": { + "tfe_skip_tls_verify": { + "description": "Set to true to disable TLS verification when connecting to HCP Terraform or Terraform Enterprise.", + "default": "false", + "choices": [ + "true", + "false" + ], + "format": "boolean", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "TRANSPORT_MODE={transport_mode}", + "description": "Selects the active transport; use streamable-http to serve HTTP clients.", + "variables": { + "transport_mode": { + "description": "Selects the active transport; use streamable-http to serve HTTP clients.", + "default": "streamable-http", + "choices": [ + "streamable-http" + ], + "isRequired": true + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "TRANSPORT_HOST={transport_host}", + "description": "Interface to bind when running in streamable-http mode.", + "variables": { + "transport_host": { + "description": "Interface to bind when running in streamable-http mode.", + "default": "0.0.0.0", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "TRANSPORT_PORT={transport_port}", + "description": "Port to bind when running in streamable-http mode.", + "variables": { + "transport_port": { + "description": "Port to bind when running in streamable-http mode.", + "default": "8080", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_ENDPOINT={mcp_endpoint}", + "description": "HTTP endpoint path exposed when streamable-http transport is enabled.", + "variables": { + "mcp_endpoint": { + "description": "HTTP endpoint path exposed when streamable-http transport is enabled.", + "default": "/mcp", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_SESSION_MODE={mcp_session_mode}", + "description": "Session management mode for the MCP server.", + "variables": { + "mcp_session_mode": { + "description": "Session management mode for the MCP server.", + "default": "stateful", + "choices": [ + "stateful", + "stateless" + ], + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_ALLOWED_ORIGINS={mcp_allowed_origins}", + "description": "Comma-separated list of allowed origins for CORS when using streamable-http transport.", + "variables": { + "mcp_allowed_origins": { + "description": "Comma-separated list of allowed origins for CORS when using streamable-http transport.", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_CORS_MODE={mcp_cors_mode}", + "description": "CORS enforcement mode; strict enforces allowed origins while development trusts localhost.", + "variables": { + "mcp_cors_mode": { + "description": "CORS enforcement mode; strict enforces allowed origins while development trusts localhost.", + "default": "strict", + "choices": [ + "strict", + "development", + "disabled" + ], + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_TLS_CERT_FILE={mcp_tls_cert_file}", + "description": "Path to the TLS certificate file to enable HTTPS for streamable-http transport.", + "variables": { + "mcp_tls_cert_file": { + "description": "Path to the TLS certificate file to enable HTTPS for streamable-http transport.", + "format": "filepath", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_TLS_KEY_FILE={mcp_tls_key_file}", + "description": "Path to the TLS key file to enable HTTPS for streamable-http transport.", + "variables": { + "mcp_tls_key_file": { + "description": "Path to the TLS key file to enable HTTPS for streamable-http transport.", + "format": "filepath", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_RATE_LIMIT_GLOBAL={mcp_rate_limit_global}", + "description": "Global rate limit in requests-per-second:burst format applied across all sessions.", + "variables": { + "mcp_rate_limit_global": { + "description": "Global rate limit in requests-per-second:burst format applied across all sessions.", + "default": "10:20", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "MCP_RATE_LIMIT_SESSION={mcp_rate_limit_session}", + "description": "Per-session rate limit in requests-per-second:burst format.", + "variables": { + "mcp_rate_limit_session": { + "description": "Per-session rate limit in requests-per-second:burst format.", + "default": "5:10", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "named", + "name": "-e", + "value": "ENABLE_TF_OPERATIONS={enable_tf_operations}", + "description": "Set to true to enable tools that execute Terraform operations requiring explicit approval.", + "variables": { + "enable_tf_operations": { + "description": "Set to true to enable tools that execute Terraform operations requiring explicit approval.", + "default": "false", + "choices": [ + "true", + "false" + ], + "format": "boolean", + "isRequired": false + } + }, + "isRepeated": true + }, + { + "type": "positional", + "value": "hashicorp/terraform-mcp-server:0.3.1" + } + ], + "environmentVariables": [ + { + "name": "TFE_ADDRESS", + "description": "HCP Terraform or Terraform Enterprise base URL.", + "default": "https://app.terraform.io/", + "isRequired": false + }, + { + "name": "TFE_TOKEN", + "description": "HCP Terraform or Terraform Enterprise API token used to authenticate requests.", + "isRequired": false, + "isSecret": true + }, + { + "name": "TFE_SKIP_TLS_VERIFY", + "description": "Set to true to disable TLS verification when connecting to HCP Terraform or Terraform Enterprise.", + "default": "false", + "format": "boolean", + "choices": [ + "true", + "false" + ], + "isRequired": false + }, + { + "name": "TRANSPORT_MODE", + "description": "Selects the active transport; use streamable-http to serve HTTP clients.", + "default": "streamable-http", + "choices": [ + "streamable-http" + ], + "isRequired": true + }, + { + "name": "TRANSPORT_HOST", + "description": "Interface to bind when running in streamable-http mode.", + "default": "0.0.0.0", + "isRequired": false + }, + { + "name": "TRANSPORT_PORT", + "description": "Port to bind when running in streamable-http mode.", + "default": "8080", + "isRequired": false + }, + { + "name": "MCP_ENDPOINT", + "description": "HTTP endpoint path exposed when streamable-http transport is enabled.", + "default": "/mcp", + "isRequired": false + }, + { + "name": "MCP_SESSION_MODE", + "description": "Session management mode for the MCP server.", + "default": "stateful", + "choices": [ + "stateful", + "stateless" + ], + "isRequired": false + }, + { + "name": "MCP_ALLOWED_ORIGINS", + "description": "Comma-separated list of allowed origins for CORS when using streamable-http transport.", + "isRequired": false + }, + { + "name": "MCP_CORS_MODE", + "description": "CORS enforcement mode; strict enforces allowed origins while development trusts localhost.", + "default": "strict", + "choices": [ + "strict", + "development", + "disabled" + ], + "isRequired": false + }, + { + "name": "MCP_TLS_CERT_FILE", + "description": "Path to the TLS certificate file to enable HTTPS for streamable-http transport.", + "format": "filepath", + "isRequired": false + }, + { + "name": "MCP_TLS_KEY_FILE", + "description": "Path to the TLS key file to enable HTTPS for streamable-http transport.", + "format": "filepath", + "isRequired": false + }, + { + "name": "MCP_RATE_LIMIT_GLOBAL", + "description": "Global rate limit in requests-per-second:burst format applied across all sessions.", + "default": "10:20", + "isRequired": false + }, + { + "name": "MCP_RATE_LIMIT_SESSION", + "description": "Per-session rate limit in requests-per-second:burst format.", + "default": "5:10", + "isRequired": false + }, + { + "name": "ENABLE_TF_OPERATIONS", + "description": "Set to true to enable tools that execute Terraform operations requiring explicit approval.", + "default": "false", + "format": "boolean", + "choices": [ + "true", + "false" + ], + "isRequired": false + } + ] + } + ] +} From 9335095460ab5a5a85f0bb3dc089f364487933c6 Mon Sep 17 00:00:00 2001 From: Lee Fowler Date: Wed, 22 Oct 2025 22:41:47 -0400 Subject: [PATCH 03/28] Update `publish-registry` GHA Workflow to use GitHub Username Whitelist (#204) * ci: update publish-registry workflow to use github username whitelist * fix: correct a misspelling in server.json server name namespace stanza --- .github/workflows/publish-registry.yml | 12 +++++++++++- server.json | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-registry.yml b/.github/workflows/publish-registry.yml index 9339dce8..4715d73e 100644 --- a/.github/workflows/publish-registry.yml +++ b/.github/workflows/publish-registry.yml @@ -10,9 +10,19 @@ permissions: jobs: publish: runs-on: ubuntu-latest - environment: publish-registry steps: + - name: Validate triggering actor + env: + ALLOWED_ACTORS: | + gautambaghel + run: | + if ! grep -Fxq "${GITHUB_ACTOR}" <<< "${ALLOWED_ACTORS}"; then + echo "github.actor '${GITHUB_ACTOR}' is not authorized to run this workflow." + exit 1 + fi + echo "Authorized actor: ${GITHUB_ACTOR}" + - name: Checkout repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 diff --git a/server.json b/server.json index 24006835..ac96cc45 100644 --- a/server.json +++ b/server.json @@ -1,6 +1,6 @@ { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", - "name": "io.gitub.hashicorp/terraform-mcp-server", + "name": "io.github.hashicorp/terraform-mcp-server", "title": "Terraform MCP Server", "description": "Generate more accurate Terraform and automate workflows for HCP Terraform and Terraform Enterprise", "websiteUrl": "https://github.com/hashicorp/terraform-mcp-server", From 04496e3b4edd3df2245fac30fa3c9fd3c9c6b09e Mon Sep 17 00:00:00 2001 From: Lee Fowler Date: Wed, 22 Oct 2025 23:14:53 -0400 Subject: [PATCH 04/28] fix: fixes to server.json (#206) --- server.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server.json b/server.json index ac96cc45..3d2df61e 100644 --- a/server.json +++ b/server.json @@ -1,9 +1,8 @@ { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", "name": "io.github.hashicorp/terraform-mcp-server", - "title": "Terraform MCP Server", + "title": "Terraform", "description": "Generate more accurate Terraform and automate workflows for HCP Terraform and Terraform Enterprise", - "websiteUrl": "https://github.com/hashicorp/terraform-mcp-server", "repository": { "url": "https://github.com/hashicorp/terraform-mcp-server", "source": "github" From 8e1e01c37da49bb272b559f8fb6306c4fb222094 Mon Sep 17 00:00:00 2001 From: Gautam Date: Wed, 22 Oct 2025 22:57:32 -0700 Subject: [PATCH 05/28] check for the release branches to make sure we update registry json files (#205) * adding a check for the release branches to make sure we update registry json files * updating scripts * minor fixes * Update .github/workflows/release-checks.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update scripts/compare-versions.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update scripts/update-json-version.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * added actions support * Revert "added actions support" This reverts commit 8c7f5d62bdc9664f519022a5c6197626335ea971. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/publish-registry.yml | 2 +- .github/workflows/release-checks.yml | 27 ++++++++ Makefile | 41 ++++++----- scripts/compare-versions.sh | 81 ++++++++++++++++++++++ scripts/update-json-version.sh | 96 ++++++++++++++++++++++++++ scripts/update-server-json-version.sh | 44 ------------ 6 files changed, 230 insertions(+), 61 deletions(-) create mode 100644 .github/workflows/release-checks.yml create mode 100755 scripts/compare-versions.sh create mode 100755 scripts/update-json-version.sh delete mode 100755 scripts/update-server-json-version.sh diff --git a/.github/workflows/publish-registry.yml b/.github/workflows/publish-registry.yml index 4715d73e..3658b11f 100644 --- a/.github/workflows/publish-registry.yml +++ b/.github/workflows/publish-registry.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: go-version-file: .go-version cache: true diff --git a/.github/workflows/release-checks.yml b/.github/workflows/release-checks.yml new file mode 100644 index 00000000..c59612ba --- /dev/null +++ b/.github/workflows/release-checks.yml @@ -0,0 +1,27 @@ +name: Version Consistency Check + +on: + push: + branches: + - 'release/*' + pull_request: + branches: + - 'release/*' + +jobs: + version-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Install jq + run: | + sudo apt-get update && sudo apt-get install -y jq + + - name: Make version check script executable + run: chmod +x scripts/compare-versions.sh + + - name: Run version consistency check + run: ./scripts/compare-versions.sh diff --git a/Makefile b/Makefile index 463811f7..b794b0c3 100644 --- a/Makefile +++ b/Makefile @@ -64,10 +64,17 @@ docker-run-http: $(DOCKER) run -p 8080:8080 --rm $(BINARY_NAME):$(VERSION) http --transport-port 8080 --transport-host 0.0.0.0 # Synchronise server.json version fields with version/VERSION -update-server-json-version: +update-json-version: @VERSION_FILE="$(CURDIR)/version/VERSION"; \ SERVER_JSON="$(CURDIR)/server.json"; \ - "$(CURDIR)/scripts/update-server-json-version.sh" "$$VERSION_FILE" "$$SERVER_JSON" + "$(CURDIR)/scripts/update-json-version.sh" "$$SERVER_JSON" "$$VERSION_FILE" + + +# Synchronise gemini-extension.json version fields with version/VERSION +update-gemini-version: + @VERSION_FILE="$(CURDIR)/version/VERSION"; \ + SERVER_JSON="$(CURDIR)/gemini-extension.json"; \ + "$(CURDIR)/scripts/update-json-version.sh" "$$SERVER_JSON" "$$VERSION_FILE" # Test HTTP endpoint test-http: @@ -93,18 +100,20 @@ cleanup-test-containers: # Show help help: @echo "Available targets:" - @echo " all - Build the binary (default)" - @echo " build - Build the binary" - @echo " test - Run all tests" - @echo " test-e2e - Run end-to-end tests" - @echo " test-security - Run security-related tests" - @echo " clean - Remove build artifacts" - @echo " deps - Download dependencies" - @echo " docker-build - Build docker image" - @echo " run-http - Run StreamableHTTP server locally on port 8080" - @echo " run-http-secure - Run StreamableHTTP server with security settings" - @echo " docker-run-http - Run StreamableHTTP server in Docker on port 8080" - @echo " test-http - Test StreamableHTTP health endpoint" + @echo " all - Build the binary (default)" + @echo " build - Build the binary" + @echo " crt-build - Build using crt-build script" + @echo " test - Run all tests" + @echo " test-e2e - Run end-to-end tests" + @echo " test-security - Run security-related tests" + @echo " test-http - Test StreamableHTTP health endpoint" + @echo " clean - Remove build artifacts" + @echo " deps - Download dependencies" + @echo " docker-build - Build docker image" + @echo " run-http - Run StreamableHTTP server locally on port 8080" + @echo " run-http-secure - Run StreamableHTTP server with security settings" + @echo " docker-run-http - Run StreamableHTTP server in Docker on port 8080" + @echo " update-json-version - Update server.json to match version/VERSION" + @echo " update-gemini-version - Update gemini-extension.json to match version/VERSION" @echo " cleanup-test-containers - Stop and remove all test containers" - @echo " update-server-json-version - Update server.json to match version/VERSION" - @echo " help - Show this help message" + @echo " help - Show this help message" diff --git a/scripts/compare-versions.sh b/scripts/compare-versions.sh new file mode 100755 index 00000000..0aaf6f6a --- /dev/null +++ b/scripts/compare-versions.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Script to compare version numbers using version/VERSION as source of truth +set -euo pipefail +echo "Checking version consistency using version/VERSION as source of truth..." + +# Read version from version/VERSION file (source of truth) +if [ -f "version/VERSION" ]; then + SOURCE_VERSION=$(tr -d '\n\r\t ' < version/VERSION) + echo "" + echo "Source version (version/VERSION): '$SOURCE_VERSION'" +else + echo "" + echo "Error: version/VERSION file not found" + exit 1 +fi + +# Configurable list of JSON files to check +JSON_FILES=("gemini-extension.json" "server.json") + +VERSION_MISMATCH=false + +# Check version field in each JSON file +for json_file in "${JSON_FILES[@]}"; do + if [ -f "$json_file" ]; then + JSON_VERSION=$(jq -r '.version' "$json_file" | tr -d '\n\r\t ') + echo "Version in $json_file: '$JSON_VERSION'" + echo "" + + if [ "$SOURCE_VERSION" != "$JSON_VERSION" ]; then + echo "❌ Version mismatch: $json_file ($JSON_VERSION) should match version/VERSION ($SOURCE_VERSION)" + VERSION_MISMATCH=true + else + echo "✅ $json_file version matches" + fi + else + echo "Warning: $json_file file not found" + fi +done + +# Check terraform-mcp-server: occurrences in JSON files only +echo "Checking terraform-mcp-server: occurrences in JSON files..." +echo "" +DOCKER_PATTERN="terraform-mcp-server:" + +for json_file in "${JSON_FILES[@]}"; do + if [ -f "$json_file" ]; then + FOUND_DOCKER_VERSIONS=$(grep -o "${DOCKER_PATTERN}[^\"[:space:]]*" "$json_file" 2>/dev/null | sort -u || true) + + if [ -n "$FOUND_DOCKER_VERSIONS" ]; then + echo "Found terraform-mcp-server references in $json_file:" + echo "$FOUND_DOCKER_VERSIONS" + echo "" + + # Check each found version + while IFS= read -r docker_ref; do + if [ -n "$docker_ref" ]; then + DOCKER_VERSION=$(echo "$docker_ref" | sed "s/${DOCKER_PATTERN}//") + # Skip if it's just the pattern without a version, or if it contains variables/placeholders + if [ -n "$DOCKER_VERSION" ] && [[ ! "$DOCKER_VERSION" =~ [\$\{] ]] && [ "$DOCKER_VERSION" != "latest" ]; then + echo "Found Docker reference version in $json_file: '$DOCKER_VERSION'" + if [ "$SOURCE_VERSION" != "$DOCKER_VERSION" ]; then + echo "❌ Version mismatch in $json_file: terraform-mcp-server:$DOCKER_VERSION should be terraform-mcp-server:$SOURCE_VERSION" + VERSION_MISMATCH=true + fi + fi + fi + done <<< "$FOUND_DOCKER_VERSIONS" + fi + fi +done + +if [ "$VERSION_MISMATCH" = true ]; then + echo "" + echo "Please run scripts/update-json-version.sh before merging into release" + exit 1 +else + echo "" + echo "✅ All files match the source version: $SOURCE_VERSION" +fi + +echo "Version comparison completed successfully." diff --git a/scripts/update-json-version.sh b/scripts/update-json-version.sh new file mode 100755 index 00000000..2dc55286 --- /dev/null +++ b/scripts/update-json-version.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Script to update version references in JSON files +# Usage: ./update-json-version.sh [version-file-path] +# +# This script updates: +# 1. "version": "" fields +# 2. terraform-mcp-server: references +# +# Arguments: +# json-file-path: Path to the JSON file to update +# version-file-path: Path to the VERSION file (default: version/VERSION) + +JSON_FILE="${1:-}" +VERSION_FILE="${2:-version/VERSION}" + +# Function to display usage +usage() { + echo "Usage: $0 [version-file-path]" + echo "" + echo "Updates version references in JSON files based on VERSION file content." + echo "" + echo "Arguments:" + echo " json-file-path Path to the JSON file to update (required)" + echo " version-file-path Path to the VERSION file (default: version/VERSION)" + echo "" + echo "Examples:" + echo " $0 server.json" + echo " $0 gemini-extension.json version/VERSION" + echo "" + exit 1 +} + +# Check if JSON file argument is provided +if [[ -z "$JSON_FILE" ]]; then + echo "Error: JSON file path is required" + echo "" + usage +fi + +# Check if JSON file exists +if [[ ! -f "$JSON_FILE" ]]; then + echo "Error: JSON file '$JSON_FILE' does not exist" + exit 1 +fi + +# Check if VERSION file exists +if [[ ! -f "$VERSION_FILE" ]]; then + echo "Error: VERSION file '$VERSION_FILE' does not exist" + exit 1 +fi + +# Read the version from the VERSION file and trim whitespace +NEW_VERSION=$(tr -d '[:space:]' < "$VERSION_FILE") + +if [[ -z "$NEW_VERSION" ]]; then + echo "Error: VERSION file '$VERSION_FILE' is empty" + exit 1 +fi + +echo "Updating JSON file: $JSON_FILE" +echo "Using version: $NEW_VERSION" +echo "Version source: $VERSION_FILE" + +# Create a backup of the original file +BACKUP_FILE="${JSON_FILE}.backup.$(date +%Y%m%d_%H%M%S)" +cp "$JSON_FILE" "$BACKUP_FILE" +echo "Created backup: $BACKUP_FILE" + +# Use sed to update version references +# Patterns updated: +# 1. "version": "" -> "version": "" +# 2. hashicorp/terraform-mcp-server: -> hashicorp/terraform-mcp-server: +# 3. docker.io/hashicorp/terraform-mcp-server: -> docker.io/hashicorp/terraform-mcp-server: + +# For macOS compatibility, we'll use a temporary file approach +TEMP_FILE=$(mktemp) + +# Update version field and terraform-mcp-server references +sed -E \ + -e 's/"version": *"[^"]*"/"version": "'"$NEW_VERSION"'"/g' \ + -e 's/(^|[^a-zA-Z0-9.-])terraform-mcp-server:[^"[:space:]]*/\1terraform-mcp-server:'"$NEW_VERSION"'/g' \ + -e 's/(docker\.io\/)?hashicorp\/terraform-mcp-server:[^"[:space:]]*/\1hashicorp\/terraform-mcp-server:'"$NEW_VERSION"'/g' \ + "$JSON_FILE" > "$TEMP_FILE" + +# Move the temporary file back to the original +mv "$TEMP_FILE" "$JSON_FILE" + +echo "Successfully updated version references in $JSON_FILE" +echo "Changed version references to: $NEW_VERSION" + +# Show what was changed (optional verification) +echo "" +echo "Updated references found:" +grep -E '"version": *"[^"]*"|(docker\.io\/)?hashicorp\/terraform-mcp-server:[^"[:space:]]*' "$JSON_FILE" | sed 's/^/ /' diff --git a/scripts/update-server-json-version.sh b/scripts/update-server-json-version.sh deleted file mode 100755 index bda8044b..00000000 --- a/scripts/update-server-json-version.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -VERSION_FILE="${1:-version/VERSION}" -SERVER_JSON="${2:-server.json}" - -if [[ ! -f "${VERSION_FILE}" ]]; then - echo "Version file not found: ${VERSION_FILE}" >&2 - exit 1 -fi - -if [[ ! -f "${SERVER_JSON}" ]]; then - echo "server.json not found: ${SERVER_JSON}" >&2 - exit 1 -fi - -VERSION_VALUE="$(<"${VERSION_FILE}")" - -if [[ -z "${VERSION_VALUE}" ]]; then - echo "Version file ${VERSION_FILE} is empty" >&2 - exit 1 -fi - -tmp_file="$(mktemp)" -trap 'rm -f "${tmp_file}"' EXIT - -jq --arg version "${VERSION_VALUE}" ' - .version = $version - | .packages = (.packages // []) - | (.packages[] |= ( - (if has("identifier") and (.identifier | type) == "string" then - .identifier = (.identifier | gsub("hashicorp/terraform-mcp-server:[^[:space:]]+"; "hashicorp/terraform-mcp-server:" + $version)) - else . end) - | .runtimeArguments = (.runtimeArguments // []) - | (.runtimeArguments[] |= ( - if has("value") and (.value | type) == "string" then - .value = (.value | gsub("hashicorp/terraform-mcp-server:[^[:space:]]+"; "hashicorp/terraform-mcp-server:" + $version)) - else . - end - )) - )) -' "${SERVER_JSON}" > "${tmp_file}" - -mv "${tmp_file}" "${SERVER_JSON}" From bdfd7e9a5940e096c8c9ca924411cd320b2163da Mon Sep 17 00:00:00 2001 From: Gautam Date: Thu, 23 Oct 2025 20:14:50 -0700 Subject: [PATCH 06/28] Feature/no-code-modules (#207) * basic elicitation working * elicitation working * adding changelog * fixing tests * fixing copilot feedback * accepting copilot suggestions * accepting copilot suggestions * removing unnecessary prints * Update pkg/tools/tfe/create_no_code_workspace.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * code cleanup * addressing 2 review comments --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 4 +- cmd/terraform-mcp-server/main.go | 1 + pkg/client/types.go | 22 + pkg/tools/dynamic_tool.go | 30 +- pkg/tools/tfe/create_no_code_workspace.go | 382 ++++++++++++++++++ .../tfe/create_no_code_workspace_test.go | 49 +++ pkg/tools/tfe/search_private_modules.go | 6 + pkg/utils/utils.go | 24 ++ 8 files changed, 508 insertions(+), 10 deletions(-) create mode 100644 pkg/tools/tfe/create_no_code_workspace.go create mode 100644 pkg/tools/tfe/create_no_code_workspace_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index acb3371f..3018a31a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ FEATURES -* Adding provider capability discovery tool to analyze available resources, data sources, functions, guides, and actions +* [New Tool] `get_provider_capabilities` Adding provider capability discovery tool to analyze available resources, data sources, functions, guides, and actions + +* [New Tool] `create_no_code_workspace` Adding capability to trigger a workspace run using a no code module FIXES diff --git a/cmd/terraform-mcp-server/main.go b/cmd/terraform-mcp-server/main.go index 1674accf..e913d7d5 100644 --- a/cmd/terraform-mcp-server/main.go +++ b/cmd/terraform-mcp-server/main.go @@ -55,6 +55,7 @@ func NewServer(version string, logger *log.Logger, opts ...server.ServerOption) server.WithResourceCapabilities(true, true), server.WithInstructions(instructions), server.WithToolHandlerMiddleware(rateLimitMiddleware.Middleware()), + server.WithElicitation(), } opts = append(defaultOpts, opts...) diff --git a/pkg/client/types.go b/pkg/client/types.go index de87145f..491d717f 100644 --- a/pkg/client/types.go +++ b/pkg/client/types.go @@ -446,3 +446,25 @@ type WorkspaceToolResponse struct { Variables []*tfe.Variable `jsonapi:"polyrelation,variables,omitempty"` Readme string `jsonapi:"attr,readme,omitempty"` } + +type ModuleMetadata struct { + Data struct { + Type string `json:"type"` + ID string `json:"id"` + Attributes struct { + GitRefTag string `json:"git-ref-tag"` + GitRepoURL string `json:"git-repo-url"` + InputVariables []struct { + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + Required bool `json:"required"` + Sensitive bool `json:"sensitive"` + } `json:"input-variables"` + Name string `json:"name"` + SourceURL string `json:"source-url"` + Version string `json:"version"` + NoCode bool `json:"no-code"` + } `json:"attributes"` + } `json:"data"` +} diff --git a/pkg/tools/dynamic_tool.go b/pkg/tools/dynamic_tool.go index a5d67ad1..59f6c77a 100644 --- a/pkg/tools/dynamic_tool.go +++ b/pkg/tools/dynamic_tool.go @@ -168,6 +168,9 @@ func (r *DynamicToolRegistry) registerTFETools() { r.mcpServer.AddTool(actionRunTool.Tool, actionRunTool.Handler) } + createNoCodeWorkspace := r.createDynamicTFEToolWithElicitation("create_no_code_workspace", tfeTools.CreateNoCodeWorkspace) + r.mcpServer.AddTool(createNoCodeWorkspace.Tool, createNoCodeWorkspace.Handler) + getRunDetailsTool := r.createDynamicTFETool("get_run_details", tfeTools.GetRunDetails) r.mcpServer.AddTool(getRunDetailsTool.Tool, getRunDetailsTool.Handler) @@ -201,16 +204,30 @@ func (r *DynamicToolRegistry) registerTFETools() { updateWorkspaceVariableTool := r.createDynamicTFETool("update_workspace_variable", tfeTools.UpdateWorkspaceVariable) r.mcpServer.AddTool(updateWorkspaceVariableTool.Tool, updateWorkspaceVariableTool.Handler) - r.tfeToolsRegistered = true } // createDynamicTFETool creates a TFE tool with dynamic availability checking func (r *DynamicToolRegistry) createDynamicTFETool(toolName string, toolFactory func(*log.Logger) server.ServerTool) server.ServerTool { originalTool := toolFactory(r.logger) + return server.ServerTool{ + Tool: originalTool.Tool, + Handler: r.wrapWithAvailabilityCheck(toolName, originalTool.Handler), + } +} - // Wrap the handler with dynamic availability checking - wrappedHandler := func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { +// createDynamicTFEToolWithElicitation creates a TFE tool with dynamic availability checking that also needs MCPServer for elicitation +func (r *DynamicToolRegistry) createDynamicTFEToolWithElicitation(toolName string, toolFactory func(*log.Logger, *server.MCPServer) server.ServerTool) server.ServerTool { + originalTool := toolFactory(r.logger, r.mcpServer) + return server.ServerTool{ + Tool: originalTool.Tool, + Handler: r.wrapWithAvailabilityCheck(toolName, originalTool.Handler), + } +} + +// wrapWithAvailabilityCheck wraps a tool handler with dynamic TFE availability checking +func (r *DynamicToolRegistry) wrapWithAvailabilityCheck(toolName string, originalHandler server.ToolHandlerFunc) server.ToolHandlerFunc { + return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Get session from context session := server.ClientSessionFromContext(ctx) if session == nil { @@ -235,11 +252,6 @@ func (r *DynamicToolRegistry) createDynamicTFETool(toolName string, toolFactory } // Tool is available, proceed with original handler - return originalTool.Handler(ctx, req) - } - - return server.ServerTool{ - Tool: originalTool.Tool, - Handler: wrappedHandler, + return originalHandler(ctx, req) } } diff --git a/pkg/tools/tfe/create_no_code_workspace.go b/pkg/tools/tfe/create_no_code_workspace.go new file mode 100644 index 00000000..cadf5e41 --- /dev/null +++ b/pkg/tools/tfe/create_no_code_workspace.go @@ -0,0 +1,382 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tools + +import ( + "context" + "encoding/json" + "fmt" + "path" + "strconv" + "strings" + + "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-mcp-server/pkg/client" + "github.com/hashicorp/terraform-mcp-server/pkg/utils" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + log "github.com/sirupsen/logrus" +) + +// CreateNoCodeWorkspace creates a tool to create a No Code module workspace. +func CreateNoCodeWorkspace(logger *log.Logger, mcpServer *server.MCPServer) server.ServerTool { + return server.ServerTool{ + Tool: mcp.NewTool("create_no_code_workspace", + mcp.WithDescription(`Creates a new Terraform No Code module workspace. The tool uses the MCP elicitation feature to automatically discover and collect required variables from the user.`), + mcp.WithTitleAnnotation("Create a No Code module workspace"), + mcp.WithOpenWorldHintAnnotation(true), + mcp.WithReadOnlyHintAnnotation(false), + mcp.WithDestructiveHintAnnotation(true), + mcp.WithString("no_code_module_id", + mcp.Required(), + mcp.Description("The ID of the No Code module to create a workspace for"), + ), + mcp.WithString("workspace_name", + mcp.Required(), + mcp.Description("The name of the workspace to create"), + ), + mcp.WithString("project_id", + mcp.Required(), + mcp.Description("The ID of the project to use"), + ), + mcp.WithBoolean("auto_apply", + mcp.Description("Whether to automatically apply changes in the workspace: 'true' or 'false'"), + mcp.DefaultBool(false), + ), + ), + Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return createNoCodeWorkspaceHandler(ctx, req, logger, mcpServer) + }, + } +} + +func createNoCodeWorkspaceHandler(ctx context.Context, request mcp.CallToolRequest, logger *log.Logger, mcpServer *server.MCPServer) (*mcp.CallToolResult, error) { + tfeClient, err := client.GetTfeClientFromContext(ctx, logger) + if err != nil { + return nil, utils.LogAndReturnError(logger, "getting Terraform client", err) + } + if tfeClient == nil { + return nil, utils.LogAndReturnError(logger, "getting Terraform client - please ensure TFE_TOKEN and TFE_ADDRESS are properly configured", nil) + } + + params, err := extractRequestParams(request, logger) + if err != nil { + return nil, err + } + + if !strings.HasPrefix(params.noCodeModuleID, "nocode-") { + return nil, utils.LogAndReturnError(logger, "no_code_module_id must start with 'nocode-'", nil) + } + + project, noCodeModule, moduleMetadata, err := fetchModuleData(ctx, tfeClient, params.projectID, params.noCodeModuleID, logger) + if err != nil { + return nil, err + } + + elicitationProperties, requestedVars := buildElicitationSchema(moduleMetadata, noCodeModule) + + result, err := requestVariableValues(ctx, mcpServer, params.noCodeModuleID, elicitationProperties, requestedVars, logger) + if err != nil { + return nil, err + } + + variables, err := processElicitationResponse(result, requestedVars, elicitationProperties, logger) + if err != nil { + return nil, err + } + + workspace, err := tfeClient.RegistryNoCodeModules.CreateWorkspace(ctx, params.noCodeModuleID, &tfe.RegistryNoCodeModuleCreateWorkspaceOptions{ + Name: params.workspaceName, + Project: project, + Variables: variables, + AutoApply: ¶ms.autoApply, + }) + if err != nil { + return nil, utils.LogAndReturnError(logger, "creating No Code module workspace", err) + } + + logger.Infof("Created No Code module workspace: %s", workspace.ID) + buf, err := getWorkspaceDetailsForTools(ctx, "create_no_code_workspace", tfeClient, workspace, logger) + if err != nil { + return nil, utils.LogAndReturnError(logger, "getting workspace details for tools", err) + } + + return mcp.NewToolResultText(buf.String()), nil +} + +type workspaceParams struct { + noCodeModuleID string + workspaceName string + projectID string + autoApply bool +} + +func extractRequestParams(request mcp.CallToolRequest, logger *log.Logger) (*workspaceParams, error) { + noCodeModuleID, err := request.RequireString("no_code_module_id") + if err != nil { + return nil, utils.LogAndReturnError(logger, "the 'no_code_module_id' parameter is required", err) + } + + workspaceName, err := request.RequireString("workspace_name") + if err != nil { + return nil, utils.LogAndReturnError(logger, "the 'workspace_name' parameter is required", err) + } + + projectID, err := request.RequireString("project_id") + if err != nil { + return nil, utils.LogAndReturnError(logger, "the 'project_id' parameter is required", err) + } + + return &workspaceParams{ + noCodeModuleID: noCodeModuleID, + workspaceName: workspaceName, + projectID: projectID, + autoApply: request.GetBool("auto_apply", false), + }, nil +} + +func fetchModuleData(ctx context.Context, tfeClient *tfe.Client, projectID, noCodeModuleID string, logger *log.Logger) (*tfe.Project, *tfe.RegistryNoCodeModule, *client.ModuleMetadata, error) { + project, err := tfeClient.Projects.Read(ctx, projectID) + if err != nil { + return nil, nil, nil, utils.LogAndReturnError(logger, "reading project", err) + } + + noCodeModule, err := tfeClient.RegistryNoCodeModules.Read(ctx, noCodeModuleID, &tfe.RegistryNoCodeModuleReadOptions{ + Include: []tfe.RegistryNoCodeModuleIncludeOpt{tfe.RegistryNoCodeIncludeVariableOptions}, + }) + if err != nil { + return nil, nil, nil, utils.LogAndReturnError(logger, "reading No Code module", err) + } + + registryModule, err := tfeClient.RegistryModules.Read(ctx, tfe.RegistryModuleID{ID: noCodeModule.RegistryModule.ID}) + if err != nil { + return nil, nil, nil, utils.LogAndReturnError(logger, "reading Registry module", err) + } + + metadataPath := path.Join("/api/registry/private/v2/modules", registryModule.Namespace, registryModule.Name, registryModule.Provider, "metadata", noCodeModule.VersionPin) + metadataData, err := utils.MakeCustomGetRequestRaw(ctx, tfeClient, metadataPath, map[string][]string{"organization_name": {noCodeModule.Organization.Name}}) + if err != nil { + return nil, nil, nil, utils.LogAndReturnError(logger, "making module metadata API request", err) + } + + var moduleMetadata client.ModuleMetadata + if err := json.Unmarshal(metadataData, &moduleMetadata); err != nil { + return nil, nil, nil, utils.LogAndReturnError(logger, "unmarshalling module metadata", err) + } + + return project, noCodeModule, &moduleMetadata, nil +} + +func buildElicitationSchema(moduleMetadata *client.ModuleMetadata, noCodeModule *tfe.RegistryNoCodeModule) (map[string]any, []string) { + elicitationProperties := make(map[string]any) + requestedVars := make([]string, 0, len(moduleMetadata.Data.Attributes.InputVariables)) + + for _, inputVar := range moduleMetadata.Data.Attributes.InputVariables { + property := buildPropertySchema(inputVar, noCodeModule) + elicitationProperties[inputVar.Name] = property + requestedVars = append(requestedVars, inputVar.Name) + } + + return elicitationProperties, requestedVars +} + +func buildPropertySchema(inputVar struct { + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + Required bool `json:"required"` + Sensitive bool `json:"sensitive"` +}, noCodeModule *tfe.RegistryNoCodeModule) map[string]any { + property := map[string]any{ + "title": inputVar.Name, + "description": inputVar.Description, + "type": mapTerraformTypeToJSON(inputVar.Type), + } + + if enumOptions := findEnumOptions(inputVar.Name, inputVar.Type, noCodeModule.VariableOptions); enumOptions != nil { + property["enum"] = enumOptions + } + + return property +} + +func mapTerraformTypeToJSON(tfType string) string { + switch tfType { + case "string": + return "string" + case "number": + return "number" + case "bool": + return "boolean" + default: + return "string" + } +} + +func findEnumOptions(varName, varType string, variableOptions []*tfe.NoCodeVariableOption) any { + for _, varOpt := range variableOptions { + if varOpt.VariableName != varName || len(varOpt.Options) == 0 { + continue + } + + switch varType { + case "number": + return convertToFloatEnum(varOpt.Options) + case "bool": + return convertToBoolEnum(varOpt.Options) + default: + return varOpt.Options + } + } + return nil +} + +func convertToFloatEnum(options []string) []float64 { + result := make([]float64, 0, len(options)) + for _, opt := range options { + if floatVal, err := strconv.ParseFloat(opt, 64); err == nil { + result = append(result, floatVal) + } + } + if len(result) > 0 { + return result + } + return nil +} + +func convertToBoolEnum(options []string) []bool { + result := make([]bool, 0, len(options)) + for _, opt := range options { + if boolVal, err := strconv.ParseBool(opt); err == nil { + result = append(result, boolVal) + } + } + if len(result) > 0 { + return result + } + return nil +} + +func requestVariableValues(ctx context.Context, mcpServer *server.MCPServer, moduleID string, properties map[string]any, required []string, logger *log.Logger) (*mcp.ElicitationResult, error) { + request := mcp.ElicitationRequest{ + Params: mcp.ElicitationParams{ + Message: fmt.Sprintf("The No Code module '%s' requires %d variable(s) to create the workspace. Please provide values for the required variables.", moduleID, len(required)), + RequestedSchema: map[string]any{ + "type": "object", + "properties": properties, + "required": required, + }, + }, + } + + result, err := mcpServer.RequestElicitation(ctx, request) + if err != nil { + return nil, utils.LogAndReturnError(logger, "failed to request elicitation", err) + } + + return result, nil +} + +func processElicitationResponse(result *mcp.ElicitationResult, requestedVars []string, elicitationProperties map[string]any, logger *log.Logger) ([]*tfe.Variable, error) { + switch result.Action { + case mcp.ElicitationResponseActionDecline: + return nil, utils.LogAndReturnError(logger, "No Code module workspace creation declined by user", nil) + case mcp.ElicitationResponseActionCancel: + return nil, utils.LogAndReturnError(logger, "No Code module workspace creation cancelled by user", nil) + case mcp.ElicitationResponseActionAccept: + return extractVariablesFromResponse(result.Content, requestedVars, elicitationProperties, logger) + default: + return nil, utils.LogAndReturnError(logger, fmt.Sprintf("unexpected elicitation response action: %s", result.Action), nil) + } +} + +func extractVariablesFromResponse(content any, requestedVars []string, elicitationProperties map[string]any, logger *log.Logger) ([]*tfe.Variable, error) { + data, ok := content.(map[string]any) + if !ok { + return nil, utils.LogAndReturnError(logger, "elicitation response content is not a map", fmt.Errorf("expected map[string]any, got %T", content)) + } + + variables := make([]*tfe.Variable, 0, len(requestedVars)) + for _, varName := range requestedVars { + variable, err := createVariable(varName, data, elicitationProperties, logger) + if err != nil { + return nil, err + } + variables = append(variables, variable) + } + + return variables, nil +} + +func createVariable(varName string, data map[string]any, elicitationProperties map[string]any, logger *log.Logger) (*tfe.Variable, error) { + valueRaw, exists := data[varName] + if !exists { + return nil, utils.LogAndReturnError(logger, fmt.Sprintf("required variable '%s' is missing from elicitation response", varName), nil) + } + + propertyDef, ok := elicitationProperties[varName].(map[string]any) + if !ok { + return nil, utils.LogAndReturnError(logger, fmt.Sprintf("invalid property definition for variable '%s'", varName), nil) + } + + varType, _ := propertyDef["type"].(string) + if varType == "" { + varType = "string" + } + + value, err := convertVariableValue(varName, varType, valueRaw, logger) + if err != nil { + return nil, err + } + + return &tfe.Variable{ + Key: varName, + Value: value, + Category: tfe.CategoryTerraform, + }, nil +} + +func convertVariableValue(varName, varType string, valueRaw any, logger *log.Logger) (string, error) { + switch varType { + case "string": + strValue, ok := valueRaw.(string) + if !ok { + return "", utils.LogAndReturnError(logger, fmt.Sprintf("variable '%s' must be a string", varName), fmt.Errorf("got %T", valueRaw)) + } + if strValue == "" { + return "", utils.LogAndReturnError(logger, fmt.Sprintf("variable '%s' cannot be empty", varName), nil) + } + return strValue, nil + + case "number": + return convertNumberValue(varName, valueRaw, logger) + + case "boolean": + boolValue, ok := valueRaw.(bool) + if !ok { + return "", utils.LogAndReturnError(logger, fmt.Sprintf("variable '%s' must be a boolean", varName), fmt.Errorf("got %T", valueRaw)) + } + return fmt.Sprintf("%t", boolValue), nil + + default: + jsonValue, err := json.Marshal(valueRaw) + if err != nil { + return "", utils.LogAndReturnError(logger, fmt.Sprintf("failed to marshal variable '%s'", varName), err) + } + return string(jsonValue), nil + } +} + +func convertNumberValue(varName string, valueRaw any, logger *log.Logger) (string, error) { + switch v := valueRaw.(type) { + case float64: + return fmt.Sprintf("%v", v), nil + case int: + return fmt.Sprintf("%d", v), nil + case string: + return v, nil + default: + return "", utils.LogAndReturnError(logger, fmt.Sprintf("variable '%s' must be a number", varName), fmt.Errorf("got %T", valueRaw)) + } +} diff --git a/pkg/tools/tfe/create_no_code_workspace_test.go b/pkg/tools/tfe/create_no_code_workspace_test.go new file mode 100644 index 00000000..ecde161c --- /dev/null +++ b/pkg/tools/tfe/create_no_code_workspace_test.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tools + +import ( + "testing" + + "github.com/mark3labs/mcp-go/server" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestCreateNoCodeWorkspace(t *testing.T) { + logger := logrus.New() + logger.SetLevel(logrus.ErrorLevel) + + // Create a mock MCP server for testing + mcpServer := &server.MCPServer{} + + t.Run("tool creation", func(t *testing.T) { + tool := CreateNoCodeWorkspace(logger, mcpServer) + + // Check that the tool is properly configured + assert.Equal(t, "create_no_code_workspace", tool.Tool.Name) + assert.Contains(t, tool.Tool.Description, "Creates a new Terraform No Code module workspace") + + // Check required parameters + assert.Contains(t, tool.Tool.InputSchema.Required, "no_code_module_id") + assert.Contains(t, tool.Tool.InputSchema.Required, "workspace_name") + + // Check that it accepts open world parameters (for dynamic variables) + // The tool should be configured to accept additional parameters beyond those defined + assert.NotNil(t, tool.Tool.InputSchema.Properties) + assert.Contains(t, tool.Tool.InputSchema.Properties, "no_code_module_id") + assert.Contains(t, tool.Tool.InputSchema.Properties, "workspace_name") + assert.Contains(t, tool.Tool.InputSchema.Properties, "auto_apply") + + // Verify the tool has elicitation capabilities through its configuration + // The WithOpenWorldHintAnnotation(true) allows for dynamic parameter acceptance + annotations := tool.Tool.Annotations + assert.NotNil(t, annotations) + assert.NotNil(t, annotations.OpenWorldHint) + assert.True(t, *annotations.OpenWorldHint) + + // Handler should not be nil + assert.NotNil(t, tool.Handler) + }) +} diff --git a/pkg/tools/tfe/search_private_modules.go b/pkg/tools/tfe/search_private_modules.go index 1babaf56..3247e3f5 100644 --- a/pkg/tools/tfe/search_private_modules.go +++ b/pkg/tools/tfe/search_private_modules.go @@ -139,6 +139,12 @@ func searchPrivateModulesHandler(ctx context.Context, request mcp.CallToolReques builder.WriteString(fmt.Sprintf(" Provider: %s\n", module.Provider)) builder.WriteString(fmt.Sprintf(" No Code Module: %t\n", module.NoCode)) + if module.NoCode { + for _, noCodeModule := range module.RegistryNoCodeModule { + builder.WriteString(fmt.Sprintf(" - no_code_module_id: %s\n", noCodeModule.ID)) + } + } + builder.WriteString("\n") } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 18a8a9dd..9389be37 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -4,12 +4,15 @@ package utils import ( + "context" "fmt" + "io" "os" "regexp" "slices" "strings" + "github.com/hashicorp/go-tfe" log "github.com/sirupsen/logrus" ) @@ -111,3 +114,24 @@ func GetEnv(key, fallback string) string { } return fallback } + +// This function is used for custom GET requests using the TFE client. +func MakeCustomGetRequestRaw(ctx context.Context, client *tfe.Client, path string, additionalQueryParams map[string][]string) ([]byte, error) { + req, err := client.NewRequestWithAdditionalQueryParams("GET", path, nil, additionalQueryParams) + if err != nil { + return nil, err + } + + respBody, err := req.DoRaw(ctx) + if err != nil { + return nil, err + } + defer respBody.Close() + + body, err := io.ReadAll(respBody) + if err != nil { + return nil, err + } + + return body, nil +} From 9696fa398403b5ed25089aaf23d5ad15e5bc87f8 Mon Sep 17 00:00:00 2001 From: Gautam Date: Thu, 23 Oct 2025 20:18:09 -0700 Subject: [PATCH 07/28] 0.3.2 release prep (#208) --- .gitignore | 2 ++ CHANGELOG.md | 2 +- gemini-extension.json | 4 ++-- server.json | 12 ++++++------ version/VERSION | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 50ec4b29..57e23dc0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ cmd/mcpcurl/mcpcurl .vscode/ dist/ bin/* +server.json.backup* +gemini-extension.json.backup* diff --git a/CHANGELOG.md b/CHANGELOG.md index 3018a31a..8aedd233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased +## 0.3.2 (Oct 23, 2025) FEATURES diff --git a/gemini-extension.json b/gemini-extension.json index 99f981af..2c3d072f 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "terraform", - "version": "0.3.1", + "version": "0.3.2", "contextFileName": "${extensionPath}${/}instructions${/}example-AGENTS.md", "mcpServers": { "terraform": { @@ -13,7 +13,7 @@ "TFE_TOKEN", "-e", "TFE_ADDRESS", - "hashicorp/terraform-mcp-server:0.3.1" + "hashicorp/terraform-mcp-server:0.3.2" ], "env": { "TFE_TOKEN": "$TFE_TOKEN", diff --git a/server.json b/server.json index 3d2df61e..3d2a9522 100644 --- a/server.json +++ b/server.json @@ -7,11 +7,11 @@ "url": "https://github.com/hashicorp/terraform-mcp-server", "source": "github" }, - "version": "0.3.1", + "version": "0.3.2", "packages": [ { "registryType": "oci", - "identifier": "docker.io/hashicorp/terraform-mcp-server:0.3.1", + "identifier": "docker.io/hashicorp/terraform-mcp-server:0.3.2", "transport": { "type": "stdio" }, @@ -262,7 +262,7 @@ }, { "type": "positional", - "value": "hashicorp/terraform-mcp-server:0.3.1" + "value": "hashicorp/terraform-mcp-server:0.3.2" } ], "environmentVariables": [ @@ -382,8 +382,8 @@ { "registryType": "oci", "registryBaseUrl": "https://docker.io/", - "identifier": "docker.io/hashicorp/terraform-mcp-server:0.3.1", - "version": "0.3.1", + "identifier": "docker.io/hashicorp/terraform-mcp-server:0.3.2", + "version": "0.3.2", "transport": { "type": "streamable-http", "url": "http://127.0.0.1:8080/mcp" @@ -640,7 +640,7 @@ }, { "type": "positional", - "value": "hashicorp/terraform-mcp-server:0.3.1" + "value": "hashicorp/terraform-mcp-server:0.3.2" } ], "environmentVariables": [ diff --git a/version/VERSION b/version/VERSION index 1d0ba9ea..d15723fb 100644 --- a/version/VERSION +++ b/version/VERSION @@ -1 +1 @@ -0.4.0 +0.3.2 From b8adf42852b22172806939540b3dc0c2a5c602f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:21:56 -0800 Subject: [PATCH 08/28] Bump actions/upload-artifact from 4.6.2 to 5.0.0 (#211) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 5.0.0. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...330a01c490aca151604b8cf639adc76d48f6c5d4) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b7624bb8..9e11f345 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,7 +85,7 @@ jobs: version: ${{ needs.set-product-version.outputs.product-version }} product: ${{ env.PKG_NAME }} repositoryOwner: "hashicorp" - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: metadata.json path: ${{ steps.generate-metadata-file.outputs.filepath }} From baa0b8e44670ea4007a11639570b1358b832a918 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:22:14 -0800 Subject: [PATCH 09/28] Bump github.com/mark3labs/mcp-go from 0.42.0 to 0.43.0 (#212) Bumps [github.com/mark3labs/mcp-go](https://github.com/mark3labs/mcp-go) from 0.42.0 to 0.43.0. - [Release notes](https://github.com/mark3labs/mcp-go/releases) - [Commits](https://github.com/mark3labs/mcp-go/compare/v0.42.0...v0.43.0) --- updated-dependencies: - dependency-name: github.com/mark3labs/mcp-go dependency-version: 0.43.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6a65bb2a..a978e450 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/go-tfe v1.95.0 github.com/hashicorp/jsonapi v1.5.0 - github.com/mark3labs/mcp-go v0.42.0 + github.com/mark3labs/mcp-go v0.43.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.10.1 github.com/spf13/viper v1.21.0 diff --git a/go.sum b/go.sum index 8f31f3e3..807b1799 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mark3labs/mcp-go v0.42.0 h1:gk/8nYJh8t3yroCAOBhNbYsM9TCKvkM13I5t5Hfu6Ls= -github.com/mark3labs/mcp-go v0.42.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= +github.com/mark3labs/mcp-go v0.43.0 h1:lgiKcWMddh4sngbU+hoWOZ9iAe/qp/m851RQpj3Y7jA= +github.com/mark3labs/mcp-go v0.43.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= From ecee0a4600d68d97fe7ed382712bbd4017432380 Mon Sep 17 00:00:00 2001 From: Gautam Date: Wed, 19 Nov 2025 10:56:29 -0800 Subject: [PATCH 10/28] Update: simplified server.json, updating other files for next release (#209) * simplified server.json, updating other files for next release * updating golang version * updating go mod version * updating dockerfile --- .go-version | 2 +- .release/ci.hcl | 2 +- CHANGELOG.md | 2 + Dockerfile | 2 +- README.md | 8 +- go.mod | 2 +- server.json | 693 ++---------------------------------------------- version/VERSION | 2 +- 8 files changed, 28 insertions(+), 685 deletions(-) diff --git a/.go-version b/.go-version index 8407e260..d905a6d1 100644 --- a/.go-version +++ b/.go-version @@ -1 +1 @@ -1.24.7 +1.25.1 diff --git a/.release/ci.hcl b/.release/ci.hcl index 821a9e77..c8f725ac 100644 --- a/.release/ci.hcl +++ b/.release/ci.hcl @@ -8,7 +8,7 @@ project "terraform-mcp-server" { # slack channel : feed-terraform-mcp-server-releases slack { - notification_channel = "C08TEJWRXDX" + notification_channel = "C09KWKM9HHB" } github { diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aedd233..eb56f6c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## Unreleased + ## 0.3.2 (Oct 23, 2025) FEATURES diff --git a/Dockerfile b/Dockerfile index c000e5ec..6cf1e27a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ RUN apk add --no-cache ca-certificates # devbuild compiles the binary # ----------------------------------- -FROM golang:1.24.6-alpine@sha256:c8c5f95d64aa79b6547f3b626eb84b16a7ce18a139e3e9ca19a8c078b85ba80d AS devbuild +FROM golang:1.25.1-alpine@sha256:b6ed3fd0452c0e9bcdef5597f29cc1418f61672e9d3a2f55bf02e7222c014abd AS devbuild ARG VERSION="dev" # Set the working directory WORKDIR /build diff --git a/README.md b/README.md index 7202b347..3e46c357 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ More about using MCP server tools in VS Code's [agent mode documentation](https: "--rm", "-e", "TFE_TOKEN=${input:tfe_token}", "-e", "TFE_ADDRESS=${input:tfe_address}", - "hashicorp/terraform-mcp-server:0.3.0" + "hashicorp/terraform-mcp-server:0.3.2" ] } }, @@ -149,7 +149,7 @@ Optionally, you can add a similar example (i.e. without the mcp key) to a file c "--rm", "-e", "TFE_TOKEN=${input:tfe_token}", "-e", "TFE_ADDRESS=${input:tfe_address}", - "hashicorp/terraform-mcp-server:0.3.0" + "hashicorp/terraform-mcp-server:0.3.2" ] } }, @@ -216,7 +216,7 @@ Add this to your Cursor config (`~/.cursor/mcp.json`) or via Settings → Cursor "--rm", "-e", "TFE_ADDRESS=<>", "-e", "TFE_TOKEN=<>", - "hashicorp/terraform-mcp-server:0.3.0" + "hashicorp/terraform-mcp-server:0.3.2" ] } } @@ -269,7 +269,7 @@ More about using MCP server tools in Claude Desktop [user documentation](https:/ "--rm", "-e", "TFE_ADDRESS=<>", "-e", "TFE_TOKEN=<>", - "hashicorp/terraform-mcp-server:0.3.0" + "hashicorp/terraform-mcp-server:0.3.2" ] } } diff --git a/go.mod b/go.mod index a978e450..5de98ff5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/hashicorp/terraform-mcp-server -go 1.24.0 +go 1.25.1 require ( github.com/hashicorp/go-cleanhttp v0.5.2 diff --git a/server.json b/server.json index 3d2a9522..4079f6cd 100644 --- a/server.json +++ b/server.json @@ -32,615 +32,44 @@ { "type": "named", "name": "-e", - "value": "TFE_ADDRESS={tfe_address}", - "description": "HCP Terraform or Terraform Enterprise base URL.", - "variables": { - "tfe_address": { - "description": "HCP Terraform or Terraform Enterprise base URL.", - "default": "https://app.terraform.io/", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "TFE_TOKEN={tfe_token}", - "description": "HCP Terraform or Terraform Enterprise API token used to authenticate requests.", - "variables": { - "tfe_token": { - "description": "HCP Terraform or Terraform Enterprise API token used to authenticate requests.", - "isRequired": false, - "isSecret": true - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "TFE_SKIP_TLS_VERIFY={tfe_skip_tls_verify}", - "description": "Set to true to disable TLS verification when connecting to HCP Terraform or Terraform Enterprise.", - "variables": { - "tfe_skip_tls_verify": { - "description": "Set to true to disable TLS verification when connecting to HCP Terraform or Terraform Enterprise.", - "default": "false", - "choices": [ - "true", - "false" - ], - "format": "boolean", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "TRANSPORT_MODE={transport_mode}", - "description": "Selects the active transport; use streamable-http to serve HTTP clients.", - "variables": { - "transport_mode": { - "description": "Selects the active transport; use streamable-http to serve HTTP clients.", - "default": "stdio", - "choices": [ - "stdio" - ], - "isRequired": true - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "TRANSPORT_HOST={transport_host}", - "description": "Interface to bind when running in streamable-http mode.", - "variables": { - "transport_host": { - "description": "Interface to bind when running in streamable-http mode.", - "default": "127.0.0.1", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "TRANSPORT_PORT={transport_port}", - "description": "Port to bind when running in streamable-http mode.", - "variables": { - "transport_port": { - "description": "Port to bind when running in streamable-http mode.", - "default": "8080", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "MCP_ENDPOINT={mcp_endpoint}", - "description": "HTTP endpoint path exposed when streamable-http transport is enabled.", - "variables": { - "mcp_endpoint": { - "description": "HTTP endpoint path exposed when streamable-http transport is enabled.", - "default": "/mcp", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "MCP_SESSION_MODE={mcp_session_mode}", - "description": "Session management mode for the MCP server.", - "variables": { - "mcp_session_mode": { - "description": "Session management mode for the MCP server.", - "default": "stateful", - "choices": [ - "stateful", - "stateless" - ], - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "MCP_ALLOWED_ORIGINS={mcp_allowed_origins}", - "description": "Comma-separated list of allowed origins for CORS when using streamable-http transport.", - "variables": { - "mcp_allowed_origins": { - "description": "Comma-separated list of allowed origins for CORS when using streamable-http transport.", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "MCP_CORS_MODE={mcp_cors_mode}", - "description": "CORS enforcement mode; strict enforces allowed origins while development trusts localhost.", - "variables": { - "mcp_cors_mode": { - "description": "CORS enforcement mode; strict enforces allowed origins while development trusts localhost.", - "default": "strict", - "choices": [ - "strict", - "development", - "disabled" - ], - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "MCP_TLS_CERT_FILE={mcp_tls_cert_file}", - "description": "Path to the TLS certificate file to enable HTTPS for streamable-http transport.", - "variables": { - "mcp_tls_cert_file": { - "description": "Path to the TLS certificate file to enable HTTPS for streamable-http transport.", - "format": "filepath", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "MCP_TLS_KEY_FILE={mcp_tls_key_file}", - "description": "Path to the TLS key file to enable HTTPS for streamable-http transport.", - "variables": { - "mcp_tls_key_file": { - "description": "Path to the TLS key file to enable HTTPS for streamable-http transport.", - "format": "filepath", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "MCP_RATE_LIMIT_GLOBAL={mcp_rate_limit_global}", - "description": "Global rate limit in requests-per-second:burst format applied across all sessions.", - "variables": { - "mcp_rate_limit_global": { - "description": "Global rate limit in requests-per-second:burst format applied across all sessions.", - "default": "10:20", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "MCP_RATE_LIMIT_SESSION={mcp_rate_limit_session}", - "description": "Per-session rate limit in requests-per-second:burst format.", - "variables": { - "mcp_rate_limit_session": { - "description": "Per-session rate limit in requests-per-second:burst format.", - "default": "5:10", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "ENABLE_TF_OPERATIONS={enable_tf_operations}", - "description": "Set to true to enable tools that execute Terraform operations requiring explicit approval.", - "variables": { - "enable_tf_operations": { - "description": "Set to true to enable tools that execute Terraform operations requiring explicit approval.", - "default": "false", - "choices": [ - "true", - "false" - ], - "format": "boolean", - "isRequired": false - } - }, + "description": "Set an environment variable in the runtime", "isRepeated": true }, { "type": "positional", - "value": "hashicorp/terraform-mcp-server:0.3.2" - } - ], - "environmentVariables": [ - { - "name": "TFE_ADDRESS", - "description": "HCP Terraform or Terraform Enterprise base URL.", - "default": "https://app.terraform.io/", - "isRequired": false - }, - { - "name": "TFE_TOKEN", - "description": "HCP Terraform or Terraform Enterprise API token used to authenticate requests.", - "isRequired": false, - "isSecret": true - }, - { - "name": "TFE_SKIP_TLS_VERIFY", - "description": "Set to true to disable TLS verification when connecting to HCP Terraform or Terraform Enterprise.", - "default": "false", - "format": "boolean", - "choices": [ - "true", - "false" - ], - "isRequired": false - }, - { - "name": "TRANSPORT_MODE", - "description": "Selects the active transport; use streamable-http to serve HTTP clients.", - "default": "stdio", - "choices": [ - "stdio" - ], - "isRequired": true - }, - { - "name": "TRANSPORT_HOST", - "description": "Interface to bind when running in streamable-http mode.", - "default": "127.0.0.1", - "isRequired": false - }, - { - "name": "TRANSPORT_PORT", - "description": "Port to bind when running in streamable-http mode.", - "default": "8080", - "isRequired": false - }, - { - "name": "MCP_ENDPOINT", - "description": "HTTP endpoint path exposed when streamable-http transport is enabled.", - "default": "/mcp", - "isRequired": false - }, - { - "name": "MCP_SESSION_MODE", - "description": "Session management mode for the MCP server.", - "default": "stateful", - "choices": [ - "stateful", - "stateless" - ], - "isRequired": false - }, - { - "name": "MCP_ALLOWED_ORIGINS", - "description": "Comma-separated list of allowed origins for CORS when using streamable-http transport.", - "isRequired": false - }, - { - "name": "MCP_CORS_MODE", - "description": "CORS enforcement mode; strict enforces allowed origins while development trusts localhost.", - "default": "strict", - "choices": [ - "strict", - "development", - "disabled" - ], - "isRequired": false - }, - { - "name": "MCP_TLS_CERT_FILE", - "description": "Path to the TLS certificate file to enable HTTPS for streamable-http transport.", - "format": "filepath", - "isRequired": false - }, - { - "name": "MCP_TLS_KEY_FILE", - "description": "Path to the TLS key file to enable HTTPS for streamable-http transport.", - "format": "filepath", - "isRequired": false - }, - { - "name": "MCP_RATE_LIMIT_GLOBAL", - "description": "Global rate limit in requests-per-second:burst format applied across all sessions.", - "default": "10:20", - "isRequired": false - }, - { - "name": "MCP_RATE_LIMIT_SESSION", - "description": "Per-session rate limit in requests-per-second:burst format.", - "default": "5:10", - "isRequired": false - }, - { - "name": "ENABLE_TF_OPERATIONS", - "description": "Set to true to enable tools that execute Terraform operations requiring explicit approval.", - "default": "false", - "format": "boolean", - "choices": [ - "true", - "false" - ], - "isRequired": false - } - ] - }, - { - "registryType": "oci", - "registryBaseUrl": "https://docker.io/", - "identifier": "docker.io/hashicorp/terraform-mcp-server:0.3.2", - "version": "0.3.2", - "transport": { - "type": "streamable-http", - "url": "http://127.0.0.1:8080/mcp" - }, - "runtimeHint": "docker", - "runtimeArguments": [ - { - "type": "positional", - "value": "run" - }, - { - "type": "named", - "name": "--rm" - }, - { - "type": "named", - "name": "-p", - "value": "8080:8080" - }, - { - "type": "named", - "name": "-i" - }, - { - "type": "named", - "name": "-e", - "value": "TFE_ADDRESS={tfe_address}", - "description": "HCP Terraform or Terraform Enterprise base URL.", - "variables": { - "tfe_address": { - "description": "HCP Terraform or Terraform Enterprise base URL.", - "default": "https://app.terraform.io/", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "TFE_TOKEN={tfe_token}", - "description": "HCP Terraform or Terraform Enterprise API token used to authenticate requests.", - "variables": { - "tfe_token": { - "description": "HCP Terraform or Terraform Enterprise API token used to authenticate requests.", - "isRequired": false, - "isSecret": true - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "TFE_SKIP_TLS_VERIFY={tfe_skip_tls_verify}", - "description": "Set to true to disable TLS verification when connecting to HCP Terraform or Terraform Enterprise.", - "variables": { - "tfe_skip_tls_verify": { - "description": "Set to true to disable TLS verification when connecting to HCP Terraform or Terraform Enterprise.", - "default": "false", - "choices": [ - "true", - "false" - ], - "format": "boolean", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "TRANSPORT_MODE={transport_mode}", - "description": "Selects the active transport; use streamable-http to serve HTTP clients.", - "variables": { - "transport_mode": { - "description": "Selects the active transport; use streamable-http to serve HTTP clients.", - "default": "streamable-http", - "choices": [ - "streamable-http" - ], - "isRequired": true - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "TRANSPORT_HOST={transport_host}", - "description": "Interface to bind when running in streamable-http mode.", - "variables": { - "transport_host": { - "description": "Interface to bind when running in streamable-http mode.", - "default": "0.0.0.0", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "TRANSPORT_PORT={transport_port}", - "description": "Port to bind when running in streamable-http mode.", - "variables": { - "transport_port": { - "description": "Port to bind when running in streamable-http mode.", - "default": "8080", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "MCP_ENDPOINT={mcp_endpoint}", - "description": "HTTP endpoint path exposed when streamable-http transport is enabled.", - "variables": { - "mcp_endpoint": { - "description": "HTTP endpoint path exposed when streamable-http transport is enabled.", - "default": "/mcp", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "MCP_SESSION_MODE={mcp_session_mode}", - "description": "Session management mode for the MCP server.", - "variables": { - "mcp_session_mode": { - "description": "Session management mode for the MCP server.", - "default": "stateful", - "choices": [ - "stateful", - "stateless" - ], - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "MCP_ALLOWED_ORIGINS={mcp_allowed_origins}", - "description": "Comma-separated list of allowed origins for CORS when using streamable-http transport.", - "variables": { - "mcp_allowed_origins": { - "description": "Comma-separated list of allowed origins for CORS when using streamable-http transport.", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "MCP_CORS_MODE={mcp_cors_mode}", - "description": "CORS enforcement mode; strict enforces allowed origins while development trusts localhost.", - "variables": { - "mcp_cors_mode": { - "description": "CORS enforcement mode; strict enforces allowed origins while development trusts localhost.", - "default": "strict", - "choices": [ - "strict", - "development", - "disabled" - ], - "isRequired": false - } - }, - "isRepeated": true + "valueHint": "env_var_name", + "value": "TFE_ADDRESS", + "description": "Environment variable name" }, { "type": "named", "name": "-e", - "value": "MCP_TLS_CERT_FILE={mcp_tls_cert_file}", - "description": "Path to the TLS certificate file to enable HTTPS for streamable-http transport.", - "variables": { - "mcp_tls_cert_file": { - "description": "Path to the TLS certificate file to enable HTTPS for streamable-http transport.", - "format": "filepath", - "isRequired": false - } - }, + "description": "Set an environment variable in the runtime", "isRepeated": true }, { - "type": "named", - "name": "-e", - "value": "MCP_TLS_KEY_FILE={mcp_tls_key_file}", - "description": "Path to the TLS key file to enable HTTPS for streamable-http transport.", - "variables": { - "mcp_tls_key_file": { - "description": "Path to the TLS key file to enable HTTPS for streamable-http transport.", - "format": "filepath", - "isRequired": false - } - }, - "isRepeated": true - }, - { - "type": "named", - "name": "-e", - "value": "MCP_RATE_LIMIT_GLOBAL={mcp_rate_limit_global}", - "description": "Global rate limit in requests-per-second:burst format applied across all sessions.", - "variables": { - "mcp_rate_limit_global": { - "description": "Global rate limit in requests-per-second:burst format applied across all sessions.", - "default": "10:20", - "isRequired": false - } - }, - "isRepeated": true + "type": "positional", + "valueHint": "env_var_name", + "value": "TFE_TOKEN", + "description": "Environment variable name" }, { "type": "named", "name": "-e", - "value": "MCP_RATE_LIMIT_SESSION={mcp_rate_limit_session}", - "description": "Per-session rate limit in requests-per-second:burst format.", - "variables": { - "mcp_rate_limit_session": { - "description": "Per-session rate limit in requests-per-second:burst format.", - "default": "5:10", - "isRequired": false - } - }, + "description": "Set an environment variable in the runtime", "isRepeated": true }, { - "type": "named", - "name": "-e", - "value": "ENABLE_TF_OPERATIONS={enable_tf_operations}", - "description": "Set to true to enable tools that execute Terraform operations requiring explicit approval.", - "variables": { - "enable_tf_operations": { - "description": "Set to true to enable tools that execute Terraform operations requiring explicit approval.", - "default": "false", - "choices": [ - "true", - "false" - ], - "format": "boolean", - "isRequired": false - } - }, - "isRepeated": true + "type": "positional", + "valueHint": "env_var_name", + "value": "ENABLE_TF_OPERATIONS", + "description": "Environment variable name" }, { "type": "positional", - "value": "hashicorp/terraform-mcp-server:0.3.2" + "valueHint": "image_name", + "value": "hashicorp/terraform-mcp-server:0.3.2", + "description": "The container image to run" } ], "environmentVariables": [ @@ -656,94 +85,6 @@ "isRequired": false, "isSecret": true }, - { - "name": "TFE_SKIP_TLS_VERIFY", - "description": "Set to true to disable TLS verification when connecting to HCP Terraform or Terraform Enterprise.", - "default": "false", - "format": "boolean", - "choices": [ - "true", - "false" - ], - "isRequired": false - }, - { - "name": "TRANSPORT_MODE", - "description": "Selects the active transport; use streamable-http to serve HTTP clients.", - "default": "streamable-http", - "choices": [ - "streamable-http" - ], - "isRequired": true - }, - { - "name": "TRANSPORT_HOST", - "description": "Interface to bind when running in streamable-http mode.", - "default": "0.0.0.0", - "isRequired": false - }, - { - "name": "TRANSPORT_PORT", - "description": "Port to bind when running in streamable-http mode.", - "default": "8080", - "isRequired": false - }, - { - "name": "MCP_ENDPOINT", - "description": "HTTP endpoint path exposed when streamable-http transport is enabled.", - "default": "/mcp", - "isRequired": false - }, - { - "name": "MCP_SESSION_MODE", - "description": "Session management mode for the MCP server.", - "default": "stateful", - "choices": [ - "stateful", - "stateless" - ], - "isRequired": false - }, - { - "name": "MCP_ALLOWED_ORIGINS", - "description": "Comma-separated list of allowed origins for CORS when using streamable-http transport.", - "isRequired": false - }, - { - "name": "MCP_CORS_MODE", - "description": "CORS enforcement mode; strict enforces allowed origins while development trusts localhost.", - "default": "strict", - "choices": [ - "strict", - "development", - "disabled" - ], - "isRequired": false - }, - { - "name": "MCP_TLS_CERT_FILE", - "description": "Path to the TLS certificate file to enable HTTPS for streamable-http transport.", - "format": "filepath", - "isRequired": false - }, - { - "name": "MCP_TLS_KEY_FILE", - "description": "Path to the TLS key file to enable HTTPS for streamable-http transport.", - "format": "filepath", - "isRequired": false - }, - { - "name": "MCP_RATE_LIMIT_GLOBAL", - "description": "Global rate limit in requests-per-second:burst format applied across all sessions.", - "default": "10:20", - "isRequired": false - }, - { - "name": "MCP_RATE_LIMIT_SESSION", - "description": "Per-session rate limit in requests-per-second:burst format.", - "default": "5:10", - "isRequired": false - }, { "name": "ENABLE_TF_OPERATIONS", "description": "Set to true to enable tools that execute Terraform operations requiring explicit approval.", diff --git a/version/VERSION b/version/VERSION index d15723fb..1d0ba9ea 100644 --- a/version/VERSION +++ b/version/VERSION @@ -1 +1 @@ -0.3.2 +0.4.0 From 8690f62220693638a3913de673a8feca0fe4b6e0 Mon Sep 17 00:00:00 2001 From: John Houston Date: Wed, 19 Nov 2025 12:26:21 -0700 Subject: [PATCH 11/28] Bump Go version to 1.25.4 (#217) --- .go-version | 2 +- Dockerfile | 2 +- go.mod | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.go-version b/.go-version index d905a6d1..26a9e99b 100644 --- a/.go-version +++ b/.go-version @@ -1 +1 @@ -1.25.1 +1.25.4 diff --git a/Dockerfile b/Dockerfile index 6cf1e27a..14e93eff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ RUN apk add --no-cache ca-certificates # devbuild compiles the binary # ----------------------------------- -FROM golang:1.25.1-alpine@sha256:b6ed3fd0452c0e9bcdef5597f29cc1418f61672e9d3a2f55bf02e7222c014abd AS devbuild +FROM golang:1.25.4-alpine@sha256:d3f0cf7723f3429e3f9ed846243970b20a2de7bae6a5b66fc5914e228d831bbb AS devbuild ARG VERSION="dev" # Set the working directory WORKDIR /build diff --git a/go.mod b/go.mod index 5de98ff5..334618a0 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/hashicorp/terraform-mcp-server -go 1.25.1 +go 1.25.4 require ( github.com/hashicorp/go-cleanhttp v0.5.2 From 06b29c02b46b28005bef495d28b60c1d670966cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:15:21 -0700 Subject: [PATCH 12/28] Bump golang.org/x/text from 0.30.0 to 0.31.0 (#213) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.30.0 to 0.31.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.30.0...v0.31.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-version: 0.31.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 334618a0..de56599d 100644 --- a/go.mod +++ b/go.mod @@ -38,9 +38,9 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/sync v0.17.0 // indirect + golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.30.0 + golang.org/x/text v0.31.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 807b1799..d40e1fa1 100644 --- a/go.sum +++ b/go.sum @@ -92,13 +92,13 @@ github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zI github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 597b8cc5a64db9963dc5d032eb2bfb20363d3d39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:15:40 -0700 Subject: [PATCH 13/28] Bump actions/checkout from 5.0.0 to 5.0.1 (#214) Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 5.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/08c6903cd8c0fde910a37f88322edcfb5dd907a8...93cb6efe18208431cddfb8368fd83d5badbf9bfd) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 5.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 10 +++++----- .github/workflows/e2e_test.yml | 2 +- .github/workflows/publish-registry.yml | 2 +- .github/workflows/release-checks.yml | 2 +- .github/workflows/security-scan.yml | 4 ++-- .github/workflows/unit_test.yml | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e11f345..0afd333c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,7 @@ jobs: outputs: go-version: ${{ steps.get-go-version.outputs.go-version }} steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Determine Go version id: get-go-version # We use .go-version as our source of truth for current Go @@ -65,7 +65,7 @@ jobs: product-prerelease-version: ${{ steps.set-product-version.outputs.prerelease-product-version }} product-minor-version: ${{ steps.set-product-version.outputs.minor-product-version }} steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set Product version id: set-product-version uses: hashicorp/actions-set-product-version@v2 @@ -77,7 +77,7 @@ jobs: filepath: ${{ steps.generate-metadata-file.outputs.filepath }} steps: - name: "Checkout directory" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Generate metadata file id: generate-metadata-file uses: hashicorp/actions-generate-metadata@v1 @@ -114,7 +114,7 @@ jobs: name: Go ${{ needs.get-go-version.outputs.go-version }} ${{ matrix.goos }} ${{ matrix.goarch }} build steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: hashicorp/actions-go-build@v1 env: @@ -152,7 +152,7 @@ jobs: repo: ${{ github.event.repository.name }} product_version: ${{ needs.set-product-version.outputs.product-version }} steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Docker Build (Action) uses: hashicorp/actions-docker-build@v2 with: diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index b3e3f3c8..cf009639 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd - name: Set up Go uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 diff --git a/.github/workflows/publish-registry.yml b/.github/workflows/publish-registry.yml index 3658b11f..aadee6db 100644 --- a/.github/workflows/publish-registry.yml +++ b/.github/workflows/publish-registry.yml @@ -24,7 +24,7 @@ jobs: echo "Authorized actor: ${GITHUB_ACTOR}" - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Go uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 diff --git a/.github/workflows/release-checks.yml b/.github/workflows/release-checks.yml index c59612ba..0d3e2b52 100644 --- a/.github/workflows/release-checks.yml +++ b/.github/workflows/release-checks.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Install jq run: | diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 9d26518f..d4a7a2d2 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -48,7 +48,7 @@ jobs: security-events: write steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: path: code - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 @@ -58,7 +58,7 @@ jobs: cache-dependency-path: '**/go.sum' - name: Clone Security Scanner repo - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: repository: hashicorp/security-scanner token: ${{ secrets.PRODSEC_SCANNER_READ_ONLY }} diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 2598661c..1c4a17a4 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd - name: Set up Go uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 From c028faacbf811913824eab65aa722295a843cb10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:16:12 -0700 Subject: [PATCH 14/28] Bump actions/setup-go from 6.0.0 to 6.1.0 (#218) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.0.0 to 6.1.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/44694675825211faa026b3c33043df3e48a5fa00...4dc6199c7b1a012772edbd06daecab0f50c9053c) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/e2e_test.yml | 2 +- .github/workflows/publish-registry.yml | 2 +- .github/workflows/security-scan.yml | 2 +- .github/workflows/unit_test.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index cf009639..23dd5bbd 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd - name: Set up Go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c with: go-version-file: "go.mod" diff --git a/.github/workflows/publish-registry.yml b/.github/workflows/publish-registry.yml index aadee6db..e719a991 100644 --- a/.github/workflows/publish-registry.yml +++ b/.github/workflows/publish-registry.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version-file: .go-version cache: true diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index d4a7a2d2..011e89bc 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -51,7 +51,7 @@ jobs: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: path: code - - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: cache: ${{ contains(runner.name, 'Github Actions') }} go-version-file: "code/.go-version" diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 1c4a17a4..f47e9721 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd - name: Set up Go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c with: go-version-file: "go.mod" From bbf9c5ef11a6a9365ee7386d03f78e20c114a9e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:30:20 -0700 Subject: [PATCH 15/28] Bump github.com/hashicorp/go-tfe from 1.95.0 to 1.96.0 (#219) Bumps [github.com/hashicorp/go-tfe](https://github.com/hashicorp/go-tfe) from 1.95.0 to 1.96.0. - [Release notes](https://github.com/hashicorp/go-tfe/releases) - [Changelog](https://github.com/hashicorp/go-tfe/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/go-tfe/compare/v1.95.0...v1.96.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/go-tfe dependency-version: 1.96.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index de56599d..27156c44 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.4 require ( github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-retryablehttp v0.7.8 - github.com/hashicorp/go-tfe v1.95.0 + github.com/hashicorp/go-tfe v1.96.0 github.com/hashicorp/jsonapi v1.5.0 github.com/mark3labs/mcp-go v0.43.0 github.com/sirupsen/logrus v1.9.3 diff --git a/go.sum b/go.sum index d40e1fa1..76700f24 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVU github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-slug v0.16.8 h1:f4/sDZqRsxx006HrE6e9BE5xO9lWXydKhVoH6Kb0v1M= github.com/hashicorp/go-slug v0.16.8/go.mod h1:hB4mUcVHl4RPu0205s0fwmB9i31MxQgeafGkko3FD+Y= -github.com/hashicorp/go-tfe v1.95.0 h1:vx1X6SlSHBnJzoy8Wy+0OyXcNmbdUt3wIk+Ll6mQLgo= -github.com/hashicorp/go-tfe v1.95.0/go.mod h1:hTnfAzkwOMvWL4sVKNPzUYTjrbwKIWnRWYSIC/Zh5SA= +github.com/hashicorp/go-tfe v1.96.0 h1:goTDOZIQ8rsf1vRQXvqvK8v/inD4SQe5T2vcEX1q2MU= +github.com/hashicorp/go-tfe v1.96.0/go.mod h1:umRhpwmiMAa5Dhu8dzF0itJfBZHJPoTmS8BpNZs9+2Y= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= From eb0abe9ac03d06882d49321bfb50005083f58fa4 Mon Sep 17 00:00:00 2001 From: John Houston Date: Thu, 20 Nov 2025 13:32:31 -0700 Subject: [PATCH 16/28] Pin alpine image by sha in Dockerfile (#221) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 14e93eff..1335b274 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ # =================================== # certbuild captures the ca-certificates -FROM docker.mirror.hashicorp.services/alpine:3.22 AS certbuild +FROM docker.mirror.hashicorp.services/alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 AS certbuild RUN apk add --no-cache ca-certificates # devbuild compiles the binary From 2e2f0e11bbd20d4c29f388f46a1eaab682f9b078 Mon Sep 17 00:00:00 2001 From: Jaylon McShan Date: Thu, 20 Nov 2025 14:32:50 -0600 Subject: [PATCH 17/28] feat: Add support for list-resources document type (#220) * feat: Add support for list-resources document type * add changelog * add list-resources to docs and test --- CHANGELOG.md | 4 +++ e2e/e2e_test.go | 2 ++ e2e/payloads.go | 26 ++++++++++++++----- .../registry/get_provider_capabilities.go | 2 +- .../get_provider_capabilities_test.go | 1 + pkg/tools/registry/search_providers.go | 5 ++-- pkg/utils/utils.go | 4 +-- pkg/utils/utils_test.go | 4 +-- 8 files changed, 35 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb56f6c6..894fff89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +IMPROVEMENTS + +* Adding support for searching Terraform List Resources documentation + ## 0.3.2 (Oct 23, 2025) FEATURES diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 648f6675..9a461462 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -126,6 +126,8 @@ func runTestSuite(t *testing.T, client mcpClient.MCPClient, transportName string require.Contains(t, textContent.Text, "functions", "expected content to contain functions") case CONST_TYPE_ACTIONS: require.Contains(t, textContent.Text, "actions", "expected content to contain actions") + case CONST_TYPE_LIST_RESOURCES: + require.Contains(t, textContent.Text, "list-resources", "expected content to contain list-resources") } } }) diff --git a/e2e/payloads.go b/e2e/payloads.go index d1697e97..c3fb6451 100644 --- a/e2e/payloads.go +++ b/e2e/payloads.go @@ -6,12 +6,13 @@ package e2e type ContentType string const ( - CONST_TYPE_RESOURCE ContentType = "resources" - CONST_TYPE_DATA_SOURCE ContentType = "data-sources" - CONST_TYPE_GUIDES ContentType = "guides" - CONST_TYPE_FUNCTIONS ContentType = "functions" - CONST_TYPE_OVERVIEW ContentType = "overview" - CONST_TYPE_ACTIONS ContentType = "actions" + CONST_TYPE_RESOURCE ContentType = "resources" + CONST_TYPE_DATA_SOURCE ContentType = "data-sources" + CONST_TYPE_GUIDES ContentType = "guides" + CONST_TYPE_FUNCTIONS ContentType = "functions" + CONST_TYPE_OVERVIEW ContentType = "overview" + CONST_TYPE_ACTIONS ContentType = "actions" + CONST_TYPE_LIST_RESOURCES ContentType = "list-resources" ) type RegistryTestCase struct { @@ -185,6 +186,19 @@ var searchProviderTestCases = []RegistryTestCase{ "service_slug": "ec2", }, }, + { + TestName: "list_resources_documentation", + TestShouldFail: false, + TestDescription: "Testing search_providers list-resources documentation with v2 API", + TestContentType: CONST_TYPE_LIST_RESOURCES, + TestPayload: map[string]interface{}{ + "provider_name": "aws", + "provider_namespace": "hashicorp", + "provider_version": "latest", + "provider_document_type": "list-resources", + "service_slug": "instance", + }, + }, } var providerDetailsTestCases = []RegistryTestCase{ diff --git a/pkg/tools/registry/get_provider_capabilities.go b/pkg/tools/registry/get_provider_capabilities.go index b584b6d7..81c4485f 100644 --- a/pkg/tools/registry/get_provider_capabilities.go +++ b/pkg/tools/registry/get_provider_capabilities.go @@ -32,7 +32,7 @@ This tool analyzes the provider documentation to determine what types of capabil - guides: Documentation guides and tutorials for using the provider - actions: Available provider actions (if any) - ephemeral resources: Temporary resources for credentials and tokens -- list resources: Resources for listing multiple items of specific types +- list-resources: List resources for querying existing cloud resources (Terraform Search) Returns a summary with counts and examples for each capability type.`), mcp.WithTitleAnnotation("Get Terraform provider capabilities and supported features"), diff --git a/pkg/tools/registry/get_provider_capabilities_test.go b/pkg/tools/registry/get_provider_capabilities_test.go index c4ff42cf..8eb19866 100644 --- a/pkg/tools/registry/get_provider_capabilities_test.go +++ b/pkg/tools/registry/get_provider_capabilities_test.go @@ -18,6 +18,7 @@ func TestAnalyzeAndFormatCapabilities(t *testing.T) { {Category: "data-sources", Title: "aws_ami", Language: "hcl"}, {Category: "functions", Title: "base64encode", Language: "hcl"}, {Category: "guides", Title: "Getting Started", Language: "hcl"}, + {Category: "list-resources", Title: "aws_instance", Language: "hcl"}, }, } diff --git a/pkg/tools/registry/search_providers.go b/pkg/tools/registry/search_providers.go index a134ff20..d4f20cf8 100644 --- a/pkg/tools/registry/search_providers.go +++ b/pkg/tools/registry/search_providers.go @@ -54,8 +54,9 @@ for general overview of the provider use 'overview', for guidance on upgrading a provider or custom configuration information use 'guides', for deploying resources use 'resources', for reading pre-deployed resources use 'data-sources', for functions use 'functions', -for Terraform actions use 'actions'`), - mcp.Enum("resources", "data-sources", "functions", "guides", "overview", "actions"), +for Terraform actions use 'actions', +for listing resources using Terraform Search use 'list-resources'`), + mcp.Enum("resources", "data-sources", "functions", "guides", "overview", "actions", "list-resources"), ), mcp.WithString("provider_version", mcp.Description("The version of the Terraform provider to retrieve in the format 'x.y.z', or 'latest' to get the latest version")), diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 9389be37..a0746baa 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -65,7 +65,7 @@ func IsValidProviderVersionFormat(version string) bool { } func IsValidProviderDocumentType(providerDocumentType string) bool { - validTypes := []string{"resources", "data-sources", "functions", "guides", "overview"} + validTypes := []string{"resources", "data-sources", "functions", "guides", "overview", "actions", "list-resources"} return slices.Contains(validTypes, providerDocumentType) } @@ -79,7 +79,7 @@ func LogAndReturnError(logger *log.Logger, context string, err error) error { } func IsV2ProviderDocumentType(dataType string) bool { - v2Categories := []string{"guides", "functions", "overview", "actions"} + v2Categories := []string{"guides", "functions", "overview", "actions", "list-resources"} return slices.Contains(v2Categories, dataType) } diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index e6863583..dbfcf9d2 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -70,7 +70,7 @@ func TestIsValidProviderVersionFormat(t *testing.T) { } func TestIsValidProviderDataType(t *testing.T) { - valid := []string{"resources", "data-sources", "functions", "guides", "overview"} + valid := []string{"resources", "data-sources", "functions", "guides", "overview", "actions", "list-resources"} invalid := []string{"foo", "bar", ""} for _, v := range valid { if !IsValidProviderDocumentType(v) { @@ -92,7 +92,7 @@ func TestLogAndReturnError_NilLogger(t *testing.T) { } func TestIsV2ProviderDataType(t *testing.T) { - valid := []string{"guides", "functions", "overview", "actions"} + valid := []string{"guides", "functions", "overview", "actions", "list-resources"} invalid := []string{"resources", "data-sources", "foo"} for _, v := range valid { if !IsV2ProviderDocumentType(v) { From 3ad2d74a0845fd4a1e11ea706fe53eec12ef7590 Mon Sep 17 00:00:00 2001 From: John Houston Date: Thu, 20 Nov 2025 22:11:33 -0700 Subject: [PATCH 18/28] Prepare for v0.3.3 (#223) --- CHANGELOG.md | 2 +- README.md | 8 ++++---- gemini-extension.json | 4 ++-- server.json | 6 +++--- version/VERSION | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 894fff89..91b9872e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased +## 0.3.3 (Nov 21, 2025) IMPROVEMENTS diff --git a/README.md b/README.md index 3e46c357..f82d723e 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ More about using MCP server tools in VS Code's [agent mode documentation](https: "--rm", "-e", "TFE_TOKEN=${input:tfe_token}", "-e", "TFE_ADDRESS=${input:tfe_address}", - "hashicorp/terraform-mcp-server:0.3.2" + "hashicorp/terraform-mcp-server:0.3.3" ] } }, @@ -149,7 +149,7 @@ Optionally, you can add a similar example (i.e. without the mcp key) to a file c "--rm", "-e", "TFE_TOKEN=${input:tfe_token}", "-e", "TFE_ADDRESS=${input:tfe_address}", - "hashicorp/terraform-mcp-server:0.3.2" + "hashicorp/terraform-mcp-server:0.3.3" ] } }, @@ -216,7 +216,7 @@ Add this to your Cursor config (`~/.cursor/mcp.json`) or via Settings → Cursor "--rm", "-e", "TFE_ADDRESS=<>", "-e", "TFE_TOKEN=<>", - "hashicorp/terraform-mcp-server:0.3.2" + "hashicorp/terraform-mcp-server:0.3.3" ] } } @@ -269,7 +269,7 @@ More about using MCP server tools in Claude Desktop [user documentation](https:/ "--rm", "-e", "TFE_ADDRESS=<>", "-e", "TFE_TOKEN=<>", - "hashicorp/terraform-mcp-server:0.3.2" + "hashicorp/terraform-mcp-server:0.3.3" ] } } diff --git a/gemini-extension.json b/gemini-extension.json index 2c3d072f..55a379aa 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "terraform", - "version": "0.3.2", + "version": "0.3.3", "contextFileName": "${extensionPath}${/}instructions${/}example-AGENTS.md", "mcpServers": { "terraform": { @@ -13,7 +13,7 @@ "TFE_TOKEN", "-e", "TFE_ADDRESS", - "hashicorp/terraform-mcp-server:0.3.2" + "hashicorp/terraform-mcp-server:0.3.3" ], "env": { "TFE_TOKEN": "$TFE_TOKEN", diff --git a/server.json b/server.json index 4079f6cd..5bda0707 100644 --- a/server.json +++ b/server.json @@ -7,11 +7,11 @@ "url": "https://github.com/hashicorp/terraform-mcp-server", "source": "github" }, - "version": "0.3.2", + "version": "0.3.3", "packages": [ { "registryType": "oci", - "identifier": "docker.io/hashicorp/terraform-mcp-server:0.3.2", + "identifier": "docker.io/hashicorp/terraform-mcp-server:0.3.3", "transport": { "type": "stdio" }, @@ -68,7 +68,7 @@ { "type": "positional", "valueHint": "image_name", - "value": "hashicorp/terraform-mcp-server:0.3.2", + "value": "hashicorp/terraform-mcp-server:0.3.3", "description": "The container image to run" } ], diff --git a/version/VERSION b/version/VERSION index 1d0ba9ea..1c09c74e 100644 --- a/version/VERSION +++ b/version/VERSION @@ -1 +1 @@ -0.4.0 +0.3.3 From f6931a99a4bfeacade5079113ecaa770bfc48937 Mon Sep 17 00:00:00 2001 From: John Houston Date: Fri, 21 Nov 2025 12:55:16 -0700 Subject: [PATCH 19/28] Update actor list for publish action (#226) --- .github/workflows/publish-registry.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish-registry.yml b/.github/workflows/publish-registry.yml index e719a991..9f7b5bed 100644 --- a/.github/workflows/publish-registry.yml +++ b/.github/workflows/publish-registry.yml @@ -16,6 +16,8 @@ jobs: env: ALLOWED_ACTORS: | gautambaghel + jrhouston + jaylonmcshan19-x run: | if ! grep -Fxq "${GITHUB_ACTOR}" <<< "${ALLOWED_ACTORS}"; then echo "github.actor '${GITHUB_ACTOR}' is not authorized to run this workflow." From 8fa297c0cc6d303295e0b3fa08a7c83dad67a2f5 Mon Sep 17 00:00:00 2001 From: John Houston Date: Fri, 21 Nov 2025 12:56:23 -0700 Subject: [PATCH 20/28] Update gemini configuration to include TF_ENABLE_OPERATIONS (#227) --- gemini-extension.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gemini-extension.json b/gemini-extension.json index 55a379aa..7f7d4166 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -13,11 +13,14 @@ "TFE_TOKEN", "-e", "TFE_ADDRESS", + "-e", + "ENABLE_TF_OPERATIONS", "hashicorp/terraform-mcp-server:0.3.3" ], "env": { "TFE_TOKEN": "$TFE_TOKEN", - "TFE_ADDRESS": "$TFE_ADDRESS" + "TFE_ADDRESS": "$TFE_ADDRESS", + "ENABLE_TF_OPERATIONS": "$ENABLE_TF_OPERATIONS" } } } From 87f9bfe336ca0c04bcf5be14e3bd535cb14629be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:05:29 -0700 Subject: [PATCH 21/28] Bump github.com/mark3labs/mcp-go from 0.43.0 to 0.43.1 (#229) Bumps [github.com/mark3labs/mcp-go](https://github.com/mark3labs/mcp-go) from 0.43.0 to 0.43.1. - [Release notes](https://github.com/mark3labs/mcp-go/releases) - [Commits](https://github.com/mark3labs/mcp-go/compare/v0.43.0...v0.43.1) --- updated-dependencies: - dependency-name: github.com/mark3labs/mcp-go dependency-version: 0.43.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 27156c44..af9526e6 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/go-tfe v1.96.0 github.com/hashicorp/jsonapi v1.5.0 - github.com/mark3labs/mcp-go v0.43.0 + github.com/mark3labs/mcp-go v0.43.1 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.10.1 github.com/spf13/viper v1.21.0 diff --git a/go.sum b/go.sum index 76700f24..2fa0e8bc 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mark3labs/mcp-go v0.43.0 h1:lgiKcWMddh4sngbU+hoWOZ9iAe/qp/m851RQpj3Y7jA= -github.com/mark3labs/mcp-go v0.43.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= +github.com/mark3labs/mcp-go v0.43.1 h1:WXNVd+bRM/7mOzCM9zulSwn/s9YEdAxbmeh9LoRHEXY= +github.com/mark3labs/mcp-go v0.43.1/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= From a3ddfb13bbbc63e8f09d1d045e16e9c966777fea Mon Sep 17 00:00:00 2001 From: Jaylon McShan Date: Wed, 3 Dec 2025 10:25:10 -0600 Subject: [PATCH 22/28] enable toolset flag (#215) * enable toolset flag * Implement --toolsets flag * Address PR feedback: simplify types and add tests * Add changelog * Fix variable names --- CHANGELOG.md | 6 + cmd/terraform-mcp-server/init.go | 16 ++- cmd/terraform-mcp-server/main.go | 51 +++++-- pkg/tools/dynamic_tool.go | 195 ++++++++++++++++---------- pkg/tools/tools.go | 67 +++++---- pkg/toolsets/mapping.go | 70 ++++++++++ pkg/toolsets/toolsets.go | 155 +++++++++++++++++++++ pkg/toolsets/toolsets_test.go | 229 +++++++++++++++++++++++++++++++ 8 files changed, 680 insertions(+), 109 deletions(-) create mode 100644 pkg/toolsets/mapping.go create mode 100644 pkg/toolsets/toolsets.go create mode 100644 pkg/toolsets/toolsets_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 91b9872e..aa2fac1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +FEATURES + +* **Toolsets Flag**: Added `--toolsets` flag to selectively enable tool groups. Three toolset groups are available: `registry` (public Terraform Registry), `registry-private` (private TFE/TFC registry), and `terraform` (TFE/TFC operations). Default is `registry` only. + ## 0.3.3 (Nov 21, 2025) IMPROVEMENTS diff --git a/cmd/terraform-mcp-server/init.go b/cmd/terraform-mcp-server/init.go index 420fc686..e572e159 100644 --- a/cmd/terraform-mcp-server/init.go +++ b/cmd/terraform-mcp-server/init.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-mcp-server/pkg/client" "github.com/hashicorp/terraform-mcp-server/pkg/resources" "github.com/hashicorp/terraform-mcp-server/pkg/tools" + "github.com/hashicorp/terraform-mcp-server/pkg/toolsets" "github.com/hashicorp/terraform-mcp-server/version" "github.com/mark3labs/mcp-go/server" log "github.com/sirupsen/logrus" @@ -37,7 +38,7 @@ var ( Use: "stdio", Short: "Start stdio server", Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`, - Run: func(_ *cobra.Command, _ []string) { + Run: func(cmd *cobra.Command, _ []string) { logFile, err := rootCmd.PersistentFlags().GetString("log-file") if err != nil { stdlog.Fatal("Failed to get log file:", err) @@ -47,7 +48,9 @@ var ( stdlog.Fatal("Failed to initialize logger:", err) } - if err := runStdioServer(logger); err != nil { + enabledToolsets := getToolsetsFromCmd(cmd.Root(), logger) + + if err := runStdioServer(logger, enabledToolsets); err != nil { stdlog.Fatal("failed to run stdio server:", err) } }, @@ -81,7 +84,9 @@ var ( stdlog.Fatal("Failed to get endpoint path:", err) } - if err := runHTTPServer(logger, host, port, endpointPath); err != nil { + enabledToolsets := getToolsetsFromCmd(cmd.Root(), logger) + + if err := runHTTPServer(logger, host, port, endpointPath, enabledToolsets); err != nil { stdlog.Fatal("failed to run streamableHTTP server:", err) } }, @@ -104,6 +109,7 @@ func init() { cobra.OnInitialize(initConfig) rootCmd.SetVersionTemplate("{{.Short}}\n{{.Version}}\n") rootCmd.PersistentFlags().String("log-file", "", "Path to log file") + rootCmd.PersistentFlags().String("toolsets", "default", toolsets.GenerateToolsetsHelp()) // Add StreamableHTTP command flags (avoid 'h' shorthand conflict with help) streamableHTTPCmd.Flags().String("transport-host", "127.0.0.1", "Host to bind to") @@ -142,8 +148,8 @@ func initLogger(outPath string) (*log.Logger, error) { } // registerToolsAndResources registers tools and resources with the MCP server -func registerToolsAndResources(hcServer *server.MCPServer, logger *log.Logger) { - tools.RegisterTools(hcServer, logger) +func registerToolsAndResources(hcServer *server.MCPServer, logger *log.Logger, enabledToolsets []string) { + tools.RegisterTools(hcServer, logger, enabledToolsets) resources.RegisterResources(hcServer, logger) resources.RegisterResourceTemplates(hcServer, logger) } diff --git a/cmd/terraform-mcp-server/main.go b/cmd/terraform-mcp-server/main.go index e913d7d5..ec887b23 100644 --- a/cmd/terraform-mcp-server/main.go +++ b/cmd/terraform-mcp-server/main.go @@ -14,6 +14,7 @@ import ( "syscall" "github.com/hashicorp/terraform-mcp-server/pkg/client" + "github.com/hashicorp/terraform-mcp-server/pkg/toolsets" "github.com/hashicorp/terraform-mcp-server/version" "github.com/mark3labs/mcp-go/server" @@ -24,27 +25,27 @@ import ( //go:embed instructions.md var instructions string -func runHTTPServer(logger *log.Logger, host string, port string, endpointPath string) error { +func runHTTPServer(logger *log.Logger, host string, port string, endpointPath string, enabledToolsets []string) error { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - hcServer := NewServer(version.Version, logger) - registerToolsAndResources(hcServer, logger) + hcServer := NewServer(version.Version, logger, enabledToolsets) + registerToolsAndResources(hcServer, logger, enabledToolsets) return streamableHTTPServerInit(ctx, hcServer, logger, host, port, endpointPath) } -func runStdioServer(logger *log.Logger) error { +func runStdioServer(logger *log.Logger, enabledToolsets []string) error { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - hcServer := NewServer(version.Version, logger) - registerToolsAndResources(hcServer, logger) + hcServer := NewServer(version.Version, logger, enabledToolsets) + registerToolsAndResources(hcServer, logger, enabledToolsets) return serverInit(ctx, hcServer, logger) } -func NewServer(version string, logger *log.Logger, opts ...server.ServerOption) *server.MCPServer { +func NewServer(version string, logger *log.Logger, enabledToolsets []string, opts ...server.ServerOption) *server.MCPServer { // Create rate limiting middleware with environment-based configuration rateLimitConfig := client.LoadRateLimitConfigFromEnv() rateLimitMiddleware := client.NewRateLimitMiddleware(rateLimitConfig, logger) @@ -80,6 +81,33 @@ func NewServer(version string, logger *log.Logger, opts ...server.ServerOption) return s } +// parseToolsets parses and validates the toolsets flag value +func parseToolsets(toolsetsFlag string, logger *log.Logger) []string { + rawToolsets := strings.Split(toolsetsFlag, ",") + + cleaned, invalid := toolsets.CleanToolsets(rawToolsets) + if len(invalid) > 0 { + logger.Warnf("Invalid toolsets ignored: %v", invalid) + } + + expanded := toolsets.ExpandDefaultToolset(cleaned) + + logger.Infof("Enabled toolsets: %v", expanded) + return expanded +} + +func getToolsetsFromCmd(cmd *cobra.Command, logger *log.Logger) []string { + toolsetsFlag, err := cmd.Flags().GetString("toolsets") + if err != nil { + toolsetsFlag, err = cmd.Root().PersistentFlags().GetString("toolsets") + if err != nil { + logger.Warnf("Failed to get toolsets flag, using default: %v", err) + toolsetsFlag = "default" + } + } + return parseToolsets(toolsetsFlag, logger) +} + // runDefaultCommand handles the default behavior when no subcommand is provided func runDefaultCommand(cmd *cobra.Command, _ []string) { // Default to stdio mode when no subcommand is provided @@ -92,7 +120,10 @@ func runDefaultCommand(cmd *cobra.Command, _ []string) { stdlog.Fatal("Failed to initialize logger:", err) } - if err := runStdioServer(logger); err != nil { + // Get toolsets from the command that was passed in + enabledToolsets := getToolsetsFromCmd(cmd, logger) + + if err := runStdioServer(logger, enabledToolsets); err != nil { stdlog.Fatal("failed to run stdio server:", err) } } @@ -110,7 +141,9 @@ func main() { stdlog.Fatal("Failed to initialize logger:", err) } - if err := runHTTPServer(logger, host, port, endpointPath); err != nil { + enabledToolsets := getToolsetsFromCmd(rootCmd, logger) + + if err := runHTTPServer(logger, host, port, endpointPath, enabledToolsets); err != nil { stdlog.Fatal("failed to run StreamableHTTP server:", err) } return diff --git a/pkg/tools/dynamic_tool.go b/pkg/tools/dynamic_tool.go index 59f6c77a..795339b8 100644 --- a/pkg/tools/dynamic_tool.go +++ b/pkg/tools/dynamic_tool.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-mcp-server/pkg/client" tfeTools "github.com/hashicorp/terraform-mcp-server/pkg/tools/tfe" + "github.com/hashicorp/terraform-mcp-server/pkg/toolsets" "github.com/hashicorp/terraform-mcp-server/pkg/utils" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -23,17 +24,19 @@ type DynamicToolRegistry struct { tfeToolsRegistered bool mcpServer *server.MCPServer logger *log.Logger + enabledToolsets []string } var globalToolRegistry *DynamicToolRegistry // registerDynamicTools registers the global tool registry -func registerDynamicTools(mcpServer *server.MCPServer, logger *log.Logger) { +func registerDynamicTools(mcpServer *server.MCPServer, logger *log.Logger, enabledToolsets []string) { globalToolRegistry = &DynamicToolRegistry{ sessionsWithTFE: make(map[string]bool), tfeToolsRegistered: false, mcpServer: mcpServer, logger: logger, + enabledToolsets: enabledToolsets, } // Set the callback in the client package to avoid circular imports @@ -102,107 +105,157 @@ func (r *DynamicToolRegistry) registerTFETools() { r.logger.Info("Registering TFE tools - first session with valid TFE client detected") - // Create TFE tools with dynamic availability checking - listTerraformOrgsTool := r.createDynamicTFETool("list_terraform_orgs", tfeTools.ListTerraformOrgs) - r.mcpServer.AddTool(listTerraformOrgsTool.Tool, listTerraformOrgsTool.Handler) + // Terraform toolset - Organization and Project tools + if toolsets.IsToolEnabled("list_terraform_orgs", r.enabledToolsets) { + tool := r.createDynamicTFETool("list_terraform_orgs", tfeTools.ListTerraformOrgs) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - listTerraformProjectsTool := r.createDynamicTFETool("list_terraform_projects", tfeTools.ListTerraformProjects) - r.mcpServer.AddTool(listTerraformProjectsTool.Tool, listTerraformProjectsTool.Handler) + if toolsets.IsToolEnabled("list_terraform_projects", r.enabledToolsets) { + tool := r.createDynamicTFETool("list_terraform_projects", tfeTools.ListTerraformProjects) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - // Workspace management tools - ListWorkspacesTool := r.createDynamicTFETool("list_workspaces", tfeTools.ListWorkspaces) - r.mcpServer.AddTool(ListWorkspacesTool.Tool, ListWorkspacesTool.Handler) + // Terraform toolset - Workspace management tools + if toolsets.IsToolEnabled("list_workspaces", r.enabledToolsets) { + tool := r.createDynamicTFETool("list_workspaces", tfeTools.ListWorkspaces) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - getWorkspaceDetailsTool := r.createDynamicTFETool("get_workspace_details", tfeTools.GetWorkspaceDetails) - r.mcpServer.AddTool(getWorkspaceDetailsTool.Tool, getWorkspaceDetailsTool.Handler) + if toolsets.IsToolEnabled("get_workspace_details", r.enabledToolsets) { + tool := r.createDynamicTFETool("get_workspace_details", tfeTools.GetWorkspaceDetails) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - createWorkspaceTool := r.createDynamicTFETool("create_workspace", tfeTools.CreateWorkspace) - r.mcpServer.AddTool(createWorkspaceTool.Tool, createWorkspaceTool.Handler) + if toolsets.IsToolEnabled("create_workspace", r.enabledToolsets) { + tool := r.createDynamicTFETool("create_workspace", tfeTools.CreateWorkspace) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - updateWorkspaceTool := r.createDynamicTFETool("update_workspace", tfeTools.UpdateWorkspace) - r.mcpServer.AddTool(updateWorkspaceTool.Tool, updateWorkspaceTool.Handler) + if toolsets.IsToolEnabled("update_workspace", r.enabledToolsets) { + tool := r.createDynamicTFETool("update_workspace", tfeTools.UpdateWorkspace) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - // Only register delete_workspace_safely if TF operations are enabled - if isTerraformOperationsEnabled() { - deleteWorkspaceSafelyTool := r.createDynamicTFETool("delete_workspace_safely", tfeTools.DeleteWorkspaceSafely) - r.mcpServer.AddTool(deleteWorkspaceSafelyTool.Tool, deleteWorkspaceSafelyTool.Handler) + // Only register delete_workspace_safely if TF operations are enabled AND toolset is enabled + if isTerraformOperationsEnabled() && toolsets.IsToolEnabled("delete_workspace_safely", r.enabledToolsets) { + tool := r.createDynamicTFETool("delete_workspace_safely", tfeTools.DeleteWorkspaceSafely) + r.mcpServer.AddTool(tool.Tool, tool.Handler) } - // Private provider tools - searchPrivateProvidersTool := r.createDynamicTFETool("search_private_providers", tfeTools.SearchPrivateProviders) - r.mcpServer.AddTool(searchPrivateProvidersTool.Tool, searchPrivateProvidersTool.Handler) + // Registry-private toolset - Private provider tools + if toolsets.IsToolEnabled("search_private_providers", r.enabledToolsets) { + tool := r.createDynamicTFETool("search_private_providers", tfeTools.SearchPrivateProviders) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - getPrivateProviderDetailsTool := r.createDynamicTFETool("get_private_provider_details", tfeTools.GetPrivateProviderDetails) - r.mcpServer.AddTool(getPrivateProviderDetailsTool.Tool, getPrivateProviderDetailsTool.Handler) + if toolsets.IsToolEnabled("get_private_provider_details", r.enabledToolsets) { + tool := r.createDynamicTFETool("get_private_provider_details", tfeTools.GetPrivateProviderDetails) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - // Private module tools - searchPrivateModulesTool := r.createDynamicTFETool("search_private_modules", tfeTools.SearchPrivateModules) - r.mcpServer.AddTool(searchPrivateModulesTool.Tool, searchPrivateModulesTool.Handler) + // Registry-private toolset - Private module tools + if toolsets.IsToolEnabled("search_private_modules", r.enabledToolsets) { + tool := r.createDynamicTFETool("search_private_modules", tfeTools.SearchPrivateModules) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - getPrivateModuleDetailsTool := r.createDynamicTFETool("get_private_module_details", tfeTools.GetPrivateModuleDetails) - r.mcpServer.AddTool(getPrivateModuleDetailsTool.Tool, getPrivateModuleDetailsTool.Handler) + if toolsets.IsToolEnabled("get_private_module_details", r.enabledToolsets) { + tool := r.createDynamicTFETool("get_private_module_details", tfeTools.GetPrivateModuleDetails) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - // Workspace tags tools - createWorkspaceTagsTool := r.createDynamicTFETool("create_workspace_tags", tfeTools.CreateWorkspaceTags) - r.mcpServer.AddTool(createWorkspaceTagsTool.Tool, createWorkspaceTagsTool.Handler) + // Terraform toolset - Workspace tags tools + if toolsets.IsToolEnabled("create_workspace_tags", r.enabledToolsets) { + tool := r.createDynamicTFETool("create_workspace_tags", tfeTools.CreateWorkspaceTags) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - readWorkspaceTagsTool := r.createDynamicTFETool("read_workspace_tags", tfeTools.ReadWorkspaceTags) - r.mcpServer.AddTool(readWorkspaceTagsTool.Tool, readWorkspaceTagsTool.Handler) + if toolsets.IsToolEnabled("read_workspace_tags", r.enabledToolsets) { + tool := r.createDynamicTFETool("read_workspace_tags", tfeTools.ReadWorkspaceTags) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - // Terraform run tools - listRunsTool := r.createDynamicTFETool("list_runs", tfeTools.ListRuns) - r.mcpServer.AddTool(listRunsTool.Tool, listRunsTool.Handler) + // Terraform toolset - Run tools + if toolsets.IsToolEnabled("list_runs", r.enabledToolsets) { + tool := r.createDynamicTFETool("list_runs", tfeTools.ListRuns) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } // Create run tool with conditional options based on TF operations setting - var createRunTool server.ServerTool - if isTerraformOperationsEnabled() { - createRunTool = r.createDynamicTFETool("create_run", tfeTools.CreateRun) - } else { - createRunTool = r.createDynamicTFETool("create_run", tfeTools.CreateRunSafe) + if toolsets.IsToolEnabled("create_run", r.enabledToolsets) { + var tool server.ServerTool + if isTerraformOperationsEnabled() { + tool = r.createDynamicTFETool("create_run", tfeTools.CreateRun) + } else { + tool = r.createDynamicTFETool("create_run", tfeTools.CreateRunSafe) + } + r.mcpServer.AddTool(tool.Tool, tool.Handler) } - r.mcpServer.AddTool(createRunTool.Tool, createRunTool.Handler) - // Only register action_run if TF operations are enabled - if isTerraformOperationsEnabled() { - actionRunTool := r.createDynamicTFETool("action_run", tfeTools.ActionRun) - r.mcpServer.AddTool(actionRunTool.Tool, actionRunTool.Handler) + // Only register action_run if TF operations are enabled AND toolset is enabled + if isTerraformOperationsEnabled() && toolsets.IsToolEnabled("action_run", r.enabledToolsets) { + tool := r.createDynamicTFETool("action_run", tfeTools.ActionRun) + r.mcpServer.AddTool(tool.Tool, tool.Handler) } - createNoCodeWorkspace := r.createDynamicTFEToolWithElicitation("create_no_code_workspace", tfeTools.CreateNoCodeWorkspace) - r.mcpServer.AddTool(createNoCodeWorkspace.Tool, createNoCodeWorkspace.Handler) + if toolsets.IsToolEnabled("create_no_code_workspace", r.enabledToolsets) { + tool := r.createDynamicTFEToolWithElicitation("create_no_code_workspace", tfeTools.CreateNoCodeWorkspace) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - getRunDetailsTool := r.createDynamicTFETool("get_run_details", tfeTools.GetRunDetails) - r.mcpServer.AddTool(getRunDetailsTool.Tool, getRunDetailsTool.Handler) + if toolsets.IsToolEnabled("get_run_details", r.enabledToolsets) { + tool := r.createDynamicTFETool("get_run_details", tfeTools.GetRunDetails) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - // Variable set tools - listVariableSetsTool := r.createDynamicTFETool("list_variable_sets", tfeTools.ListVariableSets) - r.mcpServer.AddTool(listVariableSetsTool.Tool, listVariableSetsTool.Handler) + // Terraform toolset - Variable set tools + if toolsets.IsToolEnabled("list_variable_sets", r.enabledToolsets) { + tool := r.createDynamicTFETool("list_variable_sets", tfeTools.ListVariableSets) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - createVariableSetTool := r.createDynamicTFETool("create_variable_set", tfeTools.CreateVariableSet) - r.mcpServer.AddTool(createVariableSetTool.Tool, createVariableSetTool.Handler) + if toolsets.IsToolEnabled("create_variable_set", r.enabledToolsets) { + tool := r.createDynamicTFETool("create_variable_set", tfeTools.CreateVariableSet) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - createVariableInVariableSetTool := r.createDynamicTFETool("create_variable_in_variable_set", tfeTools.CreateVariableInVariableSet) - r.mcpServer.AddTool(createVariableInVariableSetTool.Tool, createVariableInVariableSetTool.Handler) + if toolsets.IsToolEnabled("create_variable_in_variable_set", r.enabledToolsets) { + tool := r.createDynamicTFETool("create_variable_in_variable_set", tfeTools.CreateVariableInVariableSet) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - deleteVariableInVariableSetTool := r.createDynamicTFETool("delete_variable_in_variable_set", tfeTools.DeleteVariableInVariableSet) - r.mcpServer.AddTool(deleteVariableInVariableSetTool.Tool, deleteVariableInVariableSetTool.Handler) + if toolsets.IsToolEnabled("delete_variable_in_variable_set", r.enabledToolsets) { + tool := r.createDynamicTFETool("delete_variable_in_variable_set", tfeTools.DeleteVariableInVariableSet) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } // Attach/detach variable sets to/from workspaces - attachVariableSetTool := r.createDynamicTFETool("attach_variable_set_to_workspaces", tfeTools.AttachVariableSetToWorkspaces) - r.mcpServer.AddTool(attachVariableSetTool.Tool, attachVariableSetTool.Handler) + if toolsets.IsToolEnabled("attach_variable_set_to_workspaces", r.enabledToolsets) { + tool := r.createDynamicTFETool("attach_variable_set_to_workspaces", tfeTools.AttachVariableSetToWorkspaces) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - detachVariableSetTool := r.createDynamicTFETool("detach_variable_set_from_workspaces", tfeTools.DetachVariableSetFromWorkspaces) - r.mcpServer.AddTool(detachVariableSetTool.Tool, detachVariableSetTool.Handler) + if toolsets.IsToolEnabled("detach_variable_set_from_workspaces", r.enabledToolsets) { + tool := r.createDynamicTFETool("detach_variable_set_from_workspaces", tfeTools.DetachVariableSetFromWorkspaces) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - // Variable tools - listWorkspaceVariablesTool := r.createDynamicTFETool("list_workspace_variables", tfeTools.ListWorkspaceVariables) - r.mcpServer.AddTool(listWorkspaceVariablesTool.Tool, listWorkspaceVariablesTool.Handler) + // Terraform toolset - Variable tools + if toolsets.IsToolEnabled("list_workspace_variables", r.enabledToolsets) { + tool := r.createDynamicTFETool("list_workspace_variables", tfeTools.ListWorkspaceVariables) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - createWorkspaceVariableTool := r.createDynamicTFETool("create_workspace_variable", tfeTools.CreateWorkspaceVariable) - r.mcpServer.AddTool(createWorkspaceVariableTool.Tool, createWorkspaceVariableTool.Handler) + if toolsets.IsToolEnabled("create_workspace_variable", r.enabledToolsets) { + tool := r.createDynamicTFETool("create_workspace_variable", tfeTools.CreateWorkspaceVariable) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } - updateWorkspaceVariableTool := r.createDynamicTFETool("update_workspace_variable", tfeTools.UpdateWorkspaceVariable) - r.mcpServer.AddTool(updateWorkspaceVariableTool.Tool, updateWorkspaceVariableTool.Handler) + if toolsets.IsToolEnabled("update_workspace_variable", r.enabledToolsets) { + tool := r.createDynamicTFETool("update_workspace_variable", tfeTools.UpdateWorkspaceVariable) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } r.tfeToolsRegistered = true } diff --git a/pkg/tools/tools.go b/pkg/tools/tools.go index da97de6a..008d1561 100644 --- a/pkg/tools/tools.go +++ b/pkg/tools/tools.go @@ -5,41 +5,60 @@ package tools import ( registryTools "github.com/hashicorp/terraform-mcp-server/pkg/tools/registry" + "github.com/hashicorp/terraform-mcp-server/pkg/toolsets" "github.com/mark3labs/mcp-go/server" log "github.com/sirupsen/logrus" ) -func RegisterTools(hcServer *server.MCPServer, logger *log.Logger) { - // Register the dynamic tool - registerDynamicTools(hcServer, logger) +func RegisterTools(hcServer *server.MCPServer, logger *log.Logger, enabledToolsets []string) { + // Register the dynamic tools (TFE tools that require authentication) + registerDynamicTools(hcServer, logger, enabledToolsets) - // Provider tools (always available) - getResolveProviderDocIDTool := registryTools.ResolveProviderDocID(logger) - hcServer.AddTool(getResolveProviderDocIDTool.Tool, getResolveProviderDocIDTool.Handler) + // Registry toolset - Provider tools + if toolsets.IsToolEnabled("search_providers", enabledToolsets) { + tool := registryTools.ResolveProviderDocID(logger) + hcServer.AddTool(tool.Tool, tool.Handler) + } - getProviderDocsTool := registryTools.GetProviderDocs(logger) - hcServer.AddTool(getProviderDocsTool.Tool, getProviderDocsTool.Handler) + if toolsets.IsToolEnabled("get_provider_details", enabledToolsets) { + tool := registryTools.GetProviderDocs(logger) + hcServer.AddTool(tool.Tool, tool.Handler) + } - getLatestProviderVersionTool := registryTools.GetLatestProviderVersion(logger) - hcServer.AddTool(getLatestProviderVersionTool.Tool, getLatestProviderVersionTool.Handler) + if toolsets.IsToolEnabled("get_latest_provider_version", enabledToolsets) { + tool := registryTools.GetLatestProviderVersion(logger) + hcServer.AddTool(tool.Tool, tool.Handler) + } - getProviderCapabilitiesTool := registryTools.GetProviderCapabilities(logger) - hcServer.AddTool(getProviderCapabilitiesTool.Tool, getProviderCapabilitiesTool.Handler) + if toolsets.IsToolEnabled("get_provider_capabilities", enabledToolsets) { + tool := registryTools.GetProviderCapabilities(logger) + hcServer.AddTool(tool.Tool, tool.Handler) + } - // Module tools - getSearchModulesTool := registryTools.SearchModules(logger) - hcServer.AddTool(getSearchModulesTool.Tool, getSearchModulesTool.Handler) + // Registry toolset - Module tools + if toolsets.IsToolEnabled("search_modules", enabledToolsets) { + tool := registryTools.SearchModules(logger) + hcServer.AddTool(tool.Tool, tool.Handler) + } - getModuleDetailsTool := registryTools.ModuleDetails(logger) - hcServer.AddTool(getModuleDetailsTool.Tool, getModuleDetailsTool.Handler) + if toolsets.IsToolEnabled("get_module_details", enabledToolsets) { + tool := registryTools.ModuleDetails(logger) + hcServer.AddTool(tool.Tool, tool.Handler) + } - getLatestModuleVersionTool := registryTools.GetLatestModuleVersion(logger) - hcServer.AddTool(getLatestModuleVersionTool.Tool, getLatestModuleVersionTool.Handler) + if toolsets.IsToolEnabled("get_latest_module_version", enabledToolsets) { + tool := registryTools.GetLatestModuleVersion(logger) + hcServer.AddTool(tool.Tool, tool.Handler) + } - // Policy tools - getSearchPoliciesTool := registryTools.SearchPolicies(logger) - hcServer.AddTool(getSearchPoliciesTool.Tool, getSearchPoliciesTool.Handler) + // Registry toolset - Policy tools + if toolsets.IsToolEnabled("search_policies", enabledToolsets) { + tool := registryTools.SearchPolicies(logger) + hcServer.AddTool(tool.Tool, tool.Handler) + } - getPolicyDetailsTool := registryTools.PolicyDetails(logger) - hcServer.AddTool(getPolicyDetailsTool.Tool, getPolicyDetailsTool.Handler) + if toolsets.IsToolEnabled("get_policy_details", enabledToolsets) { + tool := registryTools.PolicyDetails(logger) + hcServer.AddTool(tool.Tool, tool.Handler) + } } diff --git a/pkg/toolsets/mapping.go b/pkg/toolsets/mapping.go new file mode 100644 index 00000000..54c771f1 --- /dev/null +++ b/pkg/toolsets/mapping.go @@ -0,0 +1,70 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toolsets + +var ToolToToolset = map[string]string{ + // Public Registry tools (providers, modules, policies) + "search_providers": Registry, + "get_provider_details": Registry, + "get_latest_provider_version": Registry, + "get_provider_capabilities": Registry, + "search_modules": Registry, + "get_module_details": Registry, + "get_latest_module_version": Registry, + "search_policies": Registry, + "get_policy_details": Registry, + + // Private Registry tools (TFE/TFC private registry) + "search_private_modules": RegistryPrivate, + "get_private_module_details": RegistryPrivate, + "search_private_providers": RegistryPrivate, + "get_private_provider_details": RegistryPrivate, + + // Terraform tools (TFE/TFC workspaces, runs, variables, etc.) + "list_terraform_orgs": Terraform, + "list_terraform_projects": Terraform, + "list_workspaces": Terraform, + "get_workspace_details": Terraform, + "create_workspace": Terraform, + "create_no_code_workspace": Terraform, + "update_workspace": Terraform, + "delete_workspace_safely": Terraform, + "list_runs": Terraform, + "get_run_details": Terraform, + "create_run": Terraform, + "action_run": Terraform, + "list_workspace_variables": Terraform, + "create_workspace_variable": Terraform, + "update_workspace_variable": Terraform, + "list_variable_sets": Terraform, + "create_variable_set": Terraform, + "create_variable_in_variable_set": Terraform, + "delete_variable_in_variable_set": Terraform, + "attach_variable_set_to_workspaces": Terraform, + "detach_variable_set_from_workspaces": Terraform, + "create_workspace_tags": Terraform, + "read_workspace_tags": Terraform, +} + +// GetToolsetForTool returns the toolset name for a given tool name +func GetToolsetForTool(toolName string) (string, bool) { + toolset, exists := ToolToToolset[toolName] + return toolset, exists +} + +// IsToolEnabled checks if a tool is enabled based on the enabled toolsets +func IsToolEnabled(toolName string, enabledToolsets []string) bool { + if ContainsToolset(enabledToolsets, All) { + return true + } + + // Look up which toolset this tool belongs to + toolset, exists := GetToolsetForTool(toolName) + if !exists { + return false + } + + // Check if the tool's toolset is enabled + return ContainsToolset(enabledToolsets, toolset) +} diff --git a/pkg/toolsets/toolsets.go b/pkg/toolsets/toolsets.go new file mode 100644 index 00000000..82efe6d7 --- /dev/null +++ b/pkg/toolsets/toolsets.go @@ -0,0 +1,155 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toolsets + +import "strings" + +const ( + // Core toolsets + Registry = "registry" + RegistryPrivate = "registry-private" // Private registry (TFE/TFC) + Terraform = "terraform" // TFE/TFC operations + + // Special toolsets + All = "all" + Default = "default" +) + +// Toolset represents metadata about a toolset +type Toolset struct { + Name string + Description string +} + +var ( + AllToolset = Toolset{ + Name: All, + Description: "Special toolset that enables all available toolsets", + } + DefaultToolset = Toolset{ + Name: Default, + Description: "Special toolset that enables the default toolset configuration", + } + RegistryToolset = Toolset{ + Name: Registry, + Description: "Public Terraform Registry (providers, modules, policies)", + } + RegistryPrivateToolset = Toolset{ + Name: RegistryPrivate, + Description: "Private registry access (TFE/TFC private modules and providers)", + } + TerraformToolset = Toolset{ + Name: Terraform, + Description: "HCP Terraform/TFE operations (workspaces, runs, variables, etc.)", + } +) + +func AvailableToolsets() []Toolset { + return []Toolset{ + RegistryToolset, + RegistryPrivateToolset, + TerraformToolset, + } +} + +// DefaultToolsets returns the default set of enabled toolsets +func DefaultToolsets() []string { + return []string{Registry} +} + +func GetValidToolsetNames() map[string]bool { + validNames := make(map[string]bool) + for _, ts := range AvailableToolsets() { + validNames[ts.Name] = true + } + validNames[AllToolset.Name] = true + validNames[DefaultToolset.Name] = true + return validNames +} + +func CleanToolsets(enabledToolsets []string) ([]string, []string) { + seen := make(map[string]bool) + result := make([]string, 0, len(enabledToolsets)) + invalid := make([]string, 0) + validNames := GetValidToolsetNames() + + for _, toolset := range enabledToolsets { + trimmed := strings.TrimSpace(toolset) + if trimmed == "" { + continue + } + if !seen[trimmed] { + seen[trimmed] = true + result = append(result, trimmed) + if !validNames[trimmed] { + invalid = append(invalid, trimmed) + } + } + } + + return result, invalid +} + +func ExpandDefaultToolset(toolsets []string) []string { + hasDefault := false + seen := make(map[string]bool) + + for _, ts := range toolsets { + seen[ts] = true + if ts == Default { + hasDefault = true + } + } + + if !hasDefault { + return toolsets + } + + result := make([]string, 0, len(toolsets)) + for _, ts := range toolsets { + if ts != Default { + result = append(result, ts) + } + } + + for _, defaultTS := range DefaultToolsets() { + if !seen[defaultTS] { + result = append(result, defaultTS) + } + } + + return result +} + +// ContainsToolset checks if a toolset is in the list +func ContainsToolset(toolsets []string, toCheck string) bool { + for _, ts := range toolsets { + if ts == toCheck { + return true + } + } + return false +} + +// GenerateToolsetsHelp generates help text for the toolsets flag +func GenerateToolsetsHelp() string { + defaultTools := strings.Join(DefaultToolsets(), ", ") + + allToolsets := AvailableToolsets() + var toolsetNames []string + for _, ts := range allToolsets { + toolsetNames = append(toolsetNames, ts.Name) + } + availableTools := strings.Join(toolsetNames, ", ") + + return "Comma-separated list of tool groups to enable.\n" + + "Available: " + availableTools + "\n" + + "Special toolset keywords:\n" + + " - all: Enables all available toolsets\n" + + " - default: Enables the default toolset configuration (" + defaultTools + ")\n" + + "Examples:\n" + + " - --toolsets=registry,terraform\n" + + " - --toolsets=default,registry-private\n" + + " - --toolsets=all" +} diff --git a/pkg/toolsets/toolsets_test.go b/pkg/toolsets/toolsets_test.go new file mode 100644 index 00000000..3f4276fa --- /dev/null +++ b/pkg/toolsets/toolsets_test.go @@ -0,0 +1,229 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toolsets + +import ( + "reflect" + "testing" +) + +func TestCleanToolsets(t *testing.T) { + tests := []struct { + name string + input []string + expectedValid []string + expectedInvalid []string + }{ + { + name: "valid toolsets", + input: []string{"registry", "terraform"}, + expectedValid: []string{"registry", "terraform"}, + expectedInvalid: []string{}, + }, + { + name: "invalid toolsets", + input: []string{"invalid", "fake"}, + expectedValid: []string{"invalid", "fake"}, + expectedInvalid: []string{"invalid", "fake"}, + }, + { + name: "mixed valid and invalid", + input: []string{"registry", "invalid", "terraform"}, + expectedValid: []string{"registry", "invalid", "terraform"}, + expectedInvalid: []string{"invalid"}, + }, + { + name: "empty strings", + input: []string{"registry", "", "terraform", " "}, + expectedValid: []string{"registry", "terraform"}, + expectedInvalid: []string{}, + }, + { + name: "duplicates", + input: []string{"registry", "registry", "terraform"}, + expectedValid: []string{"registry", "terraform"}, + expectedInvalid: []string{}, + }, + { + name: "whitespace trimming", + input: []string{" registry ", " terraform "}, + expectedValid: []string{"registry", "terraform"}, + expectedInvalid: []string{}, + }, + { + name: "special toolsets", + input: []string{"all", "default"}, + expectedValid: []string{"all", "default"}, + expectedInvalid: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + valid, invalid := CleanToolsets(tt.input) + + if !reflect.DeepEqual(valid, tt.expectedValid) { + t.Errorf("CleanToolsets() valid = %v, want %v", valid, tt.expectedValid) + } + + if !reflect.DeepEqual(invalid, tt.expectedInvalid) { + t.Errorf("CleanToolsets() invalid = %v, want %v", invalid, tt.expectedInvalid) + } + }) + } +} + +func TestExpandDefaultToolset(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + { + name: "no default keyword", + input: []string{"registry", "terraform"}, + expected: []string{"registry", "terraform"}, + }, + { + name: "default keyword only", + input: []string{"default"}, + expected: []string{"registry"}, + }, + { + name: "default with additional toolsets", + input: []string{"default", "terraform"}, + expected: []string{"terraform", "registry"}, + }, + { + name: "default with registry already included", + input: []string{"default", "registry", "terraform"}, + expected: []string{"registry", "terraform"}, + }, + { + name: "empty input", + input: []string{}, + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ExpandDefaultToolset(tt.input) + + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("ExpandDefaultToolset() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestContainsToolset(t *testing.T) { + tests := []struct { + name string + toolsets []string + toCheck string + expected bool + }{ + { + name: "toolset present", + toolsets: []string{"registry", "terraform"}, + toCheck: "registry", + expected: true, + }, + { + name: "toolset not present", + toolsets: []string{"registry", "terraform"}, + toCheck: "registry-private", + expected: false, + }, + { + name: "empty list", + toolsets: []string{}, + toCheck: "registry", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ContainsToolset(tt.toolsets, tt.toCheck) + + if result != tt.expected { + t.Errorf("ContainsToolset() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestGetValidToolsetNames(t *testing.T) { + validNames := GetValidToolsetNames() + + // Check that all expected toolsets are present + expected := []string{"registry", "registry-private", "terraform", "all", "default"} + for _, name := range expected { + if !validNames[name] { + t.Errorf("GetValidToolsetNames() missing expected toolset: %s", name) + } + } + + if len(validNames) != len(expected) { + t.Errorf("GetValidToolsetNames() returned %d toolsets, want %d", len(validNames), len(expected)) + } +} + +func TestIsToolEnabled(t *testing.T) { + tests := []struct { + name string + toolName string + enabledToolsets []string + expected bool + }{ + { + name: "tool enabled - registry", + toolName: "search_providers", + enabledToolsets: []string{"registry"}, + expected: true, + }, + { + name: "tool disabled", + toolName: "search_providers", + enabledToolsets: []string{"terraform"}, + expected: false, + }, + { + name: "all toolset enables everything", + toolName: "search_providers", + enabledToolsets: []string{"all"}, + expected: true, + }, + { + name: "unknown tool", + toolName: "unknown_tool", + enabledToolsets: []string{"registry"}, + expected: false, + }, + { + name: "terraform tool", + toolName: "list_workspaces", + enabledToolsets: []string{"terraform"}, + expected: true, + }, + { + name: "private registry tool", + toolName: "search_private_modules", + enabledToolsets: []string{"registry-private"}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsToolEnabled(tt.toolName, tt.enabledToolsets) + + if result != tt.expected { + t.Errorf("IsToolEnabled(%s, %v) = %v, want %v", tt.toolName, tt.enabledToolsets, result, tt.expected) + } + }) + } +} From af46bc04f15a2d93e378d006dc97720085b3ff63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:01:08 -0700 Subject: [PATCH 23/28] Bump github.com/hashicorp/go-tfe from 1.96.0 to 1.97.0 (#234) Bumps [github.com/hashicorp/go-tfe](https://github.com/hashicorp/go-tfe) from 1.96.0 to 1.97.0. - [Release notes](https://github.com/hashicorp/go-tfe/releases) - [Changelog](https://github.com/hashicorp/go-tfe/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/go-tfe/compare/v1.96.0...1.97.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/go-tfe dependency-version: 1.97.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index af9526e6..3ed9b1d2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.4 require ( github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-retryablehttp v0.7.8 - github.com/hashicorp/go-tfe v1.96.0 + github.com/hashicorp/go-tfe v1.97.0 github.com/hashicorp/jsonapi v1.5.0 github.com/mark3labs/mcp-go v0.43.1 github.com/sirupsen/logrus v1.9.3 @@ -24,7 +24,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-slug v0.16.8 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/go-version v1.8.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect github.com/mailru/easyjson v0.9.1 // indirect diff --git a/go.sum b/go.sum index 2fa0e8bc..25f21257 100644 --- a/go.sum +++ b/go.sum @@ -30,12 +30,12 @@ github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVU github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-slug v0.16.8 h1:f4/sDZqRsxx006HrE6e9BE5xO9lWXydKhVoH6Kb0v1M= github.com/hashicorp/go-slug v0.16.8/go.mod h1:hB4mUcVHl4RPu0205s0fwmB9i31MxQgeafGkko3FD+Y= -github.com/hashicorp/go-tfe v1.96.0 h1:goTDOZIQ8rsf1vRQXvqvK8v/inD4SQe5T2vcEX1q2MU= -github.com/hashicorp/go-tfe v1.96.0/go.mod h1:umRhpwmiMAa5Dhu8dzF0itJfBZHJPoTmS8BpNZs9+2Y= +github.com/hashicorp/go-tfe v1.97.0 h1:UmIUUPuWAVPKxTa9N5qTUWd7FblKbgLq+HBio1X7/Qc= +github.com/hashicorp/go-tfe v1.97.0/go.mod h1:nmGZMS3pdU7gPPmoe1xYhzU9O2BmasV36XggDOSCDW0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= -github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/jsonapi v1.5.0 h1:toO1EpzVl1b3xTjC/Tw4XMIlHgJreeTnyb1a1sHnlPk= github.com/hashicorp/jsonapi v1.5.0/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= From 82fd8533f4a3ed61fafb91f5c3a41ef47bc4eecc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:01:45 -0700 Subject: [PATCH 24/28] Bump github.com/spf13/cobra from 1.10.1 to 1.10.2 (#235) Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.10.1 to 1.10.2. - [Release notes](https://github.com/spf13/cobra/releases) - [Commits](https://github.com/spf13/cobra/compare/v1.10.1...v1.10.2) --- updated-dependencies: - dependency-name: github.com/spf13/cobra dependency-version: 1.10.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3ed9b1d2..b19a05b1 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/hashicorp/jsonapi v1.5.0 github.com/mark3labs/mcp-go v0.43.1 github.com/sirupsen/logrus v1.9.3 - github.com/spf13/cobra v1.10.1 + github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 golang.org/x/time v0.14.0 diff --git a/go.sum b/go.sum index 25f21257..468b5cb0 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,8 @@ github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= From 5112697c08c03a285ba840907802a89da17d836e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:02:12 -0700 Subject: [PATCH 25/28] Bump github.com/mark3labs/mcp-go from 0.43.1 to 0.43.2 (#236) Bumps [github.com/mark3labs/mcp-go](https://github.com/mark3labs/mcp-go) from 0.43.1 to 0.43.2. - [Release notes](https://github.com/mark3labs/mcp-go/releases) - [Commits](https://github.com/mark3labs/mcp-go/compare/v0.43.1...v0.43.2) --- updated-dependencies: - dependency-name: github.com/mark3labs/mcp-go dependency-version: 0.43.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b19a05b1..2c996109 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/go-tfe v1.97.0 github.com/hashicorp/jsonapi v1.5.0 - github.com/mark3labs/mcp-go v0.43.1 + github.com/mark3labs/mcp-go v0.43.2 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 diff --git a/go.sum b/go.sum index 468b5cb0..ae58f290 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mark3labs/mcp-go v0.43.1 h1:WXNVd+bRM/7mOzCM9zulSwn/s9YEdAxbmeh9LoRHEXY= -github.com/mark3labs/mcp-go v0.43.1/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= +github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I= +github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= From 43c2410e75dff2b44d0ad6cf305b6a3a5c982a17 Mon Sep 17 00:00:00 2001 From: Gautam Date: Mon, 15 Dec 2025 12:55:15 -0800 Subject: [PATCH 26/28] fixing tests, failing because policy name changed (#244) --- e2e/payloads.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/payloads.go b/e2e/payloads.go index c3fb6451..bbce6a1c 100644 --- a/e2e/payloads.go +++ b/e2e/payloads.go @@ -423,7 +423,7 @@ var searchPoliciesTestCases = []RegistryTestCase{ TestShouldFail: false, TestDescription: "Testing search_policies with policy name containing spaces", TestPayload: map[string]interface{}{ - "policy_query": "FSBP Foundations benchmark", + "policy_query": "Foundational Security Best Practices(FSBP)", }, }, } From 745a90c24bc1045a6d54f902ae2bcf9bd29db46c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:31:33 -0800 Subject: [PATCH 27/28] Bump golang.org/x/text from 0.31.0 to 0.32.0 (#239) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.31.0 to 0.32.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.31.0...v0.32.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-version: 0.32.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gautam --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 2c996109..13d8686e 100644 --- a/go.mod +++ b/go.mod @@ -38,9 +38,9 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/sync v0.18.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.31.0 + golang.org/x/text v0.32.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ae58f290..dbf899af 100644 --- a/go.sum +++ b/go.sum @@ -92,13 +92,13 @@ github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zI github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 18ad8430409eb1ddcf6fab93ada43ac1a1e5aa52 Mon Sep 17 00:00:00 2001 From: Gautam Date: Mon, 15 Dec 2025 21:06:30 -0800 Subject: [PATCH 28/28] Skip TLS verify wasn't passing the flag thru, should fix #241 (#243) * should fix #241 * adding changelog --- CHANGELOG.md | 4 ++++ pkg/client/common.go | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2fac1a..1b1a84b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ FEATURES * **Toolsets Flag**: Added `--toolsets` flag to selectively enable tool groups. Three toolset groups are available: `registry` (public Terraform Registry), `registry-private` (private TFE/TFC registry), and `terraform` (TFE/TFC operations). Default is `registry` only. +FIXES + +* Skip TLS flag was not propogated properly [243](https://github.com/hashicorp/terraform-mcp-server/issues/243) + ## 0.3.3 (Nov 21, 2025) IMPROVEMENTS diff --git a/pkg/client/common.go b/pkg/client/common.go index fb292f4f..acd7aece 100644 --- a/pkg/client/common.go +++ b/pkg/client/common.go @@ -90,7 +90,10 @@ func GetProviderResourceDocs(httpClient *http.Client, providerDocsID string, log func parseTerraformSkipTLSVerify(ctx context.Context) bool { terraformSkipTLSVerifyStr, ok := ctx.Value(contextKey(TerraformSkipTLSVerify)).(string) - if ok && terraformSkipTLSVerifyStr != "" { + if !ok || terraformSkipTLSVerifyStr == "" { + terraformSkipTLSVerifyStr = utils.GetEnv(TerraformSkipTLSVerify, "") + } + if terraformSkipTLSVerifyStr != "" { terraformSkipTLSVerify, err := strconv.ParseBool(terraformSkipTLSVerifyStr) if err == nil { return terraformSkipTLSVerify