diff --git a/CHANGELOG.md b/CHANGELOG.md index cb4f5e7..2696ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. +## [0.4.1] - 2026-05-18 + +### Removed + +- `pause_container` and `unpause_container` (added in 0.4.0) — live + test against DSM revealed that Container Manager on this firmware + does not expose pause/resume at all. The GUI action menu only offers + Start / Stop / Force-Stop / Restart / Reset, and direct calls to + `SYNO.Docker.Container/pause` and `/unpause` return "Method does not + exist". Both tools have been removed rather than left as a broken + surface. The remaining three lifecycle tools (`start_container`, + `stop_container`, `restart_container`) are unaffected. Tool count + drops from 33 to 31. Closes #7 (won't fix — DSM-side limitation). + ## [0.4.0] - 2026-05-18 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index a857d6e..7b0c0d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,12 +33,12 @@ Only a second consecutive failure is treated as a real auth problem. --- -## Implemented tools (33) +## Implemented tools (31) | Category | Tools | |---|---| | Projects | `list_projects`, `get_project_status`, `start_project`, `stop_project`, `redeploy_project`, `create_project`, `delete_project` | -| Containers | `list_containers`, `get_container_status`, `get_container_logs`, `exec_in_container`, `container_stats`, `delete_container`, `start_container`, `stop_container`, `restart_container`, `pause_container`, `unpause_container` | +| Containers | `list_containers`, `get_container_status`, `get_container_logs`, `exec_in_container`, `container_stats`, `delete_container`, `start_container`, `stop_container`, `restart_container` | | Compose | `read_compose`, `update_compose`, `update_image_tag`, `update_env_var` | | Images | `check_image_updates`, `list_images`, `delete_image`, `inspect_image` | | Networks | `list_networks`, `create_network`, `delete_network` | @@ -63,6 +63,11 @@ Only a second consecutive failure is treated as a real auth problem. not available via the DSM WebAPI. - **`SYNO.Docker.Registry/get`** — does not behave as documented; registry listing omitted. +- **`SYNO.Docker.Container/pause` and `/unpause`** — not implemented in + DSM Container Manager on this firmware. The action menu only offers + start/stop/force-stop/restart/reset; calls to `pause`/`unpause` return + "Method does not exist". `pause_container` and `unpause_container` + were briefly shipped in 0.4.0 and removed in 0.4.1. --- @@ -72,7 +77,7 @@ Only a second consecutive failure is treated as a real auth problem. `redeploy_project`, `create_project`, `delete_project`, `exec_in_container`, `update_image_tag`, `update_env_var`, `update_compose`, `delete_container`, `stop_container`, - `restart_container`, `pause_container` + `restart_container` - After compose changes: suggest `redeploy_project` - DSM errors → human-readable message, no stack traces - No secrets in stderr output diff --git a/pyproject.toml b/pyproject.toml index 56b2088..74ac4f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mcp-synology-container" -version = "0.4.0" +version = "0.4.1" description = "MCP server for Synology Container Manager" requires-python = ">=3.12" dependencies = [ diff --git a/src/mcp_synology_container/modules/containers.py b/src/mcp_synology_container/modules/containers.py index 1e39c72..66e3862 100644 --- a/src/mcp_synology_container/modules/containers.py +++ b/src/mcp_synology_container/modules/containers.py @@ -390,44 +390,6 @@ def register_containers(mcp: FastMCP, config: AppConfig, client: DsmClient) -> N except Exception as e: return f"Error restarting container '{container_name}': {e}" - @mcp.tool() - async def pause_container(container_name: str, confirmed: bool = False): - """Pause a running container. Requires confirmed=True.""" - if not confirmed: - return ( - f"Preview: would pause container '{container_name}'.\n" - f"Call this tool again with confirmed=True to proceed." - ) - - resolved_name = await _resolve_container_name(client, container_name) - display_name = _strip_hash_prefix(resolved_name) - try: - await client.request( - "SYNO.Docker.Container", - "pause", - version=1, - params={"name": json.dumps(resolved_name)}, - ) - return f"Paused container '{display_name}'." - except Exception as e: - return f"Error pausing container '{container_name}': {e}" - - @mcp.tool() - async def unpause_container(container_name: str): - """Unpause a paused container.""" - resolved_name = await _resolve_container_name(client, container_name) - display_name = _strip_hash_prefix(resolved_name) - try: - await client.request( - "SYNO.Docker.Container", - "unpause", - version=1, - params={"name": json.dumps(resolved_name)}, - ) - return f"Unpaused container '{display_name}'." - except Exception as e: - return f"Error unpausing container '{container_name}': {e}" - def _container_in_project(container: dict[str, Any], project_name: str) -> bool: """Check if a container belongs to a project based on its labels.""" diff --git a/tests/test_modules/test_containers.py b/tests/test_modules/test_containers.py index 152170e..85e8e7f 100644 --- a/tests/test_modules/test_containers.py +++ b/tests/test_modules/test_containers.py @@ -707,28 +707,26 @@ async def test_container_stats_no_precpu_graceful(): # ────────────────────────────────────────────────────────────────────────────── -# Lifecycle: start / stop / restart / pause / unpause +# Lifecycle: start / stop / restart # ────────────────────────────────────────────────────────────────────────────── # Tools that don't have a confirmation gate _NO_CONFIRM_LIFECYCLE = [ ("start_container", "start", "Started"), - ("unpause_container", "unpause", "Unpaused"), ] # Tools that require confirmed=True _CONFIRM_LIFECYCLE = [ ("stop_container", "stop", "Stopped", "stop"), ("restart_container", "restart", "Restarted", "restart"), - ("pause_container", "pause", "Paused", "pause"), ] @pytest.mark.parametrize("tool_name, dsm_method, success_word", _NO_CONFIRM_LIFECYCLE) @pytest.mark.asyncio async def test_lifecycle_no_confirm_calls_dsm(tool_name, dsm_method, success_word): - """start_container and unpause_container call DSM directly with json-encoded name.""" + """start_container calls DSM directly with json-encoded name.""" from mcp_synology_container.modules.containers import register_containers client = AsyncMock() @@ -753,7 +751,7 @@ async def test_lifecycle_no_confirm_calls_dsm(tool_name, dsm_method, success_wor @pytest.mark.parametrize("tool_name, dsm_method, success_word, _action", _CONFIRM_LIFECYCLE) @pytest.mark.asyncio async def test_lifecycle_confirmed_calls_dsm(tool_name, dsm_method, success_word, _action): - """stop/restart/pause call DSM with confirmed=True using json-encoded name.""" + """stop/restart call DSM with confirmed=True using json-encoded name.""" from mcp_synology_container.modules.containers import register_containers client = AsyncMock() @@ -779,7 +777,7 @@ async def test_lifecycle_confirmed_calls_dsm(tool_name, dsm_method, success_word async def test_lifecycle_preview_without_confirmation( tool_name, _dsm_method, _success_word, action ): - """stop/restart/pause without confirmed=True must NOT call DSM.""" + """stop/restart without confirmed=True must NOT call DSM.""" from mcp_synology_container.modules.containers import register_containers client = AsyncMock() @@ -799,10 +797,8 @@ async def test_lifecycle_preview_without_confirmation( "tool_name, dsm_method, kwargs", [ ("start_container", "start", {}), - ("unpause_container", "unpause", {}), ("stop_container", "stop", {"confirmed": True}), ("restart_container", "restart", {"confirmed": True}), - ("pause_container", "pause", {"confirmed": True}), ], ) @pytest.mark.asyncio @@ -836,10 +832,8 @@ async def test_lifecycle_resolves_hash_prefix(tool_name, dsm_method, kwargs): "tool_name, kwargs, error_verb", [ ("start_container", {}, "starting"), - ("unpause_container", {}, "unpausing"), ("stop_container", {"confirmed": True}, "stopping"), ("restart_container", {"confirmed": True}, "restarting"), - ("pause_container", {"confirmed": True}, "pausing"), ], ) @pytest.mark.asyncio diff --git a/uv.lock b/uv.lock index bd8b111..6eb2e74 100644 --- a/uv.lock +++ b/uv.lock @@ -362,7 +362,7 @@ wheels = [ [[package]] name = "mcp-synology-container" -version = "0.4.0" +version = "0.4.1" source = { editable = "." } dependencies = [ { name = "click" },