From b624a92a6590a377247917a591ff0faa66f5e2ab Mon Sep 17 00:00:00 2001 From: "Chris (ChrisJr404)" <11917633+ChrisJr404@users.noreply.github.com> Date: Mon, 4 May 2026 17:35:20 -0400 Subject: [PATCH 1/2] feat(tfe): add list_run_comments tool (#347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the read-side gap on the run-comment surface. `action_run` already lets agents post a comment when applying / discarding / canceling a run, but until now there was no way to read the existing comments back: an agent inspecting a run via `get_run_details` could see that comments existed (the workspace comment ID is returned) but couldn't retrieve their bodies, which limits its ability to act on context left by humans or other automation. The new tool wraps the existing TFE Comments API endpoint (`GET /runs/{run_id}/comments`) via go-tfe's Comments.List. It takes a single required `run_id`, returns each comment's id and body in a RunCommentList projection, and follows the same shape conventions as list_runs: - read-only annotation (WithReadOnlyHintAnnotation true) - non-destructive annotation - structured RunCommentSummary / RunCommentList types so future field additions (resolved-by, created-at) are passthrough Wired through the standard places — toolsets/mapping.go for toolset gating, tools/dynamic_tool.go for the conditional registration on the Terraform toolset. No pagination today because the upstream Comments.List endpoint returns the full list in one response (the go-tfe CommentList type embeds *Pagination but the endpoint doesn't actually populate page cursors). Wrapping in a struct anyway so it stays forward-compatible if pagination is later wired up. go build ./... → clean go vet ./... → clean go test ./pkg/tools/tfe/... ./pkg/toolsets/... → ok --- pkg/tools/dynamic_tool.go | 6 ++ pkg/tools/tfe/list_run_comments.go | 99 ++++++++++++++++++++++++++++++ pkg/toolsets/mapping.go | 1 + 3 files changed, 106 insertions(+) create mode 100644 pkg/tools/tfe/list_run_comments.go diff --git a/pkg/tools/dynamic_tool.go b/pkg/tools/dynamic_tool.go index 646d92a4..5ca4eb30 100644 --- a/pkg/tools/dynamic_tool.go +++ b/pkg/tools/dynamic_tool.go @@ -182,6 +182,12 @@ func (r *DynamicToolRegistry) registerTFETools() { r.mcpServer.AddTool(tool.Tool, tool.Handler) } + // Read-only counterpart to action_run's comment-write path. See #347. + if toolsets.IsToolEnabled("list_run_comments", r.enabledToolsets) { + tool := r.createDynamicTFETool("list_run_comments", tfeTools.ListRunComments) + r.mcpServer.AddTool(tool.Tool, tool.Handler) + } + // Create run tool with conditional options based on TF operations setting if toolsets.IsToolEnabled("create_run", r.enabledToolsets) { var tool server.ServerTool diff --git a/pkg/tools/tfe/list_run_comments.go b/pkg/tools/tfe/list_run_comments.go new file mode 100644 index 00000000..6950e71e --- /dev/null +++ b/pkg/tools/tfe/list_run_comments.go @@ -0,0 +1,99 @@ +// Copyright IBM Corp. 2026 +// SPDX-License-Identifier: MPL-2.0 + +package tools + +import ( + "context" + "encoding/json" + "strings" + + "github.com/hashicorp/terraform-mcp-server/pkg/client" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + log "github.com/sirupsen/logrus" +) + +// ListRunComments creates a tool that lists every comment attached to a +// specific Terraform Cloud / Enterprise run. Closes the gap described in +// https://github.com/hashicorp/terraform-mcp-server/issues/347 — `action_run` +// already supports posting a comment, but until now there was no read path, +// so an agent inspecting a run via `get_run_details` could see that comments +// existed but couldn't read their bodies. +// +// Backed by the upstream TFE Comments API: +// +// GET /runs/{run_id}/comments +// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/comments +func ListRunComments(logger *log.Logger) server.ServerTool { + return server.ServerTool{ + Tool: mcp.NewTool("list_run_comments", + mcp.WithDescription(`List all comments attached to a specific Terraform run. Useful for reading context left by humans or automation on a run before deciding next steps.`), + mcp.WithTitleAnnotation("List Terraform run comments"), + mcp.WithReadOnlyHintAnnotation(true), + mcp.WithDestructiveHintAnnotation(false), + mcp.WithString("run_id", + mcp.Required(), + mcp.Description("ID of the Terraform run whose comments should be listed (e.g. 'run-CZcmD7eagjhyX0vN')."), + ), + ), + Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return listRunCommentsHandler(ctx, req, logger) + }, + } +} + +func listRunCommentsHandler(ctx context.Context, request mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) { + runID, err := request.RequireString("run_id") + if err != nil { + return ToolError(logger, "missing required input: run_id", err) + } + runID = strings.TrimSpace(runID) + if runID == "" { + return ToolErrorf(logger, "run_id must be non-empty") + } + + tfeClient, err := client.GetTfeClientFromContext(ctx, logger) + if err != nil { + return ToolError(logger, "failed to get Terraform client", err) + } + + commentList, err := tfeClient.Comments.List(ctx, runID) + if err != nil { + return ToolErrorf(logger, "failed to list comments for run '%s': %s", runID, err.Error()) + } + + summaries := make([]*RunCommentSummary, len(commentList.Items)) + for i, c := range commentList.Items { + summaries[i] = &RunCommentSummary{ + ID: c.ID, + Body: c.Body, + } + } + + buf, err := json.Marshal(&RunCommentList{ + Items: summaries, + }) + if err != nil { + return ToolError(logger, "failed to marshal comments", err) + } + + return mcp.NewToolResultText(string(buf)), nil +} + +// RunCommentSummary is the shape returned per-comment. The upstream TFE +// Comment type carries only ID and Body, so we project both — adding +// future fields here is a passthrough exercise. +type RunCommentSummary struct { + ID string `json:"id"` + Body string `json:"body"` +} + +// RunCommentList wraps the per-run comment summaries. No pagination +// fields today because the upstream Comments.List endpoint does not +// expose a paging cursor (CommentList embeds *Pagination but TFE +// returns the full set in one response). Wrapping in a struct keeps +// the tool output forward-compatible if pagination is later wired up. +type RunCommentList struct { + Items []*RunCommentSummary `json:"items"` +} diff --git a/pkg/toolsets/mapping.go b/pkg/toolsets/mapping.go index cc48e6fa..c29f41b8 100644 --- a/pkg/toolsets/mapping.go +++ b/pkg/toolsets/mapping.go @@ -33,6 +33,7 @@ var ToolToToolset = map[string]string{ "update_workspace": Terraform, "delete_workspace_safely": Terraform, "list_runs": Terraform, + "list_run_comments": Terraform, "get_run_details": Terraform, "get_plan_details": Terraform, "get_plan_logs": Terraform, From 147e41055fbf07c45a4ad6f12a8d213d0106eb9a Mon Sep 17 00:00:00 2001 From: ChrisJr404 Date: Tue, 5 May 2026 18:22:37 -0400 Subject: [PATCH 2/2] docs(changelog): add list_run_comments entry for #352 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d849699e..0a94a31e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 0.5.2 +FEATURES + +* [New Tool] `list_run_comments` Lists comments on a Terraform run, returning each comment's ID and body so agents can read context that humans left on a run [352](https://github.com/hashicorp/terraform-mcp-server/pull/352) + IMPROVEMENTS * Add http server metrics instrumentation [330](https://github.com/hashicorp/terraform-mcp-server/pull/330)