diff --git a/Dockerfile b/Dockerfile index a26f19a81..33dadd936 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,9 @@ FROM gcr.io/distroless/base-debian12 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 e4543ecf5..4c1902f9c 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,22 @@ See [Remote Server Documentation](/docs/remote-server.md) on how to pass additio --- +## HTTP Server Mode + +To run the server in HTTP mode, use the `http` command: + +```bash +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 +``` + ## 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 cad002666..148d3fe8b 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -26,6 +26,34 @@ 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) + } + + 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"), + Port: viper.GetInt("port"), + } + return ghmcp.RunHTTPServer(httpServerConfig) + }, + } + stdioCmd = &cobra.Command{ Use: "stdio", Short: "Start stdio server", @@ -87,6 +115,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/internal/ghmcp/server.go b/internal/ghmcp/server.go index 5fb9582b9..c6c1a36ca 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" @@ -115,12 +116,40 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { } } - getClient := func(_ context.Context) (*gogithub.Client, error) { - return restClient, nil // closing over client - } - - getGQLClient := func(_ context.Context) (*githubv4.Client, error) { - return gqlClient, nil // closing over client + 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 + } + + 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 } getRawClient := func(ctx context.Context) (*raw.Client, error) { @@ -131,7 +160,6 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { return raw.NewClient(client, apiHost.rawURL), nil // closing over client } - // Create default toolsets tsg := github.DefaultToolsetGroup(cfg.ReadOnly, getClient, getGQLClient, getRawClient, cfg.Translator) err = tsg.EnableToolsets(enabledToolsets) @@ -139,7 +167,6 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { return nil, fmt.Errorf("failed to enable toolsets: %w", err) } - // Register all mcp functionality with the server tsg.RegisterAll(ghServer) if cfg.DynamicToolsets { @@ -150,6 +177,21 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) { return ghServer, nil } +type githubTokenKey struct{} + +type HTTPServerConfig struct { + Version string + Host string + Token string + EnabledToolsets []string + DynamicToolsets bool + ReadOnly bool + ExportTranslations bool + EnableCommandLogging bool + LogFilePath string + Port int +} + type StdioServerConfig struct { // Version of the server Version string @@ -182,6 +224,76 @@ type StdioServerConfig struct { LogFilePath string } +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, + }) + 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 @@ -414,3 +526,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 +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy