Files
mcp-synology-container/CHANGELOG.md
T
marcus f27a5456f6 feat: v0.5.0 — welle B Teil 1 (registry tools: search, tags, pull)
Three new SYNO.Docker.Registry tools, reverse-engineered from a live
DSM API capture (n4s4 reference disagrees on param names and methods).

- search_registry (#5): SYNO.Docker.Registry/search v1 with JSON-encoded
  q, plus offset/limit/page_size. Renders stars, downloads, official
  flag, truncated description, and total match count. Read-only.
- list_image_tags: SYNO.Docker.Registry/tags v1 with JSON-encoded repo
  (not name — DSM live capture diverges from n4s4). Response shape is
  unusual: tag list comes back as the envelope's data field directly.
  Output capped by limit (default 50); accepts both list and dict
  response shapes defensively. Read-only.
- pull_image (#3): SYNO.Docker.Registry/pull_start v1 with both
  repository and tag JSON-encoded. Async pull — no pull_status method
  confirmed on this DSM, so completion is detected by polling
  SYNO.Docker.Image/list (2–10 s backoff, 240 s budget under the
  Claude Desktop ~4 min tool-call ceiling). Timeout returns a
  non-fatal "still running" hint. Short-circuits when the image is
  already present locally. Confirmation gate required.

Tool count: 31 → 34. CLAUDE.md confirmation list updated. New DSM
quirks documented for pull_start (no pull_status) and tags (repo
param name, top-level data array).

Closes #3
Closes #5

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

463 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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_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.<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-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