Revision history for MCP::Server::Tool::FileSystem
0.2.2 2026-08-21T21:05:33+01:00
- Require MCP::Server 0.6.0, the release that actually ships what 0.2.1
leans on: tool annotations in the tools/list catalog, the annotated-batch
concurrent scheduler, and the bridge's content-only rendering. The old
0.4.0+ pin let a resolver hand this pack a server that silently dropped
its annotations.
0.2.1 2026-08-21T20:36:03+01:00
- The six reading tools (read, list, glob, stat, grep, map) now declare the
MCP annotations readOnlyHint and idempotentHint, so a host may run a
batch of them side by side and execute identical calls once. The claim
is about the handlers as much as the intent: the observation tools take
this instance's mutation lock and the map cache is behind its own, so
several reads on ONE pack instance see coherent state. The five mutating
tools deliberately declare nothing.
0.2.0 2026-08-20T22:49:48+01:00
- New reading tool: map(path, query, focus, max-chars). Returns a ranked
skeleton of the code below a directory — each file as its path plus the
definition lines of its most-referenced symbols, never bodies — so a
model can orient in an unfamiliar tree with one call instead of a dozen
speculative reads. Ranking is a personalized PageRank over the
"file A refers to something file B defines" graph; query re-centres it on
matching identifiers and focus on named paths. Registered on read-only
kits too.
- map understands C, C++, Go, Java, JavaScript, Python, Rust, TypeScript
and TSX through TreeSitter::Native, and Raku through a heuristic scanner
(no tree-sitter grammar for Raku exists), where `use Foo::Bar` resolves
to the file that is Foo::Bar. The pack ships supplemental tree-sitter
queries for C and C++ (whose vendored tags.scm files tag no references at
all) and for TypeScript/TSX (whose vendored file tags only signature
declarations, missing every function with a body).
- map picks a file's representative definitions by how much the tree refers
to them, sharing a name's references among the files that define it and
setting aside names most of the map refers to. Matched-by-name references
cannot tell whose `new` they mean, and counting them naively made
"method new" the single line printed for three unrelated files.
- map's Raku scanner skips declarator documentation in every form, the
bracketed `#|( ... )` and `#=( ... )` blocks included. Their continuation
lines carry no `#`, so prose about code — which says "sub", "class" and
"constant" constantly — was being read as code and could outrank the
declarations around it.
- map bounds itself: prunes .git, node_modules, build output and every
dot-directory; skips files over 1MB, binaries and unreadable files, and
counts them; stops after 5000 files; and fits its answer into max-chars
by dropping whole files rather than truncating one. Parse results are
cached per file on modification time and size, so mapping, editing one
file and mapping again re-reads only that file.
- New dependency: TreeSitter::Native.
- Tool results now include structured `sha256-v1` revisions while preserving
their existing text. Files hash exact bytes; directories hash a canonical
recursive tree; absent paths have a stable token.
- SECURITY: write, edit, mkdir, move and delete accept the unadvertised
`_expected-revisions` condition. Compare and mutation share one provider
lock, stale conditions perform no mutation, and concurrent conditional
writers cannot both win.
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.