Revision history for MCP::Server::Tool::FileSystem
0.1.1 2026-08-09T20:26:49+01:00
- SECURITY: symlink containment now holds on Windows. The defence relied
on IO::Path.resolve, which does not follow symlinks there, so a link
planted in the sandbox could be read and written straight through
(caught by the Windows CI lane). Containment now chases links itself
(physical-path, realpath-style over readlink, hop budget against
cycles, fail-closed on unreadable or drive-relative targets) — one
code path on every platform. path-contains unifies separator spelling
on Windows; the root itself is chased at construction. New containment
tests: chains, mid-path directory links, relative targets (in-root
'..' targets still read through), cycles.
0.1.0 2026-08-09T20:00:53+01:00
- Initial release
- MCP::Server::Tool::FileSystem: a root-confined filesystem toolkit that
plugs into any MCP::Server via $server.plug($kit) or
MCP::Server.new(:tools['FileSystem' => { root => '...' }]).
- Tools (default prefix "fs"): read, write, edit, list, glob, grep, stat,
mkdir, move, delete. A read-only kit registers only the five reading
tools — read, list, glob, grep and stat — and never advertises the rest.
- edit(path, old-string, new-string, replace-all): exact-match replacement
with no fuzzy matching. Zero matches, an ambiguous match without
replace-all, an empty old-string and an old-string equal to new-string
are all refused; the confirmation counts occurrences and bytes.
- read gained optional offset (1-based) and limit. Omitting both returns
the file byte for byte as before; a range comes back numbered "Ntext"
with a "[lines A-B of N total]" footer, and reading past the end is an
answer rather than an error.
- grep(pattern, path, regex, glob, context, max-results): GNU-grep-shaped
output ("path:line:text", "-" for context lines, "--" between groups).
Literal by default; regex mode is a native Raku regex, and a pattern
carrying a code block is refused by Rakudo's interpolation guard rather
than executed. Binary and non-UTF-8 files are skipped and counted, and
the search stops at max-results with a notice.
- move(from, to) and delete(path, recursive). Both ends of a move go
through the resolver; an existing destination, a missing destination
parent, and moving or deleting the root itself are all refused. A
recursive delete unlinks symlinks instead of following them.
- Files are read as bytes and decoded once rather than slurped as text:
Raku's text-mode slurp folds CRLF into LF and spurt never puts it back,
which would have made every edit of a CRLF file rewrite the lines it did
not touch. read is now verbatim in the sense edit needs it to be.
- Parameter naming is a documented contract: location parameters are
exactly "path", "from" and "to", and multi-word parameters are
kebab-case, so a permission layer in front of the server can find the
paths a call would touch by name.
- Containment: every path parameter must be relative, may not contain ".."
and may not name a Windows device; the nearest existing ancestor is
resolved and checked against the root by parent-chain walk, so symlinks
planted inside the sandbox cannot be used as a door out of it.
- glob is a portable, regex-free matcher: "*" matches within one path
segment, "?" matches one character, nothing else is special.