# 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` | | `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=["real_path","volume_status"] ``` --- #### `list_dir` List contents of a directory with optional pagination and sorting. **Parameters:** | Name | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `path` | str | yes | — | Absolute path on the NAS | | `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=["real_path","size","time","perm","type"] ``` --- #### `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= ``` --- ## 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 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." 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." | | 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 ```yaml # ~/.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.