@@ -323,6 +323,152 @@ func RunStdioServer(cfg StdioServerConfig) error {
323323 return nil
324324}
325325
326+ // SSEServerConfig holds configuration for the SSE server mode.
327+ type SSEServerConfig struct {
328+ // Version of the server
329+ Version string
330+
331+ // GitHub Host to target for API requests (e.g. github.com or github.enterprise.com)
332+ Host string
333+
334+ // GitHub Token to authenticate with the GitHub API
335+ Token string
336+
337+ // EnabledToolsets is a list of toolsets to enable
338+ // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#tool-configuration
339+ EnabledToolsets []string
340+
341+ // EnabledTools is a list of specific tools to enable (additive to toolsets)
342+ // When specified, these tools are registered in addition to any specified toolset tools
343+ EnabledTools []string
344+
345+ // Whether to enable dynamic toolsets
346+ // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#dynamic-tool-discovery
347+ DynamicToolsets bool
348+
349+ // ReadOnly indicates if we should only register read-only tools
350+ ReadOnly bool
351+
352+ // ExportTranslations indicates if we should export translations
353+ // See: https://github.com/github/github-mcp-server?tab=readme-ov-file#i18n--overriding-descriptions
354+ ExportTranslations bool
355+
356+ // Path to the log file if not stderr
357+ LogFilePath string
358+
359+ // Content window size
360+ ContentWindowSize int
361+
362+ // LockdownMode indicates if we should enable lockdown mode
363+ LockdownMode bool
364+
365+ // RepoAccessCacheTTL overrides the default TTL for repository access cache entries.
366+ RepoAccessCacheTTL * time.Duration
367+
368+ // SSEAddr is the address to listen on for SSE connections (e.g., ":8080" or "localhost:8080")
369+ SSEAddr string
370+ }
371+
372+ // RunSSEServer starts an HTTP server with SSE transport for the MCP server.
373+ func RunSSEServer (cfg SSEServerConfig ) error {
374+ // Create app context
375+ ctx , stop := signal .NotifyContext (context .Background (), os .Interrupt , syscall .SIGTERM )
376+ defer stop ()
377+
378+ t , dumpTranslations := translations .TranslationHelper ()
379+
380+ var slogHandler slog.Handler
381+ var logOutput io.Writer
382+ if cfg .LogFilePath != "" {
383+ file , err := os .OpenFile (cfg .LogFilePath , os .O_CREATE | os .O_WRONLY | os .O_APPEND , 0600 )
384+ if err != nil {
385+ return fmt .Errorf ("failed to open log file: %w" , err )
386+ }
387+ logOutput = file
388+ slogHandler = slog .NewTextHandler (logOutput , & slog.HandlerOptions {Level : slog .LevelDebug })
389+ } else {
390+ logOutput = os .Stderr
391+ slogHandler = slog .NewTextHandler (logOutput , & slog.HandlerOptions {Level : slog .LevelInfo })
392+ }
393+ logger := slog .New (slogHandler )
394+ logger .Info ("starting SSE server" , "version" , cfg .Version , "host" , cfg .Host , "addr" , cfg .SSEAddr , "dynamicToolsets" , cfg .DynamicToolsets , "readOnly" , cfg .ReadOnly , "lockdownEnabled" , cfg .LockdownMode )
395+
396+ ghServer , err := NewMCPServer (MCPServerConfig {
397+ Version : cfg .Version ,
398+ Host : cfg .Host ,
399+ Token : cfg .Token ,
400+ EnabledToolsets : cfg .EnabledToolsets ,
401+ EnabledTools : cfg .EnabledTools ,
402+ DynamicToolsets : cfg .DynamicToolsets ,
403+ ReadOnly : cfg .ReadOnly ,
404+ Translator : t ,
405+ ContentWindowSize : cfg .ContentWindowSize ,
406+ LockdownMode : cfg .LockdownMode ,
407+ Logger : logger ,
408+ RepoAccessTTL : cfg .RepoAccessCacheTTL ,
409+ })
410+ if err != nil {
411+ return fmt .Errorf ("failed to create MCP server: %w" , err )
412+ }
413+
414+ if cfg .ExportTranslations {
415+ // Once server is initialized, all translations are loaded
416+ dumpTranslations ()
417+ }
418+
419+ // Create SSE handler using the MCP SDK's SSEHandler
420+ sseHandler := mcp .NewSSEHandler (func (_ * http.Request ) * mcp.Server {
421+ return ghServer
422+ }, nil )
423+
424+ // Create HTTP mux with health endpoint and SSE handler
425+ mux := http .NewServeMux ()
426+ mux .HandleFunc ("/healthz" , func (w http.ResponseWriter , _ * http.Request ) {
427+ w .Header ().Set ("Content-Type" , "application/json" )
428+ w .WriteHeader (http .StatusOK )
429+ _ , _ = w .Write ([]byte (`{"status":"ok"}` ))
430+ })
431+ mux .Handle ("/" , sseHandler )
432+
433+ // Create HTTP server
434+ httpServer := & http.Server {
435+ Addr : cfg .SSEAddr ,
436+ Handler : mux ,
437+ }
438+
439+ // Start HTTP server in a goroutine
440+ errC := make (chan error , 1 )
441+ go func () {
442+ logger .Info ("HTTP server listening" , "addr" , cfg .SSEAddr )
443+ if err := httpServer .ListenAndServe (); err != nil && err != http .ErrServerClosed {
444+ errC <- err
445+ }
446+ }()
447+
448+ // Output startup message
449+ _ , _ = fmt .Fprintf (os .Stderr , "GitHub MCP Server running on SSE at %s\n " , cfg .SSEAddr )
450+
451+ // Wait for shutdown signal
452+ select {
453+ case <- ctx .Done ():
454+ logger .Info ("shutting down SSE server" , "signal" , "context done" )
455+ // Gracefully shutdown the HTTP server
456+ shutdownCtx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
457+ defer cancel ()
458+ if err := httpServer .Shutdown (shutdownCtx ); err != nil {
459+ logger .Error ("error shutting down HTTP server" , "error" , err )
460+ return fmt .Errorf ("error shutting down HTTP server: %w" , err )
461+ }
462+ case err := <- errC :
463+ if err != nil {
464+ logger .Error ("error running SSE server" , "error" , err )
465+ return fmt .Errorf ("error running SSE server: %w" , err )
466+ }
467+ }
468+
469+ return nil
470+ }
471+
326472type apiHost struct {
327473 baseRESTURL * url.URL
328474 graphqlURL * url.URL
0 commit comments