Skip to content

Commit e2207ac

Browse files
refactor(mcp-apps): remove show_ui — it can't suppress app rendering
show_ui promised "skip the form and execute directly", but it can't deliver: the host renders an MCP App for any tool that carries _meta.ui.resourceUri, and the 2026-01-26 MCP Apps spec has no per-call/per-result way to opt out of rendering. show_ui only flipped the server's defer decision, so show_ui=false created the PR/issue up-front yet the host still rendered the app — exactly the contradiction this work set out to fix. And show_ui is only ever exposed to clients that support UI, i.e. precisely the clients that always render the app. Remove it entirely: - Drop the show_ui schema property, the form-param allowlist entry, and the showUI term from the defer predicate in create_pull_request and issue_write. The gate is now FF && clientSupportsUI && !_ui_submitted && !hasNonFormParams. - Delete the now-unused UI-only schema-property strip machinery in pkg/inventory (uiOnlySchemaProperties, stripUIOnlySchemaProperties, stripSchemaProperties) and the exported ConditionalSchemaPropertyDescriptions, which existed solely to surface show_ui to UI-capable clients. _meta.ui stripping is untouched. - Drop the conditional-property annotation from the docs generator. - Update toolsnaps, generated docs, and tests. With the up-front-execution Views now rendering the result (success card), the remaining contract is simple: when MCP Apps are enabled the form is the path, and the form is only shown while the action is genuinely deferred. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 09a7a78 commit e2207ac

12 files changed

Lines changed: 9 additions & 588 deletions

File tree

cmd/github-mcp-server/generate_docs.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,6 @@ func writeToolDoc(buf *strings.Builder, tool inventory.ServerTool) {
265265
}
266266
sort.Strings(paramNames)
267267

268-
conditional := inventory.ConditionalSchemaPropertyDescriptions()
269-
270268
for i, propName := range paramNames {
271269
prop := schema.Properties[propName]
272270
required := slices.Contains(schema.Required, propName)
@@ -292,11 +290,7 @@ func writeToolDoc(buf *strings.Builder, tool inventory.ServerTool) {
292290
// Indent any continuation lines in the description to maintain markdown formatting
293291
description := indentMultilineDescription(prop.Description, " ")
294292

295-
if cond, isConditional := conditional[propName]; isConditional {
296-
fmt.Fprintf(buf, " - `%s`: %s (%s, %s, conditional — %s)", propName, description, typeStr, requiredStr, cond)
297-
} else {
298-
fmt.Fprintf(buf, " - `%s`: %s (%s, %s)", propName, description, typeStr, requiredStr)
299-
}
293+
fmt.Fprintf(buf, " - `%s`: %s (%s, %s)", propName, description, typeStr, requiredStr)
300294
if i < len(paramNames)-1 {
301295
buf.WriteString("\n")
302296
}

docs/feature-flags.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ 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 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)
4948
- `title`: PR title (string, required)
5049

5150
- **get_me** - Get my user profile
@@ -69,7 +68,6 @@ runtime behavior (such as output formatting) won't appear here.
6968
- `milestone`: Milestone number (number, optional)
7069
- `owner`: Repository owner (string, required)
7170
- `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 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)
7371
- `state`: New state (string, optional)
7472
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
7573
- `title`: Issue title (string, optional)

docs/insiders-features.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ 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 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)
4342
- `title`: PR title (string, required)
4443

4544
- **get_me** - Get my user profile
@@ -63,7 +62,6 @@ The list below is generated from the Go source. It covers tool **inventory and s
6362
- `milestone`: Milestone number (number, optional)
6463
- `owner`: Repository owner (string, required)
6564
- `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 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)
6765
- `state`: New state (string, optional)
6866
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
6967
- `title`: Issue title (string, optional)

