Files
Sam Morrow 1add5fe231 docs: auto-generate per-flag tool lists for insiders and feature flags (#2521)
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>
2026-05-22 00:31:39 +02:00

140 lines
4.9 KiB
Go

package main
import (
"context"
"fmt"
"os"
"reflect"
"sort"
"strings"
"github.com/github/github-mcp-server/pkg/github"
"github.com/github/github-mcp-server/pkg/inventory"
"github.com/github/github-mcp-server/pkg/translations"
)
// generateInsidersFeaturesDocs refreshes the auto-generated section of
// docs/insiders-features.md with the tools and schemas affected by each
// Insiders feature flag.
func generateInsidersFeaturesDocs(docsPath string) error {
body := generateFlaggedToolsDoc(github.InsidersFeatureFlags, "_No Insiders-only tool changes._")
return rewriteAutomatedSection(docsPath, "START AUTOMATED INSIDERS TOOLS", "END AUTOMATED INSIDERS TOOLS", body)
}
// generateFeatureFlagsDocs refreshes the auto-generated section of
// docs/feature-flags.md with the tools and schemas affected by each
// user-controllable feature flag.
func generateFeatureFlagsDocs(docsPath string) error {
body := generateFlaggedToolsDoc(github.AllowedFeatureFlags, "_No user-controllable feature flags affect tool registration._")
return rewriteAutomatedSection(docsPath, "START AUTOMATED FEATURE FLAG TOOLS", "END AUTOMATED FEATURE FLAG TOOLS", body)
}
// generateFlaggedToolsDoc renders, for each flag in the input set, the tools
// whose registration or definition differs from the default user experience.
// Each affected tool is printed with its full schema using the same writer
// used by the README so the output style stays consistent.
func generateFlaggedToolsDoc(flags []string, emptyMessage string) string {
t, _ := translations.TranslationHelper()
defaultTools := indexToolsByName(buildInventoryWithFlags(t, nil).ToolsForRegistration(context.Background()))
var buf strings.Builder
hasAny := false
for _, flag := range flags {
affected := flaggedToolDiff(t, flag, defaultTools)
if len(affected) == 0 {
continue
}
if hasAny {
buf.WriteString("\n\n")
}
hasAny = true
fmt.Fprintf(&buf, "### `%s`\n\n", flag)
for i, tool := range affected {
writeToolDoc(&buf, tool)
if i < len(affected)-1 {
buf.WriteString("\n\n")
}
}
}
if !hasAny {
return emptyMessage
}
// Leading/trailing newlines around the body produce blank lines between
// our content and the surrounding marker comments, so the trailing comment
// doesn't get absorbed into the final list item by markdown renderers.
return "\n" + strings.TrimSuffix(buf.String(), "\n") + "\n"
}
// flaggedToolDiff returns the tools whose definition (input schema or meta)
// differs from the default-flagged inventory when only the given flag is on,
// plus tools that exist only in the flag-on inventory. Results are sorted by
// tool name.
func flaggedToolDiff(t translations.TranslationHelperFunc, flag string, defaultTools map[string]inventory.ServerTool) []inventory.ServerTool {
flagTools := buildInventoryWithFlags(t, map[string]bool{flag: true}).ToolsForRegistration(context.Background())
out := make([]inventory.ServerTool, 0)
seen := make(map[string]struct{}, len(flagTools))
for _, tool := range flagTools {
if _, ok := seen[tool.Tool.Name]; ok {
continue
}
seen[tool.Tool.Name] = struct{}{}
baseline, hadBaseline := defaultTools[tool.Tool.Name]
if hadBaseline && reflect.DeepEqual(tool.Tool.InputSchema, baseline.Tool.InputSchema) && reflect.DeepEqual(tool.Tool.Meta, baseline.Tool.Meta) {
continue
}
out = append(out, tool)
}
sort.Slice(out, func(i, j int) bool { return out[i].Tool.Name < out[j].Tool.Name })
return out
}
// buildInventoryWithFlags constructs an inventory whose feature checker treats
// the given flags as enabled and every other flag as disabled. Passing nil
// produces the default-flagged inventory.
func buildInventoryWithFlags(t translations.TranslationHelperFunc, enabled map[string]bool) *inventory.Inventory {
checker := func(_ context.Context, flag string) (bool, error) {
return enabled[flag], nil
}
inv, _ := github.NewInventory(t).
WithToolsets([]string{"all"}).
WithFeatureChecker(checker).
Build()
return inv
}
// indexToolsByName returns a map keyed by tool name. When duplicates exist
// (e.g. flag-gated dual registrations), the first occurrence wins, mirroring
// AvailableTools' deterministic sort order.
func indexToolsByName(tools []inventory.ServerTool) map[string]inventory.ServerTool {
out := make(map[string]inventory.ServerTool, len(tools))
for _, tool := range tools {
if _, ok := out[tool.Tool.Name]; ok {
continue
}
out[tool.Tool.Name] = tool
}
return out
}
// rewriteAutomatedSection reads a markdown file, replaces the content between
// the named markers with body, and writes it back.
func rewriteAutomatedSection(path, startMarker, endMarker, body string) error {
content, err := os.ReadFile(path) //#nosec G304
if err != nil {
return fmt.Errorf("failed to read docs file: %w", err)
}
updated, err := replaceSection(string(content), startMarker, endMarker, body)
if err != nil {
return err
}
return os.WriteFile(path, []byte(updated), 0600) //#nosec G306
}