From b921e3a649aa4b1413235c267c716fcceb136239 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Mon, 13 Apr 2026 16:07:47 +0200 Subject: [PATCH] Fix compose path: strip /volumeN prefix for FileStation API Container Manager returns raw filesystem paths (/volume1/docker/...), but SYNO.FileStation.* APIs expect paths without the volume prefix (/docker/...). Add _to_filestation_path() to strip /volumeN and apply it in _find_compose_path before any FileStation call. Also switch directory probe from getinfo (returns truthy files array with embedded code:408 for missing paths) to list (empty files array for non-existent directories), and apply the same prefix stripping to the error message shown when no compose file is found. Co-Authored-By: Claude Sonnet 4.6 --- src/mcp_synology_container/modules/compose.py | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/src/mcp_synology_container/modules/compose.py b/src/mcp_synology_container/modules/compose.py index 20f6896..e001a7a 100644 --- a/src/mcp_synology_container/modules/compose.py +++ b/src/mcp_synology_container/modules/compose.py @@ -7,6 +7,8 @@ Supported filenames: docker-compose.yml, docker-compose.yaml, compose.yml, compo from __future__ import annotations import logging +import re +import sys from typing import TYPE_CHECKING, Any import yaml @@ -20,6 +22,18 @@ from mcp_synology_container.modules.projects import _find_project logger = logging.getLogger(__name__) +_VOLUME_PREFIX_RE = re.compile(r"^/volume\d+") + + +def _to_filestation_path(path: str) -> str: + """Strip /volumeN prefix so paths work with the FileStation API. + + The Docker/Container Manager API returns raw filesystem paths like + /volume1/docker/myapp, but FileStation expects /docker/myapp. + """ + return _VOLUME_PREFIX_RE.sub("", path) + + # Recognized compose file names (in priority order) _COMPOSE_FILENAMES = [ "docker-compose.yml", @@ -45,11 +59,12 @@ def register_compose(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None path = await _find_compose_path(client, config, project_name) if path is None: project = await _find_project(client, project_name) - searched = ( + raw = ( project.get("path", f"{config.compose_base_path}/{project_name}") if project else f"{config.compose_base_path}/{project_name}" ) + searched = _to_filestation_path(raw) return ( f"No compose file found for project '{project_name}'.\n" f"Looked in {searched}/ for: " + ", ".join(_COMPOSE_FILENAMES) @@ -326,18 +341,29 @@ async def _find_compose_path( base, ) + # FileStation API requires paths without the /volumeN prefix. + fs_base = _to_filestation_path(base) + + # List the directory once and match against known filenames. + # getinfo returns {"files": [{"code": 408, ...}]} for missing paths + # (truthy but erroneous), so listing the directory is more reliable. + try: + data = await client.request( + "SYNO.FileStation.List", + "list", + params={"folder_path": fs_base, "additional": "[]"}, + ) + names_present = {f.get("name", "") for f in data.get("files", [])} + sys.stderr.write(f"[compose] files in {fs_base}: {sorted(names_present)}\n") + sys.stderr.flush() + except Exception as e: + logger.debug("Could not list directory '%s': %s", fs_base, e) + names_present = set() + for filename in _COMPOSE_FILENAMES: - path = f"{base}/{filename}" - try: - data = await client.request( - "SYNO.FileStation.List", - "getinfo", - params={"path": path, "additional": "[]"}, - ) - if data.get("files"): - logger.debug("Found compose file: %s", path) - return path - except Exception: - continue + if filename in names_present: + path = f"{fs_base}/{filename}" + logger.debug("Found compose file: %s", path) + return path return None