charmbracelet/crush · error
panic: %v
Error message
panic: %v
What it means
The default branch of the same panic-recovery switch in MCP client init. When a goroutine panics with a non-string, non-error value (e.g. an int, a struct, or nil), recover() wraps it with fmt.Errorf("panic: %v", v). It prevents a whole-process crash during MCP server initialization.
Source
Thrown at internal/agent/tools/mcp/init.go:621
// (success or failure); Initialize uses it to await startup. The goroutine
// captures the server's generation at launch so a concurrent teardown
// invalidates its result rather than letting it register a stale session.
func goInitClient(ctx context.Context, cfg *config.ConfigStore, name string, m config.MCPConfig, wg *sync.WaitGroup) {
gen := currentGen(name)
go func() {
if wg != nil {
defer wg.Done()
}
defer func() {
if r := recover(); r != nil {
var err error
switch v := r.(type) {
case error:
err = v
case string:
err = fmt.Errorf("panic: %s", v)
default:
err = fmt.Errorf("panic: %v", v)
}
updateState(name, StateError, err, nil, Counts{})
slog.Error("Panic in MCP client initialization", "error", err, "name", name)
}
}()
start := time.Now()
err := initClient(ctx, cfg, name, m, gen, cfg.Resolver())
slog.Debug(
"MCP client initialization finished",
"name", name,
"duration", time.Since(start).Truncate(time.Millisecond).String(),
"error", err,
)
}()
}
// currentGen returns a server's current generation without bumping it.
func currentGen(name string) uint64 {View on GitHub (pinned to 7944b8e522)
Solutions
- Read the slog 'Panic in MCP client initialization' log entry to see the wrapped value
- Identify which MCP server config triggers the panic and test its endpoint/command standalone
- Update the MCP SDK to a version that returns errors instead of panicking
- Ensure custom transports/handlers never use panic for control flow
Example fix
// before
panic(42)
// after
return fmt.Errorf("unexpected state: %d", 42) Defensive patterns
Strategy: try-catch
Type guard
func asError(r any) error {
switch v := r.(type) {
case error: return v
case string: return fmt.Errorf("panic: %s", v)
default: return fmt.Errorf("panic: %v", v)
}
} Try / catch
// session-level: check state after init and retry with backoff
if sess.State == StateError {
err := renewSession(ctx, name)
if err != nil { return fmt.Errorf("mcp %s unavailable: %w", name, err) }
} Prevention
- Never panic with non-error values in library code; use error returns
- Enable Go 1.21+ so panic(nil) becomes a *runtime.PanicNilError
- Test custom MCP transports for panics with malformed input
- Keep the recover() deferred wrapper around all goroutine bodies
When it happens
Trigger: Any panic in the MCP init goroutine whose payload is not an error or string: panic(nil) (on older Go), panic(42), panic(someStruct), or library code panicking with arbitrary values.
Common situations: Vendor SDK code asserting on unexpected types; hand-written middleware calling panic(errSlice); Go <1.21 panic(nil) patterns.
Related errors
- panic: %s
- command execution panic: %v
- command execution panic: %v
- interactive OAuth authorization required
- failed to start OAuth callback listener: all candidate ports
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/57b18e817fdc14da.
Report an issue: GitHub.