chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
ghcontext "github.com/github/github-mcp-server/pkg/context"
|
||||
"github.com/github/github-mcp-server/pkg/github"
|
||||
"github.com/github/github-mcp-server/pkg/http/middleware"
|
||||
"github.com/github/github-mcp-server/pkg/http/oauth"
|
||||
"github.com/github/github-mcp-server/pkg/inventory"
|
||||
"github.com/github/github-mcp-server/pkg/lockdown"
|
||||
"github.com/github/github-mcp-server/pkg/observability"
|
||||
"github.com/github/github-mcp-server/pkg/observability/metrics"
|
||||
"github.com/github/github-mcp-server/pkg/scopes"
|
||||
"github.com/github/github-mcp-server/pkg/translations"
|
||||
"github.com/github/github-mcp-server/pkg/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type ServerConfig struct {
|
||||
// Version of the server
|
||||
Version string
|
||||
|
||||
// GitHub Host to target for API requests (e.g. github.com or github.enterprise.com)
|
||||
Host string
|
||||
|
||||
// Port to listen on (default: 8082).
|
||||
Port int
|
||||
|
||||
// ListenHost is the host the HTTP server binds to (e.g. "127.0.0.1").
|
||||
// When empty, the server binds to all interfaces. Combined with Port.
|
||||
ListenHost 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
|
||||
|
||||
// ResourcePath is the externally visible base path for this server (e.g., "/mcp").
|
||||
// This is used to restore the original path when a proxy strips a base path before forwarding.
|
||||
ResourcePath string
|
||||
|
||||
// TrustProxyHeaders indicates whether X-Forwarded-Host and X-Forwarded-Proto
|
||||
// should be honored when constructing OAuth resource metadata URLs. Only
|
||||
// enable this when the server is deployed behind a trusted proxy that sets
|
||||
// these headers. When BaseURL is set, it always wins and this setting has
|
||||
// no effect.
|
||||
TrustProxyHeaders bool
|
||||
|
||||
// ExportTranslations indicates if we should export translations
|
||||
// See: https://github.com/github/github-mcp-server?tab=readme-ov-file#i18n--overriding-descriptions
|
||||
ExportTranslations bool
|
||||
|
||||
// EnableCommandLogging indicates if we should log commands
|
||||
EnableCommandLogging bool
|
||||
|
||||
// Path to the log file if not stderr
|
||||
LogFilePath string
|
||||
|
||||
// Content window size
|
||||
ContentWindowSize int
|
||||
|
||||
// LockdownMode indicates if we should enable lockdown mode
|
||||
LockdownMode bool
|
||||
|
||||
// RepoAccessCacheTTL overrides the default TTL for repository access cache entries.
|
||||
RepoAccessCacheTTL *time.Duration
|
||||
|
||||
// ScopeChallenge indicates if we should return OAuth scope challenges, and if we should perform
|
||||
// tool filtering based on token scopes.
|
||||
ScopeChallenge bool
|
||||
|
||||
// ReadOnly indicates if we should only register read-only tools.
|
||||
// When set via CLI flag, this acts as an upper bound — per-request headers
|
||||
// cannot re-enable write tools.
|
||||
ReadOnly bool
|
||||
|
||||
// EnabledToolsets is a list of toolsets to enable.
|
||||
// When set via CLI flag, per-request headers can only narrow within these toolsets.
|
||||
EnabledToolsets []string
|
||||
|
||||
// EnabledTools is a list of specific tools to enable (additive to toolsets).
|
||||
EnabledTools []string
|
||||
|
||||
// ExcludeTools is a list of tool names to disable regardless of other settings.
|
||||
// When set via CLI flag, per-request headers cannot re-include these tools.
|
||||
ExcludeTools []string
|
||||
|
||||
// EnabledFeatures is a list of feature flags that are enabled.
|
||||
EnabledFeatures []string
|
||||
|
||||
// InsidersMode expands to the curated set of feature flags enabled for insiders.
|
||||
InsidersMode bool
|
||||
}
|
||||
|
||||
func RunHTTPServer(cfg ServerConfig) error {
|
||||
// Create app context
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
t, dumpTranslations := translations.TranslationHelper()
|
||||
|
||||
var slogHandler slog.Handler
|
||||
var logOutput io.Writer
|
||||
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)
|
||||
}
|
||||
logOutput = file
|
||||
slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelDebug})
|
||||
} else {
|
||||
logOutput = os.Stderr
|
||||
slogHandler = slog.NewTextHandler(logOutput, &slog.HandlerOptions{Level: slog.LevelInfo})
|
||||
}
|
||||
logger := slog.New(slogHandler)
|
||||
logger.Info("starting server", "version", cfg.Version, "host", cfg.Host, "lockdownEnabled", cfg.LockdownMode, "readOnly", cfg.ReadOnly, "insidersMode", cfg.InsidersMode)
|
||||
|
||||
apiHost, err := utils.NewAPIHost(cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse API host: %w", err)
|
||||
}
|
||||
|
||||
repoAccessOpts := []lockdown.RepoAccessOption{
|
||||
lockdown.WithLogger(logger.With("component", "lockdown")),
|
||||
}
|
||||
if cfg.RepoAccessCacheTTL != nil {
|
||||
repoAccessOpts = append(repoAccessOpts, lockdown.WithTTL(*cfg.RepoAccessCacheTTL))
|
||||
}
|
||||
|
||||
featureChecker := createHTTPFeatureChecker(cfg.EnabledFeatures, cfg.InsidersMode)
|
||||
|
||||
obs, err := observability.NewExporters(logger, metrics.NewNoopMetrics())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create observability exporters: %w", err)
|
||||
}
|
||||
|
||||
deps := github.NewRequestDeps(
|
||||
apiHost,
|
||||
cfg.Version,
|
||||
cfg.LockdownMode,
|
||||
repoAccessOpts,
|
||||
t,
|
||||
cfg.ContentWindowSize,
|
||||
featureChecker,
|
||||
obs,
|
||||
)
|
||||
|
||||
// Initialize the global tool scope map
|
||||
err = initGlobalToolScopeMap(t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize tool scope map: %w", err)
|
||||
}
|
||||
|
||||
// Register OAuth protected resource metadata endpoints
|
||||
oauthCfg := &oauth.Config{
|
||||
BaseURL: cfg.BaseURL,
|
||||
ResourcePath: cfg.ResourcePath,
|
||||
TrustProxyHeaders: cfg.TrustProxyHeaders,
|
||||
}
|
||||
|
||||
serverOptions := []HandlerOption{}
|
||||
if cfg.ScopeChallenge {
|
||||
scopeFetcher := scopes.NewFetcher(apiHost, scopes.FetcherOptions{})
|
||||
serverOptions = append(serverOptions, WithScopeFetcher(scopeFetcher))
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
handler := NewHTTPMcpHandler(ctx, &cfg, deps, t, logger, apiHost, append(serverOptions, WithFeatureChecker(featureChecker), WithOAuthConfig(oauthCfg))...)
|
||||
oauthHandler, err := oauth.NewAuthHandler(oauthCfg, apiHost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create OAuth handler: %w", err)
|
||||
}
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.SetCorsHeaders)
|
||||
|
||||
// Register Middleware First, needs to be before route registration
|
||||
handler.RegisterMiddleware(r)
|
||||
|
||||
// Register MCP server routes
|
||||
handler.RegisterRoutes(r)
|
||||
})
|
||||
logger.Info("MCP endpoints registered", "baseURL", cfg.BaseURL)
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
// Register OAuth protected resource metadata endpoints
|
||||
oauthHandler.RegisterRoutes(r)
|
||||
})
|
||||
logger.Info("OAuth protected resource endpoints registered", "baseURL", cfg.BaseURL)
|
||||
|
||||
addr := resolveListenAddress(cfg.ListenHost, cfg.Port)
|
||||
httpSvr := http.Server{
|
||||
Addr: addr,
|
||||
Handler: r,
|
||||
ReadHeaderTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
logger.Info("shutting down server")
|
||||
if err := httpSvr.Shutdown(shutdownCtx); err != nil {
|
||||
logger.Error("error during server shutdown", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if cfg.ExportTranslations {
|
||||
// Once server is initialized, all translations are loaded
|
||||
dumpTranslations()
|
||||
}
|
||||
|
||||
logger.Info("HTTP server listening", "addr", addr)
|
||||
if err := httpSvr.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
return fmt.Errorf("HTTP server error: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("server stopped gracefully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveListenAddress returns the address string passed to http.Server.
|
||||
// When host is empty the server binds to all interfaces on the given port;
|
||||
// otherwise host and port are joined into a single address.
|
||||
func resolveListenAddress(host string, port int) string {
|
||||
if host == "" {
|
||||
return fmt.Sprintf(":%d", port)
|
||||
}
|
||||
return net.JoinHostPort(host, strconv.Itoa(port))
|
||||
}
|
||||
|
||||
func initGlobalToolScopeMap(t translations.TranslationHelperFunc) error {
|
||||
// Build inventory with all tools to extract scope information
|
||||
inv, err := inventory.NewBuilder().
|
||||
SetTools(github.AllTools(t)).
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build inventory for tool scope map: %w", err)
|
||||
}
|
||||
|
||||
// Initialize the global scope map
|
||||
scopes.SetToolScopeMapFromInventory(inv)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createHTTPFeatureChecker creates a feature checker that resolves static CLI
|
||||
// features plus per-request header features and insiders mode.
|
||||
func createHTTPFeatureChecker(enabledFeatures []string, insidersMode bool) inventory.FeatureFlagChecker {
|
||||
return func(ctx context.Context, flag string) (bool, error) {
|
||||
headerFeatures := ghcontext.GetHeaderFeatures(ctx)
|
||||
features := make([]string, 0, len(enabledFeatures)+len(headerFeatures))
|
||||
features = append(features, enabledFeatures...)
|
||||
features = append(features, headerFeatures...)
|
||||
|
||||
effective := github.ResolveFeatureFlags(features, insidersMode || ghcontext.IsInsidersMode(ctx))
|
||||
return effective[flag], nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user