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
71 changes: 36 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@

MCP server for Synology NAS devices. Exposes Synology DSM API functionality as MCP tools that Claude can use.

## Migrating from synology-mcp

If you're upgrading from `synology-mcp` (v0.3.x or earlier), the package has been renamed. A migration script handles config, state, keyring entries, and Claude Desktop config automatically:

```bash
# Download and run the migration script
curl -O https://raw.githubusercontent.com/cmeans/mcp-synology/main/scripts/migrate-from-synology-mcp.py
python migrate-from-synology-mcp.py # dry run — preview changes
python migrate-from-synology-mcp.py --apply # apply changes
```

The script migrates:
- Config directory (`~/.config/synology-mcp/` → `~/.config/mcp-synology/`)
- State directory (`~/.local/state/synology-mcp/` → `~/.local/state/mcp-synology/`)
- Keyring credentials
- Claude Desktop `claude_desktop_config.json` (updates command and paths)

See [CHANGELOG.md](CHANGELOG.md) for full details on breaking changes.

## Supported Modules

### File Station
Expand All @@ -43,34 +62,28 @@ Monitor NAS health and resource utilization. 2 read-only tools:

## Quick Start

### 1. Install
### 1. Run setup

```bash
uv tool install mcp-synology
uvx mcp-synology setup
```

