46b36f6b08
A second, independent failure mode behind the intermittent "MCP not
responding" reports: every tool shares one HTTP client with generous
timeouts (FileStation download/upload used 60s) and no retry. When DSM
briefly stalled (FileStation busy, NAS under load) a single call blocked
for up to a minute, and follow-up calls - even read-only ones like
read_compose - stalled with it, so the whole MCP appeared dead. The path
to the NAS is direct LAN (dsm.gecheckt.de -> 192.168.0.2), so this is DSM
responsiveness, not a network/NAT-loopback issue.
Route every DSM round-trip through a new DsmClient._send that adds:
- a hard wall-clock ceiling (asyncio.wait_for, HARD_CALL_TIMEOUT=25s) so a
call returns even if DSM accepts a request but never responds;
- a single retry on a fresh connection for transient transport failures -
always for connection-establishment errors (request provably never
reached DSM, safe even for POST), and for GETs also on read/protocol
errors (also absorbs the keepalive race where DSM drops an idle conn);
- a clean SynologyError ("NAS did not respond, it may be busy") instead of
a raw transport traceback or a long silent hang.
Also tighten timeouts: default read 30s->15s, FileStation 60s->20s,
build_stream per-read 60s->20s. build_stream stays out of _send (it
streams and manages its own budget).
- add 6 tests for retry/no-retry/ceiling behaviour (320 pass)
- update CLAUDE.md and CHANGELOG; bump 0.8.0 -> 0.9.0
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>