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>
14 KiB
Technical Specification: mcp-synology-filestation
Overview
MCP server exposing Synology FileStation as Claude tools. Communicates with the DSM Web API over HTTPS. All file operations target a single NAS instance configured at setup time.
Architecture
CLI (click)
└── AppConfig + AuthManager
└── FileStationClient (httpx, async)
└── FastMCP server
└── tools/filestation.py (register_filestation)
- Transport: MCP stdio (Claude Desktop integration)
- HTTP: httpx async, HTTPS only in production
- Session: DSM session ID (
_sid) injected into every request; transparent re-auth on session expiry (codes 106, 107, 119) with exactly one retry - Config:
~/.config/mcp-synology-filestation/config.yaml - Credentials: OS keyring (service:
mcp-synology-filestation)
Auth Flow
mcp-synology-filestation setupcollects NAS host, port, HTTPS flag, username, password.- Credentials stored in OS keyring; connection config written to YAML.
- On
serve,AuthManager.login()callsSYNO.API.Auth::loginand holds the session ID in memory only — never written to disk or logged. - If login requires 2FA: user runs
setupinteractively; the OTP + device token are handled by the setup wizard; the device token is stored in keyring for subsequent logins. - Credential resolution order: env vars → OS keyring → (forbidden: plaintext config).
Environment variable overrides
| Variable | Purpose |
|---|---|
SYNOLOGY_HOST |
Override NAS hostname |
SYNOLOGY_USERNAME |
Override DSM username |
SYNOLOGY_PASSWORD |
Override DSM password (not stored) |
DSM API Endpoints
All requests use GET /webapi/entry.cgi with query parameters unless noted.
| API | Version | Methods used |
|---|---|---|
SYNO.API.Auth |
7 | login, logout |
SYNO.FileStation.Info |
2 | get |
SYNO.FileStation.List |
2 | list_share, list |
SYNO.FileStation.Stat |
3 | get |
SYNO.FileStation.Search |
2 | start, list, stop, clean |
SYNO.FileStation.Download |
2 | download |
SYNO.FileStation.Upload |
3 | upload (POST multipart) |
SYNO.FileStation.CreateFolder |
2 | create |
SYNO.FileStation.Rename |
2 | rename |
SYNO.FileStation.CopyMove |
3 | start, status, stop |
SYNO.FileStation.Delete |
2 | start, status, stop |
Async task pattern (CopyMove, Delete, Search)
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.
Tools
Read-only
list_shares
List all shared folders visible to the authenticated user.
Parameters: none
Returns: Formatted table of share names, paths, and volume usage.
DSM call: SYNO.FileStation.List::list_share
additional=["volume_status"]
Note: DSM returns share paths directly (e.g.
/dev,/data). Thepathfield from the response is used as-is — do not prepend the volume prefix.
list_dir
List contents of a directory with optional pagination and sorting.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
path |
str | yes | — | Share-relative path (e.g. /dev, /data/photos) |
offset |
int | no | 0 | Number of items to skip |
limit |
int | no | 100 | Max items to return (max 500) |
sort_by |
str | no | name |
name, size, user, group, mtime, atime, crtime, posix, type |
sort_direction |
str | no | asc |
asc or desc |
Returns: Table of entries (name, type, size, modified time). Includes total count for pagination context.
DSM call: SYNO.FileStation.List::list
folder_path={path}, offset={offset}, limit={limit},
sort_by={sort_by}, sort_direction={sort_direction},
additional=["size","time"]
Path requirement:
folder_pathmust be a share path as returned bylist_share(e.g./dev,/data). Volume paths (/volume1/dev) are not accepted and cause DSM error 408.
additionalformat: 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 — theadditionalfield will be absent from every file entry.
SYNO.FileStation.Stat: Not available on all NAS firmware versions. Do not use forget_info; fall back toSYNO.FileStation.List::listwithadditional=["size","time","perm","owner"].
get_info
Get detailed metadata for one or more files or folders.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
path |
str or list[str] | yes | Absolute path(s) on the NAS |
Returns: Per-path details: type, size, owner, group, permissions, timestamps, real path.
DSM call: SYNO.FileStation.Stat::get
path={comma-joined paths},
additional=["real_path","size","time","perm","owner","mount_point_type","type"]
search
Search for files matching a pattern within a directory.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
path |
str | yes | — | Root directory to search from |
pattern |
str | yes | — | Filename glob pattern (e.g. *.log) |
recursive |
bool | no | true | Search subdirectories |
max_results |
int | no | 200 | Cap on returned matches |
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 |
Returns: Object with filename, size, content_base64 (base64-encoded file bytes).
Files larger than 10 MB return an error suggesting sftp instead.
DSM call: SYNO.FileStation.Download::download (streaming GET)
path={path}, mode=download
Write (require explicit confirmation where noted)
create_folder
Create a new directory (and optionally all intermediate parents).
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
path |
str | yes | — | Parent directory path |
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.
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 |
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=false
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 |
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.
DSM call: SYNO.FileStation.Delete::start (async task)
path={path}, recursive=true, accurate_progress=false
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 |
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 |
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.
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>
Error Handling Strategy
Principles
- DSM error codes are mapped to human-readable messages before surfacing to Claude.
- Never expose raw stack traces or session IDs in tool responses.
- Auth errors (codes 400–403) trigger a clear message with a hint to run
setup. - 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."
- Network errors (timeouts, connection refused) are reported as "Cannot reach NAS at {host} — check connectivity."
- Unknown DSM error codes are reported as "DSM error {code}: {raw_message}".
DSM error code map
| Code | Meaning | User message |
|---|---|---|
| 100 | Unknown error | "DSM reported an unknown error." |
| 101 | Invalid parameter | "Invalid parameter in request." |
| 102 | API does not exist | "This DSM API is not available on your NAS." |
| 103 | Method does not exist | "This DSM API method is not supported." |
| 105 | Permission denied | "Permission denied — check your DSM account privileges." |
| 106 | Session timeout | (transparent re-auth) |
| 107 | Session displaced | (transparent re-auth) |
| 119 | Invalid session | (transparent re-auth) |
| 400 | Invalid password | "Login failed — check username and password (run setup)." |
| 401 | Guest or disabled account | "DSM account is disabled." |
| 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." |
| 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." |
| 1803 | Disk quota exceeded | "Disk quota exceeded." |
| 1804 | No space left | "No space left on the target volume." |
| 1805 | Filename too long | "Filename too long." |
| 1806 | Illegal filename characters | "Filename contains illegal characters." |
| 1807 | File is read-only | "File is read-only." |
Configuration Model
# ~/.config/mcp-synology-filestation/config.yaml
connection:
host: dsm.gecheckt.de
port: 443
https: true
verify_ssl: true
timeout: 30
auth:
username: marcus
# password: never stored here — OS keyring only
AppConfig fields
| Field | Type | Default | Description |
|---|---|---|---|
connection.host |
str | required | NAS hostname or IP |
connection.port |
int | 443/5000 | DSM port |
connection.https |
bool | true | Use HTTPS |
connection.verify_ssl |
bool | true | Verify TLS cert |
connection.timeout |
int | 30 | Request timeout (seconds) |
auth.username |
str | required | DSM username |
base_url is derived as {https_scheme}://{host}:{port}.
keyring_service is fixed to "mcp-synology-filestation".
CLI Subcommands
setup
Interactive wizard:
- Prompt for NAS host, port, HTTPS, SSL verify.
- Prompt for username and password (masked).
- Attempt login; detect 2FA requirement.
- If 2FA: prompt OTP, request device token, store token in keyring.
- Verify FileStation API availability (
SYNO.FileStation.List). - Save config to
~/.config/mcp-synology-filestation/config.yaml. - Print Claude Desktop snippet.
check
Validate config, resolve credentials, test login, list available FileStation APIs.
serve
Load config and credentials, create FileStationClient (lazy — no immediate connection),
create and run FastMCP("mcp-synology-filestation") over stdio. Uses anyio.run() for
Windows compatibility.