Skip to content
8 changes: 1 addition & 7 deletions cmd/github-mcp-server/generate_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,6 @@ func writeToolDoc(buf *strings.Builder, tool inventory.ServerTool) {
}
sort.Strings(paramNames)

conditional := inventory.ConditionalSchemaPropertyDescriptions()

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

if cond, isConditional := conditional[propName]; isConditional {
fmt.Fprintf(buf, " - `%s`: %s (%s, %s, conditional — %s)", propName, description, typeStr, requiredStr, cond)
} else {
fmt.Fprintf(buf, " - `%s`: %s (%s, %s)", propName, description, typeStr, requiredStr)
}
fmt.Fprintf(buf, " - `%s`: %s (%s, %s)", propName, description, typeStr, requiredStr)
if i < len(paramNames)-1 {
buf.WriteString("\n")
}
Expand Down
2 changes: 0 additions & 2 deletions docs/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ runtime behavior (such as output formatting) won't appear here.
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `reviewers`: GitHub usernames or ORG/team-slug team reviewers to request reviews from (string[], optional)
- `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)
- `title`: PR title (string, required)

- **get_me** - Get my user profile
Expand All @@ -69,7 +68,6 @@ runtime behavior (such as output formatting) won't appear here.
- `milestone`: Milestone number (number, optional)
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `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)
- `state`: New state (string, optional)
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
- `title`: Issue title (string, optional)
Expand Down
2 changes: 0 additions & 2 deletions docs/insiders-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ The list below is generated from the Go source. It covers tool **inventory and s
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `reviewers`: GitHub usernames or ORG/team-slug team reviewers to request reviews from (string[], optional)
- `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)
- `title`: PR title (string, required)

- **get_me** - Get my user profile
Expand All @@ -63,7 +62,6 @@ The list below is generated from the Go source. It covers tool **inventory and s
- `milestone`: Milestone number (number, optional)
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `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)
- `state`: New state (string, optional)
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
- `title`: Issue title (string, optional)
Expand Down
4 changes: 0 additions & 4 deletions pkg/github/__toolsnaps__/create_pull_request.snap
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@
},
"type": "array"
},
"show_ui": {
"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.",
"type": "boolean"
},
"title": {
"description": "PR title",
"type": "string"
Expand Down
4 changes: 0 additions & 4 deletions pkg/github/__toolsnaps__/issue_write.snap
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,6 @@
"description": "Repository name",
"type": "string"
},
"show_ui": {
"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.",
"type": "boolean"
},
"state": {
"description": "New state",
"enum": [
Expand Down
53 changes: 8 additions & 45 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -1726,7 +1726,11 @@ const IssueWriteUIResourceURI = "ui://github-mcp-server/issue-write"

// issueWriteFormParams are the parameters the issue_write MCP App form collects
// and re-sends on submit. Any other parameter present on a call cannot be
// represented by the form.
// represented by the form. The form collects (and prefills) every parameter in
// the tool's current input schema, so hasNonFormParams against this set is a
// forward-compatibility safety net: a parameter added to the schema in the
// future but not yet wired into the form trips the check and bypasses the form
// so the supplied value isn't silently dropped.
var issueWriteFormParams = map[string]struct{}{
"method": {},
"owner": {},
Expand All @@ -1742,28 +1746,9 @@ var issueWriteFormParams = map[string]struct{}{
"state": {},
"state_reason": {},
"duplicate_of": {},
"show_ui": {},
"_ui_submitted": {},
}

// issueWriteHasNonFormParams reports whether the call carries any parameter the
// issue_write MCP App form cannot represent (anything outside issueWriteFormParams).
// The form collects (and prefills) every parameter in the tool's current input
// schema, so this is a forward-compatibility safety net: a parameter added to the
// schema in the future but not yet wired into the form trips this check and bypasses
// the UI so the supplied value isn't silently dropped.
func issueWriteHasNonFormParams(args map[string]any) bool {
for key, value := range args {
if value == nil {
continue
}
if _, ok := issueWriteFormParams[key]; !ok {
return true
}
}
return false
}

// issueWriteAwaitingFormResult builds the "awaiting form submission" stub
// returned when issue_write hands off to the MCP App form. The body is shared
// by IssueWrite and LegacyIssueWrite. The result is marked IsError=true so
Expand Down Expand Up @@ -1919,17 +1904,6 @@ Options are:
Required: []string{"field_name"},
},
},
// show_ui is hidden from clients that do not advertise MCP App
// UI support. The strip happens per-request in
// inventory.ToolsForRegistration; it is present in the static
// schema (and therefore in toolsnaps and the feature-flag /
// insiders docs) so the UI-capable surface is fully
// documented. It is intentionally not in the main README,
// which renders the stripped (non-UI) schema.
"show_ui": {
Type: "boolean",
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.",
},
},
Required: []string{"method", "owner", "repo"},
},
Expand All @@ -1950,20 +1924,9 @@ Options are:
return utils.NewToolResultError(err.Error()), nil, nil
}

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

