Skip to content

Commit 741e05d

Browse files
ashwin-antClaude
and
Claude
committed
feat: add UpdatePullRequestComment tool to edit PR review comments
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent a7d741c commit 741e05d

File tree

3 files changed

+207
-0
lines changed

3 files changed

+207
-0
lines changed

pkg/github/pullrequests.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,6 +1132,76 @@ func CreatePullRequestReview(getClient GetClientFn, t translations.TranslationHe
11321132
}
11331133
}
11341134

1135+
// UpdatePullRequestComment creates a tool to update a review comment on a pull request.
1136+
func UpdatePullRequestComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
1137+
return mcp.NewTool("update_pull_request_comment",
1138+
mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_COMMENT_DESCRIPTION", "Update a review comment on a pull request")),
1139+
mcp.WithString("owner",
1140+
mcp.Required(),
1141+
mcp.Description("Repository owner"),
1142+
),
1143+
mcp.WithString("repo",
1144+
mcp.Required(),
1145+
mcp.Description("Repository name"),
1146+
),
1147+
mcp.WithNumber("commentId",
1148+
mcp.Required(),
1149+
mcp.Description("Comment ID to update"),
1150+
),
1151+
mcp.WithString("body",
1152+
mcp.Required(),
1153+
mcp.Description("The new text for the comment"),
1154+
),
1155+
),
1156+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
1157+
owner, err := requiredParam[string](request, "owner")
1158+
if err != nil {
1159+
return mcp.NewToolResultError(err.Error()), nil
1160+
}
1161+
repo, err := requiredParam[string](request, "repo")
1162+
if err != nil {
1163+
return mcp.NewToolResultError(err.Error()), nil
1164+
}
1165+
commentID, err := RequiredInt(request, "commentId")
1166+
if err != nil {
1167+
return mcp.NewToolResultError(err.Error()), nil
1168+
}
1169+
body, err := requiredParam[string](request, "body")
1170+
if err != nil {
1171+
return mcp.NewToolResultError(err.Error()), nil
1172+
}
1173+
1174+
comment := &github.PullRequestComment{
1175+
Body: github.Ptr(body),
1176+
}
1177+
1178+
client, err := getClient(ctx)
1179+
if err != nil {
1180+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
1181+
}
1182+
updatedComment, resp, err := client.PullRequests.EditComment(ctx, owner, repo, int64(commentID), comment)
1183+
if err != nil {
1184+
return nil, fmt.Errorf("failed to update pull request comment: %w", err)
1185+
}
1186+
defer func() { _ = resp.Body.Close() }()
1187+
1188+
if resp.StatusCode != http.StatusOK {
1189+
body, err := io.ReadAll(resp.Body)
1190+
if err != nil {
1191+
return nil, fmt.Errorf("failed to read response body: %w", err)
1192+
}
1193+
return mcp.NewToolResultError(fmt.Sprintf("failed to update pull request comment: %s", string(body))), nil
1194+
}
1195+
1196+
r, err := json.Marshal(updatedComment)
1197+
if err != nil {
1198+
return nil, fmt.Errorf("failed to marshal response: %w", err)
1199+
}
1200+
1201+
return mcp.NewToolResultText(string(r)), nil
1202+
}
1203+
}
1204+
11351205
// CreatePullRequest creates a tool to create a new pull request.
11361206
func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
11371207
return mcp.NewTool("create_pull_request",

pkg/github/pullrequests_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1916,3 +1916,139 @@ func Test_AddPullRequestReviewComment(t *testing.T) {
19161916
})
19171917
}
19181918
}
1919+
1920+
func Test_UpdatePullRequestComment(t *testing.T) {
1921+
// Verify tool definition once
1922+
mockClient := github.NewClient(nil)
1923+
tool, _ := UpdatePullRequestComment(stubGetClientFn(mockClient), translations.NullTranslationHelper)
1924+
1925+
assert.Equal(t, "update_pull_request_comment", tool.Name)
1926+
assert.NotEmpty(t, tool.Description)
1927+
assert.Contains(t, tool.InputSchema.Properties, "owner")
1928+
assert.Contains(t, tool.InputSchema.Properties, "repo")
1929+
assert.Contains(t, tool.InputSchema.Properties, "commentId")
1930+
assert.Contains(t, tool.InputSchema.Properties, "body")
1931+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "commentId", "body"})
1932+
1933+
// Setup mock comment for success case
1934+
mockUpdatedComment := &github.PullRequestComment{
1935+
ID: github.Ptr(int64(456)),
1936+
Body: github.Ptr("Updated comment text here"),
1937+
HTMLURL: github.Ptr("https://github.com/owner/repo/pull/1#discussion_r456"),
1938+
Path: github.Ptr("file1.txt"),
1939+
UpdatedAt: &github.Timestamp{Time: time.Now()},
1940+
User: &github.User{
1941+
Login: github.Ptr("testuser"),
1942+
},
1943+
}
1944+
1945+
tests := []struct {
1946+
name string
1947+
mockedClient *http.Client
1948+
requestArgs map[string]interface{}
1949+
expectError bool
1950+
expectedComment *github.PullRequestComment
1951+
expectedErrMsg string
1952+
}{
1953+
{
1954+
name: "successful comment update",
1955+
mockedClient: mock.NewMockedHTTPClient(
1956+
mock.WithRequestMatchHandler(
1957+
mock.PatchReposPullsCommentsByOwnerByRepoByCommentId,
1958+
expectRequestBody(t, map[string]interface{}{
1959+
"body": "Updated comment text here",
1960+
}).andThen(
1961+
mockResponse(t, http.StatusOK, mockUpdatedComment),
1962+
),
1963+
),
1964+
),
1965+
requestArgs: map[string]interface{}{
1966+
"owner": "owner",
1967+
"repo": "repo",
1968+
"commentId": float64(456),
1969+
"body": "Updated comment text here",
1970+
},
1971+
expectError: false,
1972+
expectedComment: mockUpdatedComment,
1973+
},
1974+
{
1975+
name: "comment update fails - not found",
1976+
mockedClient: mock.NewMockedHTTPClient(
1977+
mock.WithRequestMatchHandler(
1978+
mock.PatchReposPullsCommentsByOwnerByRepoByCommentId,
1979+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1980+
w.WriteHeader(http.StatusNotFound)
1981+
w.Header().Set("Content-Type", "application/json")
1982+
_, _ = w.Write([]byte(`{"message": "Comment not found"}`))
1983+
}),
1984+
),
1985+
),
1986+
requestArgs: map[string]interface{}{
1987+
"owner": "owner",
1988+
"repo": "repo",
1989+
"commentId": float64(999),
1990+
"body": "This should fail",
1991+
},
1992+
expectError: true,
1993+
expectedErrMsg: "failed to update pull request comment",
1994+
},
1995+
{
1996+
name: "comment update fails - validation error",
1997+
mockedClient: mock.NewMockedHTTPClient(
1998+
mock.WithRequestMatchHandler(
1999+
mock.PatchReposPullsCommentsByOwnerByRepoByCommentId,
2000+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
2001+
w.WriteHeader(http.StatusUnprocessableEntity)
2002+
w.Header().Set("Content-Type", "application/json")
2003+
_, _ = w.Write([]byte(`{"message": "Validation Failed"}`))
2004+
}),
2005+
),
2006+
),
2007+
requestArgs: map[string]interface{}{
2008+
"owner": "owner",
2009+
"repo": "repo",
2010+
"commentId": float64(456),
2011+
"body": "Invalid body", // Changed this to a non-empty string
2012+
},
2013+
expectError: true,
2014+
expectedErrMsg: "failed to update pull request comment",
2015+
},
2016+
}
2017+
2018+
for _, tc := range tests {
2019+
t.Run(tc.name, func(t *testing.T) {
2020+
client := github.NewClient(tc.mockedClient)
2021+
_, handler := UpdatePullRequestComment(stubGetClientFn(client), translations.NullTranslationHelper)
2022+
2023+
request := createMCPRequest(tc.requestArgs)
2024+
2025+
result, err := handler(context.Background(), request)
2026+
2027+
if tc.expectError {
2028+
require.Error(t, err)
2029+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
2030+
return
2031+
}
2032+
2033+
require.NoError(t, err)
2034+
assert.NotNil(t, result)
2035+
require.Len(t, result.Content, 1)
2036+
2037+
textContent := getTextResult(t, result)
2038+
2039+
// For non-error cases, check the returned comment
2040+
var returnedComment github.PullRequestComment
2041+
err = json.Unmarshal([]byte(textContent.Text), &returnedComment)
2042+
require.NoError(t, err)
2043+
2044+
assert.Equal(t, *tc.expectedComment.ID, *returnedComment.ID)
2045+
assert.Equal(t, *tc.expectedComment.Body, *returnedComment.Body)
2046+
if tc.expectedComment.Path != nil {
2047+
assert.Equal(t, *tc.expectedComment.Path, *returnedComment.Path)
2048+
}
2049+
if tc.expectedComment.HTMLURL != nil {
2050+
assert.Equal(t, *tc.expectedComment.HTMLURL, *returnedComment.HTMLURL)
2051+
}
2052+
})
2053+
}
2054+
}

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
6767
toolsets.NewServerTool(CreatePullRequest(getClient, t)),
6868
toolsets.NewServerTool(UpdatePullRequest(getClient, t)),
6969
toolsets.NewServerTool(AddPullRequestReviewComment(getClient, t)),
70+
toolsets.NewServerTool(UpdatePullRequestComment(getClient, t)),
7071
)
7172
codeSecurity := toolsets.NewToolset("code_security", "Code security related tools, such as GitHub Code Scanning").
7273
AddReadTools(

0 commit comments

Comments
 (0)