feat: initial project structure
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
reference/
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# .gitignore
|
||||||
|
reference/
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.pytest_cache/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# CLAUDE.md — mcp-synology-filestation
|
||||||
|
|
||||||
|
Development context for this project.
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
`mcp-synology-filestation` — MCP server exposing Synology FileStation as Claude tools.
|
||||||
|
Sibling project to `mcp-synology-container`.
|
||||||
|
|
||||||
|
## Infrastructure
|
||||||
|
|
||||||
|
- **NAS:** `https://dsm.gecheckt.de`
|
||||||
|
- **Config file:** `~/.config/mcp-synology-filestation/config.yaml`
|
||||||
|
- **Credentials:** OS keyring, service name `mcp-synology-filestation`
|
||||||
|
- **Gitea:** `https://gitea.gecheckt.de/marcus/mcp-synology-filestation`
|
||||||
|
- **Local code:** `D:\Dev\Projects\mcp-synology-filestation`
|
||||||
|
- **Runtime:** Python 3.12+, `uv`, MCP SDK, `httpx`, `keyring`, `click`, `rich`
|
||||||
|
|
||||||
|
## Deploy Workflow
|
||||||
|
|
||||||
|
1. Commit and push via Claude Code.
|
||||||
|
2. `uv tool install --reinstall git+https://gitea.gecheckt.de/marcus/mcp-synology-filestation.git`
|
||||||
|
3. Restart Claude Desktop.
|
||||||
|
|
||||||
|
## Toolchain
|
||||||
|
|
||||||
|
| Task | Command |
|
||||||
|
|------|---------|
|
||||||
|
| Format | `ruff format src/ tests/` |
|
||||||
|
| Lint | `ruff check src/ tests/` |
|
||||||
|
| Tests | `pytest` |
|
||||||
|
| Install dev | `uv sync --dev` |
|
||||||
|
| Run server | `uv run mcp-synology-filestation serve` |
|
||||||
|
| Setup | `uv run mcp-synology-filestation setup` |
|
||||||
|
|
||||||
|
## Code Standards
|
||||||
|
|
||||||
|
- **Type hints** on all public functions and methods.
|
||||||
|
- **Docstrings** (English) on all public modules, classes, and functions.
|
||||||
|
- **Async-first:** use `httpx.AsyncClient` throughout; never `requests`.
|
||||||
|
- **Formatter:** `ruff format` (line length 100).
|
||||||
|
- **Linter:** `ruff check` — fix all warnings before committing.
|
||||||
|
|
||||||
|
## Security Rules
|
||||||
|
|
||||||
|
- Never log or write credentials, session IDs, or passwords to disk or stderr.
|
||||||
|
- Never store passwords in the config YAML — OS keyring only.
|
||||||
|
- Mask sensitive query parameters (`_sid`, `passwd`) before logging request URLs.
|
||||||
|
|
||||||
|
## Destructive Operation Rules
|
||||||
|
|
||||||
|
- `delete`, `move` with `overwrite=True`, `copy` with `overwrite=True`, and `upload` with
|
||||||
|
`overwrite=True` are considered destructive.
|
||||||
|
- The `delete` tool MUST require `confirmed=True` to proceed. Without it, return a preview
|
||||||
|
message that describes exactly what would be deleted — never silently proceed.
|
||||||
|
- For overwrite scenarios in `move`/`copy`/`upload`, include a warning in the tool description
|
||||||
|
and default `overwrite` to `False`.
|
||||||
|
|
||||||
|
## Error Handling Rules
|
||||||
|
|
||||||
|
- DSM error codes must be mapped to human-readable messages. See SPEC.md for the full map.
|
||||||
|
- Never surface raw Python stack traces in tool return values.
|
||||||
|
- Session expiry (codes 106, 107, 119): transparent single retry with re-login.
|
||||||
|
- Network errors: return "Cannot reach NAS at {host} — check connectivity."
|
||||||
|
- Auth failures (400–403): return message + "Run `mcp-synology-filestation setup` to
|
||||||
|
reconfigure credentials."
|
||||||
|
|
||||||
|
## Tool Return Format
|
||||||
|
|
||||||
|
- All tools return `str`.
|
||||||
|
- Use `rich.table.Table` (rendered to string) for tabular data.
|
||||||
|
- Include item counts and pagination hints where relevant.
|
||||||
|
- Error messages are prefixed with `Error:` for easy recognition by Claude.
|
||||||
|
|
||||||
|
## Module Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/mcp_synology_filestation/
|
||||||
|
├── __init__.py # __version__
|
||||||
|
├── __main__.py # entry point
|
||||||
|
├── server.py # create_server(config, client) → FastMCP
|
||||||
|
├── client.py # FileStationClient (async httpx wrapper)
|
||||||
|
├── auth.py # AuthManager: keyring, env vars, 2FA, login/logout
|
||||||
|
├── config.py # AppConfig, ConnectionConfig, load_config, save_config
|
||||||
|
├── cli.py # click: setup / check / serve
|
||||||
|
└── tools/
|
||||||
|
├── __init__.py
|
||||||
|
└── filestation.py # register_filestation(mcp, config, client)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implemented Tools
|
||||||
|
|
||||||
|
_(none yet — pending implementation approval)_
|
||||||
|
|
||||||
|
See [SPEC.md](SPEC.md) for the full planned tool set.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# mcp-synology-filestation
|
||||||
|
|
||||||
|
MCP server for Synology FileStation — browse, search, transfer, and manage files
|
||||||
|
on your NAS via Claude.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Work in progress. See [SPEC.md](SPEC.md) for the planned tool set.
|
||||||
|
|
||||||
|
## Planned Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `list_shares` | List all shared folders |
|
||||||
|
| `list_dir` | Directory contents with pagination and sorting |
|
||||||
|
| `get_info` | File or folder metadata |
|
||||||
|
| `search` | Recursive pattern search |
|
||||||
|
| `download` | Download a file (base64 content) |
|
||||||
|
| `create_folder` | Create a new directory |
|
||||||
|
| `rename` | Rename a file or folder |
|
||||||
|
| `move` | Move to a new location |
|
||||||
|
| `copy` | Copy to a new location |
|
||||||
|
| `delete` | Delete a path (requires confirmation) |
|
||||||
|
| `upload` | Upload a file from base64 content |
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv tool install git+https://gitea.gecheckt.de/marcus/mcp-synology-filestation.git
|
||||||
|
mcp-synology-filestation setup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync --dev
|
||||||
|
uv run pytest
|
||||||
|
uv run ruff check src/
|
||||||
|
uv run ruff format src/
|
||||||
|
```
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
# 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 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.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
[project]
|
||||||
|
name = "mcp-synology-filestation"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "MCP server for Synology FileStation"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"mcp>=1.0.0",
|
||||||
|
"httpx>=0.27.0",
|
||||||
|
"pyyaml>=6.0",
|
||||||
|
"keyring>=25.0.0",
|
||||||
|
"click>=8.1.0",
|
||||||
|
"rich>=13.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0",
|
||||||
|
"pytest-asyncio>=0.24",
|
||||||
|
"respx>=0.21",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
mcp-synology-filestation = "mcp_synology_filestation.cli:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/mcp_synology_filestation"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
src = ["src", "tests"]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "W", "I", "N", "UP", "B", "SIM", "TCH"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"ruff>=0.15.10",
|
||||||
|
"pytest>=8.0",
|
||||||
|
"pytest-asyncio>=0.24",
|
||||||
|
"respx>=0.21",
|
||||||
|
]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""MCP server for Synology FileStation."""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
"""Allow running as: python -m mcp_synology_filestation."""
|
||||||
|
|
||||||
|
from mcp_synology_filestation.cli import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
"""Credential management: OS keyring, env var overrides, 2FA device token."""
|
||||||
|
# Implementation pending approval — see SPEC.md
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
"""Async HTTP client for Synology DSM / FileStation API."""
|
||||||
|
# Implementation pending approval — see SPEC.md
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
"""Application configuration: YAML loading, validation, env var overrides."""
|
||||||
|
# Implementation pending approval — see SPEC.md
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
"""MCP server factory: creates and configures the FastMCP instance."""
|
||||||
|
# Implementation pending approval — see SPEC.md
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
"""FileStation MCP tool registrations."""
|
||||||
|
# Implementation pending approval — see SPEC.md
|
||||||
Reference in New Issue
Block a user