{"record":{"id":"57b18e817fdc14da","repo":"charmbracelet/crush","slug":"panic-v","errorCode":null,"errorMessage":"panic: %v","messagePattern":"panic: (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/agent/tools/mcp/init.go","lineNumber":621,"sourceCode":"// (success or failure); Initialize uses it to await startup. The goroutine\n// captures the server's generation at launch so a concurrent teardown\n// invalidates its result rather than letting it register a stale session.\nfunc goInitClient(ctx context.Context, cfg *config.ConfigStore, name string, m config.MCPConfig, wg *sync.WaitGroup) {\n\tgen := currentGen(name)\n\tgo func() {\n\t\tif wg != nil {\n\t\t\tdefer wg.Done()\n\t\t}\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tvar err error\n\t\t\t\tswitch v := r.(type) {\n\t\t\t\tcase error:\n\t\t\t\t\terr = v\n\t\t\t\tcase string:\n\t\t\t\t\terr = fmt.Errorf(\"panic: %s\", v)\n\t\t\t\tdefault:\n\t\t\t\t\terr = fmt.Errorf(\"panic: %v\", v)\n\t\t\t\t}\n\t\t\t\tupdateState(name, StateError, err, nil, Counts{})\n\t\t\t\tslog.Error(\"Panic in MCP client initialization\", \"error\", err, \"name\", name)\n\t\t\t}\n\t\t}()\n\t\tstart := time.Now()\n\t\terr := initClient(ctx, cfg, name, m, gen, cfg.Resolver())\n\t\tslog.Debug(\n\t\t\t\"MCP client initialization finished\",\n\t\t\t\"name\", name,\n\t\t\t\"duration\", time.Since(start).Truncate(time.Millisecond).String(),\n\t\t\t\"error\", err,\n\t\t)\n\t}()\n}\n\n// currentGen returns a server's current generation without bumping it.\nfunc currentGen(name string) uint64 {","sourceCodeStart":603,"sourceCodeEnd":639,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/agent/tools/mcp/init.go#L603-L639","documentation":"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.","triggerScenarios":"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.","commonSituations":"Vendor SDK code asserting on unexpected types; hand-written middleware calling panic(errSlice); Go <1.21 panic(nil) patterns.","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"],"exampleFix":"// before\npanic(42)\n// after\nreturn fmt.Errorf(\"unexpected state: %d\", 42)","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"func asError(r any) error {\n    switch v := r.(type) {\n    case error: return v\n    case string: return fmt.Errorf(\"panic: %s\", v)\n    default: return fmt.Errorf(\"panic: %v\", v)\n    }\n}","tryCatchPattern":"// session-level: check state after init and retry with backoff\nif sess.State == StateError {\n    err := renewSession(ctx, name)\n    if err != nil { return fmt.Errorf(\"mcp %s unavailable: %w\", name, err) }\n}","preventionTips":["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"],"tags":["panic","recovery","mcp"],"backgroundTag":"recovered-panic-in-goroutine","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}