siyuan-note/siyuan · critical
stdin pipe: %w
Error message
stdin pipe: %w
What it means
Returned when exec.Cmd.StdinPipe() fails while wiring up the IOTransport for a stdio MCP server. StdinPipe allocates an OS pipe; failure means the kernel cannot create a pipe handle, not that the child process is misconfigured. The wrapped OS error (typically EMFILE/ENFILE on Unix or a handle-allocation failure on Windows) is preserved.
Source
Thrown at kernel/mcp/client/mcp.go:466
func connectStdio(ctx context.Context, client *mcp.Client, server conf.MCPServer) (*mcp.ClientSession, *exec.Cmd, error) {
if server.Command == "" {
return nil, nil, fmt.Errorf("command is required for stdio server")
}
cmd := exec.Command(server.Command, server.Args...)
cmdEnv, err := buildStdioEnvironment(server, os.LookupEnv, func(value string) string {
if model.Conf == nil {
return value
}
return conf.ResolveSecretsVars(model.Conf.Secrets, model.Conf.Variables, value)
}, runtime.GOOS)
if err != nil {
return nil, nil, fmt.Errorf("environment: %w", err)
}
cmd.Env = cmdEnv
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, nil, fmt.Errorf("stdin pipe: %w", err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, nil, fmt.Errorf("stdout pipe: %w", err)
}
cmd.Stderr = io.Discard
if err := cmd.Start(); err != nil {
return nil, nil, fmt.Errorf("start command: %w", err)
}
connectCtx, connectCancel := context.WithTimeout(ctx, serverTimeout(server))
defer connectCancel()
transport := &mcp.IOTransport{Reader: stdout, Writer: stdin}
session, err := client.Connect(connectCtx, transport, nil)
if err != nil {
cmd.Process.Kill()
cmd.Wait()View on GitHub (pinned to 251596fc0d)
Solutions
- Check the wrapped error text for EMFILE/ENFILE or 'too many open files' and raise the kernel process's file-descriptor limit (ulimit -n, systemd LimitNOFILE=, Docker --ulimit nofile=).
- Reduce the number of concurrently connected stdio MCP servers, or disable servers you are not actively using.
- Run the kernel under a leak detector / lsof to confirm whether descriptors are being held after MCP servers disconnect; if so, that is a kernel bug worth reporting.
- Retry connecting after freeing descriptors; the failure is environmental, not a permanent config defect.
Example fix
# before: low fd limit on Linux ulimit -n 1024 # after ulimit -n 65536
Defensive patterns
Strategy: retry
Validate before calling
// Check available fd budget is reasonable before spawning a stdio MCP server.
import "syscall"
func fdBudget() int {
var r syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &r); err != nil { return -1 }
return int(r.Cur)
} Try / catch
// Distinguish EMFILE/ENFILE from other errors; only retry resource errors after backoff.
if errors.Is(err, syscall.EMFILE) || errors.Is(err, syscall.ENFILE) {
// backoff and retry connectStdio once
} Prevention
- Set a generous RLIMIT_NOFILE for the kernel process.
- Limit the number of concurrently connected stdio MCP servers.
- Monitor fd usage (lsof -p <pid>) to detect leaks early.
When it happens
Trigger: connectStdio calls cmd.StdinPipe() and the OS refuses to allocate a new pipe. Common when the SiYuan process has hit its file-descriptor limit, when too many MCP child processes are already spawned, or when per-user process/handle quotas are exhausted.
Common situations: Many stdio MCP servers configured and started in parallel; a long-running kernel that has leaked file descriptors; containerized deployment with a low RLIMIT_NOFILE (e.g. systemd DefaultLimitNOFILE) or a low --ulimit on Docker.
Related errors
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/f8f8960c1a6d1021.
Report an issue: GitHub.