Skip to content

Commit 012bc0c

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 012bc0c

5 files changed

Lines changed: 502 additions & 6 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
// Leading/trailing newlines around the body produce blank lines between
67+
// our content and the surrounding marker comments, so the trailing comment
68+
// doesn't get absorbed into the final list item by markdown renderers.
69+
return "\n" + strings.TrimSuffix(buf.String(), "\n") + "\n"
70+
}
71+
72+
// flaggedToolDiff returns the tools whose definition (input schema or meta)
73+
// differs from the default-flagged inventory when only the given flag is on,
74+
// plus tools that exist only in the flag-on inventory. Results are sorted by
75+
// tool name.
76+
func flaggedToolDiff(t translations.TranslationHelperFunc, flag string, defaultTools map[string]inventory.ServerTool) []inventory.ServerTool {
77+
flagTools := buildInventoryWithFlags(t, map[string]bool{flag: true}).ToolsForRegistration(context.Background())
78+
79+
out := make([]inventory.ServerTool, 0)
80+
seen := make(map[string]struct{}, len(flagTools))
81+
82+
for _, tool := range flagTools {
83+
if _, ok := seen[tool.Tool.Name]; ok {
84+
continue
85+
}
86+
seen[tool.Tool.Name] = struct{}{}
87+
88+
baseline, hadBaseline := defaultTools[tool.Tool.Name]
89+
if hadBaseline && reflect.DeepEqual(tool.Tool.InputSchema, baseline.Tool.InputSchema) && reflect.DeepEqual(tool.Tool.Meta, baseline.Tool.Meta) {
90+
continue
91+
}
92+
out = append(out, tool)
93+
}
94+
95+
sort.Slice(out, func(i, j int) bool { return out[i].Tool.Name < out[j].Tool.Name })
96+
return out
97+
}
98+
99+
// buildInventoryWithFlags constructs an inventory whose feature checker treats
100+
// the given flags as enabled and every other flag as disabled. Passing nil
101+
// produces the default-flagged inventory.
102+
func buildInventoryWithFlags(t translations.TranslationHelperFunc, enabled map[string]bool) *inventory.Inventory {
103+
checker := func(_ context.Context, flag string) (bool, error) {
104+
return enabled[flag], nil
105+
}
106+
inv, _ := github.NewInventory(t).
107+
WithToolsets([]string{"all"}).
108+
WithFeatureChecker(checker).
109+
Build()
110+
return inv
111+
}
112+
113+
// indexToolsByName returns a map keyed by tool name. When duplicates exist
114+
// (e.g. flag-gated dual registrations), the first occurrence wins, mirroring
115+
// AvailableTools' deterministic sort order.
116+
func indexToolsByName(tools []inventory.ServerTool) map[string]inventory.ServerTool {
117+
out := make(map[string]inventory.ServerTool, len(tools))
118+
for _, tool := range tools {
119+
if _, ok := out[tool.Tool.Name]; ok {
120+
continue
121+
}
122+
out[tool.Tool.Name] = tool
123+
}
124+
return out
125+
}
126+
127+
// rewriteAutomatedSection reads a markdown file, replaces the content between
128+
// the named markers with body, and writes it back.
129+
func rewriteAutomatedSection(path, startMarker, endMarker, body string) error {
130+
content, err := os.ReadFile(path) //#nosec G304
131+
if err != nil {
132+
return fmt.Errorf("failed to read docs file: %w", err)
133+
}
134+
updated, err := replaceSection(string(content), startMarker, endMarker, body)
135+
if err != nil {
136+
return err
137+
}
138+
return os.WriteFile(path, []byte(updated), 0600) //#nosec G306
139+
}

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)