Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1073,14 +1073,6 @@ The following sets of tools are available:
- `startSide`: For multi-line comments, the starting side of the diff that the comment applies to. LEFT indicates the previous state, RIGHT indicates the new state (string, optional)
- `subjectType`: The level at which the comment is targeted (string, required)

- **add_reply_to_pull_request_comment** - Add reply to pull request comment
- **Required OAuth Scopes**: `repo`
- `body`: The text of the reply (string, required)
- `commentId`: The ID of the comment to reply to (number, required)
- `owner`: Repository owner (string, required)
- `pullNumber`: Pull request number (number, required)
- `repo`: Repository name (string, required)

- **create_pull_request** - Open new pull request
- **Required OAuth Scopes**: `repo`
- `base`: Branch to merge into (string, required)
Expand Down Expand Up @@ -1113,6 +1105,20 @@ The following sets of tools are available:
- `pullNumber`: Pull request number (number, required)
- `repo`: Repository name (string, required)

- **pull_request_comment_write** - Write operations (reply, update, delete) on pull request review comments
- **Required OAuth Scopes**: `repo`
- `body`: Comment text (required for 'reply' and 'update' methods) (string, optional)
- `commentId`: The ID of the review comment to operate on. For 'reply', this is the comment to reply to. (number, required)
- `method`: Write operation to perform on a pull request review comment.
Options are:
- 'reply' - add a reply to an existing review comment. This creates a new comment that is linked as a reply to the specified comment. Requires "pullNumber" and "body".
- 'update' - replace the body of an existing review comment. Requires "body". Only comments authored by the authenticated user can be updated.
- 'delete' - delete an existing review comment. Only comments authored by the authenticated user can be deleted.
(string, required)
- `owner`: Repository owner (string, required)
- `pullNumber`: Pull request number (required for 'reply' method) (number, optional)
- `repo`: Repository name (string, required)

- **pull_request_read** - Get details for a single pull request
- **Required OAuth Scopes**: `repo`
- `after`: Cursor for pagination, used only by the get_review_comments method. Pass the endCursor from the previous page's PageInfo to fetch the next page. (string, optional)
Expand Down
1 change: 1 addition & 0 deletions docs/tool-renaming.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Will get `issue_read` and `get_file_contents` tools registered, with no errors.
| Old Name | New Name |
|----------|----------|
| `add_project_item` | `projects_write` |
| `add_reply_to_pull_request_comment` | `pull_request_comment_write` |
| `cancel_workflow_run` | `actions_run_trigger` |
| `delete_project_item` | `projects_write` |
| `delete_workflow_run_logs` | `actions_run_trigger` |
Expand Down
39 changes: 0 additions & 39 deletions pkg/github/__toolsnaps__/add_reply_to_pull_request_comment.snap

This file was deleted.

