From 6df2b2e7a75e441b7a904164eedb89bca64e3438 Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:36:44 -0500 Subject: [PATCH 1/4] Fix Claude Desktop config: use uvx for reliable command resolution The bare `mcp-synology` command fails with ENOENT in Claude Desktop because ~/.local/bin isn't in its PATH. Switch to `uvx mcp-synology` which resolves reliably across platforms. - Setup snippet: use `uvx` instead of `uv --directory ... run` - README snippet: match the setup output - Migration script: auto-detect and update claude_desktop_config.json Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 4 +- scripts/migrate-from-synology-mcp.py | 102 +++++++++++++++++++++++++-- src/mcp_synology/cli/setup.py | 7 +- 3 files changed, 101 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 5a4e65e..cefe4cb 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,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"] } } } diff --git a/scripts/migrate-from-synology-mcp.py b/scripts/migrate-from-synology-mcp.py index 43a505b..a287c75 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,96 @@ 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 + is_old_direct = entry.get("command", "").endswith("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 (follows --config) + config_arg = None + for i, arg in enumerate(args): + if arg == "--config" and i + 1 < len(args): + config_arg = args[i + 1] + break + + # Update config path references + if config_arg: + config_arg = config_arg.replace("synology-mcp", "mcp-synology") + + # Build new entry with uvx + new_args = ["mcp-synology", "serve"] + if config_arg: + new_args.extend(["--config", config_arg]) + + 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}") + else: + entry["command"] = uvx_path + entry["args"] = new_args + print(f" UPDATED [{name}] -> {uvx_path} {' '.join(new_args)}") + + changed = True + + if not changed: + print(" OK no synology-mcp references found in Claude Desktop config") + return False + + if not dry_run: + config_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + print(f" SAVED {config_path}") + + 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 +282,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 +294,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", From e930637a2569fff8d31b162c6dd5307e6dd438e2 Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:51:17 -0500 Subject: [PATCH 2/4] Address QA findings: backup config and preserve extra args - Back up claude_desktop_config.json before modifying (.json.bak) - Preserve extra args (--verbose, --host, etc.) during migration instead of silently dropping them Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/migrate-from-synology-mcp.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/scripts/migrate-from-synology-mcp.py b/scripts/migrate-from-synology-mcp.py index a287c75..e73f89a 100755 --- a/scripts/migrate-from-synology-mcp.py +++ b/scripts/migrate-from-synology-mcp.py @@ -192,10 +192,26 @@ def migrate_claude_desktop_config(*, dry_run: bool) -> bool: 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 + # Skip the --directory value (path) + 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)}" @@ -204,10 +220,14 @@ def migrate_claude_desktop_config(*, dry_run: bool) -> bool: 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 @@ -216,8 +236,10 @@ def migrate_claude_desktop_config(*, dry_run: bool) -> bool: 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}") + print(f" SAVED {config_path} (backup: {backup})") return changed From a0bb0d3ee4b1d65fe596f3b30cc4e87db3fff85d Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:06:18 -0500 Subject: [PATCH 3/4] Add migration section to README, standardize on uvx - Add prominent migration section near top of README for synology-mcp users - Standardize Quick Start on uvx (no separate install step needed) - Move global install to an alternative section - Update all Claude Desktop snippets and CLI examples to use uvx Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 67 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index cefe4cb..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: @@ -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 From 41f543ee7f314113b8b67fa6d851ffb10e17d21a Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:08:25 -0500 Subject: [PATCH 4/4] Fix QA round 3: equals syntax, stale comment, loose match - Handle --config=/path equals syntax in addition to --config /path - Handle --directory=/path equals syntax in extra-args filter - Fix stale "Skip" comment that contradicted the append logic - Use Path().name for exact command name matching instead of endswith Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/migrate-from-synology-mcp.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/migrate-from-synology-mcp.py b/scripts/migrate-from-synology-mcp.py index e73f89a..6378326 100755 --- a/scripts/migrate-from-synology-mcp.py +++ b/scripts/migrate-from-synology-mcp.py @@ -175,18 +175,23 @@ def migrate_claude_desktop_config(*, dry_run: bool) -> bool: args = entry.get("args", []) # Detect old-style configs: command is synology-mcp, or args contain synology-mcp - is_old_direct = entry.get("command", "").endswith("synology-mcp") and "serve" in args + 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 (follows --config) + # 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: @@ -204,7 +209,8 @@ def migrate_claude_desktop_config(*, dry_run: bool) -> bool: if arg in ("--directory", "--config"): skip_next = True # skip the value that follows continue - # Skip the --directory value (path) + if arg.startswith(("--config=", "--directory=")): + continue extra_args.append(arg) # Build new entry with uvx