Summary
mecatui/mecated startup (app.Build) blocks synchronously on connecting
every MCP server before the embedded server (and therefore the TUI) becomes
usable. If any single MCP server is unreachable — a network blip, a wrong URL,
a crashed/corrupted local ToolHive workload — startup stalls for the full,
hardcoded defaultConnectTimeout (30s), even when every other server connects
in milliseconds. There is no way to configure this timeout down, no
degrade-and-continue path, and no visible progress during the wait, so from
the outside a single dead MCP server is indistinguishable from a hung/broken
build.
Reproduction (what we hit)
Root-caused via a private --diagnostics-log capture. Timestamped evidence:
time=2026-09-18T10:41:59.302+02:00 ← last line before the gap
time=2026-09-18T10:42:29.319+02:00 level=WARN msg="MCP server unreachable; skipping" name=github reason=unavailable
time=2026-09-18T10:42:29.320+02:00 level=WARN msg="MCP server unreachable; skipping" name=toolhive-doc-mcp-remote reason=unavailable
time=2026-09-18T10:42:29.320+02:00 level=INFO msg="MCP servers connected" servers=6 tools=39
That's a 30.017s gap, matching defaultConnectTimeout = 30 * time.Second in
internal/adapter/mcp/mcp.go:57 to the millisecond. Everything else in the
build (session store, permission config, agent defs, LLM provider, skills,
memory, Team/Parallel/Schedule tools, scheduler) completed in under 300ms
total — this one stalled connection is the entire startup delay.
In our case the underlying cause was a corrupted Docker overlay2 layer for
the ToolHive-managed github MCP server container (thv status github /
docker rm both failed with input/output error / read-only file system
against the Docker Desktop VM disk) — but the mecatl-side behavior is the
real issue: this class of failure (one unreachable MCP server) will always
cost every user a 30s freeze on mecatui/mecated startup, for any reason a
server can be down.
Why this matters
internal/adapter/mcp/mcp.go NewManager connects servers concurrently
under a bounded semaphore, but Build() still calls wg.Wait() and does
not return until every server has connected or timed out
(internal/adapter/mcp/mcp.go ~L1258). One dead server holds back the
other N that connected in milliseconds.
defaultConnectTimeout (30s) is a hardcoded constant with no CLI flag or
settings.yaml knob — not even for local/loopback ToolHive servers where
30s is enormous relative to a real connect.
- There's no degrade-and-continue path: MCP tool mounting happens once during
buildCatalog (Phase A of catalog construction), so there's no way to
bring up the core tool set immediately and attach straggler MCP servers'
tools later if/when they connect.
- Nothing is emitted to the user during the wait to explain why startup is
frozen — from the outside, a hung MCP server and a genuinely broken build
look identical, which is what made this hard to diagnose in the first
place.
Suggested direction (not prescriptive — open to design review)
- Make the per-server (and/or overall) MCP connect timeout configurable
(CLI flag + settings.yaml), so an operator with known-flaky or
loopback-only servers can tighten it.
- Consider connecting MCP servers in the background after the embedded
server/TUI is otherwise ready, surfacing tools as each server comes
online instead of gating the whole Build() on the slowest one. This is
a bigger architectural change (catalog assembly currently assumes a
build-once snapshot) and would need its own design pass.
- At minimum, emit a visible "connecting to MCP server(s): X, Y, Z..."
diagnostic/status line the moment connects start, so a long wait is
attributable in real time instead of only after the fact via diag.
🤖 Generated with Claude Code
Summary
mecatui/mecatedstartup (app.Build) blocks synchronously on connectingevery MCP server before the embedded server (and therefore the TUI) becomes
usable. If any single MCP server is unreachable — a network blip, a wrong URL,
a crashed/corrupted local ToolHive workload — startup stalls for the full,
hardcoded
defaultConnectTimeout(30s), even when every other server connectsin milliseconds. There is no way to configure this timeout down, no
degrade-and-continue path, and no visible progress during the wait, so from
the outside a single dead MCP server is indistinguishable from a hung/broken
build.
Reproduction (what we hit)
Root-caused via a private
--diagnostics-logcapture. Timestamped evidence:That's a 30.017s gap, matching
defaultConnectTimeout = 30 * time.Secondininternal/adapter/mcp/mcp.go:57to the millisecond. Everything else in thebuild (session store, permission config, agent defs, LLM provider, skills,
memory, Team/Parallel/Schedule tools, scheduler) completed in under 300ms
total — this one stalled connection is the entire startup delay.
In our case the underlying cause was a corrupted Docker overlay2 layer for
the ToolHive-managed
githubMCP server container (thv status github/docker rmboth failed withinput/output error/read-only file systemagainst the Docker Desktop VM disk) — but the mecatl-side behavior is the
real issue: this class of failure (one unreachable MCP server) will always
cost every user a 30s freeze on
mecatui/mecatedstartup, for any reason aserver can be down.
Why this matters
internal/adapter/mcp/mcp.goNewManagerconnects servers concurrentlyunder a bounded semaphore, but
Build()still callswg.Wait()and doesnot return until every server has connected or timed out
(
internal/adapter/mcp/mcp.go~L1258). One dead server holds back theother N that connected in milliseconds.
defaultConnectTimeout(30s) is a hardcoded constant with no CLI flag orsettings.yamlknob — not even for local/loopback ToolHive servers where30s is enormous relative to a real connect.
buildCatalog(Phase A of catalog construction), so there's no way tobring up the core tool set immediately and attach straggler MCP servers'
tools later if/when they connect.
frozen — from the outside, a hung MCP server and a genuinely broken build
look identical, which is what made this hard to diagnose in the first
place.
Suggested direction (not prescriptive — open to design review)
(CLI flag +
settings.yaml), so an operator with known-flaky orloopback-only servers can tighten it.
server/TUI is otherwise ready, surfacing tools as each server comes
online instead of gating the whole
Build()on the slowest one. This isa bigger architectural change (catalog assembly currently assumes a
build-once snapshot) and would need its own design pass.
diagnostic/status line the moment connects start, so a long wait is
attributable in real time instead of only after the fact via
diag.🤖 Generated with Claude Code