github/github-mcp-server · warning
%w: %w
Error message
%w: %w
What it means
beginPKCE wraps any listenCallback failure as '%w: %w' with the sentinel errCallbackBind (declared at flow.go:19 as 'OAuth callback listener could not bind'). The sentinel lets begin() distinguish genuine bind failures from unrelated preparation errors (e.g. randomState failing): only errors.Is(err, errCallbackBind) escalate to the fatal fixed-port message (115); everything else falls through to device flow. So this message itself appears in logs ('PKCE flow unavailable, falling back to device flow') when the port was random.
Solutions
- If you saw the fatal variant (115), follow its guidance; if this appears as a device-flow fallback 'reason', a random bind failed — usually transient, retry
- Widen the ephemeral range / reduce TIME_WAIT pressure (net.ipv4.ip_local_port_range, tcp_tw_reuse) on socket-churn hosts
- Specify a fixed high port with --oauth-callback-port to avoid ephemeral-range contention (accepting the 115 escalation if it is taken)
- Simply retry the login — random-port collisions vanish on re-attempt
Defensive patterns
Strategy: fallback
Type guard
errors.Is(err, errCallbackBind)
Try / catch
// inside begin(): only bind failures on a fixed port escalate; everything else falls back
if m.config.CallbackPort != 0 && errors.Is(err, errCallbackBind) { return nil, fatal }
m.logger.Info("PKCE flow unavailable, falling back to device flow", "reason", err) Prevention
- On hosts with heavy socket churn, widen ip_local_port_range or set a fixed high callback port
- Retry login once on a random-port bind collision before investigating
When it happens
Trigger: listenCallback fails at internal/oauth/flow.go:82-85 — port occupied (with CallbackPort==0, the OS-chosen random port is exhausted on ephemeral-range-heavy systems), ephemeral range exhausted ('cannot assign requested address'), or bind permission denied. With a fixed port, this error is rewrapped into 115; with a random port (0) or non-Docker fallback paths, it is logged as the reason for device-flow fallback.
Common situations: Massively many TIME_WAIT/ephemeral sockets exhausting the local port range; container net.ipv4.ip_local_port_range too small; a random port that collided with a busy service; heavy test churn binding thousands of listeners.
Related errors
- starting callback listener on
- callback server
- OAuth callback port is not available; another process may…
- requesting device code
- App not connected
AI-assisted analysis of github/github-mcp-server@0ea1f775a7 (2026-08-15).
Data as JSON: /api/errors/1c78b8b80bb7828b.
Report an issue: GitHub.
Appendix: source
Thrown at internal/oauth/flow.go:84
}
// beginPKCE prepares the authorization-code + PKCE flow. It binds the callback
// server and selects the most secure available display channel: browser
// auto-open, then URL elicitation, then a tool-response message. On a headless
// host with a random callback port it diverts to device flow, whose redirect
// does not depend on reaching this machine's localhost.
func (m *Manager) beginPKCE(prompter Prompter) (*flowPlan, error) {
state, err := randomState()
if err != nil {
return nil, err
}
verifier := oauth2.GenerateVerifier()
// Bind to all interfaces only inside a container, where the published port
// is delivered via eth0 rather than loopback. Native runs stay on loopback.
listener, err := listenCallback(m.config.CallbackPort, m.inDocker())
if err != nil {
return nil, fmt.Errorf("%w: %w", errCallbackBind, err)
}
if m.inDocker() {
// Inside a container the callback binds all interfaces so the published
// port is reachable, which also exposes it to the container network.
// Publishing to loopback only (e.g. -p 127.0.0.1:%d:%d) keeps the
// authorization code off the network.
m.logger.Warn(fmt.Sprintf("OAuth callback is listening on all container interfaces; publish it to loopback only (e.g. -p 127.0.0.1:%d:%d) so the authorization code is not exposed on your network", m.config.CallbackPort, m.config.CallbackPort))
}
cs := newCallbackServer(listener, state)
oc := m.oauth2Config(cs.redirect)
authURL := oc.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier))
run := func(ctx context.Context) (*oauth2.Token, error) {
code, err := cs.wait(ctx)
if err != nil {
return nil, err
}View on GitHub (pinned to 0ea1f775a7)