Skip to content

Commit 33b452d

Browse files
ashwin-antClaude
and
Claude
committed
feat: add UpdateIssueComment tool to edit issue comments
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 75b3504 commit 33b452d

File tree

3 files changed

+207
-0
lines changed

3 files changed

+207
-0
lines changed

pkg/github/issues.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,76 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc
142142
}
143143
}
144144

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

pkg/github/issues_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,3 +1130,139 @@ func Test_GetIssueComments(t *testing.T) {
11301130
})
11311131
}
11321132
}
1133+
1134+
func Test_UpdateIssueComment(t *testing.T) {
1135+
// Verify tool definition once
1136+
mockClient := github.NewClient(nil)
1137+
tool, _ := UpdateIssueComment(stubGetClientFn(mockClient), translations.NullTranslationHelper)
1138+
1139+
assert.Equal(t, "update_issue_comment", tool.Name)
1140+
assert.NotEmpty(t, tool.Description)
1141+
assert.Contains(t, tool.InputSchema.Properties, "owner")
1142+
assert.Contains(t, tool.InputSchema.Properties, "repo")
1143+
assert.Contains(t, tool.InputSchema.Properties, "commentId")
1144+
assert.Contains(t, tool.InputSchema.Properties, "body")
1145+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "commentId", "body"})
1146+
1147+
// Setup mock comment for success case
1148+
mockUpdatedComment := &github.IssueComment{
1149+
ID: github.Ptr(int64(123)),
1150+
Body: github.Ptr("Updated issue comment text here"),
1151+
HTMLURL: github.Ptr("https://github.com/owner/repo/issues/1#issuecomment-123"),
1152+
CreatedAt: &github.Timestamp{Time: time.Now().Add(-1 * time.Hour)},
1153+
UpdatedAt: &github.Timestamp{Time: time.Now()},
1154+
User: &github.User{
1155+
Login: github.Ptr("testuser"),
1156+
},
1157+
}
1158+
1159+
tests := []struct {
1160+
name string
1161+
mockedClient *http.Client
1162+
requestArgs map[string]interface{}
1163+
expectError bool
1164+
expectedComment *github.IssueComment
1165+
expectedErrMsg string
1166+
}{
1167+
{
1168+
name: "successful comment update",
1169+
mockedClient: mock.NewMockedHTTPClient(
1170+
mock.WithRequestMatchHandler(
1171+
mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId,
1172+
expectRequestBody(t, map[string]interface{}{
1173+
"body": "Updated issue comment text here",
1174+
}).andThen(
1175+
mockResponse(t, http.StatusOK, mockUpdatedComment),
1176+
),
1177+
),
1178+
),
1179+
requestArgs: map[string]interface{}{
1180+
"owner": "owner",
1181+
"repo": "repo",
1182+
"commentId": float64(123),
1183+
"body": "Updated issue comment text here",
1184+
},
1185+
expectError: false,
1186+
expectedComment: mockUpdatedComment,
1187+
},
1188+
{
1189+
name: "comment update fails - not found",
1190+
mockedClient: mock.NewMockedHTTPClient(
1191+
mock.WithRequestMatchHandler(
1192+
mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId,
1193+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1194+
w.WriteHeader(http.StatusNotFound)
1195+
w.Header().Set("Content-Type", "application/json")
1196+
_, _ = w.Write([]byte(`{"message": "Comment not found"}`))
1197+
}),
1198+
),
1199+
),
1200+
requestArgs: map[string]interface{}{
1201+
"owner": "owner",
1202+
"repo": "repo",
1203+
"commentId": float64(999),
1204+
"body": "This should fail",
1205+
},
1206+
expectError: true,
1207+
expectedErrMsg: "failed to update issue comment",
1208+
},
1209+
{
1210+
name: "comment update fails - validation error",
1211+
mockedClient: mock.NewMockedHTTPClient(
1212+
mock.WithRequestMatchHandler(
1213+
mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId,
1214+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1215+
w.WriteHeader(http.StatusUnprocessableEntity)
1216+
w.Header().Set("Content-Type", "application/json")
1217+
_, _ = w.Write([]byte(`{"message": "Validation Failed"}`))
1218+
}),
1219+
),
1220+
),
1221+
requestArgs: map[string]interface{}{
1222+
"owner": "owner",
1223+
"repo": "repo",
1224+
"commentId": float64(123),
1225+
"body": "Invalid body",
1226+
},
1227+
expectError: true,
1228+
expectedErrMsg: "failed to update issue comment",
1229+
},
1230+
}
1231+
1232+
for _, tc := range tests {
1233+
t.Run(tc.name, func(t *testing.T) {
1234+
client := github.NewClient(tc.mockedClient)
1235+
_, handler := UpdateIssueComment(stubGetClientFn(client), translations.NullTranslationHelper)
1236+
1237+
request := createMCPRequest(tc.requestArgs)
1238+
1239+
result, err := handler(context.Background(), request)
1240+
1241+
if tc.expectError {
1242+
require.Error(t, err)
1243+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
1244+
return
1245+
}
1246+
1247+
require.NoError(t, err)
1248+
assert.NotNil(t, result)
1249+
require.Len(t, result.Content, 1)
1250+
1251+
textContent := getTextResult(t, result)
1252+
1253+
// For non-error cases, check the returned comment
1254+
var returnedComment github.IssueComment
1255+
err = json.Unmarshal([]byte(textContent.Text), &returnedComment)
1256+
require.NoError(t, err)
1257+
1258+
assert.Equal(t, *tc.expectedComment.ID, *returnedComment.ID)
1259+
assert.Equal(t, *tc.expectedComment.Body, *returnedComment.Body)
1260+
if tc.expectedComment.HTMLURL != nil {
1261+
assert.Equal(t, *tc.expectedComment.HTMLURL, *returnedComment.HTMLURL)
1262+
}
1263+
if tc.expectedComment.User != nil && tc.expectedComment.User.Login != nil {
1264+
assert.Equal(t, *tc.expectedComment.User.Login, *returnedComment.User.Login)
1265+
}
1266+
})
1267+
}
1268+
}

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
4646
toolsets.NewServerTool(CreateIssue(getClient, t)),
4747
toolsets.NewServerTool(AddIssueComment(getClient, t)),
4848
toolsets.NewServerTool(UpdateIssue(getClient, t)),
49+
toolsets.NewServerTool(UpdateIssueComment(getClient, t)),
4950
)
5051
users := toolsets.NewToolset("users", "GitHub User related tools").
5152
AddReadTools(

0 commit comments

Comments
 (0)