From 9722e0ee3ad6604ddb0d96f52091b3a038b907c1 Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:22:24 -0500 Subject: [PATCH 1/2] Bump version to 0.5.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minor bump for the structured error responses landed in #9. Version is minor rather than patch because the error wire format is a client-visible behavior change: tool failures now raise ToolError with a JSON envelope (code, message, retryable, suggestion, help_url) and isError=true at the MCP protocol level, rather than returning human-readable strings. Clients keying off isError get proper failure signaling for the first time; clients pattern-matching the old text format ("[!] ... failed:") will need to update. Files touched: - pyproject.toml: 0.4.1 → 0.5.0 - server.json: both version fields bumped (top-level + pypi package) - uv.lock: re-resolved via ``uv lock`` - CHANGELOG.md: 0.5.0 entry documenting the change, the new ErrorCode enum / HELP_URLS registry / docs/error-codes.md drift test, the errno.ENOSPC fix, the unavailable retryable consistency fix, and the new system-module test coverage Local verification on release/v0.5.0: - uv run pytest -q: 312 passed, 94 deselected - uv run ruff check src/ tests/: clean - uv run ruff format --check src/ tests/: clean - uv run mypy src/: no issues found in 27 source files Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- server.json | 4 ++-- uv.lock | 2 +- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d62ff9a..0611d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # Changelog +## 0.5.0 (2026-04-10) + +### Changed + +- **Error responses are now structured JSON envelopes with `isError=true`** (#9) + - Tool errors previously returned human-readable strings like `[!] List files failed: ...`. They now raise `ToolError` with a JSON envelope: + ```json + { + "status": "error", + "error": { + "code": "not_found", + "message": "List files failed (DSM error 408): No such file or directory", + "retryable": false, + "suggestion": "Use list_files or search_files to find the correct path.", + "help_url": "https://github.com/cmeans/mcp-synology/blob/main/docs/error-codes.md#not_found" + } + } + ``` + - The MCP SDK wraps this in a `CallToolResult` with `isError=true`, which is the correct protocol signal for tool failures. Clients that only display text content see the JSON directly; clients that key off `isError` now get proper failure signaling. + - All 13 possible `code` values are documented in [`docs/error-codes.md`](docs/error-codes.md), with per-code sections covering symptoms, causes, retryability, and concrete fixes. + - This is a client-visible behavior change. Any client that was pattern-matching the old `[!] ... failed:` text format will need to update — parse the JSON envelope instead, or key off `isError` at the MCP protocol level. + +### Added + +- **`ErrorCode(StrEnum)` in `core/errors.py`** — single source of truth for every code the server can emit. `error_response(code: ErrorCode)` is typed so call-site typos become mypy errors rather than silent envelopes with missing `help_url`. +- **`docs/error-codes.md`** — 12-section reference covering every surfaceable `ErrorCode` member. Each section has root causes, fix steps with specific DSM control-panel paths, and explicit retryability statements. `session_expired` is intentionally omitted (auto-retried by the core client; never surfaced to users). +- **Multi-invariant drift test** (`tests/core/test_help_urls.py`) — enforces that `ErrorCode` ↔ `HELP_URLS` registry ↔ `docs/error-codes.md` anchors stay in sync in all directions. Adding a new code without its doc section, or renaming a section without updating the registry, fails CI. +- **`errno.ENOSPC` detection** in `download_file` OSError fallback — replaces locale-dependent substring matching on error text, so local disk-full is correctly reported as `disk_full`/`retryable=True` regardless of OS language or DSM version. +- **Unit test coverage** for `modules/system/info.py` and `modules/system/utilization.py` — both modules previously had no unit tests (13% coverage), now at 99–100%. + +### Fixed + +- **`unavailable` `retryable` semantic is now consistent across modules** — `system/utilization.py` previously reported `retryable=False` while `system/info.py` reported `retryable=True` for the same condition ("API responded but returned no data"). Both now use `retryable=True` with an inline comment explaining the transient-condition rationale. +- **`download_file` disk-full is now reported with the same code in both detection paths** — the pre-flight branch (via `shutil.disk_usage`) and the OSError fallback previously disagreed: pre-flight emitted `disk_full`/retryable=True, fallback emitted `filesystem_error`/retryable=False despite a "Free space on the local disk" suggestion. Both now emit `disk_full`/retryable=True when disk-full is the actual cause. +- **`error_response()` is safe against non-JSON-serializable `value` arguments** — `json.dumps(..., default=str)` prevents a future caller passing `bytes` or a custom object from crashing the error handler mid-envelope. + +### Dev + +- Tests: 270 → 312 (+42, including a new `tests/modules/system/` package with 13 tests covering previously-uncovered system-module error paths) +- Patch coverage: 100% on the structured-errors diff per codecov +- `mypy --strict` clean across all 27 source files +- `ruff check` / `ruff format --check` clean + ## 0.4.1 (2026-04-07) ### Fixes diff --git a/pyproject.toml b/pyproject.toml index bdaac01..87fdd24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mcp-synology" -version = "0.4.1" +version = "0.5.0" description = "MCP server for Synology NAS — manage files on your NAS via Claude" readme = "README.md" license = "Apache-2.0" diff --git a/server.json b/server.json index d0c4cf5..bf42b77 100644 --- a/server.json +++ b/server.json @@ -3,7 +3,7 @@ "name": "io.github.cmeans/mcp-synology", "title": "Synology NAS", "description": "MCP server for Synology NAS — browse files, monitor health, and automate operations", - "version": "0.4.1", + "version": "0.5.0", "repository": { "url": "https://github.com/cmeans/mcp-synology", "source": "github" @@ -12,7 +12,7 @@ { "registryType": "pypi", "identifier": "mcp-synology", - "version": "0.4.1", + "version": "0.5.0", "transport": { "type": "stdio" } diff --git a/uv.lock b/uv.lock index a0d9cc7..0c52317 100644 --- a/uv.lock +++ b/uv.lock @@ -726,7 +726,7 @@ wheels = [ [[package]] name = "mcp-synology" -version = "0.4.1" +version = "0.5.0" source = { editable = "." } dependencies = [ { name = "click" }, From 170356373fe7c3ba87eda6088c9a0b2022f37768 Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:45:06 -0500 Subject: [PATCH 2/2] Remove non-standard Dev changelog category (QA Round 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA finding: ``### Dev`` is not a Keep a Changelog category. Standard categories are Added, Changed, Deprecated, Removed, Fixed, Security. Took option (a) from the QA suggestion — delete the section rather than fold its content into Added. Internal metrics (test count deltas, patch coverage percentages, mypy/ruff status) are CI and PR-description concerns, not consumer-facing changelog entries. Operators upgrading from 0.4.1 → 0.5.0 don't need to read them; they care about behavior changes and the migration note, which are already covered under Changed/Added/Fixed. Also fixed the pre-existing ``### Fixes`` → ``### Fixed`` typo in the 0.4.1 entry. QA noted it as a non-blocking pre-existing observation; folding it into this release PR per the project's "any nits should be addressed" convention rather than leaving it for a future cleanup. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0611d77..ecb1875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,16 +36,9 @@ - **`download_file` disk-full is now reported with the same code in both detection paths** — the pre-flight branch (via `shutil.disk_usage`) and the OSError fallback previously disagreed: pre-flight emitted `disk_full`/retryable=True, fallback emitted `filesystem_error`/retryable=False despite a "Free space on the local disk" suggestion. Both now emit `disk_full`/retryable=True when disk-full is the actual cause. - **`error_response()` is safe against non-JSON-serializable `value` arguments** — `json.dumps(..., default=str)` prevents a future caller passing `bytes` or a custom object from crashing the error handler mid-envelope. -### Dev - -- Tests: 270 → 312 (+42, including a new `tests/modules/system/` package with 13 tests covering previously-uncovered system-module error paths) -- Patch coverage: 100% on the structured-errors diff per codecov -- `mypy --strict` clean across all 27 source files -- `ruff check` / `ruff format --check` clean - ## 0.4.1 (2026-04-07) -### Fixes +### Fixed - **Claude Desktop config** — setup snippet now uses `uvx mcp-synology` instead of bare command, which failed with ENOENT on systems where `~/.local/bin` isn't in Claude Desktop's PATH - **Migration script** — now auto-updates `claude_desktop_config.json` (detects and rewrites old synology-mcp entries), creates `.json.bak` backup before writing, preserves extra args, handles `--config=value` equals syntax