Skip to content

Commit d875c74

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

File tree

4 files changed

+242
-14
lines changed

4 files changed

+242
-14
lines changed

pkg/github/issues.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,76 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc
153153
}
154154
}
155155

156+
// UpdateIssueComment creates a tool to update a comment on an issue.
157+
func UpdateIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
158+
return mcp.NewTool("update_issue_comment",
159+
mcp.WithDescription(t("TOOL_UPDATE_ISSUE_COMMENT_DESCRIPTION", "Update a comment on an issue")),
160+
mcp.WithString("owner",
161+
mcp.Required(),
162+
mcp.Description("Repository owner"),
163+
),
164+
mcp.WithString("repo",
165+
mcp.Required(),
166+
mcp.Description("Repository name"),
167+
),
168+
mcp.WithNumber("commentId",
169+
mcp.Required(),
170+
mcp.Description("Comment ID to update"),
171+
),
172+
mcp.WithString("body",
173+
mcp.Required(),
174+
mcp.Description("The new text for the comment"),
175+
),
176+
),
177+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
178+
owner, err := requiredParam[string](request, "owner")
179+
if err != nil {
180+
return mcp.NewToolResultError(err.Error()), nil
181+
}
182+
repo, err := requiredParam[string](request, "repo")
183+
if err != nil {
184+
return mcp.NewToolResultError(err.Error()), nil
185+
}
186+
commentID, err := RequiredInt(request, "commentId")
187+
if err != nil {
188+
return mcp.NewToolResultError(err.Error()), nil
189+
}
190+
body, err := requiredParam[string](request, "body")
191+
if err != nil {
192+
return mcp.NewToolResultError(err.Error()), nil
193+
}
194+
195+
comment := &github.IssueComment{
196+
Body: github.Ptr(body),
197+
}
198+
199+
client, err := getClient(ctx)
200+
if err != nil {
201+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
202+
}
203+
updatedComment, resp, err := client.Issues.EditComment(ctx, owner, repo, int64(commentID), comment)
204+
if err != nil {
205+
return nil, fmt.Errorf("failed to update issue comment: %w", err)
206+
}
207+
defer func() { _ = resp.Body.Close() }()
208+
209+
if resp.StatusCode != http.StatusOK {
210+
body, err := io.ReadAll(resp.Body)
211+
if err != nil {
212+
return nil, fmt.Errorf("failed to read response body: %w", err)
213+
}
214+
return mcp.NewToolResultError(fmt.Sprintf("failed to update issue comment: %s", string(body))), nil
215+
}
216+
217+
r, err := json.Marshal(updatedComment)
218+
if err != nil {
219+
return nil, fmt.Errorf("failed to marshal response: %w", err)
220+
}
221+
222+
return mcp.NewToolResultText(string(r)), nil
223+
}
224+
}
225+
156226
// SearchIssues creates a tool to search for issues and pull requests.
157227
func SearchIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
158228
return mcp.NewTool("search_issues",

pkg/github/issues_test.go

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,140 @@ func Test_GetIssueComments(t *testing.T) {
11231123
}
11241124
}
11251125