48 changes: 48 additions & 0 deletions pkg/github/__toolsnaps__/pull_request_comment_write.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"annotations": {
"destructiveHint": true,
"title": "Write operations (reply, update, delete) on pull request review comments"
},
"description": "Write operations for pull request review comments.\n\nAvailable methods:\n- reply: Add a reply to an existing review comment. This creates a new comment that is linked as a reply to the specified comment.\n- update: Replace the body of an existing review comment. Only comments authored by the authenticated user can be updated.\n- delete: Delete an existing review comment. Only comments authored by the authenticated user can be deleted.\n",
"inputSchema": {
"properties": {
"body": {
"description": "Comment text (required for 'reply' and 'update' methods)",
"type": "string"
},
"commentId": {
"description": "The ID of the review comment to operate on. For 'reply', this is the comment to reply to.",
"type": "number"
},
"method": {
"description": "Write operation to perform on a pull request review comment.\nOptions are:\n- 'reply' - add a reply to an existing review comment. This creates a new comment that is linked as a reply to the specified comment. Requires \"pullNumber\" and \"body\".\n- 'update' - replace the body of an existing review comment. Requires \"body\". Only comments authored by the authenticated user can be updated.\n- 'delete' - delete an existing review comment. Only comments authored by the authenticated user can be deleted.\n",
"enum": [
"reply",
"update",
"delete"
],
"type": "string"
},
"owner": {
"description": "Repository owner",
"type": "string"
},
"pullNumber": {
"description": "Pull request number (required for 'reply' method)",
"type": "number"
},
"repo": {
"description": "Repository name",
"type": "string"
}
},
"required": [
"method",
"owner",
"repo",
"commentId"
],
"type": "object"
},
"name": "pull_request_comment_write"
}
3 changes: 3 additions & 0 deletions pkg/github/deprecated_tool_aliases.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ var DeprecatedToolAliases = map[string]string{
"add_project_item": "projects_write",
"update_project_item": "projects_write",
"delete_project_item": "projects_write",

// Pull request comment tools consolidated
"add_reply_to_pull_request_comment": "pull_request_comment_write",
}
2 changes: 2 additions & 0 deletions pkg/github/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ const (
PutReposPullsUpdateBranchByOwnerByRepoByPullNumber = "PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch"
PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber = "POST /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers"
PostReposPullsCommentsByOwnerByRepoByPullNumber = "POST /repos/{owner}/{repo}/pulls/{pull_number}/comments"
PatchReposPullsCommentsByOwnerByRepoByCommentID = "PATCH /repos/{owner}/{repo}/pulls/comments/{comment_id}"
DeleteReposPullsCommentsByOwnerByRepoByCommentID = "DELETE /repos/{owner}/{repo}/pulls/comments/{comment_id}"

// Notifications endpoints
GetNotifications = "GET /notifications"
Expand Down
155 changes: 121 additions & 34 deletions pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -1082,11 +1082,22 @@ func UpdatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
return st
}

// AddReplyToPullRequestComment creates a tool to add a reply to an existing pull request comment.
func AddReplyToPullRequestComment(t translations.TranslationHelperFunc) inventory.ServerTool {
// PullRequestCommentWrite creates a consolidated tool for write operations on
// pull request review comments: replying to, updating, and deleting them.
func PullRequestCommentWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
schema := &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"method": {
Type: "string",
Description: `Write operation to perform on a pull request review comment.
Options are:
- 'reply' - add a reply to an existing review comment. This creates a new comment that is linked as a reply to the specified comment. Requires "pullNumber" and "body".
- 'update' - replace the body of an existing review comment. Requires "body". Only comments authored by the authenticated user can be updated.
- 'delete' - delete an existing review comment. Only comments authored by the authenticated user can be deleted.
`,
Enum: []any{"reply", "update", "delete"},
},
"owner": {
Type: "string",
Description: "Repository owner",
Expand All @@ -1097,80 +1108,156 @@ func AddReplyToPullRequestComment(t translations.TranslationHelperFunc) inventor
},
"pullNumber": {
Type: "number",
Description: "Pull request number",
Description: "Pull request number (required for 'reply' method)",
},
"commentId": {
Type: "number",
Description: "The ID of the comment to reply to",
Description: "The ID of the review comment to operate on. For 'reply', this is the comment to reply to.",
},
"body": {
Type: "string",
Description: "The text of the reply",
Description: "Comment text (required for 'reply' and 'update' methods)",
},
},
Required: []string{"owner", "repo", "pullNumber", "commentId", "body"},
Required: []string{"method", "owner", "repo", "commentId"},
}

