Skip to content

Commit e366cf8

Browse files
committed
wip
1 parent bbaa877 commit e366cf8

File tree

8 files changed

+80
-31
lines changed

8 files changed

+80
-31
lines changed

internal/ghmcp/server.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
135135
WithReadOnly(cfg.ReadOnly).
136136
WithToolsets(cfg.EnabledToolsets).
137137
WithTools(github.CleanTools(cfg.EnabledTools)).
138-
WithServerInstructions()
139-
// WithFeatureChecker(createFeatureChecker(cfg.EnabledFeatures))
138+
WithServerInstructions().
139+
WithFeatureChecker(featureChecker)
140140

141141
// Apply token scope filtering if scopes are known (for PAT filtering)
142142
if cfg.TokenScopes != nil {

pkg/context/request.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,35 @@ func IsLockdownMode(ctx context.Context) bool {
6565
}
6666
return false
6767
}
68+
69+
// insidersCtxKey is a context key for insiders mode
70+
type insidersCtxKey struct{}
71+
72+
// WithInsidersMode adds insiders mode state to the context
73+
func WithInsidersMode(ctx context.Context, enabled bool) context.Context {
74+
return context.WithValue(ctx, insidersCtxKey{}, enabled)
75+
}
76+
77+
// IsInsidersMode retrieves the insiders mode state from the context
78+
func IsInsidersMode(ctx context.Context) bool {
79+
if enabled, ok := ctx.Value(insidersCtxKey{}).(bool); ok {
80+
return enabled
81+
}
82+
return false
83+
}
84+
85+
// headerFeaturesCtxKey is a context key for raw header feature flags
86+
type headerFeaturesCtxKey struct{}
87+
88+
// WithHeaderFeatures stores the raw feature flags from the X-MCP-Features header into context
89+
func WithHeaderFeatures(ctx context.Context, features []string) context.Context {
90+
return context.WithValue(ctx, headerFeaturesCtxKey{}, features)
91+
}
92+
93+
// GetHeaderFeatures retrieves the raw feature flags from context
94+
func GetHeaderFeatures(ctx context.Context) []string {
95+
if features, ok := ctx.Value(headerFeaturesCtxKey{}).([]string); ok {
96+
return features
97+
}
98+
return nil
99+
}

pkg/github/dependencies.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,6 @@ type RequestDeps struct {
248248
lockdownMode bool
249249
RepoAccessOpts []lockdown.RepoAccessOption
250250
T translations.TranslationHelperFunc
251-
Flags FeatureFlags
252251
ContentWindowSize int
253252

254253
// Feature flag checker for runtime checks
@@ -380,6 +379,7 @@ func (d *RequestDeps) GetT() translations.TranslationHelperFunc { return d.T }
380379
func (d *RequestDeps) GetFlags(ctx context.Context) FeatureFlags {
381380
return FeatureFlags{
382381
LockdownMode: d.lockdownMode && ghcontext.IsLockdownMode(ctx),
382+
InsidersMode: ghcontext.IsInsidersMode(ctx),
383383
}
384384
}
385385

pkg/http/features.go

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"slices"
66

7+
ghcontext "github.com/github/github-mcp-server/pkg/context"
78
"github.com/github/github-mcp-server/pkg/github"
89
"github.com/github/github-mcp-server/pkg/inventory"
910
)
@@ -14,26 +15,20 @@ var KnownFeatureFlags = []string{
1415
github.FeatureFlagHoldbackConsolidatedActions,
1516
}
1617

