{"record":{"id":"7dd7ddd1434ffd89","repo":"siyuan-note/siyuan","slug":"stdout-pipe-w","errorCode":null,"errorMessage":"stdout pipe: %w","messagePattern":"stdout pipe: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/mcp/client/mcp.go","lineNumber":472,"sourceCode":"\t// stdio 环境变量插值不受密钥 AllowedHosts 约束：目标是本地子进程而非网络主机，管理员在 Env 中\n\t// 引用 {{secrets.NAME}} 本身就是对该服务器的显式授权，与直接写入明文属于同一信任级别。\n\tcmdEnv, err := buildStdioEnvironment(server, os.LookupEnv, func(value string) string {\n\t\tif model.Conf == nil {\n\t\t\treturn value\n\t\t}\n\t\treturn conf.ResolveSecretsVars(model.Conf.Secrets, model.Conf.Variables, value)\n\t}, runtime.GOOS)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"environment: %w\", err)\n\t}\n\tcmd.Env = cmdEnv\n\tstdin, err := cmd.StdinPipe()\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"stdin pipe: %w\", err)\n\t}\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"stdout pipe: %w\", err)\n\t}\n\tcmd.Stderr = io.Discard\n\n\tif err := cmd.Start(); err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"start command: %w\", err)\n\t}\n\n\tconnectCtx, connectCancel := context.WithTimeout(ctx, serverTimeout(server))\n\tdefer connectCancel()\n\ttransport := &mcp.IOTransport{Reader: stdout, Writer: stdin}\n\tsession, err := client.Connect(connectCtx, transport, nil)\n\tif err != nil {\n\t\tcmd.Process.Kill()\n\t\tcmd.Wait()\n\t\treturn nil, cmd, fmt.Errorf(\"connect: %w\", err)\n\t}\n\n\treturn session, cmd, nil","sourceCodeStart":454,"sourceCodeEnd":490,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/mcp/client/mcp.go#L454-L490","documentation":"connectStdio calls cmd.StdoutPipe() to obtain the read end of the child's stdout, which the MCP IOTransport reads JSON-RPC messages from. A failure creating the underlying pipe is wrapped with the \"stdout pipe:\" prefix. Like the stdin-pipe error, this indicates OS-level resource exhaustion rather than a configuration problem.","triggerScenarios":"connectStdio calls cmd.StdoutPipe() and the underlying os.Pipe syscall fails, typically EMFILE/ENFILE from hitting the file-descriptor limit.","commonSituations":"Too many concurrent MCP child processes each holding stdin/stdout pipes, leaked descriptors from earlier crashed connections, or a low hard fd limit in containers/CI sandboxes.","solutions":["Raise the process file-descriptor limit (ulimit -n, LimitNOFILE) and retry the connection","Inspect for descriptor leaks: list MCP child processes (ps/lsof) and terminate stale ones","Lower the number of concurrently configured stdio MCP servers","Restart the SiYuan kernel to reclaim leaked pipe descriptors"],"exampleFix":"// before: unbounded reconnect loop spawning pipes each retry\n// after: reuse or close previous pipes/session before reconnecting\nif prev != nil { prev.Close(); prev.Cmd.Process.Kill(); prev.Cmd.Wait() }","handlingStrategy":"retry","validationCode":"var r syscall.Rlimit\nsyscall.Getrlimit(syscall.RLIMIT_NOFILE, &r)\n// ensure r.Cur comfortably exceeds 3 fds per configured stdio server","typeGuard":"null","tryCatchPattern":"if err := connectStdio(ctx, client, server); err != nil {\n    if strings.Contains(err.Error(), \"stdout pipe\") {\n        // free descriptors (close stale sessions, kill children), then retry with backoff\n        return retryAfterCleanup\n    }\n    return err\n}","preventionTips":["Raise RLIMIT_NOFILE in systemd units, containers, and CI sandboxes","Audit for descriptor leaks after connection errors with lsof","Limit the number of concurrently connected stdio MCP servers"],"tags":["mcp","stdio","os-resources","file-descriptors"],"backgroundTag":"resource-exhaustion","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}