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
|
|
}
|