@@ -321,8 +321,198 @@ func createFeatureChecker(enabledFeatures []string) inventory.FeatureFlagChecker
321321 for _ , f := range enabledFeatures {
322322 featureSet [f ] = true
323323 }
324- return func (_ context.Context , flagName string ) (bool , error ) {
325- return featureSet [flagName ], nil
324+
325+ gqlURL , err := url .Parse ("https://api.github.com/graphql" )
326+ if err != nil {
327+ return apiHost {}, fmt .Errorf ("failed to parse dotcom GraphQL URL: %w" , err )
328+ }
329+
330+ uploadURL , err := url .Parse ("https://uploads.github.com" )
331+ if err != nil {
332+ return apiHost {}, fmt .Errorf ("failed to parse dotcom Upload URL: %w" , err )
333+ }
334+
335+ rawURL , err := url .Parse ("https://raw.githubusercontent.com/" )
336+ if err != nil {
337+ return apiHost {}, fmt .Errorf ("failed to parse dotcom Raw URL: %w" , err )
338+ }
339+
340+ return apiHost {
341+ baseRESTURL : baseRestURL ,
342+ graphqlURL : gqlURL ,
343+ uploadURL : uploadURL ,
344+ rawURL : rawURL ,
345+ }, nil
346+ }
347+
348+ func newGHECHost (hostname string ) (apiHost , error ) {
349+ u , err := url .Parse (hostname )
350+ if err != nil {
351+ return apiHost {}, fmt .Errorf ("failed to parse GHEC URL: %w" , err )
352+ }
353+
354+ // Unsecured GHEC would be an error
355+ if u .Scheme == "http" {
356+ return apiHost {}, fmt .Errorf ("GHEC URL must be HTTPS" )
357+ }
358+
359+ restURL , err := url .Parse (fmt .Sprintf ("https://api.%s/" , u .Hostname ()))
360+ if err != nil {
361+ return apiHost {}, fmt .Errorf ("failed to parse GHEC REST URL: %w" , err )
362+ }
363+
364+ gqlURL , err := url .Parse (fmt .Sprintf ("https://api.%s/graphql" , u .Hostname ()))
365+ if err != nil {
366+ return apiHost {}, fmt .Errorf ("failed to parse GHEC GraphQL URL: %w" , err )
367+ }
368+
369+ uploadURL , err := url .Parse (fmt .Sprintf ("https://uploads.%s/" , u .Hostname ()))
370+ if err != nil {
371+ return apiHost {}, fmt .Errorf ("failed to parse GHEC Upload URL: %w" , err )
372+ }
373+
374+ rawURL , err := url .Parse (fmt .Sprintf ("https://raw.%s/" , u .Hostname ()))
375+ if err != nil {
376+ return apiHost {}, fmt .Errorf ("failed to parse GHEC Raw URL: %w" , err )
377+ }
378+
379+ return apiHost {
380+ baseRESTURL : restURL ,
381+ graphqlURL : gqlURL ,
382+ uploadURL : uploadURL ,
383+ rawURL : rawURL ,
384+ }, nil
385+ }
386+
387+ func newGHESHost (hostname string ) (apiHost , error ) {
388+ u , err := url .Parse (hostname )
389+ if err != nil {
390+ return apiHost {}, fmt .Errorf ("failed to parse GHES URL: %w" , err )
391+ }
392+
393+ restURL , err := url .Parse (fmt .Sprintf ("%s://%s/api/v3/" , u .Scheme , u .Hostname ()))
394+ if err != nil {
395+ return apiHost {}, fmt .Errorf ("failed to parse GHES REST URL: %w" , err )
396+ }
397+
398+ gqlURL , err := url .Parse (fmt .Sprintf ("%s://%s/api/graphql" , u .Scheme , u .Hostname ()))
399+ if err != nil {
400+ return apiHost {}, fmt .Errorf ("failed to parse GHES GraphQL URL: %w" , err )
401+ }
402+
403+ // Check if subdomain isolation is enabled
404+ // See https://docs.github.com/en/enterprise-server@3.17/admin/configuring-settings/hardening-security-for-your-enterprise/enabling-subdomain-isolation#about-subdomain-isolation
405+ hasSubdomainIsolation := checkSubdomainIsolation (u .Scheme , u .Hostname ())
406+
407+ var uploadURL * url.URL
408+ if hasSubdomainIsolation {
409+ // With subdomain isolation: https://uploads.hostname/
410+ uploadURL , err = url .Parse (fmt .Sprintf ("%s://uploads.%s/" , u .Scheme , u .Hostname ()))
411+ } else {
412+ // Without subdomain isolation: https://hostname/api/uploads/
413+ uploadURL , err = url .Parse (fmt .Sprintf ("%s://%s/api/uploads/" , u .Scheme , u .Hostname ()))
414+ }
415+ if err != nil {
416+ return apiHost {}, fmt .Errorf ("failed to parse GHES Upload URL: %w" , err )
417+ }
418+
419+ var rawURL * url.URL
420+ if hasSubdomainIsolation {
421+ // With subdomain isolation: https://raw.hostname/
422+ rawURL , err = url .Parse (fmt .Sprintf ("%s://raw.%s/" , u .Scheme , u .Hostname ()))
423+ } else {
424+ // Without subdomain isolation: https://hostname/raw/
425+ rawURL , err = url .Parse (fmt .Sprintf ("%s://%s/raw/" , u .Scheme , u .Hostname ()))
426+ }
427+ if err != nil {
428+ return apiHost {}, fmt .Errorf ("failed to parse GHES Raw URL: %w" , err )
429+ }
430+
431+ return apiHost {
432+ baseRESTURL : restURL ,
433+ graphqlURL : gqlURL ,
434+ uploadURL : uploadURL ,
435+ rawURL : rawURL ,
436+ }, nil
437+ }
438+
439+ // checkSubdomainIsolation detects if GitHub Enterprise Server has subdomain isolation enabled
440+ // by attempting to ping the raw.<host>/_ping endpoint on the subdomain. The raw subdomain must always exist for subdomain isolation.
441+ func checkSubdomainIsolation (scheme , hostname string ) bool {
442+ subdomainURL := fmt .Sprintf ("%s://raw.%s/_ping" , scheme , hostname )
443+
444+ client := & http.Client {
445+ Timeout : 5 * time .Second ,
446+ // Don't follow redirects - we just want to check if the endpoint exists
447+ //nolint:revive // parameters are required by http.Client.CheckRedirect signature
448+ CheckRedirect : func (req * http.Request , via []* http.Request ) error {
449+ return http .ErrUseLastResponse
450+ },
451+ }
452+
453+ resp , err := client .Get (subdomainURL )
454+ if err != nil {
455+ return false
456+ }
457+ defer resp .Body .Close ()
458+
459+ return resp .StatusCode == http .StatusOK
460+ }
461+
462+ // Note that this does not handle ports yet, so development environments are out.
463+ func parseAPIHost (s string ) (apiHost , error ) {
464+ if s == "" {
465+ return newDotcomHost ()
466+ }
467+
468+ u , err := url .Parse (s )
469+ if err != nil {
470+ return apiHost {}, fmt .Errorf ("could not parse host as URL: %s" , s )
471+ }
472+
473+ if u .Scheme == "" {
474+ return apiHost {}, fmt .Errorf ("host must have a scheme (http or https): %s" , s )
475+ }
476+
477+ if strings .HasSuffix (u .Hostname (), "github.com" ) {
478+ return newDotcomHost ()
479+ }
480+
481+ if strings .HasSuffix (u .Hostname (), "ghe.com" ) {
482+ return newGHECHost (s )
483+ }
484+
485+ return newGHESHost (s )
486+ }
487+
488+ type userAgentTransport struct {
489+ transport http.RoundTripper
490+ agent string
491+ }
492+
493+ func (t * userAgentTransport ) RoundTrip (req * http.Request ) (* http.Response , error ) {
494+ req = req .Clone (req .Context ())
495+ req .Header .Set ("User-Agent" , t .agent )
496+ return t .transport .RoundTrip (req )
497+ }
498+
499+ type bearerAuthTransport struct {
500+ transport http.RoundTripper
501+ token string
502+ }
503+
504+ func (t * bearerAuthTransport ) RoundTrip (req * http.Request ) (* http.Response , error ) {
505+ req = req .Clone (req .Context ())
506+ req .Header .Set ("Authorization" , "Bearer " + t .token )
507+ return t .transport .RoundTrip (req )
508+ }
509+
510+ func addGitHubAPIErrorToContext (next mcp.MethodHandler ) mcp.MethodHandler {
511+ return func (ctx context.Context , method string , req mcp.Request ) (result mcp.Result , err error ) {
512+ // Ensure the context is cleared of any previous errors
513+ // as context isn't propagated through middleware
514+ ctx = errors .ContextWithGitHubErrors (ctx )
515+ return next (ctx , method , req )
326516 }
327517}
328518
0 commit comments