mcp-fs - filesystem and command tools with optional sandboxing
mcp-fs --stdio /path/to/workdir mcp-fs --name project --bind 127.0.0.1:31741 /path/to/workdir
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.
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.
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.
exec, vcs-diff and vcs-status share three presentation options:
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.
Maximum returned stdout lines after filtering, default 2000, maximum 100000.
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.
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.
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.
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.
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).
Run a command with the named sandbox grants. No MCP server or permission rules
are used; the current directory is the workdir. With --sandbox=none the
command runs directly with normal host-user privileges.
Defaults to $SHELL or /bin/sh if no command is supplied.
The -- separator before the command is optional.
Selects permission prompting: helper (default), tty, yad, mcp, or auto.
tty and yad force the corresponding built-in interface without fallback.
Overrides PERL_ANYEVENT_MCP_PERMISSION_ASK, which can configure the
default for all servers using AnyEvent::MCP::Server::Permissions.
The default keeps approval local. mcp delegates to client form elicitation;
auto uses elicitation when advertised and otherwise the local helper.
Select bwrap (default), auto, or none. bwrap requires a usable
bubblewrap executable. auto uses bubblewrap if found, otherwise runs
unsandboxed; it does not fall back if an existing executable fails.
none deliberately 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.
Selects the bubblewrap executable. Defaults to bwrap in PATH.
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.
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.
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.
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.
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.
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.