Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
timrogers and Copilot committed Sep 30, 2026
commit cb334bd3b636ba679c6c2f0f864e9c4cd9e6cdad
23 changes: 23 additions & 0 deletions docs/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,18 @@ as output formatting) won't appear here.
- `repo`: Repository name (string, required)
- `title`: Issue title (string, required)

- **hide_comment** - Hide Comment
- **OAuth Challenge Scopes**: `repo`
- `classifier`: The reason for hiding the comment (string, required)
- `comment_id`: The numeric ID of the comment, or of the review when comment_type is 'review' (integer, required)
- `comment_type`: The kind of comment:
- 'issue_comment' - a comment on an issue, or a conversation comment on a pull request.
- 'review_comment' - an inline comment on a pull request diff.
- 'review' - the body of a pull request review. Requires 'pull_number'. (string, required)
- `owner`: Repository owner (string, required)
- `pull_number`: Pull request number. Required when comment_type is 'review'. (number, optional)
- `repo`: Repository name (string, required)

- **remove_issue_comment_reaction** - Remove Reaction from Issue or Pull Request Comment
- **OAuth Challenge Scopes**: `repo`
- `comment_id`: The issue or pull request comment ID (number, required)
Expand Down Expand Up @@ -221,6 +233,17 @@ as output formatting) won't appear here.
- `owner`: Repository owner (username or organization) (string, required)
- `repo`: Repository name (string, required)

- **unhide_comment** - Unhide Comment
- **OAuth Challenge Scopes**: `repo`
- `comment_id`: The numeric ID of the comment, or of the review when comment_type is 'review' (integer, required)
- `comment_type`: The kind of comment:
- 'issue_comment' - a comment on an issue, or a conversation comment on a pull request.
- 'review_comment' - an inline comment on a pull request diff.
- 'review' - the body of a pull request review. Requires 'pull_number'. (string, required)
- `owner`: Repository owner (string, required)
- `pull_number`: Pull request number. Required when comment_type is 'review'. (number, optional)
- `repo`: Repository name (string, required)

- **update_issue_assignees** - Update Issue Assignees
- **OAuth Challenge Scopes**: `repo`
- `assignees`: GitHub usernames to assign to this issue. ([], required)
Expand Down
62 changes: 62 additions & 0 deletions pkg/github/__toolsnaps__/hide_comment.snap
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"
}
48 changes: 48 additions & 0 deletions pkg/github/__toolsnaps__/unhide_comment.snap
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"
}
207 changes: 207 additions & 0 deletions pkg/github/comment_minimize.go
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())
}
Comment thread
timrogers marked this conversation as resolved.
Outdated
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
}
Loading
Loading