Initial implementation
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
"""MCP tools for SYNO.Docker.Image: list and check for updates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
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_images(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None:
|
||||
"""Register all image management tools with the MCP server."""
|
||||
|
||||
@mcp.tool()
|
||||
async def check_image_updates(project_name: str | None = None) -> str:
|
||||
"""Check for available image updates for a project or all images.
|
||||
|
||||
Queries the local image list and reports which images have the
|
||||
'upgradable' flag set by the NAS registry check.
|
||||
|
||||
Args:
|
||||
project_name: Optional project name to filter images.
|
||||
If omitted, checks all locally available images.
|
||||
"""
|
||||
try:
|
||||
data = await client.request(
|
||||
"SYNO.Docker.Image",
|
||||
"list",
|
||||
params={"limit": "-1", "offset": "0", "show_dsm": "false"},
|
||||
)
|
||||
except Exception as e:
|
||||
return f"Error listing images: {e}"
|
||||
|
||||
images: list[dict[str, Any]] = data.get("images", [])
|
||||
if not images:
|
||||
return "No images found."
|
||||
|
||||
# If project_name given, cross-reference with project containers
|
||||
if project_name:
|
||||
images = await _filter_images_for_project(client, project_name, images)
|
||||
if not images:
|
||||
return f"No images found for project '{project_name}'."
|
||||
header = f"Image update status for project '{project_name}':\n"
|
||||
else:
|
||||
header = f"Image update status (all {len(images)} images):\n"
|
||||
|
||||
upgradable = [img for img in images if img.get("upgradable")]
|
||||
up_to_date = [img for img in images if not img.get("upgradable")]
|
||||
|
||||
lines = [header]
|
||||
|
||||
if upgradable:
|
||||
lines.append(f"Updates available ({len(upgradable)}):")
|
||||
for img in sorted(upgradable, key=lambda x: x.get("repository", "")):
|
||||
repo = img.get("repository", "?")
|
||||
tags = ", ".join(img.get("tags", []))
|
||||
size_mb = img.get("size", 0) // (1024 * 1024)
|
||||
lines.append(f" {repo}:{tags} ({size_mb} MiB) ← UPDATE AVAILABLE")
|
||||
lines.append("")
|
||||
|
||||
if up_to_date:
|
||||
lines.append(f"Up to date ({len(up_to_date)}):")
|
||||
for img in sorted(up_to_date, key=lambda x: x.get("repository", "")):
|
||||
repo = img.get("repository", "?")
|
||||
tags = ", ".join(img.get("tags", []))
|
||||
size_mb = img.get("size", 0) // (1024 * 1024)
|
||||
lines.append(f" {repo}:{tags} ({size_mb} MiB)")
|
||||
|
||||
if not upgradable:
|
||||
lines.append("All images are up to date.")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def _filter_images_for_project(
|
||||
client: DsmClient,
|
||||
project_name: str,
|
||||
all_images: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Filter images to those used by a specific project.
|
||||
|
||||
Fetches project details and cross-references container image IDs.
|
||||
Falls back to name-based matching if project details unavailable.
|
||||
|
||||
Args:
|
||||
client: DsmClient instance.
|
||||
project_name: Project name to filter for.
|
||||
all_images: Full list of images from SYNO.Docker.Image.
|
||||
|
||||
Returns:
|
||||
Subset of images used by the project.
|
||||
"""
|
||||
# Get project details to find used images
|
||||
try:
|
||||
# Find project by name
|
||||
list_data = await client.request("SYNO.Docker.Project", "list")
|
||||
projects: dict[str, Any] = list_data if isinstance(list_data, dict) else {}
|
||||
project_entry = next(
|
||||
(p for p in projects.values() if p.get("name") == project_name), None
|
||||
)
|
||||
|
||||
if not project_entry:
|
||||
return []
|
||||
|
||||
project_id = project_entry.get("id", "")
|
||||
# Get project detail which includes container image info
|
||||
detail_data = await client.request(
|
||||
"SYNO.Docker.Project",
|
||||
"get",
|
||||
params={"id": project_id},
|
||||
)
|
||||
|
||||
containers = detail_data.get("containers", []) or []
|
||||
image_ids: set[str] = set()
|
||||
image_names: set[str] = set()
|
||||
|
||||
for container in containers:
|
||||
img_id = container.get("Image", "")
|
||||
if img_id:
|
||||
image_ids.add(img_id)
|
||||
cfg_image = container.get("Config", {}).get("Image", "")
|
||||
if cfg_image:
|
||||
# Strip tag for name matching
|
||||
name = cfg_image.split(":")[0] if ":" in cfg_image else cfg_image
|
||||
image_names.add(name)
|
||||
|
||||
# Match images
|
||||
result = []
|
||||
for img in all_images:
|
||||
img_id = img.get("id", "")
|
||||
repo = img.get("repository", "")
|
||||
if img_id in image_ids or repo in image_names:
|
||||
result.append(img)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.debug("Could not filter images for project '%s': %s", project_name, e)
|
||||
# Fallback: return all images
|
||||
return all_images
|
||||
Reference in New Issue
Block a user