8ec62491c6
* feat(repos): add confirmed repository deletion Add a destructive delete_repository tool that requires an exact owner/repo confirmation through multi-round-trip elicitation. Gate the tool to MCP protocol 2026-07-28 and newer across local and remote transports. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * refactor(inventory): generalize tool availability guards Gate protocol-restricted tools on required elicitation capabilities and enforce direct calls inside the registered handler so SDK result finalization remains intact. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * feat(http): protect MRTR request state Seal repository deletion targets for self-hosted HTTP with a stable AES-256-GCM key. Hide only delete_repository when no key is configured and expose an optional sealer interface for remote integrators. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * fix(repos): expire deletion confirmations Bind sealed repository deletion state to the immutable repository ID and a ten-minute expiry. Re-check identity before deletion so replay cannot affect a recreated repository. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * fix(http): preserve tool and scope restrictions Apply static allowlists before removing unavailable tools and fail closed on invalid configured tool names. Model independent OAuth requirements as conjunctive groups so repository deletion requires both delete_repo and repo. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * fix(repos): require protected confirmation state Give stdio a process-local request-state sealer and make deletion fail closed without one. Preserve legacy any-of OAuth behavior globally while documenting and enforcing delete_repository's conjunctive delete_repo and repo requirements. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * fix(oauth): request repository deletion scope Include delete_repo in the supported OAuth scope set used by stdio login, HTTP protected-resource metadata, and tool filtering. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * fix(oauth): require deletion scope opt-in Keep delete_repo in protected-resource discovery for step-up authorization while excluding it from the default stdio OAuth grant. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * refactor(oauth): derive scope sets from catalog Generate protected-resource supported scopes and the lower-risk default OAuth grant from one canonical scope definition list. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * refactor(scopes): own OAuth scope catalog Move supported and default OAuth scope policy into pkg/scopes so protected-resource metadata and stdio grants derive from the scope domain package. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 * fix(scopes): require workflow scope opt-in Keep workflow and codespace in protected-resource discovery while excluding both from the default OAuth grant alongside delete_repo. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8 --------- Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
532 lines
18 KiB
Go
532 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"slices"
|
|
"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"
|
|
"github.com/google/jsonschema-go/jsonschema"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var generateDocsCmd = &cobra.Command{
|
|
Use: "generate-docs",
|
|
Short: "Generate documentation for tools and toolsets",
|
|
Long: `Generate the automated sections of README.md and docs/remote-server.md with current tool and toolset information.`,
|
|
RunE: func(_ *cobra.Command, _ []string) error {
|
|
return generateAllDocs()
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.AddCommand(generateDocsCmd)
|
|
}
|
|
|
|
// noFeatureFlagsChecker reports every feature flag as disabled. It models the
|
|
// default user experience used by the generated documentation.
|
|
func noFeatureFlagsChecker(_ context.Context, _ string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
func generateAllDocs() error {
|
|
for _, doc := range []struct {
|
|
path string
|
|
fn func(string) error
|
|
}{
|
|
// File to edit, function to generate its docs
|
|
{"README.md", generateReadmeDocs},
|
|
{"docs/remote-server.md", generateRemoteServerDocs},
|
|
{"docs/insiders-features.md", generateInsidersFeaturesDocs},
|
|
{"docs/feature-flags.md", generateFeatureFlagsDocs},
|
|
{"docs/tool-renaming.md", generateDeprecatedAliasesDocs},
|
|
} {
|
|
if err := doc.fn(doc.path); err != nil {
|
|
return fmt.Errorf("failed to generate docs for %s: %w", doc.path, err)
|
|
}
|
|
fmt.Printf("Successfully updated %s with automated documentation\n", doc.path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func generateReadmeDocs(readmePath string) error {
|
|
// Create translation helper
|
|
t, _ := translations.TranslationHelper()
|
|
|
|
// The README documents the default user experience: tools that are
|
|
// enabled with no special flags set. Installing a checker that reports
|
|
// every flag as disabled excludes tools gated by FeatureFlagEnable and
|
|
// keeps the legacy variants of tools gated by FeatureFlagDisable, so
|
|
// flag-gated duplicates don't appear twice.
|
|
// Build() can only fail if WithTools specifies invalid tools - not used here
|
|
r, _ := github.NewInventory(t).
|
|
WithToolsets([]string{"all"}).
|
|
WithFeatureChecker(noFeatureFlagsChecker).
|
|
Build()
|
|
|
|
// Generate toolsets documentation
|
|
toolsetsDoc := generateToolsetsDoc(r)
|
|
|
|
// Generate tools documentation
|
|
toolsDoc := generateToolsDoc(r)
|
|
|
|
// Read the current README.md
|
|
// #nosec G304 - readmePath is controlled by command line flag, not user input
|
|
content, err := os.ReadFile(readmePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read README.md: %w", err)
|
|
}
|
|
|
|
// Replace toolsets section
|
|
updatedContent, err := replaceSection(string(content), "START AUTOMATED TOOLSETS", "END AUTOMATED TOOLSETS", toolsetsDoc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Replace tools section
|
|
updatedContent, err = replaceSection(updatedContent, "START AUTOMATED TOOLS", "END AUTOMATED TOOLS", toolsDoc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write back to file
|
|
err = os.WriteFile(readmePath, []byte(updatedContent), 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write README.md: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func generateRemoteServerDocs(docsPath string) error {
|
|
content, err := os.ReadFile(docsPath) //#nosec G304
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read docs file: %w", err)
|
|
}
|
|
|
|
toolsetsDoc := generateRemoteToolsetsDoc()
|
|
|
|
// Replace content between markers
|
|
updatedContent, err := replaceSection(string(content), "START AUTOMATED TOOLSETS", "END AUTOMATED TOOLSETS", toolsetsDoc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Also generate remote-only toolsets section
|
|
remoteOnlyDoc := generateRemoteOnlyToolsetsDoc()
|
|
updatedContent, err = replaceSection(updatedContent, "START AUTOMATED REMOTE TOOLSETS", "END AUTOMATED REMOTE TOOLSETS", remoteOnlyDoc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(docsPath, []byte(updatedContent), 0600) //#nosec G306
|
|
}
|
|
|
|
// octiconImg returns an img tag for an Octicon that works with GitHub's light/dark theme.
|
|
// Uses picture element with prefers-color-scheme for automatic theme switching.
|
|
// References icons from the repo's pkg/octicons/icons directory.
|
|
// Optional pathPrefix for files in subdirectories (e.g., "../" for docs/).
|
|
func octiconImg(name string, pathPrefix ...string) string {
|
|
if name == "" {
|
|
return ""
|
|
}
|
|
prefix := ""
|
|
if len(pathPrefix) > 0 {
|
|
prefix = pathPrefix[0]
|
|
}
|
|
// Use picture element with media queries for light/dark mode support
|
|
// GitHub renders these correctly in markdown
|
|
lightIcon := fmt.Sprintf("%spkg/octicons/icons/%s-light.png", prefix, name)
|
|
darkIcon := fmt.Sprintf("%spkg/octicons/icons/%s-dark.png", prefix, name)
|
|
return fmt.Sprintf(`<picture><source media="(prefers-color-scheme: dark)" srcset="%s"><source media="(prefers-color-scheme: light)" srcset="%s"><img src="%s" width="20" height="20" alt="%s"></picture>`, darkIcon, lightIcon, lightIcon, name)
|
|
}
|
|
|
|
func generateToolsetsDoc(i *inventory.Inventory) string {
|
|
var buf strings.Builder
|
|
|
|
// Add table header and separator (with icon column)
|
|
buf.WriteString("| | Toolset | Description |\n")
|
|
buf.WriteString("| --- | ----------------------- | ------------------------------------------------------------- |\n")
|
|
|
|
// Add the context toolset row with custom description (strongly recommended)
|
|
// Get context toolset for its icon
|
|
contextIcon := octiconImg("person")
|
|
fmt.Fprintf(&buf, "| %s | `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in |\n", contextIcon)
|
|
|
|
// AvailableToolsets() returns toolsets that have tools, sorted by ID
|
|
// Exclude context (custom description above)
|
|
for _, ts := range i.AvailableToolsets("context") {
|
|
icon := octiconImg(ts.Icon)
|
|
fmt.Fprintf(&buf, "| %s | `%s` | %s |\n", icon, ts.ID, ts.Description)
|
|
}
|
|
|
|
return strings.TrimSuffix(buf.String(), "\n")
|
|
}
|
|
|
|
func generateToolsDoc(r *inventory.Inventory) string {
|
|
tools := r.ToolsForRegistration(context.Background())
|
|
if len(tools) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var buf strings.Builder
|
|
var toolBuf strings.Builder
|
|
var currentToolsetID inventory.ToolsetID
|
|
var currentToolsetIcon string
|
|
firstSection := true
|
|
|
|
writeSection := func() {
|
|
if toolBuf.Len() == 0 {
|
|
return
|
|
}
|
|
if !firstSection {
|
|
buf.WriteString("\n\n")
|
|
}
|
|
firstSection = false
|
|
sectionName := formatToolsetName(string(currentToolsetID))
|
|
icon := octiconImg(currentToolsetIcon)
|
|
if icon != "" {
|
|
icon += " "
|
|
}
|
|
fmt.Fprintf(&buf, "<details>\n\n<summary>%s%s</summary>\n\n%s\n\n</details>", icon, sectionName, strings.TrimSuffix(toolBuf.String(), "\n\n"))
|
|
toolBuf.Reset()
|
|
}
|
|
|
|
for _, tool := range tools {
|
|
// When toolset changes, emit the previous section
|
|
if tool.Toolset.ID != currentToolsetID {
|
|
writeSection()
|
|
currentToolsetID = tool.Toolset.ID
|
|
currentToolsetIcon = tool.Toolset.Icon
|
|
}
|
|
writeToolDoc(&toolBuf, tool)
|
|
toolBuf.WriteString("\n\n")
|
|
}
|
|
|
|
// Emit the last section
|
|
writeSection()
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
func writeToolDoc(buf *strings.Builder, tool inventory.ServerTool) {
|
|
// Tool name (no icon - section header already has the toolset icon)
|
|
fmt.Fprintf(buf, "- **%s** - %s\n", tool.Tool.Name, tool.Tool.Annotations.Title)
|
|
|
|
// OAuth scopes if present
|
|
if len(tool.RequiredScopes) > 0 {
|
|
scopeList := "`" + strings.Join(tool.RequiredScopes, "`, `") + "`"
|
|
switch {
|
|
case len(tool.RequiredScopeGroups) > 1:
|
|
fmt.Fprintf(buf, " - **Required OAuth Scopes (all required)**: %s\n", scopeList)
|
|
case len(tool.RequiredScopes) > 1:
|
|
fmt.Fprintf(buf, " - **Required OAuth Scopes (any of)**: %s\n", scopeList)
|
|
default:
|
|
fmt.Fprintf(buf, " - **Required OAuth Scopes**: %s\n", scopeList)
|
|
}
|
|
|
|
// Only show accepted scopes if they differ from required scopes
|
|
if len(tool.AcceptedScopes) > 0 && !scopesEqual(tool.RequiredScopes, tool.AcceptedScopes) {
|
|
fmt.Fprintf(buf, " - **Accepted OAuth Scopes**: `%s`\n", strings.Join(tool.AcceptedScopes, "`, `"))
|
|
}
|
|
}
|
|
|
|
// MCP App UI metadata (only rendered when the remote_mcp_ui_apps flag
|
|
// applied to the inventory; for the no-flags README this section is
|
|
// stripped by inventory.ToolsForRegistration before rendering).
|
|
if ui, ok := tool.Tool.Meta["ui"].(map[string]any); ok {
|
|
if uri, ok := ui["resourceUri"].(string); ok && uri != "" {
|
|
fmt.Fprintf(buf, " - **MCP App UI**: `%s`\n", uri)
|
|
}
|
|
}
|
|
|
|
// Parameters
|
|
if tool.Tool.InputSchema == nil {
|
|
buf.WriteString(" - No parameters required")
|
|
return
|
|
}
|
|
schema, ok := tool.Tool.InputSchema.(*jsonschema.Schema)
|
|
if !ok || schema == nil {
|
|
buf.WriteString(" - No parameters required")
|
|
return
|
|
}
|
|
|
|
if len(schema.Properties) > 0 {
|
|
// Get parameter names and sort them for deterministic order
|
|
var paramNames []string
|
|
for propName := range schema.Properties {
|
|
paramNames = append(paramNames, propName)
|
|
}
|
|
sort.Strings(paramNames)
|
|
|
|
for i, propName := range paramNames {
|
|
prop := schema.Properties[propName]
|
|
required := slices.Contains(schema.Required, propName)
|
|
requiredStr := "optional"
|
|
if required {
|
|
requiredStr = "required"
|
|
}
|
|
|
|
typeStr := schemaTypeString(prop)
|
|
|
|
// Indent any continuation lines in the description to maintain markdown formatting
|
|
description := indentMultilineDescription(prop.Description, " ")
|
|
|
|
fmt.Fprintf(buf, " - `%s`: %s (%s, %s)", propName, description, typeStr, requiredStr)
|
|
if i < len(paramNames)-1 {
|
|
buf.WriteString("\n")
|
|
}
|
|
}
|
|
} else {
|
|
buf.WriteString(" - No parameters required")
|
|
}
|
|
}
|
|
|
|
func schemaTypeString(schema *jsonschema.Schema) string {
|
|
switch {
|
|
case schema.Type == "array":
|
|
if schema.Items != nil {
|
|
return schema.Items.Type + "[]"
|
|
}
|
|
return "array"
|
|
case schema.Type != "":
|
|
return schema.Type
|
|
case len(schema.Types) > 0:
|
|
return strings.Join(schema.Types, " | ")
|
|
}
|
|
|
|
var union []*jsonschema.Schema
|
|
switch {
|
|
case len(schema.AnyOf) > 0:
|
|
union = schema.AnyOf
|
|
case len(schema.OneOf) > 0:
|
|
union = schema.OneOf
|
|
default:
|
|
// A schema without type constraints accepts any value.
|
|
return "any"
|
|
}
|
|
|
|
types := make([]string, 0, len(union))
|
|
for _, member := range union {
|
|
memberType := schemaTypeString(member)
|
|
if !slices.Contains(types, memberType) {
|
|
types = append(types, memberType)
|
|
}
|
|
}
|
|
return strings.Join(types, " | ")
|
|
}
|
|
|
|
// scopesEqual checks if two scope slices contain the same elements (order-independent)
|
|
func scopesEqual(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
|
|
// Create a map for quick lookup
|
|
aMap := make(map[string]bool, len(a))
|
|
for _, scope := range a {
|
|
aMap[scope] = true
|
|
}
|
|
|
|
// Check if all elements in b are in a
|
|
for _, scope := range b {
|
|
if !aMap[scope] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// indentMultilineDescription adds the specified indent to all lines after the first line.
|
|
// This ensures that multi-line descriptions maintain proper markdown list formatting.
|
|
func indentMultilineDescription(description, indent string) string {
|
|
if !strings.Contains(description, "\n") {
|
|
return description
|
|
}
|
|
var buf strings.Builder
|
|
lines := strings.Split(description, "\n")
|
|
buf.WriteString(lines[0])
|
|
for i := 1; i < len(lines); i++ {
|
|
buf.WriteString("\n")
|
|
buf.WriteString(indent)
|
|
buf.WriteString(lines[i])
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func replaceSection(content, startMarker, endMarker, newContent string) (string, error) {
|
|
start := fmt.Sprintf("<!-- %s -->", startMarker)
|
|
end := fmt.Sprintf("<!-- %s -->", endMarker)
|
|
|
|
before, _, ok := strings.Cut(content, start)
|
|
endIdx := strings.Index(content, end)
|
|
if !ok || endIdx == -1 {
|
|
return "", fmt.Errorf("markers not found: %s / %s", start, end)
|
|
}
|
|
|
|
var buf strings.Builder
|
|
buf.WriteString(before)
|
|
buf.WriteString(start)
|
|
buf.WriteString("\n")
|
|
buf.WriteString(newContent)
|
|
buf.WriteString("\n")
|
|
buf.WriteString(content[endIdx:])
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func generateRemoteToolsetsDoc() string {
|
|
var buf strings.Builder
|
|
|
|
// Create translation helper
|
|
t, _ := translations.TranslationHelper()
|
|
|
|
// Build inventory - stateless
|
|
// Build() can only fail if WithTools specifies invalid tools - not used here
|
|
r, _ := github.NewInventory(t).Build()
|
|
|
|
// Generate table header (icon is combined with Name column)
|
|
buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n")
|
|
buf.WriteString("| ---- | ----------- | ------- | ------------------------- | -------------- | ----------------------------------- |\n")
|
|
|
|
// Add "default" and "all" meta toolsets first (special cases). The base
|
|
// URL serves the default toolset; /x/all enables every toolset at once.
|
|
metaIcon := octiconImg("apps", "../")
|
|
fmt.Fprintf(&buf, "| %s<br>`default` | Default toolset | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2F%%22%%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2Freadonly%%22%%7D) |\n", metaIcon)
|
|
fmt.Fprintf(&buf, "| %s<br>`all` | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/x/all | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-all&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2Fx%%2Fall%%22%%7D) | [read-only](https://api.githubcopilot.com/mcp/x/all/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-all&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2Fx%%2Fall%%2Freadonly%%22%%7D) |\n", metaIcon)
|
|
|
|
// AvailableToolsets() returns toolsets that have tools, sorted by ID
|
|
// Exclude context (handled separately)
|
|
for _, ts := range r.AvailableToolsets("context") {
|
|
idStr := string(ts.ID)
|
|
|
|
apiURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s", idStr)
|
|
readonlyURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s/readonly", idStr)
|
|
|
|
// Create install config JSON (URL encoded)
|
|
installConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, apiURL))
|
|
readonlyConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, readonlyURL))
|
|
|
|
// Fix URL encoding to use %20 instead of + for spaces
|
|
installConfig = strings.ReplaceAll(installConfig, "+", "%20")
|
|
readonlyConfig = strings.ReplaceAll(readonlyConfig, "+", "%20")
|
|
|
|
installLink := fmt.Sprintf("[Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, installConfig)
|
|
readonlyInstallLink := fmt.Sprintf("[Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, readonlyConfig)
|
|
|
|
icon := octiconImg(ts.Icon, "../")
|
|
fmt.Fprintf(&buf, "| %s<br>`%s` | %s | %s | %s | [read-only](%s) | %s |\n",
|
|
icon,
|
|
idStr,
|
|
ts.Description,
|
|
apiURL,
|
|
installLink,
|
|
readonlyURL,
|
|
readonlyInstallLink,
|
|
)
|
|
}
|
|
|
|
return strings.TrimSuffix(buf.String(), "\n")
|
|
}
|
|
|
|
func generateRemoteOnlyToolsetsDoc() string {
|
|
var buf strings.Builder
|
|
|
|
// Generate table header (icon is combined with Name column)
|
|
buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n")
|
|
buf.WriteString("| ---- | ----------- | ------- | ------------------------- | -------------- | ----------------------------------- |\n")
|
|
|
|
// Use RemoteOnlyToolsets from github package
|
|
for _, ts := range github.RemoteOnlyToolsets() {
|
|
idStr := string(ts.ID)
|
|
|
|
apiURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s", idStr)
|
|
readonlyURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s/readonly", idStr)
|
|
|
|
// Create install config JSON (URL encoded)
|
|
installConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, apiURL))
|
|
readonlyConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, readonlyURL))
|
|
|
|
// Fix URL encoding to use %20 instead of + for spaces
|
|
installConfig = strings.ReplaceAll(installConfig, "+", "%20")
|
|
readonlyConfig = strings.ReplaceAll(readonlyConfig, "+", "%20")
|
|
|
|
installLink := fmt.Sprintf("[Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, installConfig)
|
|
readonlyInstallLink := fmt.Sprintf("[Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, readonlyConfig)
|
|
|
|
icon := octiconImg(ts.Icon, "../")
|
|
fmt.Fprintf(&buf, "| %s<br>`%s` | %s | %s | %s | [read-only](%s) | %s |\n",
|
|
icon,
|
|
idStr,
|
|
ts.Description,
|
|
apiURL,
|
|
installLink,
|
|
readonlyURL,
|
|
readonlyInstallLink,
|
|
)
|
|
}
|
|
|
|
return strings.TrimSuffix(buf.String(), "\n")
|
|
}
|
|
|
|
func generateDeprecatedAliasesDocs(docsPath string) error {
|
|
// Read the current file
|
|
content, err := os.ReadFile(docsPath) //#nosec G304
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read docs file: %w", err)
|
|
}
|
|
|
|
// Generate the table
|
|
aliasesDoc := generateDeprecatedAliasesTable()
|
|
|
|
// Replace content between markers
|
|
updatedContent, err := replaceSection(string(content), "START AUTOMATED ALIASES", "END AUTOMATED ALIASES", aliasesDoc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write back to file
|
|
err = os.WriteFile(docsPath, []byte(updatedContent), 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write deprecated aliases docs: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func generateDeprecatedAliasesTable() string {
|
|
var buf strings.Builder
|
|
|
|
// Add table header
|
|
buf.WriteString("| Old Name | New Name |\n")
|
|
buf.WriteString("|----------|----------|\n")
|
|
|
|
aliases := github.DeprecatedToolAliases
|
|
if len(aliases) == 0 {
|
|
buf.WriteString("| *(none currently)* | |")
|
|
} else {
|
|
// Sort keys for deterministic output
|
|
var oldNames []string
|
|
for oldName := range aliases {
|
|
oldNames = append(oldNames, oldName)
|
|
}
|
|
sort.Strings(oldNames)
|
|
|
|
for i, oldName := range oldNames {
|
|
newName := aliases[oldName]
|
|
fmt.Fprintf(&buf, "| `%s` | `%s` |", oldName, newName)
|
|
if i < len(oldNames)-1 {
|
|
buf.WriteString("\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
return buf.String()
|
|
}
|