Skip to content

Server has no process-wide SIGPIPE handling — a broken-pipe stderr/stdout SIGKILLs it (27 write sites; #303's staleness warning is the earliest trigger) #320

Description

@kiki830621

Problem

The server installs no SIGPIPE handler. Any write to a broken pipe (write end open, read end gone) is killed by the kernel's default SIGPIPE disposition before write() returns to Swift — so no try? / do-catch can intercept it. There are 27 FileHandle.standardError.write sites across Sources/ (Server.swift, MailController.swift, ExportEmailsMarkdown.swift) and grep finds no signal() / SIG_IGN. Every one of them is a potential SIGPIPE kill if the host closes the read end of the server's stderr (or stdout) while the server keeps running.

Verified on the shipped binary (not a proxy)

Launched the real CheAppleMailMCP with fd 2 = the write end of a reader-closed pipe:

Scenario Result
version drift present (#303 emitDiagnostic fires at startup) server killed by signal 13 (SIGPIPE)
no drift / no sidecar (emitDiagnostic silent) server survives
standalone probe, default disposition exit 141 (128+SIGPIPE)
standalone probe, signal(SIGPIPE, SIG_IGN) first exit 0, survives

So #303's staleness warning is not a new crash class — it is the earliest stderr write on the startup path, which makes an already-fatal fd-2 state fatal at startup (on the drift path) rather than at the first log write during tool use. The other 26 writers would kill the same fd-2 state later regardless.

Surfaced by the round-5 verify of PR #307; the CHANGELOG + emitDiagnostic doc comment there now state this boundary precisely rather than claiming blanket "crash-proof".

Reachability

Narrow for a normally-launched stdio MCP server: the parent (Claude Desktop / launcher) normally holds the read end of the server's stderr for logging, so fd 2 does not become a broken pipe on its own. It requires the host to close its stderr reader while keeping the server alive. But it is a real state, and the failure is a hard SIGKILL, not a graceful degrade.

The fix is not a one-liner — hence a separate issue

The obvious fix is a process-wide signal(SIGPIPE, SIG_IGN) at startup, which converts the kill into an EPIPE errno the throwing write already swallows — fixing all 27 sites at once. But SIG_IGN without corresponding EPIPE handling on the stdout path can turn a clean "client disconnected → exit" into a spin: today a stdout write after the client closes gets SIGPIPE and the process dies (crude but terminal); with SIG_IGN it returns EPIPE, and the server must then detect that (or stdin-EOF) and exit cleanly. So this needs the MCP stdio server's shutdown path validated before flipping the disposition — deliberately out of #303's scope.

Suggested approach

  1. Confirm the server exits cleanly on stdin EOF (the normal client-disconnect signal), independent of stdout SIGPIPE.
  2. Add signal(SIGPIPE, SIG_IGN) once at process startup.
  3. Verify a broken-pipe stdout no longer spins (relies on step 1).
  4. A regression test spawning the real binary with a broken-pipe fd 2 (the harness above) asserting it survives.

Related

Current Status

  • Phase: diagnosed
  • Updated: 2026-08-01 (idd-all batch)
  • Diagnosis: see the ## Diagnosis comment below (root cause, strategy checklist, conflict class, complexity)

Metadata

Metadata

Assignees

No one assigned

    Labels

    diagnosedIDD lifecycle: diagnosis posted, awaiting plan/implementenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions