From 13b463548d25721d06abe589438f7788852b1c28 Mon Sep 17 00:00:00 2001 From: Talal Riaz Date: Mon, 13 Oct 2025 23:54:10 -0700 Subject: [PATCH 1/8] Add HTTP server mode with OAuth token support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds HTTP server mode to the GitHub MCP Server, enabling multi-client support with "bring your own token" OAuth functionality. This is useful for enterprise scenarios where a single MCP server instance handles multiple external clients, each authenticating with their own credentials. Key changes: - Add `http` command to start HTTP server on configurable port (default 8080) - Support per-request OAuth tokens via Authorization header - Fall back to GITHUB_PERSONAL_ACCESS_TOKEN env var if no header provided - Modify client factories to extract token from request context - Add comprehensive HTTP server documentation to README This implementation is inspired by PR #888 by @Dreadnoth, updated to work with the current codebase architecture and dependencies. Co-authored-by: Dreadnoth <15017947+Dreadnoth@users.noreply.github.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Dockerfile | 3 + README.md | 37 ++++++++ cmd/github-mcp-server/main.go | 37 ++++++++ go.mod | 1 + go.sum | 4 + internal/ghmcp/server.go | 154 +++++++++++++++++++++++++++++++++- 6 files changed, 234 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9d865cb21..974865101 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,9 @@ LABEL io.modelcontextprotocol.server.name="io.github.github/github-mcp-server" WORKDIR /server # Copy the binary from the build stage COPY --from=build /bin/github-mcp-server . + +EXPOSE 8080 + # Set the entrypoint to the server binary ENTRYPOINT ["/server/github-mcp-server"] # Default arguments for ENTRYPOINT diff --git a/README.md b/README.md index c0ac851a7..2754620de 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,43 @@ GitHub Enterprise Server does not support remote server hosting. Please refer to --- +## HTTP Server Mode + +The GitHub MCP Server can run in HTTP mode, allowing it to serve multiple clients concurrently. This is useful for enterprise scenarios where you want to run a single MCP server instance that handles multiple external clients. + +### Starting the HTTP Server + +To run the server in HTTP mode, use the `http` command: + +```bash +github-mcp-server http --port 8080 +``` + +Or with Docker: + +```bash +docker run -p 8080:8080 \ + -e GITHUB_PERSONAL_ACCESS_TOKEN= \ + ghcr.io/github/github-mcp-server http --port 8080 +``` + +### HTTP Server with "Bring Your Own Token" + +When running the server in HTTP mode, clients can provide their own GitHub token with each request using the `Authorization` header: + +```http +Authorization: Bearer +``` + +This allows each client to authenticate with their own credentials, enabling: +- Multi-tenant deployments where each user has their own access level +- Enterprise use cases with centralized MCP server infrastructure +- OAuth-based authentication flows + +If no `Authorization` header is provided, the server will fall back to using the token specified via the `GITHUB_PERSONAL_ACCESS_TOKEN` environment variable (if configured). + +--- + ## Local GitHub MCP Server [![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=github&inputs=%5B%7B%22id%22%3A%22github_token%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22GitHub%20Personal%20Access%20Token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22GITHUB_PERSONAL_ACCESS_TOKEN%22%2C%22ghcr.io%2Fgithub%2Fgithub-mcp-server%22%5D%2C%22env%22%3A%7B%22GITHUB_PERSONAL_ACCESS_TOKEN%22%3A%22%24%7Binput%3Agithub_token%7D%22%7D%7D) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=github&inputs=%5B%7B%22id%22%3A%22github_token%22%2C%22type%22%3A%22promptString%22%2C%22description%22%3A%22GitHub%20Personal%20Access%20Token%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22-e%22%2C%22GITHUB_PERSONAL_ACCESS_TOKEN%22%2C%22ghcr.io%2Fgithub%2Fgithub-mcp-server%22%5D%2C%22env%22%3A%7B%22GITHUB_PERSONAL_ACCESS_TOKEN%22%3A%22%24%7Binput%3Agithub_token%7D%22%7D%7D&quality=insiders) diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index a0e225293..390469b5b 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -26,6 +26,39 @@ var ( Version: fmt.Sprintf("Version: %s\nCommit: %s\nBuild Date: %s", version, commit, date), } + httpCmd = &cobra.Command{ + Use: "http", + Short: "Start HTTP server", + Long: `Start a server that communicates via HTTP using the MCP protocol.`, + RunE: func(_ *cobra.Command, _ []string) error { + token := viper.GetString("personal_access_token") + + var enabledToolsets []string + if err := viper.UnmarshalKey("toolsets", &enabledToolsets); err != nil { + return fmt.Errorf("failed to unmarshal toolsets: %w", err) + } + + if len(enabledToolsets) == 0 { + enabledToolsets = github.GetDefaultToolsetIDs() + } + + httpServerConfig := ghmcp.HTTPServerConfig{ + Version: version, + Host: viper.GetString("host"), + Token: token, + EnabledToolsets: enabledToolsets, + DynamicToolsets: viper.GetBool("dynamic_toolsets"), + ReadOnly: viper.GetBool("read-only"), + ExportTranslations: viper.GetBool("export-translations"), + EnableCommandLogging: viper.GetBool("enable-command-logging"), + LogFilePath: viper.GetString("log-file"), + ContentWindowSize: viper.GetInt("content-window-size"), + Port: viper.GetInt("port"), + } + return ghmcp.RunHTTPServer(httpServerConfig) + }, + } + stdioCmd = &cobra.Command{ Use: "stdio", Short: "Start stdio server", @@ -94,6 +127,10 @@ func init() { // Add subcommands rootCmd.AddCommand(stdioCmd) + rootCmd.AddCommand(httpCmd) + + httpCmd.Flags().Int("port", 8080, "Port to listen on for HTTP server") + _ = viper.BindPFlag("port", httpCmd.Flags().Lookup("port")) } func initConfig() { diff --git a/go.mod b/go.mod index 61b4b971a..162d9c073 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/invopop/jsonschema v0.13.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect diff --git a/go.sum b/go.sum index 184f3005d..ee8590d1a 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,8 @@ github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkv github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8= github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0= github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= @@ -87,6 +89,7 @@ github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqj github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -103,6 +106,7 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go index 2fb2fb19b..515e9cf29 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" @@ -22,6 +23,7 @@ import ( "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/shurcooL/githubv4" + "github.com/sirupsen/logrus" ) type MCPServerConfig struct { @@ -124,11 +126,39 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { server.WithHooks(hooks), ) - getClient := func(_ context.Context) (*gogithub.Client, error) { + getClient := func(ctx context.Context) (*gogithub.Client, error) { + if tokenVal := ctx.Value(githubTokenKey{}); tokenVal != nil { + if token, ok := tokenVal.(string); ok && token != "" { + client := gogithub.NewClient(nil).WithAuthToken(token) + client.UserAgent = restClient.UserAgent + client.BaseURL = apiHost.baseRESTURL + client.UploadURL = apiHost.uploadURL + return client, nil + } + } return restClient, nil // closing over client } - getGQLClient := func(_ context.Context) (*githubv4.Client, error) { + getGQLClient := func(ctx context.Context) (*githubv4.Client, error) { + if tokenVal := ctx.Value(githubTokenKey{}); tokenVal != nil { + if token, ok := tokenVal.(string); ok && token != "" { + httpClient := &http.Client{ + Transport: &bearerAuthTransport{ + transport: http.DefaultTransport, + token: token, + }, + } + if gqlHTTPClient.Transport != nil { + if uaTransport, ok := gqlHTTPClient.Transport.(*userAgentTransport); ok { + httpClient.Transport = &userAgentTransport{ + transport: httpClient.Transport, + agent: uaTransport.agent, + } + } + } + return githubv4.NewEnterpriseClient(apiHost.graphqlURL.String(), httpClient), nil + } + } return gqlClient, nil // closing over client } @@ -159,6 +189,46 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { return ghServer, nil } +type githubTokenKey struct{} + +type HTTPServerConfig struct { + // Version of the server + Version string + + // GitHub Host to target for API requests (e.g. github.com or github.enterprise.com) + Host string + + // GitHub Token to authenticate with the GitHub API (optional for HTTP mode with OAuth) + Token string + + // EnabledToolsets is a list of toolsets to enable + // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration + EnabledToolsets []string + + // Whether to enable dynamic toolsets + // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#dynamic-tool-discovery + DynamicToolsets bool + + // ReadOnly indicates if we should only register read-only tools + ReadOnly bool + + // ExportTranslations indicates if we should export translations + // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#i18n--overriding-descriptions + ExportTranslations bool + + // EnableCommandLogging indicates if we should log commands + EnableCommandLogging bool + + // Path to the log file if not stderr + LogFilePath string + + // Content window size + ContentWindowSize int + + // Port to listen on for HTTP server + Port int +} + type StdioServerConfig struct { // Version of the server Version string @@ -194,6 +264,77 @@ type StdioServerConfig struct { ContentWindowSize int } +func RunHTTPServer(cfg HTTPServerConfig) error { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + t, dumpTranslations := translations.TranslationHelper() + + ghServer, err := NewMCPServer(MCPServerConfig{ + Version: cfg.Version, + Host: cfg.Host, + Token: cfg.Token, + EnabledToolsets: cfg.EnabledToolsets, + DynamicToolsets: cfg.DynamicToolsets, + ReadOnly: cfg.ReadOnly, + Translator: t, + ContentWindowSize: cfg.ContentWindowSize, + }) + if err != nil { + return fmt.Errorf("failed to create MCP server: %w", err) + } + + logrusLogger := logrus.New() + if cfg.LogFilePath != "" { + file, err := os.OpenFile(cfg.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) + if err != nil { + return fmt.Errorf("failed to open log file: %w", err) + } + + logrusLogger.SetLevel(logrus.DebugLevel) + logrusLogger.SetOutput(file) + } + + httpOptions := []server.StreamableHTTPOption{ + server.WithLogger(logrusLogger), + server.WithHeartbeatInterval(30 * time.Second), + server.WithHTTPContextFunc(extractTokenFromAuthHeader), + } + + httpServer := server.NewStreamableHTTPServer(ghServer, httpOptions...) + + if cfg.ExportTranslations { + dumpTranslations() + } + + addr := fmt.Sprintf(":%d", cfg.Port) + srv := &http.Server{ + Addr: addr, + Handler: httpServer, + } + + _, _ = fmt.Fprintf(os.Stderr, "GitHub MCP Server running on HTTP at %s\n", addr) + + errC := make(chan error, 1) + go func() { + errC <- srv.ListenAndServe() + }() + + select { + case <-ctx.Done(): + logrusLogger.Infof("shutting down server...") + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + return srv.Shutdown(shutdownCtx) + case err := <-errC: + if err != nil && err != http.ErrServerClosed { + return fmt.Errorf("error running server: %w", err) + } + } + + return nil +} + // RunStdioServer is not concurrent safe. func RunStdioServer(cfg StdioServerConfig) error { // Create app context @@ -427,3 +568,12 @@ func (t *bearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, erro req.Header.Set("Authorization", "Bearer "+t.token) return t.transport.RoundTrip(req) } + +func extractTokenFromAuthHeader(ctx context.Context, r *http.Request) context.Context { + authHeader := r.Header.Get("Authorization") + if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") { + token := strings.TrimPrefix(authHeader, "Bearer ") + return context.WithValue(ctx, githubTokenKey{}, token) + } + return ctx +} From bd073d5e504c4f7eb759e6bcf5bcbe8434ff2c5e Mon Sep 17 00:00:00 2001 From: Talal <45835427+talalryz@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:03:53 -0700 Subject: [PATCH 2/8] Update internal/ghmcp/server.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/ghmcp/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go index 515e9cf29..ed026a074 100644 --- a/internal/ghmcp/server.go +++ b/internal/ghmcp/server.go @@ -322,7 +322,7 @@ func RunHTTPServer(cfg HTTPServerConfig) error { select { case <-ctx.Done(): - logrusLogger.Infof("shutting down server...") + logrusLogger.Infof("Shutting down server...") shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() return srv.Shutdown(shutdownCtx) 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 3/8] 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 21abc8653..1a55c1171 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 4/8] 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 2fb2fb19b..cb44dffa0 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 dfd718f7e..7ffc5fc0c 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 5/8] 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 c0ac851a7..e6309994c 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 61815a482..fe1372d7b 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 6/8] 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 e6309994c..e0119d523 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 fe1372d7b..3a4ec444a 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 7/8] 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 e0119d523..bdba0d146 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 a0e225293..e34044a89 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 cb44dffa0..5b4c5c158 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 000000000..c675306f6 --- /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 a982060de..a0b1690c9 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 } From 08954a63d4e2f6b5e11df0a7f2914f66849a47f7 Mon Sep 17 00:00:00 2001 From: Talal Date: Thu, 13 Nov 2025 22:04:26 -0800 Subject: [PATCH 8/8] Update internal/ghmcp/server.go Co-authored-by: Julia <18719332+aaaailuj@users.noreply.github.com> --- internal/ghmcp/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go index 0026cadc0..7e94383d9 100644 --- a/internal/ghmcp/server.go +++ b/internal/ghmcp/server.go @@ -613,6 +613,7 @@ func extractTokenFromAuthHeader(ctx context.Context, r *http.Request) context.Co return context.WithValue(ctx, githubTokenKey{}, token) } return ctx +} // cleanToolsets cleans and handles special toolset keywords: // - Duplicates are removed from the result // - Removes whitespaces