synology-chat-hermes-mcp
by shunnnnn-png
README.md
# synology-chat-hermes-mcp
[English](README.md) · [繁體中文](README.zh-TW.md)
A constrained [FastMCP](https://github.com/jlowin/fastmcp) HTTP server that exposes a fixed Synology NAS SMB share to AI agents as read/write file tools — with strict path jail, size limits, and active-content rejection built in.
Originally built as the file channel for a family Hermes deployment (control agent → MCP → SMB3 → NAS). Works with any MCP client (Hermes, Claude, etc.).
## Why this exists
AI agents with file access are only safe if the access is bounded. This server:
- **Jails every path** to one fixed share (`NAS_SHARE`). Path traversal (`..`, absolute paths, symlink/reparse-point escape) is rejected before any SMB call.
- **Rejects public WAN servers** — `NAS_SERVER` must be a private, link-local, or Tailscale (100.64.0.0/10) literal IP. DNS names and public IPs are refused.
- **Never stores credentials** — the SMB password is read from a docker secret file at startup (`NAS_PASSWORD_FILE`), never from env vars or code.
- **Blocks active content** — DOCX/XLSX/PDF writers generate sanitized documents only; binary uploads are scanned for macros, external relationships, and scripts; extension/media-type mismatches are rejected.
- **Bounds everything** — file size, binary size, document characters, spreadsheet cells, image pixels, search results all have hard limits.
- **Audits operations** — every mutation is appended to a JSONL audit log.
## Architecture
```
Hermes (or any MCP client)
→ HTTP :8000/mcp (FastMCP, streamable HTTP)
→ NasService (validation, limits, audit)
→ SMBBackend (smbprotocol, SMB3, encryption+signing required)
→ Synology NAS share
```
## Tools
| Tool | Description |
|---|---|
| `health` | SMB3 connectivity check |
| `list_directory` | List a directory below the share |
| `search_files` | Search file names below a path |
| `read_file` | Read a size-limited UTF-8 text file |
| `write_file` | Atomic UTF-8 write (existing files protected unless `overwrite`) |
| `write_binary_file` | Validated base64 write (TXT/DOCX/XLSX/PDF/PNG/JPEG) |
| `create_document` | Safe DOCX from title + plain/Markdown-like text |
| `create_spreadsheet` | Safe XLSX (formulas rejected) |
| `create_pdf` | Static PDF (no scripts/links/embedded files) |
| `read_document` | Extract text from TXT/MD/CSV/JSON/YAML/DOCX/XLSX/PDF |
| `create_directory` | Create a directory (note: create one level at a time — `parents=True` is unreliable, see below) |
| `move_file` | Move within the share |
| `get_file_info` | Metadata without content |
**Known limitation:** `create_directory` validates the parent before `makedirs`, so a multi-level path whose first segment doesn't exist returns `not_found` even with `parents=True`. Create one level at a time.
## Quick start
```bash
cp .env.example .env # set NAS_SERVER (Tailscale/private IP), NAS_USERNAME
# provide the SMB password as a docker secret:
printf '%s' 'YOUR_SMB_PASSWORD' > ./synology_password
chmod 600 ./synology_password
docker build -t nas-mcp .
docker run -d --name nas-mcp \
--env-file .env \
-v "$PWD/synology_password:/run/secrets/synology_password:ro" \
-v nas-mcp-audit:/var/log/nas-mcp \
-p 8000:8000 \
nas-mcp
```
Health check:
```bash
curl -X POST http://127.0.0.1:8000/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"health","arguments":{}}}'
```
### Hermes MCP config
```yaml
mcp_servers:
nas-mcp:
type: http
url: http://nas-mcp:8000/mcp
```
### Running tests
```bash
python -m nas_mcp.smoke # exercises the full tool chain against the live share
```
## Environment reference
| Variable | Required | Default | Notes |
|---|---|---|---|
| `NAS_SERVER` | ✅ | — | Private/Tailscale literal IP only |
| `NAS_SHARE` | ✅ | — | Fixed share name, enforced at runtime |
| `NAS_USERNAME` | ✅ | — | Share-level SMB user |
| `NAS_PASSWORD_FILE` | | `/run/secrets/synology_password` | Docker secret path |
| `NAS_CONNECT_TIMEOUT_SECONDS` | | `8` | |
| `NAS_REQUIRE_ENCRYPTION` | | `true` | SMB3 encryption required |
| `NAS_REQUIRE_SIGNING` | | `true` | SMB3 signing required |
| `NAS_AUDIT_LOG` | | `/var/log/nas-mcp/audit.jsonl` | |
| `NAS_MAX_FILE_BYTES` | | `1048576` | |
| `NAS_MAX_BINARY_FILE_BYTES` | | `8388608` | |
| `NAS_MAX_DOCUMENT_CHARS` | | `100000` | |
| `NAS_MAX_SPREADSHEET_CELLS` | | `50000` | |
| `NAS_MAX_DOCUMENT_EXPANDED_BYTES` | | `67108864` | |
| `NAS_MAX_IMAGE_PIXELS` | | `25000000` | |
| `NAS_MAX_SEARCH_ENTRIES` | | `5000` | |
| `NAS_MAX_SEARCH_RESULTS` | | `100` | |
| `NAS_MCP_HOST` / `NAS_MCP_PORT` | | `0.0.0.0` / `8000` | |
## Security notes
- The SMB password is the only secret in the system and it lives **only** in the docker secret file. Nothing is hardcoded, nothing is stored in `.env` in plaintext beyond non-secret tuning values (and `.env` is git-ignored).
- Bind to a private interface in production; do not expose `:8000` publicly.
- The share name and SMB user are configuration (`NAS_SHARE`, `NAS_USERNAME`) — set them per deployment; nothing in the code assumes a particular value.
## License
MIT
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues