Skip to content

Commit 0eccc22

Browse files
committed
feat: add pull request editing functionality with reviewers support
1 parent 3f9e68f commit 0eccc22

20 files changed

Lines changed: 2010 additions & 178 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,7 @@ The following sets of tools are available:
10901090
- `maintainer_can_modify`: Allow maintainer edits (boolean, optional)
10911091
- `owner`: Repository owner (string, required)
10921092
- `repo`: Repository name (string, required)
1093+
- `reviewers`: GitHub usernames or ORG/team-slug team reviewers to request reviews from (string[], optional)
10931094
- `title`: PR title (string, required)
10941095

10951096
- **list_pull_requests** - List pull requests

docs/feature-flags.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ runtime behavior (such as output formatting) won't appear here.
4444
- `maintainer_can_modify`: Allow maintainer edits (boolean, optional)
4545
- `owner`: Repository owner (string, required)
4646
- `repo`: Repository name (string, required)
47+
- `reviewers`: GitHub usernames or ORG/team-slug team reviewers to request reviews from (string[], optional)
4748
- `title`: PR title (string, required)
4849

4950
- **get_me** - Get my user profile
@@ -76,7 +77,21 @@ runtime behavior (such as output formatting) won't appear here.
7677
- **Accepted OAuth Scopes**: `admin:org`, `read:org`, `repo`, `write:org`
7778
- `method`: The type of data to fetch (string, required)
7879
- `owner`: Repository owner (required for all methods) (string, required)
79-
- `repo`: Repository name (required for labels, assignees, milestones, branches) (string, optional)
80+
- `repo`: Repository name (required for labels, assignees, milestones, branches, issue fields, reviewers) (string, optional)
81+
82+
- **update_pull_request** - Edit pull request
83+
- **Required OAuth Scopes**: `repo`
84+
- **MCP App UI**: `ui://github-mcp-server/pr-edit`
85+
- `base`: New base branch name (string, optional)
86+
- `body`: New description (string, optional)
87+
- `draft`: Mark pull request as draft (true) or ready for review (false) (boolean, optional)
88+
- `maintainer_can_modify`: Allow maintainer edits (boolean, optional)
89+
- `owner`: Repository owner (string, required)
90+
- `pullNumber`: Pull request number to update (number, required)
91+
- `repo`: Repository name (string, required)
92+
- `reviewers`: GitHub usernames or ORG/team-slug team reviewers to request reviews from (string[], optional)
93+
- `state`: New state (string, optional)
94+
- `title`: New title (string, optional)
8095

8196
### `remote_mcp_issue_fields`
8297

@@ -247,7 +262,7 @@ runtime behavior (such as output formatting) won't appear here.
247262
- `owner`: Repository owner (username or organization) (string, required)
248263
- `pullNumber`: The pull request number (number, required)
249264
- `repo`: Repository name (string, required)
250-
- `reviewers`: GitHub usernames to request reviews from (string[], required)
265+
- `reviewers`: GitHub usernames or ORG/team-slug team reviewers to request reviews from (string[], required)
251266

252267
- **resolve_review_thread** - Resolve Review Thread
253268
- **Required OAuth Scopes**: `repo`

docs/insiders-features.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ The list below is generated from the Go source. It covers tool **inventory and s
3838
- `maintainer_can_modify`: Allow maintainer edits (boolean, optional)
3939
- `owner`: Repository owner (string, required)
4040
- `repo`: Repository name (string, required)
41+
- `reviewers`: GitHub usernames or ORG/team-slug team reviewers to request reviews from (string[], optional)
4142
- `title`: PR title (string, required)
4243

4344
- **get_me** - Get my user profile
@@ -70,7 +71,21 @@ The list below is generated from the Go source. It covers tool **inventory and s
7071
- **Accepted OAuth Scopes**: `admin:org`, `read:org`, `repo`, `write:org`
7172
- `method`: The type of data to fetch (string, required)
7273
- `owner`: Repository owner (required for all methods) (string, required)
73-
- `repo`: Repository name (required for labels, assignees, milestones, branches) (string, optional)
74+
- `repo`: Repository name (required for labels, assignees, milestones, branches, issue fields, reviewers) (string, optional)
75+
76+
- **update_pull_request** - Edit pull request
77+
- **Required OAuth Scopes**: `repo`
78+
- **MCP App UI**: `ui://github-mcp-server/pr-edit`
79+
- `base`: New base branch name (string, optional)
80+
- `body`: New description (string, optional)
81+
- `draft`: Mark pull request as draft (true) or ready for review (false) (boolean, optional)
82+
- `maintainer_can_modify`: Allow maintainer edits (boolean, optional)
83+
- `owner`: Repository owner (string, required)
84+
- `pullNumber`: Pull request number to update (number, required)
85+
- `repo`: Repository name (string, required)
86+
- `reviewers`: GitHub usernames or ORG/team-slug team reviewers to request reviews from (string[], optional)
87+
- `state`: New state (string, optional)
88+
- `title`: New title (string, optional)
7489

