Summary
A profile that raises process.startupTimeoutMs still inherits the 5-second defaultShutdownTimeoutMs. For a slow stdio upstream this asymmetry is a trap: the same child process is given a large budget to start and a very small one to exit. When it misses the shutdown budget, the next start races the dying process and the operator is shown a discovery error that names neither the shutdown nor the timeout that caused it.
Evidence
A wrapper whose upstream is npx firebase-tools@15.26.0 mcp (40–60s cold start, so startupTimeoutMs: 90000, no shutdownTimeoutMs) produced this within one second, on a simultaneous multi-client restart:
03:42:23.740 tools/list success
03:42:25.121 upstream/shutdown UPSTREAM_SHUTDOWN_TIMEOUT failure
03:42:25.129 tools/list UPSTREAM_DISCOVERY_FAILED failure
03:42:44.890 upstream/start success
03:42:52.605 tools/list success
The shutdown timeout fires ~4ms before the discovery failure, and the profile recovers unaided ~27s later.
What the client surfaces is only:
UPSTREAM_DISCOVERY_FAILED: no healthy upstream completed tools discovery for profile 'X':
upstream 'default' (UPSTREAM_TOOL_LIST_FAILED: UPSTREAM_TOOL_LIST_FAILED: unable to list tools for 'X')
Nothing points at shutdown, at a timeout, or at the previous process. UPSTREAM_TOOL_LIST_FAILED is also doubled in the rendered message.
This is not rare. In one operator's audit journal the pattern appears on 26 separate days across ~7 weeks, 187 occurrences, peaking at 19 shutdown timeouts plus 11 discovery failures in a single day.
Contributing factor: each MCP client spawns its own wrapper per service, so several clients restarting together multiply the number of shutdown/start races.
Root cause
src/upstream/upstream-process-manager.ts:24
const defaultShutdownTimeoutMs = 5_000;
startupTimeoutMs and shutdownTimeoutMs are independent optional settings, so raising one leaves the other at 5s. Every one of the ten configurations reviewed set a startup timeout and none set a shutdown timeout.
Suggested directions
- Derive a shutdown floor from the configured startup timeout, or warn at validation time when
startupTimeoutMs greatly exceeds the effective shutdown timeout, so the asymmetry cannot be created silently.
- Have
doctor surface the mismatch as a check.
- Include the originating condition in the discovery error when a start races a shutdown that timed out, rather than reporting only that discovery failed, and de-duplicate the repeated
UPSTREAM_TOOL_LIST_FAILED prefix.
Workaround
Set process.shutdownTimeoutMs explicitly on slow stdio upstreams (20000 for the npx firebase-tools case, 10000 for ordinary stdio children). Verified: doctor reports healthy with DOCTOR_CLEAN_SHUTDOWN passing.
Summary
A profile that raises
process.startupTimeoutMsstill inherits the 5-seconddefaultShutdownTimeoutMs. For a slow stdio upstream this asymmetry is a trap: the same child process is given a large budget to start and a very small one to exit. When it misses the shutdown budget, the next start races the dying process and the operator is shown a discovery error that names neither the shutdown nor the timeout that caused it.Evidence
A wrapper whose upstream is
npx firebase-tools@15.26.0 mcp(40–60s cold start, sostartupTimeoutMs: 90000, noshutdownTimeoutMs) produced this within one second, on a simultaneous multi-client restart:The shutdown timeout fires ~4ms before the discovery failure, and the profile recovers unaided ~27s later.
What the client surfaces is only:
Nothing points at shutdown, at a timeout, or at the previous process.
UPSTREAM_TOOL_LIST_FAILEDis also doubled in the rendered message.This is not rare. In one operator's audit journal the pattern appears on 26 separate days across ~7 weeks, 187 occurrences, peaking at 19 shutdown timeouts plus 11 discovery failures in a single day.
Contributing factor: each MCP client spawns its own wrapper per service, so several clients restarting together multiply the number of shutdown/start races.
Root cause
src/upstream/upstream-process-manager.ts:24startupTimeoutMsandshutdownTimeoutMsare independent optional settings, so raising one leaves the other at 5s. Every one of the ten configurations reviewed set a startup timeout and none set a shutdown timeout.Suggested directions
startupTimeoutMsgreatly exceeds the effective shutdown timeout, so the asymmetry cannot be created silently.doctorsurface the mismatch as a check.UPSTREAM_TOOL_LIST_FAILEDprefix.Workaround
Set
process.shutdownTimeoutMsexplicitly on slow stdio upstreams (20000 for thenpx firebase-toolscase, 10000 for ordinary stdio children). Verified:doctorreports healthy withDOCTOR_CLEAN_SHUTDOWNpassing.