From ea4d842439ae5ef29f9eb55f16dd3505eefb6744 Mon Sep 17 00:00:00 2001 From: Lulu <59149422+LuluBeatson@users.noreply.github.com> Date: Wed, 15 Oct 2025 12:18:12 +0100 Subject: [PATCH 1/5] docs: Gemini CLI additional options (#1223) * Add "trust" option to MCP server configuration for bypassing confirmations * Additional configuration * Add co-author Co-authored-by: Michael Vorburger --------- Co-authored-by: Michael Vorburger --- docs/installation-guides/install-gemini-cli.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/installation-guides/install-gemini-cli.md b/docs/installation-guides/install-gemini-cli.md index 21abc86533..1a55c1171f 100644 --- a/docs/installation-guides/install-gemini-cli.md +++ b/docs/installation-guides/install-gemini-cli.md @@ -40,7 +40,6 @@ The simplest way is to use GitHub's hosted MCP server: "mcpServers": { "github": { "httpUrl": "https://api.githubcopilot.com/mcp/", - "trust": true, "headers": { "Authorization": "Bearer $GITHUB_PAT" } @@ -122,6 +121,10 @@ To verify that the GitHub MCP server has been configured, start Gemini CLI in yo List my GitHub repositories ``` +## Additional Configuration + +You can find more MCP configuration options for Gemini CLI here: [MCP Configuration Structure](https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html#configuration-structure). For example, bypassing tool confirmations or excluding specific tools. + ## Troubleshooting ### Local Server Issues From 99acea6ca192e4d15d4256f8e54ab7e0be6a6fb1 Mon Sep 17 00:00:00 2001 From: Tony Truong Date: Wed, 15 Oct 2025 13:21:37 +0200 Subject: [PATCH 2/5] Fix subdomain isolation URL parsing (#1218) * adding better response * adding check for subdomain isolation and return raw resp for better better llm response * adding subdomain for uploads too * remove unnecessary comments * better error message * fix linter --- internal/ghmcp/server.go | 47 ++++++++++++++++++++++++++++++++++++-- pkg/github/repositories.go | 6 +++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go index 2fb2fb19bb..cb44dffa03 100644 --- a/internal/ghmcp/server.go +++ b/internal/ghmcp/server.go @@ -12,6 +12,7 @@ import ( "os/signal" "strings" "syscall" + "time" "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/github" @@ -363,11 +364,30 @@ func newGHESHost(hostname string) (apiHost, error) { return apiHost{}, fmt.Errorf("failed to parse GHES GraphQL URL: %w", err) } - uploadURL, err := url.Parse(fmt.Sprintf("%s://%s/api/uploads/", u.Scheme, u.Hostname())) + // Check if subdomain isolation is enabled + // See https://docs.github.com/en/enterprise-server@3.17/admin/configuring-settings/hardening-security-for-your-enterprise/enabling-subdomain-isolation#about-subdomain-isolation + hasSubdomainIsolation := checkSubdomainIsolation(u.Scheme, u.Hostname()) + + var uploadURL *url.URL + if hasSubdomainIsolation { + // With subdomain isolation: https://uploads.hostname/ + uploadURL, err = url.Parse(fmt.Sprintf("%s://uploads.%s/", u.Scheme, u.Hostname())) + } else { + // Without subdomain isolation: https://hostname/api/uploads/ + uploadURL, err = url.Parse(fmt.Sprintf("%s://%s/api/uploads/", u.Scheme, u.Hostname())) + } if err != nil { return apiHost{}, fmt.Errorf("failed to parse GHES Upload URL: %w", err) } - rawURL, err := url.Parse(fmt.Sprintf("%s://%s/raw/", u.Scheme, u.Hostname())) + + var rawURL *url.URL + if hasSubdomainIsolation { + // With subdomain isolation: https://raw.hostname/ + rawURL, err = url.Parse(fmt.Sprintf("%s://raw.%s/", u.Scheme, u.Hostname())) + } else { + // Without subdomain isolation: https://hostname/raw/ + rawURL, err = url.Parse(fmt.Sprintf("%s://%s/raw/", u.Scheme, u.Hostname())) + } if err != nil { return apiHost{}, fmt.Errorf("failed to parse GHES Raw URL: %w", err) } @@ -380,6 +400,29 @@ func newGHESHost(hostname string) (apiHost, error) { }, nil } +// checkSubdomainIsolation detects if GitHub Enterprise Server has subdomain isolation enabled +// by attempting to ping the raw./_ping endpoint on the subdomain. The raw subdomain must always exist for subdomain isolation. +func checkSubdomainIsolation(scheme, hostname string) bool { + subdomainURL := fmt.Sprintf("%s://raw.%s/_ping", scheme, hostname) + + client := &http.Client{ + Timeout: 5 * time.Second, + // Don't follow redirects - we just want to check if the endpoint exists + //nolint:revive // parameters are required by http.Client.CheckRedirect signature + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + resp, err := client.Get(subdomainURL) + if err != nil { + return false + } + defer resp.Body.Close() + + return resp.StatusCode == http.StatusOK +} + // Note that this does not handle ports yet, so development environments are out. func parseAPIHost(s string) (apiHost, error) { if s == "" { diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index dfd718f7eb..7ffc5fc0c8 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -542,6 +542,8 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t // If the path is (most likely) not to be a directory, we will // first try to get the raw content from the GitHub raw content API. + + var rawAPIResponseCode int if path != "" && !strings.HasSuffix(path, "/") { // First, get file info from Contents API to retrieve SHA var fileSHA string @@ -631,8 +633,8 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t return mcp.NewToolResultResource(fmt.Sprintf("successfully downloaded binary file (SHA: %s)", fileSHA), result), nil } return mcp.NewToolResultResource("successfully downloaded binary file", result), nil - } + rawAPIResponseCode = resp.StatusCode } if rawOpts.SHA != "" { @@ -677,7 +679,7 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to marshal resolved refs: %s", err)), nil } - return mcp.NewToolResultText(fmt.Sprintf("Path did not point to a file or directory, but resolved git ref to %s with possible path matches: %s", resolvedRefs, matchingFilesJSON)), nil + return mcp.NewToolResultError(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s), but the raw content API returned an unexpected status code %d.", string(resolvedRefs), string(matchingFilesJSON), rawAPIResponseCode)), nil } return mcp.NewToolResultError("Failed to get file contents. The path does not point to a file or directory, or the file does not exist in the repository."), nil From 5f74b5338f08f0dd7314c4dbf0e9ef62260ad481 Mon Sep 17 00:00:00 2001 From: Adam Holt Date: Wed, 15 Oct 2025 15:44:27 +0200 Subject: [PATCH 3/5] Update readme for remote only tools (#1227) * Add additional toolsets in remote mcp * Add copilot * Fix urls * Move copilot and spaces toolsets to remote-only section --------- Co-authored-by: LuluBeatson --- README.md | 11 +++++++++-- docs/remote-server.md | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c0ac851a70..e6309994c1 100644 --- a/README.md +++ b/README.md @@ -375,7 +375,7 @@ GITHUB_TOOLSETS="all" ./github-mcp-server ### Available Toolsets -The following sets of tools are available (all are on by default): +The following sets of tools are available: | Toolset | Description | @@ -400,6 +400,13 @@ The following sets of tools are available (all are on by default): | `users` | GitHub User related tools | +### Additional Toolsets in Remote Github MCP Server + +| Toolset | Description | +| ----------------------- | ------------------------------------------------------------- | +| `copilot` | Copilot related tools (e.g. Copilot Coding Agent) | +| `copilot_spaces` | Copilot Spaces related tools | + ## Tools @@ -1167,7 +1174,7 @@ Possible options:
-Copilot coding agent +Copilot - **create_pull_request_with_copilot** - Perform task with GitHub Copilot coding agent - `owner`: Repository owner. You can guess the owner, but confirm it with the user before proceeding. (string, required) diff --git a/docs/remote-server.md b/docs/remote-server.md index 61815a482c..fe1372d7bb 100644 --- a/docs/remote-server.md +++ b/docs/remote-server.md @@ -46,7 +46,8 @@ These toolsets are only available in the remote GitHub MCP Server and are not in | Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) | | -------------------- | --------------------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Copilot coding agent | Perform task with GitHub Copilot coding agent | https://api.githubcopilot.com/mcp/x/copilot | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-copilot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcopilot%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/copilot/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-copilot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcopilot%2Freadonly%22%7D) | +| Copilot | Copilot related tools | https://api.githubcopilot.com/mcp/x/copilot | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-copilot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcopilot%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/copilot/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-copilot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcopilot%2Freadonly%22%7D) | +| Copilot Spaces | Copilot Spaces tools | https://api.githubcopilot.com/mcp/x/copilot_spaces | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-copilot_spaces&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcopilot_spaces%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/copilot_spaces/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-copilot_spaces&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcopilot_spaces%2Freadonly%22%7D) | ### Optional Headers From 7b4b292ddd22f48c4e17abaa13dd2c39efd925f1 Mon Sep 17 00:00:00 2001 From: Lulu <59149422+LuluBeatson@users.noreply.github.com> Date: Wed, 15 Oct 2025 14:54:49 +0100 Subject: [PATCH 4/5] docs: New "GitHub Support Docs Search" tool (#1225) * docs: add github_support_docs_search to REMOTE server * add copilot_spaces * fix typos * Add support tool to remote toolsets table --- README.md | 9 +++++++++ docs/remote-server.md | 1 + 2 files changed, 10 insertions(+) diff --git a/README.md b/README.md index e6309994c1..e0119d5236 100644 --- a/README.md +++ b/README.md @@ -406,6 +406,7 @@ The following sets of tools are available: | ----------------------- | ------------------------------------------------------------- | | `copilot` | Copilot related tools (e.g. Copilot Coding Agent) | | `copilot_spaces` | Copilot Spaces related tools | +| `github_support_docs_search` | Search docs to answer GitHub product and support questions | ## Tools @@ -1196,6 +1197,14 @@ Possible options: - **list_copilot_spaces** - List Copilot Spaces
+
+ +GitHub Support Docs Search + +- **github_support_docs_search** - Retrieve documentation relevant to answer GitHub product and support questions. Support topics include: GitHub Actions Workflows, Authentication, GitHub Support Inquiries, Pull Request Practices, Repository Maintenance, GitHub Pages, GitHub Packages, GitHub Discussions, Copilot Spaces + - `query`: Input from the user about the question they need answered. This is the latest raw unedited user message. You should ALWAYS leave the user message as it is, you should never modify it. (string, required) +
+ ## Dynamic Tool Discovery **Note**: This feature is currently in beta and may not be available in all environments. Please test it out and let us know if you encounter any issues. diff --git a/docs/remote-server.md b/docs/remote-server.md index fe1372d7bb..3a4ec444a3 100644 --- a/docs/remote-server.md +++ b/docs/remote-server.md @@ -48,6 +48,7 @@ These toolsets are only available in the remote GitHub MCP Server and are not in | -------------------- | --------------------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Copilot | Copilot related tools | https://api.githubcopilot.com/mcp/x/copilot | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-copilot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcopilot%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/copilot/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-copilot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcopilot%2Freadonly%22%7D) | | Copilot Spaces | Copilot Spaces tools | https://api.githubcopilot.com/mcp/x/copilot_spaces | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-copilot_spaces&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcopilot_spaces%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/copilot_spaces/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-copilot_spaces&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fcopilot_spaces%2Freadonly%22%7D) | +| GitHub support docs search | Retrieve documentation to answer GitHub product and support questions. Topics include: GitHub Actions Workflows, Authentication, ... | https://api.githubcopilot.com/mcp/x/github_support_docs_search | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-support&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgithub_support_docs_search%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/github_support_docs_search/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-support&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgithub_support_docs_search%2Freadonly%22%7D) | ### Optional Headers From 66fabb7a8dc5e2f9ffd621ac966b0ad6540663b3 Mon Sep 17 00:00:00 2001 From: Tony Truong Date: Wed, 15 Oct 2025 18:20:14 +0200 Subject: [PATCH 5/5] Adding default toolset as configuration (#1229) * add toolset default to make configuration easier * fix readme * adding transformer to cleanly handle special toolsets * cleaning code * fixing cli message * remove duplicated test * Update internal/ghmcp/server.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update internal/ghmcp/server_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * adding error message for invalid toolsets * fix merge conflict * add better formatting --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 33 ++-- cmd/github-mcp-server/main.go | 3 +- internal/ghmcp/server.go | 67 ++++++-- internal/ghmcp/server_test.go | 278 ++++++++++++++++++++++++++++++++++ pkg/github/tools.go | 32 +++- 5 files changed, 390 insertions(+), 23 deletions(-) create mode 100644 internal/ghmcp/server_test.go diff --git a/README.md b/README.md index e0119d5236..bdba0d146a 100644 --- a/README.md +++ b/README.md @@ -87,17 +87,12 @@ Alternatively, to manually configure VS Code, choose the appropriate JSON block ### Configuration -#### Default toolset configuration - -The default configuration is: -- context -- repos -- issues -- pull_requests -- users +#### Toolset configuration See [Remote Server Documentation](docs/remote-server.md) for full details on remote server configuration, toolsets, headers, and advanced usage. This file provides comprehensive instructions and examples for connecting, customizing, and installing the remote GitHub MCP Server in VS Code and other MCP hosts. +When no toolsets are specified, [default toolsets](#default-toolset) are used. + #### Enterprise Cloud with data residency (ghe.com) GitHub Enterprise Cloud can also make use of the remote server. @@ -329,7 +324,7 @@ The GitHub MCP Server supports enabling or disabling specific groups of function _Toolsets are not limited to Tools. Relevant MCP Resources and Prompts are also included where applicable._ -The Local GitHub MCP Server follows the same [default toolset configuration](#default-toolset-configuration) as the remote version. +When no toolsets are specified, [default toolsets](#default-toolset) are used. #### Specifying Toolsets @@ -359,7 +354,9 @@ docker run -i --rm \ ghcr.io/github/github-mcp-server ``` -### The "all" Toolset +### Special toolsets + +#### "all" toolset The special toolset `all` can be provided to enable all available toolsets regardless of any other configuration: @@ -373,6 +370,22 @@ Or using the environment variable: GITHUB_TOOLSETS="all" ./github-mcp-server ``` +#### "default" toolset +The default toolset `default` is the configuration that gets passed to the server if no toolsets are specified. + +The default configuration is: +- context +- repos +- issues +- pull_requests +- users + +To keep the default configuration and add additional toolsets: + +```bash +GITHUB_TOOLSETS="default,stargazers" ./github-mcp-server +``` + ### Available Toolsets The following sets of tools are available: diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index a0e225293a..e34044a89f 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -45,8 +45,9 @@ var ( return fmt.Errorf("failed to unmarshal toolsets: %w", err) } + // No passed toolsets configuration means we enable the default toolset if len(enabledToolsets) == 0 { - enabledToolsets = github.GetDefaultToolsetIDs() + enabledToolsets = []string{github.ToolsetMetadataDefault.ID} } stdioServerConfig := ghmcp.StdioServerConfig{ diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go index cb44dffa03..5b4c5c158b 100644 --- a/internal/ghmcp/server.go +++ b/internal/ghmcp/server.go @@ -106,15 +106,10 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { }, } - enabledToolsets := cfg.EnabledToolsets - if cfg.DynamicToolsets { - // filter "all" from the enabled toolsets - enabledToolsets = make([]string, 0, len(cfg.EnabledToolsets)) - for _, toolset := range cfg.EnabledToolsets { - if toolset != "all" { - enabledToolsets = append(enabledToolsets, toolset) - } - } + enabledToolsets, invalidToolsets := cleanToolsets(cfg.EnabledToolsets, cfg.DynamicToolsets) + + if len(invalidToolsets) > 0 { + fmt.Fprintf(os.Stderr, "Invalid toolsets ignored: %s\n", strings.Join(invalidToolsets, ", ")) } // Generate instructions based on enabled toolsets @@ -470,3 +465,57 @@ func (t *bearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, erro req.Header.Set("Authorization", "Bearer "+t.token) return t.transport.RoundTrip(req) } + +// cleanToolsets cleans and handles special toolset keywords: +// - Duplicates are removed from the result +// - Removes whitespaces +// - Validates toolset names and returns invalid ones separately +// - "all": Returns ["all"] immediately, ignoring all other toolsets +// - when dynamicToolsets is true, filters out "all" from the enabled toolsets +// - "default": Replaces with the actual default toolset IDs from GetDefaultToolsetIDs() +// Returns: (validToolsets, invalidToolsets) +func cleanToolsets(enabledToolsets []string, dynamicToolsets bool) ([]string, []string) { + seen := make(map[string]bool) + result := make([]string, 0, len(enabledToolsets)) + invalid := make([]string, 0) + validIDs := github.GetValidToolsetIDs() + + // Add non-default toolsets, removing duplicates and trimming whitespace + for _, toolset := range enabledToolsets { + trimmed := strings.TrimSpace(toolset) + if trimmed == "" { + continue + } + if !seen[trimmed] { + seen[trimmed] = true + if trimmed != github.ToolsetMetadataDefault.ID && trimmed != github.ToolsetMetadataAll.ID { + // Validate the toolset name + if validIDs[trimmed] { + result = append(result, trimmed) + } else { + invalid = append(invalid, trimmed) + } + } + } + } + + hasDefault := seen[github.ToolsetMetadataDefault.ID] + hasAll := seen[github.ToolsetMetadataAll.ID] + + // Handle "all" keyword - return early if not in dynamic mode + if hasAll && !dynamicToolsets { + return []string{github.ToolsetMetadataAll.ID}, invalid + } + + // Expand "default" keyword to actual default toolsets + if hasDefault { + for _, defaultToolset := range github.GetDefaultToolsetIDs() { + if !seen[defaultToolset] { + result = append(result, defaultToolset) + seen[defaultToolset] = true + } + } + } + + return result, invalid +} diff --git a/internal/ghmcp/server_test.go b/internal/ghmcp/server_test.go new file mode 100644 index 0000000000..c675306f64 --- /dev/null +++ b/internal/ghmcp/server_test.go @@ -0,0 +1,278 @@ +package ghmcp + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCleanToolsets(t *testing.T) { + tests := []struct { + name string + input []string + dynamicToolsets bool + expected []string + expectedInvalid []string + }{ + { + name: "empty slice", + input: []string{}, + dynamicToolsets: false, + expected: []string{}, + }, + { + name: "nil input slice", + input: nil, + dynamicToolsets: false, + expected: []string{}, + }, + // all test cases + { + name: "all only", + input: []string{"all"}, + dynamicToolsets: false, + expected: []string{"all"}, + }, + { + name: "all appears multiple times", + input: []string{"all", "actions", "all"}, + dynamicToolsets: false, + expected: []string{"all"}, + }, + { + name: "all with other toolsets", + input: []string{"all", "actions", "gists"}, + dynamicToolsets: false, + expected: []string{"all"}, + }, + { + name: "all with default", + input: []string{"default", "all", "actions"}, + dynamicToolsets: false, + expected: []string{"all"}, + }, + // default test cases + { + name: "default only", + input: []string{"default"}, + dynamicToolsets: false, + expected: []string{ + "context", + "repos", + "issues", + "pull_requests", + "users", + }, + }, + { + name: "default with additional toolsets", + input: []string{"default", "actions", "gists"}, + dynamicToolsets: false, + expected: []string{ + "actions", + "gists", + "context", + "repos", + "issues", + "pull_requests", + "users", + }, + }, + { + name: "no default present", + input: []string{"actions", "gists", "notifications"}, + dynamicToolsets: false, + expected: []string{"actions", "gists", "notifications"}, + }, + { + name: "duplicate toolsets without default", + input: []string{"actions", "gists", "actions"}, + dynamicToolsets: false, + expected: []string{"actions", "gists"}, + }, + { + name: "duplicate toolsets with default", + input: []string{"context", "repos", "issues", "pull_requests", "users", "default"}, + dynamicToolsets: false, + expected: []string{ + "context", + "repos", + "issues", + "pull_requests", + "users", + }, + }, + { + name: "default appears multiple times with different toolsets in between", + input: []string{"default", "actions", "default", "gists", "default"}, + dynamicToolsets: false, + expected: []string{ + "actions", + "gists", + "context", + "repos", + "issues", + "pull_requests", + "users", + }, + }, + // Dynamic toolsets test cases + { + name: "dynamic toolsets - all only should be filtered", + input: []string{"all"}, + dynamicToolsets: true, + expected: []string{}, + }, + { + name: "dynamic toolsets - all with other toolsets", + input: []string{"all", "actions", "gists"}, + dynamicToolsets: true, + expected: []string{"actions", "gists"}, + }, + { + name: "dynamic toolsets - all with default", + input: []string{"all", "default", "actions"}, + dynamicToolsets: true, + expected: []string{ + "actions", + "context", + "repos", + "issues", + "pull_requests", + "users", + }, + }, + { + name: "dynamic toolsets - no all present", + input: []string{"actions", "gists"}, + dynamicToolsets: true, + expected: []string{"actions", "gists"}, + }, + { + name: "dynamic toolsets - default only", + input: []string{"default"}, + dynamicToolsets: true, + expected: []string{ + "context", + "repos", + "issues", + "pull_requests", + "users", + }, + }, + { + name: "only special keywords with dynamic mode", + input: []string{"all", "default"}, + dynamicToolsets: true, + expected: []string{ + "context", + "repos", + "issues", + "pull_requests", + "users", + }, + }, + { + name: "all with default and overlapping default toolsets in dynamic mode", + input: []string{"all", "default", "issues", "repos"}, + dynamicToolsets: true, + expected: []string{ + "issues", + "repos", + "context", + "pull_requests", + "users", + }, + }, + // Whitespace test cases + { + name: "whitespace check - leading and trailing whitespace on regular toolsets", + input: []string{" actions ", " gists ", "notifications"}, + dynamicToolsets: false, + expected: []string{"actions", "gists", "notifications"}, + }, + { + name: "whitespace check - default toolset", + input: []string{" actions ", " default ", "notifications"}, + dynamicToolsets: false, + expected: []string{ + "actions", + "notifications", + "context", + "repos", + "issues", + "pull_requests", + "users", + }, + }, + { + name: "whitespace check - all toolset", + input: []string{" actions ", " gists ", "notifications", " all "}, + dynamicToolsets: false, + expected: []string{"all"}, + }, + // Invalid toolset test cases + { + name: "mix of valid and invalid toolsets", + input: []string{"actions", "invalid_toolset", "gists", "typo_repo"}, + dynamicToolsets: false, + expected: []string{"actions", "gists"}, + expectedInvalid: []string{"invalid_toolset", "typo_repo"}, + }, + { + name: "invalid with whitespace", + input: []string{" invalid_tool ", " actions ", " typo_gist "}, + dynamicToolsets: false, + expected: []string{"actions"}, + expectedInvalid: []string{"invalid_tool", "typo_gist"}, + }, + { + name: "empty string in toolsets", + input: []string{"", "actions", " ", "gists"}, + dynamicToolsets: false, + expected: []string{"actions", "gists"}, + expectedInvalid: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, invalid := cleanToolsets(tt.input, tt.dynamicToolsets) + + require.Len(t, result, len(tt.expected), "result length should match expected length") + + if tt.expectedInvalid == nil { + tt.expectedInvalid = []string{} + } + require.Len(t, invalid, len(tt.expectedInvalid), "invalid length should match expected invalid length") + + resultMap := make(map[string]bool) + for _, toolset := range result { + resultMap[toolset] = true + } + + expectedMap := make(map[string]bool) + for _, toolset := range tt.expected { + expectedMap[toolset] = true + } + + invalidMap := make(map[string]bool) + for _, toolset := range invalid { + invalidMap[toolset] = true + } + + expectedInvalidMap := make(map[string]bool) + for _, toolset := range tt.expectedInvalid { + expectedInvalidMap[toolset] = true + } + + assert.Equal(t, expectedMap, resultMap, "result should contain all expected toolsets without duplicates") + assert.Equal(t, expectedInvalidMap, invalidMap, "invalid should contain all expected invalid toolsets") + + assert.Len(t, resultMap, len(result), "result should not contain duplicates") + + assert.False(t, resultMap["default"], "result should not contain 'default'") + }) + } +} diff --git a/pkg/github/tools.go b/pkg/github/tools.go index a982060dea..a0b1690c9a 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -23,6 +23,14 @@ type ToolsetMetadata struct { } var ( + ToolsetMetadataAll = ToolsetMetadata{ + ID: "all", + Description: "Special toolset that enables all available toolsets", + } + ToolsetMetadataDefault = ToolsetMetadata{ + ID: "default", + Description: "Special toolset that enables the default toolset configuration. When no toolsets are specified, this is the set that is enabled", + } ToolsetMetadataContext = ToolsetMetadata{ ID: "context", Description: "Tools that provide context about the current user and GitHub context you are operating in", @@ -125,6 +133,18 @@ func AvailableTools() []ToolsetMetadata { } } +// GetValidToolsetIDs returns a map of all valid toolset IDs for quick lookup +func GetValidToolsetIDs() map[string]bool { + validIDs := make(map[string]bool) + for _, tool := range AvailableTools() { + validIDs[tool.ID] = true + } + // Add special keywords + validIDs[ToolsetMetadataAll.ID] = true + validIDs[ToolsetMetadataDefault.ID] = true + return validIDs +} + func GetDefaultToolsetIDs() []string { return []string{ ToolsetMetadataContext.ID, @@ -414,8 +434,14 @@ func GenerateToolsetsHelp() string { availableTools := strings.Join(availableToolsLines, ",\n\t ") toolsetsHelp := fmt.Sprintf("Comma-separated list of tool groups to enable (no spaces).\n"+ - "Default: %s\n"+ - "Available: %s\n", defaultTools, availableTools) + - "To enable all tools, use \"all\"." + "Available: %s\n", availableTools) + + "Special toolset keywords:\n" + + " - all: Enables all available toolsets\n" + + fmt.Sprintf(" - default: Enables the default toolset configuration of:\n\t %s\n", defaultTools) + + "Examples:\n" + + " - --toolsets=actions,gists,notifications\n" + + " - Default + additional: --toolsets=default,actions,gists\n" + + " - All tools: --toolsets=all" + return toolsetsHelp }