Fix get_container_status: clean name + dual-format response handling

- get_container_status now strips hash prefix from user input and calls
  SYNO.Docker.Container/get with the clean name (e.g. 'jenkins'), not the
  hash-prefixed form — the get endpoint accepts only the clean name
- _format_container_detail: unwraps 'container' wrapper key if present
  (DSM may return {"container": {State, Config, ...}} at the data level)
- Flat-format fallback: reads lowercase 'status'/'image' fields when
  Docker Engine nested format (State/Config) is absent
- Diagnostic stderr logging for data_keys, unwrap, status, image
- 25 container tests all passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 21:49:30 +02:00
parent c8cda5ef2b
commit 584d53e6e4
2 changed files with 100 additions and 23 deletions
@@ -4,6 +4,7 @@ from __future__ import annotations
import logging
import re
import sys
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
@@ -113,21 +114,28 @@ def register_containers(mcp: FastMCP, config: AppConfig, client: DsmClient) -> N
Args:
container_name: Name of the container to inspect.
"""
resolved_name = await _resolve_container_name(client, container_name)
# Strip hash prefix from user input; SYNO.Docker.Container/get accepts
# the clean name directly — do NOT resolve to hash-prefixed name, as
# the endpoint may not accept that form.
clean_name = _strip_hash_prefix(container_name)
try:
data = await client.request(
"SYNO.Docker.Container",
"get",
params={"name": resolved_name},
params={"name": clean_name},
)
except Exception as e:
return f"Error getting container '{container_name}': {e}"
sys.stderr.write(
f"[get_container_status] name={clean_name!r} data_keys={list(data.keys())}\n"
)
sys.stderr.flush()
if not data:
return f"Container '{container_name}' not found."
display_name = _strip_hash_prefix(resolved_name)
return _format_container_detail(display_name, data)
return _format_container_detail(clean_name, data)
@mcp.tool()
async def get_container_logs(
@@ -322,22 +330,47 @@ def _container_in_project(container: dict[str, Any], project_name: str) -> bool:
def _format_container_detail(name: str, data: dict[str, Any]) -> str:
"""Format container inspect data as human-readable text."""
state = data.get("State", {}) or {}
config = data.get("Config", {}) or {}
host_config = data.get("HostConfig", {}) or {}
"""Format container inspect data as human-readable text.
DSM may return the container object under a "container" key, or directly
at the top level. Supports both the Docker Engine inspect format
(nested State/Config/HostConfig) and the DSM flat format (lowercase
status/image fields as returned by the list endpoint).
"""
# Unwrap "container" wrapper if present (common DSM API pattern)
if "container" in data and isinstance(data["container"], dict):
sys.stderr.write("[get_container_status] unwrapping 'container' key\n")
sys.stderr.flush()
raw = data["container"]
else:
raw = data
# Docker Engine inspect format: State / Config / HostConfig
state = raw.get("State", {}) or {}
config = raw.get("Config", {}) or {}
host_config = raw.get("HostConfig", {}) or {}
# DSM flat format fallback (lowercase fields, same as list response)
status_str = state.get("Status") or raw.get("status") or "?"
running = state.get("Running") if "Running" in state else (status_str == "running")
image_str = config.get("Image") or raw.get("image") or "?"
sys.stderr.write(
f"[get_container_status] status={status_str!r} running={running!r} image={image_str!r}\n"
)
sys.stderr.flush()
lines = [
f"Container: {name}",
f" Status: {state.get('Status', '?')}",
f" Running: {state.get('Running', False)}",
f" Image: {config.get('Image', '?')}",
f" Status: {status_str}",
f" Running: {running}",
f" Image: {image_str}",
]
if state.get("StartedAt"):
lines.append(f" Started: {state.get('StartedAt')}")
if state.get("FinishedAt") and not state.get("Running"):
lines.append(f" Finished: {state.get('FinishedAt')}")
lines.append(f" Started: {state['StartedAt']}")
if state.get("FinishedAt") and not running:
lines.append(f" Finished: {state['FinishedAt']}")
lines.append(f" Exit code: {state.get('ExitCode', '?')}")
memory = host_config.get("Memory", 0)