Adds read_text tool with pypdf integration for PDF text extraction,
plain-text decode for TXT/MD/CSV/JSON/etc, page-filter support,
max_chars truncation, and 11 mock-based tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
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>
_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>
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>
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>
dir_size: SYNO.FileStation.DirSize v2 — async scan of one or more
directories, returns folder/file count and total size as a table.
get_md5: SYNO.FileStation.MD5 v2 — async MD5 checksum of a file.
Both follow the existing _poll_task pattern. 9 new unit tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
SYNO.FileStation.CheckExist returns error 400 for every parameter format on
this DSM firmware, so the tool falls back to SYNO.FileStation.List::getinfo.
DSM returns an entry per requested path with name=None for non-existent paths,
which provides a reliable exists/not-exists signal.
Accepts a single path or a comma-separated list; returns a table of
Path | Exists (Yes/No) with a count footer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All path/name params are json.dumps-wrapped per confirmed DSM behaviour.
copy and move use async polling via a shared _poll_task helper
(exponential backoff 200ms→2s, 60s timeout). delete requires
confirmed=True; without it only a preview is returned and no DSM call
is made. upload decodes base64 and enforces a 50 MB cap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DSM sends files=[] on the final finished=True poll even when results
exist in earlier rounds. Overwriting files each iteration discarded
the real results, producing a false "No files found" response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements SYNO.FileStation.Search with async polling (exponential
backoff 200ms→2s, 60s timeout) and SYNO.FileStation.Download with
base64 output and a 10 MB size cap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DSM accepts multiple paths as a JSON array string, not comma-separated.
Comma-separated is treated as a single literal path; repeated path[]
params return error 400. Confirmed via test_getinfo_multipath.py.
- get_info: path param changed from ",".join(paths) to json.dumps(paths)
- tests: update multi-path assertion to expect JSON array format
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SYNO.FileStation.Stat is absent from this NAS's API registry.
SYNO.FileStation.List::getinfo returns identical data and is confirmed
working.
- tools/filestation.py: new get_info tool — accepts one or more
comma-separated paths, calls getinfo with real_path/size/time/perm/
owner/type additional fields, returns a 9-column table
- tests: 6 new tests covering single file, directory, multi-path,
empty input, DSM error, and correct API method assertion
- SPEC.md: remove SYNO.FileStation.Stat from API table, rewrite get_info
tool section to reference getinfo, update list_dir note
- CLAUDE.md: update Implemented Tools list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause of DSM 408 was wrong path format (volume path vs share path),
not the additional parameter. Confirmed via test_additional.py that
json.dumps(["size","time"]) is the correct format; comma-separated string
is silently ignored by DSM.
- list_dir: restore 4-column table (Name, Type, Size, Modified)
- list_dir: use additional=json.dumps(["size","time"]) (confirmed working)
- SPEC.md: document share path requirement, additional format rules,
note SYNO.FileStation.Stat unavailability, remove comma-sep gotcha
- tests: restore size/mtime mock data and assertions
- delete test_additional.py (throwaway diagnostic script)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: DSM expects share paths (/dev) not volume paths (/volume1/dev).
The 408 errors were triggered by any additional field on the wrong path format.
- list_shares: use share["path"] directly (e.g. /dev), drop real_path from
additional — only volume_status remains
- list_dir: remove additional parameter entirely; table now shows name + type
(isdir is returned by default); update docstring to show share path examples
- client.py: remove diagnostic REQUEST and RAW ERROR stderr logging
- tests: update assertions to match share paths and two-column table output
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>