Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ You can also configure specific tools using the `--tools` flag. Tools can be use
- Tools, toolsets, and dynamic toolsets can all be used together
- Read-only mode takes priority: write tools are skipped if `--read-only` is set, even if explicitly requested via `--tools`
- Tool names must match exactly (e.g., `get_file_contents`, not `getFileContents`). Invalid tool names will cause the server to fail at startup with an error message
- When tools are renamed, old names are preserved as aliases for backward compatibility. See [Deprecated Tool Aliases](docs/deprecated-tool-aliases.md) for details.

### Using Toolsets With Docker

Expand Down Expand Up @@ -459,7 +460,6 @@ The following sets of tools are available:
| `code_security` | Code security related tools, such as GitHub Code Scanning |
| `dependabot` | Dependabot tools |
| `discussions` | GitHub Discussions related tools |
| `experiments` | Experimental features that are not considered stable yet |
| `gists` | GitHub Gist related tools |
| `git` | GitHub Git API related tools for low-level Git operations |
| `issues` | GitHub Issues related tools |
Expand Down Expand Up @@ -718,11 +718,6 @@ The following sets of tools are available:
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)

- **get_label** - Get a specific label from a repository.
- `name`: Label name. (string, required)
- `owner`: Repository owner (username or organization name) (string, required)
- `repo`: Repository name (string, required)

- **issue_read** - Get issue details
- `issue_number`: The number of the issue (number, required)
- `method`: The read operation to perform on a single issue.
Expand Down
147 changes: 89 additions & 58 deletions cmd/github-mcp-server/generate_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ import (
"strings"

"github.com/github/github-mcp-server/pkg/github"
"github.com/github/github-mcp-server/pkg/lockdown"
"github.com/github/github-mcp-server/pkg/raw"
"github.com/github/github-mcp-server/pkg/toolsets"
"github.com/github/github-mcp-server/pkg/translations"
gogithub "github.com/google/go-github/v79/github"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/shurcooL/githubv4"
"github.com/spf13/cobra"
)

Expand All @@ -39,11 +37,6 @@ func mockGetClient(_ context.Context) (*gogithub.Client, error) {
return gogithub.NewClient(nil), nil
}

// mockGetGQLClient returns a mock GraphQL client for documentation generation
func mockGetGQLClient(_ context.Context) (*githubv4.Client, error) {
return githubv4.NewClient(nil), nil
}

// mockGetRawClient returns a mock raw client for documentation generation
func mockGetRawClient(_ context.Context) (*raw.Client, error) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sure this can be removed too...

return nil, nil
Expand All @@ -58,16 +51,19 @@ func generateAllDocs() error {
return fmt.Errorf("failed to generate remote-server docs: %w", err)
}

if err := generateDeprecatedAliasesDocs("docs/deprecated-tool-aliases.md"); err != nil {
return fmt.Errorf("failed to generate deprecated aliases docs: %w", err)
}

return nil
}

func generateReadmeDocs(readmePath string) error {
// Create translation helper
t, _ := translations.TranslationHelper()

// Create toolset group with mock clients
repoAccessCache := lockdown.GetInstance(nil)
tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t, 5000, github.FeatureFlags{}, repoAccessCache)
// Create toolset group with mock clients (no deps needed for doc generation)
tsg := github.NewToolsetGroup(t, mockGetClient, mockGetRawClient)