pkg/github/__toolsnaps__/create_pull_request.snap

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@
4949
},
5050
"type": "array"
5151
},
52-
"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 the user has already confirmed the action and the form would be redundant.",
54-
"type": "boolean"
55-
},
5652
"title": {
5753
"description": "PR title",
5854
"type": "string"

pkg/github/__toolsnaps__/issue_write.snap

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,6 @@
9696
"description": "Repository name",
9797
"type": "string"
9898
},
99-
"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 the user has already confirmed the action and the form would be redundant.",
101-
"type": "boolean"
102-
},
10399
"state": {
104100
"description": "New state",
105101
"enum": [

pkg/github/issues.go

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,7 +1742,6 @@ var issueWriteFormParams = map[string]struct{}{
17421742
"state": {},
17431743
"state_reason": {},
17441744
"duplicate_of": {},
1745-
"show_ui": {},
17461745
"_ui_submitted": {},
17471746
}
17481747

@@ -1919,17 +1918,6 @@ Options are:
19191918
Required: []string{"field_name"},
19201919
},
19211920
},
1922-
// show_ui is hidden from clients that do not advertise MCP App
1923-
// UI support. The strip happens per-request in
1924-
// inventory.ToolsForRegistration; it is present in the static
1925-
// schema (and therefore in toolsnaps and the feature-flag /
1926-
// insiders docs) so the UI-capable surface is fully
1927-
// documented. It is intentionally not in the main README,
1928-
// which renders the stripped (non-UI) schema.
1929-
"show_ui": {
1930-
Type: "boolean",
1931-
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.",
1932-
},
19331921
},
19341922
Required: []string{"method", "owner", "repo"},
19351923
},
@@ -1952,18 +1940,13 @@ Options are:
19521940

19531941
// When MCP Apps are enabled and the client supports UI, route the
19541942
// call to the interactive form unless:
1955-
// - it is itself a form submission (the UI sends _ui_submitted=true),
1956-
// - the caller explicitly asked to skip the UI (show_ui=false), or
1943+
// - it is itself a form submission (the UI sends _ui_submitted=true), or
19571944
// - it carries parameters the form cannot represent (e.g. labels,
19581945
// assignees or issue_fields). Those must be applied directly so
19591946
// their values aren't silently dropped.
19601947
uiSubmitted, _ := OptionalParam[bool](args, "_ui_submitted")
1961-
showUI, err := OptionalBoolParamWithDefault(args, "show_ui", true)
1962-
if err != nil {
1963-
return utils.NewToolResultError(err.Error()), nil, nil
1964-
}
19651948

1966-
if deps.IsFeatureEnabled(ctx, MCPAppsFeatureFlag) && clientSupportsUI(ctx, req) && !uiSubmitted && showUI && !issueWriteHasNonFormParams(args) {
1949+
if deps.IsFeatureEnabled(ctx, MCPAppsFeatureFlag) && clientSupportsUI(ctx, req) && !uiSubmitted && !issueWriteHasNonFormParams(args) {
19671950
issueNumber := 0
19681951
if method == "update" {
19691952
n, numErr := RequiredInt(args, "issue_number")

pkg/github/issues_test.go

Lines changed: 0 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,86 +1674,6 @@ func Test_IssueWrite_MCPAppsFeature_UIGate(t *testing.T) {
16741674
"labels should route through UI form")
16751675
assert.True(t, result.IsError, "form-routing stub should be marked IsError so agents don't claim success")
16761676
})
1677-
1678-
t.Run("UI client with show_ui=false skips form and executes directly", func(t *testing.T) {
1679-
// show_ui=false is the explicit, model-facing way to opt out of the
1680-
// form. It must bypass the form even when every other condition would
1681-
// route the call there (UI capability, MCP Apps flag on, no
1682-
// _ui_submitted, only form params present).
1683-
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
1684-
"method": "create",
1685-
"owner": "owner",
1686-
"repo": "repo",
1687-
"title": "Test",
1688-
"show_ui": false,
1689-
})
1690-
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
1691-
require.NoError(t, err)
1692-
1693-
textContent := getTextResult(t, result)
1694-
assert.NotContains(t, textContent.Text, "interactive form has been shown",
1695-
"show_ui=false should skip UI form")
1696-
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/issues/1",
1697-
"show_ui=false call should execute directly and return issue URL")
1698-
})
1699-
1700-
t.Run("UI client with show_ui=true returns form message", func(t *testing.T) {
1701-
// show_ui=true is the explicit, redundant-with-the-default way to ask
1702-
// for the form. It must still route through the form and must not be
1703-
// treated as a non-form parameter that would trigger the safety-net
1704-
// bypass.
1705-
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
1706-
"method": "create",
1707-
"owner": "owner",
1708-
"repo": "repo",
1709-
"title": "Test",
1710-
"show_ui": true,
1711-
})
1712-
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
1713-
require.NoError(t, err)
1714-
1715-
textContent := getTextResult(t, result)
1716-
assert.Contains(t, textContent.Text, "interactive form has been shown",
1717-
"show_ui=true should still route through the form")
1718-
})
1719-
1720-
t.Run("UI client with show_ui=false and _ui_submitted=true executes directly", func(t *testing.T) {
1721-
// _ui_submitted and show_ui=false are two ways to say "execute
1722-
// directly". When both are set there must be no conflict — the call
1723-
// still executes directly.
1724-
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
1725-
"method": "create",
1726-
"owner": "owner",
1727-
"repo": "repo",
1728-
"title": "Test",
1729-
"show_ui": false,
1730-
"_ui_submitted": true,
1731-
})
1732-
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
1733-
require.NoError(t, err)
1734-
1735-
textContent := getTextResult(t, result)
1736-
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/issues/1",
1737-
"show_ui=false + _ui_submitted should execute directly")
1738-
})
1739-
1740-
t.Run("non-UI client with show_ui=false executes directly (no regression)", func(t *testing.T) {
1741-
// show_ui is irrelevant when the client does not support UI; the call
1742-
// must execute directly exactly as it does today.
1743-
request := createMCPRequest(map[string]any{
1744-
"method": "create",
1745-
"owner": "owner",
1746-
"repo": "repo",
1747-
"title": "Test",
1748-
"show_ui": false,
1749-
})
1750-
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
1751-
require.NoError(t, err)
1752-
1753-
textContent := getTextResult(t, result)
1754-
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/issues/1",
1755-
"non-UI client should execute directly regardless of show_ui")
1756-
})
17571677
}
17581678

17591679
func Test_issueWriteHasNonFormParams(t *testing.T) {
@@ -1766,8 +1686,6 @@ func Test_issueWriteHasNonFormParams(t *testing.T) {
17661686
}{
17671687
{name: "no params", args: map[string]any{}, want: false},
17681688
{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},
1769-
{name: "show_ui true is a form param", args: map[string]any{"title": "t", "show_ui": true}, want: false},
1770-
{name: "show_ui false is a form param", args: map[string]any{"title": "t", "show_ui": false}, want: false},
17711689
{name: "labels present", args: map[string]any{"title": "t", "labels": []any{"bug"}}, want: false},
17721690
{name: "assignees present", args: map[string]any{"title": "t", "assignees": []any{"octocat"}}, want: false},
17731691
{name: "milestone present", args: map[string]any{"title": "t", "milestone": float64(2)}, want: false},

pkg/github/pullrequests.go

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,6 @@ var pullRequestWriteFormParams = map[string]struct{}{
603603
"draft": {},
604604
"maintainer_can_modify": {},
605605
"reviewers": {},
606-
"show_ui": {},
607606
"_ui_submitted": {},
608607
}
609608

@@ -708,17 +707,6 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
708707
Type: "string",
709708
},
710709
},
711-
// show_ui is hidden from clients that do not advertise MCP App
712-
// UI support. The strip happens per-request in
713-
// inventory.ToolsForRegistration; it is present in the static
714-
// schema (and therefore in toolsnaps and the feature-flag /
715-
// insiders docs) so the UI-capable surface is fully
716-
// documented. It is intentionally not in the main README,
717-
// which renders the stripped (non-UI) schema.
718-
"show_ui": {
719-
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 the user has already confirmed the action and the form would be redundant.",
721-
},
722710
},
723711
Required: []string{"owner", "repo", "title", "head", "base"},
724712
},
@@ -736,17 +724,12 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
736724

737725
// When MCP Apps are enabled and the client supports UI, route the
738726
// call to the interactive form unless:
739-
// - it is itself a form submission (the UI sends _ui_submitted=true),
740-
// - the caller explicitly asked to skip the UI (show_ui=false), or
727+
// - it is itself a form submission (the UI sends _ui_submitted=true), or
741728
// - it carries parameters the form cannot represent. Those must be
742729
// applied directly so their values aren't silently dropped.
743730
uiSubmitted, _ := OptionalParam[bool](args, "_ui_submitted")
744-
showUI, err := OptionalBoolParamWithDefault(args, "show_ui", true)
745-
if err != nil {
746-
return utils.NewToolResultError(err.Error()), nil, nil
747-
}
748731

