Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
6 changes: 6 additions & 0 deletions pkg/tools/dynamic_tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
99 changes: 99 additions & 0 deletions pkg/tools/tfe/list_run_comments.go
Original file line number Diff line number Diff line change
@@ -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"`
}
1 change: 1 addition & 0 deletions pkg/toolsets/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading