Skip to content

Commit b81ca20

Browse files
committed
fix(pull-requests): relax pull_request_review_write required fields for resolve_thread
resolve_thread and unresolve_thread only need threadId, but the advertised inputSchema required owner/repo/pullNumber for every method. Schema-validating MCP clients therefore rejected valid calls. Require only method at the schema root, document per-method fields, and validate create/submit/delete vs resolve/unresolve in the argument normalizer. Fixes #3501
1 parent c59e8c1 commit b81ca20

8 files changed

Lines changed: 116 additions & 28 deletions

‎README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,9 +1337,9 @@ For negotiated supported protocol versions `2026-07-28` or newer, the consolidat
13371337
- `commitID`: SHA of commit to review (string, optional)
13381338
- `event`: Review action to perform. (string, optional)
13391339
- `method`: The write operation to perform on pull request review. (string, required)
1340-
- `owner`: Repository owner (string, required)
1341-
- `pullNumber`: Pull request number (number, required)
1342-
- `repo`: Repository name (string, required)
1340+
- `owner`: Repository owner (required for create, submit_pending, and delete_pending methods) (string, optional)
1341+
- `pullNumber`: Pull request number (required for create, submit_pending, and delete_pending methods) (number, optional)
1342+
- `repo`: Repository name (required for create, submit_pending, and delete_pending methods) (string, optional)
13431343
- `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)
13441344

13451345
- **search_pull_requests** - Search pull requests

‎docs/feature-flags.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,9 +414,9 @@ as output formatting) won't appear here.
414414
- `commitID`: SHA of commit to review (string, optional)
415415
- `event`: Review action to perform. (string, optional)
416416
- `method`: The write operation to perform on pull request review. (string, required)
417-
- `owner`: Repository owner (string, required)
418-
- `pullNumber`: Pull request number (number, required)
419-
- `repo`: Repository name (string, required)
417+
- `owner`: Repository owner (required for create, submit_pending, and delete_pending methods) (string, optional)
418+
- `pullNumber`: Pull request number (required for create, submit_pending, and delete_pending methods) (number, optional)
419+
- `repo`: Repository name (required for create, submit_pending, and delete_pending methods) (string, optional)
420420
- `resolutionReason`: Optional reason for resolving a Copilot code review thread: addressed, wont-fix, or invalid. (string, optional)
421421
- `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)
422422

‎pkg/github/__toolsnaps__/pull_request_review_write.snap‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@
3636
"type": "string"
3737
},
3838
"owner": {
39-
"description": "Repository owner",
39+
"description": "Repository owner (required for create, submit_pending, and delete_pending methods)",
4040
"type": "string"
4141
},
4242
"pullNumber": {
43-
"description": "Pull request number",
43+
"description": "Pull request number (required for create, submit_pending, and delete_pending methods)",
4444
"type": "number"
4545
},
4646
"repo": {
47-
"description": "Repository name",
47+
"description": "Repository name (required for create, submit_pending, and delete_pending methods)",
4848
"type": "string"
4949
},
5050
"threadId": {
@@ -53,10 +53,7 @@
5353
}
5454
},
5555
"required": [
56-
"method",
57-
"owner",
58-
"repo",
59-
"pullNumber"
56+
"method"
6057
],
6158
"type": "object"
6259
},

‎pkg/github/__toolsnaps__/pull_request_review_write_resolution_reason.snap‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@
3636
"type": "string"
3737
},
3838
"owner": {
39-
"description": "Repository owner",
39+
"description": "Repository owner (required for create, submit_pending, and delete_pending methods)",
4040
"type": "string"
4141
},
4242
"pullNumber": {
43-
"description": "Pull request number",
43+
"description": "Pull request number (required for create, submit_pending, and delete_pending methods)",
4444
"type": "number"
4545
},
4646
"repo": {
47-
"description": "Repository name",
47+
"description": "Repository name (required for create, submit_pending, and delete_pending methods)",
4848
"type": "string"
4949
},
5050
"resolutionReason": {
@@ -57,10 +57,7 @@
5757
}
5858
},
5959
"required": [
60-
"method",
61-
"owner",
62-
"repo",
63-
"pullNumber"
60+
"method"
6461
],
6562
"type": "object"
6663
},

