fix(mcp-apps): defer _meta.ui strip to per-request RegisterTools (#2446) · github/github-mcp-server@0e2fc38 · GitHub
Skip to content

Commit 0e2fc38

Browse files
fix(mcp-apps): defer _meta.ui strip to per-request RegisterTools (#2446)
* fix(mcp-apps): defer _meta.ui strip to per-request RegisterTools The MCP Apps `_meta.ui` strip lived in `Builder.Build()`, which calls `checkFeatureFlag(context.Background())`. The HTTP feature checker (`createHTTPFeatureChecker`) reads insiders mode from the request context — a background context never has it set, so the FF reported MCP Apps off and the strip ran eagerly at server startup. Per-request inventory factories then served pre-stripped tools regardless of whether the request actually arrived on the `/insiders` route. Symptom: `github/github-mcp-server-remote` returns 0 tools with `_meta.ui` over HTTP `/insiders`, despite the source unconditionally setting it on `get_me`, `issue_write`, and `create_pull_request`. VS Code only renders MCP App UIs because of its persistent tool cache from earlier deploys. Reproducible locally with `cmd/github-mcp-server http --insiders` plus a vanilla curl tools/list. Fix: drop the strip from `Build()`. Apply it in `RegisterTools(ctx,…)` where the per-request context is in scope and the HTTP feature checker can correctly detect insiders mode (or the remote checker can correctly read user identity for Statsig flag lookup). The same root cause affects `github/github-mcp-server-remote` — its `featureflags.NewComposedFeatureFlagChecker` reads `requestctx.User(ctx)`, which background context lacks, so the `remote_mcp_ui_apps` Statsig flag always returned false. The fix here covers both downstreams since `RegisterTools` is the single entry point for tool registration. Stdio mode is unaffected: it uses a closure-captured insiders mode flag (`createFeatureChecker`) that does not depend on context, and the per-request strip in `RegisterTools` produces the same outcome. Verified end-to-end against the deployed remote tool definitions: HTTP /insiders → 3 tools with _meta.ui (was 0) HTTP / → 0 tools with _meta.ui (correct) stdio --insiders → 3 tools with _meta.ui (unchanged) stdio → 0 tools with _meta.ui (correct) Adds: - pkg/http: TestInsidersRoutePreservesUIMeta — pins the regression - pkg/inventory: updates the existing strip tests to use the new RegisterTools-as-strip-site contract via a captureRegisteredTools helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * style: gofmt handler_test.go and registry_test.go Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * lint: address revive (context-as-argument) and unused checkFeatureFlag - Reorder captureRegisteredTools params to put context.Context first - Remove dead Builder.checkFeatureFlag (was only called by Build's former MCP Apps strip, now done in RegisterTools via the Inventory receiver's checkFeatureFlag instead) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2dab994 commit 0e2fc38

4 files changed

Lines changed: 97 additions & 35 deletions

File tree

pkg/http/handler_test.go

Lines changed: 53 additions & 0 deletions

pkg/inventory/builder.go

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -190,19 +190,6 @@ func cleanTools(tools []string) []string {
190190
return cleaned
191191
}
192192

193-
// checkFeatureFlag checks a feature flag at build time using the builder's feature checker.
194-
// Returns false if no checker is configured or the flag is not enabled.
195-
func (b *Builder) checkFeatureFlag(flag string) bool {
196-
if b.featureChecker == nil {
197-
return false
198-
}
199-
enabled, err := b.featureChecker(context.Background(), flag)
200-
if err != nil {
201-
return false
202-
}
203-
return enabled
204-
}
205-
206193
// Build creates the final Inventory with all configuration applied.
207194
// This processes toolset filtering, tool name resolution, and sets up
208195
// the inventory for use. The returned Inventory is ready for use with
@@ -214,13 +201,6 @@ func (b *Builder) checkFeatureFlag(flag string) bool {
214201
func (b *Builder) Build() (*Inventory, error) {
215202
tools := b.tools
216203

217-
// When MCP Apps feature flag is not enabled, strip UI metadata from tools
218-
// so clients won't attempt to load UI resources.
219-
// The feature checker is the single source of truth for flag evaluation.
220-
if !b.checkFeatureFlag(mcpAppsFeatureFlag) {
221-
tools = stripMCPAppsMetadata(tools)
222-
}
223-
224204
r := &Inventory{
225205
tools: tools,
226206
resourceTemplates: b.resourceTemplates,

pkg/inventory/registry.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,19 @@ func (r *Inventory) ToolsetDescriptions() map[ToolsetID]string {
170170

171171
// RegisterTools registers all available tools with the server using the provided dependencies.
172172
// The context is used for feature flag evaluation.
173+
//
174+
// MCP Apps UI metadata (`_meta.ui`) is stripped from the registered tools
175+
// when the MCP Apps feature flag is not enabled for this request. The strip
176+
// happens here (rather than at Build() time) so the per-request context is
177+
// in scope — HTTP feature checkers that read insiders mode or user identity
178+
// from ctx would otherwise see context.Background() and falsely report the
179+
// flag off, even when the actual request arrived on the /insiders route.
173180
func (r *Inventory) RegisterTools(ctx context.Context, s *mcp.Server, deps any) {
174-
for _, tool := range r.AvailableTools(ctx) {
181+
tools := r.AvailableTools(ctx)
182+
if !r.checkFeatureFlag(ctx, mcpAppsFeatureFlag) {
183+
tools = stripMCPAppsMetadata(tools)
184+
}
185+
for _, tool := range tools {
175186
tool.RegisterFunc(s, deps)
176187
}
177188
}

pkg/inventory/registry_test.go

Lines changed: 32 additions & 14 deletions

0 commit comments

Comments
 (0)