|
1 | | -// Command print-mcp-diff-configs emits the full configuration matrix consumed |
2 | | -// by the mcp-server-diff GitHub Action. The matrix is composed of three parts: |
| 1 | +// Command print-mcp-diff-configs emits the configuration matrix consumed by |
| 2 | +// the mcp-server-diff GitHub Action. The matrix is composed of three parts: |
3 | 3 | // |
4 | 4 | // 1. Hand-curated baseline configs (default, read-only, common toolset combos) |
5 | 5 | // 2. Insiders configs (--insiders, --insiders --read-only) — meta flag that |
|
8 | 8 | // in sync with the Go source so any new user-controllable feature flag |
9 | 9 | // gets diffed without touching the workflow |
10 | 10 | // |
11 | | -// Usage: go run ./script/print-mcp-diff-configs |
| 11 | +// The same logical matrix is rendered for two transports, selected by |
| 12 | +// -transport: |
| 13 | +// |
| 14 | +// stdio Default. Args are appended to the action's top-level |
| 15 | +// |
| 16 | +// start_command (one stdio process per config). |
| 17 | +// |
| 18 | +// http-headers streamable-http transport against a shared HTTP server. The |
| 19 | +// |
| 20 | +// server is started once with no extra flags and every config |
| 21 | +// provides its settings via X-MCP-* request headers, mirroring |
| 22 | +// how the remote server is invoked in production (server-side |
| 23 | +// defaults + per-user header overrides). |
| 24 | +// |
| 25 | +// Usage: |
| 26 | +// |
| 27 | +// go run ./script/print-mcp-diff-configs |
| 28 | +// go run ./script/print-mcp-diff-configs -transport http-headers |
12 | 29 | package main |
13 | 30 |
|
14 | 31 | import ( |
15 | 32 | "encoding/json" |
| 33 | + "flag" |
16 | 34 | "fmt" |
17 | 35 | "os" |
18 | 36 | "sort" |
| 37 | + "strings" |
19 | 38 |
|
20 | 39 | "github.com/github/github-mcp-server/pkg/github" |
| 40 | + mcphdr "github.com/github/github-mcp-server/pkg/http/headers" |
21 | 41 | ) |
22 | 42 |
|
23 | 43 | type config struct { |
24 | | - Name string `json:"name"` |
25 | | - Args string `json:"args"` |
| 44 | + Name string `json:"name"` |
| 45 | + Args string `json:"args,omitempty"` |
| 46 | + Transport string `json:"transport,omitempty"` |
| 47 | + ServerURL string `json:"server_url,omitempty"` |
| 48 | + Headers map[string]string `json:"headers,omitempty"` |
26 | 49 | } |
27 | 50 |
|
| 51 | +// baseEntry describes one logical configuration in transport-agnostic form. |
| 52 | +// settings are translated to either CLI flags or X-MCP-* headers depending on |
| 53 | +// the target transport. |
| 54 | +type baseEntry struct { |
| 55 | + name string |
| 56 | + settings settings |
| 57 | +} |
| 58 | + |
| 59 | +type settings struct { |
| 60 | + toolsets string // comma-separated, "" for defaults |
| 61 | + tools string |
| 62 | + excludeTools string |
| 63 | + features string |
| 64 | + readOnly bool |
| 65 | + insiders bool |
| 66 | + lockdown bool |
| 67 | +} |
| 68 | + |
| 69 | +const httpServerURL = "http://localhost:8082/mcp" |
| 70 | + |
28 | 71 | func main() { |
29 | | - configs := []config{ |
30 | | - {Name: "default", Args: ""}, |
31 | | - {Name: "read-only", Args: "--read-only"}, |
32 | | - {Name: "toolsets-repos", Args: "--toolsets=repos"}, |
33 | | - {Name: "toolsets-issues", Args: "--toolsets=issues"}, |
34 | | - {Name: "toolsets-context", Args: "--toolsets=context"}, |
35 | | - {Name: "toolsets-pull_requests", Args: "--toolsets=pull_requests"}, |
36 | | - {Name: "toolsets-repos,issues", Args: "--toolsets=repos,issues"}, |
37 | | - {Name: "toolsets-issues,context", Args: "--toolsets=issues,context"}, |
38 | | - {Name: "toolsets-all", Args: "--toolsets=all"}, |
39 | | - {Name: "tools-get_me", Args: "--tools=get_me"}, |
40 | | - {Name: "tools-get_me,list_issues", Args: "--tools=get_me,list_issues"}, |
41 | | - {Name: "toolsets-repos+read-only", Args: "--toolsets=repos --read-only"}, |
42 | | - {Name: "insiders", Args: "--insiders"}, |
43 | | - {Name: "insiders+read-only", Args: "--insiders --read-only"}, |
| 72 | + transport := flag.String("transport", "stdio", "Transport to target: stdio or http-headers") |
| 73 | + flag.Parse() |
| 74 | + |
| 75 | + entries := baseEntries() |
| 76 | + |
| 77 | + var out []config |
| 78 | + switch *transport { |
| 79 | + case "stdio": |
| 80 | + for _, e := range entries { |
| 81 | + out = append(out, config{Name: e.name, Args: e.settings.toArgs()}) |
| 82 | + } |
| 83 | + case "http-headers": |
| 84 | + for _, e := range entries { |
| 85 | + h := e.settings.toHeaders() |
| 86 | + if h == nil { |
| 87 | + h = map[string]string{} |
| 88 | + } |
| 89 | + // The action's top-level headers may be replaced (not merged) by |
| 90 | + // per-config headers, so always include the bearer token here. |
| 91 | + // The token must match a recognized GitHub prefix so the server's |
| 92 | + // Authorization parser accepts it without contacting the API. |
| 93 | + h[mcphdr.AuthorizationHeader] = "Bearer ghp_test" |
| 94 | + out = append(out, config{ |
| 95 | + Name: e.name, |
| 96 | + Transport: "streamable-http", |
| 97 | + ServerURL: httpServerURL, |
| 98 | + Headers: h, |
| 99 | + }) |
| 100 | + } |
| 101 | + default: |
| 102 | + fmt.Fprintf(os.Stderr, "unknown transport %q (want stdio or http-headers)\n", *transport) |
| 103 | + os.Exit(2) |
| 104 | + } |
| 105 | + |
| 106 | + enc := json.NewEncoder(os.Stdout) |
| 107 | + enc.SetIndent("", " ") |
| 108 | + if err := enc.Encode(out); err != nil { |
| 109 | + fmt.Fprintln(os.Stderr, err) |
| 110 | + os.Exit(1) |
| 111 | + } |
| 112 | +} |
| 113 | + |
| 114 | +func baseEntries() []baseEntry { |
| 115 | + entries := []baseEntry{ |
| 116 | + {name: "default"}, |
| 117 | + {name: "read-only", settings: settings{readOnly: true}}, |
| 118 | + {name: "toolsets-repos", settings: settings{toolsets: "repos"}}, |
| 119 | + {name: "toolsets-issues", settings: settings{toolsets: "issues"}}, |
| 120 | + {name: "toolsets-context", settings: settings{toolsets: "context"}}, |
| 121 | + {name: "toolsets-pull_requests", settings: settings{toolsets: "pull_requests"}}, |
| 122 | + {name: "toolsets-repos,issues", settings: settings{toolsets: "repos,issues"}}, |
| 123 | + {name: "toolsets-issues,context", settings: settings{toolsets: "issues,context"}}, |
| 124 | + {name: "toolsets-all", settings: settings{toolsets: "all"}}, |
| 125 | + {name: "tools-get_me", settings: settings{tools: "get_me"}}, |
| 126 | + {name: "tools-get_me,list_issues", settings: settings{tools: "get_me,list_issues"}}, |
| 127 | + {name: "toolsets-repos+read-only", settings: settings{toolsets: "repos", readOnly: true}}, |
| 128 | + {name: "insiders", settings: settings{insiders: true}}, |
| 129 | + {name: "insiders+read-only", settings: settings{insiders: true, readOnly: true}}, |
| 130 | + // Combined entries: exercise multiple settings together so we catch |
| 131 | + // regressions when several X-MCP-* headers (or CLI flags) are merged. |
| 132 | + {name: "combined-toolsets+exclude+readonly", settings: settings{ |
| 133 | + toolsets: "repos,issues", |
| 134 | + excludeTools: "delete_file", |
| 135 | + readOnly: true, |
| 136 | + }}, |
| 137 | + {name: "combined-insiders+toolsets+features", settings: settings{ |
| 138 | + insiders: true, |
| 139 | + toolsets: "repos", |
| 140 | + features: firstFeatureFlag(), |
| 141 | + }}, |
44 | 142 | } |
45 | 143 |
|
46 | 144 | flags := append([]string(nil), github.AllowedFeatureFlags...) |
47 | 145 | sort.Strings(flags) |
48 | 146 | for _, f := range flags { |
49 | | - configs = append(configs, config{ |
50 | | - Name: "feature-" + f, |
51 | | - Args: "--features=" + f, |
| 147 | + entries = append(entries, baseEntry{ |
| 148 | + name: "feature-" + f, |
| 149 | + settings: settings{features: f}, |
52 | 150 | }) |
53 | 151 | } |
| 152 | + return entries |
| 153 | +} |
54 | 154 |
|
55 | | - enc := json.NewEncoder(os.Stdout) |
56 | | - enc.SetIndent("", " ") |
57 | | - if err := enc.Encode(configs); err != nil { |
58 | | - fmt.Fprintln(os.Stderr, err) |
59 | | - os.Exit(1) |
| 155 | +func (s settings) toArgs() string { |
| 156 | + var parts []string |
| 157 | + if s.toolsets != "" { |
| 158 | + parts = append(parts, "--toolsets="+s.toolsets) |
| 159 | + } |
| 160 | + if s.tools != "" { |
| 161 | + parts = append(parts, "--tools="+s.tools) |
| 162 | + } |
| 163 | + if s.excludeTools != "" { |
| 164 | + parts = append(parts, "--exclude-tools="+s.excludeTools) |
| 165 | + } |
| 166 | + if s.features != "" { |
| 167 | + parts = append(parts, "--features="+s.features) |
| 168 | + } |
| 169 | + if s.readOnly { |
| 170 | + parts = append(parts, "--read-only") |
| 171 | + } |
| 172 | + if s.insiders { |
| 173 | + parts = append(parts, "--insiders") |
| 174 | + } |
| 175 | + if s.lockdown { |
| 176 | + parts = append(parts, "--lockdown-mode") |
| 177 | + } |
| 178 | + return strings.Join(parts, " ") |
| 179 | +} |
| 180 | + |
| 181 | +func (s settings) toHeaders() map[string]string { |
| 182 | + h := map[string]string{} |
| 183 | + if s.toolsets != "" { |
| 184 | + h[mcphdr.MCPToolsetsHeader] = s.toolsets |
60 | 185 | } |
| 186 | + if s.tools != "" { |
| 187 | + h[mcphdr.MCPToolsHeader] = s.tools |
| 188 | + } |
| 189 | + if s.excludeTools != "" { |
| 190 | + h[mcphdr.MCPExcludeToolsHeader] = s.excludeTools |
| 191 | + } |
| 192 | + if s.features != "" { |
| 193 | + h[mcphdr.MCPFeaturesHeader] = s.features |
| 194 | + } |
| 195 | + if s.readOnly { |
| 196 | + h[mcphdr.MCPReadOnlyHeader] = "true" |
| 197 | + } |
| 198 | + if s.insiders { |
| 199 | + h[mcphdr.MCPInsidersHeader] = "true" |
| 200 | + } |
| 201 | + if s.lockdown { |
| 202 | + h[mcphdr.MCPLockdownHeader] = "true" |
| 203 | + } |
| 204 | + if len(h) == 0 { |
| 205 | + return nil |
| 206 | + } |
| 207 | + return h |
| 208 | +} |
| 209 | + |
| 210 | +func firstFeatureFlag() string { |
| 211 | + flags := append([]string(nil), github.AllowedFeatureFlags...) |
| 212 | + if len(flags) == 0 { |
| 213 | + return "" |
| 214 | + } |
| 215 | + sort.Strings(flags) |
| 216 | + return flags[0] |
61 | 217 | } |
0 commit comments