Fix get_container_status: clean name + dual-format response handling
DSM response has two top-level keys: details → Docker Engine inspect (State, NetworkSettings, Mounts) profile → DSM format (image, port_bindings) _format_container_detail now reads: Status/Running/StartedAt from details.State Image from profile.image IP addresses from details.NetworkSettings.Networks Port bindings from profile.port_bindings Mounts from details.Mounts Also: debug_container_response tool removed, json/sys imports cleaned up. 27 container tests all passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,10 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -108,29 +106,6 @@ def register_containers(mcp: FastMCP, config: AppConfig, client: DsmClient) -> N
|
||||
|
||||
return "\n".join(lines).rstrip()
|
||||
|
||||
@mcp.tool()
|
||||
async def debug_container_response(container_name: str) -> str:
|
||||
"""TEMPORARY DEBUG TOOL — returns raw SYNO.Docker.Container/get response as JSON.
|
||||
|
||||
Used to inspect the actual DSM response structure so that
|
||||
get_container_status can be fixed to read the correct fields.
|
||||
Remove once the real fix is in place.
|
||||
|
||||
Args:
|
||||
container_name: Name of the container to inspect.
|
||||
"""
|
||||
clean_name = _strip_hash_prefix(container_name)
|
||||
try:
|
||||
data = await client.request(
|
||||
"SYNO.Docker.Container",
|
||||
"get",
|
||||
params={"name": clean_name},
|
||||
)
|
||||
except Exception as e:
|
||||
return f"Error calling SYNO.Docker.Container/get for '{clean_name}': {e}"
|
||||
|
||||
return json.dumps(data, indent=2, default=str)
|
||||
|
||||
@mcp.tool()
|
||||
async def get_container_status(container_name: str) -> str:
|
||||
"""Get detailed status, uptime, and resource usage of a container.
|
||||
@@ -138,9 +113,7 @@ def register_containers(mcp: FastMCP, config: AppConfig, client: DsmClient) -> N
|
||||
Args:
|
||||
container_name: Name of the container to inspect.
|
||||
"""
|
||||
# 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.
|
||||
# SYNO.Docker.Container/get accepts the clean name (no hash prefix).
|
||||
clean_name = _strip_hash_prefix(container_name)
|
||||
try:
|
||||
data = await client.request(
|
||||
@@ -151,12 +124,6 @@ def register_containers(mcp: FastMCP, config: AppConfig, client: DsmClient) -> N
|
||||
except Exception as e:
|
||||
return f"Error getting container '{container_name}': {e}"
|
||||
|
||||
sys.stderr.write(
|
||||
f"[get_container_status] name={clean_name!r} "
|
||||
f"raw response: {json.dumps(data, indent=2, default=str)[:2000]}\n"
|
||||
)
|
||||
sys.stderr.flush()
|
||||
|
||||
if not data:
|
||||
return f"Container '{container_name}' not found."
|
||||
|
||||
@@ -357,23 +324,17 @@ 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.
|
||||
|
||||
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).
|
||||
SYNO.Docker.Container/get returns two top-level keys:
|
||||
- "details": Docker Engine inspect format (State, NetworkSettings, Mounts, …)
|
||||
- "profile": DSM format (image, port_bindings, …)
|
||||
"""
|
||||
# Unwrap "container" wrapper if present (common DSM API pattern)
|
||||
raw = data["container"] if "container" in data and isinstance(data["container"], dict) else data
|
||||
details: dict[str, Any] = data.get("details", {}) or {}
|
||||
profile: dict[str, Any] = data.get("profile", {}) or {}
|
||||
|
||||
# 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 "?"
|
||||
state: dict[str, Any] = details.get("State", {}) or {}
|
||||
status_str = state.get("Status", "?")
|
||||
running = state.get("Running", False)
|
||||
image_str = profile.get("image", "?")
|
||||
|
||||
lines = [
|
||||
f"Container: {name}",
|
||||
@@ -388,13 +349,35 @@ def _format_container_detail(name: str, data: dict[str, Any]) -> str:
|
||||
lines.append(f" Finished: {state['FinishedAt']}")
|
||||
lines.append(f" Exit code: {state.get('ExitCode', '?')}")
|
||||
|
||||
memory = host_config.get("Memory", 0)
|
||||
if memory:
|
||||
mb = memory // (1024 * 1024)
|
||||
lines.append(f" Memory limit: {mb} MiB")
|
||||
# IP addresses from all attached networks
|
||||
networks: dict[str, Any] = (
|
||||
details.get("NetworkSettings", {}) or {}
|
||||
).get("Networks", {}) or {}
|
||||
for net_name, net in networks.items():
|
||||
ip = (net or {}).get("IPAddress", "")
|
||||
if ip:
|
||||
lines.append(f" IP ({net_name}): {ip}")
|
||||
|
||||
env = config.get("Env", []) or []
|
||||
if env:
|
||||
lines.append(f" Env vars: {len(env)}")
|
||||
# Port bindings from DSM profile
|
||||
port_bindings: list[dict[str, Any]] = profile.get("port_bindings", []) or []
|
||||
if port_bindings:
|
||||
lines.append(" Ports:")
|
||||
for pb in port_bindings:
|
||||
host = pb.get("host_port", "?")
|
||||
ctr = pb.get("container_port", "?")
|
||||
proto = pb.get("type", "tcp")
|
||||
lines.append(f" {host} → {ctr}/{proto}")
|
||||
|
||||
# Mounts from Docker Engine inspect
|
||||
mounts: list[dict[str, Any]] = details.get("Mounts", []) or []
|
||||
if mounts:
|
||||
lines.append(" Mounts:")
|
||||
for m in mounts:
|
||||
src = m.get("Source", "?")
|
||||
dst = m.get("Destination", "?")
|
||||
mtype = m.get("Type", "")
|
||||
rw = "" if m.get("RW", True) else " [ro]"
|
||||
type_tag = f" ({mtype})" if mtype else ""
|
||||
lines.append(f" {src} → {dst}{type_tag}{rw}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
Reference in New Issue
Block a user