Repository navigation
Add granular tools to hide and unhide issue comments, PR review comments and PR reviews #3350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
cb334bd
Add hide_comment and unhide_comment granular issue tools
timrogers d6eebd1
Rename comment_type enum values to pull_request_review(_comment)
timrogers 27c0dd5
Use realistic minimizedReason values in hide_comment test mocks
timrogers 07bf50e
Split hide/unhide comment tools per object type
timrogers 04359ff
Validate classifier and IDs before calling the GitHub API
timrogers File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next
Next commit
Add hide_comment and unhide_comment granular issue tools
Add two tools behind the issues_granular feature flag that hide (minimize) and unhide (unminimize) comments via the GraphQL minimizeComment/unminimizeComment mutations. They support issue and pull request conversation comments, pull request review comments, and pull request review bodies. Callers pass the numeric REST ID plus a comment_type; the tool resolves the GraphQL node ID via the matching REST endpoint before mutating. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Loading branch information
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| { | ||
| "annotations": { | ||
| "destructiveHint": false, | ||
| "idempotentHint": false, | ||
| "openWorldHint": true, | ||
| "readOnlyHint": false, | ||
| "title": "Hide Comment" | ||
| }, | ||
| "description": "Hide (minimize) a comment on an issue or pull request. Supports issue and pull request conversation comments, pull request review comments, and pull request review bodies. Requires triage or write access to the repository, or authorship of the comment.", | ||
| "inputSchema": { | ||
| "properties": { | ||
| "classifier": { | ||
| "description": "The reason for hiding the comment", | ||
| "enum": [ | ||
| "SPAM", | ||
| "ABUSE", | ||
| "OFF_TOPIC", | ||
| "OUTDATED", | ||
| "DUPLICATE", | ||
| "RESOLVED", | ||
| "LOW_QUALITY" | ||
| ], | ||
| "type": "string" | ||
| }, | ||
| "comment_id": { | ||
| "description": "The numeric ID of the comment, or of the review when comment_type is 'review'", | ||
| "minimum": 1, | ||
| "type": "integer" | ||
| }, | ||
| "comment_type": { | ||
| "description": "The kind of comment:\n- 'issue_comment' - a comment on an issue, or a conversation comment on a pull request.\n- 'review_comment' - an inline comment on a pull request diff.\n- 'review' - the body of a pull request review. Requires 'pull_number'.", | ||
| "enum": [ | ||
| "issue_comment", | ||
| "review_comment", | ||
| "review" | ||
| ], | ||
| "type": "string" | ||
| }, | ||
| "owner": { | ||
| "description": "Repository owner", | ||
| "type": "string" | ||
| }, | ||
| "pull_number": { | ||
| "description": "Pull request number. Required when comment_type is 'review'.", | ||
| "type": "number" | ||
| }, | ||
| "repo": { | ||
| "description": "Repository name", | ||
| "type": "string" | ||
| } | ||
| }, | ||
| "required": [ | ||
| "owner", | ||
| "repo", | ||
| "comment_type", | ||
| "comment_id", | ||
| "classifier" | ||
| ], | ||
| "type": "object" | ||
| }, | ||
| "name": "hide_comment" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| { | ||
| "annotations": { | ||
| "destructiveHint": false, | ||
| "idempotentHint": false, | ||
| "openWorldHint": true, | ||
| "readOnlyHint": false, | ||
| "title": "Unhide Comment" | ||
| }, | ||
| "description": "Unhide (unminimize) a previously hidden comment on an issue or pull request. Supports issue and pull request conversation comments, pull request review comments, and pull request review bodies. Requires triage or write access to the repository, or authorship of the comment.", | ||
| "inputSchema": { | ||
| "properties": { | ||
| "comment_id": { | ||
| "description": "The numeric ID of the comment, or of the review when comment_type is 'review'", | ||
| "minimum": 1, | ||
| "type": "integer" | ||
| }, | ||
| "comment_type": { | ||
| "description": "The kind of comment:\n- 'issue_comment' - a comment on an issue, or a conversation comment on a pull request.\n- 'review_comment' - an inline comment on a pull request diff.\n- 'review' - the body of a pull request review. Requires 'pull_number'.", | ||
| "enum": [ | ||
| "issue_comment", | ||
| "review_comment", | ||
| "review" | ||
| ], | ||
| "type": "string" | ||
| }, | ||
| "owner": { | ||
| "description": "Repository owner", | ||
| "type": "string" | ||
| }, | ||
| "pull_number": { | ||
| "description": "Pull request number. Required when comment_type is 'review'.", | ||
| "type": "number" | ||
| }, | ||
| "repo": { | ||
| "description": "Repository name", | ||
| "type": "string" | ||
| } | ||
| }, | ||
| "required": [ | ||
| "owner", | ||
| "repo", | ||
| "comment_type", | ||
| "comment_id" | ||
| ], | ||
| "type": "object" | ||
| }, | ||
| "name": "unhide_comment" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| package github | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| ghErrors "github.com/github/github-mcp-server/pkg/errors" | ||
| "github.com/github/github-mcp-server/pkg/utils" | ||
| "github.com/google/go-github/v89/github" | ||
| "github.com/google/jsonschema-go/jsonschema" | ||
| "github.com/modelcontextprotocol/go-sdk/mcp" | ||
| "github.com/shurcooL/githubv4" | ||
| ) | ||
|
|
||
| const ( | ||
| CommentTypeIssueComment = "issue_comment" | ||
| CommentTypeReviewComment = "review_comment" | ||
| CommentTypeReview = "review" | ||
| ) | ||
|
|
||
| // MinimizeCommentResult is the response returned by the minimize_comment tool. | ||
| type MinimizeCommentResult struct { | ||
| NodeID string `json:"node_id"` | ||
| IsMinimized bool `json:"is_minimized"` | ||
| MinimizedReason string `json:"minimized_reason,omitempty"` | ||
| } | ||
|
|
||
| var commentClassifiers = []any{"SPAM", "ABUSE", "OFF_TOPIC", "OUTDATED", "DUPLICATE", "RESOLVED", "LOW_QUALITY"} | ||
|
|
||
| const commentVisibilityDescriptionSuffix = "Supports issue and pull request conversation comments, pull request review comments, and pull request review bodies. " + | ||
| "Requires triage or write access to the repository, or authorship of the comment." | ||
|
|
||
| // commentTargetProperties returns the schema properties that identify the comment to hide or unhide. | ||
| func commentTargetProperties() map[string]*jsonschema.Schema { | ||
| return map[string]*jsonschema.Schema{ | ||
| "owner": { | ||
| Type: "string", | ||
| Description: "Repository owner", | ||
| }, | ||
| "repo": { | ||
| Type: "string", | ||
| Description: "Repository name", | ||
| }, | ||
| "comment_type": { | ||
| Type: "string", | ||
| Description: "The kind of comment:\n" + | ||
| "- 'issue_comment' - a comment on an issue, or a conversation comment on a pull request.\n" + | ||
| "- 'review_comment' - an inline comment on a pull request diff.\n" + | ||
| "- 'review' - the body of a pull request review. Requires 'pull_number'.", | ||
| Enum: []any{CommentTypeIssueComment, CommentTypeReviewComment, CommentTypeReview}, | ||
| }, | ||
| "comment_id": { | ||
| Type: "integer", | ||
| Description: "The numeric ID of the comment, or of the review when comment_type is 'review'", | ||
| Minimum: jsonschema.Ptr(1.0), | ||
| }, | ||
| "pull_number": { | ||
| Type: "number", | ||
| Description: "Pull request number. Required when comment_type is 'review'.", | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // setCommentVisibility resolves the comment identified by args and hides or unhides it. | ||
| func setCommentVisibility(ctx context.Context, deps ToolDependencies, args map[string]any, hide bool, classifier string) *mcp.CallToolResult { | ||
| owner, err := RequiredParam[string](args, "owner") | ||
| if err != nil { | ||
| return utils.NewToolResultError(err.Error()) | ||
| } | ||
| repo, err := RequiredParam[string](args, "repo") | ||
| if err != nil { | ||
| return utils.NewToolResultError(err.Error()) | ||
| } | ||
| commentType, err := RequiredParam[string](args, "comment_type") | ||
| if err != nil { | ||
| return utils.NewToolResultError(err.Error()) | ||
| } | ||
| commentID, err := RequiredBigInt(args, "comment_id") | ||
| if err != nil { | ||
| return utils.NewToolResultError(err.Error()) | ||
| } | ||
| pullNumber, err := OptionalIntParam(args, "pull_number") | ||
| if err != nil { | ||
| return utils.NewToolResultError(err.Error()) | ||
| } | ||
|
|
||
| client, err := deps.GetClient(ctx) | ||
| if err != nil { | ||
| return utils.NewToolResultErrorFromErr("failed to get GitHub client", err) | ||
| } | ||
|
|
||
| nodeID, errResult := resolveCommentNodeID(ctx, client, owner, repo, commentType, commentID, pullNumber) | ||
| if errResult != nil { | ||
| return errResult | ||
| } | ||
|
|
||
| gqlClient, err := deps.GetGQLClient(ctx) | ||
| if err != nil { | ||
| return utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err) | ||
| } | ||
|
|
||
| var result MinimizeCommentResult | ||
| if hide { | ||
| result, errResult = minimizeComment(ctx, gqlClient, nodeID, strings.ToUpper(classifier)) | ||
| } else { | ||
| result, errResult = unminimizeComment(ctx, gqlClient, nodeID) | ||
| } | ||
| if errResult != nil { | ||
| return errResult | ||
| } | ||
|
|
||
| r, err := json.Marshal(result) | ||
| if err != nil { | ||
| return utils.NewToolResultErrorFromErr("failed to marshal response", err) | ||
| } | ||
| return utils.NewToolResultText(string(r)) | ||
| } | ||
|
|
||
| // resolveCommentNodeID looks up the GraphQL node ID for a comment identified by its REST ID, | ||
| // since the minimize mutations only accept node IDs. | ||
| func resolveCommentNodeID(ctx context.Context, client *github.Client, owner, repo, commentType string, commentID int64, pullNumber int) (string, *mcp.CallToolResult) { | ||
| var ( | ||
| nodeID string | ||
| resp *github.Response | ||
| err error | ||
| ) | ||
|
|
||
| switch commentType { | ||
| case CommentTypeIssueComment: | ||
| var comment *github.IssueComment | ||
| comment, resp, err = client.Issues.GetComment(ctx, owner, repo, commentID) | ||
| nodeID = comment.GetNodeID() | ||
| case CommentTypeReviewComment: | ||
| var comment *github.PullRequestComment | ||
| comment, resp, err = client.PullRequests.GetComment(ctx, owner, repo, commentID) | ||
| nodeID = comment.GetNodeID() | ||
| case CommentTypeReview: | ||
| if pullNumber <= 0 { | ||
| return "", utils.NewToolResultError("pull_number is required when comment_type is 'review'") | ||
| } | ||
| var review *github.PullRequestReview | ||
| review, resp, err = client.PullRequests.GetReview(ctx, owner, repo, pullNumber, commentID) | ||
| nodeID = review.GetNodeID() | ||
| default: | ||
| return "", utils.NewToolResultError(fmt.Sprintf("unknown comment_type: %s", commentType)) | ||
| } | ||
|
|
||
| if resp != nil && resp.Body != nil { | ||
| defer func() { _ = resp.Body.Close() }() | ||
| } | ||
| if err != nil { | ||
| return "", ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get comment", resp, err) | ||
| } | ||
| if nodeID == "" { | ||
| return "", utils.NewToolResultError("comment has no node ID") | ||
| } | ||
| return nodeID, nil | ||
| } | ||
|
|
||
| func minimizeComment(ctx context.Context, client *githubv4.Client, nodeID, classifier string) (MinimizeCommentResult, *mcp.CallToolResult) { | ||
| var mutation struct { | ||
| MinimizeComment struct { | ||
| MinimizedComment struct { | ||
| IsMinimized githubv4.Boolean | ||
| MinimizedReason githubv4.String | ||
| } | ||
| } `graphql:"minimizeComment(input: $input)"` | ||
| } | ||
|
|
||
| input := githubv4.MinimizeCommentInput{ | ||
| SubjectID: githubv4.ID(nodeID), | ||
| Classifier: githubv4.ReportedContentClassifiers(classifier), | ||
| } | ||
| if err := client.Mutate(ctx, &mutation, input, nil); err != nil { | ||
| return MinimizeCommentResult{}, ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to minimize comment", err) | ||
| } | ||
|
|
||
| return MinimizeCommentResult{ | ||
| NodeID: nodeID, | ||
| IsMinimized: bool(mutation.MinimizeComment.MinimizedComment.IsMinimized), | ||
| MinimizedReason: string(mutation.MinimizeComment.MinimizedComment.MinimizedReason), | ||
| }, nil | ||
| } | ||
|
|
||
| func unminimizeComment(ctx context.Context, client *githubv4.Client, nodeID string) (MinimizeCommentResult, *mcp.CallToolResult) { | ||
| var mutation struct { | ||
| UnminimizeComment struct { | ||
| UnminimizedComment struct { | ||
| IsMinimized githubv4.Boolean | ||
| } | ||
| } `graphql:"unminimizeComment(input: $input)"` | ||
| } | ||
|
|
||
| input := githubv4.UnminimizeCommentInput{ | ||
| SubjectID: githubv4.ID(nodeID), | ||
| } | ||
| if err := client.Mutate(ctx, &mutation, input, nil); err != nil { | ||
| return MinimizeCommentResult{}, ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to unminimize comment", err) | ||
| } | ||
|
|
||
| return MinimizeCommentResult{ | ||
| NodeID: nodeID, | ||
| IsMinimized: bool(mutation.UnminimizeComment.UnminimizedComment.IsMinimized), | ||
| }, nil | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.