Skip to content

Commit fb384b6

Browse files
ashwin-antclaude
andcommitted
Add reply_to_pull_request_review_comment tool
Adds a new tool to reply to existing pull request review comments using the GitHub API's comment reply endpoint. This allows for threaded discussions on pull request reviews. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e94a9f3 commit fb384b6

File tree

3 files changed

+175
-0
lines changed

3 files changed

+175
-0
lines changed

pkg/github/pullrequests.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,77 @@ func addPullRequestReviewComment(client *github.Client, t translations.Translati
658658
}
659659
}
660660

661+
// replyToPullRequestReviewComment creates a tool to reply to an existing review comment on a pull request.
662+
func replyToPullRequestReviewComment(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool,
663+
handler server.ToolHandlerFunc) {
664+
return mcp.NewTool("reply_to_pull_request_review_comment",
665+
mcp.WithDescription(t("TOOL_REPLY_TO_PULL_REQUEST_REVIEW_COMMENT_DESCRIPTION", "Reply to an existing review comment on a pull request")),
666+
mcp.WithString("owner",
667+
mcp.Required(),
668+
mcp.Description("Repository owner"),
669+
),
670+
mcp.WithString("repo",
671+
mcp.Required(),
672+
mcp.Description("Repository name"),
673+
),
674+
mcp.WithNumber("pull_number",
675+
mcp.Required(),
676+
mcp.Description("Pull request number"),
677+
),
678+
mcp.WithNumber("comment_id",
679+
mcp.Required(),
680+
mcp.Description("The unique identifier of the comment to reply to"),
681+
),
682+
mcp.WithString("body",
683+
mcp.Required(),
684+
mcp.Description("The text of the reply comment"),
685+
),
686+
),
687+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
688+
owner, err := requiredParam[string](request, "owner")
689+
if err != nil {
690+
return mcp.NewToolResultError(err.Error()), nil
691+
}
692+
repo, err := requiredParam[string](request, "repo")
693+
if err != nil {
694+
return mcp.NewToolResultError(err.Error()), nil
695+
}
696+
pullNumber, err := requiredInt(request, "pull_number")
697+
if err != nil {
698+
return mcp.NewToolResultError(err.Error()), nil
699+
}
700+
commentID, err := requiredInt(request, "comment_id")
701+
if err != nil {
702+
return mcp.NewToolResultError(err.Error()), nil
703+
}
704+
body, err := requiredParam[string](request, "body")
705+
if err != nil {
706+
return mcp.NewToolResultError(err.Error()), nil
707+
}
708+
709+
createdReply, resp, err := client.PullRequests.CreateCommentInReplyTo(ctx, owner, repo, pullNumber, body, int64(commentID))
710+
if err != nil {
711+
return nil, fmt.Errorf("failed to reply to pull request comment: %w", err)
712+
}
713+
defer func() { _ = resp.Body.Close() }()
714+
715+
if resp.StatusCode != http.StatusCreated {
716+
body, err := io.ReadAll(resp.Body)
717+
if err != nil {
718+
return nil, fmt.Errorf("failed to read response body: %w", err)
719+
}
720+
return mcp.NewToolResultError(fmt.Sprintf("failed to reply to pull request comment: %s", string(body))), nil
721+
}
722+
723+
r, err := json.Marshal(createdReply)
724+
if err != nil {
725+
return nil, fmt.Errorf("failed to marshal response: %w", err)
726+
}
727+
728+
return mcp.NewToolResultText(string(r)), nil
729+
}
730+
}
731+
661732
// getPullRequestReviews creates a tool to get the reviews on a pull request.
662733
func getPullRequestReviews(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
663734
return mcp.NewTool("get_pull_request_reviews",

pkg/github/pullrequests_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1505,3 +1505,106 @@ func Test_AddPullRequestReviewComment(t *testing.T) {
15051505
})
15061506
}
15071507
}
1508+
1509+
func Test_ReplyToPullRequestReviewComment(t *testing.T) {
1510+
// Verify tool definition once
1511+
mockClient := github.NewClient(nil)
1512+
tool, _ := replyToPullRequestReviewComment(mockClient, translations.NullTranslationHelper)
1513+
1514+
assert.Equal(t, "reply_to_pull_request_review_comment", tool.Name)
1515+
assert.NotEmpty(t, tool.Description)
1516+
assert.Contains(t, tool.InputSchema.Properties, "owner")
1517+
assert.Contains(t, tool.InputSchema.Properties, "repo")
1518+
assert.Contains(t, tool.InputSchema.Properties, "pull_number")
1519+
assert.Contains(t, tool.InputSchema.Properties, "comment_id")
1520+
assert.Contains(t, tool.InputSchema.Properties, "body")
1521+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pull_number", "comment_id", "body"})
1522+
1523+
// Setup mock PR comment for success case
1524+
mockReply := &github.PullRequestComment{
1525+
ID: github.Ptr(int64(456)),
1526+
Body: github.Ptr("Good point, will fix!"),
1527+
}
1528+
1529+
tests := []struct {
1530+
name string
1531+
mockedClient *http.Client
1532+
requestArgs map[string]interface{}
1533+
expectError bool
1534+
expectedReply *github.PullRequestComment
1535+
expectedErrMsg string
1536+
}{
1537+
{
1538+
name: "successful reply creation",
1539+
mockedClient: mock.NewMockedHTTPClient(
1540+
mock.WithRequestMatchHandler(
1541+
mock.PostReposPullsCommentsByOwnerByRepoByPullNumber,
1542+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1543+
w.WriteHeader(http.StatusCreated)
1544+
json.NewEncoder(w).Encode(mockReply)
1545+
}),
1546+
),
1547+
),
1548+
requestArgs: map[string]interface{}{
1549+
"owner": "owner",
1550+
"repo": "repo",
1551+
"pull_number": float64(1),
1552+
"comment_id": float64(123),
1553+
"body": "Good point, will fix!",
1554+
},
1555+
expectError: false,
1556+
expectedReply: mockReply,
1557+
},
1558+
{
1559+
name: "reply creation fails",
1560+
mockedClient: mock.NewMockedHTTPClient(
1561+
mock.WithRequestMatchHandler(
1562+
mock.PostReposPullsCommentsByOwnerByRepoByPullNumber,
1563+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1564+
w.WriteHeader(http.StatusNotFound)
1565+
w.Header().Set("Content-Type", "application/json")
1566+
_, _ = w.Write([]byte(`{"message": "Comment not found"}`))
1567+
}),
1568+
),
1569+
),
1570+
requestArgs: map[string]interface{}{
1571+
"owner": "owner",
1572+
"repo": "repo",
1573+
"pull_number": float64(1),
1574+
"comment_id": float64(999),
1575+
"body": "Good point, will fix!",
1576+
},
1577+
expectError: true,
1578+
expectedErrMsg: "failed to reply to pull request comment",
1579+
},
1580+
}
1581+
1582+
for _, tc := range tests {
1583+
t.Run(tc.name, func(t *testing.T) {
1584+
mockClient := github.NewClient(tc.mockedClient)
1585+
1586+
_, handler := replyToPullRequestReviewComment(mockClient, translations.NullTranslationHelper)
1587+
1588+
request := createMCPRequest(tc.requestArgs)
1589+
1590+
result, err := handler(context.Background(), request)
1591+
1592+
if tc.name == "reply creation fails" {
1593+
require.Error(t, err)
1594+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
1595+
return
1596+
}
1597+
1598+
require.NoError(t, err)
1599+
assert.NotNil(t, result)
1600+
require.Len(t, result.Content, 1)
1601+
1602+
var returnedReply github.PullRequestComment
1603+
err = json.Unmarshal([]byte(getTextResult(t, result).Text), &returnedReply)
1604+
require.NoError(t, err)
1605+
1606+
assert.Equal(t, *tc.expectedReply.ID, *returnedReply.ID)
1607+
assert.Equal(t, *tc.expectedReply.Body, *returnedReply.Body)
1608+
})
1609+
}
1610+
}

pkg/github/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func NewServer(client *github.Client, readOnly bool, t translations.TranslationH
5454
s.AddTool(createPullRequestReview(client, t))
5555
s.AddTool(createPullRequest(client, t))
5656
s.AddTool(addPullRequestReviewComment(client, t))
57+
s.AddTool(replyToPullRequestReviewComment(client, t))
5758
}
5859

5960
// Add GitHub tools - Repositories

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy