feat: Add CSV output format for default list tools under insiders mode#2450
Conversation
8e2be4a to
3ada757
Compare
There was a problem hiding this comment.
Pull request overview
Adds an Insiders-gated CSV output mode for default list_* tools, aiming to reduce response size/context for MCP clients. This is implemented by feature-flagged tool variants that convert the existing JSON text output into a flattened CSV representation.
Changes:
- Added
csv_outputfeature flag (allowlisted and enabled by Insiders mode). - Introduced feature-gated JSON/CSV tool variants for default-toolset
list_*tools, with centralized JSON→CSV conversion (flattening + whitespace normalization). - Wired HTTP mode CLI
--features/--insidersinto feature-flag resolution; added unit tests + Insiders feature documentation.
Show a summary per file
| File | Description |
|---|---|
| cmd/github-mcp-server/main.go | Passes CLI --features into HTTP server config. |
| pkg/http/server.go | Extends HTTP server config with static enabled features; updates feature checker to combine static + header features and insiders mode. |
| pkg/http/server_test.go | Adds coverage for static feature/static insiders behavior in HTTP feature checker. |
| pkg/http/handler_test.go | Updates tests to new feature-checker signature. |
| pkg/github/feature_flags.go | Adds csv_output flag to allowlist + insiders expansion list. |
| pkg/github/tools.go | Wraps tool list construction to inject CSV-output variants for eligible tools. |
| pkg/github/csv_output.go | Implements JSON-text tool-result → flattened CSV conversion and tool handler wrapping. |
| pkg/github/csv_output_test.go | Adds unit tests for CSV conversion + feature-gated tool variants. |
| docs/insiders-features.md | Documents CSV output behavior, format, and how to enable it. |
Copilot's findings
- Files reviewed: 9/9 changed files
- Comments generated: 1
3ada757 to
b070b00
Compare
SamMorrowDrums
left a comment
There was a problem hiding this comment.
@RossTarrant I thought in a previous version similar bit of work I had asked that insiders would be expanded to the feature flags it enables, and nothing would be gated on insiders, but the feature that it is actually related to, and insiders is just a shortcut to enabled flags. Just like all and default are meta toolsets (contain more than one toolset), insiders should just be a meta feature flag, that enables a set of feature flags, so at the point they are checked, they should be considered to be regular feature flags.
Nothing else in the codebase should need to know what insiders is. That just leaks the concept too far.
Can this PR resolve that while it's making changes to the feature checker?
| featureSet := github.ResolveFeatureFlags( | ||
| enabledFeatures, | ||
| github.MetaFeatureFlagsForInsiders(insidersMode)..., | ||
| ) |
There was a problem hiding this comment.
I think you need to resolved the user provided enabled feature flags by the allowed features:
Because insiders mode flags list should be already in there. Also can't this directly reference the list of insiders features?
| if cfg.InsidersMode { | ||
| userAgent += " (insiders)" | ||
| } |
There was a problem hiding this comment.
@RossTarrant I think we do still want the UA set here. Maybe there is minor reason for some places to know insiders is on, albeit minimal. Apologies.
There was a problem hiding this comment.
@SamMorrowDrums I took your previous comment a bit too far! 😄 Have added it back
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1491adb to
bfa6fee
Compare
| "Mcp-Session-Id", | ||
| "Mcp-Protocol-Version", | ||
| "Last-Event-ID", | ||
| "X-Custom-Auth-Headers", |
There was a problem hiding this comment.
I could make it a separate PR if needed, but this is required to allow X-MCP-FEATURES headers
There was a problem hiding this comment.
Ah, I've switched it for the correct header in 0c9b8b3
| headers.MCPExcludeToolsHeader, | ||
| headers.MCPFeaturesHeader, | ||
| headers.MCPLockdownHeader, | ||
| headers.MCPInsidersHeader, |
There was a problem hiding this comment.
This seems to be missing the features header actually. So end user can enable via HTTP.
You can check remote, but I believe it is x-mcp-features or similar.
|
|
||
| // AvailableToolsWithoutFeatureFiltering returns tools that pass every filter | ||
| // except FeatureFlagEnable/FeatureFlagDisable. | ||
| func (r *Inventory) AvailableToolsWithoutFeatureFiltering(ctx context.Context) []ServerTool { |
There was a problem hiding this comment.
I am curious why we need this? Is it just for testing? I would have thought it would be sufficient to just either have features on or of. The disable tools are always on if no features are present.
SamMorrowDrums
left a comment
There was a problem hiding this comment.
This is getting there, my biggest concern is that the way CSV tools is being added, it doesn't look like it will correctly have the http mode enable/disable, and curious how that would working in github/github-mcp-server-remote also whether the wiring would automatically work or not.
| }) | ||
| } | ||
|
|
||
| sortTools(result) |
There was a problem hiding this comment.
If we don't ultimately need this function than maybe we don't need to extract sort.
| func (r *Inventory) AvailableResourceTemplatesWithoutFeatureFiltering(_ context.Context) []ServerResourceTemplate { | ||
| var result []ServerResourceTemplate | ||
| for i := range r.resourceTemplates { | ||
| res := &r.resourceTemplates[i] | ||
| if r.isToolsetEnabled(res.Toolset.ID) { | ||
| result = append(result, *res) |
There was a problem hiding this comment.
Same patter, but I'm not sure why we need.
| // AvailablePromptsWithoutFeatureFiltering returns prompts that pass every filter | ||
| // except FeatureFlagEnable/FeatureFlagDisable. | ||
| func (r *Inventory) AvailablePromptsWithoutFeatureFiltering(_ context.Context) []ServerPrompt { | ||
| var result []ServerPrompt | ||
| for i := range r.prompts { | ||
| prompt := &r.prompts[i] | ||
| if r.isToolsetEnabled(prompt.Toolset.ID) { | ||
| result = append(result, *prompt) | ||
| } | ||
| return result[i].Prompt.Name < result[j].Prompt.Name | ||
| }) | ||
| } | ||
|
|
||
| sortPrompts(result) |
There was a problem hiding this comment.
Same question here. Also, you can use Go generics to not need to do a separate typed sort function per type.
…ering when no checker (#2516) * refactor: generic toolset+name sort, clarify feature flag intent Address review feedback on #2450: - Collapse the three near-identical sort helpers in pkg/inventory/filters.go into a generic sortByToolsetThenName so adding new inventory item types doesn't require copying the comparator. - Expand the doc comments on the three *WithoutFeatureFiltering helpers to spell out why they exist: HTTP mode builds a static (process-wide) inventory as an upper bound, but per-request feature flags from headers (X-MCP-Features, X-MCP-Insiders) are evaluated later, so feature-flagged variants must be preserved here. - Strengthen the doc comment on ResolveFeatureFlags to make the contract explicit: user-supplied flags are validated against AllowedFeatureFlags, but insiders expansion deliberately is not — InsidersFeatureFlags may include server-controlled flags that are not user-toggleable. CORS comments are intentionally left for the PR author. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(feature-flags): clarify allowed and insiders sets are independent Also add tests covering: - a user-toggleable flag (FeatureFlagIssuesGranular) that insiders does not turn on automatically - insiders mode not turning on user-only allowed flags Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(inventory): collapse three *WithoutFeatureFiltering helpers into StaticUpperBound The three parallel methods (AvailableToolsWithoutFeatureFiltering, AvailableResourceTemplatesWithoutFeatureFiltering, AvailablePromptsWithoutFeatureFiltering) were always called as a triple in exactly two places: HTTP buildStaticInventory and its test mirror. They exist because the dual-variant pattern (sibling tools with mirrored FeatureFlagEnable / FeatureFlagDisable on the same name, e.g. CSV output) makes feature filtering at static-build time impossible — both variants must be kept and resolved per-request. Replace the three with one method, Inventory.StaticUpperBound(ctx), that returns (tools, resources, prompts) and carries the rationale in its doc comment. Reduces API surface, eliminates the triplication, and makes the single "skip feature filtering" concept obvious to readers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: simplify feature-flag handling Two related simplifications, both about treating insiders as a meta flag that expands once at startup and then stops mattering: - Collapse CSV's dual-variant pattern into a single tool whose handler performs a runtime feature-flag check via deps.IsFeatureEnabled. CSV is a pure response-format toggle, not a schema change, so it does not need the dual-name pattern that genuine schema variants (granular issues/PRs) still use. - When no feature checker is installed, skip feature-flag filtering and return the full upper bound. The static HTTP inventory now uses plain AvailableTools/Resources/Prompts; the per-request inventory always installs a checker, so MCP registration (which serves a tool name once) always sees a deduplicated set. The bespoke StaticUpperBound helper and the isToolEnabledWithFeatureFlags split go away. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci(mcp-diff): add insiders + per-feature configs The mcp-diff matrix now includes: - --insiders (and --insiders --read-only) - one config per github.AllowedFeatureFlags entry, generated by script/print-mcp-diff-configs so new user-controllable flags get diffed automatically without editing the workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(insiders): explain feature-flag resolution for contributors Adds a 'How feature flags are resolved' section covering: - Insiders is a meta flag, like 'all'/'default' for toolsets - User input -> allowlist filter -> insiders expansion -> server-side fallback (remote only) - AllowedFeatureFlags vs InsidersFeatureFlags are independent - How to add a new feature flag, including the TestGitHubPackageDoesNotReadInsidersMode guard Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(inventory): make feature-flag gating a regular ToolFilter Move tool feature-flag evaluation out of isToolEnabled and into a ToolFilter installed at the head of the pipeline by Build() when WithFeatureChecker received a non-nil checker. The 'no checker = no filtering' contract is now expressed structurally (the filter isn't installed) instead of by a runtime nil check inside the helper. Resources and prompts have no filter pipeline, so they call the now-pure featureFlagAllowed helper behind an explicit r.featureChecker != nil guard at the iteration site. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * perf(inventory): cache extracted toolset IDs in sort comparator Avoid evaluating the extractor closures up to three times per comparison. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hub/github-mcp-server into rosstarrant/add-csv-output-structure
Adds a sibling mcp-diff-http job that exercises the streamable-http transport against a shared HTTP server, with per-config settings supplied via X-MCP-* request headers — mirroring how the remote server is invoked in production (server-side defaults + per-user header overrides). The config generator gains a -transport flag: - stdio (default, unchanged behaviour) - http-headers (emits headers-only configs targeting a shared server) Two new combined entries layer multiple headers together as a smoke test for header-merging regressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…utput-structure # Conflicts: # README.md
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Adds feature-gated CSV output for default
list_*tools and refactors Insiders mode into a feature flag bundle.Why
CSV provides a flatter, more compact representation for list-style responses, helping evaluate token reduction from output format changes.
Refs github/copilot-mcp-core#1323
What changed
ifc_labelsas an Insiders-enabled feature flag.MCP impact
CSV mode changes list_* tool (within default toolset) response behavior under the csv_output Insiders feature flag. Tool schemas are unchanged; CSV output is server-controlled by feature flag gating.
Prompts tested (tool changes only)
Security / limits
CSV mode is intended to reduce response size for list tools. Body fields are whitespace-normalized in CSV output; full formatted content remains available through corresponding get/read tools.
Tool renaming
Lint & tests
Docs
docs/insiders-features.mddetailing the new experimental feature