Commit Graph

26 Commits

Author SHA1 Message Date
marcus 81d5acd83e Release 0.2.0: README, CHANGELOG, version bump
- README: complete 22-tool reference table across 6 categories;
  updated feature list to reflect all additions and bug fixes
- CHANGELOG: added with [0.2.0] and [0.1.0] entries
- pyproject.toml: 0.1.0 → 0.2.0
- CLAUDE.md: corrected tool count 17 → 22

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 07:11:08 +02:00
marcus d9f0e75d0a Fix get_container_status: clean name + dual-format response handling
DSM response has two top-level keys:
  details → Docker Engine inspect (State, NetworkSettings, Mounts)
  profile → DSM format (image, port_bindings)

_format_container_detail now reads:
  Status/Running/StartedAt from details.State
  Image               from profile.image
  IP addresses        from details.NetworkSettings.Networks
  Port bindings       from profile.port_bindings
  Mounts              from details.Mounts

Also: debug_container_response tool removed, json/sys imports cleaned up.
27 container tests all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 06:59:50 +02:00
marcus 2dbcc0ba5f Add debug_container_response tool for DSM response inspection
Temporary debug tool that returns the raw SYNO.Docker.Container/get
response as JSON directly in the MCP tool output — visible in chat
without needing to inspect Claude Desktop logs.

To be removed once get_container_status is fixed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 06:45:04 +02:00
marcus 7c7e63d89a Add full JSON dump to get_container_status diagnostic logging
Replaces key-only log with complete json.dumps output (up to 2000 chars)
so the actual DSM response structure is visible in Claude Desktop logs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 06:38:00 +02:00
marcus 584d53e6e4 Fix get_container_status: clean name + dual-format response handling
- get_container_status now strips hash prefix from user input and calls
  SYNO.Docker.Container/get with the clean name (e.g. 'jenkins'), not the
  hash-prefixed form — the get endpoint accepts only the clean name
- _format_container_detail: unwraps 'container' wrapper key if present
  (DSM may return {"container": {State, Config, ...}} at the data level)
- Flat-format fallback: reads lowercase 'status'/'image' fields when
  Docker Engine nested format (State/Config) is absent
- Diagnostic stderr logging for data_keys, unwrap, status, image
- 25 container tests all passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 21:49:30 +02:00
marcus c8cda5ef2b Fix container hash-prefix + status-aware redeploy
Bug 1: Container name hash-prefix (e.g. f93cb8b504f7_jenkins)
- _strip_hash_prefix(): strips 12-char hex prefix and leading slash
- _resolve_container_name(): looks up actual DSM name from container list
- Applied in list_containers (display), container_stats (matching),
  get_container_status/get_container_logs/exec_in_container (lookup)

Bug 2: redeploy_project DSM 2101/1202 on wrong project state
- Fetch project status before acting
- RUNNING     → stop then start
- STOPPED     → start directly (nothing to stop)
- BUILD_FAILED → suppress stop error, then start
- Other       → return error with workaround hint

36 tests all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 21:43:02 +02:00
marcus 6fa35e1b48 Remove pull_image + list_registries; mark Gruppen 6+7 as entfällt
DSM methods for SYNO.Docker.Image/pull and SYNO.Docker.Registry/get
did not behave as expected in production testing against the NAS.
Tools deregistered, modules deleted, tests removed, CLAUDE.md updated.
Tool count: 19 → 17.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 20:03:17 +02:00
marcus 5fe8f5bc73 Add pull_image + list_registries; remove Gruppe 5 (no Volume API)
- pull_image: SYNO.Docker.Image/pull with repository+tag split via
  rpartition; polls image list every 3 s until image appears, 120 s timeout
- list_registries: SYNO.Docker.Registry/get; shows name, URL, active marker
- Gruppe 5 (Volumes) removed from roadmap — SYNO.Docker.Volume does not exist
- CLAUDE.md: tool count 17 → 19, Volumes section removed
- 28 tests all passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 19:28:45 +02:00
marcus 59f7fc1d6c Fix create_network + delete_network: POST with DevTools-confirmed format
create_network:
  - Switch from GET request() to post_request()
  - name, subnet, gateway, iprange → json.dumps(value) per DSM convention
  - disable_masquerade=json.dumps(False) added as required fixed param

delete_network:
  - Switch from GET request() to post_request() with method "remove"
    (was "delete" — wrong method name)
  - Send full network object from list response as
    networks=json.dumps([{...}]) including all DSM fields plus _key=id
    (ipv6_gateway, ipv6_subnet, ipv6_iprange, disable_masquerade with
    safe defaults for fields absent from the list response)

Tests updated: mock post_request, assert correct method/params structure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 18:56:21 +02:00
marcus 4cee16922f Add list_networks, create_network, delete_network tools (Gruppe 4)
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>
2026-04-13 18:42:10 +02:00
marcus 6bdd2bcb6a Add system_df and system_prune tools (Gruppe 3)
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>
2026-04-13 18:36:49 +02:00
marcus a8da306ce5 Add container_stats tool (Gruppe 2); remove rename_container
container_stats(container_name) calls SYNO.Docker.Container/stats,
locates the entry by name (stripping the DSM-added leading slash),
and reports:
  - CPU %  (standard Docker formula: cpu_delta / system_delta * cpus * 100)
  - Memory used / limit  (human-readable)
  - Network I/O rx / tx  (summed across all interfaces)
  - Block I/O read / write  (from io_service_bytes_recursive)

Gracefully handles first-poll (precpu system_cpu_usage absent → 0%).
7 unit tests covering: found, CPU formula, memory format, slash-strip,
not-found, API error, no-precpu fallback.

rename_container removed: DSM Container Manager offers no rename API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 18:29:17 +02:00
marcus 5edd051830 Fix delete_image: POST with images JSON array (DevTools-confirmed format)
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>
2026-04-13 18:19:22 +02:00
marcus 2b1e2ead7d Fix delete_image: use sha256 image ID instead of name+tag
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>
2026-04-13 18:03:18 +02:00
marcus b6fa547eb4 Add diagnostic logging to delete_image for DSM error 114
- 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>
2026-04-13 17:58:50 +02:00
marcus 0b48190f99 Anpassungen 2026-04-13 17:55:08 +02:00
marcus 24c3059e83 Fix delete_image: use rpartition for name:tag splitting
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>
2026-04-13 17:51:02 +02:00
marcus 06735bb447 Add list_images and delete_image tools (Gruppe 1)
- 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>
2026-04-13 17:44:57 +02:00
marcus b921e3a649 Fix compose path: strip /volumeN prefix for FileStation API
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>
2026-04-13 16:07:47 +02:00
marcus c0257f6068 Fix compose file probe: use FileStation.List/getinfo instead of FileStation.Info/get
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>
2026-04-13 15:59:27 +02:00
marcus dac215840e Fix compose path: resolve real project path via list_projects API
_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>
2026-04-13 15:55:34 +02:00
marcus e13324e10c Fix deadlock in lazy init: _ensure_initialized calling login via request()
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>
2026-04-13 15:49:26 +02:00
marcus 737c816ee7 Add stderr diagnostics and structured httpx timeout to dsm_client
- 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>
2026-04-13 15:38:37 +02:00
marcus 61cbf41900 Fix Claude Desktop loading: lazy NAS connection on first tool call
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>
2026-04-13 15:13:24 +02:00
marcus 81ff649ab7 Fix stdio startup for Claude Desktop compatibility
- 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>
2026-04-13 15:02:29 +02:00
marcus a0c1b6ed93 Initial implementation 2026-04-13 14:22:37 +02:00