7bb9b00dcc
New tool inspect_container surfaces the full configuration of a single container as the foundation for a future GUI-container → Compose migration workflow. Output covers image, status, restart policy, network mode + per-network IPs, port bindings, volume mounts, env vars, labels, entrypoint/command, links, and capabilities. Mount paths come from details.Mounts[].Source (full /volume1/... path), NOT from profile.volume_bindings[].host_volume_file — the latter is share-relative (e.g. /docker/foo for /volume1/docker/foo) and not directly Compose-usable. Verified live against the NAS; quirk documented in CLAUDE.md. DSM API: SYNO.Docker.Container/get with name JSON-encoded (action inspect does not exist and returns code 103). Hash-prefixed names are resolved transparently, matching the convention of the other container tools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
148 lines
7.3 KiB
Markdown
148 lines
7.3 KiB
Markdown
# mcp-synology-container
|
||
|
||
## Project
|
||
|
||
`mcp-synology-container` is an MCP server for managing Docker projects on a
|
||
Synology DiskStation via Container Manager. It exposes tools for projects,
|
||
containers, images, compose files, networks, and system housekeeping.
|
||
|
||
---
|
||
|
||
## Tech stack
|
||
|
||
| | |
|
||
|---|---|
|
||
| **Language** | Python 3.12+, `uv` |
|
||
| **Key deps** | MCP SDK, `httpx`, `keyring`, `click`, `rich` |
|
||
| **Compose paths** | `/volume1/docker/<project>/` (default Synology layout) |
|
||
|
||
---
|
||
|
||
## Deploy workflow (after every code change)
|
||
|
||
```
|
||
1. Claude Code commits and pushes
|
||
2. uv tool install --reinstall git+<repo-url>
|
||
3. Restart Claude Desktop (tray icon → Quit → relaunch)
|
||
```
|
||
|
||
**Push retry:** the Gitea remote (`gitea.gecheckt.de`) occasionally
|
||
returns `Unauthorized` on the first push attempt. If `git push` fails
|
||
with an auth error, wait 1 s and retry once before reporting back.
|
||
Only a second consecutive failure is treated as a real auth problem.
|
||
|
||
---
|
||
|
||
## Implemented tools (35)
|
||
|
||
| 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`, `inspect_container`, `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` |
|
||
| Registry | `search_registry`, `list_image_tags`, `pull_image` |
|
||
| Networks | `list_networks`, `create_network`, `delete_network` |
|
||
| System | `system_df`, `system_prune`, `system_overview` |
|
||
|
||
---
|
||
|
||
## DSM API quirks
|
||
|
||
- **Hash-prefixed container names** — DSM sometimes returns names like
|
||
`a1b2c3d4e5f6_myservice` when the compose service name differs from
|
||
`container_name`. All container tools strip this prefix transparently via
|
||
`_strip_hash_prefix` / `_resolve_container_name`.
|
||
- **Async project start** — `SYNO.Docker.Project/start` returns immediately
|
||
while containers are still initialising. `redeploy_project` polls
|
||
`SYNO.Docker.Project/list` every 2 s for up to 30 s after issuing start.
|
||
- **`SYNO.Docker.Project/build_stream`** — returns a streamed plaintext
|
||
build log (content-type `text/html`), one short line per step:
|
||
`Container <name> Running` on success, `<svc> Error` followed by
|
||
`Error response from daemon: <cause>` on failure. The stream closes
|
||
when the build is done. `DsmClient.trigger_build_stream` consumes the
|
||
body line-by-line with a 210 s wall-clock budget (under the Claude
|
||
Desktop ~4 min ceiling) and returns the log as a string; on timeout
|
||
the partial log is returned with a marker appended so callers know
|
||
the build is still running server-side. `redeploy_project` and
|
||
`create_project` grep the returned log for daemon errors and abort
|
||
early — these errors are much more actionable than the eventual
|
||
`BUILD_FAILED` polling status. The log is **live-only**: it cannot
|
||
be re-fetched after the build ends, which is why no standalone
|
||
`get_project_build_log` tool exists.
|
||
- **Image delete** — requires a form-encoded POST with a JSON `images` array
|
||
(confirmed via browser DevTools); uses `DsmClient.post_request()`.
|
||
- **`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,
|
||
not wrapped in a sub-key.
|
||
- **`SYNO.Docker.Volume`** — endpoint does not exist; volume management is
|
||
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.
|
||
- **`SYNO.Docker.Container/get` response — `profile.volume_bindings[].host_volume_file`
|
||
is share-relative, not the full host path.** Live capture against a
|
||
container with bind mount `/volume1/docker/homeassistant:/config`
|
||
returned `host_volume_file = "/docker/homeassistant"` (21 chars,
|
||
share-relative) in `profile`, while `details.Mounts[].Source` carried
|
||
the full `/volume1/docker/homeassistant` and `details.HostConfig.Binds`
|
||
the full `/volume1/docker/homeassistant:/config:rw`. For
|
||
Compose-rebuild use cases the full path is required — `inspect_container`
|
||
reads mount sources from `details.Mounts[].Source`, not from
|
||
`profile.volume_bindings[].host_volume_file`. The DSM action `inspect`
|
||
(no `get`) does not exist (code 103 "Method does not exist"); use `get`.
|
||
|
||
---
|
||
|
||
## Implementation rules
|
||
|
||
- Confirmation required before destructive operations: `stop_project`,
|
||
`redeploy_project`, `create_project`, `delete_project`,
|
||
`exec_in_container`, `update_image_tag`, `update_env_var`,
|
||
`update_compose`, `delete_container`, `stop_container`,
|
||
`restart_container`, `pull_image`
|
||
- After compose changes: suggest `redeploy_project`
|
||
- DSM errors → human-readable message, no stack traces
|
||
- No secrets in stderr output
|
||
- Type hints and docstrings everywhere
|
||
- Formatter: `ruff format` · Linter: `ruff check` · Tests: `pytest`
|
||
- All text (docstrings, comments, README): English
|
||
- **CHANGELOG.md:** every user-visible change (bug fix, new/changed
|
||
tool, behavior change, security fix, dependency bump) gets a
|
||
`CHANGELOG.md` entry in the same commit — under a `## [Unreleased]`
|
||
heading between releases, which becomes `## [X.Y.Z] - YYYY-MM-DD` on
|
||
version bump. Pure internal cleanup (renames without external callers,
|
||
comment-only edits, ruff autofix) needs no entry. Don't ship a release
|
||
with a stale changelog (this was the C-2 gap that caused 0.2.7 and
|
||
0.2.8 to ship undocumented).
|
||
- **Version consistency:** the package version lives in `pyproject.toml`
|
||
and must stay in sync with `uv.lock` and the `[X.Y.Z]` heading in
|
||
`CHANGELOG.md`. `src/mcp_synology_container/__init__.py` derives
|
||
`__version__` from `importlib.metadata` and is never hand-edited.
|
||
Every version bump touches all three files in the same commit.
|
||
|
||
---
|
||
|
||
## DSM API reference
|
||
|
||
- `cmeans/mcp-synology` (GitHub) — auth, keyring, CLI structure
|
||
- `N4S4/synology-api` `docker_api.py` (GitHub) — `SYNO.Docker.*` calls
|