7590
### `remote_mcp_issue_fields`
7691

pkg/github/__toolsnaps__/create_pull_request.snap

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@
4242
"description": "Repository name",
4343
"type": "string"
4444
},
45+
"reviewers": {
46+
"description": "GitHub usernames or ORG/team-slug team reviewers to request reviews from",
47+
"items": {
48+
"type": "string"
49+
},
50+
"type": "array"
51+
},
4552
"title": {
4653
"description": "PR title",
4754
"type": "string"

pkg/github/__toolsnaps__/request_pull_request_reviewers.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@
3737
"type": "object"
3838
},
3939
"name": "request_pull_request_reviewers"
40-
}
40+
}

pkg/github/__toolsnaps__/ui_get.snap

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"readOnlyHint": true,
1111
"title": "Get UI data"
1212
},
13-
"description": "Fetch UI data for MCP Apps (labels, assignees, milestones, issue types, branches).",
13+
"description": "Fetch UI data for MCP Apps (labels, assignees, milestones, issue types, branches, issue fields, reviewers).",
1414
"inputSchema": {
1515
"properties": {
1616
"method": {
@@ -20,7 +20,9 @@
2020
"assignees",
2121
"milestones",
2222
"issue_types",
23-
"branches"
23+
"branches",
24+
"issue_fields",
25+
"reviewers"
2426
],
2527
"type": "string"
2628
},
@@ -29,7 +31,7 @@
2931
"type": "string"
3032
},
3133
"repo": {
32-
"description": "Repository name (required for labels, assignees, milestones, branches)",
34+
"description": "Repository name (required for labels, assignees, milestones, branches, issue fields, reviewers)",
3335
"type": "string"
3436
}
3537
},

pkg/github/__toolsnaps__/update_pull_request.snap

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
{
2+
"_meta": {
3+
"ui": {
4+
"resourceUri": "ui://github-mcp-server/pr-edit",
5+
"visibility": [
6+
"model",
7+
"app"
8+
]
9+
}
10+
},
211
"annotations": {
312
"title": "Edit pull request"
413
},
@@ -61,4 +70,4 @@
6170
"type": "object"
6271
},
6372
"name": "update_pull_request"
64-
}
73+
}

