chore: remove throwaway test scripts, update SPEC.md for v0.2.10
- Delete test_dirsize_md5.py, test_extract.py, test_sharing.py - SPEC.md: add all 20 tools, DSM quirks (one-shot, cold-start, FastMCP outputSchema, DirSize status v1, Sharing.delete id encoding), APIs confirmed unavailable, mark v0.2 complete, list v0.3 candidates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -62,21 +62,83 @@ All requests use `GET /webapi/entry.cgi` with query parameters unless noted.
|
||||
| `SYNO.FileStation.Rename` | 2 | `rename` |
|
||||
| `SYNO.FileStation.CopyMove` | 3 | `start`, `status`, `stop` |
|
||||
| `SYNO.FileStation.Delete` | 2 | `start`, `status`, `stop` |
|
||||
| `SYNO.FileStation.Compress` | 3 | `start`, `status`, `stop` |
|
||||
| `SYNO.FileStation.Extract` | 2 | `start`, `status`, `stop` |
|
||||
| `SYNO.FileStation.DirSize` | 2/1 | `start` (v2), `status` (v1) |
|
||||
| `SYNO.FileStation.MD5` | 2/1 | `start` (v2), `status` (v1) |
|
||||
| `SYNO.FileStation.CheckPermission` | 3 | `write` |
|
||||
| `SYNO.FileStation.Sharing` | 3 | `create`, `list`, `delete` |
|
||||
|
||||
### Async task pattern (CopyMove, Delete, Search)
|
||||
### Async task pattern (CopyMove, Delete, Search, Compress, Extract)
|
||||
|
||||
DSM returns a `taskid` from `start`; the client polls `status` with exponential backoff
|
||||
(initial 200 ms, max 2 s, timeout 60 s) until `finished=true`, then calls `stop`/`clean`.
|
||||
Implemented in `_poll_task()`.
|
||||
|
||||
### One-shot task pattern (DirSize, MD5)
|
||||
|
||||
`DirSize` and `MD5` differ from the async task pattern: DSM delivers `finished=true` exactly
|
||||
once, then immediately discards the result (subsequent polls return error 599). Implemented
|
||||
in `_start_and_poll_oneshot()`:
|
||||
|
||||
1. Call `start`.
|
||||
2. Poll `status` repeatedly until `finished=true` or timeout.
|
||||
3. On 599: if within the first ~8 s after start, the background service may still be warming
|
||||
up ("cold start") — restart the task and try again (up to 6 restarts).
|
||||
4. On `finished=true`: return the data and stop polling (never poll again).
|
||||
|
||||
**Cold-start behaviour:** After a period of inactivity, DSM's DirSize/MD5 background service
|
||||
takes ~6–8 s to initialise. During this window every `status` poll returns error 599. The
|
||||
correct recovery is to wait a moment, then restart the task. Retrying the same `taskid` does
|
||||
not help — the service must be cold-started via a new `start` call.
|
||||
|
||||
**DirSize `status` version:** Must use `version=1`. Using `version=2` always returns 599
|
||||
regardless of service state.
|
||||
|
||||
---
|
||||
|
||||
## Tools
|
||||
## Tools (v0.2.10 — 20 tools)
|
||||
|
||||
### Read-only
|
||||
|
||||
#### `list_shares`
|
||||
List all shared folders visible to the authenticated user.
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_shares` | List all shared folders with volume usage |
|
||||
| `list_dir` | List directory contents with pagination and sorting |
|
||||
| `get_info` | Get detailed metadata for one or more paths |
|
||||
| `check_exist` | Check if one or more paths exist (Yes/No table) |
|
||||
| `search` | Search for files by glob pattern with async polling |
|
||||
| `download` | Download a file as base64 (max 10 MB) |
|
||||
| `dir_size` | Total size, file count, folder count for directories |
|
||||
| `get_md5` | Compute MD5 checksum of a file |
|
||||
|
||||
### Write
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `create_folder` | Create a new folder (optionally with parent dirs) |
|
||||
| `rename` | Rename a file or folder |
|
||||
| `copy` | Copy a file or folder (async polling, overwrite=False default) |
|
||||
| `move` | Move a file or folder (async polling, overwrite=False default) |
|
||||
| `delete` | Delete a file or folder — requires confirmed=True |
|
||||
| `upload` | Upload base64-encoded content to a path (max 50 MB) |
|
||||
| `compress` | Compress paths into a ZIP or 7z archive |
|
||||
| `extract` | Extract a ZIP or 7z archive to a destination folder |
|
||||
|
||||
### Permission & Sharing
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `check_permission` | Check write permission for a filename in a directory |
|
||||
| `create_sharing_link` | Create a public sharing link (optional password + expiry) |
|
||||
| `list_sharing_links` | List all sharing links (paginated table) |
|
||||
| `delete_sharing_link` | Delete a sharing link by ID |
|
||||
|
||||
---
|
||||
|
||||
### Tool details
|
||||
|
||||
#### `list_shares`
|
||||
**Parameters:** none
|
||||
|
||||
**Returns:** Formatted table of share names, paths, and volume usage.
|
||||
@@ -92,8 +154,6 @@ additional=["volume_status"]
|
||||
---
|
||||
|
||||
#### `list_dir`
|
||||
List contents of a directory with optional pagination and sorting.
|
||||
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
@@ -119,17 +179,11 @@ additional=["size","time"]
|
||||
>
|
||||
> **`additional` format:** Must be a JSON array serialised as a string
|
||||
> (`json.dumps(["size","time"])` → `'["size", "time"]'`). A comma-separated string
|
||||
> (`"size,time"`) is silently ignored by DSM — the `additional` field will be absent from
|
||||
> every file entry.
|
||||
>
|
||||
> **`SYNO.FileStation.Stat`:** Not available on this NAS's API registry. Use
|
||||
> `SYNO.FileStation.List::getinfo` for per-path metadata instead.
|
||||
> (`"size,time"`) is silently ignored by DSM.
|
||||
|
||||
---
|
||||
|
||||
#### `get_info`
|
||||
Get detailed metadata for one or more files or folders.
|
||||
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
@@ -140,19 +194,25 @@ creation time, and real volume path for each requested item.
|
||||
|
||||
**DSM call:** `SYNO.FileStation.List::getinfo`
|
||||
```
|
||||
path={comma-joined paths},
|
||||
path=json.dumps([paths...]),
|
||||
additional=["real_path","size","time","perm","owner","type"]
|
||||
```
|
||||
|
||||
> **Note:** `SYNO.FileStation.Stat` is not available on all NAS firmware versions and is
|
||||
> absent from this NAS's API registry. `SYNO.FileStation.List::getinfo` returns identical
|
||||
> data and is confirmed working.
|
||||
---
|
||||
|
||||
#### `check_exist`
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `path` | str | yes | One or more share-relative paths, comma-separated |
|
||||
|
||||
**Returns:** Yes/No table per path.
|
||||
|
||||
**DSM call:** `SYNO.FileStation.List::getinfo` — entries with `name=null` are non-existent.
|
||||
|
||||
---
|
||||
|
||||
#### `search`
|
||||
Search for files matching a pattern within a directory.
|
||||
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
@@ -164,36 +224,48 @@ Search for files matching a pattern within a directory.
|
||||
**Returns:** List of matching paths with type, size, and modification time.
|
||||
|
||||
**DSM calls:** `SYNO.FileStation.Search::start` → poll `::list` → `::stop` + `::clean`
|
||||
```
|
||||
start: folder_path={path}, recursive={recursive}, pattern={pattern}
|
||||
list: taskid={taskid}, offset=0, limit={max_results}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `download`
|
||||
Download a single file and return its content.
|
||||
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `path` | str | yes | Absolute path to the file on the NAS |
|
||||
| `path` | str | yes | Share-relative file path |
|
||||
|
||||
**Returns:** Object with `filename`, `size`, `content_base64` (base64-encoded file bytes).
|
||||
Files larger than 10 MB return an error suggesting `sftp` instead.
|
||||
**Returns:** JSON `{filename, size, content_base64}`. Files > 10 MB return an error.
|
||||
|
||||
**DSM call:** `SYNO.FileStation.Download::download` (streaming GET)
|
||||
```
|
||||
path={path}, mode=download
|
||||
```
|
||||
**DSM call:** `SYNO.FileStation.Download::download` (streaming GET, `mode=download`)
|
||||
|
||||
---
|
||||
|
||||
### Write (require explicit confirmation where noted)
|
||||
#### `dir_size`
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `path` | str | yes | Comma-separated share-relative paths |
|
||||
|
||||
**Returns:** Table with total size, file count, folder count.
|
||||
|
||||
**DSM calls:** `SYNO.FileStation.DirSize::start` (v2) → poll `::status` (v1)
|
||||
|
||||
> See *One-shot task pattern* above for polling behaviour and cold-start recovery.
|
||||
|
||||
---
|
||||
|
||||
#### `get_md5`
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `path` | str | yes | Share-relative file path |
|
||||
|
||||
**Returns:** `MD5 of {path}: {hash}`
|
||||
|
||||
**DSM calls:** `SYNO.FileStation.MD5::start` (v2) → poll `::status` (v1)
|
||||
|
||||
---
|
||||
|
||||
#### `create_folder`
|
||||
Create a new directory (and optionally all intermediate parents).
|
||||
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
@@ -201,113 +273,169 @@ Create a new directory (and optionally all intermediate parents).
|
||||
| `name` | str | yes | — | New folder name |
|
||||
| `create_parents` | bool | no | false | Create missing parent directories |
|
||||
|
||||
**Returns:** Path of the created folder or an error message.
|
||||
|
||||
**DSM call:** `SYNO.FileStation.CreateFolder::create`
|
||||
```
|
||||
folder_path={path}, name={name}, force_parent={create_parents}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `rename`
|
||||
Rename a file or directory.
|
||||
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `path` | str | yes | Absolute path to the item |
|
||||
| `new_name` | str | yes | New filename (not a full path) |
|
||||
|
||||
**Returns:** New absolute path after rename.
|
||||
| `path` | str | yes | Current share-relative path |
|
||||
| `new_name` | str | yes | New filename (bare name, not full path) |
|
||||
|
||||
**DSM call:** `SYNO.FileStation.Rename::rename`
|
||||
```
|
||||
path={path}, name={new_name}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `move`
|
||||
Move a file or directory to a new location.
|
||||
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `src` | str | yes | — | Source absolute path |
|
||||
| `dst` | str | yes | — | Destination directory path |
|
||||
| `overwrite` | bool | no | false | Overwrite if destination exists |
|
||||
|
||||
**Returns:** Destination path on success, or a descriptive error.
|
||||
|
||||
**DSM call:** `SYNO.FileStation.CopyMove::start` (async task)
|
||||
```
|
||||
path={src}, dest_folder_path={dst}, overwrite={overwrite}, remove_src=true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `copy`
|
||||
Copy a file or directory to a new location.
|
||||
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `src` | str | yes | — | Source absolute path |
|
||||
| `src` | str | yes | — | Source path |
|
||||
| `dst` | str | yes | — | Destination directory path |
|
||||
| `overwrite` | bool | no | false | Overwrite if destination exists |
|
||||
|
||||
**Returns:** Destination path on success, or a descriptive error.
|
||||
**DSM call:** `SYNO.FileStation.CopyMove::start` (async, `remove_src=false`)
|
||||
|
||||
**DSM call:** `SYNO.FileStation.CopyMove::start` (async task)
|
||||
```
|
||||
path={src}, dest_folder_path={dst}, overwrite={overwrite}, remove_src=false
|
||||
```
|
||||
---
|
||||
|
||||
#### `move`
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `src` | str | yes | — | Source path |
|
||||
| `dst` | str | yes | — | Destination directory path |
|
||||
| `overwrite` | bool | no | false | Overwrite if destination exists |
|
||||
|
||||
**DSM call:** `SYNO.FileStation.CopyMove::start` (async, `remove_src=true`)
|
||||
|
||||
---
|
||||
|
||||
#### `delete`
|
||||
**Destructive — requires `confirmed=True`.**
|
||||
|
||||
Delete a file or directory. Without confirmation, returns a preview of what would be deleted.
|
||||
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `path` | str | yes | — | Absolute path to delete |
|
||||
| `path` | str | yes | — | Path to delete |
|
||||
| `confirmed` | bool | yes | false | Must be `true` to proceed |
|
||||
|
||||
**Returns:**
|
||||
- `confirmed=false`: Preview message listing the path and item type.
|
||||
- `confirmed=true`: Success message or error detail.
|
||||
- `confirmed=false`: Preview of what would be deleted.
|
||||
- `confirmed=true`: Success message or error.
|
||||
|
||||
**DSM call:** `SYNO.FileStation.Delete::start` (async task)
|
||||
```
|
||||
path={path}, recursive=true, accurate_progress=false
|
||||
```
|
||||
**DSM call:** `SYNO.FileStation.Delete::start` (async, `recursive=true`)
|
||||
|
||||
---
|
||||
|
||||
#### `upload`
|
||||
Upload a file to the NAS from base64-encoded content.
|
||||
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `path` | str | yes | — | Destination directory path on the NAS |
|
||||
| `path` | str | yes | — | Destination directory path |
|
||||
| `filename` | str | yes | — | Filename to create |
|
||||
| `content_base64` | str | yes | — | Base64-encoded file content |
|
||||
| `overwrite` | bool | no | false | Overwrite if a file with this name already exists |
|
||||
| `overwrite` | bool | no | false | Overwrite if file exists |
|
||||
| `create_parents` | bool | no | true | Create missing parent directories |
|
||||
|
||||
**Returns:** Full path of the uploaded file or an error message.
|
||||
Files exceeding 50 MB should not be uploaded via MCP; return a clear error.
|
||||
**Returns:** Full path of uploaded file. Files > 50 MB are rejected.
|
||||
|
||||
**DSM call:** `SYNO.FileStation.Upload::upload` (POST multipart/form-data)
|
||||
```
|
||||
path={path}, create_parents={create_parents}, overwrite={overwrite},
|
||||
file=<binary content decoded from content_base64>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `compress`
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `paths` | list[str] | yes | — | Paths to compress |
|
||||
| `dest_file_path` | str | yes | — | Output archive path incl. filename |
|
||||
| `level` | str | no | `moderate` | `store`/`fastest`/`fast`/`normal`/`moderate`/`maximum` |
|
||||
| `mode` | str | no | `add` | `add`/`update`/`refreshen` |
|
||||
| `format` | str | no | `zip` | `zip` or `7z` |
|
||||
| `password` | str | no | `""` | Archive password |
|
||||
|
||||
**DSM call:** `SYNO.FileStation.Compress::start` (async, v3)
|
||||
|
||||
---
|
||||
|
||||
#### `extract`
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `file_path` | str | yes | — | Archive path on NAS |
|
||||
| `dest_folder_path` | str | yes | — | Destination directory |
|
||||
| `overwrite` | bool | no | false | Overwrite existing files |
|
||||
| `keep_dir` | bool | no | true | Preserve directory structure |
|
||||
| `create_subfolder` | bool | no | false | Extract into a subfolder |
|
||||
| `password` | str | no | `""` | Archive password |
|
||||
|
||||
**DSM call:** `SYNO.FileStation.Extract::start` (async, v2)
|
||||
|
||||
---
|
||||
|
||||
#### `check_permission`
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `path` | str | yes | — | Directory to check |
|
||||
| `filename` | str | yes | — | Filename to check write access for |
|
||||
| `overwrite` | bool | no | false | Check overwrite permission |
|
||||
| `create_only` | bool | no | false | Check create-only permission |
|
||||
|
||||
**Returns:** `"Permission granted: write {filename} into {path}"` or `"Error: …"`
|
||||
|
||||
**DSM call:** `SYNO.FileStation.CheckPermission::write` (v3)
|
||||
|
||||
> **Parameter format:** `path` and `filename` are passed as plain strings (no `json.dumps`).
|
||||
> On success, DSM returns `{"blSkip": false}` — the tool maps this to a human-readable message.
|
||||
|
||||
---
|
||||
|
||||
#### `create_sharing_link`
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `path` | str | yes | — | File or folder path to share |
|
||||
| `password` | str | no | `""` | Password to protect the link |
|
||||
| `date_expired` | str | no | `""` | Expiry date (`YYYY-MM-DD`) |
|
||||
| `date_available` | str | no | `""` | Availability start date (`YYYY-MM-DD`) |
|
||||
|
||||
**Returns:** Sharing URL + link ID, with password-protection flag.
|
||||
|
||||
**DSM call:** `SYNO.FileStation.Sharing::create` (v3)
|
||||
|
||||
> `path` must be `json.dumps()`-encoded. The `date_expired`/`date_available` fields are not
|
||||
> echoed back in the create response — confirm via `list_sharing_links` if needed.
|
||||
|
||||
---
|
||||
|
||||
#### `list_sharing_links`
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `offset` | int | no | 0 | Pagination offset |
|
||||
| `limit` | int | no | 100 | Max links to return |
|
||||
|
||||
**Returns:** Table with ID, URL, path, owner, expiry, status. Includes total count and
|
||||
pagination hint when more results are available.
|
||||
|
||||
**DSM call:** `SYNO.FileStation.Sharing::list` (v3)
|
||||
|
||||
---
|
||||
|
||||
#### `delete_sharing_link`
|
||||
**Parameters:**
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `link_id` | str | yes | Sharing link ID (from `list_sharing_links`) |
|
||||
|
||||
**Returns:** `"Deleted sharing link: {link_id}"` or error.
|
||||
|
||||
**DSM call:** `SYNO.FileStation.Sharing::delete` (v3)
|
||||
|
||||
> `link_id` must be passed as `json.dumps(link_id)` in the `id` parameter.
|
||||
|
||||
---
|
||||
|
||||
@@ -317,8 +445,7 @@ file=<binary content decoded from content_base64>
|
||||
1. DSM error codes are mapped to human-readable messages before surfacing to Claude.
|
||||
2. Never expose raw stack traces or session IDs in tool responses.
|
||||
3. Auth errors (codes 400–403) trigger a clear message with a hint to run `setup`.
|
||||
4. Session expiry errors (106, 107, 119) are retried once transparently; if the retry also
|
||||
fails, the user sees "Session expired — please restart the MCP server."
|
||||
4. Session expiry errors (106, 107, 119) are retried once transparently.
|
||||
5. Network errors (timeouts, connection refused) are reported as
|
||||
"Cannot reach NAS at {host} — check connectivity."
|
||||
6. Unknown DSM error codes are reported as "DSM error {code}: {raw_message}".
|
||||
@@ -340,6 +467,7 @@ file=<binary content decoded from content_base64>
|
||||
| 403 | 2FA required | "Two-factor authentication required — run setup." |
|
||||
| 404 | 2FA failed | "OTP code incorrect." |
|
||||
| 408 | Device token required | "Device token required — run setup again." |
|
||||
| 599 | Background service not ready | (handled by `_start_and_poll_oneshot` — restart task) |
|
||||
| 1800 | File not found | "File or folder not found: {path}" |
|
||||
| 1801 | No write permission | "No write permission for: {path}" |
|
||||
| 1802 | File exists | "A file already exists at this path." |
|
||||
@@ -351,6 +479,59 @@ file=<binary content decoded from content_base64>
|
||||
|
||||
---
|
||||
|
||||
## DSM API Quirks
|
||||
|
||||
### Parameter encoding
|
||||
|
||||
- **JSON array parameters** (`path`, `additional`, etc.) must be serialised with
|
||||
`json.dumps()`. A plain Python list or comma-separated string is silently ignored
|
||||
by DSM — the field will be absent from the response.
|
||||
- **`path` for multi-item operations** (e.g. `List::getinfo`, `DirSize::start`):
|
||||
always pass as `json.dumps(["/path1", "/path2"])`, even for a single path.
|
||||
- **`path` for single-item operations** (e.g. `CopyMove::start`, `Rename::rename`,
|
||||
`Sharing::create`): pass as `json.dumps("/path")` (a JSON-encoded string).
|
||||
- **`Sharing::delete` `id` parameter:** must be `json.dumps(link_id)` — plain string
|
||||
is not accepted.
|
||||
- **`CheckPermission::write` `path` and `filename`:** pass as plain strings (no
|
||||
`json.dumps`).
|
||||
- **Share paths vs. volume paths:** always use share paths (`/docker`, `/homes/marcus`).
|
||||
Volume paths (`/volume1/docker`) cause DSM error 408.
|
||||
|
||||
### APIs confirmed NOT available on this NAS
|
||||
|
||||
| API | Status | Workaround |
|
||||
|-----|--------|------------|
|
||||
| `SYNO.FileStation.Stat` | Not in API registry | Use `List::getinfo` |
|
||||
| `SYNO.FileStation.CheckExist` | Returns error 400 | Use `List::getinfo` (entries with `name=null` don't exist) |
|
||||
|
||||
### FastMCP `outputSchema` / tools/list payload limit
|
||||
|
||||
Do **not** add `-> str` return type annotations to `@mcp.tool()` functions. FastMCP
|
||||
infers `outputSchema` from the annotation, which doubles the tools/list payload size.
|
||||
Claude Desktop truncates the tool list when the payload exceeds its limit, making some
|
||||
tools invisible. Omitting the return annotation suppresses `outputSchema` generation.
|
||||
|
||||
### DirSize / MD5 one-shot behaviour
|
||||
|
||||
`DirSize` and `MD5` tasks are consumed on first read: `finished=true` is returned exactly
|
||||
once, then the task is deleted and all subsequent `status` polls return error 599.
|
||||
Implementation: `_start_and_poll_oneshot()` — never call `status` a second time after
|
||||
receiving `finished=true`.
|
||||
|
||||
### DirSize cold-start
|
||||
|
||||
After a period of inactivity, DSM's DirSize background service needs ~6–8 s to initialise.
|
||||
During cold start, `status` returns 599 on every poll. The fix is to wait briefly and
|
||||
**restart** the task (new `start` call) — polling the original `taskid` again does not help.
|
||||
`_start_and_poll_oneshot()` retries up to 6 task restarts before giving up.
|
||||
|
||||
### DirSize `status` API version
|
||||
|
||||
`SYNO.FileStation.DirSize::status` must be called with `version=1`. Using `version=2`
|
||||
always returns error 599 regardless of whether the background service is running.
|
||||
|
||||
---
|
||||
|
||||
## Configuration Model
|
||||
|
||||
```yaml
|
||||
@@ -403,26 +584,27 @@ Load config and credentials, create `FileStationClient` (lazy — no immediate c
|
||||
create and run `FastMCP("mcp-synology-filestation")` over stdio. Uses `anyio.run()` for
|
||||
Windows compatibility.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations
|
||||
## Roadmap
|
||||
|
||||
### `dir_size` / `get_md5`: occasional "DSM error code 599" on large directories
|
||||
### v0.2 — complete (20 tools)
|
||||
|
||||
DSM's `DirSize` and `MD5` APIs are one-shot: once `finished=true` is returned
|
||||
by the status endpoint, the task is removed and all subsequent polls return
|
||||
error 599. The MCP server polls immediately after `start` (no initial delay)
|
||||
and retries up to 5 consecutive 599 responses before giving up.
|
||||
All tools shipped in v0.2.10:
|
||||
|
||||
For small directories and files the result is reliably read on the first or
|
||||
second poll. For large directories (e.g. `/docker`, `/music`) the task takes
|
||||
longer; if DSM removes the completed result between two polls the tool returns
|
||||
`Error: DSM error code 599`. Retrying the operation usually succeeds.
|
||||
**Group 1:** `list_shares`, `list_dir`, `get_info`, `check_exist`, `search`, `download`
|
||||
**Group 2:** `create_folder`, `rename`, `copy`, `move`, `delete`, `upload`, `compress`,
|
||||
`extract`, `dir_size`, `get_md5`
|
||||
**Group 3:** `check_permission`, `create_sharing_link`, `list_sharing_links`,
|
||||
`delete_sharing_link`
|
||||
|
||||
Root cause is not fully understood. The raw HTTP test script (`test_dirsize_md5.py`)
|
||||
reliably catches `finished=true` for the same paths, suggesting the issue is
|
||||
related to timing in the MCP stdio event loop.
|
||||
### v0.3 — candidates
|
||||
|
||||
**Workaround:** retry the `dir_size` call. It succeeds on the second or third attempt
|
||||
for most paths.
|
||||
| Tool | API | Notes |
|
||||
|------|-----|-------|
|
||||
| `get_thumbnail` | `SYNO.FileStation.Thumb` | Return base64 thumbnail for image/video |
|
||||
| `list_favorites` | `SYNO.FileStation.Favorite` | List user-pinned favourite paths |
|
||||
| `add_favorite` | `SYNO.FileStation.Favorite` | Pin a path as favourite |
|
||||
| `delete_favorite` | `SYNO.FileStation.Favorite` | Remove a favourite |
|
||||
| `list_snapshots` | `SYNO.FileStation.Snapshot` | List Btrfs snapshots for a share |
|
||||
| `background_tasks` | `SYNO.FileStation.BackgroundTask` | List/cancel background operations |
|
||||
|
||||
Reference in New Issue
Block a user