Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fa69f90
Add reaction tools for issues and pull requests
timrogers Jun 19, 2026
a5511ca
Make reaction tools available in both granular and non-granular modes
timrogers Jun 19, 2026
774b75b
Update toolsnaps with aligned reaction tool descriptions
timrogers Jun 19, 2026
94c6f2c
Align reaction tool descriptions with codebase style
timrogers Jun 19, 2026
30da9da
Improve reaction tool responses and wording
timrogers Jun 19, 2026
009bb7b
Expose reactions through default comment tools
SamMorrowDrums Jun 23, 2026
e6276cf
Clarify PR review comment IDs for reactions
SamMorrowDrums Jun 23, 2026
7c53cde
Support issue comment reactions in add_issue_comment
SamMorrowDrums Jun 23, 2026
9908fbf
Merge branch 'main' into timrogers/planning-reaction-tools
SamMorrowDrums Jun 23, 2026
2c9410b
Harden combined comment reaction tools
SamMorrowDrums Jun 23, 2026
d218ebe
Group reaction tools with granular registrations
SamMorrowDrums Jun 23, 2026
7fff6f2
Align granular reaction tool constructors
SamMorrowDrums Jun 23, 2026
9855c06
Require issue number for comment reactions
SamMorrowDrums Jun 24, 2026
b0d11b0
Address issue comment feedback
SamMorrowDrums Jun 26, 2026
ca55793
Merge branch 'main' into timrogers/planning-reaction-tools
SamMorrowDrums Jun 26, 2026
e522c77
Validate issue comment reaction target
SamMorrowDrums Jun 26, 2026
ea14878
Validate positive comment IDs
SamMorrowDrums Jun 26, 2026
95737a9
Merge remote-tracking branch 'origin/main' into pr/2732/timrogers/pla…
SamMorrowDrums Jun 26, 2026
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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,20 @@ The following sets of tools are available:
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)

- **add_issue_comment_reaction** - Add Issue Comment Reaction
- **Required OAuth Scopes**: `repo`
- `comment_id`: The issue comment ID (number, required)
- `content`: The emoji reaction type (string, required)
- `owner`: Repository owner (username or organization) (string, required)
- `repo`: Repository name (string, required)

- **add_issue_reaction** - Add Issue Reaction
- **Required OAuth Scopes**: `repo`
- `content`: The emoji reaction type (string, required)
- `issue_number`: The issue number (number, required)
- `owner`: Repository owner (username or organization) (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)
Expand Down Expand Up @@ -1095,6 +1109,13 @@ 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_pull_request_review_comment_reaction** - Add Pull Request Review Comment Reaction
- **Required OAuth Scopes**: `repo`
- `comment_id`: The pull request review comment ID (number, required)
- `content`: The emoji reaction type (string, required)
- `owner`: Repository owner (username or organization) (string, required)
- `repo`: Repository name (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)
Expand Down
47 changes: 47 additions & 0 deletions pkg/github/__toolsnaps__/add_issue_comment_reaction.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"annotations": {
"destructiveHint": false,
"openWorldHint": true,
"title": "Add Issue Comment Reaction"
},
"description": "Add a reaction to an issue comment.",
"inputSchema": {
"properties": {
"comment_id": {
"description": "The issue comment ID",
"minimum": 1,
"type": "number"
},
"content": {
"description": "The emoji reaction type",
"enum": [
"+1",
"-1",
"laugh",
"confused",
"heart",
"hooray",
"rocket",
"eyes"
],
"type": "string"
},
"owner": {
"description": "Repository owner (username or organization)",
"type": "string"
},
"repo": {
"description": "Repository name",
"type": "string"
}
},
"required": [
"owner",
"repo",
"comment_id",
"content"
],
"type": "object"
},
"name": "add_issue_comment_reaction"
}
47 changes: 47 additions & 0 deletions pkg/github/__toolsnaps__/add_issue_reaction.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"annotations": {
"destructiveHint": false,
"openWorldHint": true,
"title": "Add Issue Reaction"
},
"description": "Add a reaction to an issue.",
"inputSchema": {
"properties": {
"content": {
"description": "The emoji reaction type",
"enum": [
"+1",
"-1",
"laugh",
"confused",
"heart",
"hooray",
"rocket",
"eyes"
],
"type": "string"
},
"issue_number": {
"description": "The issue number",
"minimum": 1,
"type": "number"
},
"owner": {
"description": "Repository owner (username or organization)",
"type": "string"
},
"repo": {
"description": "Repository name",
"type": "string"
}
},
"required": [
"owner",
"repo",
"issue_number",
"content"
],
"type": "object"
},
"name": "add_issue_reaction"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"annotations": {
"destructiveHint": false,
"openWorldHint": true,
"title": "Add Pull Request Review Comment Reaction"
},
"description": "Add a reaction to a pull request review comment.",
"inputSchema": {
"properties": {
"comment_id": {
"description": "The pull request review comment ID",
"minimum": 1,
"type": "number"
},
"content": {
"description": "The emoji reaction type",
"enum": [
"+1",
"-1",
"laugh",
"confused",
"heart",
"hooray",
"rocket",
"eyes"
],
"type": "string"
},
"owner": {
"description": "Repository owner (username or organization)",
"type": "string"
},
"repo": {
"description": "Repository name",
"type": "string"
}
},
"required": [
"owner",
"repo",
"comment_id",
"content"
],
"type": "object"
},
"name": "add_pull_request_review_comment_reaction"
}
180 changes: 180 additions & 0 deletions pkg/github/granular_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ func TestGranularToolSnaps(t *testing.T) {
GranularRemoveSubIssue,
GranularReprioritizeSubIssue,
GranularSetIssueFields,
AddIssueReaction,
AddIssueCommentReaction,
GranularUpdatePullRequestTitle,
GranularUpdatePullRequestBody,
GranularUpdatePullRequestState,
Expand All @@ -54,6 +56,7 @@ func TestGranularToolSnaps(t *testing.T) {
GranularAddPullRequestReviewComment,
GranularResolveReviewThread,
GranularUnresolveReviewThread,
AddPullRequestReviewCommentReaction,
}

for _, constructor := range toolConstructors {
Expand Down Expand Up @@ -2025,3 +2028,180 @@ func TestGranularSetIssueFields(t *testing.T) {
assert.Equal(t, "update_issue_suggestions", spy.captured.Get(headers.GraphQLFeaturesHeader))
})
}

