From ad199674e715caf0ea2b5fb9de3a90ba95228024 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Tue, 21 Apr 2026 09:04:17 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20v0.2.7=20=E2=80=94=20remove=20->=20str?= =?UTF-8?q?=20annotations=20and=20trim=20docstrings=20to=20reduce=20tools/?= =?UTF-8?q?list=20payload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FastMCP generates outputSchema for every tool with a return annotation, roughly doubling the tools/list payload size. Multi-line docstrings with Args/Returns sections add further bulk that Claude Desktop must parse. - Strip -> str from all 23 @mcp.tool() functions - Trim every tool docstring to a single descriptive line (≤100 chars) Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 2 +- src/mcp_synology_container/modules/compose.py | 51 +++------------ .../modules/containers.py | 63 ++++--------------- src/mcp_synology_container/modules/images.py | 34 ++-------- .../modules/networks.py | 27 ++------ .../modules/projects.py | 58 +++-------------- src/mcp_synology_container/modules/system.py | 21 ++----- 7 files changed, 46 insertions(+), 210 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 16c2628..35aa05f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mcp-synology-container" -version = "0.2.6" +version = "0.2.7" description = "MCP server for Synology Container Manager" requires-python = ">=3.12" dependencies = [ diff --git a/src/mcp_synology_container/modules/compose.py b/src/mcp_synology_container/modules/compose.py index aa97bb9..d627ce4 100644 --- a/src/mcp_synology_container/modules/compose.py +++ b/src/mcp_synology_container/modules/compose.py @@ -66,15 +66,8 @@ def register_compose(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None """Register all compose file management tools with the MCP server.""" @mcp.tool() - async def read_compose(project_name: str) -> str: - """Read the compose file of a project. - - Args: - project_name: Name of the Container Manager project. - - Returns: - The compose file content as YAML text. - """ + async def read_compose(project_name: str): + """Read the compose file (YAML) for a project.""" path = await _find_compose_path(client, config, project_name) if path is None: project = await _find_project(client, project_name) @@ -102,17 +95,8 @@ def register_compose(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None service_name: str, new_tag: str, confirmed: bool = False, - ) -> str: - """Update the image tag of a service in the compose file. - - After confirming, suggests running redeploy_project. - - Args: - project_name: Name of the Container Manager project. - service_name: Name of the service within the compose file. - new_tag: New image tag (e.g. "latest", "1.2.3"). - confirmed: Must be True to proceed. Set to True to confirm the change. - """ + ): + """Update a service's image tag in the compose file. Requires confirmed=True.""" path = await _find_compose_path(client, config, project_name) if path is None: return f"No compose file found for project '{project_name}'." @@ -230,18 +214,8 @@ def register_compose(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None var_name: str, var_value: str, confirmed: bool = False, - ) -> str: - """Add or update an environment variable in a service's compose definition. - - After confirming, suggests running redeploy_project. - - Args: - project_name: Name of the Container Manager project. - service_name: Name of the service within the compose file. - var_name: Environment variable name. - var_value: New value for the variable. - confirmed: Must be True to proceed. Set to True to confirm the change. - """ + ): + """Add or update an env var in a service's compose definition. Requires confirmed=True.""" path = await _find_compose_path(client, config, project_name) if path is None: return f"No compose file found for project '{project_name}'." @@ -333,17 +307,8 @@ def register_compose(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None project_name: str, new_content: str, confirmed: bool = False, - ) -> str: - """Replace the entire compose file with new content. - - Validates that the content is valid YAML before writing. - After confirming, suggests running redeploy_project. - - Args: - project_name: Name of the Container Manager project. - new_content: Complete new content for the compose file (must be valid YAML). - confirmed: Must be True to proceed. Set to True to confirm the overwrite. - """ + ): + """Replace the entire compose file with new YAML content. Requires confirmed=True.""" # Validate YAML before anything else try: parsed = yaml.safe_load(new_content) diff --git a/src/mcp_synology_container/modules/containers.py b/src/mcp_synology_container/modules/containers.py index 0decf18..e30e382 100644 --- a/src/mcp_synology_container/modules/containers.py +++ b/src/mcp_synology_container/modules/containers.py @@ -64,13 +64,8 @@ def register_containers(mcp: FastMCP, config: AppConfig, client: DsmClient) -> N """Register all container management tools with the MCP server.""" @mcp.tool() - async def list_containers(project_name: str | None = None) -> str: - """List containers, optionally filtered by project name. - - Args: - project_name: Optional project name to filter containers. - If omitted, lists all containers. - """ + async def list_containers(project_name: str | None = None): + """List all containers, optionally filtered by project name.""" try: data = await client.request( "SYNO.Docker.Container", @@ -107,12 +102,8 @@ def register_containers(mcp: FastMCP, config: AppConfig, client: DsmClient) -> N return "\n".join(lines).rstrip() @mcp.tool() - async def get_container_status(container_name: str) -> str: - """Get detailed status, uptime, and resource usage of a container. - - Args: - container_name: Name of the container to inspect. - """ + async def get_container_status(container_name: str): + """Get detailed status, uptime, ports, and mounts for a container.""" # SYNO.Docker.Container/get accepts the clean name (no hash prefix). clean_name = _strip_hash_prefix(container_name) try: @@ -134,14 +125,8 @@ def register_containers(mcp: FastMCP, config: AppConfig, client: DsmClient) -> N container_name: str, tail: int = 100, keyword: str | None = None, - ) -> str: - """Get log output from a container. - - Args: - container_name: Name of the container. - tail: Number of recent log lines to return (default 100). - keyword: Optional keyword to filter log lines. - """ + ): + """Get recent log output from a container, with optional keyword filter.""" resolved_name = await _resolve_container_name(client, container_name) params: dict[str, Any] = { "name": resolved_name, @@ -181,15 +166,8 @@ def register_containers(mcp: FastMCP, config: AppConfig, client: DsmClient) -> N return header + "\n".join(lines) @mcp.tool() - async def container_stats(container_name: str) -> str: - """Get live resource usage statistics for a container. - - Reports CPU %, RAM used/limit, Network I/O (rx/tx), and Block I/O - (read/write) for the named container. - - Args: - container_name: Name of the container (e.g. "jenkins"). - """ + async def container_stats(container_name: str): + """Get live CPU, memory, network, and block I/O stats for a container.""" try: data = await client.request("SYNO.Docker.Container", "stats") except Exception as e: @@ -270,17 +248,8 @@ def register_containers(mcp: FastMCP, config: AppConfig, client: DsmClient) -> N container_name: str, command: str, confirmed: bool = False, - ) -> str: - """Execute a command in a running container. - - This executes a shell command inside the container. Use with caution. - Requires confirmation before executing. - - Args: - container_name: Name of the container. - command: Shell command to execute. - confirmed: Must be True to proceed. Set to True to confirm execution. - """ + ): + """Execute a shell command inside a running container. Requires confirmed=True.""" if not confirmed: return ( f"About to run in container '{container_name}':\n" @@ -313,16 +282,8 @@ def register_containers(mcp: FastMCP, config: AppConfig, client: DsmClient) -> N return "\n".join(result_lines) @mcp.tool() - async def delete_container(container_name: str, confirmed: bool = False) -> str: - """Delete a container. - - Container must be stopped before deletion. Without confirmed=True, - returns a preview only. - - Args: - container_name: Name of the container. - confirmed: Must be True to proceed. Set to True to confirm deletion. - """ + async def delete_container(container_name: str, confirmed: bool = False): + """Delete a stopped container. Requires confirmed=True.""" if not confirmed: return ( f"Preview: would delete container '{container_name}'.\n" diff --git a/src/mcp_synology_container/modules/images.py b/src/mcp_synology_container/modules/images.py index b8a8514..386ffdf 100644 --- a/src/mcp_synology_container/modules/images.py +++ b/src/mcp_synology_container/modules/images.py @@ -102,12 +102,8 @@ def register_images(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None: """Register all image management tools with the MCP server.""" @mcp.tool() - async def list_images() -> str: - """List all local Docker images sorted by size (largest first). - - Shows name:tag, size, creation date, and whether the image is - currently used by at least one container. - """ + async def list_images(): + """List local Docker images sorted by size, showing tag, date, and in-use status.""" try: img_data = await client.request( "SYNO.Docker.Image", @@ -156,18 +152,8 @@ def register_images(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None: return "\n".join(lines) @mcp.tool() - async def delete_image(image_id: str, confirmed: bool = False) -> str: - """Delete a local Docker image by name:tag or image ID hash. - - Without confirmed=True, returns a preview of what would be deleted. - Refuses deletion if the image is used by any container. - - Args: - image_id: Image reference as "name:tag" (e.g. "nginx:1.24") - or a full/short image hash (e.g. "sha256:14300de7…"). - confirmed: Must be True to actually delete. Default False shows - a preview only. - """ + async def delete_image(image_id: str, confirmed: bool = False): + """Delete a local image by name:tag or hash. Requires confirmed=True; refuses if in use.""" # Parse name and tag using the last ":" as separator so that # registry-prefixed images (e.g. "ghcr.io/foo/bar:v1") are handled # correctly. rpartition returns ("", "", original) when ":" is absent. @@ -284,16 +270,8 @@ def register_images(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None: return f"Deleted {display_name} — {size_str} freed." @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. - """ + async def check_image_updates(project_name: str | None = None): + """Check which local images have updates available (upgradable flag from NAS registry).""" try: data = await client.request( "SYNO.Docker.Image", diff --git a/src/mcp_synology_container/modules/networks.py b/src/mcp_synology_container/modules/networks.py index aefd9fb..401a311 100644 --- a/src/mcp_synology_container/modules/networks.py +++ b/src/mcp_synology_container/modules/networks.py @@ -19,7 +19,7 @@ def register_networks(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non """Register all network management tools with the MCP server.""" @mcp.tool() - async def list_networks() -> str: + async def list_networks(): """List all Docker networks with driver, subnet, gateway, and attached containers.""" try: data = await client.request("SYNO.Docker.Network", "list") @@ -60,18 +60,8 @@ def register_networks(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non ip_range: str | None = None, enable_ipv6: bool = False, confirmed: bool = False, - ) -> str: - """Create a new Docker network. - - Args: - name: Name for the new network. - driver: Network driver. Defaults to "bridge". - subnet: Subnet in CIDR notation (e.g. "172.28.0.0/16"). - gateway: Gateway IP address. - ip_range: Allocatable IP range in CIDR notation. - enable_ipv6: Enable IPv6 support. Defaults to False. - confirmed: Must be True to actually create. Default False shows a preview. - """ + ): + """Create a Docker network with optional subnet/gateway/IPv6. Requires confirmed=True.""" details = [f" Name: {name}", f" Driver: {driver}"] if subnet: details.append(f" Subnet: {subnet}") @@ -113,15 +103,8 @@ def register_networks(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non return f"Network '{name}' created{id_str}." @mcp.tool() - async def delete_network(name: str, confirmed: bool = False) -> str: - """Delete a Docker network by name. - - Refuses deletion if any container is still attached to the network. - - Args: - name: Name of the network to delete. - confirmed: Must be True to actually delete. Default False shows a preview. - """ + async def delete_network(name: str, confirmed: bool = False): + """Delete a Docker network. Requires confirmed=True; refuses if containers are attached.""" # Fetch network list to validate existence and check for attached containers try: data = await client.request("SYNO.Docker.Network", "list") diff --git a/src/mcp_synology_container/modules/projects.py b/src/mcp_synology_container/modules/projects.py index 48bddee..e9e3fb2 100644 --- a/src/mcp_synology_container/modules/projects.py +++ b/src/mcp_synology_container/modules/projects.py @@ -24,12 +24,8 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non """Register all project management tools with the MCP server.""" @mcp.tool() - async def list_projects() -> str: - """List all Container Manager projects with their current status. - - Returns a formatted table of projects including name, status, path, - and container count. - """ + async def list_projects(): + """List all Container Manager projects with name, status, path, and container count.""" try: data = await client.request("SYNO.Docker.Project", "list") except Exception as e: @@ -54,12 +50,8 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non return "\n".join(lines).rstrip() @mcp.tool() - async def get_project_status(project_name: str) -> str: - """Get detailed status of a specific project. - - Args: - project_name: Name of the project to inspect. - """ + async def get_project_status(project_name: str): + """Get detailed status and container list for a specific project.""" project = await _find_project(client, project_name) if project is None: return f"Project '{project_name}' not found." @@ -67,12 +59,8 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non return _format_project_detail(project) @mcp.tool() - async def start_project(project_name: str) -> str: - """Start a Container Manager project. - - Args: - project_name: Name of the project to start. - """ + async def start_project(project_name: str): + """Start a Container Manager project.""" project = await _find_project(client, project_name) if project is None: return f"Project '{project_name}' not found." @@ -89,16 +77,8 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non return f"Error starting project '{project_name}': {e}" @mcp.tool() - async def stop_project(project_name: str, confirmed: bool = False) -> str: - """Stop a running Container Manager project. - - This operation stops all containers in the project. - Requires confirmation before executing. - - Args: - project_name: Name of the project to stop. - confirmed: Must be True to proceed. Set to True to confirm the stop operation. - """ + async def stop_project(project_name: str, confirmed: bool = False): + """Stop all containers in a project. Requires confirmed=True.""" if not confirmed: return ( f"Stopping project '{project_name}' will halt all its containers.\n" @@ -121,26 +101,8 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non return f"Error stopping project '{project_name}': {e}" @mcp.tool() - async def redeploy_project(project_name: str, confirmed: bool = False) -> str: - """Redeploy a project, pulling the latest images via SYNO.Docker.Project/build_stream. - - This is the programmatic equivalent of the DSM "Erstellen" (Build) button. - Unified 3-step flow for all project states: - - Step 1 — Stop (skipped for STOPPED; error-suppressed for BUILD_FAILED) - Step 2 — Trigger build_stream: DSM pulls updated images and starts the - project. This is a Server-Sent Events endpoint; the call - returns once DSM confirms it accepted the request. - Step 3 — Poll SYNO.Docker.Project/list every 2 s for up to 5 min - until the project reaches RUNNING. A warning is emitted on - timeout instead of returning an error. - - Requires confirmation before executing. - - Args: - project_name: Name of the project to redeploy. - confirmed: Must be True to proceed. Set to True to confirm the redeploy. - """ + async def redeploy_project(project_name: str, confirmed: bool = False): + """Pull latest images and restart a project via build_stream. Requires confirmed=True.""" if not confirmed: return ( f"Redeploying project '{project_name}' will stop and restart all its " diff --git a/src/mcp_synology_container/modules/system.py b/src/mcp_synology_container/modules/system.py index fa17c41..1a745c5 100644 --- a/src/mcp_synology_container/modules/system.py +++ b/src/mcp_synology_container/modules/system.py @@ -20,13 +20,8 @@ def register_system(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None: """Register all system-level tools with the MCP server.""" @mcp.tool() - async def system_df() -> str: - """Show Docker disk usage: images, containers, and volumes. - - Assembles disk-usage data from the image and container lists. - Images that are not referenced by any container are marked as - reclaimable. - """ + async def system_df(): + """Show Docker disk usage: image count/size and container running/stopped counts.""" errors: list[str] = [] # ── Images ─────────────────────────────────────────────────────────── @@ -94,16 +89,8 @@ def register_system(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None: return "\n".join(lines) @mcp.tool() - async def system_prune(confirmed: bool = False) -> str: - """Remove unused Docker resources: dangling images, stopped containers, unused networks. - - Without confirmed=True, shows a preview of what would be removed. - With confirmed=True, runs the prune and reports reclaimed space. - - Args: - confirmed: Must be True to actually prune. Default False shows - a preview only. - """ + async def system_prune(confirmed: bool = False): + """Remove unused Docker resources (images, stopped containers). Requires confirmed=True.""" # ── Gather preview data ─────────────────────────────────────────────── dangling_images: list[dict[str, Any]] = [] stopped_containers: list[dict[str, Any]] = []