charmbracelet/crush · error

panic: %s

Error message

panic: %s

What it means

This is the panic-recovery path in MCP client initialization. When a goroutine initializing an MCP client panics with a string value, the recover() converts it into an error via fmt.Errorf("panic: %s", v). The MCP server session is then marked StateError and the panic is logged instead of crashing the whole application.

Source

Thrown at internal/agent/tools/mcp/init.go:619

// Shared by Initialize and Reinitialize so the panic-to-state policy
// lives in one place. wg, if non-nil, is Done when the attempt finishes
// (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,
		)
	}()
}

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Check the slog output for 'Panic in MCP client initialization' to get the panic string and stack context
  2. Reproduce with the specific MCP server config to identify which server's init panics
  3. Upgrade the MCP SDK / transport library, the panic is usually inside library code
  4. Validate the server config (command, args, env, url) so the library never receives invalid input

Example fix

// before: init goroutine panics with a string and crashes
panic("unexpected nil transport")

// after: this recover already exists; ensure config is validated before starting
if strings.TrimSpace(cfg.Command) == "" { return errors.New("empty command") }
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

// the library already recovers; callers should treat StateError sessions as dead
sess, ok := sessions.Get(name)
if !ok || sess.State == StateError {
    slog.Warn("MCP session unavailable after panic", "name", name)
}

Prevention

When it happens

Trigger: Any panic in the MCP init goroutine where the panic value is a plain string, e.g. panic("conn closed") or a library calling panic with a string. The deferred recover at the top of the init function catches it and formats this message.

Common situations: Third-party MCP SDK code panicking on malformed server responses; nil-map or nil-pointer bugs inside custom transports; stdio subprocess dying in a way that makes the client library panic with a string.

Related errors


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