Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
- run: uv sync --extra dev
Expand All @@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
- run: uv sync --extra dev
Expand All @@ -39,11 +39,11 @@ jobs:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
- run: uv sync --extra dev
- run: uv run pytest --cov=synology_mcp --cov-report=xml
- run: uv run pytest --cov=mcp_synology --cov-report=xml
- uses: codecov/codecov-action@v4
if: matrix.python-version == '3.12'
with:
Expand Down
24 changes: 1 addition & 23 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ name: Publish
on:
push:
tags:
- 'v*' # automatic: publishes to PyPI on version tag
workflow_dispatch: # manual: publishes to TestPyPI
- 'v*'

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
Expand Down Expand Up @@ -33,28 +32,8 @@ jobs:
name: dist
path: dist/

publish-testpypi:
needs: build
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
environment: testpypi
permissions:
id-token: write
steps:
- name: Download distributions
uses: actions/download-artifact@v4
with:
name: dist
path: dist/

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/

publish-pypi:
needs: build
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment: pypi
permissions:
Expand All @@ -71,7 +50,6 @@ jobs:

github-release:
needs: publish-pypi
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down
49 changes: 49 additions & 0 deletions .github/workflows/test-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Test Publish

on:
workflow_dispatch:

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"

- name: Run tests
run: |
uv sync --extra dev
uv run pytest

- name: Build distributions
run: uv build

- name: Upload distributions
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/

publish:
needs: build
runs-on: ubuntu-latest
environment: testpypi
permissions:
id-token: write
steps:
- name: Download distributions
uses: actions/download-artifact@v4
with:
name: dist
path: dist/

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# synology-mcp
# mcp-synology
tests/integration_config.yaml

# Virtual DSM test infrastructure (golden images ~2GB each)
.vdsm/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
Expand Down
40 changes: 37 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
# Changelog

## 0.4.0 (2026-04-05)

### Breaking Changes

- **Package renamed** — `synology-mcp` → `mcp-synology` (distribution, CLI command, config paths, state paths, keyring service)
- **Python import renamed** — `synology_mcp` → `mcp_synology`
- **Config directory** — `~/.config/synology-mcp/` → `~/.config/mcp-synology/`
- **State directory** — `~/.local/state/synology-mcp/` → `~/.local/state/mcp-synology/`
- **Keyring service** — `synology-mcp/{instance_id}` → `mcp-synology/{instance_id}` (re-run `mcp-synology setup`)
- **DSM session/device name** — `SynologyMCP` → `MCPSynology`
- **License** — MIT → Apache 2.0

### Features

- **File transfer tools** — 2 new tools for uploading and downloading files:
- `upload_file` — upload local files to NAS with overwrite control, custom filenames, and progress reporting (WRITE tier)
- `download_file` — download NAS files to local disk with pre-flight disk space check, streaming writes, partial file cleanup on failure, and progress reporting (READ tier)
- Large file warnings when transfers exceed 1 GB
- **Project icons** — light/dark SVGs, PNGs (16–256px), and favicon.ico exposed via MCP `icons` parameter
- **TestPyPI workflow** — dedicated `test-publish.yml` for manual dispatch; `publish.yml` simplified to tag-only PyPI publishing
- **Virtual DSM test framework** — container-based integration testing with golden image save/restore, Playwright-based DSM wizard automation, and Podman/Docker auto-detection (`tests/vdsm/`)

### Migration

A migration script handles config, state, and keyring automatically:

```bash
uv tool install mcp-synology
python scripts/migrate-from-synology-mcp.py # dry run — preview changes
python scripts/migrate-from-synology-mcp.py --apply # apply changes
```

Then update Claude Desktop config: change `"command"` from `"synology-mcp"` to `"mcp-synology"`.

## 0.3.1 (2026-03-18)

### Features
Expand Down Expand Up @@ -38,7 +72,7 @@ Major refactor: CLI split, module registration system, DSM API fixes, integratio

### Breaking Changes

- **CLI is now a package** — `src/synology_mcp/cli.py` split into `cli/` package with 6 submodules (main, setup, check, version, logging_). Backward-compatible re-exports via `cli/__init__.py`
- **CLI is now a package** — `src/mcp_synology/cli.py` split into `cli/` package with 6 submodules (main, setup, check, version, logging_). Backward-compatible re-exports via `cli/__init__.py`

### Bug Fixes

Expand Down Expand Up @@ -83,7 +117,7 @@ Code quality fixes from second external review.

### Documentation

- README install updated to `uv tool install synology-mcp` (PyPI) instead of git URL
- README install updated to `uv tool install mcp-synology` (PyPI) instead of git URL

## 0.2.1 (2026-03-18)

Expand Down Expand Up @@ -139,7 +173,7 @@ Initial release.
- **File Station module** — 12 tools for managing files on Synology NAS:
- READ: list_shares, list_files, list_recycle_bin, search_files, get_file_info, get_dir_size
- WRITE: create_folder, rename, copy_files, move_files, delete_files, restore_from_recycle_bin
- **Interactive setup** — `synology-mcp setup` creates config, stores credentials, handles 2FA, emits Claude Desktop snippet
- **Interactive setup** — `mcp-synology setup` creates config, stores credentials, handles 2FA, emits Claude Desktop snippet
- **2FA support** — auto-detected device token bootstrap with silent re-authentication
- **Secure credentials** — OS keyring integration (macOS Keychain, Windows Credential Manager, Linux GNOME Keyring / KWallet)
- **Linux D-Bus auto-detection** — keyring works from Claude Desktop without manual env var configuration
Expand Down
22 changes: 11 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

synology-mcp is an MCP server for Synology NAS devices. It exposes Synology DSM API functionality as MCP tools that Claude can use. Modular, secure (2FA-ready), permission-tiered. Python 3.11+, async throughout, MIT licensed.
mcp-synology is an MCP server for Synology NAS devices. It exposes Synology DSM API functionality as MCP tools that Claude can use. Modular, secure (2FA-ready), permission-tiered. Python 3.11+, async throughout, Apache 2.0 licensed.

**Current status:** v0.3.x — File Station (12 tools) + System monitoring (2 tools), CLI, integration tests.
**Current status:** v0.4.x — File Station (14 tools) + System monitoring (2 tools), CLI, integration tests.

## Architecture

Layered design: core → modules → server/CLI.

- **Core** (`src/synology_mcp/core/`): DSM API client (async httpx), auth manager (session lifecycle, 2FA, keyring), YAML+Pydantic config loader, shared response formatters, typed exception hierarchy
- **Modules** (`src/synology_mcp/modules/`): Feature-specific tool handlers. Each module declares `MODULE_INFO` with API requirements and tool metadata. File Station (12 tools: 6 READ + 6 WRITE), System (2 tools: get_system_info, get_resource_usage)
- **Server** (`src/synology_mcp/server.py`): FastMCP initialization, module loading, startup
- **CLI** (`src/synology_mcp/cli/`): click-based package with `serve`, `setup`, `check` subcommands
- **Core** (`src/mcp_synology/core/`): DSM API client (async httpx), auth manager (session lifecycle, 2FA, keyring), YAML+Pydantic config loader, shared response formatters, typed exception hierarchy
- **Modules** (`src/mcp_synology/modules/`): Feature-specific tool handlers. Each module declares `MODULE_INFO` with API requirements and tool metadata. File Station (14 tools: 7 READ + 7 WRITE), System (2 tools: get_system_info, get_resource_usage)
- **Server** (`src/mcp_synology/server.py`): FastMCP initialization, module loading, startup
- **CLI** (`src/mcp_synology/cli/`): click-based package with `serve`, `setup`, `check` subcommands

Modules are domain-split: `listing.py`, `search.py`, `metadata.py`, `operations.py`, `helpers.py` — grouped by what they do, not permission tier.

