From 7b044f11f75e5de80de4a448f362a4ace2de17a6 Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Wed, 13 Aug 2025 13:26:02 -0500 Subject: [PATCH 1/5] feat: Add edit_issue_comment tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 6 + .../__toolsnaps__/edit_issue_comment.snap | 35 +++++ pkg/github/issues.go | 85 ++++++++++++ pkg/github/issues_test.go | 125 ++++++++++++++++++ pkg/github/tools.go | 1 + 5 files changed, 252 insertions(+) create mode 100644 pkg/github/__toolsnaps__/edit_issue_comment.snap diff --git a/README.md b/README.md index ce27bdb06..da37c254a 100644 --- a/README.md +++ b/README.md @@ -513,6 +513,12 @@ The following sets of tools are available (all are on by default): - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) +- **edit_issue_comment** - Edit issue comment + - `body`: New comment text content (string, required) + - `comment_id`: The ID of the comment to edit (number, required) + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - **add_sub_issue** - Add sub-issue - `issue_number`: The number of the parent issue (number, required) - `owner`: Repository owner (string, required) diff --git a/pkg/github/__toolsnaps__/edit_issue_comment.snap b/pkg/github/__toolsnaps__/edit_issue_comment.snap new file mode 100644 index 000000000..0f7ca1ea6 --- /dev/null +++ b/pkg/github/__toolsnaps__/edit_issue_comment.snap @@ -0,0 +1,35 @@ +{ + "annotations": { + "title": "Edit issue comment", + "readOnlyHint": false + }, + "description": "Edit an existing comment on a GitHub issue.", + "inputSchema": { + "properties": { + "body": { + "description": "New comment text content", + "type": "string" + }, + "comment_id": { + "description": "The ID of the comment to edit", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "comment_id", + "body" + ], + "type": "object" + }, + "name": "edit_issue_comment" +} \ No newline at end of file diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 3a1440489..6d3ebc748 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -327,6 +327,91 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc } } +// EditIssueComment creates a tool to edit an existing issue comment. +func EditIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("edit_issue_comment", + mcp.WithDescription(t("TOOL_EDIT_ISSUE_COMMENT_DESCRIPTION", "Edit an existing comment on a GitHub issue.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_EDIT_ISSUE_COMMENT_USER_TITLE", "Edit issue comment"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ), + mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ), + mcp.WithNumber("comment_id", + mcp.Required(), + mcp.Description("The ID of the comment to edit"), + ), + mcp.WithString("body", + mcp.Required(), + mcp.Description("New comment text content"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + commentIDInt, err := RequiredInt(request, "comment_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + commentID := int64(commentIDInt) + body, err := RequiredParam[string](request, "body") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + comment := &github.IssueComment{ + Body: github.Ptr(body), + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + editedComment, resp, err := client.Issues.EditComment(ctx, owner, repo, commentID, comment) + if err != nil { + if resp != nil { + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return mcp.NewToolResultError(fmt.Sprintf("failed to edit comment: %s", string(body))), nil + } + } + return mcp.NewToolResultError(fmt.Sprintf("failed to edit comment: %v", err)), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return mcp.NewToolResultError(fmt.Sprintf("failed to edit comment: %s", string(body))), nil + } + + r, err := json.Marshal(editedComment) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} + // AddSubIssue creates a tool to add a sub-issue to a parent issue. func AddSubIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("add_sub_issue", diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 249fadef8..2f9d5be55 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -236,6 +236,131 @@ func Test_AddIssueComment(t *testing.T) { } } +func Test_EditIssueComment(t *testing.T) { + // Verify tool definition once + mockClient := github.NewClient(nil) + tool, _ := EditIssueComment(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "edit_issue_comment", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "owner") + assert.Contains(t, tool.InputSchema.Properties, "repo") + assert.Contains(t, tool.InputSchema.Properties, "comment_id") + assert.Contains(t, tool.InputSchema.Properties, "body") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "comment_id", "body"}) + + // Setup mock comment for success case + mockEditedComment := &github.IssueComment{ + ID: github.Ptr(int64(123)), + Body: github.Ptr("This is the edited comment"), + User: &github.User{ + Login: github.Ptr("testuser"), + }, + HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42#issuecomment-123"), + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]interface{} + expectError bool + expectedComment *github.IssueComment + expectedErrMsg string + }{ + { + name: "successful comment edit", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId, + mockResponse(t, http.StatusOK, mockEditedComment), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "comment_id": float64(123), + "body": "This is the edited comment", + }, + expectError: false, + expectedComment: mockEditedComment, + }, + { + name: "comment edit fails", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PatchReposIssuesCommentsByOwnerByRepoByCommentId, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message": "Comment not found"}`)) + }), + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "comment_id": float64(999), + "body": "This is the edited comment", + }, + expectError: false, + expectedErrMsg: "failed to edit comment: {\"message\": \"Comment not found\"}", + }, + { + name: "missing body parameter", + mockedClient: mock.NewMockedHTTPClient(), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "comment_id": float64(123), + }, + expectError: false, + expectedErrMsg: "missing required parameter: body", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup client with mock + client := github.NewClient(tc.mockedClient) + _, handler := EditIssueComment(stubGetClientFn(client), translations.NullTranslationHelper) + + // Create call request + request := createMCPRequest(tc.requestArgs) + + // Call handler + result, err := handler(context.Background(), request) + + // Verify results + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrMsg) + return + } + + if tc.expectedErrMsg != "" { + require.NotNil(t, result) + textContent := getTextResult(t, result) + assert.Contains(t, textContent.Text, tc.expectedErrMsg) + return + } + + require.NoError(t, err) + + // Parse the result and get the text content if no error + textContent := getTextResult(t, result) + + // Parse JSON from result + var returnedComment github.IssueComment + err = json.Unmarshal([]byte(textContent.Text), &returnedComment) + require.NoError(t, err) + assert.Equal(t, *tc.expectedComment.ID, *returnedComment.ID) + assert.Equal(t, *tc.expectedComment.Body, *returnedComment.Body) + assert.Equal(t, *tc.expectedComment.User.Login, *returnedComment.User.Login) + + }) + } +} + func Test_SearchIssues(t *testing.T) { // Verify tool definition once mockClient := github.NewClient(nil) diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 3fb39ada7..f1b9fa3e8 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -61,6 +61,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG AddWriteTools( toolsets.NewServerTool(CreateIssue(getClient, t)), toolsets.NewServerTool(AddIssueComment(getClient, t)), + toolsets.NewServerTool(EditIssueComment(getClient, t)), toolsets.NewServerTool(UpdateIssue(getClient, t)), toolsets.NewServerTool(AssignCopilotToIssue(getGQLClient, t)), toolsets.NewServerTool(AddSubIssue(getClient, t)), From 58ebf67e3a01e9442f8b1cf610288543f98e9e5d Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Wed, 13 Aug 2025 14:35:45 -0400 Subject: [PATCH 2/5] Update README.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index da37c254a..66e044d14 100644 --- a/README.md +++ b/README.md @@ -514,10 +514,10 @@ The following sets of tools are available (all are on by default): - `repo`: Repository name (string, required) - **edit_issue_comment** - Edit issue comment - - `body`: New comment text content (string, required) - - `comment_id`: The ID of the comment to edit (number, required) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) + - `comment_id`: The ID of the comment to edit (number, required) + - `body`: New comment text content (string, required) - **add_sub_issue** - Add sub-issue - `issue_number`: The number of the parent issue (number, required) From 02e5f8956fb536ae0e056edc5b9454b10581be0c Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Wed, 13 Aug 2025 13:42:08 -0500 Subject: [PATCH 3/5] refactor: simplify error handling in EditIssueComment - Use ghErrors.NewGitHubAPIErrorResponse for consistent error handling - Remove redundant status code check after successful API call - Update test expectation to match new error format Addresses review comments from PR #2 --- pkg/github/issues.go | 20 +------------------- pkg/github/issues_test.go | 2 +- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 6d3ebc748..ccae57785 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -381,28 +381,10 @@ func EditIssueComment(getClient GetClientFn, t translations.TranslationHelperFun } editedComment, resp, err := client.Issues.EditComment(ctx, owner, repo, commentID, comment) if err != nil { - if resp != nil { - defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to edit comment: %s", string(body))), nil - } - } - return mcp.NewToolResultError(fmt.Sprintf("failed to edit comment: %v", err)), nil + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to edit comment", resp, err), nil } defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to edit comment: %s", string(body))), nil - } - r, err := json.Marshal(editedComment) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 2f9d5be55..5d42a41d4 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -303,7 +303,7 @@ func Test_EditIssueComment(t *testing.T) { "body": "This is the edited comment", }, expectError: false, - expectedErrMsg: "failed to edit comment: {\"message\": \"Comment not found\"}", + expectedErrMsg: "failed to edit comment: PATCH", }, { name: "missing body parameter", From 66851514938a5dff11bd6c93aeaee3e4e0b44e30 Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Wed, 13 Aug 2025 13:43:47 -0500 Subject: [PATCH 4/5] `go run ./cmd/github-mcp-server generate-docs` Signed-off-by: Tiger Kaovilai --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 66e044d14..819e64a08 100644 --- a/README.md +++ b/README.md @@ -513,12 +513,6 @@ The following sets of tools are available (all are on by default): - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) -- **edit_issue_comment** - Edit issue comment - - `owner`: Repository owner (string, required) - - `repo`: Repository name (string, required) - - `comment_id`: The ID of the comment to edit (number, required) - - `body`: New comment text content (string, required) - - **add_sub_issue** - Add sub-issue - `issue_number`: The number of the parent issue (number, required) - `owner`: Repository owner (string, required) @@ -540,6 +534,12 @@ The following sets of tools are available (all are on by default): - `repo`: Repository name (string, required) - `title`: Issue title (string, required) +- **edit_issue_comment** - Edit issue comment + - `body`: New comment text content (string, required) + - `comment_id`: The ID of the comment to edit (number, required) + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - **get_issue** - Get issue details - `issue_number`: The number of the issue (number, required) - `owner`: The owner of the repository (string, required) From dcc978fcad2430def54cf522439106a1f8e79be1 Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Wed, 13 Aug 2025 14:17:55 -0500 Subject: [PATCH 5/5] test: fix brittle assertion to check stable error prefix Change test expectation to verify the stable prefix 'failed to edit comment:' rather than checking for the HTTP method string. This makes the test less dependent on implementation details of the error wrapper. --- pkg/github/issues_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index 5d42a41d4..324ada4ad 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -303,7 +303,7 @@ func Test_EditIssueComment(t *testing.T) { "body": "This is the edited comment", }, expectError: false, - expectedErrMsg: "failed to edit comment: PATCH", + expectedErrMsg: "failed to edit comment:", }, { name: "missing body parameter", pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy