Skip to content

Commit a217a7f

Browse files
connor4312Copilot
authored andcommitted
Add MCP App form deferral opt-out
Allow clients to keep MCP App views enabled while making form-backed write tools execute directly when explicitly configured. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b8bfb49 commit a217a7f

6 files changed

Lines changed: 91 additions & 5 deletions

File tree

docs/server-configuration.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,17 @@ See [Insiders Features](./insiders-features.md) for a full list of what's availa
396396

397397
MCP Apps is enabled by [Insiders Mode](#insiders-mode), or independently via the `remote_mcp_ui_apps` feature flag.
398398

399+
To keep MCP App result views enabled while making write tools execute directly
400+
instead of first opening an interactive form, also enable the
401+
`mcp_apps_disable_form_deferral` feature flag. For the remote server, send both
402+
flags in the request header:
403+
404+
```http
405+
X-MCP-Features: remote_mcp_ui_apps,mcp_apps_disable_form_deferral
406+
```
407+
408+
For the local server, pass both flags to `--features`.
409+
399410
**Supported tools:**
400411

401412
| Tool | Description |

pkg/github/feature_flags.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import "slices"
55
// MCPAppsFeatureFlag is the feature flag name for MCP Apps (interactive UI forms).
66
const MCPAppsFeatureFlag = "remote_mcp_ui_apps"
77

8+
// MCPAppsDisableFormDeferralFeatureFlag disables handing write-tool calls off
9+
// to MCP App forms while preserving MCP Apps UI metadata and result views.
10+
const MCPAppsDisableFormDeferralFeatureFlag = "mcp_apps_disable_form_deferral"
11+
812
// FeatureFlagCSVOutput is the feature flag name for CSV output on list tools.
913
const FeatureFlagCSVOutput = "csv_output"
1014

@@ -37,6 +41,7 @@ const FeatureFlagFieldsParam = "fields_param"
3741
// This is the single source of truth for which flags are user-controllable.
3842
var AllowedFeatureFlags = []string{
3943
MCPAppsFeatureFlag,
44+
MCPAppsDisableFormDeferralFeatureFlag,
4045
FeatureFlagCSVOutput,
4146
FeatureFlagIFCLabels,
4247
FeatureFlagIssuesGranular,

pkg/github/feature_flags_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@ func TestResolveFeatureFlags(t *testing.T) {
155155
enabledFeatures: []string{MCPAppsFeatureFlag},
156156
expectedFlags: []string{MCPAppsFeatureFlag},
157157
},
158+
{
159+
name: "MCP Apps form deferral can be disabled directly",
160+
enabledFeatures: []string{MCPAppsDisableFormDeferralFeatureFlag},
161+
expectedFlags: []string{MCPAppsDisableFormDeferralFeatureFlag},
162+
},
158163
{
159164
name: "fields param is not enabled by default",
160165
enabledFeatures: nil,
@@ -183,6 +188,12 @@ func TestResolveFeatureFlags(t *testing.T) {
183188
insidersMode: true,
184189
unexpectedFlags: []string{FeatureFlagIFCLabels},
185190
},
191+
{
192+
name: "insiders mode does not disable MCP Apps form deferral",
193+
enabledFeatures: nil,
194+
insidersMode: true,
195+
unexpectedFlags: []string{MCPAppsDisableFormDeferralFeatureFlag},
196+
},
186197
{
187198
name: "ifc_labels can be directly enabled",
188199
enabledFeatures: []string{FeatureFlagIFCLabels},

pkg/github/ui_capability.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,15 @@ func hasNonFormParams(args map[string]any, formParams map[string]struct{}) bool
6363
// shared by the form-backed write tools (create_pull_request,
6464
// update_pull_request, issue_write). It reports whether a call should be handed
6565
// off to its MCP App form instead of executing now: defer only when MCP Apps
66-
// are enabled, the client can render UI, the call is not itself a form
67-
// submission, and every supplied parameter can be represented by the form
68-
// (formParams is the tool's form-parameter allowlist). When it returns false
69-
// the handler executes directly; the host may still render the tool's view,
70-
// which renders the result rather than an input form.
66+
// are enabled, form deferral has not been disabled, the client can render UI,
67+
// the call is not itself a form submission, and every supplied parameter can
68+
// be represented by the form (formParams is the tool's form-parameter
69+
// allowlist). When it returns false the handler executes directly; the host may
70+
// still render the tool's view, which renders the result rather than an input
71+
// form.
7172
func shouldDeferToForm(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest, args map[string]any, formParams map[string]struct{}) bool {
7273
return deps.IsFeatureEnabled(ctx, MCPAppsFeatureFlag) &&
74+
!deps.IsFeatureEnabled(ctx, MCPAppsDisableFormDeferralFeatureFlag) &&
7375
clientSupportsUI(ctx, req) &&
7476
!uiSubmitted(args) &&
7577
!hasNonFormParams(args, formParams)

pkg/github/ui_capability_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,48 @@ func Test_clientSupportsUI_fromContext(t *testing.T) {
8585
assert.False(t, clientSupportsUI(context.Background(), nil))
8686
})
8787
}
88+
89+
func Test_shouldDeferToForm_featureFlags(t *testing.T) {
90+
t.Parallel()
91+
92+
ctx := ghcontext.WithUISupport(context.Background(), true)
93+
args := map[string]any{"owner": "octocat"}
94+
formParams := map[string]struct{}{"owner": {}}
95+
96+
tests := []struct {
97+
name string
98+
enabledFlags []string
99+
want bool
100+
}{
101+
{
102+
name: "MCP Apps enabled defers to form",
103+
enabledFlags: []string{MCPAppsFeatureFlag},
104+
want: true,
105+
},
106+
{
107+
name: "form deferral disabled executes directly",
108+
enabledFlags: []string{
109+
MCPAppsFeatureFlag,
110+
MCPAppsDisableFormDeferralFeatureFlag,
111+
},
112+
want: false,
113+
},
114+
{
115+
name: "form deferral opt-out does not enable MCP Apps",
116+
enabledFlags: []string{MCPAppsDisableFormDeferralFeatureFlag},
117+
want: false,
118+
},
119+
{
120+
name: "MCP Apps disabled executes directly",
121+
want: false,
122+
},
123+
}
124+
125+
for _, tc := range tests {
126+
t.Run(tc.name, func(t *testing.T) {
127+
t.Parallel()
128+
deps := BaseDeps{featureChecker: featureCheckerFor(tc.enabledFlags...)}
129+
assert.Equal(t, tc.want, shouldDeferToForm(ctx, deps, nil, args, formParams))
130+
})
131+
}
132+
}

pkg/http/server_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ func TestCreateHTTPFeatureChecker(t *testing.T) {
3838
headerFeatures: []string{github.MCPAppsFeatureFlag},
3939
wantEnabled: true,
4040
},
41+
{
42+
name: "MCP Apps form deferral opt-out accepted from header",
43+
flagName: github.MCPAppsDisableFormDeferralFeatureFlag,
44+
headerFeatures: []string{github.MCPAppsDisableFormDeferralFeatureFlag},
45+
wantEnabled: true,
46+
},
4147
{
4248
name: "unknown flag in header is ignored",
4349
flagName: "unknown_flag",
@@ -74,6 +80,12 @@ func TestCreateHTTPFeatureChecker(t *testing.T) {
7480
insidersMode: true,
7581
wantEnabled: true,
7682
},
83+
{
84+
name: "insiders mode does not disable MCP Apps form deferral",
85+
flagName: github.MCPAppsDisableFormDeferralFeatureFlag,
86+
insidersMode: true,
87+
wantEnabled: false,
88+
},
7789
{
7890
name: "static feature is enabled without header",
7991
staticFeatures: []string{github.FeatureFlagCSVOutput},

0 commit comments

Comments
 (0)