github/github-mcp-server · error
authentication required: set GITHUB_PERSONAL_ACCESS_TOKEN, c
Error message
authentication required: set GITHUB_PERSONAL_ACCESS_TOKEN, configure GitHub App auth, or pass --oauth-client-id to log in via OAuth
What it means
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).
Source
Thrown at cmd/github-mcp-server/main.go:63
appPrivateKeyPath := viper.GetString("app-private-key-path")
appPrivateKeyInline := viper.GetString("app-private-key")
appAuthRequested := appID != "" || appInstallationID != "" || appPrivateKeyPath != "" || appPrivateKeyInline != ""
oauthClientID := viper.GetString("oauth-client-id")
oauthClientSecret := viper.GetString("oauth-client-secret")
// Fall back to the build-time baked-in client (official releases) when none is
// configured explicitly. The baked-in app is registered on github.com, so it is
// only applied to the default host; GHES/ghe.com users must bring their own
// --oauth-client-id. Recognizing the host via NormalizeHost means an explicit
// GITHUB_HOST=github.com (or api.github.com) still counts as the default and keeps
// zero-config login working. The secret tracks the id, so an explicitly provided
// id with no secret never picks up the baked-in secret.
if oauthClientID == "" && !appAuthRequested && oauth.NormalizeHost(viper.GetString("host")) == "https://github.com" {
oauthClientID = buildinfo.OAuthClientID
oauthClientSecret = buildinfo.OAuthClientSecret
}
if token == "" && !appAuthRequested && oauthClientID == "" {
return errors.New("authentication required: set GITHUB_PERSONAL_ACCESS_TOKEN, configure GitHub App auth, or pass --oauth-client-id to log in via OAuth")
}
if appAuthRequested && token != "" {
return errors.New("GitHub App authentication and GITHUB_PERSONAL_ACCESS_TOKEN are mutually exclusive: set only one")
}
if appAuthRequested && oauthClientID != "" {
return errors.New("GitHub App authentication and OAuth login (--oauth-client-id) are mutually exclusive: set only one")
}
// If you're wondering why we're not using viper.GetStringSlice("toolsets"),
// it's because viper doesn't handle comma-separated values correctly for env
// vars when using GetStringSlice.
// https://github.com/spf13/viper/issues/380
//
// Additionally, viper.UnmarshalKey returns an empty slice even when the flag
// is not set, but we need nil to indicate "use defaults". So we check IsSet first.
var enabledToolsets []string
if viper.IsSet("toolsets") {
if err := viper.UnmarshalKey("toolsets", &enabledToolsets); err != nil {View on GitHub (pinned to 0ea1f775a7)
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.
Example fix
// before - empty AuthorizationServer forces runtime resolution
authHandler, err := oauth.NewAuthHandler(&oauth.Config{
BaseURL: "https://mcp.example.com",
// AuthorizationServer omitted
}, myFlakyResolver)
// after - pin the authorization server, resolution can no longer 500
authHandler, err := oauth.NewAuthHandler(&oauth.Config{
BaseURL: "https://mcp.example.com",
AuthorizationServer: "https://github.com/login/oauth",
}, myFlakyResolver) Defensive patterns
Strategy: validation
Validate before calling
// Fail fast at startup: verify the resolver can produce the auth server URL
// before the HTTP server accepts traffic.
func validateOAuthDeps(cfg *oauth.Config, host string) error {
if cfg == nil || cfg.AuthorizationServer == "" {
resolver, err := utils.NewAPIHost(host) // enforces scheme + https rules
if err != nil {
return fmt.Errorf("api host invalid: %w", err)
}
u, err := resolver.AuthorizationServerURL(context.Background())
if err != nil || u == nil {
return fmt.Errorf("cannot resolve authorization server URL: %v", err)
}
}
if _, err := url.Parse(cfg.AuthorizationServer); cfg != nil && cfg.AuthorizationServer != "" && err != nil {
return fmt.Errorf("AuthorizationServer not a valid URL: %w", err)
}
return nil
} Try / catch
// Server-side wrapper: log and convert metadata 500s distinctly so clients
// can distinguish discovery-path breakage from tool failures.
if resp.StatusCode == http.StatusInternalServerError &&
strings.HasPrefix(r.URL.Path, "/.well-known/oauth-protected-resource") {
// server could not build authorization_servers metadata;
// check Config.AuthorizationServer / APIHostResolver health, do not retry blindly
}
// Go server authors: wrap the resolver so the error is logged with its cause
wrapped := func(ctx context.Context) (*url.URL, error) {
u, err := apiHost.AuthorizationServerURL(ctx)
if err != nil {
log.Printf("AuthorizationServerURL failed: %v", err)
}
return u, err
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- App not connected
- starting callback listener on %s: %w
- no authorization code in callback
- OAuth callback port %d is not available; another process may
- exchanging authorization code: %w
AI-assisted analysis of github/github-mcp-server@0ea1f775a7 (2026-08-15).
Data as JSON: /api/errors/46dac45548c5e27b.
Report an issue: GitHub.