{"record":{"id":"46dac45548c5e27b","repo":"github/github-mcp-server","slug":"authentication-required-set-github-personal-acces","errorCode":null,"errorMessage":"authentication required: set GITHUB_PERSONAL_ACCESS_TOKEN, configure GitHub App auth, or pass --oauth-client-id to log in via OAuth","messagePattern":"authentication required: set GITHUB_PERSONAL_ACCESS_TOKEN, configure GitHub App auth, or pass --oauth-client-id to log in via OAuth","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/github-mcp-server/main.go","lineNumber":63,"sourceCode":"\t\t\tappPrivateKeyPath := viper.GetString(\"app-private-key-path\")\n\t\t\tappPrivateKeyInline := viper.GetString(\"app-private-key\")\n\t\t\tappAuthRequested := appID != \"\" || appInstallationID != \"\" || appPrivateKeyPath != \"\" || appPrivateKeyInline != \"\"\n\n\t\t\toauthClientID := viper.GetString(\"oauth-client-id\")\n\t\t\toauthClientSecret := viper.GetString(\"oauth-client-secret\")\n\t\t\t// Fall back to the build-time baked-in client (official releases) when none is\n\t\t\t// configured explicitly. The baked-in app is registered on github.com, so it is\n\t\t\t// only applied to the default host; GHES/ghe.com users must bring their own\n\t\t\t// --oauth-client-id. Recognizing the host via NormalizeHost means an explicit\n\t\t\t// GITHUB_HOST=github.com (or api.github.com) still counts as the default and keeps\n\t\t\t// zero-config login working. The secret tracks the id, so an explicitly provided\n\t\t\t// id with no secret never picks up the baked-in secret.\n\t\t\tif oauthClientID == \"\" && !appAuthRequested && oauth.NormalizeHost(viper.GetString(\"host\")) == \"https://github.com\" {\n\t\t\t\toauthClientID = buildinfo.OAuthClientID\n\t\t\t\toauthClientSecret = buildinfo.OAuthClientSecret\n\t\t\t}\n\t\t\tif token == \"\" && !appAuthRequested && oauthClientID == \"\" {\n\t\t\t\treturn errors.New(\"authentication required: set GITHUB_PERSONAL_ACCESS_TOKEN, configure GitHub App auth, or pass --oauth-client-id to log in via OAuth\")\n\t\t\t}\n\t\t\tif appAuthRequested && token != \"\" {\n\t\t\t\treturn errors.New(\"GitHub App authentication and GITHUB_PERSONAL_ACCESS_TOKEN are mutually exclusive: set only one\")\n\t\t\t}\n\t\t\tif appAuthRequested && oauthClientID != \"\" {\n\t\t\t\treturn errors.New(\"GitHub App authentication and OAuth login (--oauth-client-id) are mutually exclusive: set only one\")\n\t\t\t}\n\n\t\t\t// If you're wondering why we're not using viper.GetStringSlice(\"toolsets\"),\n\t\t\t// it's because viper doesn't handle comma-separated values correctly for env\n\t\t\t// vars when using GetStringSlice.\n\t\t\t// https://github.com/spf13/viper/issues/380\n\t\t\t//\n\t\t\t// Additionally, viper.UnmarshalKey returns an empty slice even when the flag\n\t\t\t// is not set, but we need nil to indicate \"use defaults\". So we check IsSet first.\n\t\t\tvar enabledToolsets []string\n\t\t\tif viper.IsSet(\"toolsets\") {\n\t\t\t\tif err := viper.UnmarshalKey(\"toolsets\", &enabledToolsets); err != nil {","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/github/github-mcp-server/blob/0ea1f775a7c73eff1bd2e25904d01136756bbfe2/cmd/github-mcp-server/main.go#L45-L81","documentation":"HTTP 500 from the /.well-known/oauth-protected-resource metadata handler (pkg/http/oauth/oauth.go). When Config.AuthorizationServer is empty, the handler must derive the authorization server URL from the injected utils.APIHostResolver; if that call fails, the server cannot advertise authorization_servers in its RFC 9728 protected-resource metadata and returns 500. Note the built-in utils.APIHost parses all URLs at construction and never fails here, so a live failure almost always comes from a custom APIHostResolver implementation returning an error (bad host config, nil URL, failed lookup).","triggerScenarios":"A GET to any /.well-known/oauth-protected-resource route (root, /readonly, /insiders, /x/{toolset} variants) while oauth.Config.AuthorizationServer == \"\" and apiHost.AuthorizationServerURL(ctx) returns err != nil - typically because a custom APIHostResolver was injected (tests, hosted deployments, wrappers) whose resolution depends on runtime state or an unparseable GHE host string.","commonSituations":"Embedding the GitHub MCP Server HTTP handler with a hand-rolled APIHostResolver that can return errors; GHE_HOST/GHE API host misconfigured (missing scheme, cleartext http rejected by requireSecureScheme) so host-derived URL construction degrades; upgrading versions where the OAuth handler switched from a hardcoded https://github.com/login/oauth to resolver-based derivation; deployments that previously set AuthorizationServer and lost the setting during config migration.","solutions":["Set oauth.Config.AuthorizationServer explicitly (e.g. https://github.com/login/oauth for dotcom, https://<ghe-host>/login/oauth for GHES) - this bypasses the resolver entirely and is the most direct fix.","If you inject a custom utils.APIHostResolver, make AuthorizationServerURL return an already-parsed *url.URL with a nil error, mirroring utils.APIHost (parse once at construction, fail fast there).","Validate the API host string at startup with utils.NewAPIHost(host) (it enforces scheme presence and https-except-loopback) and surface construction errors before routes are served rather than at request time.","Check server logs for the wrapped %v error to identify which resolver implementation failed, then fix its inputs (host string, env var) accordingly."],"exampleFix":"// before - empty AuthorizationServer forces runtime resolution\nauthHandler, err := oauth.NewAuthHandler(&oauth.Config{\n    BaseURL: \"https://mcp.example.com\",\n    // AuthorizationServer omitted\n}, myFlakyResolver)\n\n// after - pin the authorization server, resolution can no longer 500\nauthHandler, err := oauth.NewAuthHandler(&oauth.Config{\n    BaseURL:            \"https://mcp.example.com\",\n    AuthorizationServer: \"https://github.com/login/oauth\",\n}, myFlakyResolver)","handlingStrategy":"validation","validationCode":"// Fail fast at startup: verify the resolver can produce the auth server URL\n// before the HTTP server accepts traffic.\nfunc validateOAuthDeps(cfg *oauth.Config, host string) error {\n    if cfg == nil || cfg.AuthorizationServer == \"\" {\n        resolver, err := utils.NewAPIHost(host) // enforces scheme + https rules\n        if err != nil {\n            return fmt.Errorf(\"api host invalid: %w\", err)\n        }\n        u, err := resolver.AuthorizationServerURL(context.Background())\n        if err != nil || u == nil {\n            return fmt.Errorf(\"cannot resolve authorization server URL: %v\", err)\n        }\n    }\n    if _, err := url.Parse(cfg.AuthorizationServer); cfg != nil && cfg.AuthorizationServer != \"\" && err != nil {\n        return fmt.Errorf(\"AuthorizationServer not a valid URL: %w\", err)\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"// Server-side wrapper: log and convert metadata 500s distinctly so clients\n// can distinguish discovery-path breakage from tool failures.\nif resp.StatusCode == http.StatusInternalServerError &&\n    strings.HasPrefix(r.URL.Path, \"/.well-known/oauth-protected-resource\") {\n    // server could not build authorization_servers metadata;\n    // check Config.AuthorizationServer / APIHostResolver health, do not retry blindly\n}\n// Go server authors: wrap the resolver so the error is logged with its cause\nwrapped := func(ctx context.Context) (*url.URL, error) {\n    u, err := apiHost.AuthorizationServerURL(ctx)\n    if err != nil {\n        log.Printf(\"AuthorizationServerURL failed: %v\", err)\n    }\n    return u, err\n}","preventionTips":["Pin oauth.Config.AuthorizationServer explicitly in every deployment (dotcom: https://github.com/login/oauth; GHES: https://<host>/login/oauth) instead of relying on runtime derivation.","Custom APIHostResolver implementations should parse all URLs in their constructor and return only pre-resolved values from AuthorizationServerURL, copying the utils.APIHost pattern.","Run utils.NewAPIHost(host) as a startup gate; it rejects missing schemes, cleartext http for non-loopback hosts, and unparseable GHE hosts before any request can 500.","Health-check the /.well-known/oauth-protected-resource endpoint in deployment smoke tests so resolver regressions surface at rollout, not from client reports."],"tags":["oauth","configuration","metadata","http","ghes"],"backgroundTag":null,"analyzedSha":"0ea1f775a7c73eff1bd2e25904d01136756bbfe2","analyzedAt":"2026-08-15T18:10:19.804Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}