charmbracelet/crush · error
failed to start OAuth callback listener: all candidate ports
Error message
failed to start OAuth callback listener: all candidate ports in use
What it means
NewHandler could not bind its local HTTP callback listener: every candidate port was already in use. The handler needs a localhost port to receive the OAuth redirect, so without one it cannot start the authorization flow.
Source
Thrown at internal/oauth/mcp/handler.go:149
// first free candidate, just long enough to learn which is open. Either
// way the chosen port is baked into the redirect URI below and pinned on
// the receiver, so bindLocked always rebinds the SAME port. The probe is
// not a reservation — another process can take the port before the first
// real login — but a busy port then fails loudly rather than silently
// binding a port the redirect URI does not point at.
port := callbackPort
if port == 0 {
lc := &net.ListenConfig{}
for _, p := range callbackPorts {
probe, err := lc.Listen(context.Background(), "tcp", fmt.Sprintf("localhost:%d", p))
if err == nil {
_ = probe.Close()
port = p
break
}
}
if port == 0 {
return nil, errors.New("failed to start OAuth callback listener: all candidate ports in use")
}
}
receiver.fixedPort = port
redirectURL := fmt.Sprintf("http://localhost:%d%s", port, callbackPath)
h := &Handler{
receiver: receiver,
serverURL: serverURL,
openURL: browser.OpenURL,
interactive: interactive,
onTokenRefresh: onTokenRefresh,
}
receiver.handler = h
// newTokenSource is the SDK hook invoked once after a successful code
// exchange. The token it hands us is brand new, so persist it right
// away, then wrap the source so later refreshes persist on change. TheView on GitHub (pinned to 7944b8e522)
Solutions
- Find and stop the process holding the port (lsof -i :<port> / ss -ltnp) or wait for it to exit.
- Remove or change any configured fixed callback port so the handler can pick a free port from the candidate range.
- Retry creating the handler after the conflicting process exits.
- Run only one crush instance that performs MCP OAuth at a time.
Example fix
// before
handler, err := oauth.NewHandler(cfg) // fails when fixed port is taken
// after
listener, err := net.Listen("tcp", "localhost:0")
if err != nil { return err }
listener.Close() // probe a free port first, or drop fixedPort from cfg
cfg.CallbackPort = 0
handler, err := oauth.NewHandler(cfg) Defensive patterns
Strategy: fallback
Validate before calling
ln, err := net.Listen("tcp", "localhost:0")
if err != nil { /* ports exhausted or networking broken */ }
ln.Close() Try / catch
handler, err := oauth.NewHandler(cfg)
if err != nil && strings.Contains(err.Error(), "ports in use") {
freeConflictingPort()
handler, err = oauth.NewHandler(cfg)
} Prevention
- Avoid configuring a fixed callback port shared by other services.
- Limit concurrent crush instances doing OAuth at the same time.
- Clean up stale listeners from crashed processes.
- Probe a free port before constructing the handler.
When it happens
Trigger: Creating a new MCP OAuth handler when all candidate callback ports (including the fixed port when configured) fail to bind — typically because another process holds them.
Common situations: Running multiple crush instances simultaneously, a stale crashed process still holding the port, or a configured fixed callback port occupied by another service.
Related errors
- interactive OAuth authorization required
- OAuth callback listener closed
- mcp '%s' does not use OAuth authentication
- mcp '%s' already has an authentication in progress
- oauth_client_id: %w
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/412559617052b51f.
Report an issue: GitHub.