Commit Graph

19 Commits

Author SHA1 Message Date
marcus f2abf2af1e docs: rewrite README, mark v0.3 complete in CLAUDE.md (v0.3.6)
- README.md: replace v0.1 placeholder with full v0.3.5 reference docs
  (all 26 tools documented with parameters, return values, caveats)
- CLAUDE.md: bump implemented-tools header to v0.3.5; roadmap updated
  (v0.3 complete, v0.4 candidates: none planned)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 06:46:45 +02:00
marcus 4430807b55 fix: get_thumbnail size limits, default=small, quality quirk docs (v0.3.5)
- Default size changed large → small (avoids MCP buffer overflows)
- Hard limit: return Error: when thumbnail exceeds ~2 MB base64 (1.5 MB raw)
- Soft limit: add "warning" field to JSON when thumbnail exceeds ~500 KB base64
  (375 KB raw), advising to use size='small'
- Constants _THUMB_ABORT_BYTES / _THUMB_WARN_BYTES moved to module level
- 6 new tests for size cap/warning/default/DSM-error paths (113 total)
- SPEC.md: document quality-ignored quirk, size ranges, soft+hard limits
- CLAUDE.md: DSM Quirks entry for Thumb quality/size behaviour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 06:39:57 +02:00
marcus 4c6de3bfc7 feat: add background_tasks + list_snapshots tools (v0.3.4)
- background_tasks: SYNO.FileStation.BackgroundTask::list (v3) — paginated
  table of active/recent copy/move/delete/extract/compress tasks
- list_snapshots: SYNO.FileStation.Snapshot::list (v2) — Btrfs snapshots
  per share; maps error 400 to a clear Btrfs-required message
- 20 new tests (107 total)
- SPEC.md and CLAUDE.md updated (26 tools)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 21:54:03 +02:00
marcus 161121b140 fix: search empty results + extract error 408 (v0.3.3)
Bug 1 — search::start folder_path format (already fixed in 314fae9):
  json.dumps([path]) is confirmed correct per official Synology API docs
  and multiple independent implementations (N4S4/synology-api, kwent/syno).
  Poll-loop last-non-empty guard (if current_files:) is also in place.
  No further change needed for Bug 1.

Bug 2 — extract::start wrong parameter key:
  The previous fix attempt renamed "file_path" → "path", which was wrong.
  Official API docs and independent implementations confirm the key is
  "file_path". The json.dumps() wrapping on file_path and dest_folder_path
  was already correct. Reverted the key rename.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 18:18:03 +02:00
marcus 314fae9167 fix: search empty results + extract error 408 (v0.3.2)
Bug 1 — search always returned empty results:
  Search::start was passing folder_path as a plain string.
  DSM silently ignores a plain string for this parameter and returns
  finished=True with files=[] immediately, as if nothing was found.
  Fix: json.dumps([path]) — JSON array, matching the multi-path API
  pattern used by DirSize::start and List::getinfo.

Bug 2 — extract returned DSM error 408:
  Extract::start was using "file_path" as the parameter key for the
  source archive. DSM expects "path". Without a valid path DSM returned
  error 408. The json.dumps wrapping was already correct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 17:55:22 +02:00
marcus 79b7384aeb feat: add get_thumbnail, list_favorites, add_favorite, delete_favorite (v0.3.1)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 17:11:05 +02:00
marcus 800b36a2b0 chore: release v0.2.10, bump to 0.3.0-dev
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 15:33:35 +02:00
marcus ae90e5f09a feat: add check_permission + 3 sharing tools (v0.2.10)
Implements Group 3 of the planned tool set:
- check_permission: SYNO.FileStation.CheckPermission/write
- create_sharing_link: SYNO.FileStation.Sharing/create (password + expiry optional)
- list_sharing_links: SYNO.FileStation.Sharing/list (paginated table)
- delete_sharing_link: SYNO.FileStation.Sharing/delete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 15:04:00 +02:00
marcus 451ee7116f fix: cold-start 599 on DirSize/MD5 — restart task instead of giving up
DSM's DirSize and MD5 background service needs ~6-8 s to initialise
after a period of inactivity. During this cold-start window tasks are
registered but every status poll returns error 599 ("no such task").

Replace the ad-hoc start+_poll_task call in dir_size and get_md5 with
a new _start_and_poll_oneshot helper that:
- polls with exponential backoff (0.2 s → cap 2 s)
- on 5 consecutive 599s: restarts the task (up to 6 attempts total)
- honours a shared 60 s wall-clock budget across all restarts
- returns a clear error if all restart attempts are exhausted

