Skip to content

Commit 8d26314

Browse files
Merge branch 'main' into get_check_runs
2 parents d42bfaa + a9edf9e commit 8d26314

File tree

6 files changed

+304
-0
lines changed

6 files changed

+304
-0
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,14 @@ The following sets of tools are available:
10331033
- `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)
10341034
- `subjectType`: The level at which the comment is targeted (string, required)
10351035

1036+
- **add_reply_to_pull_request_comment** - Add reply to pull request comment
1037+
- **Required OAuth Scopes**: `repo`
1038+
- `body`: The text of the reply (string, required)
1039+
- `commentId`: The ID of the comment to reply to (number, required)
1040+
- `owner`: Repository owner (string, required)
1041+
- `pullNumber`: Pull request number (number, required)
1042+
- `repo`: Repository name (string, required)
1043+
10361044
- **create_pull_request** - Open new pull request
10371045
- **Required OAuth Scopes**: `repo`
10381046
- `base`: Branch to merge into (string, required)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"annotations": {
3+
"title": "Add reply to pull request comment"
4+
},
5+
"description": "Add a reply to an existing pull request comment. This creates a new comment that is linked as a reply to the specified comment.",
6+
"inputSchema": {
7+
"properties": {
8+
"body": {
9+
"description": "The text of the reply",
10+
"type": "string"
11+
},
12+
"commentId": {
13+
"description": "The ID of the comment to reply to",
14+
"type": "number"
15+
},
16+
"owner": {
17+
"description": "Repository owner",
18+
"type": "string"
19+
},
20+
"pullNumber": {
21+
"description": "Pull request number",
22+
"type": "number"
23+
},
24+
"repo": {
25+
"description": "Repository name",
26+
"type": "string"
27+
}
28+
},
29+
"required": [
30+
"owner",
31+
"repo",
32+
"pullNumber",
33+
"commentId",
34+
"body"
35+
],
36+
"type": "object"
37+
},
38+
"name": "add_reply_to_pull_request_comment"
39+
}

pkg/github/helper_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const (
7373
PutReposPullsMergeByOwnerByRepoByPullNumber = "PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge"
7474
PutReposPullsUpdateBranchByOwnerByRepoByPullNumber = "PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch"
7575
PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber = "POST /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers"
76+
PostReposPullsCommentsByOwnerByRepoByPullNumber = "POST /repos/{owner}/{repo}/pulls/{pull_number}/comments"
7677

7778
// Notifications endpoints
7879
GetNotifications = "GET /notifications"

pkg/github/pullrequests.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,97 @@ func UpdatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
971971
})
972972
}
973973

974+
// AddReplyToPullRequestComment creates a tool to add a reply to an existing pull request comment.
975+
func AddReplyToPullRequestComment(t translations.TranslationHelperFunc) inventory.ServerTool {
976+
schema := &jsonschema.Schema{
977+
Type: "object",
978+
Properties: map[string]*jsonschema.Schema{
979+
"owner": {
980+
Type: "string",
981+
Description: "Repository owner",
982+
},
983+
"repo": {
984+
Type: "string",
985+
Description: "Repository name",
986+
},
987+
"pullNumber": {
988+
Type: "number",
989+
Description: "Pull request number",
990+
},
991+
"commentId": {
992+
Type: "number",
993+
Description: "The ID of the comment to reply to",
994+
},
995+
"body": {
996+
Type: "string",
997+
Description: "The text of the reply",
998+
},
999+
},
1000+
Required: []string{"owner", "repo", "pullNumber", "commentId", "body"},
1001+
}
1002+
1003+
return NewTool(
1004+
ToolsetMetadataPullRequests,
1005+
mcp.Tool{
1006+
Name: "add_reply_to_pull_request_comment",
1007+
Description: t("TOOL_ADD_REPLY_TO_PULL_REQUEST_COMMENT_DESCRIPTION", "Add a reply to an existing pull request comment. This creates a new comment that is linked as a reply to the specified comment."),
1008+
Annotations: &mcp.ToolAnnotations{
1009+
Title: t("TOOL_ADD_REPLY_TO_PULL_REQUEST_COMMENT_USER_TITLE", "Add reply to pull request comment"),
1010+
ReadOnlyHint: false,
1011+
},
1012+
InputSchema: schema,
1013+
},
1014+
[]scopes.Scope{scopes.Repo},
1015+
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
1016+
owner, err := RequiredParam[string](args, "owner")
1017+
if err != nil {
1018+
return utils.NewToolResultError(err.Error()), nil, nil
1019+
}
1020+
repo, err := RequiredParam[string](args, "repo")
1021+
if err != nil {
1022+
return utils.NewToolResultError(err.Error()), nil, nil
1023+
}
1024+
pullNumber, err := RequiredInt(args, "pullNumber")
1025+
if err != nil {
1026+
return utils.NewToolResultError(err.Error()), nil, nil
1027+
}
1028+
commentID, err := RequiredInt(args, "commentId")
1029+
if err != nil {
1030+
return utils.NewToolResultError(err.Error()), nil, nil
1031+
}
1032+
body, err := RequiredParam[string](args, "body")
1033+
if err != nil {
1034+
return utils.NewToolResultError(err.Error()), nil, nil
1035+
}
1036+
1037+
client, err := deps.GetClient(ctx)
1038+
if err != nil {
1039+
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
1040+
}
1041+
1042+
comment, resp, err := client.PullRequests.CreateCommentInReplyTo(ctx, owner, repo, pullNumber, body, int64(commentID))
1043+
if err != nil {
1044+
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to add reply to pull request comment", resp, err), nil, nil
1045+
}
1046+
defer func() { _ = resp.Body.Close() }()
1047+
1048+
if resp.StatusCode != http.StatusCreated {
1049+
bodyBytes, err := io.ReadAll(resp.Body)
1050+
if err != nil {
1051+
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
1052+
}
1053+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to add reply to pull request comment", resp, bodyBytes), nil, nil
1054+
}
1055+
1056+
r, err := json.Marshal(comment)
1057+
if err != nil {
1058+
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
1059+
}
1060+
1061+
return utils.NewToolResultText(string(r)), nil, nil
1062+
})
1063+
}
1064+
9741065
// ListPullRequests creates a tool to list and filter repository pull requests.
9751066
func ListPullRequests(t translations.TranslationHelperFunc) inventory.ServerTool {
9761067
schema := &jsonschema.Schema{

pkg/github/pullrequests_test.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3382,3 +3382,167 @@ func getLatestPendingReviewQuery(p getLatestPendingReviewQueryParams) githubv4mo
33823382
),
33833383
)
33843384
}
3385+
3386+
func TestAddReplyToPullRequestComment(t *testing.T) {
3387+
t.Parallel()
3388+
3389+
// Verify tool definition once
3390+
serverTool := AddReplyToPullRequestComment(translations.NullTranslationHelper)
3391+
tool := serverTool.Tool
3392+
require.NoError(t, toolsnaps.Test(tool.Name, tool))
3393+
3394+
assert.Equal(t, "add_reply_to_pull_request_comment", tool.Name)
3395+
assert.NotEmpty(t, tool.Description)
3396+
schema := tool.InputSchema.(*jsonschema.Schema)
3397+
assert.Contains(t, schema.Properties, "owner")
3398+
assert.Contains(t, schema.Properties, "repo")
3399+
assert.Contains(t, schema.Properties, "pullNumber")
3400+
assert.Contains(t, schema.Properties, "commentId")
3401+
assert.Contains(t, schema.Properties, "body")
3402+
assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "pullNumber", "commentId", "body"})
3403+
3404+
// Setup mock reply comment for success case
3405+
mockReplyComment := &github.PullRequestComment{
3406+
ID: github.Ptr(int64(456)),
3407+
Body: github.Ptr("This is a reply to the comment"),
3408+
InReplyTo: github.Ptr(int64(123)),
3409+
HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42#discussion_r456"),
3410+
User: &github.User{
3411+
Login: github.Ptr("responder"),
3412+
},
3413+
CreatedAt: &github.Timestamp{Time: time.Now()},
3414+
UpdatedAt: &github.Timestamp{Time: time.Now()},
3415+
}
3416+
3417+
tests := []struct {
3418+
name string
3419+
mockedClient *http.Client
3420+
requestArgs map[string]interface{}
3421+
expectToolError bool
3422+
expectedToolErrMsg string
3423+
}{
3424+
{
3425+
name: "successful reply to pull request comment",
3426+
requestArgs: map[string]interface{}{
3427+
"owner": "owner",
3428+
"repo": "repo",
3429+
"pullNumber": float64(42),
3430+
"commentId": float64(123),
3431+
"body": "This is a reply to the comment",
3432+
},
3433+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
3434+
PostReposPullsCommentsByOwnerByRepoByPullNumber: func(w http.ResponseWriter, _ *http.Request) {
3435+
w.WriteHeader(http.StatusCreated)
3436+
responseData, _ := json.Marshal(mockReplyComment)
3437+
_, _ = w.Write(responseData)
3438+
},
3439+
}),
3440+
},
3441+
{
3442+
name: "missing required parameter owner",
3443+
requestArgs: map[string]interface{}{
3444+
"repo": "repo",
3445+
"pullNumber": float64(42),
3446+
"commentId": float64(123),
3447+
"body": "This is a reply to the comment",
3448+
},
3449+
expectToolError: true,
3450+
expectedToolErrMsg: "missing required parameter: owner",
3451+
},
3452+
{
3453+
name: "missing required parameter repo",
3454+
requestArgs: map[string]interface{}{
3455+
"owner": "owner",
3456+
"pullNumber": float64(42),
3457+
"commentId": float64(123),
3458+
"body": "This is a reply to the comment",
3459+
},
3460+
expectToolError: true,
3461+
expectedToolErrMsg: "missing required parameter: repo",
3462+
},
3463+
{
3464+
name: "missing required parameter pullNumber",
3465+
requestArgs: map[string]interface{}{
3466+
"owner": "owner",
3467+
"repo": "repo",
3468+
"commentId": float64(123),
3469+
"body": "This is a reply to the comment",
3470+
},
3471+
expectToolError: true,
3472+
expectedToolErrMsg: "missing required parameter: pullNumber",
3473+
},
3474+
{
3475+
name: "missing required parameter commentId",
3476+
requestArgs: map[string]interface{}{
3477+
"owner": "owner",
3478+
"repo": "repo",
3479+
"pullNumber": float64(42),
3480+
"body": "This is a reply to the comment",
3481+
},
3482+
expectToolError: true,
3483+
expectedToolErrMsg: "missing required parameter: commentId",
3484+
},
3485+
{
3486+
name: "missing required parameter body",
3487+
requestArgs: map[string]interface{}{
3488+
"owner": "owner",
3489+
"repo": "repo",
3490+
"pullNumber": float64(42),
3491+
"commentId": float64(123),
3492+
},
3493+
expectToolError: true,
3494+
expectedToolErrMsg: "missing required parameter: body",
3495+
},
3496+
{
3497+
name: "API error when adding reply",
3498+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
3499+
PostReposPullsCommentsByOwnerByRepoByPullNumber: func(w http.ResponseWriter, _ *http.Request) {
3500+
w.WriteHeader(http.StatusNotFound)
3501+
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
3502+
},
3503+
}),
3504+
requestArgs: map[string]interface{}{
3505+
"owner": "owner",
3506+
"repo": "repo",
3507+
"pullNumber": float64(42),
3508+
"commentId": float64(123),
3509+
"body": "This is a reply to the comment",
3510+
},
3511+
expectToolError: true,
3512+
expectedToolErrMsg: "failed to add reply to pull request comment",
3513+
},
3514+
}
3515+
3516+
for _, tc := range tests {
3517+
t.Run(tc.name, func(t *testing.T) {
3518+
t.Parallel()
3519+
3520+
// Setup client with mock
3521+
client := github.NewClient(tc.mockedClient)
3522+
serverTool := AddReplyToPullRequestComment(translations.NullTranslationHelper)
3523+
deps := BaseDeps{
3524+
Client: client,
3525+
}
3526+
handler := serverTool.Handler(deps)
3527+
3528+
// Create call request
3529+
request := createMCPRequest(tc.requestArgs)
3530+
3531+
// Call handler
3532+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
3533+
require.NoError(t, err)
3534+
3535+
if tc.expectToolError {
3536+
require.True(t, result.IsError)
3537+
errorContent := getErrorResult(t, result)
3538+
assert.Contains(t, errorContent.Text, tc.expectedToolErrMsg)
3539+
return
3540+
}
3541+
3542+
// Parse the result and verify it's not an error
3543+
require.False(t, result.IsError)
3544+
textContent := getTextResult(t, result)
3545+
assert.Contains(t, textContent.Text, "This is a reply to the comment")
3546+
})
3547+
}
3548+
}

pkg/github/tools.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ func AllTools(t translations.TranslationHelperFunc) []inventory.ServerTool {
213213
RequestCopilotReview(t),
214214
PullRequestReviewWrite(t),
215215
AddCommentToPendingReview(t),
216+
AddReplyToPullRequestComment(t),
216217

217218
// Code security tools
218219
GetCodeScanningAlert(t),

0 commit comments

Comments
 (0)