Skip to content

Commit e94a9f3

Browse files
ashwin-antclaude
andcommitted
Add add_pull_request_review_comment tool for PR review comments
Adds the ability to add review comments to pull requests with support for line, multi-line, and file-level comments, as well as replying to existing comments. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 270bbf7 commit e94a9f3

File tree

3 files changed

+264
-0
lines changed

3 files changed

+264
-0
lines changed

pkg/github/pullrequests.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,156 @@ func getPullRequestComments(client *github.Client, t translations.TranslationHel
508508
}
509509
}
510510

511+
// addPullRequestReviewComment creates a tool to add a review comment to a pull request.
512+
func addPullRequestReviewComment(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
513+
return mcp.NewTool("add_pull_request_review_comment",
514+
mcp.WithDescription(t("TOOL_ADD_PULL_REQUEST_COMMENT_DESCRIPTION", "Add a review comment to a pull request")),
515+
mcp.WithString("owner",
516+
mcp.Required(),
517+
mcp.Description("Repository owner"),
518+
),
519+
mcp.WithString("repo",
520+
mcp.Required(),
521+
mcp.Description("Repository name"),
522+
),
523+
mcp.WithNumber("pull_number",
524+
mcp.Required(),
525+
mcp.Description("Pull request number"),
526+
),
527+
mcp.WithString("body",
528+
mcp.Required(),
529+
mcp.Description("The text of the review comment"),
530+
),
531+
mcp.WithString("commit_id",
532+
mcp.Required(),
533+
mcp.Description("The SHA of the commit to comment on"),
534+
),
535+
mcp.WithString("path",
536+
mcp.Required(),
537+
mcp.Description("The relative path to the file that necessitates a comment"),
538+
),
539+
mcp.WithString("subject_type",
540+
mcp.Description("The level at which the comment is targeted, 'line' or 'file'"),
541+
mcp.Enum("line", "file"),
542+
),
543+
mcp.WithNumber("line",
544+
mcp.Description("The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range"),
545+
),
546+
mcp.WithString("side",
547+
mcp.Description("The side of the diff to comment on. Can be LEFT or RIGHT"),
548+
mcp.Enum("LEFT", "RIGHT"),
549+
),
550+
mcp.WithNumber("start_line",
551+
mcp.Description("For multi-line comments, the first line of the range that the comment applies to"),
552+
),
553+
mcp.WithString("start_side",
554+
mcp.Description("For multi-line comments, the starting side of the diff that the comment applies to. Can be LEFT or RIGHT"),
555+
mcp.Enum("LEFT", "RIGHT"),
556+
),
557+
mcp.WithNumber("in_reply_to",
558+
mcp.Description("The ID of the review comment to reply to. When specified, all parameters other than body are ignored"),
559+
),
560+
),
561+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
562+
owner, err := requiredParam[string](request, "owner")
563+
if err != nil {
564+
return mcp.NewToolResultError(err.Error()), nil
565+
}
566+
repo, err := requiredParam[string](request, "repo")
567+
if err != nil {
568+
return mcp.NewToolResultError(err.Error()), nil
569+
}
570+
pullNumber, err := requiredInt(request, "pull_number")
571+
if err != nil {
572+
return mcp.NewToolResultError(err.Error()), nil
573+
}
574+
body, err := requiredParam[string](request, "body")
575+
if err != nil {
576+
return mcp.NewToolResultError(err.Error()), nil
577+
}
578+
commitID, err := requiredParam[string](request, "commit_id")
579+
if err != nil {
580+
return mcp.NewToolResultError(err.Error()), nil
581+
}
582+
path, err := requiredParam[string](request, "path")
583+
if err != nil {
584+
return mcp.NewToolResultError(err.Error()), nil
585+
}
586+
587+
comment := &github.PullRequestComment{
588+
Body: github.Ptr(body),
589+
CommitID: github.Ptr(commitID),
590+
Path: github.Ptr(path),
591+
}
592+
593+
// Check for in_reply_to parameter which takes precedence
594+
if replyToFloat, ok := request.Params.Arguments["in_reply_to"].(float64); ok {
595+
comment.InReplyTo = github.Ptr(int64(replyToFloat))
596+
} else {
597+
// Handle subject_type parameter
598+
subjectType, err := optionalParam[string](request, "subject_type")
599+
if err != nil {
600+
return mcp.NewToolResultError(err.Error()), nil
601+
}
602+
if subjectType == "file" {
603+
// When commenting on a file, no line/position fields are needed
604+
} else {
605+
// Handle line or position-based comments
606+
line, lineExists := request.Params.Arguments["line"].(float64)
607+
startLine, startLineExists := request.Params.Arguments["start_line"].(float64)
608+
side, sideExists := request.Params.Arguments["side"].(string)
609+
startSide, startSideExists := request.Params.Arguments["start_side"].(string)
610+
611+
if subjectType != "file" && !lineExists {
612+
return mcp.NewToolResultError("line parameter is required unless using subject_type:file or in_reply_to"), nil
613+
}
614+
615+
if lineExists {
616+
comment.Line = github.Ptr(int(line))
617+
}
618+
if sideExists {
619+
comment.Side = github.Ptr(side)
620+
}
621+
if startLineExists {
622+
comment.StartLine = github.Ptr(int(startLine))
623+
}
624+
if startSideExists {
625+
comment.StartSide = github.Ptr(startSide)
626+
}
627+
628+
// Validate multi-line comment parameters
629+
if startLineExists && !lineExists {
630+
return mcp.NewToolResultError("if start_line is provided, line must also be provided"), nil
631+
}
632+
if startSideExists && !sideExists {
633+
return mcp.NewToolResultError("if start_side is provided, side must also be provided"), nil
634+
}
635+
}
636+
}
637+
638+
createdComment, resp, err := client.PullRequests.CreateComment(ctx, owner, repo, pullNumber, comment)
639+
if err != nil {
640+
return nil, fmt.Errorf("failed to create pull request comment: %w", err)
641+
}
642+
defer func() { _ = resp.Body.Close() }()
643+
644+
if resp.StatusCode != http.StatusCreated {
645+
body, err := io.ReadAll(resp.Body)
646+
if err != nil {
647+
return nil, fmt.Errorf("failed to read response body: %w", err)
648+
}
649+
return mcp.NewToolResultError(fmt.Sprintf("failed to create pull request comment: %s", string(body))), nil
650+
}
651+
652+
r, err := json.Marshal(createdComment)
653+
if err != nil {
654+
return nil, fmt.Errorf("failed to marshal response: %w", err)
655+
}
656+
657+
return mcp.NewToolResultText(string(r)), nil
658+
}
659+
}
660+
511661
// getPullRequestReviews creates a tool to get the reviews on a pull request.
512662
func getPullRequestReviews(client *github.Client, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
513663
return mcp.NewTool("get_pull_request_reviews",

pkg/github/pullrequests_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,3 +1392,116 @@ func Test_CreatePullRequest(t *testing.T) {
13921392
})
13931393
}
13941394
}
1395+
1396+
func Test_AddPullRequestReviewComment(t *testing.T) {
1397+
mockClient := github.NewClient(nil)
1398+
tool, _ := addPullRequestReviewComment(mockClient, translations.NullTranslationHelper)
1399+
1400+
assert.Equal(t, "add_pull_request_review_comment", tool.Name)
1401+
assert.NotEmpty(t, tool.Description)
1402+
assert.Contains(t, tool.InputSchema.Properties, "owner")
1403+
assert.Contains(t, tool.InputSchema.Properties, "repo")
1404+
assert.Contains(t, tool.InputSchema.Properties, "pull_number")
1405+
assert.Contains(t, tool.InputSchema.Properties, "body")
1406+
assert.Contains(t, tool.InputSchema.Properties, "commit_id")
1407+
assert.Contains(t, tool.InputSchema.Properties, "path")
1408+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "pull_number", "body", "commit_id", "path"})
1409+
1410+
mockComment := &github.PullRequestComment{
1411+
ID: github.Ptr(int64(123)),
1412+
Body: github.Ptr("Great stuff!"),
1413+
Path: github.Ptr("file1.txt"),
1414+
Line: github.Ptr(2),
1415+
Side: github.Ptr("RIGHT"),
1416+
}
1417+
1418+
tests := []struct {
1419+
name string
1420+
mockedClient *http.Client
1421+
requestArgs map[string]interface{}
1422+
expectError bool
1423+
expectedComment *github.PullRequestComment
1424+
expectedErrMsg string
1425+
}{
1426+
{
1427+
name: "successful line comment creation",
1428+
mockedClient: mock.NewMockedHTTPClient(
1429+
mock.WithRequestMatchHandler(
1430+
mock.PostReposPullsCommentsByOwnerByRepoByPullNumber,
1431+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1432+
w.WriteHeader(http.StatusCreated)
1433+
json.NewEncoder(w).Encode(mockComment)
1434+
}),
1435+
),
1436+
),
1437+
requestArgs: map[string]interface{}{
1438+
"owner": "owner",
1439+
"repo": "repo",
1440+
"pull_number": float64(1),
1441+
"body": "Great stuff!",
1442+
"commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
1443+
"path": "file1.txt",
1444+
"line": float64(2),
1445+
"side": "RIGHT",
1446+
},
1447+
expectError: false,
1448+
expectedComment: mockComment,
1449+
},
1450+
{
1451+
name: "comment creation fails",
1452+
mockedClient: mock.NewMockedHTTPClient(
1453+
mock.WithRequestMatchHandler(
1454+
mock.PostReposPullsCommentsByOwnerByRepoByPullNumber,
1455+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1456+
w.WriteHeader(http.StatusUnprocessableEntity)
1457+
w.Header().Set("Content-Type", "application/json")
1458+
_, _ = w.Write([]byte(`{"message": "Validation Failed"}`))
1459+
}),
1460+
),
1461+
),
1462+
requestArgs: map[string]interface{}{
1463+
"owner": "owner",
1464+
"repo": "repo",
1465+
"pull_number": float64(1),
1466+
"body": "Great stuff!",
1467+
"commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
1468+
"path": "file1.txt",
1469+
"line": float64(2),
1470+
},
1471+
expectError: false,
1472+
expectedErrMsg: "failed to create pull request comment",
1473+
},
1474+
}
1475+
1476+
for _, tc := range tests {
1477+
t.Run(tc.name, func(t *testing.T) {
1478+
mockClient := github.NewClient(tc.mockedClient)
1479+
1480+
_, handler := addPullRequestReviewComment(mockClient, translations.NullTranslationHelper)
1481+
1482+
request := createMCPRequest(tc.requestArgs)
1483+
1484+
result, err := handler(context.Background(), request)
1485+
1486+
if tc.name == "comment creation fails" {
1487+
require.Error(t, err)
1488+
assert.Contains(t, err.Error(), tc.expectedErrMsg)
1489+
return
1490+
}
1491+
1492+
require.NoError(t, err)
1493+
assert.NotNil(t, result)
1494+
require.Len(t, result.Content, 1)
1495+
1496+
var returnedComment github.PullRequestComment
1497+
err = json.Unmarshal([]byte(getTextResult(t, result).Text), &returnedComment)
1498+
require.NoError(t, err)
1499+
1500+
assert.Equal(t, *tc.expectedComment.ID, *returnedComment.ID)
1501+
assert.Equal(t, *tc.expectedComment.Body, *returnedComment.Body)
1502+
assert.Equal(t, *tc.expectedComment.Path, *returnedComment.Path)
1503+
assert.Equal(t, *tc.expectedComment.Line, *returnedComment.Line)
1504+
assert.Equal(t, *tc.expectedComment.Side, *returnedComment.Side)
1505+
})
1506+
}
1507+
}

pkg/github/server.go

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

5859
// 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