Skip to content

Commit 1193cdf

Browse files
docs: auto-generate per-flag tool lists for insiders and feature flags
Adds two auto-generated documentation sections that describe how feature flags shape the tool surface: - docs/insiders-features.md gets a per-flag block under its existing hand-written prose. Each Insiders flag whose tools differ from the default surface is listed with the full tool schema rendered through the same writer used for README, so contributors can see exactly what Insiders Mode adds or changes. - docs/feature-flags.md is new and gives the same treatment to every flag in AllowedFeatureFlags (user-controllable flags). It links back to the Insiders doc for the auto-enabled subset. Both sections are produced by a single generator that diffs the flag-on inventory against the default-flagged inventory and reports any tool that is new or has a different InputSchema/Meta. No reason classification - just tools and their schemas, kept intentionally simple so contributors don't have to update the generator when adding a new flag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6fd9d07 commit 1193cdf

5 files changed

Lines changed: 494 additions & 6 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"reflect"
8+
"sort"
9+
"strings"
10+
11+
"github.com/github/github-mcp-server/pkg/github"
12+
"github.com/github/github-mcp-server/pkg/inventory"
13+
"github.com/github/github-mcp-server/pkg/translations"
14+
)
15+
16+
// generateInsidersFeaturesDocs refreshes the auto-generated section of
17+
// docs/insiders-features.md with the tools and schemas affected by each
18+
// Insiders feature flag.
19+
func generateInsidersFeaturesDocs(docsPath string) error {
20+
body := generateFlaggedToolsDoc(github.InsidersFeatureFlags, "_No Insiders-only tool changes._")
21+
return rewriteAutomatedSection(docsPath, "START AUTOMATED INSIDERS TOOLS", "END AUTOMATED INSIDERS TOOLS", body)
22+
}
23+
24+
// generateFeatureFlagsDocs refreshes the auto-generated section of
25+
// docs/feature-flags.md with the tools and schemas affected by each
26+
// user-controllable feature flag.
27+
func generateFeatureFlagsDocs(docsPath string) error {
28+
body := generateFlaggedToolsDoc(github.AllowedFeatureFlags, "_No user-controllable feature flags affect tool registration._")
29+
return rewriteAutomatedSection(docsPath, "START AUTOMATED FEATURE FLAG TOOLS", "END AUTOMATED FEATURE FLAG TOOLS", body)
30+
}
31+
32+
// generateFlaggedToolsDoc renders, for each flag in the input set, the tools
33+
// whose registration or definition differs from the default user experience.
34+
// Each affected tool is printed with its full schema using the same writer
35+
// used by the README so the output style stays consistent.
36+
func generateFlaggedToolsDoc(flags []string, emptyMessage string) string {
37+
t, _ := translations.TranslationHelper()
38+
defaultTools := indexToolsByName(buildInventoryWithFlags(t, nil).ToolsForRegistration(context.Background()))
39+
40+
var buf strings.Builder
41+
hasAny := false
42+
43+
for _, flag := range flags {
44+
affected := flaggedToolDiff(t, flag, defaultTools)
45+
if len(affected) == 0 {
46+
continue
47+
}
48+
49+
if hasAny {
50+
buf.WriteString("\n\n")
51+
}
52+
hasAny = true
53+
54+
fmt.Fprintf(&buf, "### `%s`\n\n", flag)
55+
for i, tool := range affected {
56+
writeToolDoc(&buf, tool)
57+
if i < len(affected)-1 {
58+
buf.WriteString("\n\n")
59+
}
60+
}
61+
}
62+
63+
if !hasAny {
64+
return emptyMessage
65+
}
66+
return strings.TrimSuffix(buf.String(), "\n")
67+
}
68+
69+
// flaggedToolDiff returns the tools whose definition (input schema or meta)
70+
// differs from the default-flagged inventory when only the given flag is on,
71+
// plus tools that exist only in the flag-on inventory. Results are sorted by
72+
// tool name.
73+
func flaggedToolDiff(t translations.TranslationHelperFunc, flag string, defaultTools map[string]inventory.ServerTool) []inventory.ServerTool {
74+
flagTools := buildInventoryWithFlags(t, map[string]bool{flag: true}).ToolsForRegistration(context.Background())
75+
76+
out := make([]inventory.ServerTool, 0)
77+
seen := make(map[string]struct{}, len(flagTools))
78+
79+
for _, tool := range flagTools {
80+
if _, ok := seen[tool.Tool.Name]; ok {
81+
continue
82+
}
83+
seen[tool.Tool.Name] = struct{}{}
84+
85+
baseline, hadBaseline := defaultTools[tool.Tool.Name]
86+
if hadBaseline && reflect.DeepEqual(tool.Tool.InputSchema, baseline.Tool.InputSchema) && reflect.DeepEqual(tool.Tool.Meta, baseline.Tool.Meta) {
87+
continue
88+
}
89+
out = append(out, tool)
90+
}
91+
92+
sort.Slice(out, func(i, j int) bool { return out[i].Tool.Name < out[j].Tool.Name })
93+
return out
94+
}
95+
96+
// buildInventoryWithFlags constructs an inventory whose feature checker treats
97+
// the given flags as enabled and every other flag as disabled. Passing nil
98+
// produces the default-flagged inventory.
99+
func buildInventoryWithFlags(t translations.TranslationHelperFunc, enabled map[string]bool) *inventory.Inventory {
100+
checker := func(_ context.Context, flag string) (bool, error) {
101+
return enabled[flag], nil
102+
}
103+
inv, _ := github.NewInventory(t).
104+
WithToolsets([]string{"all"}).
105+
WithFeatureChecker(checker).
106+
Build()
107+
return inv
108+
}
109+
110+
// indexToolsByName returns a map keyed by tool name. When duplicates exist
111+
// (e.g. flag-gated dual registrations), the first occurrence wins, mirroring
112+
// AvailableTools' deterministic sort order.
113+
func indexToolsByName(tools []inventory.ServerTool) map[string]inventory.ServerTool {
114+
out := make(map[string]inventory.ServerTool, len(tools))
115+
for _, tool := range tools {
116+
if _, ok := out[tool.Tool.Name]; ok {
117+
continue
118+
}
119+
out[tool.Tool.Name] = tool
120+
}
121+
return out
122+
}
123+
124+
// rewriteAutomatedSection reads a markdown file, replaces the content between
125+
// the named markers with body, and writes it back.
126+
func rewriteAutomatedSection(path, startMarker, endMarker, body string) error {
127+
content, err := os.ReadFile(path) //#nosec G304
128+
if err != nil {
129+
return fmt.Errorf("failed to read docs file: %w", err)
130+
}
131+
updated, err := replaceSection(string(content), startMarker, endMarker, body)
132+
if err != nil {
133+
return err
134+
}
135+
return os.WriteFile(path, []byte(updated), 0600) //#nosec G306
136+
}

