diff --git a/README.md b/README.md index 5a4e65e..7020c77 100644 --- a/README.md +++ b/README.md @@ -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 @@ -43,25 +62,19 @@ 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: @@ -69,8 +82,8 @@ Copy the snippet from setup into your `claude_desktop_config.json` and restart C { "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"] } } } @@ -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//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 @@ -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", @@ -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 diff --git a/scripts/migrate-from-synology-mcp.py b/scripts/migrate-from-synology-mcp.py index 43a505b..6378326 100755 --- a/scripts/migrate-from-synology-mcp.py +++ b/scripts/migrate-from-synology-mcp.py @@ -12,8 +12,8 @@ from __future__ import annotations import argparse +import json import shutil -import sys from pathlib import Path OLD_NAME = "synology-mcp" @@ -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", @@ -196,6 +310,11 @@ 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.") @@ -203,7 +322,8 @@ def main() -> None: 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__": diff --git a/src/mcp_synology/cli/setup.py b/src/mcp_synology/cli/setup.py index 3bdc106..0c3c98c 100644 --- a/src/mcp_synology/cli/setup.py +++ b/src/mcp_synology/cli/setup.py @@ -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 "" + uvx_path = shutil.which("uvx") or "" server_entry: dict[str, Any] = { - "command": uv_path, + "command": uvx_path, "args": [ - "--directory", - str(Path.cwd()), - "run", "mcp-synology", "serve", "--config",