pkg/github/issues.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1764,8 +1764,7 @@ func searchIssuesHandler(ctx context.Context, deps ToolDependencies, args map[st
17641764
const IssueWriteUIResourceURI = "ui://github-mcp-server/issue-write"
17651765

17661766
// issueWriteFormParams are the parameters the issue_write MCP App form collects
1767-
// and re-sends on submit. The form only supports title/body editing (plus the
1768-
// routing/identity fields), so any other parameter present on a call cannot be
1767+
// and re-sends on submit. Any other parameter present on a call cannot be
17691768
// represented by the form.
17701769
var issueWriteFormParams = map[string]struct{}{
17711770
"method": {},
@@ -1774,12 +1773,16 @@ var issueWriteFormParams = map[string]struct{}{
17741773
"title": {},
17751774
"body": {},
17761775
"issue_number": {},
1776+
"issue_fields": {},
1777+
"state": {},
1778+
"state_reason": {},
1779+
"duplicate_of": {},
17771780
"_ui_submitted": {},
17781781
}
17791782

17801783
// issueWriteHasNonFormParams reports whether the call carries any parameter the
17811784
// issue_write MCP App form cannot represent (anything outside issueWriteFormParams,
1782-
// e.g. labels, assignees, issue_fields or a state change). Such calls must bypass
1785+
// e.g. labels, assignees, milestones or issue types). Such calls must bypass
17831786
// the UI form and execute directly so the supplied values aren't silently dropped.
17841787
func issueWriteHasNonFormParams(args map[string]any) bool {
17851788
for key, value := range args {

pkg/github/issues_test.go

Lines changed: 18 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,78 +1595,10 @@ func Test_IssueWrite_MCPAppsFeature_UIGate(t *testing.T) {
15951595
"non-UI client should execute directly")
15961596
})
15971597

1598-
t.Run("UI client with state change skips form and executes directly", func(t *testing.T) {
1599-
mockBaseIssue := &github.Issue{
1600-
Number: github.Ptr(1),
1601-
Title: github.Ptr("Test"),
1602-
State: github.Ptr("open"),
1603-
HTMLURL: github.Ptr("https://github.com/owner/repo/issues/1"),
1604-
}
1605-
issueIDQueryResponse := githubv4mock.DataResponse(map[string]any{
1606-
"repository": map[string]any{
1607-
"issue": map[string]any{
1608-
"id": "I_kwDOA0xdyM50BPaO",
1609-
},
1610-
},
1611-
})
1612-
closeSuccessResponse := githubv4mock.DataResponse(map[string]any{
1613-
"closeIssue": map[string]any{
1614-
"issue": map[string]any{
1615-
"id": "I_kwDOA0xdyM50BPaO",
1616-
"number": 1,
1617-
"url": "https://github.com/owner/repo/issues/1",
1618-
"state": "CLOSED",
1619-
},
1620-
},
1621-
})
1622-
completedReason := IssueClosedStateReasonCompleted
1623-
1624-
closeClient := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
1625-
PatchReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockBaseIssue),
1626-
}))
1627-
closeGQLClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(
1628-
githubv4mock.NewQueryMatcher(
1629-
struct {
1630-
Repository struct {
1631-
Issue struct {
1632-
ID githubv4.ID
1633-
} `graphql:"issue(number: $issueNumber)"`
1634-
} `graphql:"repository(owner: $owner, name: $repo)"`
1635-
}{},
1636-
map[string]any{
1637-
"owner": githubv4.String("owner"),
1638-
"repo": githubv4.String("repo"),
1639-
"issueNumber": githubv4.Int(1),
1640-
},
1641-
issueIDQueryResponse,
1642-
),
1643-
githubv4mock.NewMutationMatcher(
1644-
struct {
1645-
CloseIssue struct {
1646-
Issue struct {
1647-
ID githubv4.ID
1648-
Number githubv4.Int
1649-
URL githubv4.String
1650-
State githubv4.String
1651-
}
1652-
} `graphql:"closeIssue(input: $input)"`
1653-
}{},
1654-
CloseIssueInput{
1655-
IssueID: "I_kwDOA0xdyM50BPaO",
1656-
StateReason: &completedReason,
1657-
},
1658-
nil,
1659-
closeSuccessResponse,
1660-
),
1661-
))
1662-
1663-
closeDeps := BaseDeps{
1664-
Client: closeClient,
1665-
GQLClient: closeGQLClient,
1666-
featureChecker: featureCheckerFor(MCPAppsFeatureFlag),
1667-
}
1668-
closeHandler := serverTool.Handler(closeDeps)
1669-
1598+
t.Run("UI client with state change routes through UI form", func(t *testing.T) {
1599+
// state/state_reason/duplicate_of are form params (the issue-write view
1600+
// renders close/reopen controls), so a call carrying them must go to
1601+
// the form rather than execute directly.
16701602
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
16711603
"method": "update",
16721604
"owner": "owner",
@@ -1675,14 +1607,12 @@ func Test_IssueWrite_MCPAppsFeature_UIGate(t *testing.T) {
16751607
"state": "closed",
16761608
"state_reason": "completed",
16771609
})
1678-
result, err := closeHandler(ContextWithDeps(context.Background(), closeDeps), &request)
1610+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
16791611
require.NoError(t, err)
16801612

16811613
textContent := getTextResult(t, result)
1682-
assert.NotContains(t, textContent.Text, "Ready to update issue",
1683-
"state change should skip UI form")
1684-
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/issues/1",
1685-
"state change should execute directly and return issue URL")
1614+
assert.Contains(t, textContent.Text, "Ready to update issue #1",
1615+
"state change should route through UI form")
16861616
})
16871617

16881618
t.Run("UI client update without state change returns form message", func(t *testing.T) {
@@ -1701,61 +1631,10 @@ func Test_IssueWrite_MCPAppsFeature_UIGate(t *testing.T) {
17011631
"update without state should show UI form")
17021632
})
17031633