// Generate toolsets documentation
toolsetsDoc := generateToolsetsDoc(tsg)
Expand Down Expand Up @@ -133,20 +129,16 @@ func generateToolsetsDoc(tsg *toolsets.ToolsetGroup) string {
// Add the context toolset row (handled separately in README)
lines = append(lines, "| `context` | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in |")

// Get all toolsets except context (which is handled separately above)
var toolsetNames []string
for name := range tsg.Toolsets {
if name != "context" && name != "dynamic" { // Skip context and dynamic toolsets as they're handled separately
toolsetNames = append(toolsetNames, name)
}
}

// Sort toolset names for consistent output
sort.Strings(toolsetNames)
// Get toolset IDs and descriptions
toolsetIDs := tsg.ToolsetIDs()
descriptions := tsg.ToolsetDescriptions()

for _, name := range toolsetNames {
toolset := tsg.Toolsets[name]
lines = append(lines, fmt.Sprintf("| `%s` | %s |", name, toolset.Description))
// Filter out context and dynamic toolsets (handled separately)
for _, id := range toolsetIDs {
if id != "context" && id != "dynamic" {
description := descriptions[id]
lines = append(lines, fmt.Sprintf("| `%s` | %s |", id, description))
}
}

return strings.Join(lines, "\n")
Expand All @@ -155,30 +147,22 @@ func generateToolsetsDoc(tsg *toolsets.ToolsetGroup) string {
func generateToolsDoc(tsg *toolsets.ToolsetGroup) string {
var sections []string

// Get all toolset names and sort them alphabetically for deterministic order
var toolsetNames []string
for name := range tsg.Toolsets {
if name != "dynamic" { // Skip dynamic toolset as it's handled separately
toolsetNames = append(toolsetNames, name)
}
}
sort.Strings(toolsetNames)
// Get toolset IDs (already sorted deterministically)
toolsetIDs := tsg.ToolsetIDs()

for _, toolsetName := range toolsetNames {
toolset := tsg.Toolsets[toolsetName]
for _, toolsetID := range toolsetIDs {
if toolsetID == "dynamic" { // Skip dynamic toolset as it's handled separately
continue
}

tools := toolset.GetAvailableTools()
// Get tools for this toolset (already sorted deterministically)
tools := tsg.ToolsForToolset(toolsetID)
if len(tools) == 0 {
continue
}

// Sort tools by name for deterministic order
sort.Slice(tools, func(i, j int) bool {
return tools[i].Tool.Name < tools[j].Tool.Name
})

// Generate section header - capitalize first letter and replace underscores
sectionName := formatToolsetName(toolsetName)
sectionName := formatToolsetName(string(toolsetID))

var toolDocs []string
for _, serverTool := range tools {
Expand Down Expand Up @@ -322,33 +306,30 @@ func generateRemoteToolsetsDoc() string {
t, _ := translations.TranslationHelper()

// Create toolset group with mock clients
repoAccessCache := lockdown.GetInstance(nil)
tsg := github.DefaultToolsetGroup(false, mockGetClient, mockGetGQLClient, mockGetRawClient, t, 5000, github.FeatureFlags{}, repoAccessCache)
tsg := github.NewToolsetGroup(t, mockGetClient, mockGetRawClient)

// Generate table header
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")

// Get all toolsets
toolsetNames := make([]string, 0, len(tsg.Toolsets))
for name := range tsg.Toolsets {
if name != "context" && name != "dynamic" { // Skip context and dynamic toolsets as they're handled separately
toolsetNames = append(toolsetNames, name)
}
}
sort.Strings(toolsetNames)
// Get toolset IDs and descriptions
toolsetIDs := tsg.ToolsetIDs()
descriptions := tsg.ToolsetDescriptions()

// Add "all" toolset first (special case)
buf.WriteString("| all | All available GitHub MCP tools | 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")

// Add individual toolsets
for _, name := range toolsetNames {
toolset := tsg.Toolsets[name]
for _, id := range toolsetIDs {
idStr := string(id)
if idStr == "context" || idStr == "dynamic" { // Skip context and dynamic toolsets as they're handled separately
continue
}

formattedName := formatToolsetName(name)
description := toolset.Description
apiURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s", name)
readonlyURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s/readonly", name)
description := descriptions[id]
formattedName := formatToolsetName(idStr)
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))
Expand All @@ -358,8 +339,8 @@ func generateRemoteToolsetsDoc() string {
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)", name, installConfig)
readonlyInstallLink := fmt.Sprintf("[Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", name, readonlyConfig)
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)

buf.WriteString(fmt.Sprintf("| %-14s | %-48s | %-53s | %-218s | %-110s | %-288s |\n",
formattedName,
Expand All @@ -373,3 +354,53 @@ func generateRemoteToolsetsDoc() string {

return buf.String()
}

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 := replaceSection(string(content), "START AUTOMATED ALIASES", "END AUTOMATED ALIASES", aliasesDoc)

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

fmt.Println("Successfully updated docs/deprecated-tool-aliases.md with automated documentation")
return nil
}

func generateDeprecatedAliasesTable() string {
var lines []string

// Add table header
lines = append(lines, "| Old Name | New Name |")
lines = append(lines, "|----------|----------|")

aliases := github.DeprecatedToolAliases
if len(aliases) == 0 {
lines = append(lines, "| *(none currently)* | |")
} else {
// Sort keys for deterministic output
var oldNames []string
for oldName := range aliases {
oldNames = append(oldNames, oldName)
}
sort.Strings(oldNames)

for _, oldName := range oldNames {
newName := aliases[oldName]
lines = append(lines, fmt.Sprintf("| `%s` | `%s` |", oldName, newName))
}
}

return strings.Join(lines, "\n")
}
20 changes: 15 additions & 5 deletions cmd/github-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,27 @@ var (
// it's because viper doesn't handle comma-separated values correctly for env
// vars when using GetStringSlice.
// https://github.com/spf13/viper/issues/380
//
// Additionally, viper.UnmarshalKey returns an empty slice even when the flag
// is not set, but we need nil to indicate "use defaults". So we check IsSet first.
var enabledToolsets []string
if err := viper.UnmarshalKey("toolsets", &enabledToolsets); err != nil {
return fmt.Errorf("failed to unmarshal toolsets: %w", err)
if viper.IsSet("toolsets") {
if err := viper.UnmarshalKey("toolsets", &enabledToolsets); err != nil {
return fmt.Errorf("failed to unmarshal toolsets: %w", err)
}
}
// else: enabledToolsets stays nil, meaning "use defaults"

// Parse tools (similar to toolsets)
var enabledTools []string
if err := viper.UnmarshalKey("tools", &enabledTools); err != nil {
return fmt.Errorf("failed to unmarshal tools: %w", err)
}

// If neither toolset config nor tools config is passed we enable the default toolset
if len(enabledToolsets) == 0 && len(enabledTools) == 0 {
enabledToolsets = []string{github.ToolsetMetadataDefault.ID}
// Parse enabled features (similar to toolsets)
var enabledFeatures []string
if err := viper.UnmarshalKey("features", &enabledFeatures); err != nil {
return fmt.Errorf("failed to unmarshal features: %w", err)
}

ttl := viper.GetDuration("repo-access-cache-ttl")
Expand All @@ -64,6 +71,7 @@ var (
Token: token,
EnabledToolsets: enabledToolsets,
EnabledTools: enabledTools,
EnabledFeatures: enabledFeatures,
DynamicToolsets: viper.GetBool("dynamic_toolsets"),
ReadOnly: viper.GetBool("read-only"),
ExportTranslations: viper.GetBool("export-translations"),
Expand All @@ -87,6 +95,7 @@ func init() {
// Add global flags that will be shared by all commands
rootCmd.PersistentFlags().StringSlice("toolsets", nil, github.GenerateToolsetsHelp())
rootCmd.PersistentFlags().StringSlice("tools", nil, "Comma-separated list of specific tools to enable")
rootCmd.PersistentFlags().StringSlice("features", nil, "Comma-separated list of feature flags to enable")
rootCmd.PersistentFlags().Bool("dynamic-toolsets", false, "Enable dynamic toolsets")
rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server to read-only operations")
rootCmd.PersistentFlags().String("log-file", "", "Path to log file")
Expand All @@ -100,6 +109,7 @@ func init() {
// Bind flag to viper
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
_ = viper.BindPFlag("tools", rootCmd.PersistentFlags().Lookup("tools"))
_ = viper.BindPFlag("features", rootCmd.PersistentFlags().Lookup("features"))
_ = viper.BindPFlag("dynamic_toolsets", rootCmd.PersistentFlags().Lookup("dynamic-toolsets"))
_ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
Expand Down
31 changes: 31 additions & 0 deletions docs/deprecated-tool-aliases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Deprecated Tool Aliases

This document tracks tool renames in the GitHub MCP Server. When tools are renamed, the old names are preserved as aliases for backward compatibility. Using a deprecated alias will still work, but clients should migrate to the new canonical name.

## Current Deprecations

<!-- START AUTOMATED ALIASES -->
| Old Name | New Name |
|----------|----------|
| *(none currently)* | |
<!-- END AUTOMATED ALIASES -->

## How It Works

When a tool is renamed:

1. The old name is added to `DeprecatedToolAliases` in [pkg/github/deprecated_tool_aliases.go](../pkg/github/deprecated_tool_aliases.go)
2. Clients using the old name will receive the new tool
3. A deprecation notice is logged when the alias is used

## For Developers

To deprecate a tool name when renaming:

```go
var DeprecatedToolAliases = map[string]string{
"old_tool_name": "new_tool_name",
}
```

The alias resolution happens at server startup, ensuring backward compatibility for existing client configurations.
Loading