1126+
func Test_UpdateIssueComment(t *testing.T) {
1127+
// Verify tool definition once
1128+
mockClient := github.NewClient(nil)
1129+
tool, _ := UpdateIssueComment(stubGetClientFn(mockClient), translations.NullTranslationHelper)
1130+
1131+
assert.Equal(t, "update_issue_comment", tool.Name)
1132+
assert.NotEmpty(t, tool.Description)
1133+
assert.Contains(t, tool.InputSchema.Properties, "owner")
1134+
assert.Contains(t, tool.InputSchema.Properties, "repo")
1135+
assert.Contains(t, tool.InputSchema.Properties, "commentId")
1136+
assert.Contains(t, tool.InputSchema.Properties, "body")
1137+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "commentId", "body"})
1138+
1139+
// Setup mock comment for success case
1140+
mockUpdatedComment := &github.IssueComment{
1141+
ID: github.Ptr(int64(789)),
1142+
Body: github.Ptr("Updated issue comment text"),
1143+
HTMLURL: github.Ptr("https://github.com/owner/repo/issues/1#issuecomment-789"),
1144+
UpdatedAt: &github.Timestamp{Time: time.Now()},
1145+
User: &github.User{
1146+
Login: github.Ptr("testuser"),
1147+
},
1148+
}
1149+
1150+
tests := []struct {
1151+
name string
1152+
mockedClient *http.Client
1153+
requestArgs map[string]interface{}
1154+
expectError bool
1155+
expectedComment *github.IssueComment
1156+
expectedErrMsg string
1157+
}{
1158+
{
1159+
name: "successful update",
1160+
mockedClient: mock.NewMockedHTTPClient(
1161+
mock.WithRequestMatchHandler(
1162+
mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId,
1163+
expectRequestBody(t, map[string]any{
1164+
"body": "Updated issue comment text",
1165+
}).andThen(
1166+
mockResponse(t, http.StatusOK, mockUpdatedComment),
1167+
),
1168+
),
1169+
),
1170+
requestArgs: map[string]interface{}{
1171+
"owner": "testowner",
1172+
"repo": "testrepo",
1173+
"commentId": float64(789),
1174+
"body": "Updated issue comment text",
1175+
},
1176+
expectError: false,
1177+
expectedComment: mockUpdatedComment,
1178+
},
1179+
{
1180+
name: "missing required parameters",
1181+
mockedClient: mock.NewMockedHTTPClient(
1182+
mock.WithRequestMatch(
1183+
mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId,
1184+
mockUpdatedComment,
1185+
),
1186+
),
1187+
requestArgs: map[string]interface{}{
1188+
"owner": "testowner",
1189+
"repo": "testrepo",
1190+
// Missing commentId and body
1191+
},
1192+
expectError: true,
1193+
expectedErrMsg: "missing required parameter: commentId",
1194+
},
1195+
{
1196+
name: "http error",
1197+
mockedClient: mock.NewMockedHTTPClient(
1198+
mock.WithRequestMatchHandler(
1199+
mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId,
1200+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1201+
w.WriteHeader(http.StatusNotFound)
1202+
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
1203+
}),
1204+
),
1205+
),
1206+
requestArgs: map[string]interface{}{
1207+
"owner": "testowner",
1208+
"repo": "testrepo",
1209+
"commentId": float64(789),
1210+
"body": "New comment text",
1211+
},
1212+
expectError: true,
1213+
expectedErrMsg: "failed to update issue comment",
1214+
},
1215+
}
1216+
1217+
for _, tc := range tests {
1218+
t.Run(tc.name, func(t *testing.T) {
1219+
client := github.NewClient(tc.mockedClient)
1220+
_, handler := UpdateIssueComment(stubGetClientFn(client), translations.NullTranslationHelper)
1221+
1222+
request := createMCPRequest(tc.requestArgs)
1223+
1224+
// Call handler
1225+
result, err := handler(context.Background(), request)
1226+
1227+
if tc.expectError {
1228+
if err != nil {
1229+
// For HTTP errors, the handler returns an error
1230+
require.Error(t, err)
1231+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
1232+
} else {
1233+
// For validation errors, the handler returns a result with IsError=true
1234+
require.NoError(t, err)
1235+
textContent := getTextResult(t, result)
1236+
require.True(t, result.IsError)
1237+
assert.Contains(t, textContent.Text, tc.expectedErrMsg)
1238+
}
1239+
return
1240+
}
1241+
1242+
require.NoError(t, err)
1243+
textContent := getTextResult(t, result)
1244+
1245+
// Parse the result for success case
1246+
require.False(t, result.IsError)
1247+
1248+
var returnedComment *github.IssueComment
1249+
err = json.Unmarshal([]byte(textContent.Text), &returnedComment)
1250+
require.NoError(t, err)
1251+
1252+
// Verify comment details
1253+
assert.Equal(t, *tc.expectedComment.ID, *returnedComment.ID)
1254+
assert.Equal(t, *tc.expectedComment.Body, *returnedComment.Body)
1255+
assert.Equal(t, *tc.expectedComment.HTMLURL, *returnedComment.HTMLURL)
1256+
})
1257+
}
1258+
}
1259+
11261260
func TestAssignCopilotToIssue(t *testing.T) {
11271261
t.Parallel()
11281262

@@ -1515,7 +1649,6 @@ func TestAssignCopilotToIssue(t *testing.T) {
15151649

15161650
for _, tc := range tests {
15171651
t.Run(tc.name, func(t *testing.T) {
1518-
15191652
t.Parallel()
15201653
// Setup client with mock
15211654
client := githubv4.NewClient(tc.mockedClient)

pkg/github/pullrequests_test.go

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1690,8 +1690,15 @@ func Test_UpdatePullRequestComment(t *testing.T) {
16901690
}{
16911691
{
16921692
name: "successful update",
1693-
mockedClient: httpMock(
1694-
NewJSONResponder(200, mockUpdatedComment),
1693+
mockedClient: mock.NewMockedHTTPClient(
1694+
mock.WithRequestMatchHandler(
1695+
mock.PatchReposPullsCommentsByOwnerByRepoByCommentId,
1696+
expectRequestBody(t, map[string]any{
1697+
"body": "Updated comment text here",
1698+
}).andThen(
1699+
mockResponse(t, http.StatusOK, mockUpdatedComment),
1700+
),
1701+
),
16951702
),
16961703
requestArgs: map[string]interface{}{
16971704
"owner": "testowner",
@@ -1704,21 +1711,30 @@ func Test_UpdatePullRequestComment(t *testing.T) {
17041711
},
17051712
{
17061713
name: "missing required parameters",
1707-
mockedClient: httpMock(
1708-
NewJSONResponder(200, mockUpdatedComment),
1714+
mockedClient: mock.NewMockedHTTPClient(
1715+
mock.WithRequestMatch(
1716+
mock.PatchReposPullsCommentsByOwnerByRepoByCommentId,
1717+
mockUpdatedComment,
1718+
),
17091719
),
17101720
requestArgs: map[string]interface{}{
17111721
"owner": "testowner",
17121722
"repo": "testrepo",
17131723
// Missing commentId and body
17141724
},
17151725
expectError: true,
1716-
expectedErrMsg: "commentId is required",
1726+
expectedErrMsg: "missing required parameter: commentId",
17171727
},
17181728
{
17191729
name: "http error",
1720-
mockedClient: httpMock(
1721-
NewStringResponder(400, "Bad Request"),
1730+
mockedClient: mock.NewMockedHTTPClient(
1731+
mock.WithRequestMatchHandler(
1732+
mock.PatchReposPullsCommentsByOwnerByRepoByCommentId,
1733+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1734+
w.WriteHeader(http.StatusBadRequest)
1735+
_, _ = w.Write([]byte(`{"message": "Bad Request"}`))
1736+
}),
1737+
),
17221738
),
17231739
requestArgs: map[string]interface{}{
17241740
"owner": "testowner",
@@ -1740,16 +1756,25 @@ func Test_UpdatePullRequestComment(t *testing.T) {
17401756

17411757
// Call handler
17421758
result, err := handler(context.Background(), request)
1743-
require.NoError(t, err)
1744-
1745-
textContent := getTextResult(t, result)
17461759

17471760
if tc.expectError {
1748-
require.True(t, result.IsError)
1749-
assert.Contains(t, textContent.Text, tc.expectedErrMsg)
1761+
if err != nil {
1762+
// For HTTP errors, the handler returns an error
1763+
require.Error(t, err)
1764+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
1765+
} else {
1766+
// For validation errors, the handler returns a result with IsError=true
1767+
require.NoError(t, err)
1768+
textContent := getTextResult(t, result)
1769+
require.True(t, result.IsError)
1770+
assert.Contains(t, textContent.Text, tc.expectedErrMsg)
1771+
}
17501772
return
17511773
}
17521774

1775+
require.NoError(t, err)
1776+
textContent := getTextResult(t, result)
1777+
17531778
// Parse the result for success case
17541779
require.False(t, result.IsError)
17551780

pkg/github/tools.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
5252
toolsets.NewServerTool(AddIssueComment(getClient, t)),
5353
toolsets.NewServerTool(UpdateIssue(getClient, t)),
5454
toolsets.NewServerTool(AssignCopilotToIssue(getGQLClient, t)),
55+
toolsets.NewServerTool(UpdateIssueComment(getClient, t)),
5556
)
5657
users := toolsets.NewToolset("users", "GitHub User related tools").
5758
AddReadTools(
@@ -73,7 +74,6 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
7374
toolsets.NewServerTool(CreatePullRequest(getClient, t)),
7475
toolsets.NewServerTool(UpdatePullRequest(getClient, t)),
7576
toolsets.NewServerTool(RequestCopilotReview(getClient, t)),
76-
toolsets.NewServerTool(AddPullRequestReviewComment(getClient, t)),
7777
toolsets.NewServerTool(UpdatePullRequestComment(getClient, t)),
7878

7979
// Reviews

0 commit comments

Comments
 (0)