Skip to content

Commit 3e30fc8

Browse files
committed
Initial HTTP stack
1 parent 31b541e commit 3e30fc8

File tree

18 files changed

+1930
-1293
lines changed

18 files changed

+1930
-1293
lines changed

cmd/github-mcp-server/main.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,24 @@ var (
8888
return ghmcp.RunStdioServer(stdioServerConfig)
8989
},
9090
}
91+
92+
httpCmd = &cobra.Command{
93+
Use: "http",
94+
Short: "Start HTTP server",
95+
Long: `Start an HTTP server that listens for MCP requests over HTTP.`,
96+
RunE: func(_ *cobra.Command, _ []string) error {
97+
httpConfig := ghmcp.HTTPServerConfig{
98+
Version: version,
99+
Host: viper.GetString("host"),
100+
ExportTranslations: viper.GetBool("export-translations"),
101+
EnableCommandLogging: viper.GetBool("enable-command-logging"),
102+
LogFilePath: viper.GetString("log-file"),
103+
ContentWindowSize: viper.GetInt("content-window-size"),
104+
}
105+
106+
return ghmcp.RunHTTPServer(httpConfig)
107+
},
108+
}
91109
)
92110

93111
func init() {
@@ -126,6 +144,7 @@ func init() {
126144

127145
// Add subcommands
128146
rootCmd.AddCommand(stdioCmd)
147+
rootCmd.AddCommand(httpCmd)
129148
}
130149

131150
func initConfig() {

internal/ghmcp/http.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package ghmcp
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"log/slog"
8+
"net/http"
9+
"os"
10+
"os/signal"
11+
"syscall"
12+
"time"
13+
14+
"github.com/github/github-mcp-server/pkg/github"
15+
"github.com/github/github-mcp-server/pkg/http/middleware"
16+
"github.com/github/github-mcp-server/pkg/lockdown"
17+
"github.com/github/github-mcp-server/pkg/translations"
18+
"github.com/github/github-mcp-server/pkg/utils"
19+
"github.com/modelcontextprotocol/go-sdk/mcp"
20+
)
21+
22+
type HTTPServerConfig struct {
23+
// Version of the server
24+
Version string
25+
26+
// GitHub Host to target for API requests (e.g. github.com or github.enterprise.com)
27+
Host string
28+
29+
// EnabledToolsets is a list of toolsets to enable
30+
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
31+
EnabledToolsets []string
32+
33+
// EnabledTools is a list of specific tools to enable (additive to toolsets)
34+
// When specified, these tools are registered in addition to any specified toolset tools
35+
EnabledTools []string
36+
37+
// EnabledFeatures is a list of feature flags that are enabled
38+
// Items with FeatureFlagEnable matching an entry in this list will be available
39+
EnabledFeatures []string
40+
41+
// Whether to enable dynamic toolsets
42+
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#dynamic-tool-discovery
43+
DynamicToolsets bool
44+
45+
// ReadOnly indicates if we should only register read-only tools
46+
ReadOnly bool
47+
48+
// ExportTranslations indicates if we should export translations
49+
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#i18n--overriding-descriptions
50+
ExportTranslations bool
51+
52+
// EnableCommandLogging indicates if we should log commands
53+
EnableCommandLogging bool
54+
55+
// Path to the log file if not stderr
56+
LogFilePath string
57+
58+
// Content window size
59+
ContentWindowSize int
60+
61+
// LockdownMode indicates if we should enable lockdown mode
62+
LockdownMode bool
63+
64+
// RepoAccessCacheTTL overrides the default TTL for repository access cache entries.
65+
RepoAccessCacheTTL *time.Duration
66+
}
67+
68+
func RunHTTPServer(cfg HTTPServerConfig) error {
69+
// Create app context
70+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
71+
defer stop()
72+
73+
t, dumpTranslations := translations.TranslationHelper()
74+
75+
var slogHandler slog.Handler
76+
var logOutput io.Writer
77+
if cfg.LogFilePath != "" {
78+
file, err := os.OpenFile(cfg.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
79+
if err != nil {
80+
return fmt.Errorf("failed to open log file: %w", err)
81+
}
82+
logOutput = file
83+
slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelDebug})
84+
} else {
85+
logOutput = os.Stderr
86+
slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelInfo})
87+
}
88+
logger := slog.New(slogHandler)
89+
logger.Info("starting server", "version", cfg.Version, "host", cfg.Host, "dynamicToolsets", cfg.DynamicToolsets, "readOnly", cfg.ReadOnly, "lockdownEnabled", cfg.LockdownMode)
90+
91+
// Set up repo access cache for lockdown mode
92+
var opts []lockdown.RepoAccessOption
93+
if cfg.LockdownMode {
94+
opts = []lockdown.RepoAccessOption{
95+
lockdown.WithLogger(logger.With("component", "lockdown")),
96+
}
97+
if cfg.RepoAccessCacheTTL != nil {
98+
opts = append(opts, lockdown.WithTTL(*cfg.RepoAccessCacheTTL))
99+
}
100+
}
101+
102+
apiHost, err := utils.ParseAPIHost(cfg.Host)
103+
if err != nil {
104+
return fmt.Errorf("failed to parse API host: %w", err)
105+
}
106+
107+
deps := github.NewRequestDeps(
108+
&apiHost,
109+
cfg.Version,
110+
cfg.LockdownMode,
111+
opts,
112+
t,
113+
github.FeatureFlags{
114+
LockdownMode: cfg.LockdownMode,
115+
},
116+
cfg.ContentWindowSize,
117+
)
118+
119+
ghServer, err := github.NewMcpServer(&github.MCPServerConfig{
120+
Version: cfg.Version,
121+
Host: cfg.Host,
122+
EnabledToolsets: cfg.EnabledToolsets,
123+
EnabledTools: cfg.EnabledTools,
124+
EnabledFeatures: cfg.EnabledFeatures,
125+
DynamicToolsets: cfg.DynamicToolsets,
126+
ReadOnly: cfg.ReadOnly,
127+
Translator: t,
128+
ContentWindowSize: cfg.ContentWindowSize,
129+
LockdownMode: cfg.LockdownMode,
130+
Logger: logger,
131+
RepoAccessTTL: cfg.RepoAccessCacheTTL,
132+
}, deps)
133+
if err != nil {
134+
return fmt.Errorf("failed to create MCP server: %w", err)
135+
}
136+
137+
handler := NewHttpMcpHandler(&cfg, ghServer)
138+
139+
httpSvr := http.Server{
140+
Addr: ":8082",
141+
Handler: handler,
142+
}
143+
144+
go func() {
145+
<-ctx.Done()
146+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
147+
defer cancel()
148+
logger.Info("shutting down server")
149+
if err := httpSvr.Shutdown(shutdownCtx); err != nil {
150+
logger.Error("error during server shutdown", "error", err)
151+
}
152+
}()
153+
154+
if cfg.ExportTranslations {
155+
// Once server is initialized, all translations are loaded
156+
dumpTranslations()
157+
}
158+
159+
logger.Info("HTTP server listening on :8082")
160+
if err := httpSvr.ListenAndServe(); err != nil && err != http.ErrServerClosed {
161+
return fmt.Errorf("HTTP server error: %w", err)
162+
}
163+
164+
logger.Info("server stopped gracefully")
165+
return nil
166+
}
167+
168+
type HttpMcpHandler struct {
169+
config *HTTPServerConfig
170+
ghServer *mcp.Server
171+
}
172+
173+
func NewHttpMcpHandler(cfg *HTTPServerConfig, mcpServer *mcp.Server) *HttpMcpHandler {
174+
return &HttpMcpHandler{
175+
config: cfg,
176+
ghServer: mcpServer,
177+
}
178+
}
179+
180+
func (s *HttpMcpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
181+
mcpHandler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
182+
return s.ghServer
183+
}, &mcp.StreamableHTTPOptions{
184+
Stateless: true,
185+
})
186+
187+
middleware.ExtractUserToken()(mcpHandler).ServeHTTP(w, r)
188+
}

0 commit comments

Comments
 (0)