charmbracelet/crush · error

timed out after %s

Error message

timed out after %s

What it means

maybeTimeoutErr converts a context.Canceled error into a 'timed out after <duration>' error in the MCP layer. Since a canceled context during MCP calls is treated as the operation timeout, this rewrites the generic cancel into a clearer timeout message carrying the configured timeout.

Source

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

	// channel gate today); the stdio transport we're probing for is the
	// innermost one. Unwrap all of them — without this the assertion below
	// never matches and stdio startup failures report a bare EOF instead of
	// the child's actual output. Every wrapper must implement
	// unwrapTransport or it will hide this diagnostic again.
	transport = unwrapTransport(transport)
	ct, ok := transport.(*mcp.CommandTransport)
	if !ok {
		return err
	}
	if err2 := stdioCheck(ct.Command); err2 != nil {
		err = errors.Join(err, err2)
	}
	return err
}

func maybeTimeoutErr(err error, timeout time.Duration) error {
	if errors.Is(err, context.Canceled) {
		return fmt.Errorf("timed out after %s", timeout)
	}
	return err
}

func createTransport(ctx context.Context, cfg *config.ConfigStore, name string, m config.MCPConfig, resolver config.VariableResolver) (mcp.Transport, *mcpoauth.Handler, error) {
	switch m.Type {
	case config.MCPStdio:
		command, err := resolver.ResolveValue(m.Command)
		if err != nil {
			return nil, nil, fmt.Errorf("invalid mcp command: %w", err)
		}
		if strings.TrimSpace(command) == "" {
			return nil, nil, fmt.Errorf("mcp stdio config requires a non-empty 'command' field")
		}
		args, err := m.ResolvedArgs(resolver)
		if err != nil {
			return nil, nil, err
		}

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Increase the MCP timeout setting in config for that server
  2. Check the MCP server is actually responsive (run its command manually or curl the URL)
  3. Distinguish user-cancellation from timeout in calling code with your own cancellation channel
  4. Reduce server startup cost so pings complete within the timeout

Example fix

// before: 5s timeout kills slow servers
timeout: 5s
// after
timeout: 30s
Defensive patterns

Strategy: retry

Validate before calling

// pre-check server responsiveness before the timed call
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if err := pingSession(ctx, sess, timeout); err != nil {
    return fmt.Errorf("mcp server unreachable within %s: %w", timeout, err)
}

Try / catch

if errors.Is(err, context.Canceled) || strings.Contains(err.Error(), "timed out after") {
    // increase timeout or retry with backoff
}

Prevention

When it happens

Trigger: An MCP ping/call exceeding the configured timeout, causing the context to be canceled, and then being passed through maybeTimeoutErr. Also occurs if a caller cancels the context externally (it will be misreported as a timeout).

Common situations: Slow or hung MCP server (stdio process blocked, HTTP server unresponsive); timeout configured too aggressively for a cold-starting server; network latency to remote MCP endpoints.

Understand the failure class

Related errors


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