Skip to content

Commit 75b3504

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 495c0cb commit 75b3504

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
@@ -1073,6 +1073,76 @@ func CreatePullRequestReview(getClient GetClientFn, t translations.TranslationHe
10731073
}
10741074
}
10751075

1076+
// UpdatePullRequestComment creates a tool to update a review comment on a pull request.
1077+
func UpdatePullRequestComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
1078+
return mcp.NewTool("update_pull_request_comment",
1079+
mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_COMMENT_DESCRIPTION", "Update a review comment on a pull request")),
1080+
mcp.WithString("owner",
1081+
mcp.Required(),
1082+
mcp.Description("Repository owner"),
1083+
),
1084+
mcp.WithString("repo",
1085+
mcp.Required(),
1086+
mcp.Description("Repository name"),
1087+
),
1088+
mcp.WithNumber("commentId",
1089+
mcp.Required(),
1090+
mcp.Description("Comment ID to update"),
1091+
),
1092+
mcp.WithString("body",
1093+
mcp.Required(),
1094+
mcp.Description("The new text for the comment"),
1095+
),
1096+
),
1097+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
1098+
owner, err := requiredParam[string](request, "owner")
1099+
if err != nil {
1100+
return mcp.NewToolResultError(err.Error()), nil
1101+
}
1102+
repo, err := requiredParam[string](request, "repo")
1103+
if err != nil {
1104+
return mcp.NewToolResultError(err.Error()), nil
1105+
}
1106+
commentID, err := RequiredInt(request, "commentId")
1107+
if err != nil {
1108+
return mcp.NewToolResultError(err.Error()), nil
1109+
}
1110+
body, err := requiredParam[string](request, "body")
1111+
if err != nil {
1112+
return mcp.NewToolResultError(err.Error()), nil
1113+
}
1114+
1115+
comment := &github.PullRequestComment{
1116+
Body: github.Ptr(body),
1117+
}
1118+
1119+
client, err := getClient(ctx)
1120+
if err != nil {
1121+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
1122+
}
1123+
updatedComment, resp, err := client.PullRequests.EditComment(ctx, owner, repo, int64(commentID), comment)
1124+
if err != nil {
1125+
return nil, fmt.Errorf("failed to update pull request comment: %w", err)
1126+
}
1127+
defer func() { _ = resp.Body.Close() }()
1128+
1129+
if resp.StatusCode != http.StatusOK {
1130+
body, err := io.ReadAll(resp.Body)
1131+
if err != nil {
1132+
return nil, fmt.Errorf("failed to read response body: %w", err)
1133+
}
1134+
return mcp.NewToolResultError(fmt.Sprintf("failed to update pull request comment: %s", string(body))), nil
1135+
}
1136+
1137+
r, err := json.Marshal(updatedComment)
1138+
if err != nil {
1139+
return nil, fmt.Errorf("failed to marshal response: %w", err)
1140+
}
1141+
1142+
return mcp.NewToolResultText(string(r)), nil
1143+
}
1144+
}
1145+
10761146
// CreatePullRequest creates a tool to create a new pull request.
10771147
func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
10781148
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)