{"record":{"id":"8a75e9e38762ce8d","repo":"charmbracelet/crush","slug":"timed-out-after-s","errorCode":null,"errorMessage":"timed out after %s","messagePattern":"timed out after (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/agent/tools/mcp/init.go","lineNumber":1030,"sourceCode":"\t// channel gate today); the stdio transport we're probing for is the\n\t// innermost one. Unwrap all of them — without this the assertion below\n\t// never matches and stdio startup failures report a bare EOF instead of\n\t// the child's actual output. Every wrapper must implement\n\t// unwrapTransport or it will hide this diagnostic again.\n\ttransport = unwrapTransport(transport)\n\tct, ok := transport.(*mcp.CommandTransport)\n\tif !ok {\n\t\treturn err\n\t}\n\tif err2 := stdioCheck(ct.Command); err2 != nil {\n\t\terr = errors.Join(err, err2)\n\t}\n\treturn err\n}\n\nfunc maybeTimeoutErr(err error, timeout time.Duration) error {\n\tif errors.Is(err, context.Canceled) {\n\t\treturn fmt.Errorf(\"timed out after %s\", timeout)\n\t}\n\treturn err\n}\n\nfunc createTransport(ctx context.Context, cfg *config.ConfigStore, name string, m config.MCPConfig, resolver config.VariableResolver) (mcp.Transport, *mcpoauth.Handler, error) {\n\tswitch m.Type {\n\tcase config.MCPStdio:\n\t\tcommand, err := resolver.ResolveValue(m.Command)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"invalid mcp command: %w\", err)\n\t\t}\n\t\tif strings.TrimSpace(command) == \"\" {\n\t\t\treturn nil, nil, fmt.Errorf(\"mcp stdio config requires a non-empty 'command' field\")\n\t\t}\n\t\targs, err := m.ResolvedArgs(resolver)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}","sourceCodeStart":1012,"sourceCodeEnd":1048,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/agent/tools/mcp/init.go#L1012-L1048","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before: 5s timeout kills slow servers\ntimeout: 5s\n// after\ntimeout: 30s","handlingStrategy":"retry","validationCode":"// pre-check server responsiveness before the timed call\nctx, cancel := context.WithTimeout(context.Background(), timeout)\ndefer cancel()\nif err := pingSession(ctx, sess, timeout); err != nil {\n    return fmt.Errorf(\"mcp server unreachable within %s: %w\", timeout, err)\n}","typeGuard":null,"tryCatchPattern":"if errors.Is(err, context.Canceled) || strings.Contains(err.Error(), \"timed out after\") {\n    // increase timeout or retry with backoff\n}","preventionTips":["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"],"tags":["timeout","context","mcp"],"backgroundTag":"operation-timed-out","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}