Installs the `mcp-synology` command globally from [PyPI](https://pypi.org/project/mcp-synology/). Requires [uv](https://docs.astral.sh/uv/).

### 2. Run setup

```bash
mcp-synology setup
```
Requires [uv](https://docs.astral.sh/uv/). `uvx` downloads and runs the latest version automatically — no separate install step needed.

Setup will prompt for your NAS host, credentials, and preferences. If your account has 2FA enabled, it will prompt for an OTP code and store a device token for automatic future logins.

At the end, it prints a Claude Desktop JSON snippet ready to copy-paste.

### 3. Add to Claude Desktop
### 2. Add to Claude Desktop

Copy the snippet from setup into your `claude_desktop_config.json` and restart Claude Desktop. It will look something like:

```json
{
"mcpServers": {
"synology-nas": {
"command": "mcp-synology",
"args": ["serve", "--config", "~/.config/mcp-synology/nas.yaml"]
"command": "uvx",
"args": ["mcp-synology", "serve", "--config", "~/.config/mcp-synology/nas.yaml"]
}
}
}
Expand All @@ -80,33 +93,21 @@ The config file name (e.g., `nas.yaml`) also serves as a natural identifier for

On Linux, the server auto-detects the D-Bus session socket for keyring access. If auto-detection fails, add `"env": {"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/<uid>/bus"}` to the Claude Desktop config. The setup command includes this in the generated snippet.

### 4. Verify
### 3. Verify

```bash
mcp-synology check # Validates credentials work
mcp-synology setup --list # Shows all configured NAS instances
uvx mcp-synology check # Validates credentials work
uvx mcp-synology setup --list # Shows all configured NAS instances
```

### Alternative: run without global install
### Alternative: global install

If you prefer not to install globally, `uvx` downloads and runs the latest version on each invocation:

```json
{
"mcpServers": {
"synology": {
"command": "uvx",
"args": ["mcp-synology", "serve", "--config", "~/.config/mcp-synology/config.yaml"]
}
}
}
```

You can also use `uvx` for CLI commands:
If you prefer a persistent install (avoids download on each invocation):

```bash
uvx mcp-synology setup
uvx mcp-synology check
uv tool install mcp-synology
mcp-synology setup
mcp-synology check
```

### Alternative: env-var-only mode
Expand All @@ -117,8 +118,8 @@ No config file needed if `SYNOLOGY_HOST` is set. This is useful for Docker or CI
{
"mcpServers": {
"synology": {
"command": "mcp-synology",
"args": ["serve"],
"command": "uvx",
"args": ["mcp-synology", "serve"],
"env": {
"SYNOLOGY_HOST": "192.168.1.100",
"SYNOLOGY_USERNAME": "your_user",
Expand All @@ -132,7 +133,7 @@ No config file needed if `SYNOLOGY_HOST` is set. This is useful for Docker or CI
Or from the CLI:

```bash
SYNOLOGY_HOST=192.168.1.100 mcp-synology check
SYNOLOGY_HOST=192.168.1.100 uvx mcp-synology check
```

## 2FA Support
Expand Down
130 changes: 125 additions & 5 deletions scripts/migrate-from-synology-mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from __future__ import annotations

import argparse
import json
import shutil
import sys
from pathlib import Path

OLD_NAME = "synology-mcp"
Expand Down Expand Up @@ -134,10 +134,124 @@ def cleanup_keyring(instances: set[str], *, dry_run: bool) -> None:
print(f" ERROR deleting {old_service}/{key}: {e}")


def _find_claude_desktop_config() -> Path | None:
"""Locate claude_desktop_config.json across platforms."""
home = Path.home()
candidates = [
home / ".config" / "Claude" / "claude_desktop_config.json", # Linux
home / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json", # macOS
home / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json", # Windows
]
for path in candidates:
if path.exists():
return path
return None


def migrate_claude_desktop_config(*, dry_run: bool) -> bool:
"""Update Claude Desktop config: replace old synology-mcp references.

Rewrites command/args entries that reference synology-mcp to use
uvx mcp-synology, and updates config paths from synology-mcp to mcp-synology.
Returns True if changes were made (or would be made in dry run).
"""
config_path = _find_claude_desktop_config()
if not config_path:
print(" SKIP claude_desktop_config.json not found")
return False

text = config_path.read_text(encoding="utf-8")
try:
data = json.loads(text)
except json.JSONDecodeError:
print(f" ERROR could not parse {config_path}")
return False

servers = data.get("mcpServers", {})
changed = False
uvx_path = shutil.which("uvx") or "uvx"

for name, entry in servers.items():
args = entry.get("args", [])

# Detect old-style configs: command is synology-mcp, or args contain synology-mcp
cmd = entry.get("command", "")
cmd_name = Path(cmd).name if cmd else ""
is_old_direct = cmd_name == "synology-mcp" and "serve" in args
is_old_uv_run = "run" in args and "synology-mcp" in args

if not is_old_direct and not is_old_uv_run:
continue

# Extract the config path from args (handles both --config /path and --config=/path)
config_arg = None
for i, arg in enumerate(args):
if arg == "--config" and i + 1 < len(args):
config_arg = args[i + 1]
break
if arg.startswith("--config="):
config_arg = arg.split("=", 1)[1]
break

# Update config path references
if config_arg:
config_arg = config_arg.replace("synology-mcp", "mcp-synology")

# Collect extra args (anything that's not the old command structure or --config)
extra_args: list[str] = []
skip_next = False
known_old = {"--directory", "run", "synology-mcp", "mcp-synology", "serve", "--config"}
for arg in args:
if skip_next:
skip_next = False
continue
if arg in known_old:
if arg in ("--directory", "--config"):
skip_next = True # skip the value that follows
continue
if arg.startswith(("--config=", "--directory=")):
continue
extra_args.append(arg)

# Build new entry with uvx
new_args = ["mcp-synology", "serve"]
if config_arg:
new_args.extend(["--config", config_arg])
new_args.extend(extra_args)

old_desc = f"{entry.get('command', '?')} {' '.join(args)}"
new_desc = f"{uvx_path} {' '.join(new_args)}"

if dry_run:
print(f" UPDATE [{name}]")
print(f" old: {old_desc}")
print(f" new: {new_desc}")
if extra_args:
print(f" preserved extra args: {extra_args}")
else:
entry["command"] = uvx_path
entry["args"] = new_args
print(f" UPDATED [{name}] -> {uvx_path} {' '.join(new_args)}")
if extra_args:
print(f" preserved extra args: {extra_args}")

changed = True

if not changed:
print(" OK no synology-mcp references found in Claude Desktop config")
return False

if not dry_run:
backup = config_path.with_suffix(".json.bak")
shutil.copy2(config_path, backup)
config_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
print(f" SAVED {config_path} (backup: {backup})")

return changed


def main() -> None:
parser = argparse.ArgumentParser(
description="Migrate from synology-mcp to mcp-synology"
)
parser = argparse.ArgumentParser(description="Migrate from synology-mcp to mcp-synology")
parser.add_argument(
"--apply",
action="store_true",
Expand Down Expand Up @@ -196,14 +310,20 @@ def main() -> None:
print("\n TIP: Run with --apply --cleanup to remove old keyring entries")
print()

# --- Claude Desktop config ---
print("Claude Desktop:")
desktop_changed = migrate_claude_desktop_config(dry_run=dry_run)
print()

# --- Summary ---
if dry_run:
print("Re-run with --apply to execute these changes.")
else:
print("Migration complete.")
print(f" - Config: {new_config}")
print(f" - State: {new_state}")
print(" - Update Claude Desktop config: change \"synology-mcp\" to \"mcp-synology\"")
if desktop_changed:
print(" - Claude Desktop config updated — restart Claude Desktop")


if __name__ == "__main__":
Expand Down
7 changes: 2 additions & 5 deletions src/mcp_synology/cli/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,14 +387,11 @@ async def _setup_login(config: object, username: str, password: str, service: st

def _emit_claude_desktop_snippet(config: Any, config_path: Path) -> None:
"""Print a Claude Desktop JSON snippet for the user to copy."""
uv_path = shutil.which("uv") or "<path-to-uv>"
uvx_path = shutil.which("uvx") or "<path-to-uvx>"

server_entry: dict[str, Any] = {
"command": uv_path,
"command": uvx_path,
"args": [
"--directory",
str(Path.cwd()),
"run",
"mcp-synology",
"serve",
"--config",
Expand Down
Loading