Files
mcp-synology-filestation/SPEC.md
T

386 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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=<binary content decoded from content_base64>
```
---
## 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; 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.