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
8 changes: 8 additions & 0 deletions cmd/github-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ var (
}
}

var enabledFeatures []string
if viper.IsSet("features") {
if err := viper.UnmarshalKey("features", &enabledFeatures); err != nil {
return fmt.Errorf("failed to unmarshal features: %w", err)
}
}

ttl := viper.GetDuration("repo-access-cache-ttl")
httpConfig := ghhttp.ServerConfig{
Version: version,
Expand All @@ -144,6 +151,7 @@ var (
EnabledToolsets: enabledToolsets,
EnabledTools: enabledTools,
ExcludeTools: excludeTools,
EnabledFeatures: enabledFeatures,
InsidersMode: viper.GetBool("insiders"),
}

Expand Down
25 changes: 25 additions & 0 deletions docs/insiders-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,28 @@ MCP Apps requires a host that supports the [MCP Apps extension](https://modelcon

- **VS Code Insiders** — enable via the `chat.mcp.apps.enabled` setting
- **Visual Studio Code** — enable via the `chat.mcp.apps.enabled` setting

---

## CSV output for list tools

CSV output mode returns supported list tool responses as CSV instead of JSON. This is intended to reduce response context for agents when scanning or summarising lists of GitHub data.

CSV output applies only to tools in default toolsets whose names start with `list_`, such as `list_issues`, `list_pull_requests`, `list_commits`, and `list_branches`. It does not add new tools or expose a tool argument for selecting the format; the server controls the response format through the Insiders feature flag.

### Format

- Nested objects are flattened into dot-notation columns, for example `user.login`, `category.name`, or `head.ref`.
- Arrays are represented as compact single-cell values joined with `;`.
- `body` fields are whitespace-normalized so multiline Markdown does not expand a list response into many output lines.
- Response metadata present in wrapped responses, such as `pageInfo.*` and `totalCount`, is emitted as `#`-prefixed lines before the CSV rows, followed by a blank line. Tools that return a root JSON array do not include metadata preamble lines.

### Enabling CSV output

CSV output is enabled by Insiders Mode. For local development, it can also be enabled explicitly with the `csv_output` feature flag:

```bash
github-mcp-server stdio --features csv_output
```

Because this changes list tool response shape, clients that require JSON list responses should avoid enabling this feature.
8 changes: 2 additions & 6 deletions internal/ghmcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
cfg.Translator,
github.FeatureFlags{
LockdownMode: cfg.LockdownMode,
InsidersMode: cfg.InsidersMode,
},
cfg.ContentWindowSize,
featureChecker,
Expand Down Expand Up @@ -229,7 +228,7 @@ type StdioServerConfig struct {
// LockdownMode indicates if we should enable lockdown mode
LockdownMode bool

// InsidersMode indicates if we should enable experimental features
// InsidersMode expands to the curated set of feature flags enabled for insiders.
InsidersMode bool

// ExcludeTools is a list of tool names to disable regardless of other settings.
Expand Down Expand Up @@ -345,7 +344,7 @@ func RunStdioServer(cfg StdioServerConfig) error {

// createFeatureChecker returns a FeatureFlagChecker that resolves features
// using the centralized ResolveFeatureFlags function. For the local server,
// features are resolved once at startup from --features CLI flag + insiders mode.
// features are resolved once at startup from --features CLI flag and insiders mode.
func createFeatureChecker(enabledFeatures []string, insidersMode bool) inventory.FeatureFlagChecker {
featureSet := github.ResolveFeatureFlags(enabledFeatures, insidersMode)
return func(_ context.Context, flagName string) (bool, error) {
Expand All @@ -372,9 +371,6 @@ func addUserAgentsMiddleware(cfg github.MCPServerConfig, restUATransp *transport
message.Params.ClientInfo.Name,
message.Params.ClientInfo.Version,
)
if cfg.InsidersMode {
userAgent += " (insiders)"
}

restUATransp.Agent = userAgent

Expand Down
2 changes: 1 addition & 1 deletion pkg/github/context_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func GetMe(t translations.TranslationHelperFunc) inventory.ServerTool {
}

result := MarshalledTextResult(minimalUser)
if deps.GetFlags(ctx).InsidersMode {
if deps.IsFeatureEnabled(ctx, FeatureFlagIFCLabels) {
if result.Meta == nil {
result.Meta = mcp.Meta{}
}
Expand Down
33 changes: 20 additions & 13 deletions pkg/github/context_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func Test_GetMe(t *testing.T) {
}
}

func Test_GetMe_IFC_InsidersMode(t *testing.T) {
func Test_GetMe_IFC_FeatureFlag(t *testing.T) {
t.Parallel()

serverTool := GetMe(translations.NullTranslationHelper)
Expand All @@ -153,34 +153,41 @@ func Test_GetMe_IFC_InsidersMode(t *testing.T) {
GetUser: mockResponse(t, http.StatusOK, mockUser),
})

t.Run("insiders mode disabled omits ifc label from result meta", func(t *testing.T) {
deps := BaseDeps{
Client: mustNewGHClient(t, mockedHTTPClient),
Flags: FeatureFlags{InsidersMode: false},
}
depsWithIFCFeature := func(enabled bool) *BaseDeps {
return NewBaseDeps(
mustNewGHClient(t, mockedHTTPClient), nil, nil, nil,
translations.NullTranslationHelper,
FeatureFlags{},
0,
func(_ context.Context, flagName string) (bool, error) {
return flagName == FeatureFlagIFCLabels && enabled, nil
},
stubExporters(),
)
}

t.Run("feature disabled omits ifc label from result meta", func(t *testing.T) {
deps := depsWithIFCFeature(false)
handler := serverTool.Handler(deps)

request := createMCPRequest(map[string]any{})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)

assert.Nil(t, result.Meta, "result meta should be nil when insiders mode is disabled")
assert.Nil(t, result.Meta, "result meta should be nil when IFC labels are disabled")
})

t.Run("insiders mode enabled includes ifc label in result meta", func(t *testing.T) {
deps := BaseDeps{
Client: mustNewGHClient(t, mockedHTTPClient),
Flags: FeatureFlags{InsidersMode: true},
}
t.Run("feature enabled includes ifc label in result meta", func(t *testing.T) {
deps := depsWithIFCFeature(true)
handler := serverTool.Handler(deps)

request := createMCPRequest(map[string]any{})
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError)

require.NotNil(t, result.Meta, "result meta should be set when insiders mode is enabled")
require.NotNil(t, result.Meta, "result meta should be set when IFC labels are enabled")
ifcLabel, ok := result.Meta["ifc"]
require.True(t, ok, "result meta should contain ifc key")

Expand Down
Loading
Loading