1704-
t.Run("UI client with issue_fields skips form and executes directly", func(t *testing.T) {
1705-
// The MCP App form does not collect or re-send issue_fields, so a call
1706-
// carrying them must bypass the form and apply the values directly.
1707-
fieldsClient := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
1708-
PostReposIssuesByOwnerByRepo: expectRequestBody(t, map[string]any{
1709-
"title": "Issue with fields",
1710-
"body": "",
1711-
"labels": []any{},
1712-
"assignees": []any{},
1713-
"issue_field_values": []any{
1714-
map[string]any{"field_id": float64(101), "value": "P1"},
1715-
},
1716-
}).andThen(
1717-
mockResponse(t, http.StatusCreated, &github.Issue{
1718-
Number: github.Ptr(125),
1719-
Title: github.Ptr("Issue with fields"),
1720-
HTMLURL: github.Ptr("https://github.com/owner/repo/issues/125"),
1721-
State: github.Ptr("open"),
1722-
}),
1723-
),
1724-
}))
1725-
fieldsGQLClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(
1726-
githubv4mock.NewQueryMatcher(
1727-
issueFieldWriteMetadataQuery{},
1728-
map[string]any{
1729-
"owner": githubv4.String("owner"),
1730-
"repo": githubv4.String("repo"),
1731-
},
1732-
githubv4mock.DataResponse(map[string]any{
1733-
"repository": map[string]any{
1734-
"issueFields": map[string]any{
1735-
"nodes": []any{
1736-
map[string]any{
1737-
"__typename": "IssueFieldSingleSelect",
1738-
"fullDatabaseId": "101",
1739-
"name": "Priority",
1740-
"dataType": "single_select",
1741-
"options": []any{
1742-
map[string]any{"fullDatabaseId": "9001", "name": "P1"},
1743-
},
1744-
},
1745-
},
1746-
},
1747-
},
1748-
}),
1749-
),
1750-
))
1751-
1752-
fieldsDeps := BaseDeps{
1753-
Client: fieldsClient,
1754-
GQLClient: fieldsGQLClient,
1755-
featureChecker: featureCheckerFor(MCPAppsFeatureFlag),
1756-
}
1757-
fieldsHandler := serverTool.Handler(fieldsDeps)
1758-
1634+
t.Run("UI client with issue_fields routes through UI form", func(t *testing.T) {
1635+
// issue_fields is now a form param (the issue-write view renders a
1636+
// per-field editor), so a call carrying it must go to the form rather
1637+
// than execute directly.
17591638
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
17601639
"method": "create",
17611640
"owner": "owner",
@@ -1765,14 +1644,12 @@ func Test_IssueWrite_MCPAppsFeature_UIGate(t *testing.T) {
17651644
map[string]any{"field_name": "Priority", "field_option_name": "P1"},
17661645
},
17671646
})
1768-
result, err := fieldsHandler(ContextWithDeps(context.Background(), fieldsDeps), &request)
1647+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
17691648
require.NoError(t, err)
17701649

17711650
textContent := getTextResult(t, result)
1772-
assert.NotContains(t, textContent.Text, "Ready to create an issue",
1773-
"issue_fields should skip UI form")
1774-
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/issues/125",
1775-
"issue_fields call should execute directly and return issue URL")
1651+
assert.Contains(t, textContent.Text, "Ready to create an issue",
1652+
"issue_fields should route through UI form")
17761653
})
17771654

17781655
t.Run("UI client with labels skips form and executes directly", func(t *testing.T) {
@@ -1810,10 +1687,10 @@ func Test_issueWriteHasNonFormParams(t *testing.T) {
18101687
{name: "assignees present", args: map[string]any{"title": "t", "assignees": []any{"octocat"}}, want: true},
18111688
{name: "milestone present", args: map[string]any{"title": "t", "milestone": float64(2)}, want: true},
18121689
{name: "type present", args: map[string]any{"title": "t", "type": "Bug"}, want: true},
1813-
{name: "issue_fields present", args: map[string]any{"issue_fields": []any{map[string]any{"field_name": "Priority"}}}, want: true},
1814-
{name: "state present", args: map[string]any{"state": "closed"}, want: true},
1815-
{name: "state_reason present", args: map[string]any{"state_reason": "completed"}, want: true},
1816-
{name: "duplicate_of present", args: map[string]any{"duplicate_of": float64(7)}, want: true},
1690+
{name: "issue_fields present", args: map[string]any{"issue_fields": []any{map[string]any{"field_name": "Priority"}}}, want: false},
1691+
{name: "state present", args: map[string]any{"state": "closed"}, want: false},
1692+
{name: "state_reason present", args: map[string]any{"state_reason": "completed"}, want: false},
1693+
{name: "duplicate_of present", args: map[string]any{"duplicate_of": float64(7)}, want: false},
18171694
{name: "nil value is ignored", args: map[string]any{"issue_fields": nil}, want: false},
18181695
}
18191696

0 commit comments

Comments
 (0)