Make long-running tools fire-and-return to fix intermittent timeouts

redeploy_project, create_project and pull_image used to block the tool
call while polling DSM until the project reached RUNNING / the image
appeared - up to 300s and 240s respectively. On large images this
regularly ran past Claude Desktop's ~4 min tool-call ceiling (the live
MCP log shows redeploy_project calls up to 260s), which the client
reported as a timeout even though the server kept working. Whether a
call crossed the ceiling depended on image size and NAS load, which is
why the failure was intermittent.

These tools now fire-and-return: they trigger the operation, consume
build_stream only for a short early-error window (20s, to catch fast
daemon errors like "manifest unknown"), then return a "running in the
background - check get_project_status / check_image_updates" hint.
Completion is observed via the existing fast status tools.

- DsmClient.trigger_build_stream gains a `budget` parameter
- remove _wait_for_project_running and the _POLL_*/_BUILD_POLL_TIMEOUT
  constants (projects.py) and _PULL_POLL_* constants (registry.py)
- update tests, CLAUDE.md DSM quirks and CHANGELOG; bump 0.7.0 -> 0.8.0

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 05:56:42 +02:00
parent 7bb9b00dcc
commit 4a49407883
9 changed files with 208 additions and 302 deletions
+27 -17
View File
@@ -54,22 +54,30 @@ Only a second consecutive failure is treated as a real auth problem.
`container_name`. All container tools strip this prefix transparently via
`_strip_hash_prefix` / `_resolve_container_name`.
- **Async project start** — `SYNO.Docker.Project/start` returns immediately
while containers are still initialising. `redeploy_project` polls
`SYNO.Docker.Project/list` every 2 s for up to 30 s after issuing start.
while containers are still initialising. The long-running tools
(`redeploy_project`, `create_project`, `pull_image`) are **fire-and-return**:
they trigger the operation, surface any fast failure, then return so the
tool call never approaches the Claude Desktop ~4 min ceiling. Completion is
observed by the model polling `get_project_status` / `check_image_updates`,
not by the tool blocking. (Blocking until RUNNING was the root cause of the
intermittent "MCP not responding" timeouts — large-image pulls regularly
ran 4+ min; see CHANGELOG 0.8.0.)
- **`SYNO.Docker.Project/build_stream`** — returns a streamed plaintext
build log (content-type `text/html`), one short line per step:
`Container <name> Running` on success, `<svc> Error` followed by
`Error response from daemon: <cause>` on failure. The stream closes
when the build is done. `DsmClient.trigger_build_stream` consumes the
body line-by-line with a 210 s wall-clock budget (under the Claude
Desktop ~4 min ceiling) and returns the log as a string; on timeout
the partial log is returned with a marker appended so callers know
the build is still running server-side. `redeploy_project` and
`create_project` grep the returned log for daemon errors and abort
early — these errors are much more actionable than the eventual
`BUILD_FAILED` polling status. The log is **live-only**: it cannot
be re-fetched after the build ends, which is why no standalone
`get_project_build_log` tool exists.
when the build is done. `DsmClient.trigger_build_stream(project_id,
budget=…)` consumes the body line-by-line up to a configurable wall-clock
budget and returns the log so far; if the budget elapses before the stream
closes, the partial log is returned with `BUILD_STREAM_TIMEOUT_MARKER`
appended so callers know the build is still running server-side.
`redeploy_project` / `create_project` pass a **short** budget
(`_BUILD_EARLY_BUDGET`, 20 s) purely to catch fast daemon errors
(e.g. `manifest unknown`) — they grep the returned log and abort early on
those, otherwise return a "build running in the background, check
`get_project_status`" hint. They do **not** block until RUNNING. The log is
**live-only**: it cannot be re-fetched after the build ends, which is why no
standalone `get_project_build_log` tool exists.
- **Image delete** — requires a form-encoded POST with a JSON `images` array
(confirmed via browser DevTools); uses `DsmClient.post_request()`.
- **`SYNO.Docker.Image/pull` vs. `pull_start`** — the legacy `pull` method
@@ -80,11 +88,13 @@ Only a second consecutive failure is treated as a real auth problem.
`SYNO.Docker.Registry` — the Registry API only exposes the synchronous
read-only methods (`search`, `tags`, `get/set/create/delete`, `using`);
calling `Registry/pull_start` returns "Method does not exist". No
matching `pull_status` method is confirmed on either API, so completion
is detected by polling `SYNO.Docker.Image/list` until `repository:tag`
appears (210 s backoff, 240 s budget). Timeout returns a "still
running" hint instead of raising — DSM keeps pulling server-side
regardless of the HTTP response.
matching `pull_status` method is confirmed on either API. `pull_image` is
**fire-and-return**: it short-circuits if the tag is already local, calls
`pull_start`, then returns a "pull started in the background, verify with
`check_image_updates` / `list_images`" hint. DSM keeps pulling server-side
regardless of the HTTP response, so the tool does not block polling
`Image/list` for the tag to appear (that loop used to run the full 240 s on
large images and trip the Claude Desktop tool-call timeout).
- **`SYNO.Docker.Registry/tags`** — uses `repo` (JSON-encoded) as the
parameter name; the n4s4 reference's `name` does not work on this DSM
version. Returns the tag list as the envelope's `data` field directly,