From 371639f05b9e9e045f6372e5491d1f2f14b92447 Mon Sep 17 00:00:00 2001 From: Lczyrny Date: Fri, 1 Aug 2025 21:55:08 +0200 Subject: [PATCH] Added streamable http option --- Dockerfile | 3 + README.md | 16 ++++ cmd/github-mcp-server/main.go | 32 ++++++++ internal/ghmcp/server.go | 137 ++++++++++++++++++++++++++++++++-- 4 files changed, 180 insertions(+), 8 deletions(-) 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 8ba842a46..28a0d2cd3 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,22 @@ See [Remote Server Documentation](docs/remote-server.md) on how to pass addition --- +## 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 568af10d1..1521c0902 100644 --- a/internal/ghmcp/server.go +++ b/internal/ghmcp/server.go @@ -11,6 +11,7 @@ import ( "os/signal" "strings" "syscall" + "time" "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/github" @@ -113,12 +114,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) { @@ -129,7 +158,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) @@ -137,7 +165,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 { @@ -148,6 +175,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 @@ -180,6 +222,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 @@ -406,3 +518,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