Files
mcp-synology-container/CHANGELOG.md
T
marcus 7bb9b00dcc feat: v0.7.0 — inspect_container (full-path mount source)
New tool inspect_container surfaces the full configuration of a single
container as the foundation for a future GUI-container → Compose
migration workflow. Output covers image, status, restart policy,
network mode + per-network IPs, port bindings, volume mounts, env
vars, labels, entrypoint/command, links, and capabilities.

Mount paths come from details.Mounts[].Source (full /volume1/...
path), NOT from profile.volume_bindings[].host_volume_file — the
latter is share-relative (e.g. /docker/foo for /volume1/docker/foo)
and not directly Compose-usable. Verified live against the NAS;
quirk documented in CLAUDE.md.

DSM API: SYNO.Docker.Container/get with name JSON-encoded (action
inspect does not exist and returns code 103). Hash-prefixed names
are resolved transparently, matching the convention of the other
container tools.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 15:59:51 +02:00

26 KiB
Raw Blame History

Changelog

All notable changes to this project will be documented in this file.

[0.7.0] - 2026-05-18

Added

inspect_container — new tool that returns the full configuration of a single container, intended as the foundation for a future GUI-container → Compose migration workflow.

  • API: SYNO.Docker.Container/get with name JSON-encoded (the same endpoint already used by get_container_status and delete_container, but called directly with a focus on the migration-relevant fields). The DSM action inspect (no get) does not exist and returns DSM code 103 — use get.
  • Output covers: image, status / running, restart policy, network mode and per-network IP addresses, port bindings, volume mounts (with FULL /volume1/... host path — see DSM quirk below), environment variables, labels, entrypoint / command, links, and capabilities / privileged.
  • Hash-prefixed container names (abcdef012345_myservice) are resolved to the actual DSM name for the API call and stripped from the display header — same convention as the other container tools.

Documented (DSM quirk)

profile.volume_bindings[].host_volume_file is share-relative, not the full host path. A live capture against a container with a /volume1/docker/homeassistant bind mount returned host_volume_file = "/docker/homeassistant" in profile, while details.Mounts[].Source carried the full /volume1/docker/homeassistant. For a Compose-rebuild use case the full path is required — inspect_container therefore reads mount sources from details.Mounts[].Source, not from profile.volume_bindings[].host_volume_file. Recorded in CLAUDE.md.

[0.6.0] - 2026-05-18

Changed

build_stream is no longer fire-and-forget (#2). A live stream capture confirmed that DSM emits a readable plaintext build log over the HTTP body — one short status line per step — and closes the connection when the build is done. The 0.2.5 implementation sent the request and dropped the body unread, which meant a failed image pull left users with nothing more than a BUILD_FAILED polling status and no actionable diagnostic.

  • DsmClient.trigger_build_stream now consumes the body line by line and returns the collected log as a string instead of None. A wall- clock budget of 210 s (BUILD_STREAM_BUDGET, kept under the Claude Desktop ~4 min tool-call ceiling) caps the read; on timeout the partial log is returned with a [build_stream: timeout — stream still open server-side] marker appended so the caller knows the build is still going server-side. Per-chunk ReadTimeout is treated the same way — return what we have plus the marker.
  • redeploy_project and create_project now parse the returned log via _parse_build_stream_log, which classifies any line containing Error response from daemon: or ending in Error as a failure. When the log contains errors the tools abort immediately, surface the daemon line(s) in the result (e.g. Error response from daemon: manifest for nginx:9.9.9-nonexistent not found: manifest unknown), and skip the polling step entirely — DSM has already told us the build is dead. The existing BUILD_FAILED / ERROR polling guard (M-5) stays as a second safety net for late failures where the stream log was clean but the container exited after start.
  • No new MCP tool: the build log is a live stream and cannot be re-fetched after the build ends, so it is surfaced during redeploy_project / create_project rather than exposed as a standalone get_project_build_log call.

JSON error envelope handling, transport-error mapping (M-4), and the SID-scrubbed HTTP-error formatting are unchanged. Closes #2.

Minor version bump because redeploy_project and create_project return materially different strings on a failed build and exit earlier in the failure path; signatures unchanged.

[0.5.1] - 2026-05-18

Fixed

  • pull_image (#3): the 0.5.0 implementation called SYNO.Docker.Registry/pull_start, which DSM rejects with "Method does not exist". A live DSM API capture confirmed that pull_start actually lives on SYNO.Docker.Image, not SYNO.Docker.Registry. The Registry API only exposes the synchronous read-only methods (search, tags, get/set/create/ delete, using). Parameters are unchanged — both repository and tag are still JSON-encoded — and the Image/list polling for completion detection works as before. Note: #3 is already closed; this is the follow-up fix.

[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 210 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:<hash> (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_imagename: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.<docker fields> 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 <digits>-<suffix> pattern. For example, changing 2.558-jdk212.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