if deps.IsFeatureEnabled(ctx, MCPAppsFeatureFlag) && clientSupportsUI(ctx, req) && !uiSubmitted && showUI && !issueWriteHasNonFormParams(args) {
// Hand off to the interactive MCP App form unless this call must
// execute now (see shouldDeferToForm).
if shouldDeferToForm(ctx, deps, req, args, issueWriteFormParams) {
issueNumber := 0
if method == "update" {
n, numErr := RequiredInt(args, "issue_number")
Expand Down
86 changes: 2 additions & 84 deletions pkg/github/issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1674,86 +1674,6 @@ func Test_IssueWrite_MCPAppsFeature_UIGate(t *testing.T) {
"labels should route through UI form")
assert.True(t, result.IsError, "form-routing stub should be marked IsError so agents don't claim success")
})

t.Run("UI client with show_ui=false skips form and executes directly", func(t *testing.T) {
// show_ui=false is the explicit, model-facing way to opt out of the
// form. It must bypass the form even when every other condition would
// route the call there (UI capability, MCP Apps flag on, no
// _ui_submitted, only form params present).
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
"method": "create",
"owner": "owner",
"repo": "repo",
"title": "Test",
"show_ui": false,
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)

textContent := getTextResult(t, result)
assert.NotContains(t, textContent.Text, "interactive form has been shown",
"show_ui=false should skip UI form")
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/issues/1",
"show_ui=false call should execute directly and return issue URL")
})

t.Run("UI client with show_ui=true returns form message", func(t *testing.T) {
// show_ui=true is the explicit, redundant-with-the-default way to ask
// for the form. It must still route through the form and must not be
// treated as a non-form parameter that would trigger the safety-net
// bypass.
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
"method": "create",
"owner": "owner",
"repo": "repo",
"title": "Test",
"show_ui": true,
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)

textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, "interactive form has been shown",
"show_ui=true should still route through the form")
})

t.Run("UI client with show_ui=false and _ui_submitted=true executes directly", func(t *testing.T) {
// _ui_submitted and show_ui=false are two ways to say "execute
// directly". When both are set there must be no conflict — the call
// still executes directly.
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
"method": "create",
"owner": "owner",
"repo": "repo",
"title": "Test",
"show_ui": false,
"_ui_submitted": true,
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)

textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/issues/1",
"show_ui=false + _ui_submitted should execute directly")
})

t.Run("non-UI client with show_ui=false executes directly (no regression)", func(t *testing.T) {
// show_ui is irrelevant when the client does not support UI; the call
// must execute directly exactly as it does today.
request := createMCPRequest(map[string]any{
"method": "create",
"owner": "owner",
"repo": "repo",
"title": "Test",
"show_ui": false,
})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)

textContent := getTextResult(t, result)
assert.Contains(t, textContent.Text, "https://github.com/owner/repo/issues/1",
"non-UI client should execute directly regardless of show_ui")
})
}

