Skip to content

Commit e0f735d

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 8b5299a commit e0f735d

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
@@ -649,6 +649,77 @@ func AddPullRequestReviewComment(client *github.Client, t translations.Translati
649649
}
650650
}
651651

652+
// ReplyToPullRequestReviewComment creates a tool to reply to an existing review comment on a pull request.
653+
func ReplyToPullRequestReviewComment(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool,
654+
handler server.ToolHandlerFunc) {
655+
return mcp.NewTool("reply_to_pull_request_review_comment",
656+
mcp.WithDescription(t("TOOL_REPLY_TO_PULL_REQUEST_REVIEW_COMMENT_DESCRIPTION", "Reply to an existing review comment on a pull request")),
657+
mcp.WithString("owner",
658+
mcp.Required(),
659+
mcp.Description("Repository owner"),
660+
),
661+
mcp.WithString("repo",
662+
mcp.Required(),
663+
mcp.Description("Repository name"),
664+
),
665+
mcp.WithNumber("pull_number",
666+
mcp.Required(),
667+
mcp.Description("Pull request number"),
668+
),
669+
mcp.WithNumber("comment_id",
670+
mcp.Required(),
671+
mcp.Description("The unique identifier of the comment to reply to"),
672+
),
673+
mcp.WithString("body",
674+
mcp.Required(),
675+
mcp.Description("The text of the reply comment"),
676+
),
677+
),
678+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
679+
owner, err := requiredParam[string](request, "owner")
680+
if err != nil {
681+
return mcp.NewToolResultError(err.Error()), nil
682+
}
683+
repo, err := requiredParam[string](request, "repo")
684+
if err != nil {
685+
return mcp.NewToolResultError(err.Error()), nil
686+
}
687+
pullNumber, err := requiredInt(request, "pull_number")
688+
if err != nil {
689+
return mcp.NewToolResultError(err.Error()), nil
690+
}
691+
commentID, err := requiredInt(request, "comment_id")
692+
if err != nil {
693+
return mcp.NewToolResultError(err.Error()), nil
694+
}
695+
body, err := requiredParam[string](request, "body")
696+
if err != nil {
697+
return mcp.NewToolResultError(err.Error()), nil
698+
}
699+
700+
createdReply, resp, err := client.PullRequests.CreateCommentInReplyTo(ctx, owner, repo, pullNumber, body, int64(commentID))
701+
if err != nil {
702+
return nil, fmt.Errorf("failed to reply to pull request comment: %w", err)
703+
}
704+
defer func() { _ = resp.Body.Close() }()
705+
706+
if resp.StatusCode != http.StatusCreated {
707+
body, err := io.ReadAll(resp.Body)
708+
if err != nil {
709+
return nil, fmt.Errorf("failed to read response body: %w", err)
710+
}
711+
return mcp.NewToolResultError(fmt.Sprintf("failed to reply to pull request comment: %s", string(body))), nil
712+
}
713+
714+
r, err := json.Marshal(createdReply)
715+
if err != nil {
716+
return nil, fmt.Errorf("failed to marshal response: %w", err)
717+
}
718+
719+
return mcp.NewToolResultText(string(r)), nil
720+
}
721+
}
722+
652723
// GetPullRequestReviews creates a tool to get the reviews on a pull request.
653724
func GetPullRequestReviews(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
654725
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
@@ -1650,3 +1650,106 @@ func Test_AddPullRequestReviewComment(t *testing.T) {
16501650
})
16511651
}
16521652
}
1653+
1654+
func Test_ReplyToPullRequestReviewComment(t *testing.T) {
1655+
// Verify tool definition once
1656+
mockClient := github.NewClient(nil)
1657+
tool, _ := ReplyToPullRequestReviewComment(mockClient, translations.NullTranslationHelper)
1658+
1659+
assert.Equal(t, "reply_to_pull_request_review_comment", tool.Name)
1660+
assert.NotEmpty(t, tool.Description)
1661+
assert.Contains(t, tool.InputSchema.Properties, "owner")
1662+
assert.Contains(t, tool.InputSchema.Properties, "repo")
1663+
assert.Contains(t, tool.InputSchema.Properties, "pull_number")
1664+
assert.Contains(t, tool.InputSchema.Properties, "comment_id")
1665+
assert.Contains(t, tool.InputSchema.Properties, "body")
1666+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pull_number", "comment_id", "body"})
1667+
1668+
// Setup mock PR comment for success case
1669+
mockReply := &github.PullRequestComment{
1670+
ID: github.Ptr(int64(456)),
1671+
Body: github.Ptr("Good point, will fix!"),
1672+
}
1673+
1674+
tests := []struct {
1675+
name string
1676+
mockedClient *http.Client
1677+
requestArgs map[string]interface{}
1678+
expectError bool
1679+
expectedReply *github.PullRequestComment
1680+
expectedErrMsg string
1681+
}{
1682+
{
1683+
name: "successful reply creation",
1684+
mockedClient: mock.NewMockedHTTPClient(
1685+
mock.WithRequestMatchHandler(
1686+
mock.PostReposPullsCommentsByOwnerByRepoByPullNumber,
1687+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1688+
w.WriteHeader(http.StatusCreated)
1689+
json.NewEncoder(w).Encode(mockReply)
1690+
}),
1691+
),
1692+
),
1693+
requestArgs: map[string]interface{}{
1694+
"owner": "owner",
1695+
"repo": "repo",
1696+
"pull_number": float64(1),
1697+
"comment_id": float64(123),
1698+
"body": "Good point, will fix!",
1699+
},
1700+
expectError: false,
1701+
expectedReply: mockReply,
1702+
},
1703+
{
1704+
name: "reply creation fails",
1705+
mockedClient: mock.NewMockedHTTPClient(
1706+
mock.WithRequestMatchHandler(
1707+
mock.PostReposPullsCommentsByOwnerByRepoByPullNumber,
1708+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1709+
w.WriteHeader(http.StatusNotFound)
1710+
w.Header().Set("Content-Type", "application/json")
1711+
_, _ = w.Write([]byte(`{"message": "Comment not found"}`))
1712+
}),
1713+
),
1714+
),
1715+
requestArgs: map[string]interface{}{
1716+
"owner": "owner",
1717+
"repo": "repo",
1718+
"pull_number": float64(1),
1719+
"comment_id": float64(999),
1720+
"body": "Good point, will fix!",
1721+
},
1722+
expectError: true,
1723+
expectedErrMsg: "failed to reply to pull request comment",
1724+
},
1725+
}
1726+
1727+
for _, tc := range tests {
1728+
t.Run(tc.name, func(t *testing.T) {
1729+
mockClient := github.NewClient(tc.mockedClient)
1730+
1731+
_, handler := replyToPullRequestReviewComment(mockClient, translations.NullTranslationHelper)
1732+
1733+
request := createMCPRequest(tc.requestArgs)
1734+
1735+
result, err := handler(context.Background(), request)
1736+
1737+
if tc.name == "reply creation fails" {
1738+
require.Error(t, err)
1739+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
1740+
return
1741+
}
1742+
1743+
require.NoError(t, err)
1744+
assert.NotNil(t, result)
1745+
require.Len(t, result.Content, 1)
1746+
1747+
var returnedReply github.PullRequestComment
1748+
err = json.Unmarshal([]byte(getTextResult(t, result).Text), &returnedReply)
1749+
require.NoError(t, err)
1750+
1751+
assert.Equal(t, *tc.expectedReply.ID, *returnedReply.ID)
1752+
assert.Equal(t, *tc.expectedReply.Body, *returnedReply.Body)
1753+
})
1754+
}
1755+
}

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, version string, readOnly bool, t translati
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