Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,7 @@ The following sets of tools are available:
- `owner`: Repository owner (string, required)
- `pullNumber`: Pull request number (number, required)
- `repo`: Repository name (string, required)
- `resolutionReason`: Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid. (string, optional)
- `threadId`: The node ID of the review thread (e.g., PRRT_kwDOxxx). Required for resolve_thread and unresolve_thread methods. Get thread IDs from pull_request_read with method get_review_comments. (string, optional)

- **search_pull_requests** - Search pull requests
Expand Down
1 change: 1 addition & 0 deletions docs/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ runtime behavior (such as output formatting) won't appear here.

- **resolve_review_thread** - Resolve Review Thread
- **Required OAuth Scopes**: `repo`
- `resolutionReason`: Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid. (string, optional)
- `threadID`: The node ID of the review thread to resolve (e.g., PRRT_kwDOxxx) (string, required)

- **submit_pending_pull_request_review** - Submit Pending Pull Request Review
Expand Down
4 changes: 4 additions & 0 deletions pkg/github/__toolsnaps__/pull_request_review_write.snap
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
"description": "Repository name",
"type": "string"
},
"resolutionReason": {
"description": "Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid.",
"type": "string"
},
"threadId": {
"description": "The node ID of the review thread (e.g., PRRT_kwDOxxx). Required for resolve_thread and unresolve_thread methods. Get thread IDs from pull_request_read with method get_review_comments.",
"type": "string"
Expand Down
4 changes: 4 additions & 0 deletions pkg/github/__toolsnaps__/resolve_review_thread.snap
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
"description": "Resolve a review thread on a pull request. Resolving an already-resolved thread is a no-op.",
"inputSchema": {
"properties": {
"resolutionReason": {
"description": "Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid.",
"type": "string"
},
"threadID": {
"description": "The node ID of the review thread to resolve (e.g., PRRT_kwDOxxx)",
"type": "string"
Expand Down
77 changes: 46 additions & 31 deletions pkg/github/granular_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1728,38 +1728,53 @@ func TestGranularAddPullRequestReviewComment(t *testing.T) {
}

func TestGranularResolveReviewThread(t *testing.T) {
mockedClient := githubv4mock.NewMockedHTTPClient(
githubv4mock.NewMutationMatcher(
struct {
ResolveReviewThread struct {
Thread struct {
ID githubv4.ID
IsResolved githubv4.Boolean
}
} `graphql:"resolveReviewThread(input: $input)"`
}{},
githubv4.ResolveReviewThreadInput{
ThreadID: githubv4.ID("PRRT_123"),
},
nil,
githubv4mock.DataResponse(map[string]any{
"resolveReviewThread": map[string]any{
"thread": map[string]any{"id": "PRRT_123", "isResolved": true},
},
}),
),
)
gqlClient := githubv4.NewClient(mockedClient)
deps := BaseDeps{GQLClient: gqlClient}
serverTool := GranularResolveReviewThread(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)
tests := []struct {
name string
resolutionReason *string
}{
{name: "with resolution reason", resolutionReason: gogithub.Ptr("addressed")},
{name: "without resolution reason"},
}

request := createMCPRequest(map[string]any{
"threadID": "PRRT_123",
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mockedClient := githubv4mock.NewMockedHTTPClient(
githubv4mock.NewMutationMatcher(
struct {
ResolveReviewThread struct {
Thread struct {
ID githubv4.ID
IsResolved githubv4.Boolean
}
} `graphql:"resolveReviewThread(input: $input)"`
}{},
resolveReviewThreadInput{
ThreadID: githubv4.ID("PRRT_123"),
ResolutionReason: newGQLStringlikePtr[githubv4.String](tc.resolutionReason),
},
nil,
githubv4mock.DataResponse(map[string]any{
"resolveReviewThread": map[string]any{
"thread": map[string]any{"id": "PRRT_123", "isResolved": true},
},
}),
),
)
gqlClient := githubv4.NewClient(mockedClient)
deps := BaseDeps{GQLClient: gqlClient}
serverTool := GranularResolveReviewThread(translations.NullTranslationHelper)
handler := serverTool.Handler(deps)

args := map[string]any{"threadID": "PRRT_123"}
if tc.resolutionReason != nil {
args["resolutionReason"] = *tc.resolutionReason
}
request := createMCPRequest(args)
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
assert.False(t, result.IsError)
})
}
}

func TestGranularUnresolveReviewThread(t *testing.T) {
Expand Down
38 changes: 27 additions & 11 deletions pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -1760,14 +1760,15 @@ func UpdatePullRequestBranch(t translations.TranslationHelperFunc) inventory.Ser
}

type PullRequestReviewWriteParams struct {
Method string
Owner string
Repo string
PullNumber int32
Body string
Event string
CommitID *string
ThreadID string
Method string
Owner string
Repo string
PullNumber int32
Body string
Event string
CommitID *string
ThreadID string
ResolutionReason *string
}

func PullRequestReviewWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
Expand Down Expand Up @@ -1811,6 +1812,10 @@ func PullRequestReviewWrite(t translations.TranslationHelperFunc) inventory.Serv
Type: "string",
Description: "The node ID of the review thread (e.g., PRRT_kwDOxxx). Required for resolve_thread and unresolve_thread methods. Get thread IDs from pull_request_read with method get_review_comments.",
},
"resolutionReason": {
Type: "string",
Description: "Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid.",
},
Comment thread
cagesellchen marked this conversation as resolved.
},
Required: []string{"method", "owner", "repo", "pullNumber"},
}
Expand Down Expand Up @@ -1858,7 +1863,7 @@ Available methods:
result, err := DeletePendingPullRequestReview(ctx, client, params)
return result, nil, err
case "resolve_thread":
result, err := ResolveReviewThread(ctx, client, params.ThreadID, true)
result, err := ResolveReviewThreadWithReason(ctx, client, params.ThreadID, params.ResolutionReason, true)
return result, nil, err
case "unresolve_thread":
result, err := ResolveReviewThread(ctx, client, params.ThreadID, false)
Expand Down Expand Up @@ -2094,8 +2099,18 @@ func DeletePendingPullRequestReview(ctx context.Context, client *githubv4.Client
return utils.NewToolResultText("pending pull request review successfully deleted"), nil
}