Expand All @@ -25,7 +25,7 @@ Modules are domain-split: `listing.py`, `search.py`, `metadata.py`, `operations.

- `docs/specs/architecture.md` — layered architecture, module system, auth strategy chain, session lifecycle, credential storage
- `docs/specs/project-scaffolding-spec.md` — repo structure, pyproject.toml, CI, testing strategy
- `docs/specs/filestation-module-spec.md` — all 12 File Station tools with parameters, response shapes, error codes
- `docs/specs/filestation-module-spec.md` — all 14 File Station tools with parameters, response shapes, error codes
- `docs/specs/config-schema-spec.md` — YAML config structure, validation rules, env var overrides, state file

## Build & Development Commands
Expand All @@ -40,7 +40,7 @@ uv run pytest # Run unit + module tests
uv run pytest tests/modules/filestation/test_listing.py # Single test file
uv run pytest -k "test_list_shares" # Single test by name
uv run pytest -m integration # Integration tests (requires real NAS)
uv run pytest --cov=synology_mcp # Tests with coverage
uv run pytest --cov=mcp_synology # Tests with coverage
```

## Key Conventions
Expand Down Expand Up @@ -71,7 +71,7 @@ uv run pytest --cov=synology_mcp # Tests with coverage
- **DEBUG**: detailed operational trace — every DSM request/response (passwords masked), credential resolution steps, config discovery, version negotiation, API cache contents, session lifecycle, module registration
- **INFO**: significant lifecycle events only — successful auth, re-auth, security config notes
- **WARNING/ERROR**: configuration issues, failures
- Three ways to enable debug: `synology-mcp check -v` (flag), `SYNOLOGY_LOG_LEVEL=debug` (env var), `logging.level: debug` (config)
- Three ways to enable debug: `mcp-synology check -v` (flag), `SYNOLOGY_LOG_LEVEL=debug` (env var), `logging.level: debug` (config)
- The `setup` and `check` commands accept `-v`/`--verbose`; `serve` uses config/env var (no interactive flag since it's launched by Claude Desktop)
- Logging is initialized *before* config loading so config discovery is visible at debug level

Expand All @@ -85,7 +85,7 @@ uv run pytest --cov=synology_mcp # Tests with coverage
### Auth
- Strategy chain auto-detects 2FA vs non-2FA on login attempt
- Credential lookup: keyring → env vars → config file (last resort, plaintext warning)
- DSM session name format: `SynologyMCP_{instance_id}_{unique_id}`
- DSM session name format: `MCPSynology_{instance_id}_{unique_id}`
- Lazy keepalive (re-auth on next request, no proactive pings)

### DSM API Client
Expand All @@ -97,7 +97,7 @@ uv run pytest --cov=synology_mcp # Tests with coverage

### Config
- Config is read-only from the server's perspective — never write to it
- Runtime state goes in `~/.local/state/synology-mcp/{instance_id}/state.yaml`
- Runtime state goes in `~/.local/state/mcp-synology/{instance_id}/state.yaml`
- Strict validation at top level (unknown keys = error), lenient within module settings (unknown keys = warning)
- Two-phase loading: parse YAML → merge env var overrides → apply defaults → validate with Pydantic

Expand Down
69 changes: 62 additions & 7 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
## Setup

```bash
git clone https://github.com/cmeans/synology-mcp.git
cd synology-mcp
git clone https://github.com/cmeans/mcp-synology.git
cd mcp-synology
uv sync --extra dev # Install dependencies
```

Expand All @@ -16,7 +16,7 @@ uv run ruff format --check src/ tests/ # Format check
uv run ruff format src/ tests/ # Auto-format
uv run mypy src/ # Type check (strict mode)
uv run pytest # Run unit tests (no NAS needed)
uv run pytest --cov=synology_mcp # Tests with coverage
uv run pytest --cov=mcp_synology # Tests with coverage
```

## Integration Tests
Expand All @@ -29,7 +29,7 @@ Integration tests run against a real Synology NAS. They verify the full stack
cp tests/integration_config.yaml.example tests/integration_config.yaml
```

Edit `integration_config.yaml` with your NAS connection details. The file is gitignored — credentials come from the OS keyring (populated by `synology-mcp setup`). For CI, use environment variables (`SYNOLOGY_HOST`, `SYNOLOGY_USERNAME`, `SYNOLOGY_PASSWORD`).
Edit `integration_config.yaml` with your NAS connection details. The file is gitignored — credentials come from the OS keyring (populated by `mcp-synology setup`). For CI, use environment variables (`SYNOLOGY_HOST`, `SYNOLOGY_USERNAME`, `SYNOLOGY_PASSWORD`).

Configure `test_paths` to match folders on your NAS:

Expand All @@ -44,7 +44,7 @@ test_paths:
For tests requiring admin privileges (e.g., resource utilization), point `admin_config` to a config using an admin account:

```yaml
admin_config: ~/.config/synology-mcp/admin.yaml
admin_config: ~/.config/mcp-synology/admin.yaml
```

### Running
Expand All @@ -55,19 +55,74 @@ uv run pytest -m integration -v -k "TestSearch" # Just search tests
uv run pytest -m integration -v -k "TestSystemInfo" # Just system info tests
```

Integration tests are excluded from CI by default (`addopts = "-m 'not integration'"` in `pyproject.toml`).
Integration tests are excluded from CI by default (`addopts` in `pyproject.toml`).

### Known quirks

- **Search service throttling** — DSM's search service on non-indexed shares can be overwhelmed by rapid-fire requests, returning 0 results or 502 errors. Allow recovery time between search-heavy test runs.
- **Background task cleanup** — orphaned Search/DirSize tasks consume CPU indefinitely. The code uses `try/finally` to prevent this, but if tests are interrupted (Ctrl+C), tasks may linger. Check DSM Resource Monitor > Processes for `synoscgi_SYNO.FileStation.Search` entries.

## Virtual-DSM Tests

Virtual-DSM tests run the same integration test suite against a Docker container
running full Synology DSM via QEMU/KVM. This enables testing across multiple DSM
versions without a physical NAS.

### Requirements

- Linux host with KVM support (`/dev/kvm` must exist)
- Docker installed and running
- ~2 GB disk per DSM version (golden images)

macOS is not currently supported — KVM is Linux-only. Use the real NAS integration
tests on macOS.

### First-Time Setup

Each DSM version needs a one-time golden image creation (~15 min):

```bash
uv sync --extra dev --extra vdsm
python scripts/vdsm_setup.py --version 7.2.2
```

The script boots a fresh virtual-dsm container. Complete the DSM setup
wizard in your browser (set admin password, basic storage). The script then
automatically creates test users, shared folders, and seed data via the DSM API.

Golden images are stored in `.vdsm/golden/` (~2 GB each, gitignored).

### Running

```bash
uv run pytest -m vdsm -v --log-cli-level=INFO # Default (7.2.2)
uv run pytest -m vdsm -v --dsm-version 7.1 # Specific version
uv run pytest -m vdsm -v -k "TestSearch" # Single test class
```

### Supported DSM Versions

| Version | Build | PAT |
|---------|-------|-----|
| 7.0.1 | 42218 | VirtualDSM_42218.pat |
| 7.1 | 42661 | VirtualDSM_42661.pat |
| 7.2.1 | 69057 | VirtualDSM_69057.pat |
| 7.2.2 | 72806 | VirtualDSM_72806.pat (default) |
| 7.3.2 | 86009 | VirtualDSM_86009.pat |

### How It Works

1. Restores a golden image (pre-configured DSM) to a temp storage directory
2. Boots the virtual-dsm Docker container (~2 min)
3. Runs the same test functions as the real NAS integration tests
4. Container is stopped and cleaned up automatically

## Design Docs

Detailed specs live in `docs/specs/`. These were the original design documents — the code is authoritative where they diverge (e.g., DSM API version pinning, GET-only requests, and search behavior were discovered during live testing and are documented in `CLAUDE.md`).

- `architecture.md` — layered architecture, auth strategy, session lifecycle
- `filestation-module-spec.md` — all 12 File Station tools
- `filestation-module-spec.md` — all 14 File Station tools (7 READ + 7 WRITE)
- `config-schema-spec.md` — YAML config structure and validation
- `project-scaffolding-spec.md` — repo structure, CI, testing

Expand Down
Loading
Loading