# 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//` (default Synology layout) | --- ## Deploy workflow (after every code change) ``` 1. Claude Code commits and pushes 2. uv tool install --reinstall git+ 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 start** — `SYNO.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 Running` on success, ` Error` followed by `Error response from daemon: ` 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