Skip to content

Commit 7b044f1

Browse files
kaovilaiclaude
andcommitted
feat: Add edit_issue_comment tool
Implements the edit_issue_comment tool to allow editing existing issue comments via the GitHub API. This addresses the feature request in issue #868. Changes: - Added EditIssueComment function in pkg/github/issues.go - Registered the new tool in pkg/github/tools.go - Added comprehensive tests for the new functionality - Updated README documentation with the new tool Fixes #868 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1832210 commit 7b044f1

File tree

5 files changed

+252
-0
lines changed

5 files changed

+252
-0
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,12 @@ The following sets of tools are available (all are on by default):
513513
- `owner`: Repository owner (string, required)
514514
- `repo`: Repository name (string, required)
515515

516+
- **edit_issue_comment** - Edit issue comment
517+
- `body`: New comment text content (string, required)
518+
- `comment_id`: The ID of the comment to edit (number, required)
519+
- `owner`: Repository owner (string, required)
520+
- `repo`: Repository name (string, required)
521+
516522
- **add_sub_issue** - Add sub-issue
517523
- `issue_number`: The number of the parent issue (number, required)
518524
- `owner`: Repository owner (string, required)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"annotations": {
3+
"title": "Edit issue comment",
4+
"readOnlyHint": false
5+
},
6+
"description": "Edit an existing comment on a GitHub issue.",
7+
"inputSchema": {
8+
"properties": {
9+
"body": {
10+
"description": "New comment text content",
11+
"type": "string"
12+
},
13+
"comment_id": {
14+
"description": "The ID of the comment to edit",
15+
"type": "number"
16+
},
17+
"owner": {
18+
"description": "Repository owner",
19+
"type": "string"
20+
},
21+
"repo": {
22+
"description": "Repository name",
23+
"type": "string"
24+
}
25+
},
26+
"required": [
27+
"owner",
28+
"repo",
29+
"comment_id",
30+
"body"
31+
],
32+
"type": "object"
33+
},
34+
"name": "edit_issue_comment"
35+
}

pkg/github/issues.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,91 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc
327327
}
328328
}
329329

330+
// EditIssueComment creates a tool to edit an existing issue comment.
331+
func EditIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
332+
return mcp.NewTool("edit_issue_comment",
333+
mcp.WithDescription(t("TOOL_EDIT_ISSUE_COMMENT_DESCRIPTION", "Edit an existing comment on a GitHub issue.")),
334+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
335+
Title: t("TOOL_EDIT_ISSUE_COMMENT_USER_TITLE", "Edit issue comment"),
336+
ReadOnlyHint: ToBoolPtr(false),
337+
}),
338+
mcp.WithString("owner",
339+
mcp.Required(),
340+
mcp.Description("Repository owner"),
341+
),
342+
mcp.WithString("repo",
343+
mcp.Required(),
344+
mcp.Description("Repository name"),
345+
),
346+
mcp.WithNumber("comment_id",
347+
mcp.Required(),
348+
mcp.Description("The ID of the comment to edit"),
349+
),
350+
mcp.WithString("body",
351+
mcp.Required(),
352+
mcp.Description("New comment text content"),
353+
),
354+
),
355+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
356+
owner, err := RequiredParam[string](request, "owner")
357+
if err != nil {
358+
return mcp.NewToolResultError(err.Error()), nil
359+
}
360+
repo, err := RequiredParam[string](request, "repo")
361+
if err != nil {
362+
return mcp.NewToolResultError(err.Error()), nil
363+
}
364+
commentIDInt, err := RequiredInt(request, "comment_id")
365+
if err != nil {
366+
return mcp.NewToolResultError(err.Error()), nil
367+
}
368+
commentID := int64(commentIDInt)
369+
body, err := RequiredParam[string](request, "body")
370+
if err != nil {
371+
return mcp.NewToolResultError(err.Error()), nil
372+
}
373+
374+
comment := &github.IssueComment{
375+
Body: github.Ptr(body),
376+
}
377+
378+
client, err := getClient(ctx)
379+
if err != nil {
380+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
381+
}
382+
editedComment, resp, err := client.Issues.EditComment(ctx, owner, repo, commentID, comment)
383+
if err != nil {
384+
if resp != nil {
385+
defer func() { _ = resp.Body.Close() }()
386+
if resp.StatusCode != http.StatusOK {
387+
body, err := io.ReadAll(resp.Body)
388+
if err != nil {
389+
return nil, fmt.Errorf("failed to read response body: %w", err)
390+
}
391+
return mcp.NewToolResultError(fmt.Sprintf("failed to edit comment: %s", string(body))), nil
392+
}
393+
}
394+
return mcp.NewToolResultError(fmt.Sprintf("failed to edit comment: %v", err)), nil
395+
}
396+
defer func() { _ = resp.Body.Close() }()
397+
398+
if resp.StatusCode != http.StatusOK {
399+
body, err := io.ReadAll(resp.Body)
400+
if err != nil {
401+
return nil, fmt.Errorf("failed to read response body: %w", err)
402+
}
403+
return mcp.NewToolResultError(fmt.Sprintf("failed to edit comment: %s", string(body))), nil
404+
}
405+
406+
r, err := json.Marshal(editedComment)
407+
if err != nil {
408+
return nil, fmt.Errorf("failed to marshal response: %w", err)
409+
}
410+
411+
return mcp.NewToolResultText(string(r)), nil
412+
}
413+
}
414+
330415
// AddSubIssue creates a tool to add a sub-issue to a parent issue.
331416
func AddSubIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
332417
return mcp.NewTool("add_sub_issue",

pkg/github/issues_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,131 @@ func Test_AddIssueComment(t *testing.T) {
236236
}
237237
}
238238

