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>
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>
Three path formats tested against NAS:
a) plain string -> 599 (wrong, task fails silently)
b) json.dumps([path]) -> works (JSON array is the correct format)
c) json.dumps(path) -> 599 (wrong, double-encoded string)
Confirms current implementation (json.dumps(paths)) is correct.
MD5 probe simplified to final confirmed settings (status v1, 0ms).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Live NAS investigation (test_dirsize_md5.py) revealed two bugs:
1. _poll_task() always slept 200ms before the first status call.
DirSize and MD5 complete near-instantly on small data, so the result
window closes before the first poll. Fix: add initial_delay parameter
(default 0.2s for CopyMove/Delete/Compress/Extract); DirSize and MD5
pass initial_delay=0.0 to poll immediately after start.
2. get_md5 used status version=2, but the NAS only serves the result on
status v1 (v2 always returns 599 regardless of timing). Fix: change
_poll_task version to 1 for SYNO.FileStation.MD5.
MD5 is one-shot: the result is consumed on the first successful status
read. Polling at 0ms ensures we catch it before it expires.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DirSize: _poll_task now uses status version=1 (v2 always returns 599)
- MD5: _poll_task keeps status version=2 (confirmed working via live NAS test)
Investigation notes documented in test_dirsize_md5.py:
both APIs use start v2; DirSize status needs v1, MD5 status needs v2;
tiny data causes one-shot race condition (no issue with real-world data).
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>
Reduced tools/list JSON payload from ~45 KB to 5.7 KB by replacing
verbose multi-paragraph docstrings with 1-2 line summaries on all
14 @mcp.tool() functions. Fixes Claude Desktop truncation of tools/list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FastMCP generates an outputSchema from the return type annotation, which
inflates the tools/list payload and triggers truncation in Claude Desktop.
Dropping the annotations suppresses outputSchema generation entirely.
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>
test_additional.py tests SYNO.FileStation.List::list with three variants:
no additional, comma-separated string, and JSON array — bypasses client
error mapping to log the raw DSM response for each. Delete after use.
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>
Diagnostic build to identify root cause of 408 on list_dir:
- client.py: log resolved version + NAS maxVersion before every request
- client.py: log full raw body on every error response
- tools/filestation.py: remove additional parameter entirely to test if
any additional field triggers 408, or if the issue is elsewhere
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SYNO.FileStation.List::list returns error 408 ("Non-supported additional
field") for real_path, perm, and type. Reduce additional to ["size","time"]
— the only fields reliably supported across DSM versions. isdir is already
present in the default response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>