feat: add get_info tool using SYNO.FileStation.List::getinfo
SYNO.FileStation.Stat is absent from this NAS's API registry. SYNO.FileStation.List::getinfo returns identical data and is confirmed working. - tools/filestation.py: new get_info tool — accepts one or more comma-separated paths, calls getinfo with real_path/size/time/perm/ owner/type additional fields, returns a 9-column table - tests: 6 new tests covering single file, directory, multi-path, empty input, DSM error, and correct API method assertion - SPEC.md: remove SYNO.FileStation.Stat from API table, rewrite get_info tool section to reference getinfo, update list_dir note - CLAUDE.md: update Implemented Tools list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -211,3 +211,104 @@ def register_filestation(
|
||||
lines.append(f"Use offset={end} to fetch the next page.")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@mcp.tool()
|
||||
async def get_info(path: str) -> str:
|
||||
"""Get detailed metadata for one or more files or folders on the NAS.
|
||||
|
||||
Accepts a single path or a comma-separated list of paths.
|
||||
Use share paths as returned by list_shares (e.g. "/dev/file.txt").
|
||||
|
||||
Args:
|
||||
path: One or more share-relative paths, comma-separated
|
||||
(e.g. "/dev/notes.txt" or "/dev/notes.txt,/data/photo.jpg").
|
||||
|
||||
Returns:
|
||||
Formatted table with type, size, owner, permissions, and timestamps
|
||||
for each requested path.
|
||||
"""
|
||||
from mcp_synology_filestation.client import SynologyError
|
||||
|
||||
paths = [p.strip() for p in path.split(",") if p.strip()]
|
||||
if not paths:
|
||||
return "Error: no path provided."
|
||||
|
||||
try:
|
||||
data = await client.request(
|
||||
"SYNO.FileStation.List",
|
||||
"getinfo",
|
||||
params={
|
||||
"path": ",".join(paths),
|
||||
"additional": json.dumps(
|
||||
["real_path", "size", "time", "perm", "owner", "type"]
|
||||
),
|
||||
},
|
||||
)
|
||||
except SynologyError as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
files: list[dict] = data.get("files", [])
|
||||
if not files:
|
||||
return "No information returned for the given path(s)."
|
||||
|
||||
rows = []
|
||||
for f in files:
|
||||
item_path = f.get("path") or f.get("name", "")
|
||||
is_dir = f.get("isdir", False)
|
||||
ftype = "dir" if is_dir else "file"
|
||||
add = f.get("additional", {})
|
||||
|
||||
size_str = "-" if is_dir else _fmt_size(add.get("size"))
|
||||
mtime_str = _fmt_time((add.get("time") or {}).get("mtime"))
|
||||
crtime_str = _fmt_time((add.get("time") or {}).get("crtime"))
|
||||
|
||||
owner_info = add.get("owner") or {}
|
||||
owner = owner_info.get("user", "-")
|
||||
group = owner_info.get("group", "-")
|
||||
|
||||
perm_info = add.get("perm") or {}
|
||||
posix = perm_info.get("posix", 0)
|
||||
perm_str = oct(posix)[2:] if posix else "-"
|
||||
|
||||
real_path = (add.get("real_path") or "").strip() or "-"
|
||||
|
||||
rows.append(
|
||||
(
|
||||
item_path,
|
||||
ftype,
|
||||
size_str,
|
||||
owner,
|
||||
group,
|
||||
perm_str,
|
||||
mtime_str,
|
||||
crtime_str,
|
||||
real_path,
|
||||
)
|
||||
)
|
||||
|
||||
headers = (
|
||||
"Path",
|
||||
"Type",
|
||||
"Size",
|
||||
"Owner",
|
||||
"Group",
|
||||
"Perm",
|
||||
"Modified",
|
||||
"Created",
|
||||
"Real path",
|
||||
)
|
||||
col_widths = [max(len(h), *(len(r[i]) for r in rows)) for i, h in enumerate(headers)]
|
||||
|
||||
def _sep() -> str:
|
||||
return "+" + "+".join("-" * (w + 2) for w in col_widths) + "+"
|
||||
|
||||
def _row(vals: tuple[str, ...]) -> str:
|
||||
return "| " + " | ".join(f"{v:<{col_widths[i]}}" for i, v in enumerate(vals)) + " |"
|
||||
|
||||
lines = [_sep(), _row(headers), _sep()]
|
||||
for r in rows:
|
||||
lines.append(_row(r))
|
||||
lines.append(_sep())
|
||||
lines.append(f"\n{len(rows)} item(s).")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
Reference in New Issue
Block a user