‎pkg/github/consolidated_pull_request_types.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,16 @@ func normalizePullRequestReviewWriteArguments(raw json.RawMessage) (json.RawMess
594594
if !slices.Contains([]string{"create", "submit_pending", "delete_pending", "resolve_thread", "unresolve_thread"}, params.Method) {
595595
return nil, fmt.Errorf("unknown method: %s", params.Method)
596596
}
597+
switch params.Method {
598+
case "resolve_thread", "unresolve_thread":
599+
if params.ThreadID == "" {
600+
return nil, fmt.Errorf("threadId is required for resolve_thread and unresolve_thread methods")
601+
}
602+
case "create", "submit_pending", "delete_pending":
603+
if params.Owner == "" || params.Repo == "" || params.PullNumber == 0 {
604+
return nil, fmt.Errorf("owner, repo, and pullNumber are required for %s method", params.Method)
605+
}
606+
}
597607
canonical := map[string]any{
598608
"method": params.Method, "owner": params.Owner, "repo": params.Repo, "pullNumber": params.PullNumber,
599609
}

‎pkg/github/pullrequests.go‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2015,15 +2015,15 @@ func pullRequestReviewWrite(t translations.TranslationHelperFunc, withResolution
20152015
},
20162016
"owner": {
20172017
Type: "string",
2018-
Description: "Repository owner",
2018+
Description: "Repository owner (required for create, submit_pending, and delete_pending methods)",
20192019
},
20202020
"repo": {
20212021
Type: "string",
2022-
Description: "Repository name",
2022+
Description: "Repository name (required for create, submit_pending, and delete_pending methods)",
20232023
},
20242024
"pullNumber": {
20252025
Type: "number",
2026-
Description: "Pull request number",
2026+
Description: "Pull request number (required for create, submit_pending, and delete_pending methods)",
20272027
},
20282028
"body": {
20292029
Type: "string",
@@ -2043,7 +2043,9 @@ func pullRequestReviewWrite(t translations.TranslationHelperFunc, withResolution
20432043
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.",
20442044
},
20452045
},
2046-
Required: []string{"method", "owner", "repo", "pullNumber"},
2046+
// Only method is globally required: resolve_thread / unresolve_thread use
2047+
// threadId alone, matching discussion_comment_write and the granular tools.
2048+
Required: []string{"method"},
20472049
}
20482050
if withResolutionReason {
20492051
schema.Properties["resolutionReason"] = &jsonschema.Schema{

‎pkg/github/pullrequests_test.go‎

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3203,7 +3203,7 @@ func TestCreateAndSubmitPullRequestReview(t *testing.T) {
32033203
assert.Contains(t, schema.Properties, "body")
32043204
assert.Contains(t, schema.Properties, "event")
32053205
assert.Contains(t, schema.Properties, "commitID")
3206-
assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"})
3206+
assert.ElementsMatch(t, schema.Required, []string{"method"})
32073207

32083208
tests := []struct {
32093209
name string
@@ -3460,7 +3460,7 @@ func TestCreatePendingPullRequestReview(t *testing.T) {
34603460
assert.Contains(t, schema.Properties, "repo")
34613461
assert.Contains(t, schema.Properties, "pullNumber")
34623462
assert.Contains(t, schema.Properties, "commitID")
3463-
assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"})
3463+
assert.ElementsMatch(t, schema.Required, []string{"method"})
34643464

34653465
tests := []struct {
34663466
name string
@@ -3918,7 +3918,7 @@ func TestSubmitPendingPullRequestReview(t *testing.T) {
39183918
assert.Contains(t, schema.Properties, "pullNumber")
39193919
assert.Contains(t, schema.Properties, "event")
39203920
assert.Contains(t, schema.Properties, "body")
3921-
assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"})
3921+
assert.ElementsMatch(t, schema.Required, []string{"method"})
39223922

39233923
tests := []struct {
39243924
name string
@@ -4019,7 +4019,7 @@ func TestDeletePendingPullRequestReview(t *testing.T) {
40194019
assert.Contains(t, schema.Properties, "owner")
40204020
assert.Contains(t, schema.Properties, "repo")
40214021
assert.Contains(t, schema.Properties, "pullNumber")
4022-
assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"})
4022+
assert.ElementsMatch(t, schema.Required, []string{"method"})
40234023

40244024
tests := []struct {
40254025
name string
@@ -4681,6 +4681,12 @@ func TestAddReplyToPullRequestComment(t *testing.T) {
46814681
func TestResolveReviewThread(t *testing.T) {
46824682
t.Parallel()
46834683

4684+
serverTool := PullRequestReviewWrite(translations.NullTranslationHelper)
4685+
schema := serverTool.Tool.InputSchema.(*jsonschema.Schema)
4686+
assert.ElementsMatch(t, schema.Required, []string{"method"})
4687+
assert.Contains(t, schema.Properties["owner"].Description, "required for create")
4688+
assert.Contains(t, schema.Properties["threadId"].Description, "Required for resolve_thread")
4689+
46844690
tests := []struct {
46854691
name string
46864692
requestArgs map[string]any
@@ -4725,6 +4731,38 @@ func TestResolveReviewThread(t *testing.T) {
47254731
),
47264732
expectedResult: "review thread resolved successfully",
47274733
},
4734+
{
4735+
name: "successful resolve thread without owner repo pullNumber",
4736+
requestArgs: map[string]any{
4737+
"method": "resolve_thread",
4738+
"threadId": "PRRT_kwDOTest123",
4739+
},
4740+
mockedClient: githubv4mock.NewMockedHTTPClient(
4741+
githubv4mock.NewMutationMatcher(
4742+
struct {
4743+
ResolveReviewThread struct {
4744+
Thread struct {
4745+
ID githubv4.ID
4746+
IsResolved githubv4.Boolean
4747+
}
4748+
} `graphql:"resolveReviewThread(input: $input)"`
4749+
}{},
4750+
ResolveReviewThreadInput{
4751+
ThreadID: githubv4.ID("PRRT_kwDOTest123"),
4752+
},
4753+
nil,
4754+
githubv4mock.DataResponse(map[string]any{
4755+
"resolveReviewThread": map[string]any{
4756+
"thread": map[string]any{
4757+
"id": "PRRT_kwDOTest123",
4758+
"isResolved": true,
4759+
},
4760+
},
4761+
}),
4762+
),
4763+
),
4764+
expectedResult: "review thread resolved successfully",
4765+
},
47284766
{
47294767
name: "successful resolve thread with resolution reason",
47304768
withResolutionReason: true,

‎pkg/github/typed_consolidated_pull_request_outputs_test.go‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,3 +632,47 @@ func TestConsolidatedPullRequestOutputTypesAreCurated(t *testing.T) {
632632
check(typ)
633633
}
634634
}
635+
636+
func TestPullRequestReviewWriteResolveThreadSchema(t *testing.T) {
637+
// Advertised schema must not require owner/repo/pullNumber so MCP clients can
638+
// call resolve_thread / unresolve_thread with only threadId (#3501).
639+
for _, protocol := range typedPullRequestProtocols {
640+
t.Run("protocol="+protocol, func(t *testing.T) {
641+
session, _ := consolidatedPullRequestSession(t, consolidatedPullRequestDeps(t), protocol, false)
642+
list, err := session.ListTools(context.Background(), nil)
643+
require.NoError(t, err)
644+
var schema map[string]any
645+
for _, tool := range list.Tools {
646+
if tool.Name != "pull_request_review_write" {
647+
continue
648+
}
649+
require.NoError(t, json.Unmarshal([]byte(mustMarshalJSON(t, tool.InputSchema)), &schema))
650+
}
651+
require.NotEmpty(t, schema)
652+
required, _ := schema["required"].([]any)
653+
assert.Equal(t, []any{"method"}, required)
654+
655+
result, err := session.CallTool(context.Background(), &mcp.CallToolParams{
656+
Name: "pull_request_review_write",
657+
Arguments: map[string]any{
658+
"method": "resolve_thread",
659+
"threadId": "T_1",
660+
},
661+
})
662+
require.NoError(t, err)
663+
require.False(t, result.IsError, mustMarshalJSON(t, result))
664+
assert.Contains(t, getTextResult(t, result).Text, "review thread resolved successfully")
665+
666+
missing, err := session.CallTool(context.Background(), &mcp.CallToolParams{
667+
Name: "pull_request_review_write",
668+
Arguments: map[string]any{
669+
"method": "create",
670+
"pullNumber": 1,
671+
},
672+
})
673+
require.NoError(t, err)
674+
require.True(t, missing.IsError)
675+
assert.Contains(t, getErrorResult(t, missing).Text, "owner, repo, and pullNumber are required for create method")
676+
})
677+
}
678+
}

0 commit comments

Comments
 (0)