github/github-mcp-server · error
installation token response did not contain a token
Error message
installation token response did not contain a token
What it means
decodeBlameCursor parses the 'after' pagination parameter of get_file_blame. Cursors are opaque values minted by encodeBlameCursor: base64.RawURLEncoding of "blame-range:"+integer offset. This variant fires at the first guard when base64.RawURLEncoding.DecodeString rejects the input, meaning it is not unpadded URL-safe base64 - it contains '+', '/', '=' padding, or non-alphabet characters.
Source
Thrown at internal/githubapp/githubapp.go:169
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",
Expiry: body.ExpiresAt.Add(-refreshBuffer),
}, nil
}
// Provider caches and refreshes GitHub App installation access tokens.
type Provider struct {
source oauth2.TokenSource
logger *slog.Logger
mu sync.Mutex
errLogged boolView on GitHub (pinned to 0ea1f775a7)
Solutions
- Echo the cursor exactly as returned in the previous get_file_blame response's pageInfo - never re-encode it
- If constructing one yourself: base64.RawURLEncoding.EncodeToString([]byte("blame-range:100"))
- Send after as the plain JSON string value with no URL encoding or trimming
- If pagination state is corrupted, restart from after:"" and re-page
Example fix
// before: standard base64 with '=' padding is rejected by RawURLEncoding
{"owner":"octocat","repo":"Hello-World","path":"README.md","after":"YmxhbWUtcmFuZ2U6MTAw="}
// after: unpadded base64url, exactly as returned by the previous response
{"owner":"octocat","repo":"Hello-World","path":"README.md","after":"YmxhbWUtcmFuZ2U6MTAw"} Defensive patterns
Strategy: validation
Validate before calling
// Validate the 'after' cursor client-side before calling get_file_blame.
func validBlameCursorEncoding(s string) bool {
if s == "" {
return true // first page
}
if _, err := base64.RawURLEncoding.DecodeString(s); err != nil {
return false // not unpadded URL-safe base64
}
return true
} Type guard
func isInvalidCursorError(err error) bool {
return err != nil && strings.Contains(err.Error(), "after cursor is invalid")
} Try / catch
res, err := callGetFileBlame(ctx, args)
if isInvalidCursorError(err) {
// cursor state is corrupt: restart pagination from the first page
args["after"] = ""
res, err = callGetFileBlame(ctx, args)
} Prevention
- Treat cursors as opaque: store and echo the returned value verbatim
- Never re-encode cursors with standard base64 (padding/+// break Raw URL encoding)
- Pass after as a plain JSON string without URL encoding
- Use after:"" for the first page instead of a fabricated value
When it happens
Trigger: Passing a standard-alphabet base64 cursor (with '+', '/', or trailing '='), a truncated or whitespace-padded cursor, a cursor that survived URL-encoding as literal '%xx', or arbitrary free text in the after field.
Common situations: Client re-encodes the returned cursor with std base64 instead of passing it through; JSON layers that escape or trim the value; hand-building cursors instead of echoing the previous page's nextCursor.
Related errors
- installation token response did not contain an expiry
- OAuth callback listener could not bind
- perPage value %d exceeds maximum of 100
- perPage value %d cannot be negative
- authorization prompt has expired
AI-assisted analysis of github/github-mcp-server@0ea1f775a7 (2026-08-15).
Data as JSON: /api/errors/609170d3b7494a58.
Report an issue: GitHub.