type resolveReviewThreadInput struct {
ThreadID githubv4.ID `json:"threadId"`
ResolutionReason *githubv4.String `json:"resolutionReason,omitempty"`
}

// ResolveReviewThread resolves or unresolves a PR review thread using GraphQL mutations.
func ResolveReviewThread(ctx context.Context, client *githubv4.Client, threadID string, resolve bool) (*mcp.CallToolResult, error) {
return ResolveReviewThreadWithReason(ctx, client, threadID, nil, resolve)
}

// ResolveReviewThreadWithReason resolves or unresolves a PR review thread with an optional resolution reason.
func ResolveReviewThreadWithReason(ctx context.Context, client *githubv4.Client, threadID string, resolutionReason *string, resolve bool) (*mcp.CallToolResult, error) {
if threadID == "" {
return utils.NewToolResultError("threadId is required for resolve_thread and unresolve_thread methods"), nil
}
Expand All @@ -2110,8 +2125,9 @@ func ResolveReviewThread(ctx context.Context, client *githubv4.Client, threadID
} `graphql:"resolveReviewThread(input: $input)"`
}

input := githubv4.ResolveReviewThreadInput{
ThreadID: githubv4.ID(threadID),
input := resolveReviewThreadInput{
ThreadID: githubv4.ID(threadID),
ResolutionReason: newGQLStringlikePtr[githubv4.String](resolutionReason),
}

if err := client.Mutate(ctx, &mutation, input, nil); err != nil {
Expand Down
14 changes: 13 additions & 1 deletion pkg/github/pullrequests_granular.go
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,10 @@ func GranularResolveReviewThread(t translations.TranslationHelperFunc) inventory
Type: "string",
Description: "The node ID of the review thread to resolve (e.g., PRRT_kwDOxxx)",
},
"resolutionReason": {
Type: "string",
Description: "Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid.",
},
Comment thread
cagesellchen marked this conversation as resolved.
},
Required: []string{"threadID"},
},
Expand All @@ -700,13 +704,21 @@ func GranularResolveReviewThread(t translations.TranslationHelperFunc) inventory
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
resolutionReason, hasResolutionReason, err := OptionalParamOK[string](args, "resolutionReason")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
var resolutionReasonPtr *string
if hasResolutionReason {
resolutionReasonPtr = &resolutionReason
}

gqlClient, err := deps.GetGQLClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err), nil, nil
}

result, err := ResolveReviewThread(ctx, gqlClient, threadID, true)
result, err := ResolveReviewThreadWithReason(ctx, gqlClient, threadID, resolutionReasonPtr, true)
return result, nil, err
},
)
Expand Down
41 changes: 39 additions & 2 deletions pkg/github/pullrequests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4572,7 +4572,7 @@ func TestResolveReviewThread(t *testing.T) {
}
} `graphql:"resolveReviewThread(input: $input)"`
}{},
githubv4.ResolveReviewThreadInput{
resolveReviewThreadInput{
ThreadID: githubv4.ID("PRRT_kwDOTest123"),
},
nil,
Expand All @@ -4588,6 +4588,43 @@ func TestResolveReviewThread(t *testing.T) {
),
expectedResult: "review thread resolved successfully",
},
{
name: "successful resolve thread with resolution reason",
requestArgs: map[string]any{
"method": "resolve_thread",
"owner": "owner",
"repo": "repo",
"pullNumber": float64(42),
"threadId": "PRRT_kwDOTest123",
"resolutionReason": "wont-fix",
},
mockedClient: githubv4mock.NewMockedHTTPClient(
githubv4mock.NewMutationMatcher(
struct {
ResolveReviewThread struct {
Thread struct {
ID githubv4.ID
IsResolved githubv4.Boolean
}
} `graphql:"resolveReviewThread(input: $input)"`
}{},
resolveReviewThreadInput{
ThreadID: githubv4.ID("PRRT_kwDOTest123"),
ResolutionReason: newGQLStringlike[githubv4.String]("wont-fix"),
},
nil,
githubv4mock.DataResponse(map[string]any{
"resolveReviewThread": map[string]any{
"thread": map[string]any{
"id": "PRRT_kwDOTest123",
"isResolved": true,
},
},
}),
),
),
expectedResult: "review thread resolved successfully",
},
{
name: "successful unresolve thread",
requestArgs: map[string]any{
Expand Down Expand Up @@ -4692,7 +4729,7 @@ func TestResolveReviewThread(t *testing.T) {
}
} `graphql:"resolveReviewThread(input: $input)"`
}{},
githubv4.ResolveReviewThreadInput{
resolveReviewThreadInput{
ThreadID: githubv4.ID("PRRT_invalid"),
},
nil,
Expand Down
Loading