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

  1. Read the slog 'Panic in MCP client initialization' log entry to see the wrapped value
  2. Identify which MCP server config triggers the panic and test its endpoint/command standalone
  3. Update the MCP SDK to a version that returns errors instead of panicking
  4. 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

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


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/57b18e817fdc14da. Report an issue: GitHub.