Skip to content

Commit 4c1ad6b

Browse files
authored
Merge branch 'main' into http-stack-2
2 parents cfb7820 + a9edf9e commit 4c1ad6b

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
@@ -72,6 +72,7 @@ const (
7272
PutReposPullsMergeByOwnerByRepoByPullNumber = "PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge"
7373
PutReposPullsUpdateBranchByOwnerByRepoByPullNumber = "PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch"
7474
PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber = "POST /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers"
75+
PostReposPullsCommentsByOwnerByRepoByPullNumber = "POST /repos/{owner}/{repo}/pulls/{pull_number}/comments"
7576

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

pkg/github/pullrequests.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,97 @@ func UpdatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
919919
})
920920
}
921921

922+
// AddReplyToPullRequestComment creates a tool to add a reply to an existing pull request comment.
923+
func AddReplyToPullRequestComment(t translations.TranslationHelperFunc) inventory.ServerTool {
924+
schema := &jsonschema.Schema{
925+
Type: "object",
926+
Properties: map[string]*jsonschema.Schema{
927+
"owner": {
928+
Type: "string",
929+
Description: "Repository owner",
930+
},
931+
"repo": {
932+
Type: "string",
933+
Description: "Repository name",
934+
},
935+
"pullNumber": {
936+
Type: "number",
937+
Description: "Pull request number",
938+
},
939+
"commentId": {
940+
Type: "number",
941+
Description: "The ID of the comment to reply to",
942+
},
943+
"body": {
944+
Type: "string",
945+
Description: "The text of the reply",
946+
},
947+
},
948+
Required: []string{"owner", "repo", "pullNumber", "commentId", "body"},
949+
}
950+
951+
return NewTool(
952+
ToolsetMetadataPullRequests,
953+
mcp.Tool{
954+
Name: "add_reply_to_pull_request_comment",
955+
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."),
956+
Annotations: &mcp.ToolAnnotations{
957+
Title: t("TOOL_ADD_REPLY_TO_PULL_REQUEST_COMMENT_USER_TITLE", "Add reply to pull request comment"),
958+
ReadOnlyHint: false,
959+
},
960+
InputSchema: schema,
961+
},
962+
[]scopes.Scope{scopes.Repo},
963+
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
964+
owner, err := RequiredParam[string](args, "owner")
965+
if err != nil {
966+
return utils.NewToolResultError(err.Error()), nil, nil
967+
}
968+
repo, err := RequiredParam[string](args, "repo")
969+
if err != nil {
970+
return utils.NewToolResultError(err.Error()), nil, nil
971+
}
972+
pullNumber, err := RequiredInt(args, "pullNumber")
973+
if err != nil {
974+
return utils.NewToolResultError(err.Error()), nil, nil
975+
}
976+
commentID, err := RequiredInt(args, "commentId")
977+
if err != nil {
978+
return utils.NewToolResultError(err.Error()), nil, nil
979+
}
980+
body, err := RequiredParam[string](args, "body")
981+
if err != nil {
982+
return utils.NewToolResultError(err.Error()), nil, nil
983+
}
984+
985+
client, err := deps.GetClient(ctx)
986+
if err != nil {
987+
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
988+
}
989+
990+
comment, resp, err := client.PullRequests.CreateCommentInReplyTo(ctx, owner, repo, pullNumber, body, int64(commentID))
991+
if err != nil {
992+
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to add reply to pull request comment", resp, err), nil, nil
993+
}
994+
defer func() { _ = resp.Body.Close() }()
995+
996+
if resp.StatusCode != http.StatusCreated {
997+
bodyBytes, err := io.ReadAll(resp.Body)
998+
if err != nil {
999+
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
1000+
}
1001+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to add reply to pull request comment", resp, bodyBytes), nil, nil
1002+
}
1003+
1004+
r, err := json.Marshal(comment)
1005+
if err != nil {
1006+
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
1007+
}
1008+
1009+
return utils.NewToolResultText(string(r)), nil, nil
1010+
})
1011+
}
1012+
9221013
// ListPullRequests creates a tool to list and filter repository pull requests.
9231014
func ListPullRequests(t translations.TranslationHelperFunc) inventory.ServerTool {
9241015
schema := &jsonschema.Schema{

pkg/github/pullrequests_test.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3227,3 +3227,167 @@ func getLatestPendingReviewQuery(p getLatestPendingReviewQueryParams) githubv4mo
32273227
),
32283228
)
32293229
}
3230+
3231+
func TestAddReplyToPullRequestComment(t *testing.T) {
3232+
t.Parallel()
3233+
3234+
// Verify tool definition once
3235+
serverTool := AddReplyToPullRequestComment(translations.NullTranslationHelper)
3236+
tool := serverTool.Tool
3237+
require.NoError(t, toolsnaps.Test(tool.Name, tool))
3238+
3239+
assert.Equal(t, "add_reply_to_pull_request_comment", tool.Name)
3240+
assert.NotEmpty(t, tool.Description)
3241+
schema := tool.InputSchema.(*jsonschema.Schema)
3242+
assert.Contains(t, schema.Properties, "owner")
3243+
assert.Contains(t, schema.Properties, "repo")
3244+
assert.Contains(t, schema.Properties, "pullNumber")
3245+
assert.Contains(t, schema.Properties, "commentId")
3246+
assert.Contains(t, schema.Properties, "body")
3247+
assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "pullNumber", "commentId", "body"})
3248+
3249+
// Setup mock reply comment for success case
3250+
mockReplyComment := &github.PullRequestComment{
3251+
ID: github.Ptr(int64(456)),
3252+
Body: github.Ptr("This is a reply to the comment"),
3253+
InReplyTo: github.Ptr(int64(123)),
3254+
HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42#discussion_r456"),
3255+
User: &github.User{
3256+
Login: github.Ptr("responder"),
3257+
},
3258+
CreatedAt: &github.Timestamp{Time: time.Now()},
3259+
UpdatedAt: &github.Timestamp{Time: time.Now()},
3260+
}
3261+
3262+
tests := []struct {
3263+
name string
3264+
mockedClient *http.Client
3265+
requestArgs map[string]interface{}
3266+
expectToolError bool
3267+
expectedToolErrMsg string
3268+
}{
3269+
{
3270+
name: "successful reply to pull request comment",
3271+
requestArgs: map[string]interface{}{
3272+
"owner": "owner",
3273+
"repo": "repo",
3274+
"pullNumber": float64(42),
3275+
"commentId": float64(123),
3276+
"body": "This is a reply to the comment",
3277+
},
3278+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
3279+
PostReposPullsCommentsByOwnerByRepoByPullNumber: func(w http.ResponseWriter, _ *http.Request) {
3280+
w.WriteHeader(http.StatusCreated)
3281+
responseData, _ := json.Marshal(mockReplyComment)
3282+
_, _ = w.Write(responseData)
3283+
},
3284+
}),
3285+
},
3286+
{
3287+
name: "missing required parameter owner",
3288+
requestArgs: map[string]interface{}{
3289+
"repo": "repo",
3290+
"pullNumber": float64(42),
3291+
"commentId": float64(123),
3292+
"body": "This is a reply to the comment",
3293+
},
3294+
expectToolError: true,
3295+
expectedToolErrMsg: "missing required parameter: owner",
3296+
},
3297+
{
3298+
name: "missing required parameter repo",
3299+
requestArgs: map[string]interface{}{
3300+
"owner": "owner",
3301+
"pullNumber": float64(42),
3302+
"commentId": float64(123),
3303+
"body": "This is a reply to the comment",
3304+
},
3305+
expectToolError: true,
3306+
expectedToolErrMsg: "missing required parameter: repo",
3307+
},
3308+
{
3309+
name: "missing required parameter pullNumber",
3310+
requestArgs: map[string]interface{}{
3311+
"owner": "owner",
3312+
"repo": "repo",
3313+
"commentId": float64(123),
3314+
"body": "This is a reply to the comment",
3315+
},
3316+
expectToolError: true,
3317+
expectedToolErrMsg: "missing required parameter: pullNumber",
3318+
},
3319+
{
3320+
name: "missing required parameter commentId",
3321+
requestArgs: map[string]interface{}{
3322+
"owner": "owner",
3323+
"repo": "repo",
3324+
"pullNumber": float64(42),
3325+
"body": "This is a reply to the comment",
3326+
},
3327+
expectToolError: true,
3328+
expectedToolErrMsg: "missing required parameter: commentId",
3329+
},
3330+
{
3331+
name: "missing required parameter body",
3332+
requestArgs: map[string]interface{}{
3333+
"owner": "owner",
3334+
"repo": "repo",
3335+
"pullNumber": float64(42),
3336+
"commentId": float64(123),
3337+
},
3338+
expectToolError: true,
3339+
expectedToolErrMsg: "missing required parameter: body",
3340+
},
3341+
{
3342+
name: "API error when adding reply",
3343+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
3344+
PostReposPullsCommentsByOwnerByRepoByPullNumber: func(w http.ResponseWriter, _ *http.Request) {
3345+
w.WriteHeader(http.StatusNotFound)
3346+
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
3347+
},
3348+
}),
3349+
requestArgs: map[string]interface{}{
3350+
"owner": "owner",
3351+
"repo": "repo",
3352+
"pullNumber": float64(42),
3353+
"commentId": float64(123),
3354+
"body": "This is a reply to the comment",
3355+
},
3356+
expectToolError: true,
3357+
expectedToolErrMsg: "failed to add reply to pull request comment",
3358+
},
3359+
}
3360+
3361+
for _, tc := range tests {
3362+
t.Run(tc.name, func(t *testing.T) {
3363+
t.Parallel()
3364+
3365+
// Setup client with mock
3366+
client := github.NewClient(tc.mockedClient)
3367+
serverTool := AddReplyToPullRequestComment(translations.NullTranslationHelper)
3368+
deps := BaseDeps{
3369+
Client: client,
3370+
}
3371+
handler := serverTool.Handler(deps)
3372+
3373+
// Create call request
3374+
request := createMCPRequest(tc.requestArgs)
3375+
3376+
// Call handler
3377+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
3378+
require.NoError(t, err)
3379+
3380+
if tc.expectToolError {
3381+
require.True(t, result.IsError)
3382+
errorContent := getErrorResult(t, result)
3383+
assert.Contains(t, errorContent.Text, tc.expectedToolErrMsg)
3384+
return
3385+
}
3386+
3387+
// Parse the result and verify it's not an error
3388+
require.False(t, result.IsError)
3389+
textContent := getTextResult(t, result)
3390+
assert.Contains(t, textContent.Text, "This is a reply to the comment")
3391+
})
3392+
}
3393+
}

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)