Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ff237aa
Add --features CLI flag for feature flag support
SamMorrowDrums Dec 13, 2025
7a78ec6
Add validation tests for tools, resources, and prompts metadata
SamMorrowDrums Dec 13, 2025
689a040
Fix default toolsets behavior when not in dynamic mode
SamMorrowDrums Dec 13, 2025
a03b3bf
refactor: address PR review feedback for toolsets
SamMorrowDrums Dec 14, 2025
8b850d0
refactor: Apply HandlerFunc pattern to resources for stateless NewToo…
SamMorrowDrums Dec 14, 2025
b13d9fe
refactor: simplify ForMCPRequest switch cases
SamMorrowDrums Dec 14, 2025
74d2c56
refactor(generate_docs): use strings.Builder and AllTools() iteration
SamMorrowDrums Dec 14, 2025
9bcf526
feat(toolsets): add AvailableToolsets() with exclude filter
SamMorrowDrums Dec 14, 2025
2142b02
refactor(generate_docs): hoist success logging to generateAllDocs
SamMorrowDrums Dec 14, 2025
6b6a874
refactor: consolidate toolset validation into ToolsetGroup
SamMorrowDrums Dec 14, 2025
3c44fdd
refactor: rename toolsets package to registry with builder pattern
SamMorrowDrums Dec 15, 2025
e943fd1
fix: remove unnecessary type arguments in helper_test.go
SamMorrowDrums Dec 15, 2025
a816ac1
fix: restore correct behavior for --tools and --toolsets flags
SamMorrowDrums Dec 15, 2025
8e80346
Move labels tools to issues toolset
SamMorrowDrums Dec 15, 2025
855a489
Restore labels toolset with get_label in both issues and labels
SamMorrowDrums Dec 15, 2025
6364d86
Fix instruction generation and capability advertisement
SamMorrowDrums Dec 15, 2025
1bbac36
Add tests for dynamic toolset management tools
SamMorrowDrums Dec 15, 2025
7eab169
Advertise all capabilities in dynamic toolsets mode
SamMorrowDrums Dec 15, 2025
3914a92
Improve conformance test with dynamic tool calls and JSON normalization
SamMorrowDrums Dec 15, 2025
eeb2984
Add conformance-report to .gitignore
SamMorrowDrums Dec 15, 2025
4665d23
Add conformance test CI workflow
SamMorrowDrums Dec 15, 2025
c913d58
Add map indexes for O(1) lookups in Registry
SamMorrowDrums Dec 15, 2025
7864d80
perf(registry): O(1) HasToolset lookup via pre-computed set
SamMorrowDrums Dec 15, 2025
9aeded4
simplify: remove lazy toolsByName map - not needed for actual use cases
SamMorrowDrums Dec 15, 2025
7070d03
Add generic tool filtering mechanisms to registry package
Copilot Dec 16, 2025
d76ca28
docs: improve filter evaluation order and FilteredTools documentation
SamMorrowDrums Dec 16, 2025
c274d78
Refactor GenerateToolsetsHelp() to use strings.Builder pattern
Copilot Dec 15, 2025
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
Copy Markdown
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")
}
10 changes: 7 additions & 3 deletions cmd/github-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ var (
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 +65,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 +89,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 +103,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