Files
mcp-synology-container/CLAUDE.md
T
marcus 4a49407883 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>
2026-06-16 05:56:42 +02:00

8.1 KiB

mcp-synology-container

Project

mcp-synology-container is an MCP server for managing Docker projects on a Synology DiskStation via Container Manager. It exposes tools for projects, containers, images, compose files, networks, and system housekeeping.


Tech stack

Language Python 3.12+, uv
Key deps MCP SDK, httpx, keyring, click, rich
Compose paths /volume1/docker/<project>/ (default Synology layout)

Deploy workflow (after every code change)

1. Claude Code commits and pushes
2. uv tool install --reinstall git+<repo-url>
3. Restart Claude Desktop (tray icon → Quit → relaunch)

Push retry: the Gitea remote (gitea.gecheckt.de) occasionally returns Unauthorized on the first push attempt. If git push fails with an auth error, wait 1 s and retry once before reporting back. Only a second consecutive failure is treated as a real auth problem.


Implemented tools (35)

Category Tools
Projects list_projects, get_project_status, start_project, stop_project, redeploy_project, create_project, delete_project
Containers list_containers, get_container_status, get_container_logs, exec_in_container, container_stats, inspect_container, delete_container, start_container, stop_container, restart_container
Compose read_compose, update_compose, update_image_tag, update_env_var
Images check_image_updates, list_images, delete_image, inspect_image
Registry search_registry, list_image_tags, pull_image
Networks list_networks, create_network, delete_network
System system_df, system_prune, system_overview

DSM API quirks

  • Hash-prefixed container names — DSM sometimes returns names like a1b2c3d4e5f6_myservice when the compose service name differs from container_name. All container tools strip this prefix transparently via _strip_hash_prefix / _resolve_container_name.
  • Async project startSYNO.Docker.Project/start returns immediately 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(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 exists but behaviour varies by DSM version; not exposed as a standalone tool. pull_image uses SYNO.Docker.Image/pull_start (asynchronous pull entry point) with both repository and tag JSON-encoded. Note that pull_start lives on SYNO.Docker.Image, NOT on 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. 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, not wrapped in a sub-key.
  • SYNO.Docker.Volume — endpoint does not exist; volume management is not available via the DSM WebAPI.
  • SYNO.Docker.Registry/get — does not behave as documented; registry listing omitted.
  • SYNO.Docker.Container/pause and /unpause — not implemented in DSM Container Manager on this firmware. The action menu only offers start/stop/force-stop/restart/reset; calls to pause/unpause return "Method does not exist". pause_container and unpause_container were briefly shipped in 0.4.0 and removed in 0.4.1.
  • SYNO.Docker.Container/get response — profile.volume_bindings[].host_volume_file is share-relative, not the full host path. Live capture against a container with bind mount /volume1/docker/homeassistant:/config returned host_volume_file = "/docker/homeassistant" (21 chars, share-relative) in profile, while details.Mounts[].Source carried the full /volume1/docker/homeassistant and details.HostConfig.Binds the full /volume1/docker/homeassistant:/config:rw. For Compose-rebuild use cases the full path is required — inspect_container reads mount sources from details.Mounts[].Source, not from profile.volume_bindings[].host_volume_file. The DSM action inspect (no get) does not exist (code 103 "Method does not exist"); use get.

Implementation rules

  • Confirmation required before destructive operations: stop_project, redeploy_project, create_project, delete_project, exec_in_container, update_image_tag, update_env_var, update_compose, delete_container, stop_container, restart_container, pull_image
  • After compose changes: suggest redeploy_project
  • DSM errors → human-readable message, no stack traces
  • No secrets in stderr output
  • Type hints and docstrings everywhere
  • Formatter: ruff format · Linter: ruff check · Tests: pytest
  • All text (docstrings, comments, README): English
  • CHANGELOG.md: every user-visible change (bug fix, new/changed tool, behavior change, security fix, dependency bump) gets a CHANGELOG.md entry in the same commit — under a ## [Unreleased] heading between releases, which becomes ## [X.Y.Z] - YYYY-MM-DD on version bump. Pure internal cleanup (renames without external callers, comment-only edits, ruff autofix) needs no entry. Don't ship a release with a stale changelog (this was the C-2 gap that caused 0.2.7 and 0.2.8 to ship undocumented).
  • Version consistency: the package version lives in pyproject.toml and must stay in sync with uv.lock and the [X.Y.Z] heading in CHANGELOG.md. src/mcp_synology_container/__init__.py derives __version__ from importlib.metadata and is never hand-edited. Every version bump touches all three files in the same commit.

DSM API reference

  • cmeans/mcp-synology (GitHub) — auth, keyring, CLI structure
  • N4S4/synology-api docker_api.py (GitHub) — SYNO.Docker.* calls