# Changelog All notable changes to this project will be documented in this file. ## [0.5.0] - 2026-05-18 ### Added **Welle B Teil 1 — Registry tools (#3, #5).** Three new `SYNO.Docker.Registry` tools, reverse-engineered from a live DSM API capture (the n4s4 reference disagrees on parameter names and methods; the live capture wins): - `search_registry` (#5) — search the active Docker registry by query string. Uses `SYNO.Docker.Registry/search` (version 1) with the query JSON-encoded as `q`, plus `offset`, `limit`, and `page_size`. Renders stars, downloads, the official-image flag, and a truncated description per hit, and shows the total match count so the caller knows when to raise `limit`. No confirmation gate (read-only). - `list_image_tags` — list available tags for a repository (bonus tool). Uses `SYNO.Docker.Registry/tags` (version 1) with the repository JSON-encoded as `repo` — note the parameter name diverges from the n4s4 reference which uses `name`. The response shape is unusual: DSM returns the tag list as the envelope's `data` field directly (not wrapped in a sub-key), so the tool accepts both shapes defensively. Output is capped by `limit` (default 50) because popular images like `alpine` ship 200+ tags. No confirmation gate (read-only). - `pull_image` (#3) — pull an image into the local cache via `SYNO.Docker.Registry/pull_start` (version 1) with both `repository` and `tag` JSON-encoded. Requires `confirmed=True`. DSM exposes no confirmed `pull_status` method, so completion is detected by polling `SYNO.Docker.Image/list` for the new `repository:tag` pair with a 2–10 s backoff schedule and a 240 s overall budget (kept under the Claude Desktop ~4 min tool-call ceiling). A timeout returns a non-fatal "still running" hint pointing at `list_images` instead of raising — DSM keeps pulling server-side regardless. Short-circuits when the image is already present locally so a repeated call is cheap. Closes #3 and #5. Tool count rises from 31 to 34. ### Fixed - `inspect_image` rendering polish (follow-up to #4 after the 0.4.2 parameter-contract fix). Three live-NAS observations that the initial implementation got wrong: - **Header showed `?:?`** — DSM `SYNO.Docker.Image/get` returns `image=""` and `tag=""` when the lookup is by name:tag, so the response fields are unreliable. The header now echoes the user- supplied `image_id` (e.g. `gitea/gitea:1.26.1`), falling back to the sha256 `id` only if `image_id` itself were empty. - **Ports rendered as raw Python dicts** like `{'port': '22', 'protocol': 'tcp'}`. The `ports` array is actually a list of `{"port", "protocol"}` objects; each entry is now formatted as `22/tcp`. - **Environment rendered as raw Python dicts** like `{'key': 'PATH', 'value': '...'}`. The `env` array is actually a list of `{"key", "value"}` objects; each entry is now formatted as `PATH=/usr/local/...`. `cmd`, `entrypoint`, and `volumes` (plain string arrays) were already correct and are unchanged. Referencing #4 (already closed). ## [0.4.2] - 2026-05-18 ### Fixed - `inspect_image` (#4): the 0.4.0 implementation called `SYNO.Docker.Image/get` with `name` + `tag` + `id` parameters, which DSM rejected with error 114 ("invalid parameter"). Reverse- engineering the live API capture revealed the correct contract: one JSON-encoded parameter named `identity` that accepts either form: - `name:tag` (e.g. `"gitea/gitea:1.26.1"`) - `sha256:` (e.g. `"sha256:cd21e54e..."`) The pre-resolution lookup against `list_images` is no longer needed — the user input is JSON-encoded and passed straight through. - `inspect_image` response parsing: the endpoint does NOT return `details.Config.*` / `RootFS.Layers` (the Docker-engine inspect shape the previous code assumed). The actual top-level fields are `image`, `tag`, `id`, `digest`, `size`, `virtual_size`, `author`, `docker_version`, `cmd`, `entrypoint`, `env`, `ports`, `volumes`. Layer rendering is removed (the endpoint does not surface layers); digest, author, docker version, and volumes are now displayed. ## [0.4.1] - 2026-05-18 ### Removed - `pause_container` and `unpause_container` (added in 0.4.0) — live test against DSM revealed that Container Manager on this firmware does not expose pause/resume at all. The GUI action menu only offers Start / Stop / Force-Stop / Restart / Reset, and direct calls to `SYNO.Docker.Container/pause` and `/unpause` return "Method does not exist". Both tools have been removed rather than left as a broken surface. The remaining three lifecycle tools (`start_container`, `stop_container`, `restart_container`) are unaffected. Tool count drops from 33 to 31. Closes #7 (won't fix — DSM-side limitation). ## [0.4.0] - 2026-05-18 ### Added **Container lifecycle (#1, #7)** — five new tools fill the gap between "project" and "exec" operations. Until now the only way to stop or restart a single container was to stop the whole project. All five use `SYNO.Docker.Container` (version=1) with the same JSON-encoded `name` parameter pattern as `delete_container`, and route through `_resolve_container_name` so a clean name like `jenkins` resolves to DSM's hash-prefixed `f93cb8b504f7_jenkins`: - `start_container` — start a stopped container (no confirmation). - `stop_container` — stop a running container (confirmation required). - `restart_container` — stop + start in one call (confirmation required). - `pause_container` — SIGSTOP the container's processes (confirmation required). - `unpause_container` — SIGCONT the container's processes (no confirmation; reversing a pause is non-destructive). `stop` is live-verified against DSM; `start`, `restart`, `pause`, and `unpause` are implemented by symmetry on the same API surface. If any of them turns out to need a different parameter shape in production, it will be reverse-engineered via DevTools capture in a follow-up. **Image inspection (#4)** — `inspect_image` returns the full image detail blob (layers + sizes, environment variables, exposed ports, entrypoint/cmd, working directory, labels) via `SYNO.Docker.Image/get`. Accepts the same identifier forms as `delete_image` — `name:tag`, registry-prefixed `ghcr.io/foo/bar:v1`, and bare hash — and resolves the user input against `list_images` first so typos produce a clean "not found" rather than an opaque DSM error. The response parser is defensive about wrapper shape (`details.` vs. flat) so the tool keeps working if DSM varies the envelope between firmware versions. **System overview (#6)** — `system_overview` aggregates CPU %, RAM used/limit, network I/O, and block I/O across all running containers plus running/stopped counts. No new DSM endpoint: composed from `SYNO.Docker.Container/list` + `SYNO.Docker.Container/stats`, re-using the same Docker-formula CPU calculation as `container_stats`. Errors from either upstream call are non-fatal — the available section is rendered and the failure is surfaced under a `Warnings:` block. ## [0.3.3] - 2026-05-18 ### Fixed - `delete_container`: `SYNO.Docker.Container/delete` requires three parameters — `name` (JSON-encoded), `force=false`, and `preserve_profile=false`. Previously only `name` was sent (without JSON-encoding), causing DSM to reject the call with error 114. - `delete_project`: DSM does **not** reject `Project/delete` on a running project — it silently removes the registration and leaves the containers running as orphans. The connector now blocks the call itself when the project status is `RUNNING`, before issuing any DSM request, and tells the user to `stop_project` first. The previous implementation relied on a DSM-level rejection that never occurs in practice; the corresponding unit test was a false positive (mocked an error that real DSM never returns) and has been replaced with a test that asserts `Project/delete` is never called for a `RUNNING` project. ### Added - `delete_project` — remove a Container Manager project's registration via `SYNO.Docker.Project/delete` with the UUID JSON-encoded as the `id` parameter (per DSM convention). Mirrors the "Delete project" action in Container Manager: only the registration is removed; the project folder and compose file remain on the NAS. The success message explicitly states the folder was preserved so the user is not surprised. Closes the project lifecycle (create → start/stop/redeploy → delete). Safety: - Project-name validation runs before any I/O. - A `_find_project` pre-flight returns "not found" with a clear message rather than letting DSM reject an unknown UUID. - The tool deliberately does NOT auto-stop a running project. If DSM rejects the delete on a `RUNNING` project, the response tells the user to `stop_project` first rather than silently halting containers under the guise of a "delete" call. - Requires `confirmed=True`; the preview shows name, UUID, status, full path, and share path so the user can verify before deleting. ### Added - `create_project` — register a new Container Manager project from a compose YAML string. Three-step flow: 1. Create the target folder via `SYNO.FileStation.CreateFolder` with `force_parent=true` (idempotent — does not fail if the folder already exists, and creates missing intermediate directories). Without this step, `SYNO.Docker.Project/create` fails with DSM error code 2100. 2. `SYNO.Docker.Project/create` (form-encoded POST, JSON-encoded string parameters per DSM convention) returns the new project's UUID. 3. `trigger_build_stream` + `_wait_for_project_running` — reuses the existing image-pull / start / poll machinery (including the `BUILD_FAILED` early-exit from welle 2). Defaults: `share_path` is derived from `compose_base_path` (e.g. `/volume1/docker` + `myapp` → `/docker/myapp`). The compose content is validated as YAML before any side effects. A pre-flight `list_projects` check rejects duplicate names with a clear message rather than leaving an orphaned folder on the NAS. Requires `confirmed=True`; the preview shows the resolved share path and the service count parsed from the compose content. ### Fixed - `DsmClient.trigger_build_stream`: broaden transport-error handling (M-4). `httpx.ReadTimeout` is still treated as a success signal (the request was sent; DSM is processing it server-side), but all other `httpx.HTTPError` subclasses (`ConnectError`, `ConnectTimeout`, `WriteError`, `RemoteProtocolError`, …) are now converted into a `SynologyError` with a clear message. Previously these propagated as raw httpx exceptions and left callers with a stack trace. - `redeploy_project`: when build_stream (or the polling step) fails after the project has already been stopped, the response now explicitly tells the user that the project is in `STOPPED` state and must be recovered with `start_project` or another `redeploy_project` call. Previously the response suggested "use stop + start separately", which was misleading because stop had already happened. - `_wait_for_project_running`: exit the polling loop early on `BUILD_FAILED` / `ERROR` (M-5). DSM signals these statuses within seconds of a failed image pull; the old polling loop kept waiting up to 5 minutes for `RUNNING`. `redeploy_project` surfaces the terminal status with a hint to `update_image_tag` and retry when the cause is `BUILD_FAILED`. - `system_prune` preview: count unused user-created networks alongside dangling images and stopped containers (M-6). Built-in networks (`bridge`, `host`, `none`) are excluded because Docker never prunes them. Previously the preview noted "Unused networks: (not counted)", even though the underlying `SYNO.Docker.Utils/prune` call deletes them — users could lose networks they had not been warned about. ### Changed - Minor version bump because `redeploy_project` and `system_prune` return different strings (and `redeploy_project` returns earlier on `BUILD_FAILED`). No tool signatures changed. ## [0.2.9] - 2026-05-18 ### Fixed - `__version__` is now derived from package metadata via `importlib.metadata.version()`, eliminating the drift between `pyproject.toml` and `src/mcp_synology_container/__init__.py` (was stuck at `0.1.0` since the initial release). - `compose.py`: reject project names that contain path separators or other unsafe characters before they reach `_find_compose_path`. Previously a name like `"../../etc"` could traverse out of `compose_base_path` when the project was not yet registered with Container Manager. The new `_validate_project_name` helper enforces `^[a-zA-Z0-9_-]+$` and is applied to `read_compose`, `update_compose`, `update_image_tag`, and `update_env_var`. Addresses M-3 from the v0.2.8 review. ### Docs - `CHANGELOG.md` backfilled for releases 0.2.7 and 0.2.8 (entries had been missed during those releases). ## [0.2.8] - 2026-04-21 ### Added - `tests/test_dsm_client.py` — comprehensive offline test suite for `DsmClient`: - `_scrub_url` and `_error_message` pure helpers. - `request()` happy-path, API-not-cached, `_sid` scrubbing in `HTTPStatusError`, sensitive-param log masking. - Session re-auth retry: single-retry semantics, auth-manager-absent path, re-auth failure path, thundering-herd (login called once under concurrent 106 responses). - `trigger_build_stream`: SSE fire-and-forget, JSON error detection, `ReadTimeout` swallowing, HTTP-error scrubbing. - `upload_text` and `download_text` happy-path + error-response branches. - `_ensure_initialized` double-checked locking and negative-cache cooldown behavior. ### Fixed - `DsmClient._ensure_initialized`: cache failed init outcomes for 60 s so that repeated tool calls during a credential outage (wrong password, IP-blocked 407, DNS failure) do not keep hammering DSM. Each caller receives the cached exception until the cooldown window expires, after which a fresh attempt is made. Adds `INIT_ERROR_COOLDOWN` module constant and `_init_error` / `_init_error_until` state. Addresses M4 from the 0.2.7 review. ## [0.2.7] - 2026-04-21 ### Fixed - `DsmClient`: scrub `_sid` query-parameter values from URLs embedded in `httpx.HTTPStatusError` messages so the raw DSM session ID never reaches log output or MCP tool responses (C1). - `DsmClient`: re-auth lock now snapshots `_sid` before entering the lock and skips the redundant login if another task has already refreshed the session, eliminating duplicate logins on concurrent 106/107/119 responses (M3). - `DsmClient.trigger_build_stream`: re-instates immediate JSON-error detection (regression from 0.2.6). Inspects the `Content-Type` header and reads a small capped prefix of the body for `application/json` responses to surface DSM error codes without forcing the caller into a multi-minute polling timeout. SSE responses remain fire-and-forget (C2/M8). - `compose.update_env_var`: parenthesise the apply-branch match condition so `(isinstance AND startswith) OR (entry == var_name)` no longer evaluates the equality branch for non-string entries — aligns the apply side with the preview-side detection logic (M1). ### Changed - All 23 `@mcp.tool()` functions: strip `-> str` return annotations and trim docstrings to a single line (≤100 chars). FastMCP generates an `outputSchema` entry for every annotated tool, which roughly doubles the `tools/list` payload size; multi-line docstrings with `Args:`/`Returns:` sections add further bulk that Claude Desktop must parse on every connection. ### Chore - `uv.lock` resynced (was stale at `0.2.2`). - `.gitignore`: exclude `.claude/` per-user Claude Code settings. - Mechanical `ruff check --fix` + `ruff format` cleanup (import sorting, unused-import removal). No functional change. ## [0.2.6] - 2026-04-21 ### Fixed - `DsmClient.trigger_build_stream`: Claude Desktop aborts tool calls after ~4 minutes. The previous implementation read the first SSE chunk before returning, which could block for the entire duration of an image pull. Fixed by making the call truly fire-and-forget: the HTTP request is sent, response headers are received (HTTP status check only), then the connection is closed immediately without reading any SSE events. DSM continues the build server-side regardless. The `_json` import added in 0.2.5 is removed. ## [0.2.5] - 2026-04-21 ### Changed - `redeploy_project`: Replaced the delete-before-start image workaround with `SYNO.Docker.Project/build_stream` — the programmatic equivalent of the DSM "Erstellen" (Build) button, confirmed via browser DevTools capture. New 3-step flow for all project states: 1. Stop (skipped for STOPPED; error-suppressed for BUILD_FAILED) 2. `build_stream` — DSM pulls updated images and starts the project via SSE 3. Poll for RUNNING (timeout raised from 30 s to 5 min to accommodate image pulls) `build_stream` errors are now fatal (abort the redeploy with a clear message). ### Added - `DsmClient.trigger_build_stream(project_id)` — fires a streaming GET to `SYNO.Docker.Project/build_stream`, reads the first SSE chunk to confirm DSM accepted the request, then closes the connection. The build continues server-side. Handles immediate JSON error responses; swallows `ReadTimeout` (stream still open = build running). ## [0.2.4] - 2026-04-21 ### Changed - `redeploy_project`: Replaced broken `SYNO.Docker.Image/pull` with a delete-before-start workaround. The tool now reads image tags from the project's compose file via FileStation, deletes each cached image before calling `start` (so DSM auto-pulls the latest version), then polls for `RUNNING`. Image deletion is non-fatal — if it fails the project still starts. Unified 4-step flow for all project states (RUNNING, STOPPED, BUILD_FAILED). ### Added - `update_image_tag`: Auto-updates environment variables whose value equals the numeric version prefix of the old tag when the new tag shares the same `-` pattern. For example, changing `2.558-jdk21` → `2.560-jdk21` automatically updates `JENKINS_VERSION=2.558` to `JENKINS_VERSION=2.560`. The preview (unconfirmed call) now lists which env vars will be updated. Only triggers when the variable exists and the pattern matches; no change for plain tags like `latest`. ## [0.2.3] - 2026-04-21 ### Changed - `CLAUDE.md` rewritten: removed all operator-specific infrastructure details (hostnames, container names, image tags, personal notes). Kept DSM API quirks, implementation rules, and tool inventory. ## [0.2.2] - 2026-04-21 ### Fixed - `redeploy_project` (BUILD_FAILED): Pull errors are no longer silently suppressed. If the image pull fails (e.g. the tag in compose.yaml does not exist on the registry), redeploy aborts immediately with a clear message pointing to `update_image_tag`. ## [0.2.1] - 2026-04-21 ### Fixed - `redeploy_project`: After issuing `start`, the tool now polls the project status every 2 seconds for up to 30 seconds until the project reaches `RUNNING`. Previously DSM returned immediately while containers were still starting, causing the project to appear as `exited` when checked right after redeploy. On timeout a warning is returned instead of an error. - `delete_image`: Now distinguishes between running and stopped container references. A stopped container holding the image produces a clear hint to use `delete_container` or `system_prune` instead of a generic "in use" error. - `redeploy_project` (BUILD_FAILED path): Added explicit image pull step before restart (`stop → pull → start`). Previously the old cached image could be reused. ### Added - `delete_container` — delete a stopped container by name; refuses if container is still running; requires `confirmed=True`. ## [0.2.0] - 2026-04-14 ### Added **Images** - `list_images` — list local images sorted by size; marks images in use and with available updates - `delete_image` — delete image by `name:tag` or hash; refuses if image is used by a container **Container** - `container_stats` — live CPU %, RAM used/limit, network I/O, block I/O **System** - `system_df` — Docker disk usage: image count/size, running/stopped containers, reclaimable space - `system_prune` — remove dangling images, stopped containers, and unused networks **Networks** - `list_networks` — list all Docker networks with driver, subnet, gateway, attached containers - `create_network` — create a new bridge (or other driver) network - `delete_network` — delete a network; refuses if any container is still attached ### Fixed - `get_container_status` now reads the correct DSM response fields (`details.State` for status/running, `profile.image` for image name) and displays IP addresses, port bindings, and mounts - `redeploy_project` is now status-aware: RUNNING → stop + start; STOPPED → start directly; BUILD_FAILED → force-stop + start; unknown status returns a clear error with workaround hint - Container names with hash prefix (e.g. `f93cb8b504f7_jenkins`) are now transparently stripped in `list_containers`, `container_stats`, `get_container_status`, `get_container_logs`, and `exec_in_container` ### Changed - `DsmClient` gained a `post_request()` method for form-encoded POST operations required by image delete, network create/delete ## [0.1.0] - 2026-04-13 ### Added - Initial implementation - **Projects**: `list_projects`, `get_project_status`, `start_project`, `stop_project`, `redeploy_project` - **Containers**: `list_containers`, `get_container_status`, `get_container_logs`, `exec_in_container` - **Compose**: `read_compose`, `update_compose`, `update_image_tag`, `update_env_var` - **Images**: `check_image_updates` - DSM session management with auto re-authentication on session timeout - OS keyring integration for secure credential storage - 2FA / device token support - Interactive `setup` wizard and `check` connectivity command