239+
func Test_EditIssueComment(t *testing.T) {
240+
// Verify tool definition once
241+
mockClient := github.NewClient(nil)
242+
tool, _ := EditIssueComment(stubGetClientFn(mockClient), translations.NullTranslationHelper)
243+
require.NoError(t, toolsnaps.Test(tool.Name, tool))
244+
245+
assert.Equal(t, "edit_issue_comment", tool.Name)
246+
assert.NotEmpty(t, tool.Description)
247+
assert.Contains(t, tool.InputSchema.Properties, "owner")
248+
assert.Contains(t, tool.InputSchema.Properties, "repo")
249+
assert.Contains(t, tool.InputSchema.Properties, "comment_id")
250+
assert.Contains(t, tool.InputSchema.Properties, "body")
251+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "comment_id", "body"})
252+
253+
// Setup mock comment for success case
254+
mockEditedComment := &github.IssueComment{
255+
ID: github.Ptr(int64(123)),
256+
Body: github.Ptr("This is the edited comment"),
257+
User: &github.User{
258+
Login: github.Ptr("testuser"),
259+
},
260+
HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42#issuecomment-123"),
261+
}
262+
263+
tests := []struct {
264+
name string
265+
mockedClient *http.Client
266+
requestArgs map[string]interface{}
267+
expectError bool
268+
expectedComment *github.IssueComment
269+
expectedErrMsg string
270+
}{
271+
{
272+
name: "successful comment edit",
273+
mockedClient: mock.NewMockedHTTPClient(
274+
mock.WithRequestMatchHandler(
275+
mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId,
276+
mockResponse(t, http.StatusOK, mockEditedComment),
277+
),
278+
),
279+
requestArgs: map[string]interface{}{
280+
"owner": "owner",
281+
"repo": "repo",
282+
"comment_id": float64(123),
283+
"body": "This is the edited comment",
284+
},
285+
expectError: false,
286+
expectedComment: mockEditedComment,
287+
},
288+
{
289+
name: "comment edit fails",
290+
mockedClient: mock.NewMockedHTTPClient(
291+
mock.WithRequestMatchHandler(
292+
mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId,
293+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
294+
w.WriteHeader(http.StatusNotFound)
295+
_, _ = w.Write([]byte(`{"message": "Comment not found"}`))
296+
}),
297+
),
298+
),
299+
requestArgs: map[string]interface{}{
300+
"owner": "owner",
301+
"repo": "repo",
302+
"comment_id": float64(999),
303+
"body": "This is the edited comment",
304+
},
305+
expectError: false,
306+
expectedErrMsg: "failed to edit comment: {\"message\": \"Comment not found\"}",
307+
},
308+
{
309+
name: "missing body parameter",
310+
mockedClient: mock.NewMockedHTTPClient(),
311+
requestArgs: map[string]interface{}{
312+
"owner": "owner",
313+
"repo": "repo",
314+
"comment_id": float64(123),
315+
},
316+
expectError: false,
317+
expectedErrMsg: "missing required parameter: body",
318+
},
319+
}
320+
321+
for _, tc := range tests {
322+
t.Run(tc.name, func(t *testing.T) {
323+
// Setup client with mock
324+
client := github.NewClient(tc.mockedClient)
325+
_, handler := EditIssueComment(stubGetClientFn(client), translations.NullTranslationHelper)
326+
327+
// Create call request
328+
request := createMCPRequest(tc.requestArgs)
329+
330+
// Call handler
331+
result, err := handler(context.Background(), request)
332+
333+
// Verify results
334+
if tc.expectError {
335+
require.Error(t, err)
336+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
337+
return
338+
}
339+
340+
if tc.expectedErrMsg != "" {
341+
require.NotNil(t, result)
342+
textContent := getTextResult(t, result)
343+
assert.Contains(t, textContent.Text, tc.expectedErrMsg)
344+
return
345+
}
346+
347+
require.NoError(t, err)
348+
349+
// Parse the result and get the text content if no error
350+
textContent := getTextResult(t, result)
351+
352+
// Parse JSON from result
353+
var returnedComment github.IssueComment
354+
err = json.Unmarshal([]byte(textContent.Text), &returnedComment)
355+
require.NoError(t, err)
356+
assert.Equal(t, *tc.expectedComment.ID, *returnedComment.ID)
357+
assert.Equal(t, *tc.expectedComment.Body, *returnedComment.Body)
358+
assert.Equal(t, *tc.expectedComment.User.Login, *returnedComment.User.Login)
359+
360+
})
361+
}
362+
}
363+
239364
func Test_SearchIssues(t *testing.T) {
240365
// Verify tool definition once
241366
mockClient := github.NewClient(nil)

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
6161
AddWriteTools(
6262
toolsets.NewServerTool(CreateIssue(getClient, t)),
6363
toolsets.NewServerTool(AddIssueComment(getClient, t)),
64+
toolsets.NewServerTool(EditIssueComment(getClient, t)),
6465
toolsets.NewServerTool(UpdateIssue(getClient, t)),
6566
toolsets.NewServerTool(AssignCopilotToIssue(getGQLClient, t)),
6667
toolsets.NewServerTool(AddSubIssue(getClient, t)),

0 commit comments

Comments
 (0)