cmd/github-mcp-server/generate_docs.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ func generateAllDocs() error {
4343
// File to edit, function to generate its docs
4444
{"README.md", generateReadmeDocs},
4545
{"docs/remote-server.md", generateRemoteServerDocs},
46+
{"docs/insiders-features.md", generateInsidersFeaturesDocs},
47+
{"docs/feature-flags.md", generateFeatureFlagsDocs},
4648
{"docs/tool-renaming.md", generateDeprecatedAliasesDocs},
4749
} {
4850
if err := doc.fn(doc.path); err != nil {
@@ -168,7 +170,7 @@ func generateToolsetsDoc(i *inventory.Inventory) string {
168170
}
169171

170172
func generateToolsDoc(r *inventory.Inventory) string {
171-
tools := r.AvailableTools(context.Background())
173+
tools := r.ToolsForRegistration(context.Background())
172174
if len(tools) == 0 {
173175
return ""
174176
}
@@ -227,6 +229,15 @@ func writeToolDoc(buf *strings.Builder, tool inventory.ServerTool) {
227229
}
228230
}
229231

232+
// MCP App UI metadata (only rendered when the remote_mcp_ui_apps flag
233+
// applied to the inventory; for the no-flags README this section is
234+
// stripped by inventory.ToolsForRegistration before rendering).
235+
if ui, ok := tool.Tool.Meta["ui"].(map[string]any); ok {
236+
if uri, ok := ui["resourceUri"].(string); ok && uri != "" {
237+
fmt.Fprintf(buf, " - **MCP App UI**: `%s`\n", uri)
238+
}
239+
}
240+
230241
// Parameters
231242
if tool.Tool.InputSchema == nil {
232243
buf.WriteString(" - No parameters required")

0 commit comments

Comments
 (0)