|
| 1 | +# EnableCondition Migration Guide for Remote Server |
| 2 | + |
| 3 | +This guide explains how to adopt the composable `EnableCondition` system in `github/github-mcp-server-remote`. |
| 4 | + |
| 5 | +## Go Module Update |
| 6 | + |
| 7 | +To use this feature, update your `go.mod`: |
| 8 | + |
| 9 | +```bash |
| 10 | +go get github.com/github/github-mcp-server@3a52f7bd4ba45a6799eb9209db09080428bf0878 |
| 11 | +``` |
| 12 | + |
| 13 | +Or add directly to `go.mod`: |
| 14 | +``` |
| 15 | +require github.com/github/github-mcp-server v0.0.0-20251218XXXXXX-3a52f7bd4ba4 |
| 16 | +``` |
| 17 | + |
| 18 | +Then run `go mod tidy`. |
| 19 | + |
| 20 | +> **Note**: Once PR #1641 is merged, you can use `@main` or wait for a tagged release. |
| 21 | +
|
| 22 | +## Quick Start |
| 23 | + |
| 24 | +### Before (old approach with Enabled func) |
| 25 | + |
| 26 | +```go |
| 27 | +tool := inventory.ServerTool{ |
| 28 | + Tool: mcp.Tool{Name: "my_tool"}, |
| 29 | + Enabled: func(ctx context.Context) (bool, error) { |
| 30 | + // CCA bypass pattern |
| 31 | + if isCCA(ctx) { |
| 32 | + return true, nil |
| 33 | + } |
| 34 | + // Otherwise check feature flag |
| 35 | + return checkFeatureFlag(ctx, "my_feature") |
| 36 | + }, |
| 37 | +} |
| 38 | +``` |
| 39 | + |
| 40 | +### After (new approach with EnableCondition) |
| 41 | + |
| 42 | +```go |
| 43 | +tool := inventory.ServerTool{ |
| 44 | + Tool: mcp.Tool{Name: "my_tool"}, |
| 45 | + EnableCondition: inventory.Or( |
| 46 | + inventory.ContextBool("is_cca"), |
| 47 | + inventory.FeatureFlag("my_feature"), |
| 48 | + ), |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +## Available Primitives |
| 53 | + |
| 54 | +```go |
| 55 | +import "github.com/github/github-mcp-server/pkg/inventory" |
| 56 | + |
| 57 | +// Feature flag - checked via FeatureCheckerFromContext |
| 58 | +inventory.FeatureFlag("flag_name") |
| 59 | + |
| 60 | +// Context bool - checked via ContextBoolFromContext |
| 61 | +inventory.ContextBool("key_name") |
| 62 | + |
| 63 | +// Static values |
| 64 | +inventory.Always() // always enabled |
| 65 | +inventory.Never() // always disabled |
| 66 | +inventory.Static(true) // explicit bool |
| 67 | +``` |
| 68 | + |
| 69 | +## Combinators |
| 70 | + |
| 71 | +```go |
| 72 | +// AND - all must be true (short-circuits on first false) |
| 73 | +inventory.And(cond1, cond2, cond3) |
| 74 | + |
| 75 | +// OR - any must be true (short-circuits on first true) |
| 76 | +inventory.Or(cond1, cond2, cond3) |
| 77 | + |
| 78 | +// NOT - inverts condition |
| 79 | +inventory.Not(cond) |
| 80 | +``` |
| 81 | + |
| 82 | +## Setting Up Context |
| 83 | + |
| 84 | +At request start, set up the context with your actor/request info: |
| 85 | + |
| 86 | +```go |
| 87 | +// 1. Set feature flag checker |
| 88 | +ctx = inventory.ContextWithFeatureChecker(ctx, func(ctx context.Context, flagName string) (bool, error) { |
| 89 | + // Your feature flag service call |
| 90 | + return myFeatureFlagService.IsEnabled(ctx, flagName, actor) |
| 91 | +}) |
| 92 | + |
| 93 | +// 2. Set context bools (pre-computed actor properties) |
| 94 | +ctx = inventory.ContextWithBools(ctx, inventory.ContextBools{ |
| 95 | + "is_cca": actor.IsCCA(), |
| 96 | + "is_copilot_chat": actor.IsCopilotChatHost(), |
| 97 | + "has_paid_bing": actor.HasPaidBing(), |
| 98 | + "is_premium": actor.IsPremium(), |
| 99 | + "is_staff": actor.IsStaff(), |
| 100 | +}) |
| 101 | +``` |
| 102 | + |
| 103 | +## Common Patterns |
| 104 | + |
| 105 | +### CCA Bypass (most common) |
| 106 | +```go |
| 107 | +// CCA users always get the tool, others need the feature flag |
| 108 | +tool.EnableCondition = inventory.Or( |
| 109 | + inventory.ContextBool("is_cca"), |
| 110 | + inventory.FeatureFlag("my_feature"), |
| 111 | +) |
| 112 | +``` |
| 113 | + |
| 114 | +### Copilot-chat Host Bypass |
| 115 | +```go |
| 116 | +tool.EnableCondition = inventory.Or( |
| 117 | + inventory.ContextBool("is_copilot_chat"), |
| 118 | + inventory.FeatureFlag("my_feature"), |
| 119 | +) |
| 120 | +``` |
| 121 | + |
| 122 | +### Multiple Requirements (AND) |
| 123 | +```go |
| 124 | +// Requires both premium AND feature flag |
| 125 | +tool.EnableCondition = inventory.And( |
| 126 | + inventory.ContextBool("is_premium"), |
| 127 | + inventory.FeatureFlag("advanced_features"), |
| 128 | +) |
| 129 | +``` |
| 130 | + |
| 131 | +### Kill Switch Pattern |
| 132 | +```go |
| 133 | +// Enabled by default, but can be killed |
| 134 | +tool.EnableCondition = inventory.Not( |
| 135 | + inventory.FeatureFlag("kill_my_tool"), |
| 136 | +) |
| 137 | +``` |
| 138 | + |
| 139 | +### Complex Nested Conditions |
| 140 | +```go |
| 141 | +// Premium users OR (staff with beta flag), but not if kill switch is on |
| 142 | +tool.EnableCondition = inventory.And( |
| 143 | + inventory.Or( |
| 144 | + inventory.ContextBool("is_premium"), |
| 145 | + inventory.And( |
| 146 | + inventory.ContextBool("is_staff"), |
| 147 | + inventory.FeatureFlag("beta_access"), |
| 148 | + ), |
| 149 | + ), |
| 150 | + inventory.Not(inventory.FeatureFlag("kill_switch")), |
| 151 | +) |
| 152 | +``` |
| 153 | + |
| 154 | +### Custom Logic (fallback) |
| 155 | +```go |
| 156 | +// For complex cases that can't be expressed with primitives |
| 157 | +tool.EnableCondition = inventory.ConditionFunc(func(ctx context.Context) (bool, error) { |
| 158 | + actor := actorFromContext(ctx) |
| 159 | + return someComplexLogic(actor), nil |
| 160 | +}) |
| 161 | +``` |
| 162 | + |
| 163 | +## How Optimization Works |
| 164 | + |
| 165 | +You don't need to do anything special - optimization is automatic: |
| 166 | + |
| 167 | +1. **Build time**: `Builder.Build()` compiles all `EnableCondition`s into bitmask evaluators |
| 168 | +2. **Request time**: `AvailableTools(ctx)` builds a `RequestMask` once (evaluates all flags/bools), then uses O(1) bitmask operations per tool |
| 169 | +3. **Pre-sorted**: Tools are sorted at build time, so filtering preserves order without re-sorting |
| 170 | + |
| 171 | +### Performance |
| 172 | + |
| 173 | +| Metric | Before | After | Improvement | |
| 174 | +|--------|--------|-------|-------------| |
| 175 | +| Time (1000 req × 50 tools) | 23.7ms | 12.9ms | **46% faster** | |
| 176 | +| Allocations | 15000 | 12000 | 20% fewer | |
| 177 | + |
| 178 | +## Migration Strategy |
| 179 | + |
| 180 | +1. **Phase 1**: Add `EnableCondition` alongside existing `Enabled` func (both work) |
| 181 | +2. **Phase 2**: Gradually migrate tools to `EnableCondition` |
| 182 | +3. **Phase 3**: Remove old `Enabled` funcs once all migrated |
| 183 | + |
| 184 | +The system is fully backward compatible - `Enabled` func is checked first, then `EnableCondition`. |
| 185 | + |
| 186 | +## Files to Reference |
| 187 | + |
| 188 | +- [conditions.go](../pkg/inventory/conditions.go) - EnableCondition interface and primitives |
| 189 | +- [condition_compiler.go](../pkg/inventory/condition_compiler.go) - Bitmask compiler (you don't need to use this directly) |
| 190 | +- [conditions_test.go](../pkg/inventory/conditions_test.go) - Usage examples in tests |
| 191 | + |
| 192 | +## Questions? |
| 193 | + |
| 194 | +See PR #1641 for discussion or reach out to @SamMorrowDrums. |
0 commit comments