diff --git a/README.md b/README.md index ce27bdb06..819e64a08 100644 --- a/README.md +++ b/README.md @@ -534,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) 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..ccae57785 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -327,6 +327,73 @@ 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 { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to edit comment", resp, err), nil + } + defer func() { _ = resp.Body.Close() }() + + 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..324ada4ad 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:", + }, + { + 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)),
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: