1add5fe231
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>
140 lines
4.9 KiB
Go
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
|
|
}
|