Skip to content

Commit 8b41e08

Browse files
ashwin-antclaude
andcommitted
feat: add UpdatePullRequestComment tool to edit PR review comments
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 023f59d commit 8b41e08

File tree

3 files changed

+182
-0
lines changed

3 files changed

+182
-0
lines changed

pkg/github/pullrequests.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,76 @@ func CreateAndSubmitPullRequestReview(getGQLClient GetGQLClientFn, t translation
959959
}
960960
}
961961

962+
// UpdatePullRequestComment creates a tool to update a review comment on a pull request.
963+
func UpdatePullRequestComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
964+
return mcp.NewTool("update_pull_request_comment",
965+
mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_COMMENT_DESCRIPTION", "Update a review comment on a pull request")),
966+
mcp.WithString("owner",
967+
mcp.Required(),
968+
mcp.Description("Repository owner"),
969+
),
970+
mcp.WithString("repo",
971+
mcp.Required(),
972+
mcp.Description("Repository name"),
973+
),
974+
mcp.WithNumber("commentId",
975+
mcp.Required(),
976+
mcp.Description("Comment ID to update"),
977+
),
978+
mcp.WithString("body",
979+
mcp.Required(),
980+
mcp.Description("The new text for the comment"),
981+
),
982+
),
983+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
984+
owner, err := requiredParam[string](request, "owner")
985+
if err != nil {
986+
return mcp.NewToolResultError(err.Error()), nil
987+
}
988+
repo, err := requiredParam[string](request, "repo")
989+
if err != nil {
990+
return mcp.NewToolResultError(err.Error()), nil
991+
}
992+
commentID, err := RequiredInt(request, "commentId")
993+
if err != nil {
994+
return mcp.NewToolResultError(err.Error()), nil
995+
}
996+
body, err := requiredParam[string](request, "body")
997+
if err != nil {
998+
return mcp.NewToolResultError(err.Error()), nil
999+
}
1000+
1001+
comment := &github.PullRequestComment{
1002+
Body: github.Ptr(body),
1003+
}
1004+
1005+
client, err := getClient(ctx)
1006+
if err != nil {
1007+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
1008+
}
1009+
updatedComment, resp, err := client.PullRequests.EditComment(ctx, owner, repo, int64(commentID), comment)
1010+
if err != nil {
1011+
return nil, fmt.Errorf("failed to update pull request comment: %w", err)
1012+
}
1013+
defer func() { _ = resp.Body.Close() }()
1014+
1015+
if resp.StatusCode != http.StatusOK {
1016+
body, err := io.ReadAll(resp.Body)
1017+
if err != nil {
1018+
return nil, fmt.Errorf("failed to read response body: %w", err)
1019+
}
1020+
return mcp.NewToolResultError(fmt.Sprintf("failed to update pull request comment: %s", string(body))), nil
1021+
}
1022+
1023+
r, err := json.Marshal(updatedComment)
1024+
if err != nil {
1025+
return nil, fmt.Errorf("failed to marshal response: %w", err)
1026+
}
1027+
1028+
return mcp.NewToolResultText(string(r)), nil
1029+
}
1030+
}
1031+
9621032
// CreatePendingPullRequestReview creates a tool to create a pending review on a pull request.
9631033
func CreatePendingPullRequestReview(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
9641034
return mcp.NewTool("create_pending_pull_request_review",

pkg/github/pullrequests_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1655,6 +1655,116 @@ func Test_RequestCopilotReview(t *testing.T) {
16551655
}
16561656
}
16571657

1658+
func Test_UpdatePullRequestComment(t *testing.T) {
1659+
// Verify tool definition once
1660+
mockClient := github.NewClient(nil)
1661+
tool, _ := UpdatePullRequestComment(stubGetClientFn(mockClient), translations.NullTranslationHelper)
1662+
1663+
assert.Equal(t, "update_pull_request_comment", tool.Name)
1664+
assert.NotEmpty(t, tool.Description)
1665+
assert.Contains(t, tool.InputSchema.Properties, "owner")
1666+
assert.Contains(t, tool.InputSchema.Properties, "repo")
1667+
assert.Contains(t, tool.InputSchema.Properties, "commentId")
1668+
assert.Contains(t, tool.InputSchema.Properties, "body")
1669+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "commentId", "body"})
1670+
1671+
// Setup mock comment for success case
1672+
mockUpdatedComment := &github.PullRequestComment{
1673+
ID: github.Ptr(int64(456)),
1674+
Body: github.Ptr("Updated comment text here"),
1675+
HTMLURL: github.Ptr("https://github.com/owner/repo/pull/1#discussion_r456"),
1676+
Path: github.Ptr("file1.txt"),
1677+
UpdatedAt: &github.Timestamp{Time: time.Now()},
1678+
User: &github.User{
1679+
Login: github.Ptr("testuser"),
1680+
},
1681+
}
1682+
1683+
tests := []struct {
1684+
name string
1685+
mockedClient *http.Client
1686+
requestArgs map[string]interface{}
1687+
expectError bool
1688+
expectedComment *github.PullRequestComment
1689+
expectedErrMsg string
1690+
}{
1691+
{
1692+
name: "successful update",
1693+
mockedClient: httpMock(
1694+
NewJSONResponder(200, mockUpdatedComment),
1695+
),
1696+
requestArgs: map[string]interface{}{
1697+
"owner": "testowner",
1698+
"repo": "testrepo",
1699+
"commentId": float64(456),
1700+
"body": "Updated comment text here",
1701+
},
1702+
expectError: false,
1703+
expectedComment: mockUpdatedComment,
1704+
},
1705+
{
1706+
name: "missing required parameters",
1707+
mockedClient: httpMock(
1708+
NewJSONResponder(200, mockUpdatedComment),
1709+
),
1710+
requestArgs: map[string]interface{}{
1711+
"owner": "testowner",
1712+
"repo": "testrepo",
1713+
// Missing commentId and body
1714+
},
1715+
expectError: true,
1716+
expectedErrMsg: "commentId is required",
1717+
},
1718+
{
1719+
name: "http error",
1720+
mockedClient: httpMock(
1721+
NewStringResponder(400, "Bad Request"),
1722+
),
1723+
requestArgs: map[string]interface{}{
1724+
"owner": "testowner",
1725+
"repo": "testrepo",
1726+
"commentId": float64(456),
1727+
"body": "Invalid body", // Changed this to a non-empty string
1728+
},
1729+
expectError: true,
1730+
expectedErrMsg: "failed to update pull request comment",
1731+
},
1732+
}
1733+
1734+
for _, tc := range tests {
1735+
t.Run(tc.name, func(t *testing.T) {
1736+
client := github.NewClient(tc.mockedClient)
1737+
_, handler := UpdatePullRequestComment(stubGetClientFn(client), translations.NullTranslationHelper)
1738+
1739+
request := createMCPRequest(tc.requestArgs)
1740+
1741+
// Call handler
1742+
result, err := handler(context.Background(), request)
1743+
require.NoError(t, err)
1744+
1745+
textContent := getTextResult(t, result)
1746+
1747+
if tc.expectError {
1748+
require.True(t, result.IsError)
1749+
assert.Contains(t, textContent.Text, tc.expectedErrMsg)
1750+
return
1751+
}
1752+
1753+
// Parse the result for success case
1754+
require.False(t, result.IsError)
1755+
1756+
var returnedComment *github.PullRequestComment
1757+
err = json.Unmarshal([]byte(textContent.Text), &returnedComment)
1758+
require.NoError(t, err)
1759+
1760+
// Verify comment details
1761+
assert.Equal(t, *tc.expectedComment.ID, *returnedComment.ID)
1762+
assert.Equal(t, *tc.expectedComment.Body, *returnedComment.Body)
1763+
assert.Equal(t, *tc.expectedComment.HTMLURL, *returnedComment.HTMLURL)
1764+
})
1765+
}
1766+
}
1767+
16581768
func TestCreatePendingPullRequestReview(t *testing.T) {
16591769
t.Parallel()
16601770

pkg/github/tools.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
7373
toolsets.NewServerTool(CreatePullRequest(getClient, t)),
7474
toolsets.NewServerTool(UpdatePullRequest(getClient, t)),
7575
toolsets.NewServerTool(RequestCopilotReview(getClient, t)),
76+
toolsets.NewServerTool(AddPullRequestReviewComment(getClient, t)),
77+
toolsets.NewServerTool(UpdatePullRequestComment(getClient, t)),
7678

7779
// Reviews
7880
toolsets.NewServerTool(CreateAndSubmitPullRequestReview(getGQLClient, t)),

0 commit comments

Comments
 (0)