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
- Increase the MCP timeout setting in config for that server
- Check the MCP server is actually responsive (run its command manually or curl the URL)
- Distinguish user-cancellation from timeout in calling code with your own cancellation channel
- 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
- Set realistic timeouts accounting for cold-start of stdio servers
- Health-check slow MCP endpoints before relying on them
- Do not cancel contexts you want reported as cancellations
- Alert on repeated timeout errors for a given server
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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- session ID is required for creating a new file
- session ID is required for reading MCP resources
- failed to wait for MCP initialization: %w
- failed to authenticate MCP: %w
- failed to refresh MCP prompts: %w
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/8a75e9e38762ce8d.
Report an issue: GitHub.