// Command print-mcp-diff-configs emits the configuration matrix consumed by // the mcp-server-diff GitHub Action. The matrix is composed of three parts: // // 1. Hand-curated baseline configs (default, read-only, common toolset combos) // 2. Insiders configs (--insiders, --insiders --read-only) — meta flag that // expands to the curated insiders feature set // 3. One config per entry in github.AllowedFeatureFlags — automatically kept // in sync with the Go source so any new user-controllable feature flag // gets diffed without touching the workflow // // The same logical matrix is rendered for two transports, selected by // -transport: // // stdio Default. Args are appended to the action's top-level // // start_command (one stdio process per config). // // http-headers streamable-http transport against a shared HTTP server. The // // server is started once with no extra flags and every config // provides its settings via X-MCP-* request headers, mirroring // how the remote server is invoked in production (server-side // defaults + per-user header overrides). // // Usage: // // go run ./script/print-mcp-diff-configs // go run ./script/print-mcp-diff-configs -transport http-headers package main import ( "encoding/json" "flag" "fmt" "os" "sort" "strings" "github.com/github/github-mcp-server/pkg/github" mcphdr "github.com/github/github-mcp-server/pkg/http/headers" ) type config struct { Name string `json:"name"` Args string `json:"args,omitempty"` Transport string `json:"transport,omitempty"` ServerURL string `json:"server_url,omitempty"` Headers map[string]string `json:"headers,omitempty"` } // baseEntry describes one logical configuration in transport-agnostic form. // settings are translated to either CLI flags or X-MCP-* headers depending on // the target transport. type baseEntry struct { name string settings settings } type settings struct { toolsets string // comma-separated, "" for defaults tools string excludeTools string features string readOnly bool insiders bool lockdown bool } const httpServerURL = "http://localhost:8082/mcp" func main() { transport := flag.String("transport", "stdio", "Transport to target: stdio or http-headers") flag.Parse() entries := baseEntries() var out []config switch *transport { case "stdio": for _, e := range entries { out = append(out, config{Name: e.name, Args: e.settings.toArgs()}) } case "http-headers": for _, e := range entries { h := e.settings.toHeaders() if h == nil { h = map[string]string{} } // The action's top-level headers may be replaced (not merged) by // per-config headers, so always include the bearer token here. // The token must match a recognized GitHub prefix so the server's // Authorization parser accepts it without contacting the API. h[mcphdr.AuthorizationHeader] = "Bearer ghp_test" out = append(out, config{ Name: e.name, Transport: "streamable-http", ServerURL: httpServerURL, Headers: h, }) } default: fmt.Fprintf(os.Stderr, "unknown transport %q (want stdio or http-headers)\n", *transport) os.Exit(2) } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") if err := enc.Encode(out); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func baseEntries() []baseEntry { entries := []baseEntry{ {name: "default"}, {name: "read-only", settings: settings{readOnly: true}}, {name: "toolsets-repos", settings: settings{toolsets: "repos"}}, {name: "toolsets-issues", settings: settings{toolsets: "issues"}}, {name: "toolsets-context", settings: settings{toolsets: "context"}}, {name: "toolsets-pull_requests", settings: settings{toolsets: "pull_requests"}}, {name: "toolsets-repos,issues", settings: settings{toolsets: "repos,issues"}}, {name: "toolsets-issues,context", settings: settings{toolsets: "issues,context"}}, {name: "toolsets-all", settings: settings{toolsets: "all"}}, {name: "tools-get_me", settings: settings{tools: "get_me"}}, {name: "tools-get_me,list_issues", settings: settings{tools: "get_me,list_issues"}}, {name: "toolsets-repos+read-only", settings: settings{toolsets: "repos", readOnly: true}}, {name: "insiders", settings: settings{insiders: true}}, {name: "insiders+read-only", settings: settings{insiders: true, readOnly: true}}, // Combined entries: exercise multiple settings together so we catch // regressions when several X-MCP-* headers (or CLI flags) are merged. {name: "combined-toolsets+exclude+readonly", settings: settings{ toolsets: "repos,issues", excludeTools: "delete_file", readOnly: true, }}, {name: "combined-insiders+toolsets+features", settings: settings{ insiders: true, toolsets: "repos", features: firstFeatureFlag(), }}, } flags := append([]string(nil), github.AllowedFeatureFlags...) sort.Strings(flags) for _, f := range flags { entries = append(entries, baseEntry{ name: "feature-" + f, settings: settings{features: f}, }) } return entries } func (s settings) toArgs() string { var parts []string if s.toolsets != "" { parts = append(parts, "--toolsets="+s.toolsets) } if s.tools != "" { parts = append(parts, "--tools="+s.tools) } if s.excludeTools != "" { parts = append(parts, "--exclude-tools="+s.excludeTools) } if s.features != "" { parts = append(parts, "--features="+s.features) } if s.readOnly { parts = append(parts, "--read-only") } if s.insiders { parts = append(parts, "--insiders") } if s.lockdown { parts = append(parts, "--lockdown-mode") } return strings.Join(parts, " ") } func (s settings) toHeaders() map[string]string { h := map[string]string{} if s.toolsets != "" { h[mcphdr.MCPToolsetsHeader] = s.toolsets } if s.tools != "" { h[mcphdr.MCPToolsHeader] = s.tools } if s.excludeTools != "" { h[mcphdr.MCPExcludeToolsHeader] = s.excludeTools } if s.features != "" { h[mcphdr.MCPFeaturesHeader] = s.features } if s.readOnly { h[mcphdr.MCPReadOnlyHeader] = "true" } if s.insiders { h[mcphdr.MCPInsidersHeader] = "true" } if s.lockdown { h[mcphdr.MCPLockdownHeader] = "true" } if len(h) == 0 { return nil } return h } func firstFeatureFlag() string { flags := append([]string(nil), github.AllowedFeatureFlags...) if len(flags) == 0 { return "" } sort.Strings(flags) return flags[0] }