From a54d6bd832fcb37f56319e92a6acdce410b69f48 Mon Sep 17 00:00:00 2001 From: pyama Date: Tue, 9 Jun 2026 23:33:50 +0900 Subject: [PATCH] feat(http): support custom listen address Add --listen-address flag (env: GITHUB_LISTEN_ADDRESS) so the HTTP server can bind to a specific host:port instead of always listening on all interfaces. When unset the server keeps the existing :PORT behavior. --- cmd/github-mcp-server/main.go | 5 ++++- pkg/http/server.go | 19 ++++++++++++++-- pkg/http/server_test.go | 41 +++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index 558fdb9980..74e0c5cdcb 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -138,6 +138,7 @@ var ( Version: version, Host: viper.GetString("host"), Port: viper.GetInt("port"), + ListenAddress: viper.GetString("listen-address"), BaseURL: viper.GetString("base-url"), ResourcePath: viper.GetString("base-path"), ExportTranslations: viper.GetBool("export-translations"), @@ -183,7 +184,8 @@ func init() { rootCmd.PersistentFlags().Duration("repo-access-cache-ttl", 5*time.Minute, "Override the repo access cache TTL (e.g. 1m, 0s to disable)") // HTTP-specific flags - httpCmd.Flags().Int("port", 8082, "HTTP server port") + httpCmd.Flags().Int("port", 8082, "HTTP server port (ignored when --listen-address is set)") + httpCmd.Flags().String("listen-address", "", "HTTP server listen address (host:port). Overrides --port when set (e.g. 127.0.0.1:8082)") httpCmd.Flags().String("base-url", "", "Base URL where this server is publicly accessible (for OAuth resource metadata)") httpCmd.Flags().String("base-path", "", "Externally visible base path for the HTTP server (for OAuth resource metadata)") httpCmd.Flags().Bool("scope-challenge", false, "Enable OAuth scope challenge responses") @@ -204,6 +206,7 @@ func init() { _ = viper.BindPFlag("insiders", rootCmd.PersistentFlags().Lookup("insiders")) _ = viper.BindPFlag("repo-access-cache-ttl", rootCmd.PersistentFlags().Lookup("repo-access-cache-ttl")) _ = viper.BindPFlag("port", httpCmd.Flags().Lookup("port")) + _ = viper.BindPFlag("listen-address", httpCmd.Flags().Lookup("listen-address")) _ = viper.BindPFlag("base-url", httpCmd.Flags().Lookup("base-url")) _ = viper.BindPFlag("base-path", httpCmd.Flags().Lookup("base-path")) _ = viper.BindPFlag("scope-challenge", httpCmd.Flags().Lookup("scope-challenge")) diff --git a/pkg/http/server.go b/pkg/http/server.go index 3c9d7679e4..6dd27af299 100644 --- a/pkg/http/server.go +++ b/pkg/http/server.go @@ -32,9 +32,14 @@ type ServerConfig struct { // GitHub Host to target for API requests (e.g. github.com or github.enterprise.com) Host string - // Port to listen on (default: 8082) + // Port to listen on (default: 8082). + // Ignored when ListenAddress is set. Port int + // ListenAddress is the full listen address (host:port) for the HTTP server. + // When set it takes precedence over Port; otherwise the server listens on ":". + ListenAddress string + // BaseURL is the publicly accessible URL of this server for OAuth resource metadata. // If not set, the server will derive the URL from incoming request headers. BaseURL string @@ -192,7 +197,7 @@ func RunHTTPServer(cfg ServerConfig) error { }) logger.Info("OAuth protected resource endpoints registered", "baseURL", cfg.BaseURL) - addr := fmt.Sprintf(":%d", cfg.Port) + addr := resolveListenAddress(cfg.ListenAddress, cfg.Port) httpSvr := http.Server{ Addr: addr, Handler: r, @@ -223,6 +228,16 @@ func RunHTTPServer(cfg ServerConfig) error { return nil } +// resolveListenAddress returns the address string passed to http.Server. +// If listenAddress is non-empty it wins; otherwise the server binds to all +// interfaces on the given port. +func resolveListenAddress(listenAddress string, port int) string { + if listenAddress != "" { + return listenAddress + } + return fmt.Sprintf(":%d", port) +} + func initGlobalToolScopeMap(t translations.TranslationHelperFunc) error { // Build inventory with all tools to extract scope information inv, err := inventory.NewBuilder(). diff --git a/pkg/http/server_test.go b/pkg/http/server_test.go index 5458a6b395..50b2265bbd 100644 --- a/pkg/http/server_test.go +++ b/pkg/http/server_test.go @@ -131,6 +131,47 @@ func TestCreateHTTPFeatureChecker(t *testing.T) { } } +func TestResolveListenAddress(t *testing.T) { + tests := []struct { + name string + listenAddress string + port int + want string + }{ + { + name: "empty address falls back to :port", + listenAddress: "", + port: 8082, + want: ":8082", + }, + { + name: "explicit host:port wins over port", + listenAddress: "127.0.0.1:9090", + port: 8082, + want: "127.0.0.1:9090", + }, + { + name: "explicit :port form is preserved", + listenAddress: ":9090", + port: 8082, + want: ":9090", + }, + { + name: "ipv6 address with port is preserved", + listenAddress: "[::1]:9090", + port: 8082, + want: "[::1]:9090", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := resolveListenAddress(tt.listenAddress, tt.port) + assert.Equal(t, tt.want, got) + }) + } +} + func TestHeaderAllowedFeatureFlagsMatchesAllowed(t *testing.T) { // Ensure HeaderAllowedFeatureFlags delegates to AllowedFeatureFlags allowed := github.HeaderAllowedFeatureFlags()