github/github-mcp-server · error

installation token response did not contain an expiry

Error message

installation token response did not contain an expiry

What it means

Second guard inside decodeBlameCursor: the value decoded successfully as base64url but does not start with the blameCursorPrefix constant "blame-range:". The server therefore recognizes the bytes as base64 but not as one of its own blame cursors. Cursors from other tools or versions use different internal formats and fail here.

Source

Thrown at internal/githubapp/githubapp.go:172

		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 bool
}

func NewProvider(cfg Config, logger *slog.Logger) (*Provider, error) {

View on GitHub (pinned to 0ea1f775a7)

Solutions

  1. Use only the pageInfo/nextCursor value returned by a prior get_file_blame call on the same server version
  2. Build cursors with the exact format: base64url("blame-range:"+offset)
  3. Reset pagination with after:"" when switching tools or after a server upgrade
  4. Keep per-tool cursor state instead of a shared cursor variable

Example fix

// before: GraphQL cursor from a different tool
{"after":"Y3Vyc29yOnYyOpHOAA=="}

// after: blame cursor from the previous get_file_blame page
{"after":"YmxhbWUtcmFuZ2U6MTAw"}
Defensive patterns

Strategy: validation

Validate before calling

func validBlameCursorPrefix(s string) bool {
	if s == "" {
		return true
	}
	b, err := base64.RawURLEncoding.DecodeString(s)
	if err != nil {
		return false
	}
	return strings.HasPrefix(string(b), "blame-range:")
}

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) {
	// wrong cursor source (e.g. a GraphQL cursor from another tool): reset
	args["after"] = ""
	res, err = callGetFileBlame(ctx, args)
}

Prevention

When it happens

Trigger: Reusing a GitHub GraphQL page cursor (base64 of "cursor:...") from list tools as get_file_blame's after; base64-encoding a bare integer without the prefix; cursors minted by a github-mcp-server version that used a different prefix.

Common situations: Generic pagination helpers that share one cursor variable across tools; copy-pasting cursors between sessions/tools; upgrading the server across a cursor format change while keeping stale cursor state.

Related errors


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