From 1bccf1e5d2b79bba0c9dd585d53d3d3d647c2b4b Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Tue, 14 Apr 2026 09:32:11 +0200 Subject: [PATCH] fix: use json.dumps(paths) for getinfo multi-path, delete probe script DSM accepts multiple paths as a JSON array string, not comma-separated. Comma-separated is treated as a single literal path; repeated path[] params return error 400. Confirmed via test_getinfo_multipath.py. - get_info: path param changed from ",".join(paths) to json.dumps(paths) - tests: update multi-path assertion to expect JSON array format Co-Authored-By: Claude Sonnet 4.6 --- .../tools/filestation.py | 2 +- test_getinfo_multipath.py | 121 ------------------ tests/test_tools_filestation.py | 5 +- 3 files changed, 4 insertions(+), 124 deletions(-) delete mode 100644 test_getinfo_multipath.py diff --git a/src/mcp_synology_filestation/tools/filestation.py b/src/mcp_synology_filestation/tools/filestation.py index 9e4df5a..d9ef23d 100644 --- a/src/mcp_synology_filestation/tools/filestation.py +++ b/src/mcp_synology_filestation/tools/filestation.py @@ -238,7 +238,7 @@ def register_filestation( "SYNO.FileStation.List", "getinfo", params={ - "path": ",".join(paths), + "path": json.dumps(paths), "additional": json.dumps( ["real_path", "size", "time", "perm", "owner", "type"] ), diff --git a/test_getinfo_multipath.py b/test_getinfo_multipath.py deleted file mode 100644 index 225ed4c..0000000 --- a/test_getinfo_multipath.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Throwaway script: test SYNO.FileStation.List::getinfo with multiple paths. - -Run with: - uv run python test_getinfo_multipath.py - -Tests three variants for passing multiple paths to getinfo and prints the -raw DSM response for each so we know which format the API actually accepts. -""" - -from __future__ import annotations - -import asyncio -import json -import sys - - -# ── config ──────────────────────────────────────────────────────────────── -# Change these to two paths that exist on your NAS (as returned by list_shares -# or list_dir — share-relative, e.g. "/dev/somefile.txt") -PATH_A = "/dev" -PATH_B = "/data" -# ────────────────────────────────────────────────────────────────────────── - - -async def run() -> None: - from mcp_synology_filestation.auth import AuthManager - from mcp_synology_filestation.client import FileStationClient - from mcp_synology_filestation.config import load_config - - config = load_config() - auth = AuthManager(config) - - async with FileStationClient( - config.base_url, - config.connection.verify_ssl, - config.connection.timeout, - ) as client: - await client.query_api_info() - sid = await auth.login(client) - client.sid = sid - - api = "SYNO.FileStation.List" - method = "getinfo" - api_info = client._api_cache[api] - version = api_info["maxVersion"] - url = f"{client._base_url}/webapi/{api_info['path']}" - - print( - f"SYNO.FileStation.List — path={api_info.get('path')!r} " - f"v{api_info.get('minVersion')}-v{api_info.get('maxVersion')}\n" - ) - - additional = json.dumps(["real_path", "size", "time", "perm", "owner", "type"]) - - http = client._http - - # ── Variant 1: comma-separated string ───────────────────────────── - print("─" * 60) - print(f"Variant 1 — path='{PATH_A},{PATH_B}' (comma-separated string)") - base_params = { - "api": api, - "version": str(version), - "method": method, - "_sid": sid, - "additional": additional, - } - params_v1 = {**base_params, "path": f"{PATH_A},{PATH_B}"} - resp = await http.get(url, params=params_v1) - body = resp.json() - print(f" HTTP {resp.status_code} success={body.get('success')}") - if body.get("success"): - files = body.get("data", {}).get("files", []) - print(f" files returned: {len(files)}") - for f in files: - print(f" path={f.get('path')!r} isdir={f.get('isdir')} add={list((f.get('additional') or {}).keys())}") - else: - print(f" ERROR: {body!r}") - print() - - # ── Variant 2: repeated path[] parameters ───────────────────────── - print("─" * 60) - print(f"Variant 2 — path[]={PATH_A!r} & path[]={PATH_B!r} (repeated params)") - # httpx accepts a list of tuples for repeated keys - params_v2_list = list(base_params.items()) + [("path[]", PATH_A), ("path[]", PATH_B)] - resp = await http.get(url, params=params_v2_list) - body = resp.json() - print(f" HTTP {resp.status_code} success={body.get('success')}") - if body.get("success"): - files = body.get("data", {}).get("files", []) - print(f" files returned: {len(files)}") - for f in files: - print(f" path={f.get('path')!r} isdir={f.get('isdir')} add={list((f.get('additional') or {}).keys())}") - else: - print(f" ERROR: {body!r}") - print() - - # ── Variant 3: JSON array ────────────────────────────────────────── - print("─" * 60) - print(f"Variant 3 — path=json.dumps([{PATH_A!r}, {PATH_B!r}]) (JSON array)") - params_v3 = {**base_params, "path": json.dumps([PATH_A, PATH_B])} - resp = await http.get(url, params=params_v3) - body = resp.json() - print(f" HTTP {resp.status_code} success={body.get('success')}") - if body.get("success"): - files = body.get("data", {}).get("files", []) - print(f" files returned: {len(files)}") - for f in files: - print(f" path={f.get('path')!r} isdir={f.get('isdir')} add={list((f.get('additional') or {}).keys())}") - else: - print(f" ERROR: {body!r}") - print() - - await auth.logout(client) - - -if __name__ == "__main__": - try: - asyncio.run(run()) - except Exception as e: - print(f"Fatal: {e}", file=sys.stderr) - sys.exit(1) diff --git a/tests/test_tools_filestation.py b/tests/test_tools_filestation.py index 2834576..edcb38b 100644 --- a/tests/test_tools_filestation.py +++ b/tests/test_tools_filestation.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from unittest.mock import AsyncMock, MagicMock import pytest @@ -369,9 +370,9 @@ async def test_get_info_multiple_paths(config: AppConfig) -> None: assert "/data/b.txt" in result assert "2 item(s)" in result - # Verify the API received both paths as a comma-joined string + # Verify the API received paths as a JSON array (confirmed working format) call_params = client.request.call_args[1]["params"] - assert call_params["path"] == "/dev/a.txt,/data/b.txt" + assert call_params["path"] == json.dumps(["/dev/a.txt", "/data/b.txt"]) @pytest.mark.asyncio