list_networks:
SYNO.Docker.Network/list → shows name, driver, subnet, gateway,
attached containers. Response key is "network" (not "networks").
create_network(name, driver, subnet, gateway, ip_range, enable_ipv6, confirmed):
Dry-run preview without confirmed=True. Passes enable_ipv6 as
json.dumps(bool) per N4S4 reference. Optional params (subnet,
gateway, iprange) omitted from request when not provided.
delete_network(name, confirmed):
Validates network exists and has no attached containers before
deleting. Clear error listing attached container names if blocked.
15 unit tests covering all paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
system_df:
Assembles disk-usage report from SYNO.Docker.Image/list and
SYNO.Docker.Container/list. Reports image count/size/reclaimable
(images not referenced by any container), container running/stopped.
Gracefully degrades when one API is unavailable.
system_prune:
Without confirmed=True: lists dangling/unused images and stopped
containers with sizes (dry-run preview).
With confirmed=True: calls SYNO.Docker.Utils/prune and reports
reclaimed space from the response (SpaceReclaimed field).
10 unit tests: stats counts, reclaimable detection, preview content,
confirmed execution, missing-response-field graceful handling, API error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DSM Container Manager rejects name+tag and sha256 id params (error 114).
Browser DevTools capture shows the correct call is:
POST SYNO.Docker.Image / delete / version=1
images=[{"repository":"nouchka/sqlite3","tags":["latest"]}]
Changes:
- Add DsmClient.post_request() for form-encoded POST requests
- delete_image now calls post_request with version=1 and the images
JSON array built from the resolved repository name and tag
- Remove unused docker error-code dict from _error_message()
- Tests mock post_request and assert the images param content
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DSM SYNO.Docker.Image/delete returns error 114 when called with name+tag.
The API expects the sha256 hash from the image list (field "id") as the
"id" parameter.
- Look up the sha256 hash from SYNO.Docker.Image/list (already fetched
for the in-use check), then pass params={"id": img_hash}
- Guard against missing hash with a clear error message
- Updated tests to assert id param is sent, name/tag are absent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Log api, method, name, tag to stderr immediately before the DSM call
- Log the DSM error code on failure (visible in Claude Desktop stderr)
- Include DSM error code in the return message so it appears in chat
This makes the exact request params and the DSM code visible without
needing debug-level logging enabled, to help diagnose why error 114
("Invalid API call") is returned by SYNO.Docker.Image/delete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
partition(":") split at the first colon, so registry-prefixed images
like "ghcr.io/open-webui/open-webui:v0.8.10" produced name="ghcr"
instead of the correct repository name, causing DSM error 114.
rpartition(":") always splits at the last colon, correctly handling:
- plain images: "nginx:1.24" → name="nginx", tag="1.24"
- namespaced: "nouchka/sqlite3:latest" → correct split
- registry URLs: "ghcr.io/foo/bar:v1" → name="ghcr.io/foo/bar", tag="v1"
- no tag: "nginx" → name="nginx", tag="latest" (fallback)
Added regression test verifying correct params sent to DSM delete API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- list_images: lists all local Docker images sorted by size desc,
shows size (human-readable), creation date, in-use marker, and
update-available marker; gracefully handles container list failure
- delete_image: accepts name:tag or image hash, blocks deletion when
image is in use by a container, requires confirmed=True to execute;
default shows a dry-run preview
- 16 unit tests covering all paths (mock DSM client)
- ruff format + check clean
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Container Manager returns raw filesystem paths (/volume1/docker/...),
but SYNO.FileStation.* APIs expect paths without the volume prefix
(/docker/...). Add _to_filestation_path() to strip /volumeN and apply
it in _find_compose_path before any FileStation call.
Also switch directory probe from getinfo (returns truthy files array
with embedded code:408 for missing paths) to list (empty files array
for non-existent directories), and apply the same prefix stripping to
the error message shown when no compose file is found.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SYNO.FileStation.Info/get is a system-info API and returns success
regardless of whether a path exists, so the probe always returned the
first candidate (docker-compose.yml) even when only compose.yaml
was present. SYNO.FileStation.List/getinfo returns {"files": [...]}
with an empty list for non-existent paths, enabling correct detection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_find_compose_path was constructing {compose_base_path}/{project_name}
which didn't match the actual NAS path (e.g. /volume1/docker/frostiq/jenkins).
Now calls _find_project() first and uses project["path"] as the base
directory, with the old constructed path as a fallback only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
login() called client.request() which called _ensure_initialized() which
tried to re-acquire _init_lock — deadlocking forever. Fix: set
_initializing=True while inside the init critical section so request()
skips the _ensure_initialized() guard when called from within init
(safe because query_api_info() already populated the API cache).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Write progress markers to stderr at each lazy-init step so Claude
Desktop logs show exactly where a timeout occurs
- Replace flat timeout=30 integer with httpx.Timeout(connect=10,
read=30, write=10, pool=5) to fail fast on connection issues
instead of waiting up to 90 s across three sequential requests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the server blocked at startup waiting for query_api_info()
and login() before starting the MCP protocol. Claude Desktop has a short
initialization timeout and dropped the server before the handshake started.
Changes:
- DsmClient: add _ensure_initialized() with asyncio.Lock for thread-safe
lazy init; called automatically at the start of request(), upload_text(),
and download_text() on the first use.
- cli.py serve: remove upfront query_api_info() and auth.login() calls;
the server now starts immediately ("MCP server ready" on stderr) and
connects to the NAS on the first tool invocation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace asyncio.run() with anyio.run() in serve command: FastMCP uses
anyio.create_task_group() internally, and anyio.run() ensures the correct
backend context. asyncio.run() can misbehave on Windows (ProactorEventLoop).
- Add SERVER STARTING / MCP server ready messages to stderr for diagnostics.
- Replace sys.exit(1) with early return + stderr write inside anyio context;
sys.exit inside anyio.run() is less predictable than a clean return.
- Eagerly import all modules at serve startup so ImportErrors surface on
stderr immediately instead of silently killing the process.
- Add __main__.py to allow python -m mcp_synology_container invocation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>