Add pull_image + list_registries; remove Gruppe 5 (no Volume API)

- pull_image: SYNO.Docker.Image/pull with repository+tag split via
  rpartition; polls image list every 3 s until image appears, 120 s timeout
- list_registries: SYNO.Docker.Registry/get; shows name, URL, active marker
- Gruppe 5 (Volumes) removed from roadmap — SYNO.Docker.Volume does not exist
- CLAUDE.md: tool count 17 → 19, Volumes section removed
- 28 tests all passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 19:28:45 +02:00
parent 59f7fc1d6c
commit 5fe8f5bc73
6 changed files with 421 additions and 46 deletions
+52 -1
View File
@@ -1,7 +1,8 @@
"""MCP tools for SYNO.Docker.Image: list, check updates, delete."""
"""MCP tools for SYNO.Docker.Image: list, check updates, delete, pull."""
from __future__ import annotations
import asyncio
import json
import logging
import sys
@@ -269,6 +270,56 @@ def register_images(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None:
return f"Deleted {display_name}{size_str} freed."
@mcp.tool()
async def pull_image(image: str) -> str:
"""Pull a Docker image from the active registry.
Splits the image reference into repository and tag, triggers the pull
via DSM, then polls the image list until the image appears (up to 120 s).
Args:
image: Image reference as "name:tag" (e.g. "postgres:17.8").
Tag defaults to "latest" when omitted.
"""
repository, sep, tag = image.rpartition(":")
if not sep:
repository = image
tag = "latest"
try:
await client.request(
"SYNO.Docker.Image",
"pull",
params={"repository": repository, "tag": tag},
)
except Exception as e:
return f"Error starting pull of '{image}': {e}"
# DSM starts the pull asynchronously; poll until the image appears.
deadline = 120
interval = 3
elapsed = 0
while elapsed < deadline:
await asyncio.sleep(interval)
elapsed += interval
try:
img_data = await client.request(
"SYNO.Docker.Image",
"list",
params={"limit": "-1", "offset": "0", "show_dsm": "false"},
)
except Exception:
continue
for img in img_data.get("images", []):
if img.get("repository") == repository and tag in (img.get("tags") or []):
size_str = _human_size(img.get("size", 0))
return f"Pulled {repository}:{tag}{size_str}."
return (
f"Pull of '{repository}:{tag}' started but did not complete within "
f"{deadline} s. Check DSM Container Manager for status."
)
@mcp.tool()
async def check_image_updates(project_name: str | None = None) -> str:
"""Check for available image updates for a project or all images.
@@ -0,0 +1,49 @@
"""MCP tools for SYNO.Docker.Registry: list."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from mcp.server.fastmcp import FastMCP
from mcp_synology_container.config import AppConfig
from mcp_synology_container.dsm_client import DsmClient
logger = logging.getLogger(__name__)
def register_registries(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None:
"""Register all registry management tools with the MCP server."""
@mcp.tool()
async def list_registries() -> str:
"""List all configured Docker registries.
Shows name, URL, and marks the currently active registry.
Uses SYNO.Docker.Registry/get which returns the registries array
and the name of the currently active registry in the "using" field.
"""
try:
data = await client.request("SYNO.Docker.Registry", "get")
except Exception as e:
return f"Error listing registries: {e}"
registries = data.get("registries", [])
using = data.get("using", "")
if not registries:
return "No registries configured."
lines = [f"Registries ({len(registries)} total):", ""]
for reg in registries:
name = reg.get("name", "?")
url = reg.get("url", "?")
active_marker = " [active]" if name == using else ""
mirror_marker = " [mirror enabled]" if reg.get("enable_registry_mirror") else ""
lines.append(f" {name}{active_marker}")
lines.append(f" URL: {url}{mirror_marker}")
lines.append("")
return "\n".join(lines).rstrip()
+2
View File
@@ -31,6 +31,7 @@ def create_server(config: AppConfig, client: DsmClient) -> FastMCP:
from mcp_synology_container.modules.images import register_images
from mcp_synology_container.modules.networks import register_networks
from mcp_synology_container.modules.projects import register_projects
from mcp_synology_container.modules.registries import register_registries
from mcp_synology_container.modules.system import register_system
register_projects(mcp, config, client)
@@ -39,6 +40,7 @@ def create_server(config: AppConfig, client: DsmClient) -> FastMCP:
register_images(mcp, config, client)
register_system(mcp, config, client)
register_networks(mcp, config, client)
register_registries(mcp, config, client)
logger.info("MCP server configured with all tool modules")
return mcp