Skip to content

Commit ca55793

Browse files
Merge branch 'main' into timrogers/planning-reaction-tools
2 parents b0d11b0 + 63d313a commit ca55793

9 files changed

Lines changed: 225 additions & 35 deletions

File tree

docs/feature-flags.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ runtime behavior (such as output formatting) won't appear here.
4545
- `owner`: Repository owner (string, required)
4646
- `repo`: Repository name (string, required)
4747
- `reviewers`: GitHub usernames or ORG/team-slug team reviewers to request reviews from (string[], optional)
48-
- `show_ui`: Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when you have all required values (especially ones the form does not collect, like reviewers) and the user has already confirmed the action. (boolean, optional, conditional — visible when remote_mcp_ui_apps is enabled unless the client explicitly indicates it does not support io.modelcontextprotocol/ui)
48+
- `show_ui`: Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when the user has already confirmed the action and the form would be redundant. (boolean, optional, conditional — visible when remote_mcp_ui_apps is enabled unless the client explicitly indicates it does not support io.modelcontextprotocol/ui)
4949
- `title`: PR title (string, required)
5050

5151
- **get_me** - Get my user profile
@@ -69,7 +69,7 @@ runtime behavior (such as output formatting) won't appear here.
6969
- `milestone`: Milestone number (number, optional)
7070
- `owner`: Repository owner (string, required)
7171
- `repo`: Repository name (string, required)
72-
- `show_ui`: Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when you have all required values (especially ones the form does not collect, like labels, assignees, milestone, type, issue_fields, or state changes) and the user has already confirmed the action. (boolean, optional, conditional — visible when remote_mcp_ui_apps is enabled unless the client explicitly indicates it does not support io.modelcontextprotocol/ui)
72+
- `show_ui`: Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when the user has already confirmed the action and the form would be redundant. (boolean, optional, conditional — visible when remote_mcp_ui_apps is enabled unless the client explicitly indicates it does not support io.modelcontextprotocol/ui)
7373
- `state`: New state (string, optional)
7474
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
7575
- `title`: Issue title (string, optional)

docs/insiders-features.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ The list below is generated from the Go source. It covers tool **inventory and s
3939
- `owner`: Repository owner (string, required)
4040
- `repo`: Repository name (string, required)
4141
- `reviewers`: GitHub usernames or ORG/team-slug team reviewers to request reviews from (string[], optional)
42-
- `show_ui`: Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when you have all required values (especially ones the form does not collect, like reviewers) and the user has already confirmed the action. (boolean, optional, conditional — visible when remote_mcp_ui_apps is enabled unless the client explicitly indicates it does not support io.modelcontextprotocol/ui)
42+
- `show_ui`: Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when the user has already confirmed the action and the form would be redundant. (boolean, optional, conditional — visible when remote_mcp_ui_apps is enabled unless the client explicitly indicates it does not support io.modelcontextprotocol/ui)
4343
- `title`: PR title (string, required)
4444

4545
- **get_me** - Get my user profile
@@ -63,7 +63,7 @@ The list below is generated from the Go source. It covers tool **inventory and s
6363
- `milestone`: Milestone number (number, optional)
6464
- `owner`: Repository owner (string, required)
6565
- `repo`: Repository name (string, required)
66-
- `show_ui`: Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when you have all required values (especially ones the form does not collect, like labels, assignees, milestone, type, issue_fields, or state changes) and the user has already confirmed the action. (boolean, optional, conditional — visible when remote_mcp_ui_apps is enabled unless the client explicitly indicates it does not support io.modelcontextprotocol/ui)
66+
- `show_ui`: Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when the user has already confirmed the action and the form would be redundant. (boolean, optional, conditional — visible when remote_mcp_ui_apps is enabled unless the client explicitly indicates it does not support io.modelcontextprotocol/ui)
6767
- `state`: New state (string, optional)
6868
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
6969
- `title`: Issue title (string, optional)