17-
// ComposeFeatureChecker combines header-based feature flags with a static checker.
18-
func ComposeFeatureChecker(headerFeatures []string, staticChecker inventory.FeatureFlagChecker) inventory.FeatureFlagChecker {
19-
if len(headerFeatures) == 0 && staticChecker == nil {
20-
return nil
21-
}
22-
23-
// Only accept header features that are in the known list
24-
headerSet := make(map[string]bool, len(headerFeatures))
25-
for _, f := range headerFeatures {
26-
if slices.Contains(KnownFeatureFlags, f) {
27-
headerSet[f] = true
28-
}
18+
// CreateHTTPFeatureChecker creates a feature checker that reads header features from context
19+
func CreateHTTPFeatureChecker(staticChecker inventory.FeatureFlagChecker) inventory.FeatureFlagChecker {
20+
// Pre-compute whitelist as set for O(1) lookup
21+
knownSet := make(map[string]bool, len(KnownFeatureFlags))
22+
for _, f := range KnownFeatureFlags {
23+
knownSet[f] = true
2924
}
3025

3126
return func(ctx context.Context, flag string) (bool, error) {
32-
// Header-based: static string matching
33-
if headerSet[flag] {
27+
// Check whitelist first, then header features
28+
if knownSet[flag] && slices.Contains(ghcontext.GetHeaderFeatures(ctx), flag) {
3429
return true, nil
3530
}
36-
// Static checker
31+
// Fall back to static checker
3732
if staticChecker != nil {
3833
return staticChecker(ctx, flag)
3934
}

pkg/http/handler.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77

88
ghcontext "github.com/github/github-mcp-server/pkg/context"
99
"github.com/github/github-mcp-server/pkg/github"
10-
"github.com/github/github-mcp-server/pkg/http/headers"
1110
"github.com/github/github-mcp-server/pkg/http/middleware"
1211
"github.com/github/github-mcp-server/pkg/inventory"
1312
"github.com/github/github-mcp-server/pkg/translations"
@@ -31,6 +30,7 @@ type Handler struct {
3130
type HandlerOptions struct {
3231
GitHubMcpServerFactory GitHubMCPServerFactoryFunc
3332
InventoryFactory InventoryFactoryFunc
33+
FeatureChecker inventory.FeatureFlagChecker
3434
}
3535

3636
type HandlerOption func(*HandlerOptions)
@@ -47,6 +47,12 @@ func WithInventoryFactory(f InventoryFactoryFunc) HandlerOption {
4747
}
4848
}
4949

50+
func WithHandlerFeatureChecker(checker inventory.FeatureFlagChecker) HandlerOption {
51+
return func(o *HandlerOptions) {
52+
o.FeatureChecker = checker
53+
}
54+
}
55+
5056
func NewHTTPMcpHandler(
5157
ctx context.Context,
5258
cfg *ServerConfig,
@@ -66,7 +72,7 @@ func NewHTTPMcpHandler(
6672

6773
inventoryFactory := opts.InventoryFactory
6874
if inventoryFactory == nil {
69-
inventoryFactory = DefaultInventoryFactory(cfg, t, nil)
75+
inventoryFactory = DefaultInventoryFactory(cfg, t, opts.FeatureChecker)
7076
}
7177

7278
return &Handler{
@@ -141,15 +147,12 @@ func DefaultGitHubMCPServerFactory(r *http.Request, deps github.ToolDependencies
141147
return github.NewMCPServer(r.Context(), cfg, deps, inventory)
142148
}
143149

144-
func DefaultInventoryFactory(_ *ServerConfig, t translations.TranslationHelperFunc, staticChecker inventory.FeatureFlagChecker) InventoryFactoryFunc {
150+
// DefaultInventoryFactory creates the default inventory factory for HTTP mode
151+
func DefaultInventoryFactory(_ *ServerConfig, t translations.TranslationHelperFunc, featureChecker inventory.FeatureFlagChecker) InventoryFactoryFunc {
145152
return func(r *http.Request) (*inventory.Inventory, error) {
146-
b := github.NewInventory(t).WithDeprecatedAliases(github.DeprecatedToolAliases)
147-
148-
// Feature checker composition
149-
headerFeatures := headers.ParseCommaSeparated(r.Header.Get(headers.MCPFeaturesHeader))
150-
if checker := ComposeFeatureChecker(headerFeatures, staticChecker); checker != nil {
151-
b = b.WithFeatureChecker(checker)
152-
}
153+
b := github.NewInventory(t).
154+
WithDeprecatedAliases(github.DeprecatedToolAliases).
155+
WithFeatureChecker(featureChecker)
153156

154157
b = InventoryFiltersForRequest(r, b)
155158
b.WithServerInstructions()

pkg/http/headers/headers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ const (
3434
MCPToolsHeader = "X-MCP-Tools"
3535
// MCPLockdownHeader indicates whether lockdown mode is enabled.
3636
MCPLockdownHeader = "X-MCP-Lockdown"
37+
// MCPInsidersHeader indicates whether insiders mode is enabled for early access features.
38+
MCPInsidersHeader = "X-MCP-Insiders"
3739
// MCPFeaturesHeader is a comma-separated list of feature flags to enable.
3840
MCPFeaturesHeader = "X-MCP-Features"
3941
)

pkg/http/middleware/request_config.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,42 @@ import (
99
"github.com/github/github-mcp-server/pkg/http/headers"
1010
)
1111

12-
// WithRequestConfig is a middleware that extracts MCP-related headers and sets them in the request context
12+
// WithRequestConfig is a middleware that extracts MCP-related headers and sets them in the request context.
13+
// This includes readonly mode, toolsets, tools, lockdown mode, insiders mode, and feature flags.
1314
func WithRequestConfig(next http.Handler) http.Handler {
1415
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1516
ctx := r.Context()
1617

18+
// Readonly mode
1719
if relaxedParseBool(r.Header.Get(headers.MCPReadOnlyHeader)) {
1820
ctx = ghcontext.WithReadonly(ctx, true)
1921
}
2022

23+
// Toolsets
2124
if toolsets := headers.ParseCommaSeparated(r.Header.Get(headers.MCPToolsetsHeader)); len(toolsets) > 0 {
2225
ctx = ghcontext.WithToolsets(ctx, toolsets)
2326
}
2427

28+
// Tools
2529
if tools := headers.ParseCommaSeparated(r.Header.Get(headers.MCPToolsHeader)); len(tools) > 0 {
2630
ctx = ghcontext.WithTools(ctx, tools)
2731
}
2832

33+
// Lockdown mode
2934
if relaxedParseBool(r.Header.Get(headers.MCPLockdownHeader)) {
3035
ctx = ghcontext.WithLockdownMode(ctx, true)
3136
}
3237

38+
// Insiders mode
39+
if relaxedParseBool(r.Header.Get(headers.MCPInsidersHeader)) {
40+
ctx = ghcontext.WithInsidersMode(ctx, true)
41+
}
42+
43+
// Feature flags
44+
if features := headers.ParseCommaSeparated(r.Header.Get(headers.MCPFeaturesHeader)); len(features) > 0 {
45+
ctx = ghcontext.WithHeaderFeatures(ctx, features)
46+
}
47+
3348
next.ServeHTTP(w, r.WithContext(ctx))
3449
})
3550
}

pkg/http/server.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,19 +83,21 @@ func RunHTTPServer(cfg ServerConfig) error {
8383
repoAccessOpts = append(repoAccessOpts, lockdown.WithTTL(*cfg.RepoAccessCacheTTL))
8484
}
8585

86+
featureChecker := CreateHTTPFeatureChecker(nil)
87+
8688
deps := github.NewRequestDeps(
8789
apiHost,
8890
cfg.Version,
8991
cfg.LockdownMode,
9092
repoAccessOpts,
9193
t,
9294
cfg.ContentWindowSize,
93-
nil,
95+
featureChecker,
9496
)
9597

9698
r := chi.NewRouter()
9799

98-
handler := NewHTTPMcpHandler(ctx, &cfg, deps, t, logger)
100+
handler := NewHTTPMcpHandler(ctx, &cfg, deps, t, logger, WithHandlerFeatureChecker(featureChecker))
99101
handler.RegisterRoutes(r)
100102

101103
addr := fmt.Sprintf(":%d", cfg.Port)

0 commit comments

Comments
 (0)