749-
if deps.IsFeatureEnabled(ctx, MCPAppsFeatureFlag) && clientSupportsUI(ctx, req) && !uiSubmitted && showUI && !pullRequestWriteHasNonFormParams(args) {
732+
if deps.IsFeatureEnabled(ctx, MCPAppsFeatureFlag) && clientSupportsUI(ctx, req) && !uiSubmitted && !pullRequestWriteHasNonFormParams(args) {
750733
return utils.NewToolResultAwaitingFormSubmission(fmt.Sprintf(
751734
"An interactive form has been shown to the user for creating a new pull request in %s/%s. "+
752735
"STOP — do not call any other tools, do not respond as if the pull request was created, "+

pkg/github/pullrequests_test.go

Lines changed: 1 addition & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2686,89 +2686,6 @@ func Test_CreatePullRequest_MCPAppsFeature_UIGate(t *testing.T) {
26862686
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/pull/42",
26872687
"non-form param call should execute directly and return PR URL")
26882688
})
2689-
2690-
t.Run("UI client with show_ui=false skips form and executes directly", func(t *testing.T) {
2691-
// show_ui=false is the explicit, model-facing way to opt out of the
2692-
// form. It must bypass the form even when every other condition would
2693-
// route the call there (UI capability, MCP Apps flag on, no
2694-
// _ui_submitted, only form params present).
2695-
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
2696-
"owner": "owner",
2697-
"repo": "repo",
2698-
"title": "Test PR",
2699-
"head": "feature",
2700-
"base": "main",
2701-
"show_ui": false,
2702-
})
2703-
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
2704-
require.NoError(t, err)
2705-
2706-
textContent := getTextResult(t, result)
2707-
assert.NotContains(t, textContent.Text, "interactive form has been shown",
2708-
"show_ui=false should skip UI form")
2709-
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/pull/42",
2710-
"show_ui=false call should execute directly and return PR URL")
2711-
})
2712-
2713-
t.Run("UI client with show_ui=true returns form message", func(t *testing.T) {
2714-
// show_ui=true must still route through the form and must not be
2715-
// treated as a non-form parameter that would trigger the safety-net
2716-
// bypass.
2717-
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
2718-
"owner": "owner",
2719-
"repo": "repo",
2720-
"title": "Test PR",
2721-
"head": "feature",
2722-
"base": "main",
2723-
"show_ui": true,
2724-
})
2725-
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
2726-
require.NoError(t, err)
2727-
2728-
textContent := getTextResult(t, result)
2729-
assert.Contains(t, textContent.Text, "interactive form has been shown",
2730-
"show_ui=true should still route through the form")
2731-
})
2732-
2733-
t.Run("UI client with show_ui=false and _ui_submitted=true executes directly", func(t *testing.T) {
2734-
// _ui_submitted and show_ui=false are two ways to say "execute
2735-
// directly". When both are set there must be no conflict — the call
2736-
// still executes directly.
2737-
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
2738-
"owner": "owner",
2739-
"repo": "repo",
2740-
"title": "Test PR",
2741-
"head": "feature",
2742-
"base": "main",
2743-
"show_ui": false,
2744-
"_ui_submitted": true,
2745-
})
2746-
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
2747-
require.NoError(t, err)
2748-
2749-
textContent := getTextResult(t, result)
2750-
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/pull/42",
2751-
"show_ui=false + _ui_submitted should execute directly")
2752-
})
2753-
2754-
t.Run("non-UI client with show_ui=false executes directly (no regression)", func(t *testing.T) {
2755-
// show_ui is irrelevant when the client does not support UI; the call
2756-
// must execute directly exactly as it does today.
2757-
request := createMCPRequest(map[string]any{
2758-
"owner": "owner",
2759-
"repo": "repo",
2760-
"title": "Test PR",
2761-
"head": "feature",
2762-
"base": "main",
2763-
"show_ui": false,
2764-
})
2765-
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
2766-
require.NoError(t, err)
2767-
2768-
textContent := getTextResult(t, result)
2769-
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/pull/42",
2770-
"non-UI client should execute directly regardless of show_ui")
2771-
})
27722689
}
27732690

27742691
// Test_UpdatePullRequest_MCPAppsFeature_UIGate verifies the form-routing
@@ -2876,9 +2793,7 @@ func Test_pullRequestWriteHasNonFormParams(t *testing.T) {
28762793
want bool
28772794
}{
28782795
{name: "no params", args: map[string]any{}, want: false},
2879-
{name: "only form params", args: map[string]any{"owner": "o", "repo": "r", "title": "t", "body": "b", "head": "h", "base": "b", "draft": true, "maintainer_can_modify": false, "reviewers": []any{"octocat"}, "show_ui": true, "_ui_submitted": true}, want: false},
2880-
{name: "show_ui true is a form param", args: map[string]any{"title": "t", "show_ui": true}, want: false},
2881-
{name: "show_ui false is a form param", args: map[string]any{"title": "t", "show_ui": false}, want: false},
2796+
{name: "only form params", args: map[string]any{"owner": "o", "repo": "r", "title": "t", "body": "b", "head": "h", "base": "b", "draft": true, "maintainer_can_modify": false, "reviewers": []any{"octocat"}, "_ui_submitted": true}, want: false},
28822797
{name: "unknown param present", args: map[string]any{"title": "t", "unknown_param": "value"}, want: true},
28832798
{name: "nil value is ignored", args: map[string]any{"reviewers": nil}, want: false},
28842799
}

0 commit comments

Comments
 (0)