Root cause confirmed by test_dirsize_md5.py: after ~6 s / 2 restarts
the service warms up and the very first poll on the new task succeeds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 14:49:32 +02:00
marcus 8b2f07d9c3 revert: restore _poll_task and dir_size/get_md5 to 0.2.2 state
All changes since 0.2.2 to _poll_task, dir_size, and get_md5 (window_timeout,
_poll_oneshot, start_and_poll_immediately) are reverted. The 0.2.2 behaviour
worked reliably for small directories and is the last known-good baseline.

The remaining known limitation (occasional 599 on large directories) is
documented in SPEC.md. Retry the operation as a workaround.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 14:18:43 +02:00
marcus 62f8e41931 fix: _poll_oneshot for DirSize/MD5 with burst-retry on early 599
Add FileStationClient.start_and_poll_immediately: starts the async task and
immediately makes the first status poll within the same method, with no
intermediate awaits other than the two HTTP calls. This minimises scheduler
latency between start and first poll for one-shot tasks.

_poll_oneshot now accepts the first_status from start_and_poll_immediately:
- finished=True on first poll → return immediately
- finished=False → Phase 2 (exponential backoff, 60 s timeout)
- None (first poll was 599) → burst-retry 10× at 10 ms, then Phase 2
  (Phase 2 keeps polling through 599 until seen_alive, then fails fast)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 13:50:28 +02:00
marcus 0e8ffaa6df fix: _poll_oneshot for DirSize/MD5 with burst-retry on early 599
Replace _poll_task for one-shot tasks with _poll_oneshot, which uses two
phases: (1) a burst of up to 11 immediate polls at 50ms intervals to catch
tasks that complete in <500ms, and (2) exponential-backoff polling once
finished=False is observed. A 599 during burst → window missed (fail fast).
A 599 during Phase 2 (task was seen running) → same. _poll_task is
simplified back to a plain long-poll with no window_timeout logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 13:38:42 +02:00
marcus 6510493930 fix: window_timeout for one-shot tasks, drop debug stderr logging
_poll_task now accepts window_timeout. For DirSize and MD5 (one-shot result
windows), if only 599 errors arrive for window_timeout seconds without ever
seeing the task alive (finished=False), return a fast "result window missed —
please retry" error instead of waiting the full 60 s. Tasks that return
finished=False at least once (large dirs, large files) are unaffected.

Also removes the stale [dsm] debug stderr.write left in client.request().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 13:30:20 +02:00
marcus 4bf655236d fix: treat DSM 599 as task-not-ready in _poll_task, poll until 60s timeout
DirSize/MD5 return error 599 while the async task is still initialising on
the NAS, not only after the task is gone. Remove the 5-consecutive-599 abort
limit and the debug stderr logging; instead pass on 599 and keep polling
until the existing 60 s timeout fires. Rename the test that checked the old
limit to reflect the new timeout-based behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 13:19:32 +02:00
marcus c0d4c347c5 debug: add stderr logging to _poll_task for every status poll attempt
Logs [poll] lines to stderr so Claude Desktop's MCP log shows exactly
what DSM returns on each status call: attempt number, elapsed time,
finished flag, data keys (on success) or error code + message (on 599).

Version 0.2.3 — remove this logging once the 599 root cause is confirmed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 13:05:37 +02:00
marcus e3fa71b458 fix: retry _poll_task on transient 599 instead of aborting immediately
DirSize for large directories (e.g. /docker, 8441 folders, 46832 files)
takes ~800ms to compute. While running, status returns intermediate
progress (finished=false). But on the very first poll the task can return
599 transiently (task just started, not yet available). Previously
_poll_task caught any SynologyError and returned immediately, making
dir_size always fail on the first 599.

Fix: treat 599 as a transient condition and continue polling. Give up
only after 5 consecutive 599 responses. All other error codes remain
immediately fatal.

Investigation confirmed with test_dirsize_md5.py:
- /test-mcp (2937 B): finished=true at 0ms
- /docker (3.9 GB, 46832 files): finished=false at 35ms, finished=true at 789ms

Tests: 2 new cases (retry-succeeds, 5x-599-gives-up) → 95 total

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 13:01:27 +02:00
marcus 4d8eae752d chore: bump version to 0.2.1
Includes all DirSize/MD5 polling fixes:
- initial_delay=0.0 for DirSize and MD5 (poll immediately after start)
- MD5 status uses version=1 (v2 always returns 599 on this NAS)
- __version__ kept in sync with pyproject.toml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 12:52:42 +02:00
marcus 473c771c20 feat: add compress and extract tools
Implements SYNO.FileStation.Compress (v3) and SYNO.FileStation.Extract (v2)
with async polling identical to copy/move. Includes input validation for
compress (level, mode, format, empty paths) and 11 new unit tests.
Bumps version to 0.2.0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 11:26:08 +02:00
marcus 9fc5a3d68c feat: initial project structure 2026-04-14 07:51:51 +02:00