github/github-mcp-server · error
installation token request failed: %s: %s
Error message
installation token request failed: %s: %s
What it means
The installation-token request completed but GitHub returned a status other than 201 Created; the message embeds the HTTP status plus the first 512 bytes of GitHub's JSON error body (e.g. 'Bad credentials', 'Not Found', 'Integration must acquire access tokens'). This is the primary channel for API-level auth/config failures in the githubapp package — the status + snippet pinpoint which credential is wrong.
Source
Thrown at internal/githubapp/githubapp.go:158
if err != nil {
return nil, fmt.Errorf("creating installation token request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+jwt)
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
resp, err := s.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("requesting installation token: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusCreated {
snippet, readErr := io.ReadAll(io.LimitReader(resp.Body, 512))
if readErr != nil {
return nil, fmt.Errorf("installation token request failed: %s (reading response: %w)", resp.Status, readErr)
}
return nil, fmt.Errorf("installation token request failed: %s: %s", resp.Status, strings.TrimSpace(string(snippet)))
}
var body struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return nil, fmt.Errorf("decoding installation token response: %w", err)
}
if body.Token == "" {
return nil, errors.New("installation token response did not contain a token")
}
if body.ExpiresAt.IsZero() {
return nil, errors.New("installation token response did not contain an expiry")
}
return &oauth2.Token{
AccessToken: body.Token,
TokenType: "token",View on GitHub (pinned to 0ea1f775a7)
Solutions
- 401: verify the AppID matches the app whose .pem you load; if the key was regenerated in the UI, download the new .pem and update GITHUB_APP_PRIVATE_KEY_PATH
- 404: confirm the app is installed on the target org/repo and copy the installation ID from the app's install page URL (/installations/{ID})
- 403: check IP allow-list / SSO enforcement policies for the installation
- Search the snippet text in GitHub's REST docs for the exact condition (the message is GitHub's own documentation field)
Example fix
// before: AppID and key belong to different apps
// error: installation token request failed: 401 Unauthorized: {"message":"Bad credentials"...}
// after: align them
// GITHUB_APP_ID=123456 (from https://github.com/settings/apps/<app> -> About)
// GITHUB_APP_INSTALLATION_ID from https://github.com/settings/installations/<ID>
// GITHUB_APP_PRIVATE_KEY_PATH=./<app>.pem downloaded fresh from the same app page Defensive patterns
Strategy: try-catch
Validate before calling
// fail-fast credential probe: a 401 on this unauthenticated call proves reachability,
// while a minted-JWT 401 proves the key/AppID pair is wrong
func probeToken(cfg githubapp.Config) error {
p, err := githubapp.NewProvider(cfg, slog.Default())
if err != nil { return err }
if p.AccessToken() == "" { return errors.New("token mint failed; see logs") }
return nil
} Try / catch
// status-code dispatch on the wrapped message
msg := err.Error()
switch {
case strings.Contains(msg, "401"): // key/AppID mismatch — refresh the .pem
case strings.Contains(msg, "404"): // installation ID wrong or app uninstalled
case strings.Contains(msg, "403"): // policy: IP allow-list / SSO
} Prevention
- Keep AppID, installation ID, and .pem sourced from the same app page in one config unit
- When regenerating a key in GitHub settings, deploy the new .pem atomically with (or before) the config that references it
- Run a startup token probe so credential drift fails the deploy, not the first user request
When it happens
Trigger: 401: the JWT signature does not match AppID (key from a different app), the .pem was regenerated after download, or the AppID is wrong. 403: IP not allow-listed or SSO enforcement. 404: InstallationID does not exist for this app (wrong number or app not installed on the target org/repo). 422: malformed request path built from a broken InstallationID. All surface via the fmt.Errorf at internal/githubapp/githubapp.go:158.
Common situations: App owner regenerated the private key in GitHub settings but the server still loads the old .pem; AppID/App installation ID mixed up between apps; the app was uninstalled from the organization (404); GitHub Enterprise Server version predating the access_tokens endpoint; org SAML SSO session expired for the installation.
Related errors
- failed to get GitHub client: %w
- GitHub App authentication and OAuth login (--oauth-client-id
- GitHub App installation ID is required (GITHUB_APP_INSTALLAT
- GitHub App REST base URL is required
- owner is required
AI-assisted analysis of github/github-mcp-server@0ea1f775a7 (2026-08-15).
Data as JSON: /api/errors/edacbc26c258ee7d.
Report an issue: GitHub.