return NewTool(
ToolsetMetadataPullRequests,
mcp.Tool{
Name: "add_reply_to_pull_request_comment",
Description: t("TOOL_ADD_REPLY_TO_PULL_REQUEST_COMMENT_DESCRIPTION", "Add a reply to an existing pull request comment. This creates a new comment that is linked as a reply to the specified comment."),
Name: "pull_request_comment_write",
Description: t("TOOL_PULL_REQUEST_COMMENT_WRITE_DESCRIPTION", `Write operations for pull request review comments.

Available methods:
- reply: Add a reply to an existing review comment. This creates a new comment that is linked as a reply to the specified comment.
- update: Replace the body of an existing review comment. Only comments authored by the authenticated user can be updated.
- delete: Delete an existing review comment. Only comments authored by the authenticated user can be deleted.
`),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_ADD_REPLY_TO_PULL_REQUEST_COMMENT_USER_TITLE", "Add reply to pull request comment"),
ReadOnlyHint: false,
Title: t("TOOL_PULL_REQUEST_COMMENT_WRITE_USER_TITLE", "Write operations (reply, update, delete) on pull request review comments"),
ReadOnlyHint: false,
DestructiveHint: jsonschema.Ptr(true),
},
InputSchema: schema,
},
[]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")
method, err := RequiredParam[string](args, "method")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
repo, err := RequiredParam[string](args, "repo")
owner, err := RequiredParam[string](args, "owner")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
pullNumber, err := RequiredInt(args, "pullNumber")
repo, err := RequiredParam[string](args, "repo")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
commentID, err := RequiredInt(args, "commentId")
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
}

client, err := deps.GetClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
}

comment, resp, err := client.PullRequests.CreateCommentInReplyTo(ctx, owner, repo, pullNumber, body, int64(commentID))
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to add reply to pull request comment", resp, err), nil, nil
switch method {
case "reply":
return replyToPullRequestComment(ctx, client, args, owner, repo, int64(commentID))
case "update":
return updatePullRequestComment(ctx, client, args, owner, repo, int64(commentID))
case "delete":
return deletePullRequestComment(ctx, client, owner, repo, int64(commentID))
default:
return utils.NewToolResultError("invalid method, must be one of: 'reply', 'update', 'delete'"), nil, nil
}
defer func() { _ = resp.Body.Close() }()
})
}

if resp.StatusCode != http.StatusCreated {
bodyBytes, 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 add reply to pull request comment", resp, bodyBytes), nil, nil
}
func replyToPullRequestComment(ctx context.Context, client *github.Client, args map[string]any, owner, repo string, commentID int64) (*mcp.CallToolResult, any, error) {
pullNumber, err := RequiredInt(args, "pullNumber")
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
}

r, err := json.Marshal(comment)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
}
comment, resp, err := client.PullRequests.CreateCommentInReplyTo(ctx, owner, repo, pullNumber, body, commentID)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to add reply to pull request comment", resp, err), nil, nil
}
defer func() { _ = resp.Body.Close() }()

return utils.NewToolResultText(string(r)), nil, nil
})
if resp.StatusCode != http.StatusCreated {
bodyBytes, 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 add reply to pull request comment", resp, bodyBytes), nil, nil
}

r, err := json.Marshal(comment)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
}

return utils.NewToolResultText(string(r)), nil, nil
}

func updatePullRequestComment(ctx context.Context, client *github.Client, args map[string]any, owner, repo string, commentID int64) (*mcp.CallToolResult, any, error) {
body, err := RequiredParam[string](args, "body")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

comment, resp, err := client.PullRequests.EditComment(ctx, owner, repo, commentID, &github.PullRequestComment{
Body: github.Ptr(body),
})
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to update pull request comment", resp, err), nil, nil
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
bodyBytes, 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 pull request comment", resp, bodyBytes), nil, nil
}

r, err := json.Marshal(MinimalResponse{
ID: fmt.Sprintf("%d", comment.GetID()),
URL: comment.GetHTMLURL(),
})
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
}

return utils.NewToolResultText(string(r)), nil, nil
}

func deletePullRequestComment(ctx context.Context, client *github.Client, owner, repo string, commentID int64) (*mcp.CallToolResult, any, error) {
resp, err := client.PullRequests.DeleteComment(ctx, owner, repo, commentID)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to delete pull request comment", resp, err), nil, nil
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusNoContent {
bodyBytes, 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 delete pull request comment", resp, bodyBytes), nil, nil
}

return utils.NewToolResultText(fmt.Sprintf("pull request comment %d deleted successfully", commentID)), nil, nil
}

// ListPullRequests creates a tool to list and filter repository pull requests.
Expand Down
Loading