NAME
mcp-fs - filesystem and command tools with optional sandboxing
SYNOPSIS
mcp-fs --stdio /path/to/workdir
mcp-fs --name project --bind 127.0.0.1:31741 /path/to/workdir
DESCRIPTION
mcp-fs gives an MCP client tools to read, search and change files, and run commands. An assistant can use these tools to work on your projects without you writing a Perl server.
Start it with one or more project directories, called workdirs. The client connects to the server, lists its tools and makes requests. A request can select one of these workdirs; with only one, the selection can be omitted. Tools operate on files on disk. To work with unsaved Vim buffers, use mcp-vim.
Before starting an operation, the server checks permission rules and, when needed, asks you for approval. Operations normally run in a bubblewrap sandbox with the approved access. Workdirs provide the default filesystem scope; operations needing access elsewhere require the corresponding sandbox grants.
You can connect an MCP client directly over HTTP or stdin/stdout, or run the server through mcp-multibroker. See AnyEvent::MCP::Intro for a walkthrough from installation to a first filesystem request.
FIRST USE
Configure your MCP client to launch the server with an existing project:
mcp-fs --stdio --name project /home/alice/src/project
The client starts the process and speaks MCP over stdin/stdout. Do not run another copy in a terminal. For HTTP clients, run without --stdio and use --print-url with the same --name and --bind settings; see "YOUR FIRST FILESYSTEM SERVER" in AnyEvent::MCP::Intro.
To inspect the tools available with your installed utilities without starting a listener, run mcp-fs --tool-help /home/alice/src/project.
Once connected, ask the assistant:
Show the configured workdirs and list my project's top-level files.
Read its README if present. Do not change files or run commands.
workdirs shows the available paths without a worker. Listing requests inspect permission, and reading a file requests read. Allow each once to try them. Approval is specific to the requested action and sandbox grants. If no dialog appears, inspect the tool error and server stderr: the helper needs a YAD display or usable terminal. See "PERMISSIONS".
For a separate sandbox check without MCP, change to your project directory and run:
mcp-fs --exec-cmd workdir-read -- ls
This uses the current directory as the workdir. It tests the sandbox, not client connectivity or permission prompting.
TOOLS
workdirs reports configured workdirs and available sandbox grants. list and find enumerate directory entries; stat returns lstat metadata without following symlinks. file-read reads byte ranges and returns Base64 data; read-lines reads LF-delimited lines with selectable UTF-8, Latin-1 or Base64 representation. grep searches files using a literal pattern or PCRE. file-write writes Base64 bytes at an offset without implicit truncation; path-op performs pathname mutations; file-patch applies a unified diff to a single file.
exec runs an argv array, not a shell command string. To use shell syntax, explicitly invoke a shell in that array. stdin_encoding selects utf-8 (default), latin1 or base64. The 4 MiB limit applies to the resulting stdin bytes. Stdout and stderr are decoded as UTF-8, with a raw capture limit of 1 MiB per stream; use the byte-range tools for binary file contents. Its optional sandbox_grants replaces the default workdir-read grant rather than adding to it. Explicitly passing an empty array therefore asks for no grants. Command exit codes are normal tool results, not transport errors. On timeout, output collection stops and truncated is true; exit and signal still describe the reaped child. Perl is required for normal server operation; missing optional GNU utilities can cause their tools to be omitted. The command requested by exec is resolved when the worker runs; it need not be present when the server starts. Inspect tools/list rather than assuming every tool is present.
Command output
exec, vcs-diff and vcs-status share three presentation options:
- grep => $pattern
-
Optional PCRE line filter for stdout. It runs in a separate timed GNU grep worker after command output has been captured. It is not passed to the original command or a shell. Stderr remains unfiltered.
- max_lines => $integer
-
Maximum returned stdout lines after filtering, default 2000, maximum 100000.
- max_bytes => $integer
-
Maximum returned UTF-8 bytes per stream, default 65536, maximum 1048576. Clipping does not leave an incomplete UTF-8 character. Stderr has its own budget, so a large stdout does not hide diagnostics.
The command runs once and is drained to completion, subject to its timeout. Raw capture is bounded to 1 MiB per stream, independently of the returned output limits. capture_truncated means bytes were omitted before filtering or collection ended on timeout: matches in that omitted output are unknown. stdout_truncated, stderr_truncated and the combined truncated flag also account for returned-output limits.
Changing presentation options does not change the command's permission subject. exit, signal and timed_out always describe the original command, including a diff exit code indicating differences. A filter error makes the tool result an error and adds filter_error, while retaining that command status. The command may already have completed; do not rerun it merely because its output filter failed.
Version control
vcs-diff and vcs-status take a required backend: git, svn, cvs or hg, a workdir, an optional cwd (default .), and an optional files array. cwd selects a directory within the workdir; it does not change the granted mount boundary. Files are relative to cwd and must stay within the selected workdir. The worker checks its resolved directory after chdir and before starting the client, rejecting symlink escapes. Backend selection is explicit. With no files, the command examines the working copy at cwd. Files must be inside the selected workdir; they are passed as literal filenames, not options, revisions or shell code. Git diff shows unstaged changes; SVN, CVS and Mercurial diff compare the working copy with its base. No commit, update, fetch or push is performed.
These tools have separate vcs-diff and vcs-status permission actions. They request workdir-read for the selected working copy. CVS additionally requests ssh-agent, which includes network access. Git, SVN and Mercurial perform these local operations without network or home access. A saved approval can cover the fixed operation regardless of filenames or output filters, for that workdir and backend, or all supported backends. Configured VCS clients and their configuration are trusted, including any helpers they invoke; these tools do not sandbox the client's own semantics. The ordinary sandbox grants still bound its host access. In unsandboxed mode, the normal full-authority approval requirement applies.
Clients are resolved at startup. executable.git, executable.svn, executable.cvs and executable.hg in trusted program/instance settings can override them. A missing client produces a tool error for that backend.
REQUIREMENTS
Bubblewrap is required by default, but can be disabled explicitly. Perl 5.8.4 or later must run in the worker environment at startup (except for --exec-cmd). GNU find, grep, patch and sed are optional; tools requiring an unavailable utility are omitted. Use tools/list to see which tools are available.
CONNECTIONS
HTTP is the default and does not print its URL on startup. --print-url prints the credential-bearing URL; the same token works in a bearer header. --stdio or PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL=stdio selects stdin/stdout. See "GLOBAL CONFIGURATION" in AnyEvent::MCP for named HTTP settings.
OPTIONS
Common options, including --name, --bind, --token, --stdio, --connect, --print-url, --auto-allow and --auto-deny, are documented in "mcp_getoptions" in AnyEvent::MCP::Server::Simple. The default service-instance name is default; --name also selects the filesystem permission-rule file described below. Unlike ordinary startup, --print-url does not require a workdir. --tool-help prints the tools available with the selected configuration and installed utilities, including argument types and descriptions, then exits without starting the MCP listener. Supply the usual WORKDIR arguments.
For example, --auto-allow '*,inspect,workdir-read' together with --auto-allow '*,read,workdir-read' approves only those action classes within workdir-read authority. --auto-allow '*,*,workdir-write' approves any action needing at most workdir-write authority (including read and no grants).
- --exec-cmd SANDBOX-GRANT,...
-
Run a command with the named sandbox grants. No MCP server or permission rules are used; the current directory is the workdir. With
--sandbox=nonethe command runs directly with normal host-user privileges. Defaults to$SHELLor/bin/shif no command is supplied. The--separator before the command is optional. - --ask $method
-
Selects permission prompting:
helper(default),tty,yad,mcp, orauto.ttyandyadforce the corresponding built-in interface without fallback. OverridesPERL_ANYEVENT_MCP_PERMISSION_ASK, which can configure the default for all servers usingAnyEvent::MCP::Server::Permissions. The default keeps approval local.mcpdelegates to client form elicitation;autouses elicitation when advertised and otherwise the local helper. - --sandbox MODE
-
Select
bwrap(default),auto, ornone.bwraprequires a usable bubblewrap executable.autouses bubblewrap if found, otherwise runs unsandboxed; it does not fall back if an existing executable fails.nonedeliberately runs workers with full host-user privileges.In unsandboxed mode every MCP worker (including read-only tools) requires approval for the actual maximum execution authority: world-write, network, shared-tmp and host-proc. Narrower approvals do not authorize execution; the server does not pretend that workdir mounts restrict direct processes. Startup warns prominently when there is no sandbox.
- --bwrap PATH
-
Selects the bubblewrap executable. Defaults to
bwrapinPATH.
Rules reside in $XDG_CONFIG_HOME/anyevent-mcp/rules/fs-NAME.ini, or the corresponding $HOME/.config path. The rules filename shares the service-instance name but has an fs namespace. The default instance uses rules/fs-default.ini. The rules path is printed on stderr when starting HTTP; normal startup leaves stdout untouched.
TRUSTED CONFIGURATION
In ~/.config/anyevent-mcp/global.ini (or the corresponding XDG_CONFIG_HOME location), [program NAME] supplies defaults based on the executable basename, while [instance NAME] overrides them for --name NAME. A symlinked executable therefore has its own program defaults.
[program mcp-fs]
mount.opt = ro:/opt
prepend-path = /opt/bin:/opt/sbin
mount.shared = rw:/srv/shared
env.cvs = CVS_RSH=/my/cvs/rsh
executable.perl = /opt/bin/perl
[instance myproject]
mount.opt =
prepend-path =
mount.extra = ro:/srv/project-tools
Automatic permission rules can also be configured:
[program mcp-fs]
auto-rule.10 = allow *,read,workdir-read
auto-rule.20 = allow *,write,workdir-write
auto-rule.30 = deny *,exec,*
These apply to every named mcp-fs instance; CLI auto-rules are evaluated first.
mount.SUFFIX entries are applied in alphabetical key order after the private sandbox mounts. The suffix is an arbitrary stable name; an empty instance value suppresses an inherited entry. Paths must be absolute and exist. A colon after ro: or rw: starts the literal pathname; no separate quoting or escaping syntax is used.
env.SUFFIX entries contain NAME=value to set a literal value, or NAME to inherit that variable from the server environment if present. Values may contain spaces and equals signs; there is no shell expansion. Entries are applied in alphabetical key order after the default environment. An empty instance entry suppresses the corresponding program entry. SSH_AUTH_SOCK is managed exclusively by the ssh-agent grant, even when listed in these settings. Environment settings do not expose paths; custom executables outside the runtime/workdir mounts need configured mounts.
prepend-path adds colon-separated directories before the bubblewrapped worker's fixed PATH of /bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin. Trailing colons are ignored, so both /opt/bin and /opt/bin: work. The server's PATH is not inherited. An empty instance value restores the fixed default rather than the program value. --sandbox=none continues to use the host environment. Directories outside the normal runtime mounts still need a configured mount to be accessible.
Configured mounts grant baseline access to every sandbox invocation, independently of tool permission grants. With world-read, only rw: exceptions are applied over the read-only host filesystem. With world-write, these configured mounts are ignored. Baseline execution paths include /usr, /bin, system libraries and selected system configuration, but not /opt. Shared host temporary directories require shared-tmp, a configured mount, or a world-* grant.
executable.find, executable.grep, executable.patch, executable.perl, executable.sed, and executable.bwrap override automatic executable discovery. Explicit choices must resolve to executable files and are trusted instead of version-probed, except for executable.perl, which must satisfy the Perl minimum version. executable.perl defaults to perl in the worker's PATH; the other worker executables are discovered there too. --bwrap overrides executable.bwrap and is located on the host.
Defaults also include alternatives, font configuration, local timezone, TLS certificate stores, NSS configuration and DNS configuration where present. Unless world-read or world-write is granted, a synthetic passwd entry is supplied without exposing the host passwd or group databases. With either world grant, the host databases remain visible.
The workdirs result includes workdir_vcs, a mapping from configured workdir identities to arrays of backends with metadata markers directly in that directory. Detection checks CVS, .git, .hg and .svn; a Git metadata file is supported as well as a directory. Symlink markers are reported without following them. This is a hint, not validation of a repository. There is no recursive scan or parent search, multiple markers are retained, and an empty array means no marker was found. Detection is independent of the available clients in vcs_backends and does not choose the backend for a VCS command.
SANDBOX GRANTS
workdir-read and workdir-write expose only the selected workdir; write implies read. all-workdirs-read and all-workdirs-write expose every configured workdir. all-workdirs-read combined with workdir-write mounts all workdirs read-only except the selected one, which is writable. all-workdirs-write makes all writable. Configured workdirs are mounted using resolved absolute paths, but their permission-rule identities are verbatim command-line arguments. Persistent permission rules default to the selected workdir, with an explicit option to match any configured workdir, including future additions. This controls reuse of the rule, not the workdirs accessible in one operation. The multibroker alias is shown in permission prompts, without changing the rule file. Tools accept workdir as a persistent positive integer ID, a numeric string containing that ID, or an exact configured or resolved workdir path. workdirs returns workdirs and resolved_workdirs objects keyed by ID, not arrays indexed by argument position. IDs are shared across server instances in $XDG_CONFIG_HOME/anyevent-mcp/fs-workdir-ids.json, or under $HOME/.config when XDG_CONFIG_HOME is unset. Resolved absolute paths determine IDs; permission-rule identities remain the verbatim arguments.
The registry stores ID => [workdir, last_use] pairs, with last_use as a Unix timestamp. Starting a server resolves its IDs once and reserves any new allocations under the file lock. An AnyEvent timer runs immediately and every 5.25 days, rereading the registry and overwriting those fixed assignments with fresh timestamps. Other entries expire after seven days without refresh; their IDs can be reused. Restart servers after manually changing the registry. Numeric IDs therefore survive restarts and argument reordering while retained, but are not permanent references to paths that have expired.
Non-numeric strings are matched only against configured workdirs, never interpreted as arbitrary filesystem paths. Unknown or ambiguous selectors fail. With only one workdir, the field may be omitted. Overlapping workdirs have unspecified mount order and access effects; do not rely on overlapping workdir boundaries for isolation. homedir-read and homedir-write similarly control HOME. world-read and world-write expose the host filesystem, with write implying read. network and shared-tmp independently enable networking and the host's /tmp and /var/tmp. long-running permits exec timeouts over 300 seconds. host-proc is unavailable with bubblewrap; direct execution has host-process access.
ssh-agent implies network. It exposes the server's SSH_AUTH_SOCK at /run/anyevent-mcp/ssh-agent and sets the child's variable to that path. It also exposes ~/.ssh/known_hosts read-only if present, without mounting the rest of HOME. Private key files and SSH user configuration are not exposed. An unset or empty SSH_AUTH_SOCK exposes no socket, allowing rsh-based CVS; a configured socket must be an absolute path naming an existing socket node. Socket replacement requires a new invocation; an existing bind mount keeps the old node. Agent access permits authentication, not retrieval of private keys.
LIMITATIONS
With bubblewrap, regardless of granted sandbox grants, the sandbox mounts procfs at /proc. Its process entries are restricted by the PID namespace, but kernel-wide and hardware information (including /proc/irq and /proc/bus) remains visible. This is a limitation of bubblewrap's --proc option, which does not support subset=pid.
PERMISSIONS
AnyEvent::MCP::Server::Permissions applies ordered allow, deny and ask decisions to server-defined action classes. inspect groups stat/list/find; read groups file-read/read-lines/grep; write groups file-write/file-patch; manage covers path-op, and exec covers command execution. file-write may create files despite being in the write class; manage refers specifically to pathname operations rather than an exclusive right to create files. Filesystem operations have no path-based persistent subjects: their rules match action, sandbox-grants, and workdir when selected-workdir authority is required. All-workdirs-only requests have no workdir identity. Rules without workdir can apply to any selected workdir if their sandbox grants cover the request; dialog-generated selected-workdir rules are exact. Requested paths are shown in the approval dialog for information only. Exec additionally offers persistent command/argv subjects. When stdin is provided, exec attaches display-only details to the permission request: Unicode text for UTF-8 input, or a bounded escaped-byte preview for Latin-1/Base64 input. The YAD helper offers a separate View stdin... window; the preview shows input size and truncation. This does not change authorization: persistent exec rules match argv, not stdin, and later runs may supply different scripts or bytes. Conversion takes place before approval and the worker receives the exact resulting bytes. file-patch supplies View patch details with the target and supplied diff. Base64 patches are decoded for display: valid UTF-8 is shown as Unicode, otherwise non-printable bytes and backslashes are escaped in a byte view. The UI chooses whether to show a separate viewer or an inline preview. Each tool checks permissions before starting its worker. Approval defaults to the local helper selected by PERL_ANYEVENT_MCP_PERMISSION_HELPER, which tries YAD, then a terminal, then denies once. Client elicitation is selected with --ask mcp, --ask auto, or the corresponding PERL_ANYEVENT_MCP_PERMISSION_ASK value.
You can edit the rule file at any time. It is reread on permission checks, so changes apply without restarting the server. Keep it owned by your user and private (mode 0600). See AnyEvent::MCP::Server::Permissions for the rule syntax and ordering.
Permission retries retain earlier one-time approvals within the same call. For one worker accessing several paths, the required sandbox-grant set combines the authority needed for all paths and is approved once. An insufficient reduction fails before the worker starts. Multi-path stat requires all paths inside its selected workdir. path-op can attempt cross-workdir renames after approving the sandbox grants required for both paths. The sandboxed rename(2) may fail with EXDEV across mounts; no copy/delete fallback is attempted. mkpath requires a path inside the selected workdir. path-op performs requested operations in this fixed order: unlink, rmdir, mkpath, mkdir, symlink, truncate, chmod, rename. mkpath includes each missing component in the combined permission check before starting the worker. The first failure stops subsequent operations; successful earlier mutations remain in place. There is no rollback or all-or-nothing transaction.
Exec defaults to a 30-second timeout and permits up to 300 seconds without additional timeout authority. A larger explicit timeout, up to 21600 seconds (six hours), also requires long-running. This capability is added to the requested permissions automatically. Cancellation still terminates the worker, regardless of its allowed timeout.
The host-proc grant is unavailable with bubblewrap. Runtime mounts provide sandbox machinery. Symlinks resolve within granted mounts. Without bubblewrap, workers operate in the host mount and process namespaces.
PATCHES
file-patch uses "PATCH UTILITIES" in AnyEvent::MCP for normalization and validation. GNU patch runs inside the granted sandbox with fuzz zero. Successful output replaces the original by rename after checking its identity, size, timestamps, metadata and link count. These stat checks do not detect every concurrent edit; they are not a filesystem transaction.