Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,17 @@ See [Insiders Features](./insiders-features.md) for a full list of what's availa

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

To keep MCP App result views enabled while making write tools execute directly
instead of first opening an interactive form, also enable the
`mcp_apps_disable_form_deferral` feature flag. For the remote server, send both
flags in the request header:

```http
X-MCP-Features: remote_mcp_ui_apps,mcp_apps_disable_form_deferral
```

For the local server, pass both flags to `--features`.

**Supported tools:**

| Tool | Description |
Expand Down
5 changes: 5 additions & 0 deletions pkg/github/feature_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import "slices"
// MCPAppsFeatureFlag is the feature flag name for MCP Apps (interactive UI forms).
const MCPAppsFeatureFlag = "remote_mcp_ui_apps"

// MCPAppsDisableFormDeferralFeatureFlag disables handing write-tool calls off
// to MCP App forms while preserving MCP Apps UI metadata and result views.
const MCPAppsDisableFormDeferralFeatureFlag = "mcp_apps_disable_form_deferral"

// FeatureFlagCSVOutput is the feature flag name for CSV output on list tools.
const FeatureFlagCSVOutput = "csv_output"

Expand Down Expand Up @@ -37,6 +41,7 @@ const FeatureFlagFieldsParam = "fields_param"
// This is the single source of truth for which flags are user-controllable.
var AllowedFeatureFlags = []string{
MCPAppsFeatureFlag,
MCPAppsDisableFormDeferralFeatureFlag,
FeatureFlagCSVOutput,
FeatureFlagIFCLabels,
FeatureFlagIssuesGranular,
Expand Down
11 changes: 11 additions & 0 deletions pkg/github/feature_flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ func TestResolveFeatureFlags(t *testing.T) {
enabledFeatures: []string{MCPAppsFeatureFlag},
expectedFlags: []string{MCPAppsFeatureFlag},
},
{
name: "MCP Apps form deferral can be disabled directly",
enabledFeatures: []string{MCPAppsDisableFormDeferralFeatureFlag},
expectedFlags: []string{MCPAppsDisableFormDeferralFeatureFlag},
},
{
name: "fields param is not enabled by default",
enabledFeatures: nil,
Expand Down Expand Up @@ -183,6 +188,12 @@ func TestResolveFeatureFlags(t *testing.T) {
insidersMode: true,
unexpectedFlags: []string{FeatureFlagIFCLabels},
},
{
name: "insiders mode does not disable MCP Apps form deferral",
enabledFeatures: nil,
insidersMode: true,
unexpectedFlags: []string{MCPAppsDisableFormDeferralFeatureFlag},
},
{
name: "ifc_labels can be directly enabled",
enabledFeatures: []string{FeatureFlagIFCLabels},
Expand Down
12 changes: 7 additions & 5 deletions pkg/github/ui_capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,15 @@ func hasNonFormParams(args map[string]any, formParams map[string]struct{}) bool
// shared by the form-backed write tools (create_pull_request,
// update_pull_request, issue_write). It reports whether a call should be handed
// off to its MCP App form instead of executing now: defer only when MCP Apps
// are enabled, the client can render UI, the call is not itself a form
// submission, and every supplied parameter can be represented by the form
// (formParams is the tool's form-parameter allowlist). When it returns false
// the handler executes directly; the host may still render the tool's view,
// which renders the result rather than an input form.
// are enabled, form deferral has not been disabled, the client can render UI,
// the call is not itself a form submission, and every supplied parameter can
// be represented by the form (formParams is the tool's form-parameter
// allowlist). When it returns false the handler executes directly; the host may
// still render the tool's view, which renders the result rather than an input
// form.
func shouldDeferToForm(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest, args map[string]any, formParams map[string]struct{}) bool {
return deps.IsFeatureEnabled(ctx, MCPAppsFeatureFlag) &&
!deps.IsFeatureEnabled(ctx, MCPAppsDisableFormDeferralFeatureFlag) &&
clientSupportsUI(ctx, req) &&
!uiSubmitted(args) &&
!hasNonFormParams(args, formParams)
Expand Down
45 changes: 45 additions & 0 deletions pkg/github/ui_capability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,48 @@ func Test_clientSupportsUI_fromContext(t *testing.T) {
assert.False(t, clientSupportsUI(context.Background(), nil))
})
}

func Test_shouldDeferToForm_featureFlags(t *testing.T) {
t.Parallel()

ctx := ghcontext.WithUISupport(context.Background(), true)
args := map[string]any{"owner": "octocat"}
formParams := map[string]struct{}{"owner": {}}

tests := []struct {
name string
enabledFlags []string
want bool
}{
{
name: "MCP Apps enabled defers to form",
enabledFlags: []string{MCPAppsFeatureFlag},
want: true,
},
{
name: "form deferral disabled executes directly",
enabledFlags: []string{
MCPAppsFeatureFlag,
MCPAppsDisableFormDeferralFeatureFlag,
},
want: false,
},
{
name: "form deferral opt-out does not enable MCP Apps",
enabledFlags: []string{MCPAppsDisableFormDeferralFeatureFlag},
want: false,
},
{
name: "MCP Apps disabled executes directly",
want: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
deps := BaseDeps{featureChecker: featureCheckerFor(tc.enabledFlags...)}
assert.Equal(t, tc.want, shouldDeferToForm(ctx, deps, nil, args, formParams))
})
}
}
12 changes: 12 additions & 0 deletions pkg/http/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ func TestCreateHTTPFeatureChecker(t *testing.T) {
headerFeatures: []string{github.MCPAppsFeatureFlag},
wantEnabled: true,
},
{
name: "MCP Apps form deferral opt-out accepted from header",
flagName: github.MCPAppsDisableFormDeferralFeatureFlag,
headerFeatures: []string{github.MCPAppsDisableFormDeferralFeatureFlag},
wantEnabled: true,
},
{
name: "unknown flag in header is ignored",
flagName: "unknown_flag",
Expand Down Expand Up @@ -74,6 +80,12 @@ func TestCreateHTTPFeatureChecker(t *testing.T) {
insidersMode: true,
wantEnabled: true,
},
{
name: "insiders mode does not disable MCP Apps form deferral",
flagName: github.MCPAppsDisableFormDeferralFeatureFlag,
insidersMode: true,
wantEnabled: false,
},
{
name: "static feature is enabled without header",
staticFeatures: []string{github.FeatureFlagCSVOutput},
Expand Down
Loading