This is a Rust port of the official JavaScript filesystem MCP server. I did it for a number of reasons: partially for training both with MCPs and Rust, partially because original version never worked for me in Codex and I wanted something that works everywhere, and something I can control and fix and add some new features.
Rust allows for supereasy combining of any crates, so when I ported/created a dozen of MCPs, I decided to just merge some of them into a single useful toolkit. It's not about "memory safety" or something like that, I'm doing that just because I can and having fun doing that.
Crate version is still 0.2.1; everything after that lives on main as Unreleased. Full write-up: CHANGELOG.md.
- One state root:
~/.filesystem-mcp-rs/(orFS_MCP_STATE_DIR). Logs, panics and tool-call counters are one file per run:<state>/{logs,panics,stats}/<YYYY-MM-DD>/<machine>_<timestamp>_<instance>.*.FS_MCP_STATS=off/FS_MCP_LOG=offare the opt-outs. Nothing deletes a log. - Logging is on by default at
info, every transport. stdio still never writes stderr (that would close the handshake). shell: "bash"on Windows is git-bash, notSystem32\bash.exe(WSL). A WSL-only host is refused.- Feature-gated tool families each have their own router, so
http-tools/s3-tools/screenshot-tools/ eachctl-*domain can actually be compiled out. S3 args no longer overflow the stack. Six computer-control knobs (wait.kind, mouse buttons,win_geom.state, …) are real enums on the wire. tools/listis ~23% smaller and a test guard keeps descriptions honest. Session-lock footer: first result, then every 7 calls (FS_MCP_SESSION_FOOTER_EVERY).rmcp3.4.0.ServerHandler::get_inforeturnsServerConfig(ServerInfowas a deprecated alias).cargo install --path . --lockedkeeps the binary on the lockfile.computer-toolsis a default feature;--list-envandinstallboth come fromsrc/env_spec.rs. Blank env values mean unset.installsnapshotsPATH.- Locate name index:
locate_search,locate_refresh, andlocate_statususe the sharedlocate-rscrate and thefscan-rsscanner, fetched from GitHub over SSH at revisions pinned inCargo.tomlandCargo.lock. Building requires SSH access to both repositories. Multiple server processes share the SQLite index; one worker scans while other clients can search or queue requests. - The index knows directory sizes. Every directory used to be stored with size 0. A scan now totals each one (bytes, files, directories beneath it, schema v10),
locate_searchandlist_directory_with_sizesreport it, and an index built earlier gets its totals from the entries it already holds, without a rescan. Verified on a real 11.9 M-entry index: the totals equal the sum of the entries exactly. locate_sql: read-only SQL over the index (fs_entries,fs_roots) for the questions a name search cannot answer: the largest files under a folder, size per extension, folders holding two given entries. Fenced in byquery_only, an authorizer and a time limit. Lookups by name answer at once; a sweep over millions of rows takes seconds to tens of seconds.- A tool map for the model. Which tools work together is now data (
src/core/tool_graph.rs), not scattered prose: the server instructions open with one line per family (Read,Find,Space,Index,Write,Files,Run,Memory,Network,Screen,LLM) and the usual chains (disk_usage > file_stats > locate_sql > delete_path), and about thirty descriptions gain aSee also:tail. The map comes first because Claude Code shows only the first ~2,000 characters of a server's instructions; the rest, including the memory workflow, used to sit after the cut. A tool no family lists fails the build. file_statsrewritten: parallel walk that survives unreadable folders, a du-stylechildrenbreakdown,maxDepth,exclude,top,timeoutMs, cloud-placeholder counts, andfromIndexfor an instant answer from the index.filesystem-locateis eight files, not one: the 2154-linelib.rssplit intotypes/db/roots/time_util/worker/indexer/tests, same behavior —cargo test --manifest-path /path/to/locate-rs/Cargo.tomlandclippy -D warningsboth pass unchanged.- GitHub SSH dependencies:
locate-rsaliases the upstreamfilesystem-locatepackage atssh://git@github.com/ssoj13/locate-rs.git. A Cargo patch fetchesfscan-rsfromssh://git@github.com/ssoj13/fscan-rs.gitinstead of the scanner snapshot inside upstream locate. Both sources use pinnedrevvalues; there are no local crate copies.[workspace.metadata.release] publish = false: release tags build binaries rather than publish to crates.io. CI and release jobs require theDEPENDENCIES_SSH_KEYrepository secret with read access to both repositories.
0.2.1 — 2026-08-29
- ContentRef is tolerant: a bare string is inline text; a
{-prefixed string is unwrapped if a host double-encoded the object. Errors name line/column/field. Inline/chunk limit 8 → 64 KiB.
0.2.0 — 2026-08-29
- Computer control (
src/tools/computer/): mouse, keyboard, windows, capture, macros, UI Automation, OCR, toasts, clipboard files. Input tools needarm {ttl_ms}first; read-only (capture,monitors,win_list) do not. Shipped opt-in; default since Unreleased. install --forcetakes over a foreign MCP entry (backup first).installwith no dirs allowlists/or every Windows drive root. Vendoredmcp-setupreportsbrokenwhen the stored command is gone.
0.1.25 — 2026-08-17
rmcp2.2 → 3.1.3 (RequestMetaObject,CallToolResponse).- Content Plane SSOT: writes/edits/
run_command.stdingo throughContentRef/blob_*. Then the BH correctness audit (silent data-loss-as-success across grep,run_command, HTTP/S3, PDF, memory).
Opt out of computer control with --no-default-features --features http-tools,s3-tools,screenshot-tools.
LLM-friendly type coercion: All parameters use flexible types that tolerate common LLM serialization quirks:
- Numbers:
42and"42"both work (FlexU32,FlexUsize, etc.) - Booleans:
true,"true","1",1all accepted (FlexBool) - Arrays:
["a"],"a", or a JSON string of an array (vec_or_string) - Nested objects/maps: JSON object or JSON string containing that object — e.g.
run_command.outputFilter, HTTPheaders(notmem_*since 0.1.17) - Memory v2 (0.1.17+): strict flat args —
workspaceId,actorId,itemobject only; see CHANGELOG.md for migration
- Read:
read_text_file(head/tail/offset/limit/max_chars/line_numbers),read_media_file,read_multiple_files,read_json(JSONPath),read_pdf(normalize + quality warnings) - Write/Edit:
write_file,edit_file(ContentRef + diff/dry-run),edit_lines,bulk_edits; large payloads viablob_begin/blob_append/blob_finalize - Extract:
extract_lines(cut lines),extract_symbols(cut characters) - Binary:
read_binary,write_binary(ContentRef),extract_binary,patch_binary - FS ops:
create_directory,move_file,copy_file(files/dirs, overwrite),delete_path(recursive) - Hashing:
file_hash(MD5/SHA1/SHA256/SHA512/XXH64/Murmur3/Spooky + offset/length),file_hash_multiple(batch + comparison) - Comparison:
compare_files(binary diff),compare_directories(tree diff) - Archives:
archive_extract(ZIP/TAR/TAR.GZ),archive_create - Watch:
tail_file(follow mode),watch_file(change events) - Stats:
file_stats(size/count by extension, largest files, du-style per-child sizes, from a walk or from the index),find_duplicates - Introspection:
list_directory,list_directory_with_sizes(directory sizes from the locate index),get_file_info,directory_tree(depth/size/hash) - Search/roots:
search_files(glob + type/size/time filters),grep_files(regex + exclude + invert/count modes),grep_context(context-aware),list_allowed_directories - Indexed names:
locate_search(several roots, exact/prefix/contains/glob/regex, file/directory kind and include/exclude fragments),locate_refresh(queue a scan),locate_status(asynchronous progress),bgnd_scan_ctl(status / start / stop / pause / resume the scans),locate_sql(read-only SQL over the index: sorted, filtered or grouped questions) - Session lock:
[MCP lock] Use filesystem-mcp-rs tools only over built-ins. Do everything systematically, don't guess, re-check the work.The reminder appears on the first tool result, then every 7 tool calls. SetFS_MCP_SESSION_FOOTER_EVERYor--session-footer-everyto change the interval (0disables it);--no-session-footeralso disables it.mcp-setupembeds the full policy inCLAUDE.md/AGENTS.mdat install. - Process:
run_command(3 modes: sync/managed/detached, progress heartbeat, output filter, named shells includingpwshvspowershell,failFast, process tree kill),kill_process(tree kill),list_processes,search_processesrun_commandJSON: use camelCase keys (streamOutput,timeoutMs,failFast) or snake_case aliases.argsmay be an array or a JSON string.cwdmust be a quoted string — e.g."C:/projects/repo"(forward slashes). UnquotedC:\...is invalid JSON and fails in the MCP client before the server runs. Do not put$NAMEincommand/args(host may strip them; the server rejects leftovers — use stdin or a script file).
- Network (feature):
http_request,http_request_batch,http_download,http_download_batch - S3 (feature):
s3_list_buckets,s3_list,s3_stat,s3_get,s3_put,s3_delete,s3_copy,s3_presign, batch ops - Screenshot (feature):
screenshot_list_monitors,screenshot_list_windows,screenshot_capture_screen,screenshot_capture_window,screenshot_capture_region,screenshot_copy_to_clipboard - Computer control (
computer-tools, on by default; Windows backend): mouse/keyboard input,win_*window management, cursor-anchoredcapture, macro/wait steps, UI Automation (ui,ui_click,ui_set,ui_get), OCRfind, toasts, clipboard file lists — input gated behindarm {ttl_ms}with a TTL and an ops/minute cap - Safety: allowlist/roots validation, escape protection, optional
--allow_symlink_escape - Wave2:
port_users,net_connections,port_available,proc_tree,proc_env,proc_files,disk_usage,sys_info,file_diff,file_touch,clipboard_*,env_*,which - Document:
xlsx_read,xlsx_info(Excel),docx_read,docx_info(Word) - AI/LLM:
ai_messages_gemini,ai_messages_cerebras,ai_messages_openai,ai_count_tokens_*(needs API keys) - Memory v2:
mem_put,mem_update,mem_link,mem_search,mem_get,mem_get_summarywith scoped SQLite-backed storage
Use locate_search for repeated searches by file name or path. It accepts path or paths. Searching never scans: a path nobody indexed returns no matches with indexState: "unindexed" and a hint, so index it first with locate_refresh (or bgnd_scan_ctl start) and poll locate_status. waitMs only waits for a scan that is already running. bgnd_scan_ctl stops a scan (its rows are kept, a later start resumes it), pauses it in place, or lists every root and its progress; it works across MCP processes. All roots must be allowed by the server. grep_files searches file contents instead.
Use locate_sql for what a name search cannot say: the largest files under a folder, size per extension, folders that hold two given entries (Unity projects, Rust projects with a target), node_modules not nested in another. It takes one SELECT over two views, fs_entries(path, name, dir, ext, kind, size, modified, files, dirs, depth, under) and fs_roots(path, state, last_verified, last_error), limited to the directories the server allows. A directory's size is the total beneath it. The under parameter narrows fs_entries to one folder and everything beneath it (through the primary key, so there is no path arithmetic to get wrong), and dir = under lists that folder's children. Without it a subtree of P is path >= 'P\' AND path < 'P]'; filters on dir, ext and depth scan the subtree. help: true returns the columns, the functions (fs_ext, REGEXP) and ten ready recipes (largest files, what takes the space in a folder, size per extension, old big files, duplicates by name, Unity projects and their Library, Cargo target folders, non-nested node_modules); saveTo writes every row to a file as JSON lines; timeoutMs (default 120000) stops a runaway query. A question over millions of rows takes seconds to tens of seconds; a range on path keeps it small. Nothing else is reachable: writes, pragmas, ATTACH, other tables and functions that allocate without bound are refused, and the connection is query_only. The answers are as fresh as roots[].lastVerified.
{
"paths": ["C:/work", "D:/archive"],
"filters": {
"name": { "include": ["invoice", "2026"], "exclude": ["draft"] },
"extension": { "include": ["pdf"] }
},
"kind": "files",
"waitMs": 5000
}Fragment arrays use case-sensitive substring matching: every included fragment must occur and no excluded fragment may occur. The same rules apply independently to name, extension without its dot, and the full path. query adds one mode: exact, prefix, contains (default), glob, or regex. kind is all (default), files, or directories.
Requests for overlapping directories coalesce in a shared queue. The quiet timer starts at 3 seconds, resets at most three times, and never extends beyond 10 seconds from the first request. A refresh traverses the filesystem again, but publishes only changed rows to the index. There are no automatic full scans: nothing rescans on a timer and nothing scans because a search asked. A scan that is interrupted (client closed, crash, shutdown) is resumed by the next locate_refresh or bgnd_scan_ctl start instead of starting over. Scan pacing and skipped paths are set by FS_MCP_LOCATE_SCAN_BATCH, FS_MCP_LOCATE_WRITE_REST, FS_MCP_LOCATE_WRITE_PAUSE_MAX_MS and FS_MCP_LOCATE_EXCLUDE (by default the component store WinSxS, container layer copies and System Volume Information are skipped); install writes them all, with their defaults, into the client config, and --list-env prints them. On Windows, NTFS subtree scans use fscan-rs and preserve hard-link names; volume roots use its portable walker. The NTFS tree has a 250,000-node safety limit and falls back to the portable walker when native scanning fails.
Cost: the first scan of a volume is the expensive one (about 2.5 hours for 11.9 million entries on one measured system: two full-text indexes are built as it goes, and the scan paces its own writes). A refresh still walks the whole volume, but stages the rows in a table without full-text indexes and applies only the difference, so it should be cheaper; it has not been timed. Skipping trees you never search with FS_MCP_LOCATE_EXCLUDE shortens both. A scan also totals every directory. The schema change to v10 is additive: an older binary ignores the new table and leaves a root without totals for the attempt it publishes, and only the process that owns the scan lock backfills them, so directory sizes appear once a process of this version owns the lock.
Run filesystem-mcp-rs --list-env for the authoritative list for your build — the table
below, the env block written by install, and that command all come from one registry
(src/env_spec.rs), and only keys whose feature is compiled in are shown. Precedence is
CLI arg > env > default, and a blank value means "unset", not "empty".
Everything this server owns lives under one per-user root, identically on every platform. The keys below are listed first because the locations named further down resolve relative to it.
| Variable | Default | Description |
|---|---|---|
FS_MCP_STATE_DIR |
(unset) | State directory for every file this server owns; must be absolute. Unset = ~/.filesystem-mcp-rs |
FS_MCP_LOG |
info |
Level for this server's log under <state>/logs: trace|debug|info|warn|error, or off. Bare words are levels; a target filter needs = or , (info,hyper=warn) |
FS_MCP_STATS |
on |
Count tool calls, outcomes and latency per tool, and write them once at exit: on | off |
FS_MCP_TMP_KEEP_HOURS |
24 |
Delete scratch under <state>/tmp older than this many hours. 0 = never sweep |
Logging is on by default in every transport mode, at info. Each run writes its own file,
<state>/logs/<YYYY-MM-DD>/<machine>_<timestamp>_<instance>.log — one file per process, so dozens of
servers on one machine never contend for a shared file and there is no rotation to arbitrate. The
stamp carries milliseconds and the name ends in the run's instance id, so even a dozen servers
launched together — which start inside the same millisecond routinely — each get their own file. A new day is a new directory; a process that outlives midnight keeps the file it
opened. --log <FILE> writes to that path instead, and FS_MCP_LOG=off is the way out: no
subscriber, no file.
If the log cannot be opened at all — a read-only state directory, a full disk, a --log pointing
at a directory — the server still starts, and says why in <state>/logging-degraded.log. That
file is the only place it can say it under stdio, where a byte on stderr would close the
handshake; stream transport also prints the reason on stderr.
Under stdio the file is the only sink — anything on stderr during the MCP handshake closes the connection — so a log file that cannot be opened leaves that run silent rather than breaking the transport. Stream mode also writes to stderr, which nobody is parsing.
A panic writes its own crash report, <state>/panics/<YYYY-MM-DD>/<machine>_<timestamp>_<instance>.log —
named the same way, one file per panic, so you can tell which run produced which backtrace.
Each run counts its own tool calls — how many, how they ended, how long they took — and writes
them once, on the way out, to <state>/stats/<YYYY-MM-DD>/<machine>_<timestamp>_<instance>.json. Same naming
as the logs, for the same reason: nothing is shared between processes, so nothing has to be
arbitrated between them.
Every tool the router knows starts at zero, so a tool nobody called is a row of zeros rather than
a missing row — which is what makes the unused tail of the surface visible at all. The five
outcome columns partition the calls: ok, err_flagged (the call answered with is_error),
err_params, err_internal, deferred.
Counters live in memory for the life of the process and are written exactly once: there is no
database, no periodic flush and no aggregation. A run killed outright (SIGKILL) loses them; a
panic still files them, beside the crash report. Reading a week of runs is reading that directory
— this server's own grep_files and read_json do that better than a bespoke query tool would.
Nothing ever deletes a log file or a crash report. There is no retention for <state>/logs
or <state>/panics: clear those directories by hand when you want them clear. (<state>/tmp is
a different matter — it is swept by age, see FS_MCP_TMP_KEEP_HOURS.)
| Variable | Default | Description |
|---|---|---|
FS_MCP_HTTP_ALLOW_LIST |
* |
HTTP allowlist domains (comma/semicolon/whitespace separated). Use * to allow all |
FS_MCP_S3_ALLOW_LIST |
* |
S3 allowlist buckets (comma/semicolon/whitespace separated). Use * to allow all |
FS_MCP_MEMORY_ACCESS_MODE |
enforce_private_only |
Memory access mode: enforce_private_only, allow_all, or enforce_visibility |
FS_MCP_MEMORY_DB |
(unset) | SQLite file for the memory tools. Unset = ~/.filesystem-mcp-rs/memory2.db |
DISABLE_THOUGHT_LOGGING |
(unset) | Set to true to disable thought logging |
| Variable | Default | Description |
|---|---|---|
FS_MCP_CTL_TYPE_MODE |
paste |
key_type strategy: paste (clipboard, ~100× faster) or chars (per-char Unicode) |
FS_MCP_CTL_TYPE_INTERVAL_MS |
30 |
chars-mode per-char delay. Below ~25 ms Windows 11 mangles runs into repeats |
FS_MCP_CTL_ARM_TTL_MS |
30000 |
How long one arm call keeps input tools unlocked |
FS_MCP_CTL_OPS_PER_MIN |
240 |
Runaway cap on executed input ops per minute (also --ctl-ops-per-min) |
FS_MCP_CTL_BACKEND |
(unset) | Pin the desktop backend. Unset = auto-detect; null disables input (testing) |
FS_MCP_CTL_OCRS_MODELS_DIR |
(unset) | Cache dir for the downloaded ocrs models. Unset = ~/.filesystem-mcp-rs/ocrs |
| Variable | Description |
|---|---|
LLM_MCP_GEMINI_API_KEY |
Gemini API key (or use GEMINI_API_KEY) |
LLM_MCP_CEREBRAS_API_KEY |
Cerebras API key (or use CEREBRAS_API_KEY) |
LLM_MCP_OPENAI_API_KEY |
OpenAI API key (or use OPENAI_API_KEY) |
| Variable | Description |
|---|---|
LLM_MCP_PROVIDERS |
Comma-separated list of enabled providers |
LLM_MCP_PROVIDER |
Default provider name |
LLM_MCP_PROVIDER_ENDPOINT |
Custom API endpoint URL |
LLM_MCP_PROVIDER_API_KEY |
Generic API key (for custom providers) |
LLM_MCP_PROVIDER_API_KEY_HEADER |
Custom header name for API key (default: Authorization) |
LLM_MCP_PROVIDER_API_KEY_PREFIX |
API key prefix (default: Bearer ) |
LLM_MCP_MODEL_MAPPING |
Model name mappings (JSON format) |
LLM_MCP_BIG_MODEL |
Alias for "big" model |
LLM_MCP_SMALL_MODEL |
Alias for "small" model |
LLM_MCP_MAX_TOKENS_LIMIT |
Maximum tokens limit |
LLM_MCP_REQUEST_TIMEOUT |
Request timeout in seconds |
LLM_MCP_MAX_RETRIES |
Maximum retry attempts |
LLM_MCP_MAX_STREAMING_RETRIES |
Maximum streaming retry attempts |
LLM_MCP_RETRY_BACKOFF_MS |
Retry backoff in milliseconds |
LLM_MCP_STREAMING_RETRY_BACKOFF_MS |
Streaming retry backoff in milliseconds |
LLM_MCP_FORCE_DISABLE_STREAMING |
Set to true to disable streaming |
LLM_MCP_EMERGENCY_DISABLE_STREAMING |
Emergency streaming disable flag |
HTTP, S3, screenshot and computer-control tools are all enabled by default:
cargo buildTo build without computer control (much faster: no ocrs / rten / windows crates):
cargo build --no-default-features --features http-tools,s3-tools,screenshot-tools
# …or none of them at all; the tool families are genuinely optional
cargo build --no-default-featurescomputer-tools is an umbrella over ctl-input (core; ctl-uia implies it) / ctl-uia /
ctl-ocr / ctl-notify / ctl-clip-files, so you can also enable just the domains you want —
each one builds on its own, which is checked. Two further flags, ctl-any and ctl-desktop, are
derived: the domains switch them on to say "some control domain is present" and "a domain that
acts on the desktop is present", so shared code carries one gate instead of listing domains.
Enabling either by hand selects nothing.
The desktop backend is Windows today; elsewhere the driver seam resolves to a null backend
that reports "unsupported" instead of pretending to work.
HTTP/S3 tools require allowlists at runtime (CLI flags or env vars):
--http-allowlist-domain example.com --http-allowlist-domain "*.example.org"--s3-allowlist-bucket my-bucketAlternatively via env vars (comma/semicolon/whitespace separated):FS_MCP_HTTP_ALLOW_LIST=example.com,*.example.org(use*to allow all)FS_MCP_S3_ALLOW_LIST=my-bucket;other-bucket(use*to allow all)
filesystem-mcp-rs install (mcp-setup) writes every supported FS_MCP_* key into the client MCP config with its default value — including FS_MCP_HTTP_ALLOW_LIST=* and FS_MCP_S3_ALLOW_LIST=* — so the config lists the available knobs instead of hiding them. Override with --http-allowlist-domain / --s3-allowlist-bucket or --env FS_MCP_HTTP_ALLOW_LIST=....
filesystem-mcp-rs install also snapshots the current process PATH into each client's env.PATH (the default install target is every supported client: Claude Code, Cursor, VS Code, Codex, …). That is how git/cargo stay visible when the GUI starts the server with a short PATH. Re-run install from a terminal that already has the PATH you want, or edit the key / pass --env PATH=.... The server does not read HKCU/HKLM.
filesystem-mcp-rs install also defaults the server's allowed directories to the whole disk when you pass none (/ on Unix, every mounted drive root such as C:\, D:\, … on Windows). Pass explicit directories to scope it down: filesystem-mcp-rs install C:\projects D:\data.
The server now uses scoped memory v2 by default with a local SQLite database at memory2.db.
Tools:
mem_putmem_updatemem_linkmem_searchmem_getmem_get_summary
Memory access modes:
enforce_private_only- Default. Only
privaterecords are restricted to creator, owner, orsystem.
- Default. Only
allow_all- No ACL enforcement inside the requested scope.
enforce_visibility- Full visibility enforcement for
private,session,topic,workspace,app,tenant, andpublic_read.
- Full visibility enforcement for
CLI:
filesystem-mcp-rs --memory-db C:/data/memory2.db --memory-access-mode enforce_private_onlyEnvironment:
FS_MCP_MEMORY_DB=C:/data/memory2.db
FS_MCP_MEMORY_ACCESS_MODE=enforce_visibilityMCP client config example:
{
"command": "filesystem-mcp-rs",
"args": ["--memory-access-mode", "enforce_private_only"],
"env": {
"FS_MCP_MEMORY_DB": "C:/data/memory2.db"
}
}Recommended defaults:
- local/single-user:
enforce_private_only - most relaxed/shared setup:
allow_all - stricter collaborative setup:
enforce_visibility
Tools: screenshot_list_monitors, screenshot_list_windows, screenshot_capture_screen, screenshot_capture_window, screenshot_capture_region, screenshot_copy_to_clipboard
Examples:
// List monitors
{"tool": "screenshot_list_monitors", "arguments": {}}
// List windows with title filter
{"tool": "screenshot_list_windows", "arguments": {"title_filter": "Chrome"}}
// Capture primary monitor to a file
{"tool": "screenshot_capture_screen", "arguments": {"output": "file", "path": "C:/temp/screen.png"}}
// Capture a window by title to base64
{"tool": "screenshot_capture_window", "arguments": {"title": "Terminal", "output": "base64"}}
// Capture a region on monitor 0
{"tool": "screenshot_capture_region", "arguments": {"monitor_id": 0, "x": 100, "y": 100, "width": 800, "height": 600, "output": "file", "path": "C:/temp/region.png"}}
// Copy an existing PNG to clipboard
{"tool": "screenshot_copy_to_clipboard", "arguments": {"path": "C:/temp/region.png"}}Cross-platform tools for network, process, system info, and utilities.
{"tool": "port_users", "arguments": {"port": 8080}}
// Returns: [{"pid": 1234, "name": "node", "local_addr": "127.0.0.1:8080", ...}]{"tool": "net_connections", "arguments": {}}
{"tool": "net_connections", "arguments": {"pid": 1234}} // Filter by process{"tool": "port_available", "arguments": {"port": 3000}}
// Returns: {"port": 3000, "available": true}{"tool": "proc_tree", "arguments": {}} // Full tree
{"tool": "proc_tree", "arguments": {"root_pid": 1234}} // Subtree from PID{"tool": "proc_env", "arguments": {"pid": 1234}}{"tool": "proc_files", "arguments": {"pid": 1234}}
// Linux: /proc/pid/fd, macOS: lsof, Windows: limited info{"tool": "disk_usage", "arguments": {}} // All disks
{"tool": "disk_usage", "arguments": {"path": "C:/"}} // Specific mount{"tool": "sys_info", "arguments": {}}
// Returns: CPU cores, total/used RAM, swap, OS name/version, hostname, uptimeCompare two files using the similar crate. Returns git-compatible unified diff:
{"tool": "file_diff", "arguments": {"path1": "old.txt", "path2": "new.txt"}}
{"tool": "file_diff", "arguments": {"path1": "a.rs", "path2": "b.rs", "context": 5}}Returns:
unified_diff: Standard unified diff format (can be applied withpatch -p0)hunks: Structured JSON with changes (type: insert/delete/context, line numbers)additions,deletions: Change counts
{"tool": "file_touch", "arguments": {"path": "marker.txt"}}
{"tool": "file_touch", "arguments": {"path": "deep/nested/file.txt", "create_parents": true}}Requires screenshot-tools feature (uses arboard crate):
{"tool": "clipboard_read", "arguments": {}}
{"tool": "clipboard_write", "arguments": {"text": "Hello clipboard"}}Environment variables (current process only):
{"tool": "env_get", "arguments": {"name": "PATH"}}
{"tool": "env_set", "arguments": {"name": "MY_VAR", "value": "hello"}}
{"tool": "env_remove", "arguments": {"name": "MY_VAR"}}
{"tool": "env_list", "arguments": {}}{"tool": "which", "arguments": {"command": "python"}}
// Returns: {"command": "python", "found": true, "path": "/usr/bin/python", "all_matches": [...]}Read Excel spreadsheets via calamine (supports .xlsx, .xls, .ods):
{"tool": "xlsx_info", "arguments": {"path": "data.xlsx"}}
// Returns: sheet names, row/column counts
{"tool": "xlsx_read", "arguments": {"path": "data.xlsx"}}
{"tool": "xlsx_read", "arguments": {"path": "data.xlsx", "sheet": "Sheet2", "range": "A1:D10"}}Read Word documents via docx-lite:
{"tool": "docx_info", "arguments": {"path": "doc.docx"}}
{"tool": "docx_read", "arguments": {"path": "doc.docx"}}Integrated from llm-mcp-rs. Requires API keys via environment variables.
- Gemini:
GEMINI_API_KEYorLLM_MCP_GEMINI_API_KEY - Cerebras:
CEREBRAS_API_KEYorLLM_MCP_CEREBRAS_API_KEY - OpenAI:
OPENAI_API_KEYorLLM_MCP_OPENAI_API_KEY
// Send messages to LLM
{"tool": "ai_messages_gemini", "arguments": {"model": "gemini-pro", "messages": "Hello", "max_tokens": 1000}}
{"tool": "ai_messages_openai", "arguments": {"model": "gpt-4", "messages": [...], "max_tokens": 2000}}
// Count tokens
{"tool": "ai_count_tokens_gemini", "arguments": {"model": "gemini-pro", "messages": "Text to count"}}Large or nested UTF-8 must not ride inside MCP tool-argument JSON. Every write path takes a ContentRef object:
kind |
Shape | Limit / notes |
|---|---|---|
inline |
{ "kind": "inline", "text": "..." } |
max 8 KiB UTF-8 |
base64 |
{ "kind": "base64", "data": "..." } |
max 8 KiB decoded |
path |
{ "kind": "path", "path": "..." } |
allowlisted file on disk |
blob |
{ "kind": "blob", "id": "<sha256>" } |
from blob_finalize |
Staging large content: blob_begin → blob_append (≤8 KiB per chunk, text or dataBase64) → blob_finalize → pass {kind:"blob",id} to write_file / edit_file / run_command.stdin / write_binary. Optional expectSha256 on write_file / finalize. blob_stat checks a finalized id.
{"path": "notes.txt", "content": {"kind": "inline", "text": "hello"}}
{"path": "big.rs", "content": {"kind": "blob", "id": "<sha256 from blob_finalize>"}}Precise editing by line numbers (1-indexed). Perfect when you know exact locations:
- Operations:
replace,insert_before,insert_after,delete - Supports: Single lines or ranges (startLine-endLine)
- Use cases: Fixing specific lines, adding imports at known positions, removing exact code blocks
- Features: Returns unified diff, dry-run mode for preview
Apply the same edits to multiple files at once. More efficient than editing files individually:
- File selection: Glob patterns (e.g.,
*.rs,**/*.txt,src/**/*.js) - Operations: Search/replace text across all matching files
- Regex support:
isRegex: trueenables regex patterns with capture groups ($1,$2, etc.) - Replace all:
replaceAll: truereplaces ALL occurrences, not just the first one - Error handling: Continues on failure, reports errors per-file
- Use cases: Renaming functions/variables across codebase, updating imports, fixing typos everywhere, refactoring patterns
- Features: Returns summary with diffs, dry-run mode for preview
- failOnNoMatch: If true, files without matches return errors (default false)
Examples:
// Literal replace all occurrences
{"oldText": "use crate::foo", "newText": "use crate::bar::foo", "replaceAll": true}
// Regex with capture groups (refactor imports)
{"oldText": "use crate::(cache_man|event_bus|workers)", "newText": "use crate::core::$1", "isRegex": true, "replaceAll": true}
// Rename function across codebase
{"oldText": "old_function_name", "newText": "new_function_name", "replaceAll": true}
// Update version in all Cargo.toml
{"oldText": "version = \"0\\.1\\.\\d+\"", "newText": "version = \"0.2.0\"", "isRegex": true}Find files and directories by path pattern and metadata (not by text inside files — use grep_files for that):
- pattern: glob (
**/*.rs,src/**/*.txt) - excludePatterns: e.g.
target/**,node_modules/** - fileType:
file,dir,symlink,any(default) - minSize / maxSize: bytes
- modifiedAfter / modifiedBefore: RFC3339 timestamp or relative duration (
17m 20s,17m20s,2h,7d) — duration means cutoff atnow - span
Examples:
// All regular files touched in the last 17 minutes 20 seconds
{
"path": ".",
"pattern": "**/*",
"fileType": "file",
"excludePatterns": ["target/**", "node_modules/**"],
"modifiedAfter": "17m 20s"
}
// Rust sources modified in the last hour
{
"path": "src",
"pattern": "**/*.rs",
"modifiedAfter": "1h"
}
// Files older than 7 days
{
"path": ".",
"pattern": "**/*",
"fileType": "file",
"modifiedBefore": "7d"
}Search for text/regex patterns inside file contents (not filenames):
- Supports: Regex patterns, case-insensitive search, context lines
- File filtering: Optional glob include/exclude patterns to limit scope
- Returns: Matching lines with file paths and line numbers
- Use cases: Finding code patterns, locating function definitions, searching across codebase
- Note: Do not use
rg/grepviarun_command; usegrep_filesorsearch_filesinstead
Example:
{
"path": ".",
"pattern": "TODO|FIXME",
"filePattern": "**/*.rs",
"excludePatterns": ["target/**", "**/*.generated.rs"]
}Find a pattern only when specific terms appear nearby:
- Nearby terms:
nearbyPatternslist (literal by default, regex ifnearbyIsRegextrue) - Window:
nearbyWindowWordsand/ornearbyWindowChars - Direction:
nearbyDirection= before/after/both - Match mode:
nearbyMatchMode= any/all
Example:
{
"path": ".",
"pattern": "error",
"nearbyPatterns": ["timeout", "retry"],
"nearbyWindowWords": 6,
"nearbyDirection": "before",
"filePattern": "**/*.log"
}Read files with flexible pagination options for handling large files:
head: First N lines (like Unix head)tail: Last N lines (like Unix tail)offset+limit: Read N lines starting from line M (1-indexed pagination)max_chars: Truncate output to N characters (UTF-8 safe)- Returns:
totalLinesin metadata for pagination planning
Examples:
// Read lines 100-199 (page 2 with 100 lines per page)
{"path": "large.txt", "offset": 100, "limit": 100}
// First 50 lines
{"path": "large.txt", "head": 50}
// Last 20 lines
{"path": "large.txt", "tail": 20}
// Limit output size (useful for token limits)
{"path": "large.txt", "max_chars": 50000}
// Combine pagination with truncation
{"path": "large.txt", "offset": 1, "limit": 100, "max_chars": 10000}Remove lines from a file and optionally return extracted content:
- Parameters:
path,line(1-indexed),endLine(optional),dryRun,returnExtracted - Examples: Delete line 5, remove lines 10-20, preview deletion
- Use cases: Remove imports, delete code blocks, cut sections to paste elsewhere
Remove characters from a file by Unicode position:
- Parameters:
path,start(0-indexed),endorlength,dryRun,returnExtracted - Note: Uses Unicode chars (safe for multibyte), not raw bytes
- Use cases: Remove headers, cut text blocks, extract specific ranges
All binary tools use base64 encoding for data transfer.
Read bytes from a binary file at specified offset:
- Parameters:
path,offset,length - Returns: Base64-encoded data
- Use cases: Read binary headers, extract sections of images/executables
Write bytes to a binary file:
- Parameters:
path,offset,data(ContentRef in binary mode),mode(replace/insert) - Creates file if missing
- Use cases: Patch executables, inject data, modify headers
Remove bytes from a binary file and return them:
- Parameters:
path,offset,length,dryRun - Returns: Base64-encoded extracted data
- Use cases: Remove binary sections, cut data to relocate
Search and replace binary patterns in a file:
- Parameters:
path,find(base64),replace(base64),all - Use cases: Patch executables, fix binary data, search-replace in non-text files
Compute hash of a file with various algorithms:
- Parameters:
path,algorithm,offset,length - Algorithms: md5, sha1, sha256 (default), sha512, xxh64, murmur3, spooky
- Returns:
{hash, size, algorithm, offset, length} - Partial hashing: Use offset/length to hash only a portion of the file
- Non-crypto: murmur3/spooky are 128-bit fast hashes (great for checksums, deduplication)
- Use cases: Verify file integrity, detect changes, compare files without reading content
Examples:
// Hash entire file with SHA256
{"path": "file.bin"}
// Hash with fast non-crypto algorithm
{"path": "large.bin", "algorithm": "xxh64"}
// Hash first 1KB only
{"path": "file.bin", "offset": 0, "length": 1024}
// Hash from position 512 to end
{"path": "file.bin", "offset": 512}Hash multiple files and check if they match:
- Parameters:
paths[],algorithm - Returns:
{results[], all_match} - Use cases: Verify file copies, check backup integrity, detect duplicate content
Compare two files byte-by-byte with detailed analysis:
- Parameters:
path1,path2,offset1,offset2,length,max_diffs,context_bytes - Returns:
{identical, size1, size2, hash1, hash2, first_diff_offset, total_diff_regions, match_percentage, diff_samples[]} - Use cases: Verify export/conversion parity, debug serialization, find binary differences
Compare two directory trees recursively:
- Parameters:
path1,path2,recursive,compareContent(hash-based),ignorePatterns[] - Returns:
{identical, only_in_first[], only_in_second[], different[], same_count, diff_count} - Use cases: Sync verification, backup validation, migration testing
Read the last N lines or bytes of a file:
- Parameters:
path,lines,bytes,follow,timeout_ms - Returns:
{content, lines_returned, file_size, truncated} - Follow mode: Wait for new content to be appended
- Use cases: Log monitoring, watching build output, debugging
Block until a file changes or timeout:
- Parameters:
path,timeout_ms,events[](modify/create/delete) - Returns:
{changed, event, new_size, elapsed_ms} - Use cases: Wait for build artifacts, monitor config changes
Read and query JSON files using JSONPath:
- Parameters:
path,query(JSONPath like$.store.book[0].title),pretty - Returns:
{result, query_matched, pretty} - Use cases: Extract config values, query API responses, parse structured data
Extract text content from PDF files:
- Parameters:
path,pages(e.g., "1-5", "1,3,5"),maxChars,normalize(default true),includeRaw(default false) - Returns:
{text, pagesCount, pagesExtracted[], truncated, charCount, normalized, quality{score, warnings, suspiciousTokens, ...}} - Quality: low
quality.scoreor warnings likeextraction_quality_degradedmean encoding maps failed — verify against a PDF viewer before trusting names/tables - Use cases: Read documentation, extract report content
Extract ZIP, TAR, or TAR.GZ archives:
- Parameters:
path,destination,format(auto-detect by extension),files[](optional filter) - Returns:
{extracted_count, files[]} - Use cases: Unpack downloads, extract specific files from archives
Create ZIP or TAR.GZ archives:
- Parameters:
paths[],destination,format(zip/tar.gz) - Returns:
{path, size, file_count} - Use cases: Package files for backup, create distribution archives
Get detailed statistics about files and directories from a parallel walk that tolerates unreadable folders:
- Parameters:
path,recursive,children(du-style breakdown),maxDepth,exclude[](globs of names or relative paths),top(rows per breakdown; default 10 largest files, 50 children, 100 extensions),fromIndex,timeoutMs(120000, 0 = none) - Returns:
{totalFiles, totalDirs, totalSize, totalSizeHuman, byExtension{}, topExtensions[], largestFiles[], byChild[], childrenOmitted, links, cloudOnlyFiles, cloudOnlyBytes, skipped{count, examples[]}, incomplete, source, asOf, indexPartial, changedSinceScan, elapsedMs} children: trueaddsbyChild: the direct children with the size, file and directory counts of everything beneath each, largest first. This is the way to see what takes the space.fromIndex: truereturns totals andbyChildonly, instantly, from the locate index when it covers the path and knows every directory involved (source: "index",asOf= when the index was last checked,changedSinceScan= a directory changed after that); otherwise it walks (source: "live"). The default is always a walk.- Sizes are logical.
cloudOnlyBytesare online-only placeholders (OneDrive, iCloud, Dropbox) that occupy no disk. Symlinks and junctions are counted inlinksand not followed.incomplete: truemeans the time limit ended the walk and the numbers are lower bounds. - Use cases: Find what takes the space, analyze project size, find large files, understand codebase composition
Find files with identical content:
- Parameters:
path,min_size,by_content(hash-based or size-only) - Returns:
{duplicate_groups[], total_wasted_space} - Use cases: Cleanup disk space, find redundant files
Robust process execution for LLM workflows. Cross-platform (Windows/macOS/Linux).
Execution modes (mode):
| Mode | Behavior |
|---|---|
sync (default) |
Wait for completion. Sends progress heartbeat every ~30s to prevent MCP client timeout. |
managed |
Wait for completion. Sends progress notifications with output snippets every ~10s. |
detached |
Return immediately with PID. Use tail_file on log files for output. |
Parameters:
- Core:
command,args[],cwd,mode,shell,failFast(default true),timeoutMs,killAfterMs - Environment:
env{}(set/override),envPrepend{}(prepend to existing),envAppend{}(append to existing),clearEnv - Stdin:
stdinContentRef (inline/base64/path/blob) - Output files:
stdoutFile,stderrFile,streamOutput(default: true),streamDir - Output control:
stdoutHead,stdoutTail,stderrHead,stderrTail - Output filter:
outputFilter: {include[], exclude[], context, contextBefore, contextAfter, maxLines}(grep-like regex filtering)
Returns: {exitCode, stdout, stderr, pid, killed, timedOut, cancelled, durationMs, background, startedAt, finishedAt, stdoutFile, stderrFile, stdoutTotalLines, stderrTotalLines}
Key features:
- Progress heartbeat: Prevents MCP client 120s timeout for long builds
- Process tree kill: On timeout/cancel, kills all child processes (cargo build -> rustc, etc.)
- MCP cancellation: Client can cancel, process tree is killed immediately
- Shell mode:
false(default) = no shell;true= platform default (cmd /C/sh -c);"bash"=bash -c;"pwsh"= PowerShell 7 (pwsh.exe, error if missing);"powershell"/"ps"= Windows PowerShell 5. On Windows, GUI clients often have a short PATH —installsnapshots PATH intoenv.PATH. "bash"on Windows is git-bash, resolved from the installed git (git --exec-path, then the usual install locations) instead of taking whateverPATHoffers first.C:\Windows\System32\bash.exeis the WSL launcher and normally precedes git-bash onPATH; running a command through it crosses the Win32→WSL boundary, so variables and loops expand on the Linux side and come back empty, theenvmap never arrives and Windows paths mean nothing. If the only bash found is that launcher the call fails with an error naming it rather than silently using it — useshell: "pwsh"or install git-bash.failFast(default true): stop after a failing simplecmdline; PowerShell uses ErrorAction Stop.failFast: falseruns every line (old batch semantics).$NAMEincommand/args: rejected when still present. Pass scripts via stdin ContentRef or-File. Hosts may strip tokens before the server sees them.- Output filter: Grep-like filtering with include/exclude regex and context lines. Only affects inline results; full output always goes to log files.
Examples:
// Quick command
{"command": "git", "args": ["status"]}
// Long build with managed progress
{"command": "cargo", "args": ["build", "--release"], "mode": "managed", "timeoutMs": 1200000}
// Filter build output for errors/warnings
{"command": "cargo", "args": ["build"], "outputFilter": {"include": ["error\\[", "warning\\["], "context": 2, "maxLines": 50}}
// Shell pipes
{"command": "cat file.txt | grep error | head -20", "shell": true}
// Background server
{"command": "npm", "args": ["start"], "mode": "detached"}
// Debug with RUST_LOG
{"command": "cargo", "args": ["test"], "env": {"RUST_LOG": "debug"}}
// Prepend to PATH
{"command": "python", "args": ["script.py"], "envPrepend": {"PATH": "C:/custom/bin;"}}
// Pipe string to stdin
{"command": "python", "args": ["script.py"], "stdin": {"kind": "inline", "text": "input data"}}
// Head + tail (first 5 lines + last 10 lines)
{"command": "cargo", "args": ["test"], "stdoutHead": 5, "stdoutTail": 10, "streamOutput": false}Terminate a process or entire process tree. Cross-platform:
- Parameters:
pid,force(SIGKILL/TerminateProcess),tree(kill all child processes) - Returns:
{pid, success, killedCount, tree} - Use cases: Stop runaway builds, terminate servers with all children
// Kill single process
{"pid": 12345, "force": true}
// Kill entire process tree
{"pid": 12345, "force": true, "tree": true}List processes started by this server with run_command(mode: 'detached'):
- Parameters:
filter(optional command name filter) - Returns:
{processes[]} - Note: Only tracks processes started by THIS server session
Search for running processes by name or command line regex. Cross-platform via sysinfo crate:
- Parameters:
name_pattern(regex),cmdline_pattern(regex) - Returns:
{processes[{pid, name, command_line, exe_path, memory_bytes, cpu_percent, status, user}], count} - Examples:
- Find Chrome:
{name_pattern: "chrome"} - Find by port:
{cmdline_pattern: "--port=3000"} - Find Python scripts:
{name_pattern: "python", cmdline_pattern: "script\\.py"}
- Find Chrome:
Send requests with headers, cookies, query params, and body:
{
"method": "POST",
"url": "https://api.example.com/v1/items",
"headers": { "Authorization": "Bearer TOKEN", "Content-Type": "application/json" },
"cookies": { "session": "abc123" },
"query": { "page": "1" },
"body": "{\"name\":\"demo\"}",
"accept": "json",
"timeoutMs": 20000
}Run multiple requests in one call:
{
"requests": [
{ "id": "a", "method": "GET", "url": "https://example.com/a" },
{ "id": "b", "method": "GET", "url": "https://example.com/b" }
]
}Download files to local paths:
{ "url": "https://example.com/file.zip", "path": "downloads/file.zip" }{}{ "bucket": "my-bucket", "prefix": "reports/", "maxKeys": 100 }{ "bucket": "my-bucket", "key": "reports/2025.csv", "outputPath": "reports/2025.csv" }{ "bucket": "my-bucket", "key": "uploads/log.txt", "path": "logs/log.txt", "contentType": "text/plain" }{ "bucket": "my-bucket", "key": "old/file.txt" }{ "sourceBucket": "my-bucket", "sourceKey": "a.txt", "destBucket": "my-bucket", "destKey": "b.txt" }{ "bucket": "my-bucket", "key": "uploads/file.bin", "method": "GET", "expiresInSeconds": 600 }cargo build --releaseSome clients (qwen code, gemini-cli) validate tool schemas with a Draft 7 validator, while rmcp generates JSON Schema 2020-12. Such a client fails to resolve the dialect the schema declares:
no schema with key or ref "https://json-schema.org/draft/2020-12/schema"
Fix applied here (reviewed 2026-09-17): tool input schemas are served with no $schema key at
all, which is what that error is about — a validator cannot fail to resolve a meta-schema that
was never named. MCP 2025-06-18 requires no dialect (inputSchema is just "JSON Schema defining
expected parameters"), so dropping the key is spec-shaped, and it also takes ~7.4k characters of
repeated boilerplate out of every session.
The schema body is served exactly as rmcp generates it — 2020-12, with $defs and
#/$defs/... refs. An earlier version of this server also rewrote $defs into draft-07
definitions; that rewrite was removed on 2026-09-17 because nothing was found to need it: rmcp
chooses 2020-12 deliberately to match MCP's own alignment (modelcontextprotocol PR #655), so every
unmodified rmcp server — and every pydantic-based server in the official Python SDK — already puts
$defs on the wire, and #/$defs/... resolves as a plain JSON pointer under a Draft 7 validator
too. The only post-processing left is in src/core/schema.rs.
filesystem-mcp-rs supports dual-mode transport:
Local MCP clients (Claude Desktop, Cursor, Codex):
- stdin/stdout communication
- No stderr by default (prevents client connection errors)
- File logging with
-l
Remote access, web integrations, cloud deployments:
- HTTP server with SSE streaming
- MCP endpoint:
/mcp - Health check:
/health - Console logging enabled (optional file with
-l)
filesystem-mcp-rs --help
filesystem-mcp-rs -V # version# Basic
filesystem-mcp-rs /projects /tmp
# With logging (writes to filesystem-mcp-rs.log)
filesystem-mcp-rs -l /projects
# Custom log file
filesystem-mcp-rs -l /var/log/mcp.log /projectsLog location: Current working directory or specified path
# Local (http://127.0.0.1:8000)
filesystem-mcp-rs -s
# Custom port
filesystem-mcp-rs -s -p 9000
# Network accessible
filesystem-mcp-rs -s -b 0.0.0.0 -p 8000
# With file logging
filesystem-mcp-rs -s -l server.log
# Production setup
filesystem-mcp-rs -s -b 0.0.0.0 -p 8000 -l /var/log/mcp-server.logCheck health:
curl http://localhost:8000/health
# Returns: OKLogs: Console by default, file with -l flag
Usage: filesystem-mcp-rs [OPTIONS] [DIRS...]
Arguments:
[DIRS...] Allowed directories
Options:
--allow-symlink-escape Follow symlinks outside allowed dirs
-s, --stream HTTP mode (default: stdio)
-p, --port <PORT> HTTP port [default: 8000]
-b, --bind <ADDR> Bind address [default: 127.0.0.1]
-l, --log [<FILE>] Log to this file instead of the per-process default
-h, --help Print help
-V, --version Print version
cargo test # All tests (unit + integration + HTTP transport)
cargo test --test http_transport # HTTP transport onlycargo test is the gate, and it runs 637 tests across three suites:
- 569 unit tests (
src/, 5 ignored): path resolution and the centralization guard, housekeeping sweeps and leases, logging plans and retention, the env registry, text/binary/archive/document readers, process and system queries, and the computer-control driver. - 64 integration tests (
tests/integration.rs): file operations, search, grep, extract, binary, pagination. - 4 HTTP transport tests (
tests/http_transport.rs): server startup, health, MCP endpoint, logging to an explicit--logpath. - Unicode tested: Russian (Привет), Chinese (你好), Emoji (🦀)
Every spawned server in the suite gets a FS_MCP_STATE_DIR of its own under a TempDir, so a
full run leaves nothing in the real ~/.filesystem-mcp-rs/.
src/
├── main.rs - Entry point, CLI args, transport modes, MCP tools
├── core/
│ ├── allowed.rs - Directory allowlist/validation
│ ├── logging.rs - Transport-aware logging (stdio/stream)
│ ├── path.rs - Path resolution, escape protection
│ └── format.rs - Schema utilities
├── tools/
│ ├── fs_ops.rs - File read/head/tail
│ ├── edit.rs - Text-based edits + unified diff
│ ├── line_edit.rs - Line-based surgical edits
│ ├── bulk_edit.rs - Mass search/replace
│ ├── search.rs - Glob search with excludes + type/size/time filters
│ ├── grep.rs - Regex content search + invert/count modes
│ ├── binary.rs - Binary file operations (read/write/extract/patch)
│ ├── hash.rs - File hashing (MD5/SHA1/SHA256/SHA512/XXH64)
│ ├── compare.rs - File and directory comparison
│ ├── watch.rs - Tail file and watch for changes
│ ├── json_reader.rs - JSON reading with JSONPath queries
│ ├── pdf_reader.rs - PDF text extraction
│ ├── archive.rs - ZIP/TAR/TAR.GZ archive handling
│ ├── http_tools.rs - HTTP/HTTPS requests + batch
│ ├── s3_tools.rs - AWS S3 operations + batch
│ ├── stats.rs - File/directory statistics
│ ├── duplicates.rs - Duplicate file detection
│ ├── process.rs - Process execution and management
│ ├── xlsx.rs - Excel file reading (calamine)
│ ├── docx.rs - Word document reading (docx-lite)
│ ├── llm/ - LLM provider integrations (Gemini, Cerebras, OpenAI)
│ └── wave2/ - System utilities:
│ ├── net.rs - Network tools (port_users, net_connections, port_available)
│ ├── proc.rs - Process tools (proc_tree, proc_env, proc_files)
│ ├── sys.rs - System info (disk_usage, sys_info)
│ ├── file.rs - File tools (file_diff, file_touch)
│ └── util.rs - Utilities (clipboard, env_*, which)
tests/
├── integration.rs - MCP tool integration tests
└── http_transport.rs - HTTP server tests
HTTP tests spawn server subprocess and verify endpoints:
#[tokio::test]
async fn test_http_server_health_check() {
// Start server on random port
// Poll /health until ready
// Assert response
}- stdio:
rmcp::transport::stdio()— log file only (never stderr; that would close the handshake) - HTTP:
StreamableHttpService+LocalSessionManager- SSE streaming
rmcp 3.4.0- MCP SDK (features:transport-io,server,macros,transport-streamable-http-server)axum 0.8- HTTP server frameworktokio- Async runtime
Important: Claude Code on Windows requires git-bash. If git is installed but bash is not in PATH, set the environment variable:
# PowerShell (run as user, not admin)
[Environment]::SetEnvironmentVariable('CLAUDE_CODE_GIT_BASH_PATH', 'C:\Program Files\Git\bin\bash.exe', 'User')Or if git is installed elsewhere, find it with:
where git.exe
# Example output: C:\Programs\Git\bin\git.exe
# Then set: C:\Programs\Git\bin\bash.exeRestart your terminal after setting the variable.
Build and install the binary:
cargo build --release
# Or install globally:
cargo install --path .Unix/Linux:
claude mcp add filesystem -- filesystem-mcp-rs /projects /tmp /home/user/workWindows (using full path):
claude mcp add filesystem -- "C:/path/to/filesystem-mcp-rs/target/release/filesystem-mcp-rs.exe" "C:/projects"Important: Do NOT use --log-level or other flags when adding via claude mcp add - they are not supported by the executable. Only pass directory paths.
Edit ~/.config/claude-code/config.json (Unix/Linux) or C:\Users\<username>\.config\claude-code\config.json (Windows):
stdio mode (default):
{
"mcpServers": {
"filesystem": {
"command": "filesystem-mcp-rs",
"args": ["/projects", "/tmp"]
}
}
}stdio with logging:
{
"mcpServers": {
"filesystem": {
"command": "filesystem-mcp-rs",
"args": ["-l", "mcp-server.log", "/projects"]
}
}
}HTTP stream mode:
{
"mcpServers": {
"filesystem-http": {
"command": "filesystem-mcp-rs",
"args": ["-s", "-p", "8000", "-b", "127.0.0.1"]
}
}
}HTTP with custom port and logging:
{
"mcpServers": {
"filesystem-http": {
"command": "filesystem-mcp-rs",
"args": ["-s", "-p", "9000", "-l", "http-server.log"]
}
}
}Check that the server is connected:
claude mcp list
# Should show: filesystem: ... - ✓ ConnectedFor Claude Desktop, use the same format in claude_desktop_config.json.
Install the binary:
cargo install --path .Edit ~/.codex/config.toml (Unix/Linux) or C:\Users\<username>\.codex\config.toml (Windows):
stdio mode (default):
[mcp_servers.filesystem]
command = "filesystem-mcp-rs"
args = ["/projects", "/tmp"]stdio with logging:
[mcp_servers.filesystem]
command = "filesystem-mcp-rs"
args = ["-l", "codex-mcp.log", "/projects"]HTTP stream mode:
[mcp_servers.filesystem_http]
command = "filesystem-mcp-rs"
args = ["-s", "-p", "8000"]HTTP with custom settings:
[mcp_servers.filesystem_http]
command = "filesystem-mcp-rs"
args = ["-s", "-b", "0.0.0.0", "-p", "9000", "-l", "http-codex.log"]Note: Use forward slashes (C:/path) or double backslashes (C:\\path) in TOML strings on Windows.
- Default: paths are canonicalized; symlinks escaping the allowlist are rejected.
--allow_symlink_escape: if a symlink itself is inside the allowlist, operations may follow it even if the target is outside.- Tools always validate paths; no raw "operate on the link itself" mode yet. If you need non-follow (operate on the link inode), we can add an opt-in flag per tool.
src/main.rs— MCP server + toolssrc/core/path.rs— path validation/escape protectionsrc/tools/fs_ops.rs— read/head/tailsrc/tools/edit.rs,src/tools/diff.rs— text-based edits + unified diffsrc/tools/line_edit.rs— line-based surgical editssrc/tools/bulk_edit.rs— mass search/replace across filessrc/tools/search.rs— glob search with type/size/time filterssrc/tools/grep.rs— regex content search with invert/count modessrc/tools/binary.rs— binary file operations (read/write/extract/patch)src/tools/hash.rs— file hashing (MD5/SHA1/SHA256/SHA512/XXH64)src/tools/compare.rs— file and directory comparisonsrc/tools/watch.rs— tail file and watch for changessrc/tools/json_reader.rs— JSON reading with JSONPath queriessrc/tools/pdf_reader.rs— PDF text extractionsrc/tools/archive.rs— ZIP/TAR/TAR.GZ archive handlingsrc/tools/http_tools.rs— HTTP/HTTPS tools (feature)src/tools/s3_tools.rs— S3 tools (feature)src/tools/file_stats.rs— file/directory statistics (parallel walk, per-child sizes, index-backed summaries)locate-rs/src/aggregate.rs(SSH dependency) — per-directory subtree totals, independent of walk orderlocate-rs/src/query.rs(SSH dependency) — the read-only SQL behindlocate_sql(views, authorizer, time limit)src/tools/duplicates.rs— duplicate file detectiontests/integration.rs— per-tool integration coverage
Open to extensions (non-follow symlink mode, extra tools).
This is a Rust port of the official Model Context Protocol filesystem server.
For the JavaScript version, see: https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem