diff --git a/CHANGELOG.md b/CHANGELOG.md index b075ff7..6d7d90e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to this project will be documented in this file. +## [0.5.1] - 2026-05-18 + +### Fixed + +- `pull_image` (#3): the 0.5.0 implementation called + `SYNO.Docker.Registry/pull_start`, which DSM rejects with + "Method does not exist". A live DSM API capture confirmed that + `pull_start` actually lives on `SYNO.Docker.Image`, not + `SYNO.Docker.Registry`. The Registry API only exposes the + synchronous read-only methods (`search`, `tags`, `get/set/create/ + delete`, `using`). Parameters are unchanged — both `repository` + and `tag` are still JSON-encoded — and the `Image/list` polling + for completion detection works as before. Note: #3 is already + closed; this is the follow-up fix. + ## [0.5.0] - 2026-05-18 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index dfce8fb..5d65dcf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,14 +58,19 @@ Only a second consecutive failure is treated as a real auth problem. `SYNO.Docker.Project/list` every 2 s for up to 30 s after issuing start. - **Image delete** — requires a form-encoded POST with a JSON `images` array (confirmed via browser DevTools); uses `DsmClient.post_request()`. -- **`SYNO.Docker.Image/pull`** — API method exists but behaviour varies by - DSM version; not exposed as a standalone tool. `pull_image` uses - `SYNO.Docker.Registry/pull_start` instead (see below). -- **`SYNO.Docker.Registry/pull_start`** — asynchronous pull entry point; - no matching `pull_status` method confirmed. `pull_image` polls - `SYNO.Docker.Image/list` until `repository:tag` appears (2–10 s backoff, - 240 s budget) and returns a "still running" hint on timeout instead of - raising — DSM keeps pulling server-side regardless of the HTTP response. +- **`SYNO.Docker.Image/pull` vs. `pull_start`** — the legacy `pull` method + exists but behaviour varies by DSM version; not exposed as a standalone + tool. `pull_image` uses `SYNO.Docker.Image/pull_start` (asynchronous + pull entry point) with both `repository` and `tag` JSON-encoded. Note + that `pull_start` lives on **`SYNO.Docker.Image`**, NOT on + `SYNO.Docker.Registry` — the Registry API only exposes the synchronous + read-only methods (`search`, `tags`, `get/set/create/delete`, `using`); + calling `Registry/pull_start` returns "Method does not exist". No + matching `pull_status` method is confirmed on either API, so completion + is detected by polling `SYNO.Docker.Image/list` until `repository:tag` + appears (2–10 s backoff, 240 s budget). Timeout returns a "still + running" hint instead of raising — DSM keeps pulling server-side + regardless of the HTTP response. - **`SYNO.Docker.Registry/tags`** — uses `repo` (JSON-encoded) as the parameter name; the n4s4 reference's `name` does not work on this DSM version. Returns the tag list as the envelope's `data` field directly, diff --git a/pyproject.toml b/pyproject.toml index 1dea638..10d6eac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mcp-synology-container" -version = "0.5.0" +version = "0.5.1" description = "MCP server for Synology Container Manager" requires-python = ">=3.12" dependencies = [ diff --git a/src/mcp_synology_container/modules/registry.py b/src/mcp_synology_container/modules/registry.py index 4086a2f..1704045 100644 --- a/src/mcp_synology_container/modules/registry.py +++ b/src/mcp_synology_container/modules/registry.py @@ -169,9 +169,13 @@ def register_registry(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non if await _image_present(client, repository, tag): return f"{target} is already present locally — nothing to pull." + # `pull_start` lives on SYNO.Docker.Image, NOT SYNO.Docker.Registry + # (live DSM capture, confirmed). The Registry API only exposes the + # synchronous read-only methods (search, tags, get/set/create/delete, + # using). Calling Registry/pull_start returns "Method does not exist". try: await client.request( - "SYNO.Docker.Registry", + "SYNO.Docker.Image", "pull_start", version=1, params={ diff --git a/tests/test_modules/test_registry.py b/tests/test_modules/test_registry.py index fd0ee54..c24201f 100644 --- a/tests/test_modules/test_registry.py +++ b/tests/test_modules/test_registry.py @@ -279,7 +279,7 @@ async def test_pull_image_already_present(): result = await tools["pull_image"](repository="nginx", tag="1.24", confirmed=True) assert "already present" in result - # Only the Image/list pre-check was called; pull_start must NOT fire. + # Only the Image/list pre-check was called; no pull_start of any kind. calls = client.request.call_args_list assert all(c.args[1] != "pull_start" for c in calls) @@ -307,7 +307,7 @@ async def test_pull_image_confirmed_success(monkeypatch): ] } return {"images": []} - if api == "SYNO.Docker.Registry" and method == "pull_start": + if api == "SYNO.Docker.Image" and method == "pull_start": state["pulled"] = True return {} raise AssertionError(f"Unexpected call: {api}/{method}") @@ -322,9 +322,11 @@ async def test_pull_image_confirmed_success(monkeypatch): assert "Pulled" in result assert "nginx:1.24" in result - # Verify pull_start was invoked with JSON-encoded params + # Verify pull_start was invoked on SYNO.Docker.Image (not Registry) + # with JSON-encoded params. pull_calls = [c for c in client.request.call_args_list if c.args[1] == "pull_start"] assert len(pull_calls) == 1 + assert pull_calls[0].args[0] == "SYNO.Docker.Image" params = pull_calls[0].kwargs.get("params", {}) assert json.loads(params["repository"]) == "nginx" assert json.loads(params["tag"]) == "1.24" @@ -348,7 +350,7 @@ async def test_pull_image_timeout(monkeypatch): async def mock_request(api, method, **kwargs): if api == "SYNO.Docker.Image" and method == "list": return {"images": []} - if api == "SYNO.Docker.Registry" and method == "pull_start": + if api == "SYNO.Docker.Image" and method == "pull_start": return {} raise AssertionError(f"Unexpected call: {api}/{method}") @@ -371,7 +373,7 @@ async def test_pull_image_start_error(): async def mock_request(api, method, **kwargs): if api == "SYNO.Docker.Image" and method == "list": return {"images": []} - if api == "SYNO.Docker.Registry" and method == "pull_start": + if api == "SYNO.Docker.Image" and method == "pull_start": raise SynologyError("Permission denied", code=105) raise AssertionError(f"Unexpected call: {api}/{method}") diff --git a/uv.lock b/uv.lock index 35a079a..6549658 100644 --- a/uv.lock +++ b/uv.lock @@ -362,7 +362,7 @@ wheels = [ [[package]] name = "mcp-synology-container" -version = "0.5.0" +version = "0.5.1" source = { editable = "." } dependencies = [ { name = "click" },