Skip to content

Commit 32fb743

Browse files
committed
migrate mcp apps support from insiders mode to feature flag with insiders opt-in
1 parent 15315b9 commit 32fb743

File tree

12 files changed

+242
-95
lines changed

12 files changed

+242
-95
lines changed

docs/insiders-features.md

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,4 @@ For configuration examples, see the [Server Configuration Guide](./server-config
2020

2121
---
2222

23-
## MCP Apps
24-
25-
[MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps) is an extension to the Model Context Protocol that enables servers to deliver interactive user interfaces to end users. Instead of returning plain text that the LLM must interpret and relay, tools can render forms, profiles, and dashboards right in the chat using MCP Apps.
26-
27-
This means you can interact with GitHub visually: fill out forms to create issues, see user profiles with avatars, open pull requests — all without leaving your agent chat.
28-
29-
### Supported tools
30-
31-
The following tools have MCP Apps UIs:
32-
33-
| Tool | Description |
34-
|------|-------------|
35-
| `get_me` | Displays your GitHub user profile with avatar, bio, and stats in a rich card |
36-
| `issue_write` | Opens an interactive form to create or update issues |
37-
| `create_pull_request` | Provides a full PR creation form to create a pull request (or a draft pull request) |
38-
39-
### Client requirements
40-
41-
MCP Apps requires a host that supports the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps). Currently tested and working with:
42-
43-
- **VS Code Insiders** — enable via the `chat.mcp.apps.enabled` setting
44-
- **Visual Studio Code** — enable via the `chat.mcp.apps.enabled` setting
23+
_There are currently no insiders-only features. [MCP Apps](./server-configuration.md#mcp-apps) has graduated to a feature flag (`remote_mcp_ui_apps`) and can be enabled independently._

docs/server-configuration.md

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ We currently support the following ways in which the GitHub MCP Server can be co
1414
| Dynamic Mode | Not available | `--dynamic-toolsets` flag or `GITHUB_DYNAMIC_TOOLSETS` env var |
1515
| Lockdown Mode | `X-MCP-Lockdown` header | `--lockdown-mode` flag or `GITHUB_LOCKDOWN_MODE` env var |
1616
| Insiders Mode | `X-MCP-Insiders` header or `/insiders` URL | `--insiders` flag or `GITHUB_INSIDERS` env var |
17+
| Feature Flags | `X-MCP-Features` header | `--features` flag |
1718
| Scope Filtering | Always enabled | Always enabled |
1819
| Server Name/Title | Not available | `GITHUB_MCP_SERVER_NAME` / `GITHUB_MCP_SERVER_TITLE` env vars or `github-mcp-server-config.json` |
1920

@@ -390,7 +391,10 @@ Lockdown mode ensures the server only surfaces content in public repositories fr
390391

391392
**Best for:** Users who want early access to experimental features and new tools before they reach general availability.
392393

393-
Insiders Mode unlocks experimental features, such as [MCP Apps](./insiders-features.md#mcp-apps) support. We created this mode to have a way to roll out experimental features and collect feedback. So if you are using Insiders, please don't hesitate to share your feedback with us! Features in Insiders Mode may change, evolve, or be removed based on user feedback.
394+
Insiders Mode unlocks experimental features. We created this mode to have a way to roll out experimental features and collect feedback. So if you are using Insiders, please don't hesitate to share your feedback with us! Features in Insiders Mode may change, evolve, or be removed based on user feedback.
395+
396+
> [!NOTE]
397+
> Insiders mode also enables the `remote_mcp_ui_apps` feature flag for backward compatibility. See [MCP Apps](#mcp-apps) below.
394398
395399
<table>
396400
<tr><th>Remote Server</th><th>Local Server</th></tr>
@@ -443,6 +447,62 @@ See [Insiders Features](./insiders-features.md) for a full list of what's availa
443447

444448
---
445449

450+
### MCP Apps
451+
452+
[MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps) is an extension to the Model Context Protocol that enables servers to deliver interactive user interfaces to end users. Instead of returning plain text that the LLM must interpret and relay, tools can render forms, profiles, and dashboards right in the chat.
453+
454+
MCP Apps is controlled by the `remote_mcp_ui_apps` feature flag and can be enabled independently of insiders mode.
455+
456+
**Supported tools:**
457+
458+
| Tool | Description |
459+
|------|-------------|
460+
| `get_me` | Displays your GitHub user profile with avatar, bio, and stats in a rich card |
461+
| `issue_write` | Opens an interactive form to create or update issues |
462+
| `create_pull_request` | Provides a full PR creation form to create a pull request (or a draft pull request) |
463+
464+
**Client requirements:** MCP Apps requires a host that supports the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps). Currently tested with VS Code (`chat.mcp.apps.enabled` setting).
465+
466+
<table>
467+
<tr><th>Remote Server</th><th>Local Server</th></tr>
468+
<tr valign="top">
469+
<td>
470+
471+
```json
472+
{
473+
"type": "http",
474+
"url": "https://api.githubcopilot.com/mcp/",
475+
"headers": {
476+
"X-MCP-Features": "remote_mcp_ui_apps"
477+
}
478+
}
479+
```
480+
481+
</td>
482+
<td>
483+
484+
```json
485+
{
486+
"type": "stdio",
487+
"command": "go",
488+
"args": [
489+
"run",
490+
"./cmd/github-mcp-server",
491+
"stdio",
492+
"--features=remote_mcp_ui_apps"
493+
],
494+
"env": {
495+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}"
496+
}
497+
}
498+
```
499+
500+
</td>
501+
</tr>
502+
</table>
503+
504+
---
505+
446506
### Scope Filtering
447507

448508
**Automatic feature:** The server handles OAuth scopes differently depending on authentication type:

internal/ghmcp/server.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http"
99
"os"
1010
"os/signal"
11+
"slices"
1112
"strings"
1213
"syscall"
1314
"time"
@@ -114,8 +115,13 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
114115
return nil, fmt.Errorf("failed to create GitHub clients: %w", err)
115116
}
116117

117-
// Create feature checker
118-
featureChecker := createFeatureChecker(cfg.EnabledFeatures)
118+
// Create feature checker — insiders mode transitionally enables remote_mcp_ui_apps
119+
enabledFeatures := cfg.EnabledFeatures
120+
if cfg.InsidersMode && !slices.Contains(enabledFeatures, github.MCPAppsFeatureFlag) {
121+
enabledFeatures = append(slices.Clone(enabledFeatures), github.MCPAppsFeatureFlag)
122+
}
123+
featureChecker := createFeatureChecker(enabledFeatures)
124+
mcpAppsEnabled := slices.Contains(enabledFeatures, github.MCPAppsFeatureFlag)
119125

120126
// Create dependencies for tool handlers
121127
obs, err := observability.NewExporters(cfg.Logger, metrics.NewNoopMetrics())
@@ -145,7 +151,8 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
145151
WithExcludeTools(cfg.ExcludeTools).
146152
WithServerInstructions().
147153
WithFeatureChecker(featureChecker).
148-
WithInsidersMode(cfg.InsidersMode)
154+
WithInsidersMode(cfg.InsidersMode).
155+
WithMCPApps(mcpAppsEnabled)
149156

150157
// Apply token scope filtering if scopes are known (for PAT filtering)
151158
if cfg.TokenScopes != nil {
@@ -162,10 +169,11 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
162169
return nil, fmt.Errorf("failed to create GitHub MCP server: %w", err)
163170
}
164171

165-
// Register MCP App UI resources if available (requires running script/build-ui).
166-
// We check availability to allow Insiders mode to work for non-UI features
167-
// even when UI assets haven't been built.
168-
if cfg.InsidersMode && github.UIAssetsAvailable() {
172+
// Register MCP App UI resources if the remote_mcp_ui_apps feature flag is enabled
173+
// and UI assets are available (requires running script/build-ui).
174+
// We check availability to allow the feature flag to be enabled without
175+
// requiring a UI build (graceful degradation).
176+
if mcpAppsEnabled && github.UIAssetsAvailable() {
169177
github.RegisterUIResources(ghServer)
170178
}
171179

pkg/github/feature_flags.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
package github
22

3+
// MCPAppsFeatureFlag is the feature flag name that enables MCP Apps
4+
// (interactive UI forms) for supported tools. When enabled, tools like
5+
// get_me, issue_write, and create_pull_request can render rich UI via
6+
// the MCP Apps extension instead of plain text responses.
7+
const MCPAppsFeatureFlag = "remote_mcp_ui_apps"
8+
39
// FeatureFlags defines runtime feature toggles that adjust tool behavior.
410
type FeatureFlags struct {
511
LockdownMode bool

pkg/github/issues.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,12 +1073,12 @@ Options are:
10731073
return utils.NewToolResultError(err.Error()), nil, nil
10741074
}
10751075

1076-
// When insiders mode is enabled and the client supports MCP Apps UI,
1076+
// When the MCP Apps feature flag is enabled and the client supports MCP Apps UI,
10771077
// check if this is a UI form submission. The UI sends _ui_submitted=true
10781078
// to distinguish form submissions from LLM calls.
10791079
uiSubmitted, _ := OptionalParam[bool](args, "_ui_submitted")
10801080

1081-
if deps.GetFlags(ctx).InsidersMode && clientSupportsUI(ctx, req) && !uiSubmitted {
1081+
if deps.IsFeatureEnabled(ctx, MCPAppsFeatureFlag) && clientSupportsUI(ctx, req) && !uiSubmitted {
10821082
if method == "update" {
10831083
// Skip the UI form when a state change is requested because
10841084
// the form only handles title/body editing and would lose the

pkg/github/issues_test.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -932,9 +932,9 @@ func Test_CreateIssue(t *testing.T) {
932932
}
933933
}
934934

935-
// Test_IssueWrite_InsidersMode_UIGate verifies the insiders mode UI gate
935+
// Test_IssueWrite_MCPApps_UIGate verifies the MCP Apps feature flag UI gate
936936
// behavior: UI clients get a form message, non-UI clients execute directly.
937-
func Test_IssueWrite_InsidersMode_UIGate(t *testing.T) {
937+
func Test_IssueWrite_MCPApps_UIGate(t *testing.T) {
938938
t.Parallel()
939939

940940
mockIssue := &github.Issue{
@@ -949,11 +949,17 @@ func Test_IssueWrite_InsidersMode_UIGate(t *testing.T) {
949949
PostReposIssuesByOwnerByRepo: mockResponse(t, http.StatusCreated, mockIssue),
950950
}))
951951

952-
deps := BaseDeps{
953-
Client: client,
954-
GQLClient: githubv4.NewClient(nil),
955-
Flags: FeatureFlags{InsidersMode: true},
952+
mcpAppsChecker := func(_ context.Context, flag string) (bool, error) {
953+
return flag == MCPAppsFeatureFlag, nil
956954
}
955+
deps := NewBaseDeps(
956+
client, githubv4.NewClient(nil), nil, nil,
957+
translations.NullTranslationHelper,
958+
FeatureFlags{},
959+
0,
960+
mcpAppsChecker,
961+
stubExporters(),
962+
)
957963
handler := serverTool.Handler(deps)
958964

959965
t.Run("UI client without _ui_submitted returns form message", func(t *testing.T) {
@@ -1066,11 +1072,14 @@ func Test_IssueWrite_InsidersMode_UIGate(t *testing.T) {
10661072
),
10671073
))
10681074

1069-
closeDeps := BaseDeps{
1070-
Client: closeClient,
1071-
GQLClient: closeGQLClient,
1072-
Flags: FeatureFlags{InsidersMode: true},
1073-
}
1075+
closeDeps := NewBaseDeps(
1076+
closeClient, closeGQLClient, nil, nil,
1077+
translations.NullTranslationHelper,
1078+
FeatureFlags{},
1079+
0,
1080+
mcpAppsChecker,
1081+
stubExporters(),
1082+
)
10741083
closeHandler := serverTool.Handler(closeDeps)
10751084

10761085
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{

pkg/github/pullrequests.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -601,12 +601,12 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
601601
return utils.NewToolResultError(err.Error()), nil, nil
602602
}
603603

604-
// When insiders mode is enabled and the client supports MCP Apps UI,
604+
// When the MCP Apps feature flag is enabled and the client supports MCP Apps UI,
605605
// check if this is a UI form submission. The UI sends _ui_submitted=true
606606
// to distinguish form submissions from LLM calls.
607607
uiSubmitted, _ := OptionalParam[bool](args, "_ui_submitted")
608608

609-
if deps.GetFlags(ctx).InsidersMode && clientSupportsUI(ctx, req) && !uiSubmitted {
609+
if deps.IsFeatureEnabled(ctx, MCPAppsFeatureFlag) && clientSupportsUI(ctx, req) && !uiSubmitted {
610610
return utils.NewToolResultText(fmt.Sprintf("Ready to create a pull request in %s/%s. IMPORTANT: The PR has NOT been created yet. Do NOT tell the user the PR was created. The user MUST click Submit in the form to create it.", owner, repo)), nil, nil
611611
}
612612

pkg/github/pullrequests_test.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2312,9 +2312,9 @@ func Test_CreatePullRequest(t *testing.T) {
23122312
}
23132313
}
23142314

2315-
// Test_CreatePullRequest_InsidersMode_UIGate verifies the insiders mode UI gate
2315+
// Test_CreatePullRequest_MCPApps_UIGate verifies the MCP Apps feature flag UI gate
23162316
// behavior: UI clients get a form message, non-UI clients execute directly.
2317-
func Test_CreatePullRequest_InsidersMode_UIGate(t *testing.T) {
2317+
func Test_CreatePullRequest_MCPApps_UIGate(t *testing.T) {
23182318
t.Parallel()
23192319

23202320
mockPR := &github.PullRequest{
@@ -2332,11 +2332,17 @@ func Test_CreatePullRequest_InsidersMode_UIGate(t *testing.T) {
23322332
PostReposPullsByOwnerByRepo: mockResponse(t, http.StatusCreated, mockPR),
23332333
}))
23342334

2335-
deps := BaseDeps{
2336-
Client: client,
2337-
GQLClient: githubv4.NewClient(nil),
2338-
Flags: FeatureFlags{InsidersMode: true},
2335+
mcpAppsChecker := func(_ context.Context, flag string) (bool, error) {
2336+
return flag == MCPAppsFeatureFlag, nil
23392337
}
2338+
deps := NewBaseDeps(
2339+
client, githubv4.NewClient(nil), nil, nil,
2340+
translations.NullTranslationHelper,
2341+
FeatureFlags{},
2342+
0,
2343+
mcpAppsChecker,
2344+
stubExporters(),
2345+
)
23402346
handler := serverTool.Handler(deps)
23412347

23422348
t.Run("UI client without _ui_submitted returns form message", func(t *testing.T) {

pkg/http/handler.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"log/slog"
77
"net/http"
8+
"slices"
89

910
ghcontext "github.com/github/github-mcp-server/pkg/context"
1011
"github.com/github/github-mcp-server/pkg/github"
@@ -261,6 +262,19 @@ func InventoryFiltersForRequest(r *http.Request, builder *inventory.Builder) *in
261262
builder = builder.WithReadOnly(true)
262263
}
263264

265+
insiders := ghcontext.IsInsidersMode(ctx)
266+
if insiders {
267+
builder = builder.WithInsidersMode(true)
268+
}
269+
270+
// Enable MCP Apps if the feature flag is present in the request headers
271+
// or if insiders mode is active (transitional: insiders implies remote_mcp_ui_apps).
272+
headerFeatures := ghcontext.GetHeaderFeatures(ctx)
273+
mcpApps := slices.Contains(headerFeatures, github.MCPAppsFeatureFlag) || insiders
274+
if mcpApps {
275+
builder = builder.WithMCPApps(true)
276+
}
277+
264278
toolsets := ghcontext.GetToolsets(ctx)
265279
tools := ghcontext.GetTools(ctx)
266280

pkg/http/server.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ import (
2727

2828
// knownFeatureFlags are the feature flags that can be enabled via X-MCP-Features header.
2929
// Only these flags are accepted from headers.
30-
var knownFeatureFlags = []string{}
30+
var knownFeatureFlags = []string{
31+
github.MCPAppsFeatureFlag,
32+
}
3133

3234
type ServerConfig struct {
3335
// Version of the server
@@ -212,7 +214,8 @@ func initGlobalToolScopeMap(t translations.TranslationHelperFunc) error {
212214
}
213215

214216
// createHTTPFeatureChecker creates a feature checker that reads header features from context
215-
// and validates them against the knownFeatureFlags whitelist
217+
// and validates them against the knownFeatureFlags whitelist.
218+
// It also handles transitional behavior where insiders mode implies remote_mcp_ui_apps.
216219
func createHTTPFeatureChecker() inventory.FeatureFlagChecker {
217220
// Pre-compute whitelist as set for O(1) lookup
218221
knownSet := make(map[string]bool, len(knownFeatureFlags))
@@ -224,6 +227,10 @@ func createHTTPFeatureChecker() inventory.FeatureFlagChecker {
224227
if knownSet[flag] && slices.Contains(ghcontext.GetHeaderFeatures(ctx), flag) {
225228
return true, nil
226229
}
230+
// Transitional: insiders mode implies remote_mcp_ui_apps feature flag
231+
if flag == github.MCPAppsFeatureFlag && ghcontext.IsInsidersMode(ctx) {
232+
return true, nil
233+
}
227234
return false, nil
228235
}
229236
}

0 commit comments

Comments
 (0)