4a49407883
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>
8.1 KiB
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_myservicewhen the compose service name differs fromcontainer_name. All container tools strip this prefix transparently via_strip_hash_prefix/_resolve_container_name. - Async project start —
SYNO.Docker.Project/startreturns 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 pollingget_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-typetext/html), one short line per step:Container <name> Runningon success,<svc> Errorfollowed byError 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 withBUILD_STREAM_TIMEOUT_MARKERappended so callers know the build is still running server-side.redeploy_project/create_projectpass 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, checkget_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 standaloneget_project_build_logtool exists.- Image delete — requires a form-encoded POST with a JSON
imagesarray (confirmed via browser DevTools); usesDsmClient.post_request(). SYNO.Docker.Image/pullvs.pull_start— the legacypullmethod exists but behaviour varies by DSM version; not exposed as a standalone tool.pull_imageusesSYNO.Docker.Image/pull_start(asynchronous pull entry point) with bothrepositoryandtagJSON-encoded. Note thatpull_startlives onSYNO.Docker.Image, NOT onSYNO.Docker.Registry— the Registry API only exposes the synchronous read-only methods (search,tags,get/set/create/delete,using); callingRegistry/pull_startreturns "Method does not exist". No matchingpull_statusmethod is confirmed on either API.pull_imageis fire-and-return: it short-circuits if the tag is already local, callspull_start, then returns a "pull started in the background, verify withcheck_image_updates/list_images" hint. DSM keeps pulling server-side regardless of the HTTP response, so the tool does not block pollingImage/listfor 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— usesrepo(JSON-encoded) as the parameter name; the n4s4 reference'snamedoes not work on this DSM version. Returns the tag list as the envelope'sdatafield 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/pauseand/unpause— not implemented in DSM Container Manager on this firmware. The action menu only offers start/stop/force-stop/restart/reset; calls topause/unpausereturn "Method does not exist".pause_containerandunpause_containerwere briefly shipped in 0.4.0 and removed in 0.4.1.SYNO.Docker.Container/getresponse —profile.volume_bindings[].host_volume_fileis share-relative, not the full host path. Live capture against a container with bind mount/volume1/docker/homeassistant:/configreturnedhost_volume_file = "/docker/homeassistant"(21 chars, share-relative) inprofile, whiledetails.Mounts[].Sourcecarried the full/volume1/docker/homeassistantanddetails.HostConfig.Bindsthe full/volume1/docker/homeassistant:/config:rw. For Compose-rebuild use cases the full path is required —inspect_containerreads mount sources fromdetails.Mounts[].Source, not fromprofile.volume_bindings[].host_volume_file. The DSM actioninspect(noget) does not exist (code 103 "Method does not exist"); useget.
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.mdentry in the same commit — under a## [Unreleased]heading between releases, which becomes## [X.Y.Z] - YYYY-MM-DDon 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.tomland must stay in sync withuv.lockand the[X.Y.Z]heading inCHANGELOG.md.src/mcp_synology_container/__init__.pyderives__version__fromimportlib.metadataand 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 structureN4S4/synology-apidocker_api.py(GitHub) —SYNO.Docker.*calls