// --- Reaction granular tool handler tests ---

func TestAddIssueReaction(t *testing.T) {
mockReaction := &gogithub.Reaction{
ID: gogithub.Ptr(int64(12345)),
Content: gogithub.Ptr("+1"),
}

tests := []struct {
name string
mockedClient *http.Client
args map[string]any
expectErr bool
}{
{
name: "add reaction to issue successfully",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposIssuesReactionsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusCreated, mockReaction),
}),
args: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(42),
"content": "+1",
},
expectErr: false,
},
{
name: "missing owner returns error",
mockedClient: MockHTTPClientWithHandlers(nil),
args: map[string]any{
"repo": "repo",
"issue_number": float64(42),
"content": "+1",
},
expectErr: true,
},
{
name: "missing content returns error",
mockedClient: MockHTTPClientWithHandlers(nil),
args: map[string]any{
"owner": "owner",
"repo": "repo",
"issue_number": float64(42),
},
expectErr: true,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{Client: client}
serverTool := AddIssueReaction(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.args)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
if tc.expectErr {
assert.True(t, result.IsError)
} else {
assert.False(t, result.IsError)
}
})
}
}

func TestAddIssueCommentReaction(t *testing.T) {
mockReaction := &gogithub.Reaction{
ID: gogithub.Ptr(int64(67890)),
Content: gogithub.Ptr("heart"),
}

tests := []struct {
name string
mockedClient *http.Client
args map[string]any
expectErr bool
}{
{
name: "add reaction to issue comment successfully",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposIssuesCommentsReactionsByOwnerByRepoByCommentID: mockResponse(t, http.StatusCreated, mockReaction),
}),
args: map[string]any{
"owner": "owner",
"repo": "repo",
"comment_id": float64(999),
"content": "heart",
},
expectErr: false,
},
{
name: "missing comment_id returns error",
mockedClient: MockHTTPClientWithHandlers(nil),
args: map[string]any{
"owner": "owner",
"repo": "repo",
"content": "heart",
},
expectErr: true,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{Client: client}
serverTool := AddIssueCommentReaction(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.args)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
if tc.expectErr {
assert.True(t, result.IsError)
} else {
assert.False(t, result.IsError)
}
})
}
}

func TestAddPullRequestReviewCommentReaction(t *testing.T) {
mockReaction := &gogithub.Reaction{
ID: gogithub.Ptr(int64(54321)),
Content: gogithub.Ptr("rocket"),
}

tests := []struct {
name string
mockedClient *http.Client
args map[string]any
expectErr bool
}{
{
name: "add reaction to PR review comment successfully",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
PostReposPullsCommentsReactionsByOwnerByRepoByCommentID: mockResponse(t, http.StatusCreated, mockReaction),
}),
args: map[string]any{
"owner": "owner",
"repo": "repo",
"comment_id": float64(888),
"content": "rocket",
},
expectErr: false,
},
{
name: "missing repo returns error",
mockedClient: MockHTTPClientWithHandlers(nil),
args: map[string]any{
"owner": "owner",
"comment_id": float64(888),
"content": "rocket",
},
expectErr: true,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
client := mustNewGHClient(t, tc.mockedClient)
deps := BaseDeps{Client: client}
serverTool := AddPullRequestReviewCommentReaction(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
request := createMCPRequest(tc.args)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
if tc.expectErr {
assert.True(t, result.IsError)
} else {
assert.False(t, result.IsError)
}
})
}
}
Loading
Loading