Skip to content

Commit fc03634

Browse files
authored
Feature/Variables (#170)
* adding variable/variable set tools * adding changelog * fixing varsets * splitting the PR * fixing changelog
1 parent 48b1e44 commit fc03634

File tree

4 files changed

+303
-0
lines changed

4 files changed

+303
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ FEATURES
77
* Implement dynamic tool registration. See [#121](https://github.com/hashicorp/terraform-mcp-server/pull/121)
88
* Adding 2 new HCP TF/TFE tools for admins. List Terraform organizations & projects. See [#121](https://github.com/hashicorp/terraform-mcp-server/pull/121)
99
* Adding 4 new HCP TF/TFE tools for private registry support. See [#142](https://github.com/hashicorp/terraform-mcp-server/pull/142)
10+
* Adding 3 HCP TF/TFE tools for workspace variables support. See [#170](https://github.com/hashicorp/terraform-mcp-server/pull/170)
1011
* Adding 2 new HCP TF/TFE tools for workspace tags. See [#171](https://github.com/hashicorp/terraform-mcp-server/pull/171)
1112
* Adding 4 new HCP TF/TFE tools for creating Terraform runs. See [#159](https://github.com/hashicorp/terraform-mcp-server/pull/159)
1213

pkg/tools/dynamic_tool.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,16 @@ func (r *DynamicToolRegistry) registerTFETools() {
151151
getRunDetailsTool := r.createDynamicTFETool("get_run_details", tfeTools.GetRunDetails)
152152
r.mcpServer.AddTool(getRunDetailsTool.Tool, getRunDetailsTool.Handler)
153153

154+
// Variable tools
155+
listWorkspaceVariablesTool := r.createDynamicTFETool("list_workspace_variables", tfeTools.ListWorkspaceVariables)
156+
r.mcpServer.AddTool(listWorkspaceVariablesTool.Tool, listWorkspaceVariablesTool.Handler)
157+
158+
createWorkspaceVariableTool := r.createDynamicTFETool("create_workspace_variable", tfeTools.CreateWorkspaceVariable)
159+
r.mcpServer.AddTool(createWorkspaceVariableTool.Tool, createWorkspaceVariableTool.Handler)
160+
161+
updateWorkspaceVariableTool := r.createDynamicTFETool("update_workspace_variable", tfeTools.UpdateWorkspaceVariable)
162+
r.mcpServer.AddTool(updateWorkspaceVariableTool.Tool, updateWorkspaceVariableTool.Handler)
163+
154164
r.tfeToolsRegistered = true
155165
}
156166

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package tools
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"fmt"
10+
11+
"github.com/hashicorp/go-tfe"
12+
"github.com/hashicorp/jsonapi"
13+
"github.com/hashicorp/terraform-mcp-server/pkg/client"
14+
"github.com/hashicorp/terraform-mcp-server/pkg/utils"
15+
log "github.com/sirupsen/logrus"
16+
17+
"github.com/mark3labs/mcp-go/mcp"
18+
"github.com/mark3labs/mcp-go/server"
19+
)
20+
21+
// ListWorkspaceVariables creates a tool to list workspace variables.
22+
func ListWorkspaceVariables(logger *log.Logger) server.ServerTool {
23+
return server.ServerTool{
24+
Tool: mcp.NewTool("list_workspace_variables",
25+
mcp.WithDescription("List all variables in a Terraform workspace. Returns all variables if query is empty."),
26+
mcp.WithString("terraform_org_name", mcp.Required(), mcp.Description("Organization name")),
27+
mcp.WithString("workspace_name", mcp.Required(), mcp.Description("Workspace name")),
28+
utils.WithPagination(),
29+
),
30+
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
31+
orgName, err := request.RequireString("terraform_org_name")
32+
if err != nil {
33+
return nil, utils.LogAndReturnError(logger, "terraform_org_name is required", err)
34+
}
35+
workspaceName, err := request.RequireString("workspace_name")
36+
if err != nil {
37+
return nil, utils.LogAndReturnError(logger, "workspace_name is required", err)
38+
}
39+
40+
tfeClient, err := client.GetTfeClientFromContext(ctx, logger)
41+
if err != nil {
42+
return nil, utils.LogAndReturnError(logger, "getting Terraform client", err)
43+
}
44+
45+
pagination, err := utils.OptionalPaginationParams(request)
46+
if err != nil {
47+
return mcp.NewToolResultError(err.Error()), nil
48+
}
49+
50+
workspace, err := tfeClient.Workspaces.Read(ctx, orgName, workspaceName)
51+
if err != nil {
52+
return nil, utils.LogAndReturnError(logger, "reading workspace", err)
53+
}
54+
55+
vars, err := tfeClient.Variables.List(ctx, workspace.ID, &tfe.VariableListOptions{
56+
ListOptions: tfe.ListOptions{
57+
PageNumber: pagination.Page,
58+
PageSize: pagination.PageSize,
59+
},
60+
})
61+
if err != nil {
62+
return nil, utils.LogAndReturnError(logger, "listing variables", err)
63+
}
64+
65+
buf := bytes.NewBuffer(nil)
66+
err = jsonapi.MarshalPayload(buf, vars.Items)
67+
if err != nil {
68+
return nil, utils.LogAndReturnError(logger, "marshalling variables to JSON", err)
69+
}
70+
71+
return &mcp.CallToolResult{
72+
Content: []mcp.Content{
73+
mcp.NewTextContent(buf.String()),
74+
},
75+
}, nil
76+
},
77+
}
78+
}
79+
80+
// CreateWorkspaceVariable creates a tool to create a workspace variable.
81+
func CreateWorkspaceVariable(logger *log.Logger) server.ServerTool {
82+
return server.ServerTool{
83+
Tool: mcp.NewTool("create_workspace_variable",
84+
mcp.WithDescription("Create a new variable in a Terraform workspace."),
85+
mcp.WithString("terraform_org_name", mcp.Required(), mcp.Description("Organization name")),
86+
mcp.WithString("workspace_name", mcp.Required(), mcp.Description("Workspace name")),
87+
mcp.WithString("key", mcp.Required(), mcp.Description("Variable key/name")),
88+
mcp.WithString("value", mcp.Required(), mcp.Description("Variable value")),
89+
mcp.WithString("description", mcp.Description("Variable description"), mcp.DefaultString("")),
90+
mcp.WithString("category",
91+
mcp.Description("Variable category: terraform or env"),
92+
mcp.Enum("terraform", "env"),
93+
mcp.DefaultString("env"),
94+
),
95+
mcp.WithBoolean("sensitive", mcp.Description("Whether variable is sensitive: true or false"), mcp.DefaultBool(false)),
96+
mcp.WithBoolean("hcl", mcp.Description("Whether variable is HCL: true or false"), mcp.DefaultBool(false)),
97+
),
98+
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
99+
orgName, err := request.RequireString("terraform_org_name")
100+
if err != nil {
101+
return nil, utils.LogAndReturnError(logger, "terraform_org_name is required", err)
102+
}
103+
workspaceName, err := request.RequireString("workspace_name")
104+
if err != nil {
105+
return nil, utils.LogAndReturnError(logger, "workspace_name is required", err)
106+
}
107+
key, err := request.RequireString("key")
108+
if err != nil {
109+
return nil, utils.LogAndReturnError(logger, "key is required", err)
110+
}
111+
value, err := request.RequireString("value")
112+
if err != nil {
113+
return nil, utils.LogAndReturnError(logger, "value is required", err)
114+
}
115+
116+
category := tfe.CategoryEnv
117+
if request.GetString("category", "") == "terraform" {
118+
category = tfe.CategoryTerraform
119+
}
120+
121+
sensitive := request.GetBool("sensitive", false)
122+
hcl := request.GetBool("hcl", false)
123+
description := request.GetString("description", "")
124+
125+
tfeClient, err := client.GetTfeClientFromContext(ctx, logger)
126+
if err != nil {
127+
return nil, utils.LogAndReturnError(logger, "getting Terraform client", err)
128+
}
129+
130+
workspace, err := tfeClient.Workspaces.Read(ctx, orgName, workspaceName)
131+
if err != nil {
132+
return nil, utils.LogAndReturnError(logger, "reading workspace", err)
133+
}
134+
135+
variable, err := tfeClient.Variables.Create(ctx, workspace.ID, tfe.VariableCreateOptions{
136+
Key: &key,
137+
Value: &value,
138+
Category: &category,
139+
Sensitive: &sensitive,
140+
HCL: &hcl,
141+
Description: &description,
142+
})
143+
if err != nil {
144+
return nil, utils.LogAndReturnError(logger, "creating variable", err)
145+
}
146+
147+
return &mcp.CallToolResult{
148+
Content: []mcp.Content{
149+
mcp.NewTextContent(fmt.Sprintf("Created variable %s with ID %s", variable.Key, variable.ID)),
150+
},
151+
}, nil
152+
},
153+
}
154+
}
155+
156+
// UpdateWorkspaceVariable creates a tool to update a workspace variable.
157+
func UpdateWorkspaceVariable(logger *log.Logger) server.ServerTool {
158+
return server.ServerTool{
159+
Tool: mcp.NewTool("update_workspace_variable",
160+
mcp.WithDescription("Update an existing variable in a Terraform workspace."),
161+
mcp.WithString("terraform_org_name", mcp.Required(), mcp.Description("Organization name")),
162+
mcp.WithString("workspace_name", mcp.Required(), mcp.Description("Workspace name")),
163+
mcp.WithString("variable_id", mcp.Required(), mcp.Description("Variable ID to update")),
164+
mcp.WithString("key", mcp.Required(), mcp.Description("Variable key/name")),
165+
mcp.WithString("value", mcp.Required(), mcp.Description("Variable value")),
166+
mcp.WithBoolean("sensitive", mcp.Description("Whether variable is sensitive: true or false"), mcp.DefaultBool(false)),
167+
mcp.WithBoolean("hcl", mcp.Description("Whether variable is HCL: true or false"), mcp.DefaultBool(false)),
168+
mcp.WithString("description", mcp.Description("Variable description")),
169+
),
170+
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
171+
orgName, err := request.RequireString("terraform_org_name")
172+
if err != nil {
173+
return nil, utils.LogAndReturnError(logger, "terraform_org_name is required", err)
174+
}
175+
workspaceName, err := request.RequireString("workspace_name")
176+
if err != nil {
177+
return nil, utils.LogAndReturnError(logger, "workspace_name is required", err)
178+
}
179+
variableID, err := request.RequireString("variable_id")
180+
if err != nil {
181+
return nil, utils.LogAndReturnError(logger, "variable_id is required", err)
182+
}
183+
key, err := request.RequireString("key")
184+
if err != nil {
185+
return nil, utils.LogAndReturnError(logger, "key is required", err)
186+
}
187+
value, err := request.RequireString("value")
188+
if err != nil {
189+
return nil, utils.LogAndReturnError(logger, "value is required", err)
190+
}
191+
192+
options := tfe.VariableUpdateOptions{
193+
Key: &key,
194+
Value: &value,
195+
}
196+
if sensitiveStr := request.GetString("sensitive", ""); sensitiveStr != "" {
197+
sensitive := sensitiveStr == "true"
198+
options.Sensitive = &sensitive
199+
}
200+
if hclStr := request.GetString("hcl", ""); hclStr != "" {
201+
hcl := hclStr == "true"
202+
options.HCL = &hcl
203+
}
204+
if description := request.GetString("description", ""); description != "" {
205+
options.Description = &description
206+
}
207+
208+
tfeClient, err := client.GetTfeClientFromContext(ctx, logger)
209+
if err != nil {
210+
return nil, utils.LogAndReturnError(logger, "getting Terraform client", err)
211+
}
212+
213+
workspace, err := tfeClient.Workspaces.Read(ctx, orgName, workspaceName)
214+
if err != nil {
215+
return nil, utils.LogAndReturnError(logger, "reading workspace", err)
216+
}
217+
218+
variable, err := tfeClient.Variables.Update(ctx, workspace.ID, variableID, options)
219+
if err != nil {
220+
return nil, utils.LogAndReturnError(logger, "updating variable", err)
221+
}
222+
223+
return &mcp.CallToolResult{
224+
Content: []mcp.Content{
225+
mcp.NewTextContent(fmt.Sprintf("Updated variable %s with ID %s", variable.Key, variable.ID)),
226+
},
227+
}, nil
228+
},
229+
}
230+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package tools
5+
6+
import (
7+
"testing"
8+
9+
log "github.com/sirupsen/logrus"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestListWorkspaceVariables(t *testing.T) {
14+
logger := log.New()
15+
logger.SetLevel(log.ErrorLevel)
16+
17+
t.Run("tool creation", func(t *testing.T) {
18+
tool := ListWorkspaceVariables(logger)
19+
20+
assert.Equal(t, "list_workspace_variables", tool.Tool.Name)
21+
assert.Contains(t, tool.Tool.Description, "List all variables in a Terraform workspace")
22+
assert.NotNil(t, tool.Handler)
23+
24+
assert.Contains(t, tool.Tool.InputSchema.Required, "terraform_org_name")
25+
assert.Contains(t, tool.Tool.InputSchema.Required, "workspace_name")
26+
})
27+
}
28+
29+
func TestCreateWorkspaceVariable(t *testing.T) {
30+
logger := log.New()
31+
logger.SetLevel(log.ErrorLevel)
32+
33+
t.Run("tool creation", func(t *testing.T) {
34+
tool := CreateWorkspaceVariable(logger)
35+
36+
assert.Equal(t, "create_workspace_variable", tool.Tool.Name)
37+
assert.Contains(t, tool.Tool.Description, "Create a new variable in a Terraform workspace")
38+
assert.NotNil(t, tool.Handler)
39+
40+
assert.Contains(t, tool.Tool.InputSchema.Required, "terraform_org_name")
41+
assert.Contains(t, tool.Tool.InputSchema.Required, "workspace_name")
42+
assert.Contains(t, tool.Tool.InputSchema.Required, "key")
43+
assert.Contains(t, tool.Tool.InputSchema.Required, "value")
44+
})
45+
}
46+
47+
func TestUpdateWorkspaceVariable(t *testing.T) {
48+
logger := log.New()
49+
logger.SetLevel(log.ErrorLevel)
50+
51+
t.Run("tool creation", func(t *testing.T) {
52+
tool := UpdateWorkspaceVariable(logger)
53+
54+
assert.Equal(t, "update_workspace_variable", tool.Tool.Name)
55+
assert.Contains(t, tool.Tool.Description, "Update an existing variable")
56+
assert.NotNil(t, tool.Handler)
57+
58+
assert.Contains(t, tool.Tool.InputSchema.Required, "terraform_org_name")
59+
assert.Contains(t, tool.Tool.InputSchema.Required, "workspace_name")
60+
assert.Contains(t, tool.Tool.InputSchema.Required, "variable_id")
61+
})
62+
}

0 commit comments

Comments
 (0)