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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Changed

- **Structured error envelopes now include `param`/`value` on five more call sites** (#29) — addresses F17 from the PR #9 self-audit. Five `error_response()` call sites in `modules/filestation/*.py` previously emitted envelopes without the `param`/`value` fields that smart clients could dispatch on: `metadata.py:76` (`get_file_info` multi-path empty-result → `param="paths"`, `value=<paths>`), `metadata.py:222` (`get_dir_size` timeout → `param="timeout"`, `value=<timeout>`), `operations.py:245` (copy/move timeout), `operations.py:353` (delete timeout), and `transfer.py:209` (download local-write `OSError` → `param="dest_folder"`, `value=<dest_folder>`). Existing `message` text is unchanged. Regression assertions added to `test_empty_files_list_returns_not_found`, `test_dir_size_timeout`, `test_copy_timeout`, `test_delete_timeout`, and `test_download_write_permission_error`.

### Added

- **Auto-publish to MCP registry on release** (#27) — adds a `publish-registry` job to `.github/workflows/publish.yml` that runs after `publish-pypi` and publishes `server.json` to `registry.modelcontextprotocol.io` via [`mcp-publisher`](https://github.com/modelcontextprotocol/registry). Uses GitHub OIDC authentication (`permissions: id-token: write`) so no long-lived registry token is needed. The step is idempotent — if the tag's workflow is re-run after a successful registry publish, the duplicate-version error (registry requires every version string to be unique per [its versioning docs](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/versioning.mdx)) is caught and downgraded to a `::warning::` rather than failing the release. Also adds a `validate-server-json` job to `.github/workflows/ci.yml` that runs `mcp-publisher validate server.json` on every PR so schema breakage is caught before a tag push, complementing the existing `version-sync` check which enforces alignment with `pyproject.toml`.
Expand Down
4 changes: 4 additions & 0 deletions src/mcp_synology/modules/filestation/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ async def get_file_info(
ErrorCode.NOT_FOUND,
"Get file info failed: No file information returned.",
retryable=False,
param="paths",
value=paths,
suggestion="Check that the paths exist.",
)

Expand Down Expand Up @@ -221,5 +223,7 @@ async def get_dir_size(
ErrorCode.TIMEOUT,
f"Get directory size failed: timed out after {timeout}s.",
retryable=True,
param="timeout",
value=timeout,
suggestion="The directory may be very large. Try a subdirectory.",
)
4 changes: 4 additions & 0 deletions src/mcp_synology/modules/filestation/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ async def _copy_move(
ErrorCode.TIMEOUT,
f"{operation} files failed: timed out after {timeout}s.",
retryable=True,
param="timeout",
value=timeout,
suggestion="The operation may still be running on the NAS.",
)

Expand Down Expand Up @@ -350,6 +352,8 @@ async def delete_files(
ErrorCode.TIMEOUT,
f"Delete files failed: timed out after {timeout}s.",
retryable=True,
param="timeout",
value=timeout,
suggestion="The operation may still be running on the NAS.",
)

Expand Down
2 changes: 2 additions & 0 deletions src/mcp_synology/modules/filestation/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ async def download_file(
ErrorCode.FILESYSTEM_ERROR,
f"Download failed: Failed to write local file: {e}",
retryable=False,
param="dest_folder",
value=dest_folder,
suggestion=(
"The filename may contain characters not allowed on this OS. "
"Use the filename parameter to specify a compatible name."
Expand Down
5 changes: 5 additions & 0 deletions tests/modules/filestation/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ async def test_empty_files_list_returns_not_found(self, mock_client: DsmClient)
body = json.loads(str(exc_info.value))
assert body["error"]["code"] == "not_found"
assert body["error"]["retryable"] is False
# F17: envelope names the offending argument so clients can dispatch
assert body["error"]["param"] == "paths"
assert body["error"]["value"] == ["/video/missing1", "/video/missing2"]


class TestGetDirSize:
Expand Down Expand Up @@ -228,3 +231,5 @@ def side_effect(request: httpx.Request) -> httpx.Response:
body = json.loads(str(exc_info.value))
assert body["error"]["code"] == "timeout"
assert body["error"]["retryable"] is True
assert body["error"]["param"] == "timeout"
assert body["error"]["value"] == 1.0
4 changes: 4 additions & 0 deletions tests/modules/filestation/test_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ def side_effect(request: httpx.Request) -> httpx.Response:
assert body["error"]["code"] == "timeout"
assert body["error"]["retryable"] is True
assert "Copy files" in body["error"]["message"]
assert body["error"]["param"] == "timeout"
assert body["error"]["value"] == 1.0

@respx.mock
async def test_copy_task_completes_with_error(self, mock_client: DsmClient) -> None:
Expand Down Expand Up @@ -343,6 +345,8 @@ def side_effect(request: httpx.Request) -> httpx.Response:
assert body["error"]["code"] == "timeout"
assert body["error"]["retryable"] is True
assert "Delete files" in body["error"]["message"]
assert body["error"]["param"] == "timeout"
assert body["error"]["value"] == 1.0

@respx.mock
async def test_delete_poll_error_mid_operation(self, mock_client: DsmClient) -> None:
Expand Down
2 changes: 2 additions & 0 deletions tests/modules/filestation/test_transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ async def test_download_write_permission_error(
body = json.loads(str(exc_info.value))
assert body["status"] == "error"
assert body["error"]["code"] == "filesystem_error"
assert body["error"]["param"] == "dest_folder"
assert body["error"]["value"] == str(readonly_dir)
finally:
# Restore permissions for cleanup
readonly_dir.chmod(0o755)
Expand Down
Loading