func Test_issueWriteHasNonFormParams(t *testing.T) {
Expand All @@ -1766,8 +1686,6 @@ func Test_issueWriteHasNonFormParams(t *testing.T) {
}{
{name: "no params", args: map[string]any{}, want: false},
{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},
{name: "show_ui true is a form param", args: map[string]any{"title": "t", "show_ui": true}, want: false},
{name: "show_ui false is a form param", args: map[string]any{"title": "t", "show_ui": false}, want: false},
{name: "labels present", args: map[string]any{"title": "t", "labels": []any{"bug"}}, want: false},
{name: "assignees present", args: map[string]any{"title": "t", "assignees": []any{"octocat"}}, want: false},
{name: "milestone present", args: map[string]any{"title": "t", "milestone": float64(2)}, want: false},
Expand All @@ -1783,7 +1701,7 @@ func Test_issueWriteHasNonFormParams(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tc.want, issueWriteHasNonFormParams(tc.args))
assert.Equal(t, tc.want, hasNonFormParams(tc.args, issueWriteFormParams))
})
}
}
Expand All @@ -1797,7 +1715,7 @@ func Test_issueWriteSchemaClassification(t *testing.T) {
t.Parallel()

// Schema properties the MCP App form cannot represent — their presence
// must trigger the safety-net bypass via issueWriteHasNonFormParams. The
// must trigger the safety-net bypass via hasNonFormParams. The
// form currently collects every schema property, so this allowlist is
// empty; add a property here only if it is added to the schema without
// corresponding form support.
Expand Down
61 changes: 6 additions & 55 deletions pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,6 @@ var pullRequestWriteFormParams = map[string]struct{}{
"draft": {},
"maintainer_can_modify": {},
"reviewers": {},
"show_ui": {},
"_ui_submitted": {},
}

Expand All @@ -621,34 +620,6 @@ var pullRequestUpdateFormParams = map[string]struct{}{
"_ui_submitted": {},
}

// pullRequestWriteHasNonFormParams reports whether the call carries any parameter
// the create_pull_request MCP App form cannot represent (anything outside
// pullRequestWriteFormParams). Such calls must bypass the UI form and execute
// directly so the supplied values aren't silently dropped.
func pullRequestWriteHasNonFormParams(args map[string]any) bool {
for key, value := range args {
if value == nil {
continue
}
if _, ok := pullRequestWriteFormParams[key]; !ok {
return true
}
}
return false
}

func pullRequestUpdateHasNonFormParams(args map[string]any) bool {
for key, value := range args {
if value == nil {
continue
}
if _, ok := pullRequestUpdateFormParams[key]; !ok {
return true
}
}
return false
}

// CreatePullRequest creates a tool to create a new pull request.
func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerTool {
return NewTool(
Expand Down Expand Up @@ -708,17 +679,6 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
Type: "string",
},
},
// show_ui is hidden from clients that do not advertise MCP App
// UI support. The strip happens per-request in
// inventory.ToolsForRegistration; it is present in the static
// schema (and therefore in toolsnaps and the feature-flag /
// insiders docs) so the UI-capable surface is fully
// documented. It is intentionally not in the main README,
// which renders the stripped (non-UI) schema.
"show_ui": {
Type: "boolean",
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.",
},
},
Required: []string{"owner", "repo", "title", "head", "base"},
},
Expand All @@ -734,19 +694,9 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
return utils.NewToolResultError(err.Error()), nil, nil
}

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

if deps.IsFeatureEnabled(ctx, MCPAppsFeatureFlag) && clientSupportsUI(ctx, req) && !uiSubmitted && showUI && !pullRequestWriteHasNonFormParams(args) {
// Hand off to the interactive MCP App form unless this call must
// execute now (see shouldDeferToForm).
if shouldDeferToForm(ctx, deps, req, args, pullRequestWriteFormParams) {
return utils.NewToolResultAwaitingFormSubmission(fmt.Sprintf(
"An interactive form has been shown to the user for creating a new pull request in %s/%s. "+
"STOP — do not call any other tools, do not respond as if the pull request was created, "+
Expand Down Expand Up @@ -965,8 +915,9 @@ func UpdatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
return utils.NewToolResultError(err.Error()), nil, nil
}

uiSubmitted, _ := OptionalParam[bool](args, "_ui_submitted")
if deps.IsFeatureEnabled(ctx, MCPAppsFeatureFlag) && clientSupportsUI(ctx, req) && !uiSubmitted && !pullRequestUpdateHasNonFormParams(args) {
// Hand off to the interactive MCP App form unless this call must
// execute now (see shouldDeferToForm).
if shouldDeferToForm(ctx, deps, req, args, pullRequestUpdateFormParams) {
return utils.NewToolResultAwaitingFormSubmission(fmt.Sprintf(
"An interactive form has been shown to the user for editing pull request #%d in %s/%s. "+
"STOP — do not call any other tools, do not respond as if the pull request was updated, "+
Expand Down
Loading
Loading