fix: v0.5.1 — pull_image API (Image/pull_start, not Registry/pull_start)
The 0.5.0 prompt mis-attributed the API: pull_start lives on SYNO.Docker.Image, not SYNO.Docker.Registry (live DSM capture). search and tags ARE correctly on Registry; only pull_start belongs to Image. Registry/pull_start returns "Method does not exist". Parameters are unchanged (repository + tag both JSON-encoded), and the Image/list polling for completion detection is untouched. Tests updated to assert SYNO.Docker.Image/pull_start. CLAUDE.md DSM-quirks section consolidates the Image vs. Registry split so this trap is documented for future surface additions. References #3 (already closed). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,21 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
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
|
## [0.5.0] - 2026-05-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -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.
|
`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
|
- **Image delete** — requires a form-encoded POST with a JSON `images` array
|
||||||
(confirmed via browser DevTools); uses `DsmClient.post_request()`.
|
(confirmed via browser DevTools); uses `DsmClient.post_request()`.
|
||||||
- **`SYNO.Docker.Image/pull`** — API method exists but behaviour varies by
|
- **`SYNO.Docker.Image/pull` vs. `pull_start`** — the legacy `pull` method
|
||||||
DSM version; not exposed as a standalone tool. `pull_image` uses
|
exists but behaviour varies by DSM version; not exposed as a standalone
|
||||||
`SYNO.Docker.Registry/pull_start` instead (see below).
|
tool. `pull_image` uses `SYNO.Docker.Image/pull_start` (asynchronous
|
||||||
- **`SYNO.Docker.Registry/pull_start`** — asynchronous pull entry point;
|
pull entry point) with both `repository` and `tag` JSON-encoded. Note
|
||||||
no matching `pull_status` method confirmed. `pull_image` polls
|
that `pull_start` lives on **`SYNO.Docker.Image`**, NOT on
|
||||||
`SYNO.Docker.Image/list` until `repository:tag` appears (2–10 s backoff,
|
`SYNO.Docker.Registry` — the Registry API only exposes the synchronous
|
||||||
240 s budget) and returns a "still running" hint on timeout instead of
|
read-only methods (`search`, `tags`, `get/set/create/delete`, `using`);
|
||||||
raising — DSM keeps pulling server-side regardless of the HTTP response.
|
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
|
- **`SYNO.Docker.Registry/tags`** — uses `repo` (JSON-encoded) as the
|
||||||
parameter name; the n4s4 reference's `name` does not work on this DSM
|
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,
|
version. Returns the tag list as the envelope's `data` field directly,
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "mcp-synology-container"
|
name = "mcp-synology-container"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
description = "MCP server for Synology Container Manager"
|
description = "MCP server for Synology Container Manager"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -169,9 +169,13 @@ def register_registry(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non
|
|||||||
if await _image_present(client, repository, tag):
|
if await _image_present(client, repository, tag):
|
||||||
return f"{target} is already present locally — nothing to pull."
|
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:
|
try:
|
||||||
await client.request(
|
await client.request(
|
||||||
"SYNO.Docker.Registry",
|
"SYNO.Docker.Image",
|
||||||
"pull_start",
|
"pull_start",
|
||||||
version=1,
|
version=1,
|
||||||
params={
|
params={
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ async def test_pull_image_already_present():
|
|||||||
result = await tools["pull_image"](repository="nginx", tag="1.24", confirmed=True)
|
result = await tools["pull_image"](repository="nginx", tag="1.24", confirmed=True)
|
||||||
assert "already present" in result
|
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
|
calls = client.request.call_args_list
|
||||||
assert all(c.args[1] != "pull_start" for c in calls)
|
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": []}
|
return {"images": []}
|
||||||
if api == "SYNO.Docker.Registry" and method == "pull_start":
|
if api == "SYNO.Docker.Image" and method == "pull_start":
|
||||||
state["pulled"] = True
|
state["pulled"] = True
|
||||||
return {}
|
return {}
|
||||||
raise AssertionError(f"Unexpected call: {api}/{method}")
|
raise AssertionError(f"Unexpected call: {api}/{method}")
|
||||||
@@ -322,9 +322,11 @@ async def test_pull_image_confirmed_success(monkeypatch):
|
|||||||
assert "Pulled" in result
|
assert "Pulled" in result
|
||||||
assert "nginx:1.24" 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"]
|
pull_calls = [c for c in client.request.call_args_list if c.args[1] == "pull_start"]
|
||||||
assert len(pull_calls) == 1
|
assert len(pull_calls) == 1
|
||||||
|
assert pull_calls[0].args[0] == "SYNO.Docker.Image"
|
||||||
params = pull_calls[0].kwargs.get("params", {})
|
params = pull_calls[0].kwargs.get("params", {})
|
||||||
assert json.loads(params["repository"]) == "nginx"
|
assert json.loads(params["repository"]) == "nginx"
|
||||||
assert json.loads(params["tag"]) == "1.24"
|
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):
|
async def mock_request(api, method, **kwargs):
|
||||||
if api == "SYNO.Docker.Image" and method == "list":
|
if api == "SYNO.Docker.Image" and method == "list":
|
||||||
return {"images": []}
|
return {"images": []}
|
||||||
if api == "SYNO.Docker.Registry" and method == "pull_start":
|
if api == "SYNO.Docker.Image" and method == "pull_start":
|
||||||
return {}
|
return {}
|
||||||
raise AssertionError(f"Unexpected call: {api}/{method}")
|
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):
|
async def mock_request(api, method, **kwargs):
|
||||||
if api == "SYNO.Docker.Image" and method == "list":
|
if api == "SYNO.Docker.Image" and method == "list":
|
||||||
return {"images": []}
|
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 SynologyError("Permission denied", code=105)
|
||||||
raise AssertionError(f"Unexpected call: {api}/{method}")
|
raise AssertionError(f"Unexpected call: {api}/{method}")
|
||||||
|
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mcp-synology-container"
|
name = "mcp-synology-container"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
|
|||||||
Reference in New Issue
Block a user