pkg/github/__toolsnaps__/create_pull_request.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"type": "array"
5151
},
5252
"show_ui": {
53-
"description": "Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when you have all required values (especially ones the form does not collect, like reviewers) and the user has already confirmed the action.",
53+
"description": "Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when the user has already confirmed the action and the form would be redundant.",
5454
"type": "boolean"
5555
},
5656
"title": {

pkg/github/__toolsnaps__/issue_write.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
"type": "string"
9898
},
9999
"show_ui": {
100-
"description": "Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when you have all required values (especially ones the form does not collect, like labels, assignees, milestone, type, issue_fields, or state changes) and the user has already confirmed the action.",
100+
"description": "Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when the user has already confirmed the action and the form would be redundant.",
101101
"type": "boolean"
102102
},
103103
"state": {

pkg/github/issues.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1816,6 +1816,10 @@ var issueWriteFormParams = map[string]struct{}{
18161816
"body": {},
18171817
"issue_number": {},
18181818
"issue_fields": {},
1819+
"labels": {},
1820+
"assignees": {},
1821+
"milestone": {},
1822+
"type": {},
18191823
"state": {},
18201824
"state_reason": {},
18211825
"duplicate_of": {},
@@ -1824,9 +1828,11 @@ var issueWriteFormParams = map[string]struct{}{
18241828
}
18251829

18261830
// issueWriteHasNonFormParams reports whether the call carries any parameter the
1827-
// issue_write MCP App form cannot represent (anything outside issueWriteFormParams,
1828-
// e.g. labels, assignees, milestones or issue types). Such calls must bypass
1829-
// the UI form and execute directly so the supplied values aren't silently dropped.
1831+
// issue_write MCP App form cannot represent (anything outside issueWriteFormParams).
1832+
// The form collects (and prefills) every parameter in the tool's current input
1833+
// schema, so this is a forward-compatibility safety net: a parameter added to the
1834+
// schema in the future but not yet wired into the form trips this check and bypasses
1835+
// the UI so the supplied value isn't silently dropped.
18301836
func issueWriteHasNonFormParams(args map[string]any) bool {
18311837
for key, value := range args {
18321838
if value == nil {
@@ -2003,7 +2009,7 @@ Options are:
20032009
// which renders the stripped (non-UI) schema.
20042010
"show_ui": {
20052011
Type: "boolean",
2006-
Description: "Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when you have all required values (especially ones the form does not collect, like labels, assignees, milestone, type, issue_fields, or state changes) and the user has already confirmed the action.",
2012+
Description: "Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when the user has already confirmed the action and the form would be redundant.",
20072013
},
20082014
},
20092015
Required: []string{"method", "owner", "repo"},

pkg/github/issues_test.go

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1652,9 +1652,10 @@ func Test_IssueWrite_MCPAppsFeature_UIGate(t *testing.T) {
16521652
assert.True(t, result.IsError, "form-routing stub should be marked IsError so agents don't claim success")
16531653
})
16541654

1655-
t.Run("UI client with labels skips form and executes directly", func(t *testing.T) {
1656-
// The form does not collect labels, so a call carrying them must bypass
1657-
// the form rather than silently drop them.
1655+
t.Run("UI client with labels routes through UI form", func(t *testing.T) {
1656+
// labels is now a form param (the issue-write view prefills and renders
1657+
// a label selector), so a call carrying them must route to the form
1658+
// rather than execute directly.
16581659
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
16591660
"method": "create",
16601661
"owner": "owner",
@@ -1666,10 +1667,9 @@ func Test_IssueWrite_MCPAppsFeature_UIGate(t *testing.T) {
16661667
require.NoError(t, err)
16671668

16681669
textContent := getTextResult(t, result)
1669-
assert.NotContains(t, textContent.Text, "interactive form has been shown",
1670-
"labels should skip UI form")
1671-
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/issues/1",
1672-
"labels call should execute directly and return issue URL")
1670+
assert.Contains(t, textContent.Text, "interactive form has been shown to the user for creating a new issue",
1671+
"labels should route through UI form")
1672+
assert.True(t, result.IsError, "form-routing stub should be marked IsError so agents don't claim success")
16731673
})
16741674

16751675
t.Run("UI client with show_ui=false skips form and executes directly", func(t *testing.T) {
@@ -1765,14 +1765,15 @@ func Test_issueWriteHasNonFormParams(t *testing.T) {
17651765
{name: "only form params", args: map[string]any{"method": "create", "owner": "o", "repo": "r", "title": "t", "body": "b", "issue_number": float64(1), "_ui_submitted": true}, want: false},
17661766
{name: "show_ui true is a form param", args: map[string]any{"title": "t", "show_ui": true}, want: false},
17671767
{name: "show_ui false is a form param", args: map[string]any{"title": "t", "show_ui": false}, want: false},
1768-
{name: "labels present", args: map[string]any{"title": "t", "labels": []any{"bug"}}, want: true},
1769-
{name: "assignees present", args: map[string]any{"title": "t", "assignees": []any{"octocat"}}, want: true},
1770-
{name: "milestone present", args: map[string]any{"title": "t", "milestone": float64(2)}, want: true},
1771-
{name: "type present", args: map[string]any{"title": "t", "type": "Bug"}, want: true},
1768+
{name: "labels present", args: map[string]any{"title": "t", "labels": []any{"bug"}}, want: false},
1769+
{name: "assignees present", args: map[string]any{"title": "t", "assignees": []any{"octocat"}}, want: false},
1770+
{name: "milestone present", args: map[string]any{"title": "t", "milestone": float64(2)}, want: false},
1771+
{name: "type present", args: map[string]any{"title": "t", "type": "Bug"}, want: false},
17721772
{name: "issue_fields present", args: map[string]any{"issue_fields": []any{map[string]any{"field_name": "Priority"}}}, want: false},
17731773
{name: "state present", args: map[string]any{"state": "closed"}, want: false},
17741774
{name: "state_reason present", args: map[string]any{"state_reason": "completed"}, want: false},
17751775
{name: "duplicate_of present", args: map[string]any{"duplicate_of": float64(7)}, want: false},
1776+
{name: "unknown non-schema param present", args: map[string]any{"title": "t", "not_a_real_param": "x"}, want: true},
17761777
{name: "nil value is ignored", args: map[string]any{"issue_fields": nil}, want: false},
17771778
}
17781779

@@ -1793,13 +1794,11 @@ func Test_issueWriteSchemaClassification(t *testing.T) {
17931794
t.Parallel()
17941795

17951796
// Schema properties the MCP App form cannot represent — their presence
1796-
// must trigger the safety-net bypass via issueWriteHasNonFormParams.
1797-
knownNonForm := map[string]struct{}{
1798-
"assignees": {},
1799-
"labels": {},
1800-
"milestone": {},
1801-
"type": {},
1802-
}
1797+
// must trigger the safety-net bypass via issueWriteHasNonFormParams. The
1798+
// form currently collects every schema property, so this allowlist is
1799+
// empty; add a property here only if it is added to the schema without
1800+
// corresponding form support.
1801+
knownNonForm := map[string]struct{}{}
18031802

18041803
cases := []struct {
18051804
name string

pkg/github/pullrequests.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,7 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
717717
// which renders the stripped (non-UI) schema.
718718
"show_ui": {
719719
Type: "boolean",
720-
Description: "Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when you have all required values (especially ones the form does not collect, like reviewers) and the user has already confirmed the action.",
720+
Description: "Whether to render the MCP App form instead of executing the request immediately. Defaults to true. Set to false to skip the form and execute directly — useful when the user has already confirmed the action and the form would be redundant.",
721721
},
722722
},
723723
Required: []string{"owner", "repo", "title", "head", "base"},

pkg/github/ui_tools.go

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ import (
2020
"github.com/shurcooL/githubv4"
2121
)
2222

23+
// uiGetMaxPages bounds how many pages each ui_get pagination loop will fetch.
24+
// ui_get backs a synchronous UI picker (dropdowns for labels/assignees/etc. in
25+
// the MCP App issue/PR write surfaces), so responsiveness matters more than
26+
// completeness. At PerPage 100 this caps a call at ~1000 items and a bounded
27+
// number of API round-trips, keeping latency predictable on very large
28+
// repos/orgs. Results past the cap are truncated and surfaced via a "has_more"
29+
// flag, which is acceptable because the picker pairs truncation with typeahead.
30+
const uiGetMaxPages = 10
31+
2332
// UIGet creates a tool to fetch UI data for MCP Apps.
2433
func UIGet(t translations.TranslationHelperFunc) inventory.ServerTool {
2534
st := NewTool(
@@ -131,7 +140,8 @@ func uiGetLabels(ctx context.Context, deps ToolDependencies, args map[string]any
131140

132141
labels := make([]map[string]any, 0)
133142
var totalCount int
134-
for {
143+
hasMore := false
144+
for page := 1; ; page++ {
135145
if err := client.Query(ctx, &query, vars); err != nil {
136146
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to list labels", err), nil, nil
137147
}
@@ -147,12 +157,17 @@ func uiGetLabels(ctx context.Context, deps ToolDependencies, args map[string]any
147157
if !query.Repository.Labels.PageInfo.HasNextPage {
148158
break
149159
}
160+
if page >= uiGetMaxPages {
161+
hasMore = true
162+
break
163+
}
150164
vars["cursor"] = githubv4.NewString(query.Repository.Labels.PageInfo.EndCursor)
151165
}
152166

153167
response := map[string]any{
154168
"labels": labels,
155169
"totalCount": totalCount,
170+
"has_more": hasMore,
156171
}
157172

158173
out, err := json.Marshal(response)
@@ -176,8 +191,9 @@ func uiGetAssignees(ctx context.Context, deps ToolDependencies, args map[string]
176191

177192
opts := &github.ListOptions{PerPage: 100}
178193
var allAssignees []*github.User
194+
hasMore := false
179195

180-
for {
196+
for page := 1; ; page++ {
181197
assignees, resp, err := client.Issues.ListAssignees(ctx, owner, repo, opts)
182198
if err != nil {
183199
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list assignees", resp, err), nil, nil
@@ -189,6 +205,10 @@ func uiGetAssignees(ctx context.Context, deps ToolDependencies, args map[string]
189205
if resp.NextPage == 0 {
190206
break
191207
}
208+
if page >= uiGetMaxPages {
209+
hasMore = true
210+
break
211+
}
192212
opts.Page = resp.NextPage
193213
}
194214

@@ -203,6 +223,7 @@ func uiGetAssignees(ctx context.Context, deps ToolDependencies, args map[string]
203223
out, err := json.Marshal(map[string]any{
204224
"assignees": result,
205225
"totalCount": len(result),
226+
"has_more": hasMore,
206227
})
207228
if err != nil {
208229
return utils.NewToolResultErrorFromErr("failed to marshal assignees", err), nil, nil
@@ -228,7 +249,8 @@ func uiGetMilestones(ctx context.Context, deps ToolDependencies, args map[string
228249
}
229250

230251
var allMilestones []*github.Milestone
231-
for {
252+
hasMore := false
253+
for page := 1; ; page++ {
232254
milestones, resp, err := client.Issues.ListMilestones(ctx, owner, repo, opts)
233255
if err != nil {
234256
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list milestones", resp, err), nil, nil
@@ -240,6 +262,10 @@ func uiGetMilestones(ctx context.Context, deps ToolDependencies, args map[string
240262
if resp.NextPage == 0 {
241263
break
242264
}
265+
if page >= uiGetMaxPages {
266+
hasMore = true
267+
break
268+
}
243269
opts.Page = resp.NextPage
244270
}
245271

@@ -262,6 +288,7 @@ func uiGetMilestones(ctx context.Context, deps ToolDependencies, args map[string
262288
out, err := json.Marshal(map[string]any{
263289
"milestones": result,
264290
"totalCount": len(result),
291+
"has_more": hasMore,
265292
})
266293
if err != nil {
267294
return utils.NewToolResultErrorFromErr("failed to marshal milestones", err), nil, nil
@@ -314,7 +341,8 @@ func uiGetBranches(ctx context.Context, deps ToolDependencies, args map[string]a
314341
}
315342

316343
var allBranches []*github.Branch
317-
for {
344+
hasMore := false
345+
for page := 1; ; page++ {
318346
branches, resp, err := client.Repositories.ListBranches(ctx, owner, repo, opts)
319347
if err != nil {
320348
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list branches", resp, err), nil, nil
@@ -326,6 +354,10 @@ func uiGetBranches(ctx context.Context, deps ToolDependencies, args map[string]a
326354
if resp.NextPage == 0 {
327355
break
328356
}
357+
if page >= uiGetMaxPages {
358+
hasMore = true
359+
break
360+
}
329361
opts.Page = resp.NextPage
330362
}
331363

@@ -337,6 +369,7 @@ func uiGetBranches(ctx context.Context, deps ToolDependencies, args map[string]a
337369
r, err := json.Marshal(map[string]any{
338370
"branches": minimalBranches,
339371
"totalCount": len(minimalBranches),
372+
"has_more": hasMore,
340373
})
341374
if err != nil {
342375
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
@@ -446,7 +479,8 @@ func uiGetReviewers(ctx context.Context, deps ToolDependencies, args map[string]
446479
ListOptions: github.ListOptions{PerPage: 100},
447480
}
448481
var allCollaborators []*github.User
449-
for {
482+
hasMore := false
483+
for page := 1; ; page++ {
450484
collaborators, resp, err := client.Repositories.ListCollaborators(ctx, owner, repo, collaboratorOpts)
451485
if err != nil {
452486
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list reviewers", resp, err), nil, nil
@@ -458,12 +492,16 @@ func uiGetReviewers(ctx context.Context, deps ToolDependencies, args map[string]
458492
if resp.NextPage == 0 {
459493
break
460494
}
495+
if page >= uiGetMaxPages {
496+
hasMore = true
497+
break
498+
}
461499
collaboratorOpts.Page = resp.NextPage
462500
}
463501

464502
teamOpts := &github.ListOptions{PerPage: 100}
465503
var allTeams []*github.Team
466-
for {
504+
for page := 1; ; page++ {
467505
teams, resp, err := client.Repositories.ListTeams(ctx, owner, repo, teamOpts)
468506
if err != nil {
469507
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list reviewer teams", resp, err), nil, nil
@@ -475,6 +513,10 @@ func uiGetReviewers(ctx context.Context, deps ToolDependencies, args map[string]
475513
if resp.NextPage == 0 {
476514
break
477515
}
516+
if page >= uiGetMaxPages {
517+
hasMore = true
518+
break
519+
}
478520
teamOpts.Page = resp.NextPage
479521
}
480522

@@ -503,6 +545,7 @@ func uiGetReviewers(ctx context.Context, deps ToolDependencies, args map[string]
503545
"users": users,
504546
"teams": teams,
505547
"totalCount": len(users) + len(teams),
548+
"has_more": hasMore,
506549
})
507550
if err != nil {
508551
return utils.NewToolResultErrorFromErr("failed to marshal reviewers", err), nil, nil

0 commit comments

Comments
 (0)