github/github-mcp-server · error

OAuth callback listener could not bind

Error message

OAuth callback listener could not bind

What it means

Third guard inside decodeBlameCursor: the decoded value has the "blame-range:" prefix but the remainder is not a valid non-negative integer (strconv.Atoi fails or the result is negative). The cursor decoded and matched the format, yet carries a corrupt or hand-mangled offset.

Source

Thrown at internal/oauth/flow.go:19

package oauth

import (
	"context"
	"errors"
	"fmt"
	"time"

	"golang.org/x/oauth2"
)

// deviceAuthTimeout bounds the synchronous device-code request made while
// preparing the device flow (before any waiting on the user).
const deviceAuthTimeout = 30 * time.Second

// errCallbackBind marks a failure to bind the local OAuth callback listener, so
// begin can treat a busy fixed port as fatal without mislabeling unrelated
// errors (e.g. a failure to generate the state parameter) as a port conflict.
var errCallbackBind = errors.New("OAuth callback listener could not bind")

// flowPlan is a prepared authorization flow ready to run in the background.
type flowPlan struct {
	// run performs the blocking part of the flow (await callback + exchange, or
	// poll the device endpoint) and returns the token.
	run func(context.Context) (*oauth2.Token, error)
	// display, if set, presents the prompt to the user via the Prompter and
	// blocks until they act. ErrPromptDeclined (the user said no) or any other
	// error aborts the flow, except ErrPromptUnavailable, which degrades to
	// fallback when that is set.
	display func(context.Context) error
	// fallback, if set alongside display, is the manual user action to surface
	// when the display prompt cannot be delivered (ErrPromptUnavailable). It lets
	// a runtime elicitation failure degrade to the manual channel — keeping the
	// background flow alive — instead of aborting.
	fallback *UserAction
	// userAction, if set, indicates the last-resort channel: the caller must
	// surface it and the user retries after authorizing out of band.

View on GitHub (pinned to 0ea1f775a7)

Solutions

  1. Prefer the server-issued nextCursor over any locally constructed cursor
  2. When building one, append exactly strconv.Itoa(n) with n >= 0 and no surrounding whitespace
  3. Re-request from after:"" if your offset state is suspect
  4. Validate client-side before sending: decode, strip prefix, Atoi, check n >= 0

Example fix

// before: negative and malformed offsets
base64.RawURLEncoding.EncodeToString([]byte("blame-range:-5"))

// after: non-negative decimal offset
base64.RawURLEncoding.EncodeToString([]byte("blame-range:100"))
Defensive patterns

Strategy: validation

Validate before calling

func validBlameCursorOffset(s string) bool {
	if s == "" {
		return true
	}
	b, err := base64.RawURLEncoding.DecodeString(s)
	if err != nil || !strings.HasPrefix(string(b), "blame-range:") {
		return false
	}
	n, err := strconv.Atoi(strings.TrimPrefix(string(b), "blame-range:"))
	return err == nil && n >= 0
}

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) {
	// offset payload is corrupt: drop it and page from the start
	args["after"] = ""
	res, err = callGetFileBlame(ctx, args)
}

Prevention

When it happens

Trigger: Suffixes like "blame-range:abc", "blame-range:-5" (negative offset), "blame-range:1e3" (Atoi rejects scientific notation), or "blame-range:10 " (trailing whitespace); also partial truncation of a valid cursor.

Common situations: Hand-assembling cursors by string concatenation; string slicing that clips digits off the end; whitespace introduced when copying values through terminals or config files.

Related errors


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