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

  1. 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.
  2. 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).
  3. 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.
  4. 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

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

Related errors


AI-assisted analysis of github/github-mcp-server@0ea1f775a7 (2026-08-15). Data as JSON: /api/errors/46dac45548c5e27b. Report an issue: GitHub.