From d0f8aaf77a7fb3e38407a015022995c371d65f4d Mon Sep 17 00:00:00 2001 From: lihuanshuai Date: Tue, 24 Feb 2026 19:17:56 +0800 Subject: [PATCH 1/2] feat: add update_issue_comment tool - Add UpdateIssueComment in pkg/github/issues.go: new tool to update an existing issue or PR comment by comment_id (owner, repo, comment_id, body) - Register UpdateIssueComment in pkg/github/tools.go under issues toolset - Document update_issue_comment in README.md Issues section - Uses GitHub REST Issues.EditComment; comment_id from issue_read get_comments or add_issue_comment response --- README.md | 11 ++++-- pkg/github/issues.go | 84 ++++++++++++++++++++++++++++++++++++++++++++ pkg/github/tools.go | 1 + 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f83989a1e..457c07ef7 100644 --- a/README.md +++ b/README.md @@ -815,6 +815,13 @@ The following sets of tools are available: - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) +- **update_issue_comment** - Update an existing issue or pull request comment + - **Required OAuth Scopes**: `repo` + - `body`: New comment content (string, required) + - `comment_id`: ID of the comment to update, from issue_read get_comments or add_issue_comment response (number, required) + - `owner`: Repository owner (string, required) + - `repo`: Repository name (string, required) + - **get_label** - Get a specific label from a repository. - **Required OAuth Scopes**: `repo` - `name`: Label name. (string, required) @@ -1093,8 +1100,8 @@ The following sets of tools are available: - **pull_request_read** - Get details for a single pull request - **Required OAuth Scopes**: `repo` - - `method`: Action to specify what pull request data needs to be retrieved from GitHub. - Possible options: + - `method`: Action to specify what pull request data needs to be retrieved from GitHub. + Possible options: 1. get - Get details of a specific pull request. 2. get_diff - Get the diff of a pull request. 3. get_status - Get combined commit status of a head commit in a pull request. diff --git a/pkg/github/issues.go b/pkg/github/issues.go index ce3c4e945..4669831cd 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -702,6 +702,90 @@ func AddIssueComment(t translations.TranslationHelperFunc) inventory.ServerTool }) } +// UpdateIssueComment creates a tool to update an existing comment on an issue (or pull request). +func UpdateIssueComment(t translations.TranslationHelperFunc) inventory.ServerTool { + return NewTool( + ToolsetMetadataIssues, + mcp.Tool{ + Name: "update_issue_comment", + Description: t("TOOL_UPDATE_ISSUE_COMMENT_DESCRIPTION", "Update an existing comment on an issue or pull request in a GitHub repository. Use the comment ID from issue_read with method get_comments, or from the comment object when adding a comment."), + Annotations: &mcp.ToolAnnotations{ + Title: t("TOOL_UPDATE_ISSUE_COMMENT_USER_TITLE", "Update issue comment"), + ReadOnlyHint: false, + }, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "owner": { + Type: "string", + Description: "Repository owner", + }, + "repo": { + Type: "string", + Description: "Repository name", + }, + "comment_id": { + Type: "number", + Description: "ID of the comment to update (from issue_read get_comments or add_issue_comment response)", + }, + "body": { + Type: "string", + Description: "New comment content", + }, + }, + Required: []string{"owner", "repo", "comment_id", "body"}, + }, + }, + []scopes.Scope{scopes.Repo}, + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + owner, err := RequiredParam[string](args, "owner") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + repo, err := RequiredParam[string](args, "repo") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + commentID, err := RequiredBigInt(args, "comment_id") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + body, err := RequiredParam[string](args, "body") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + + comment := &github.IssueComment{ + Body: github.Ptr(body), + } + + client, err := deps.GetClient(ctx) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil + } + updatedComment, resp, err := client.Issues.EditComment(ctx, owner, repo, commentID, comment) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to update comment", err), nil, nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil + } + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to update comment", resp, body), nil, nil + } + + r, err := json.Marshal(updatedComment) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil + } + + return utils.NewToolResultText(string(r)), nil, nil + }) +} + // SubIssueWrite creates a tool to add a sub-issue to a parent issue. func SubIssueWrite(t translations.TranslationHelperFunc) inventory.ServerTool { return NewTool( diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 3f1c291a7..d41dfe25f 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -196,6 +196,7 @@ func AllTools(t translations.TranslationHelperFunc) []inventory.ServerTool { ListIssueTypes(t), IssueWrite(t), AddIssueComment(t), + UpdateIssueComment(t), SubIssueWrite(t), // User tools From cf0c753294ea6f1ee0287cac3a3f124f9d4050cb Mon Sep 17 00:00:00 2001 From: lihuanshuai Date: Wed, 25 Feb 2026 19:07:51 +0800 Subject: [PATCH 2/2] feat: implement UpdateIssueComment test and snapshot - Add unit test for UpdateIssueComment tool in pkg/github/issues_test.go to verify functionality and input validation. - Introduce a snapshot file for UpdateIssueComment tool to document expected behavior and input schema. - Include new PATCH endpoint constant for updating issue comments in pkg/github/helper_test.go. --- .../__toolsnaps__/update_issue_comment.snap | 34 ++++++ pkg/github/helper_test.go | 1 + pkg/github/issues_test.go | 103 ++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 pkg/github/__toolsnaps__/update_issue_comment.snap diff --git a/pkg/github/__toolsnaps__/update_issue_comment.snap b/pkg/github/__toolsnaps__/update_issue_comment.snap new file mode 100644 index 000000000..f3ab6a042 --- /dev/null +++ b/pkg/github/__toolsnaps__/update_issue_comment.snap @@ -0,0 +1,34 @@ +{ + "annotations": { + "title": "Update issue comment" + }, + "description": "Update an existing comment on an issue or pull request in a GitHub repository. Use the comment ID from issue_read with method get_comments, or from the comment object when adding a comment.", + "inputSchema": { + "properties": { + "body": { + "description": "New comment content", + "type": "string" + }, + "comment_id": { + "description": "ID of the comment to update (from issue_read get_comments or add_issue_comment response)", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "comment_id", + "body" + ], + "type": "object" + }, + "name": "update_issue_comment" +} \ No newline at end of file diff --git a/pkg/github/helper_test.go b/pkg/github/helper_test.go index ff752f5f3..ef11ed0a3 100644 --- a/pkg/github/helper_test.go +++ b/pkg/github/helper_test.go @@ -58,6 +58,7 @@ const ( GetReposIssuesCommentsByOwnerByRepoByIssueNumber = "GET /repos/{owner}/{repo}/issues/{issue_number}/comments" PostReposIssuesByOwnerByRepo = "POST /repos/{owner}/{repo}/issues" PostReposIssuesCommentsByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/comments" + PatchReposIssuesCommentsByOwnerByRepoByCommentID = "PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}" PatchReposIssuesByOwnerByRepoByIssueNumber = "PATCH /repos/{owner}/{repo}/issues/{issue_number}" GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber = "GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues" PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber = "POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues" diff --git a/pkg/github/issues_test.go b/pkg/github/issues_test.go index f9b8c7c62..72284e376 100644 --- a/pkg/github/issues_test.go +++ b/pkg/github/issues_test.go @@ -469,6 +469,109 @@ func Test_AddIssueComment(t *testing.T) { } } +func Test_UpdateIssueComment(t *testing.T) { + // Verify tool definition once + serverTool := UpdateIssueComment(translations.NullTranslationHelper) + tool := serverTool.Tool + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "update_issue_comment", tool.Name) + assert.NotEmpty(t, tool.Description) + + assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner") + assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo") + assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "comment_id") + assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "body") + assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "comment_id", "body"}) + + mockComment := &github.IssueComment{ + ID: github.Ptr(int64(456)), + Body: github.Ptr("Updated comment body"), + User: &github.User{ + Login: github.Ptr("testuser"), + }, + HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42#issuecomment-456"), + } + + tests := []struct { + name string + mockedClient *http.Client + requestArgs map[string]any + expectError bool + expectedComment *github.IssueComment + expectedErrMsg string + }{ + { + name: "successful comment update", + mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + PatchReposIssuesCommentsByOwnerByRepoByCommentID: mockResponse(t, http.StatusOK, mockComment), + }), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "comment_id": float64(456), + "body": "Updated comment body", + }, + expectError: false, + expectedComment: mockComment, + }, + { + name: "comment update fails - missing body", + mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + PatchReposIssuesCommentsByOwnerByRepoByCommentID: mockResponse(t, http.StatusOK, mockComment), + }), + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "comment_id": float64(456), + "body": "", + }, + expectError: false, + expectedErrMsg: "missing required parameter: body", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + client := github.NewClient(tc.mockedClient) + deps := BaseDeps{ + Client: client, + } + handler := serverTool.Handler(deps) + + request := createMCPRequest(tc.requestArgs) + + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + + if tc.expectError { + require.Error(t, err) + if tc.expectedErrMsg != "" { + 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) + + textContent := getTextResult(t, 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 serverTool := SearchIssues(translations.NullTranslationHelper)