Files
mcp-synology-filestation/SPEC.md
T
marcus 83bccbcb53 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>
2026-04-14 15:33:22 +02:00

22 KiB
Raw Blame History

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

  1. mcp-synology-filestation setup collects NAS host, port, HTTPS flag, username, password.
  2. Credentials stored in OS keyring; connection config written to YAML.
  3. On serve, AuthManager.login() calls SYNO.API.Auth::login and holds the session ID in memory only — never written to disk or logged.
  4. If login requires 2FA: user runs setup interactively; the OTP + device token are handled by the setup wizard; the device token is stored in keyring for subsequent logins.
  5. 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, getinfo
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
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, 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 ~68 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 (v0.2.10 — 20 tools)

Read-only

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.

DSM call: SYNO.FileStation.List::list_share

additional=["volume_status"]

Note: DSM returns share paths directly (e.g. /dev, /data). The path field from the response is used as-is — do not prepend the volume prefix.


list_dir

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_path must be a share path as returned by list_share (e.g. /dev, /data). Volume paths (/volume1/dev) are not accepted and cause DSM error 408.

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.


get_info

Parameters:

Name Type Required Description
path str yes One or more share-relative paths, comma-separated

Returns: Table with type, size, owner, group, permissions (octal), modification time, creation time, and real volume path for each requested item.

DSM call: SYNO.FileStation.List::getinfo

path=json.dumps([paths...]),
additional=["real_path","size","time","perm","owner","type"]

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.


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


download

Parameters:

Name Type Required Description
path str yes Share-relative file path

Returns: JSON {filename, size, content_base64}. Files > 10 MB return an error.

DSM call: SYNO.FileStation.Download::download (streaming GET, mode=download)


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

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

DSM call: SYNO.FileStation.CreateFolder::create


rename

Parameters:

Name Type Required Description
path str yes Current share-relative path
new_name str yes New filename (bare name, not full path)

DSM call: SYNO.FileStation.Rename::rename


copy

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=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.

Parameters:

Name Type Required Default Description
path str yes Path to delete
confirmed bool yes false Must be true to proceed

Returns:

  • confirmed=false: Preview of what would be deleted.
  • confirmed=true: Success message or error.

DSM call: SYNO.FileStation.Delete::start (async, recursive=true)


upload

Parameters:

Name Type Required Default Description
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 file exists
create_parents bool no true Create missing parent directories

Returns: Full path of uploaded file. Files > 50 MB are rejected.

DSM call: SYNO.FileStation.Upload::upload (POST multipart/form-data)


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.


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.


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)


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.


Error Handling Strategy

Principles

  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 400403) trigger a clear message with a hint to run setup.
  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}".

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."
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."
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."

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 ~68 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

# ~/.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:

  1. Prompt for NAS host, port, HTTPS, SSL verify.
  2. Prompt for username and password (masked).
  3. Attempt login; detect 2FA requirement.
  4. If 2FA: prompt OTP, request device token, store token in keyring.
  5. Verify FileStation API availability (SYNO.FileStation.List).
  6. Save config to ~/.config/mcp-synology-filestation/config.yaml.
  7. 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.


Roadmap

v0.2 — complete (20 tools)

All tools shipped in v0.2.10:

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

v0.3 — candidates

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