repo_description: GitHub's official MCP Server
repo_topics: github mcp mcp-server
code: package http
  
  import (
  	"context"
  	"errors"
  	"log/slog"
  	"net/http"
  
  	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/scopes"
  	"github.com/github/github-mcp-server/pkg/translations"
  	"github.com/github/github-mcp-server/pkg/utils"
  	"github.com/go-chi/chi/v5"
  	"github.com/modelcontextprotocol/go-sdk/mcp"
  )
  
  type InventoryFactoryFunc func(r *http.Request) (*inventory.Inventory, error)
  
  // GitHubMCPServerFactoryFunc is a function type for creating a new MCP Server instance.
  // middleware are applied AFTER the default GitHub MCP Server middlewares (like error context injection)
  type GitHubMCPServerFactoryFunc func(r *http.Request, deps github.ToolDependencies, inventory *inventory.Inventory, cfg *github.MCPServerConfig) (*mcp.Server, error)
  
  type Handler struct {
  	ctx                    context.Context
  	config                 *ServerConfig
  	deps                   github.ToolDependencies
  	logger                 *slog.Logger
  	apiHosts               utils.APIHostResolver
  	t                      translations.TranslationHelperFunc
  	githubMcpServerFactory GitHubMCPServerFactoryFunc
  	inventoryFactoryFunc   InventoryFactoryFunc
  	oauthCfg               *oauth.Config
  	scopeFetcher           scopes.FetcherInterface
  	schemaCache            *mcp.SchemaCache
  }
  
  type HandlerOptions struct {
  	GitHubMcpServerFactory GitHubMCPServerFactoryFunc
  	InventoryFactory       InventoryFactoryFunc
  	OAuthConfig            *oauth.Config
  	ScopeFetcher           scopes.FetcherInterface
  	FeatureChecker         inventory.FeatureFlagChecker
  }
  
  type HandlerOption func(*HandlerOptions)
  
  func WithScopeFetcher(f scopes.FetcherInterface) HandlerOption {
  	return func(o *HandlerOptions) {
  		o.ScopeFetcher = f
  	}
  }
  
  func WithGitHubMCPServerFactory(f GitHubMCPServerFactoryFunc) HandlerOption {
  	return func(o *HandlerOptions) {
  		o.GitHubMcpServerFactory = f
  	}
  }
  
  func WithInventoryFactory(f InventoryFactoryFunc) HandlerOption {
  	return func(o *HandlerOptions) {
  		o.InventoryFactory = f
  	}
  }
  
  func WithOAuthConfig(cfg *oauth.Config) HandlerOption {
  	return func(o *HandlerOptions) {
  		o.OAuthConfig = cfg
  	}
  }
  
  func WithFeatureChecker(checker inventory.FeatureFlagChecker) HandlerOption {
  	return func(o *HandlerOptions) {
  		o.FeatureChecker = checker
  	}
  }
  
  func NewHTTPMcpHandler(
  	ctx context.Context,
  	cfg *ServerConfig,
  	deps github.ToolDependencies,
  	t translations.TranslationHelperFunc,
  	logger *slog.Logger,
  	apiHost utils.APIHostResolver,
  	options ...HandlerOption) *Handler {
  	opts := &HandlerOptions{}
  	for _, o := range options {
  		o(opts)
  	}
  
  	githubMcpServerFactory := opts.GitHubMcpServerFactory
  	if githubMcpServerFactory == nil {
  		githubMcpServerFactory = DefaultGitHubMCPServerFactory
  	}
  
  	scopeFetcher := opts.ScopeFetcher
  	if scopeFetcher == nil {
  		scopeFetcher = scopes.NewFetcher(apiHost, scopes.FetcherOptions{})
  	}
  
  	inventoryFactory := opts.InventoryFactory
  	if inventoryFactory == nil {
  		inventoryFactory = DefaultInventoryFactory(cfg, t, opts.FeatureChecker, scopeFetcher)
  	}
  
  	// Create a shared schema cache to avoid repeated JSON schema reflection
  	// when a new MCP Server is created per request in stateless mode.
  	schemaCache := mcp.NewSchemaCache()
  
  	return &Handler{
  		ctx:                    ctx,
  		config:                 cfg,
  		deps:                   deps,
  		logger:                 logger,
  		apiHosts:               apiHost,
  		t:                      t,
  		githubMcpServerFactory: githubMcpServerFactory,
  		inventoryFactoryFunc:   inventoryFactory,
  		oauthCfg:               opts.OAuthConfig,
  		scopeFetcher:           scopeFetcher,
  		schemaCache:            schemaCache,
  	}
  }
  
  func (h *Handler) RegisterMiddleware(r chi.Router) {
  	r.Use(
  		middleware.ExtractUserToken(h.oauthCfg),
  		middleware.WithRequestConfig,
  		middleware.WithMCPParse(),
  		middleware.WithPATScopes(h.logger, h.scopeFetcher),
  	)
  
  	if h.config.ScopeChallenge {
  		r.Use(middleware.WithScopeChallenge(h.oauthCfg, h.scopeFetcher))
  	}
  }
  
  // RegisterRoutes registers the routes for the MCP server
  // URL-based values take precedence over header-based values
  func (h *Handler) RegisterRoutes(r chi.Router) {
  	// Base routes
  	r.Mount("/", h)
  	r.With(withReadonly).Mount("/readonly", h)
  	r.With(withInsiders).Mount("/insiders", h)
  	r.With(withReadonly, withInsiders).Mount("/readonly/insiders", h)
  
  	// Toolset routes
  	r.With(withToolset).Mount("/x/{toolset}", h)
  	r.With(withToolset, withReadonly).Mount("/x/{toolset}/readonly", h)
  	r.With(withToolset, withInsiders).Mount("/x/{toolset}/insiders", h)
  	r.With(withToolset, withReadonly, withInsiders).Mount("/x/{toolset}/readonly/insiders", h)
  }
  
  // withReadonly is middleware that sets readonly mode in the request context
  func withReadonly(next http.Handler) http.Handler {
  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		ctx := ghcontext.WithReadonly(r.Context(), true)
  		next.ServeHTTP(w, r.WithContext(ctx))
  	})
  }
  
  // withToolset is middleware that extracts the toolset from the URL and sets it in the request context
  func withToolset(next http.Handler) http.Handler {
  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		toolset := chi.URLParam(r, "toolset")
  		ctx := ghcontext.WithToolsets(r.Context(), []string{toolset})
  		next.ServeHTTP(w, r.WithContext(ctx))
  	})
  }
  
  // withInsiders is middleware that sets insiders mode in the request context
  func withInsiders(next http.Handler) http.Handler {
  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		ctx := ghcontext.WithInsidersMode(r.Context(), true)
  		next.ServeHTTP(w, r.WithContext(ctx))
  	})
  }
  
  func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  	inv, err := h.inventoryFactoryFunc(r)
  	if err != nil {
  		if errors.Is(err, inventory.ErrUnknownTools) {
  			w.WriteHeader(http.StatusBadRequest)
  			if _, writeErr := w.Write([]byte(err.Error())); writeErr != nil {
  				h.logger.Error("failed to write response", "error", writeErr)
  			}
  			return
  		}
  
  		w.WriteHeader(http.StatusInternalServerError)
  		return
  	}
  
  	invToUse := inv
  	if methodInfo, ok := ghcontext.MCPMethod(r.Context()); ok && methodInfo != nil {
  		invToUse = inv.ForMCPRequest(methodInfo.Method, methodInfo.ItemName)
  	}
  
  	ghServer, err := h.githubMcpServerFactory(r, h.deps, invToUse, &github.MCPServerConfig{
  		Version:           h.config.Version,
  		Translator:        h.t,
  		ContentWindowSize: h.config.ContentWindowSize,
  		Logger:            h.logger,
  		RepoAccessTTL:     h.config.RepoAccessCacheTTL,
  		// Explicitly set empty capabilities. inv.ForMCPRequest currently returns nothing for Initialize.
  		ServerOptions: []github.MCPServerOption{
  			func(so *mcp.ServerOptions) {
  				so.Capabilities = &mcp.ServerCapabilities{
  					Tools:     &mcp.ToolCapabilities{},
  					Resources: &mcp.ResourceCapabilities{},
  					Prompts:   &mcp.PromptCapabilities{},
  				}
  				so.SchemaCache = h.schemaCache
  			},
  		},
  	})
  
  	if err != nil {
  		w.WriteHeader(http.StatusInternalServerError)
  		return
  	}
  
  	mcpHandler := mcp.NewStreamableHTTPHandler(func(_ *http.Request) *mcp.Server {
  		return ghServer
  	}, &mcp.StreamableHTTPOptions{
  		Stateless: true,
  	})
  
  	mcpHandler.ServeHTTP(w, r)
  }
  
  func DefaultGitHubMCPServerFactory(r *http.Request, deps github.ToolDependencies, inventory *inventory.Inventory, cfg *github.MCPServerConfig) (*mcp.Server, error) {
  	return github.NewMCPServer(r.Context(), cfg, deps, inventory)
  }
  
  // DefaultInventoryFactory creates the default inventory factory for HTTP mode.
  // When the ServerConfig includes static flags (--toolsets, --read-only, etc.),
  // a static inventory is built once at factory creation to pre-filter the tool
  // universe. Per-request headers can only narrow within these bounds.
  func DefaultInventoryFactory(cfg *ServerConfig, t translations.TranslationHelperFunc, featureChecker inventory.FeatureFlagChecker, scopeFetcher scopes.FetcherInterface) InventoryFactoryFunc {
  	// Build the static tool/resource/prompt universe from CLI flags.
  	// This is done once at startup and captured in the closure.
  	staticTools, staticResources, staticPrompts := buildStaticInventory(cfg, t, featureChecker)
  	hasStaticFilters := hasStaticConfig(cfg)
  
  	// Pre-compute valid tool names for filtering per-request tool headers.
  	// When a request asks for a tool by name that's been excluded from the
  	// static universe, we silently drop it rather than returning an error.
  	validToolNames := make(map[string]bool, len(staticTools))
  	for i := range staticTools {
  		validToolNames[staticTools[i].Tool.Name] = true
  	}
  
  	return func(r *http.Request) (*inventory.Inventory, error) {
  		b := inventory.NewBuilder().
  			SetTools(staticTools).
  			SetResources(staticResources).
  			SetPrompts(staticPrompts).
  			WithDeprecatedAliases(github.DeprecatedToolAliases).
  			WithFeatureChecker(featureChecker)
  
  		// When static flags constrain the universe, default to showing
  		// everything within those bounds (per-request filters narrow further).
  		// When no static flags are set, preserve existing behavior where
  		// the default toolsets apply.
  		if hasStaticFilters {
  			b = b.WithToolsets([]string{"all"})
  		}
  
  		// Static read-only is an upper bound — enforce before request filters
  		if cfg.ReadOnly {
  			b = b.WithReadOnly(true)
  		}
  
  		// Filter request tool names to only those in the static universe,
  		// so requests for statically-excluded tools degrade gracefully.
  		if hasStaticFilters {
  			r = filterRequestTools(r, validToolNames)
  		}
  
  		b = InventoryFiltersForRequest(r, b)
  		b = PATScopeFilter(b, r, scopeFetcher)
  
  		b.WithServerInstructions()
  
  		return b.Build()
  	}
  }
  
  // filterRequestTools returns a shallow copy of the request with any per-request
  // tool names (from X-MCP-Tools header) filtered to only include tools that exist
  // in validNames. This ensures requests for statically-excluded tools are silently
  // ignored rather than causing build errors.
  func filterRequestTools(r *http.Request, validNames map[string]bool) *http.Request {
  	reqTools := ghcontext.GetTools(r.Context())
  	if len(reqTools) == 0 {
  		return r
  	}
  
  	filtered := make([]string, 0, len(reqTools))
  	for _, name := range reqTools {
  		if validNames[name] {
  			filtered = append(filtered, name)
  		}
  	}
  	ctx := ghcontext.WithTools(r.Context(), filtered)
  	return r.WithContext(ctx)
  }
  
  // hasStaticConfig returns true if any static filtering flags are set on the ServerConfig.
  func hasStaticConfig(cfg *ServerConfig) bool {
  	return cfg.ReadOnly ||
  		cfg.EnabledToolsets != nil ||
  		cfg.EnabledTools != nil ||
  		cfg.DynamicToolsets ||
  		len(cfg.ExcludeTools) > 0 ||
  		cfg.InsidersMode
  }
  
  // buildStaticInventory pre-filters the full tool/resource/prompt universe using
  // the static CLI flags (--toolsets, --read-only, --exclude-tools, etc.).
  // The returned slices serve as the upper bound for per-request inventory builders.
  func buildStaticInventory(cfg *ServerConfig, t translations.TranslationHelperFunc, featureChecker inventory.FeatureFlagChecker) ([]inventory.ServerTool, []inventory.ServerResourceTemplate, []inventory.ServerPrompt) {
  	if !hasStaticConfig(cfg) {
  		return github.AllTools(t), github.AllResources(t), github.AllPrompts(t)
  	}
  
  	b := github.NewInventory(t).
  		WithFeatureChecker(featureChecker).
  		WithReadOnly(cfg.ReadOnly).
  		WithToolsets(github.ResolvedEnabledToolsets(cfg.DynamicToolsets, cfg.EnabledToolsets, cfg.EnabledTools))
  
  	if len(cfg.EnabledTools) > 0 {
  		b = b.WithTools(github.CleanTools(cfg.EnabledTools))
  	}
  
  	if len(cfg.ExcludeTools) > 0 {
  		b = b.WithExcludeTools(cfg.ExcludeTools)
  	}
  
  	inv, err := b.Build()
  	if err != nil {
  		// Fall back to all tools if there's an error (e.g. unknown tool names).
  		// The error will surface again at per-request time if relevant.
  		return github.AllTools(t), github.AllResources(t), github.AllPrompts(t)
  	}
  
  	ctx := context.Background()
  	return inv.AvailableTools(ctx), inv.AvailableResourceTemplates(ctx), inv.AvailablePrompts(ctx)
  }
  
  // InventoryFiltersForRequest applies filters to the inventory builder
  // based on the request context and headers.
  // MCP Apps UI metadata is handled by the builder via the feature checker —
  // no need to check headers here.
  func InventoryFiltersForRequest(r *http.Request, builder *inventory.Builder) *inventory.Builder {
  	ctx := r.Context()
  
  	if ghcontext.IsReadonly(ctx) {
  		builder = builder.WithReadOnly(true)
  	}
  
  	toolsets := ghcontext.GetToolsets(ctx)
  	tools := ghcontext.GetTools(ctx)
  
  	if len(toolsets) > 0 {
  		builder = builder.WithToolsets(github.ResolvedEnabledToolsets(false, toolsets, tools)) // No dynamic toolsets in HTTP mode
  	}
  
  	if len(tools) > 0 {
  		if len(toolsets) == 0 {
  			builder = builder.WithToolsets([]string{})
  		}
  		builder = builder.WithTools(github.CleanTools(tools))
  	}
  
  	if excluded := ghcontext.GetExcludeTools(ctx); len(excluded) > 0 {
  		builder = builder.WithExcludeTools(excluded)
  	}
  
  	return builder
  }
  
  func PATScopeFilter(b *inventory.Builder, r *http.Request, fetcher scopes.FetcherInterface) *inventory.Builder {
  	ctx := r.Context()
  
  	tokenInfo, ok := ghcontext.GetTokenInfo(ctx)
  	if !ok || tokenInfo == nil {
  		return b
  	}
  
  	// Scopes should have already been fetched by the WithPATScopes middleware.
  	// Only classic PATs (ghp_ prefix) return OAuth scopes via X-OAuth-Scopes header.
  	// Fine-grained PATs and other token types don't support this, so we skip filtering.
  	if tokenInfo.TokenType == utils.TokenTypePersonalAccessToken {
  		// Check if scopes are already in context (should be set by WithPATScopes). If not, fetch them.
  		existingScopes, ok := ghcontext.GetTokenScopes(ctx)
  		if ok {
  			return b.WithFilter(github.CreateToolScopeFilter(existingScopes))
  		}
  
  		scopesList, err := fetcher.FetchTokenScopes(ctx, tokenInfo.Token)
  		if err != nil {
  			return b
  		}
  
  		return b.WithFilter(github.CreateToolScopeFilter(scopesList))
  	}
  
  	return b
  }
  