A local-first policy firewall and audit proxy for MCP stdio servers.
mcpwall sits between an MCP client and a local MCP server, validates every JSON-RPC request against an inspectable TOML policy, and forwards only requests that pass. It is a small Rust binary with no cloud control plane, telemetry, Docker dependency, database, or network transport.
It is designed for operators who want a boring, reviewable trust boundary around filesystem tools, coding agents, browser agents, automation servers, and private AI infrastructure.
MCP servers can expose powerful capabilities through a convenient tool interface. The operational failure mode is rarely an exotic exploit; it is an over-broad tool, an unsafe path, an accidental secret in logs, a stale capability list, or a destructive call that was forwarded without a human gate.
mcpwall makes those decisions explicit and locally auditable:
- Policy before forwarding — malformed, oversized, unauthorized, or unsafe requests fail closed.
- Least privilege — tool allowlists, denylists, per-tool argument rules, JSON Schema, and path roots.
- Human gates — destructive calls bind to an exact request ID and SHA-256 request hash, expire, and can be consumed once.
- Runtime hardening — optional process groups, environment isolation, resource limits, timeout cleanup,
NoNewPrivileges, seccomp denial rules, and UID/GID dropping. - Evidence — redacted JSONL audit records, capability inventory, status diagnostics, checksums, and release manifests.
- Sovereign operation — local TOML and JSON Schema files; network schema resolution is disabled.
mcpwall is a local policy firewall and process-hardening layer. It is not a complete MCP implementation, container runtime, kernel security boundary, or hosted security service. It cannot protect against root, a malicious same-user process, a compromised kernel/host, or a fully compromised child that finds a vulnerability outside the controls enabled in its policy.
Use it when you need a small, inspectable control point. Use a properly configured container, VM, dedicated service account, or stronger sandbox when you need those guarantees.
- Single-object newline-delimited JSON-RPC 2.0 validation and non-blocking notification support
- Tool allowlists and denylists
- Per-tool allowed and required argument fields
- Per-tool JSON type checks
- External local JSON Schema validation with
$ref/$defs, nested objects, arrays, combinators, enums, patterns, and conditionals supported by the validator - Symlink-aware canonical path enforcement for existing files and parents
- Request and argument byte limits
- Denied argument keys and values
- Per-minute rate limits
- One-time, TTL-bound, SHA-256-bound approvals
- MCP
tools/listinventory capture and optional known-tool enforcement - Redacted JSONL audit logging with restrictive Unix permissions and deep value redaction
- Dry-run / simulation mode (
dry_run = true) for non-blocking onboarding and policy tuning - Optional Linux sandbox process groups and descendant cleanup
- Optional Linux x86_64 seccomp deny filter for selected high-risk syscalls
- Optional Linux mount namespace and read-only root filesystem hardening
- Optional Linux capability bounding-set drops
- Optional non-root UID/GID execution identity with fail-closed validation
doctor,status,inventory,approvals,approve, anddenycommands- Release manifests, SHA-256 checksums, dependency metadata, and CI verification
Build locally:
cargo build --release
./target/release/mcpwall --helpInstall from a source checkout:
PREFIX="$HOME/.local" ./install.sh
"$HOME/.local/bin/mcpwall" --helpCopy and inspect the example policy:
cp mcpwall.example.toml /tmp/mcpwall.toml
./target/release/mcpwall doctor --config /tmp/mcpwall.toml --server filesystemThe example command is intentionally a placeholder. Replace command, args, allowed roots, and audit paths before using it with a real server.
[server.filesystem]
command = "/usr/local/bin/mcp-filesystem"
args = ["/home/scott/projects"]
allowed_tools = ["read_file", "list_directory"]
denied_tools = ["write_file", "delete_file"]
allowed_roots = ["/home/scott/projects"]
audit_path = "/home/scott/.local/state/mcpwall/filesystem.jsonl"
[server.filesystem.tool_policies.read_file]
allowed_arguments = ["path"]
required_arguments = ["path"]
argument_types = { path = "string" }
path_arguments = ["path"]Run the proxy:
./target/release/mcpwall proxy \
--config /tmp/mcpwall.toml \
--server filesystemThe proxy reads requests from stdin and writes responses to stdout. Child stderr is inherited for diagnostics. Keep policy, audit, approval, and inventory files on a private local filesystem.
- Run
doctorand fix every reported configuration error. - Capture the child capability set with
inventory. - Enable
require_known_toolsonly after reviewing the inventory. - Run
statusand confirm audit/approval permissions. - Exercise a representative allowed request and a denied request.
- For destructive tools, capture
request_idandrequest_hash, approve the exact hash, retry once, and verify the approval becomesconsumed. - Keep the proxy and child under the least-privileged suitable account.
See docs/operations.md for installation, rollback, approvals, inventory freshness, sandbox operation, and incident handling.
See docs/threat-model.md for protected assets, assumptions, mitigations, and explicit limitations. Security controls are opt-in where compatibility requires it; the default policy remains conservative but does not silently alter an existing child’s runtime.
The sandbox is opt-in per server:
[server.filesystem.sandbox]
enabled = true
clear_environment = true
environment_allowlist = ["PATH", "HOME", "LANG"]
working_dir = "/home/scott/projects"
timeout_seconds = 120
max_memory_bytes = 1073741824
max_cpu_seconds = 60
max_file_bytes = 104857600
max_open_files = 256
max_processes = 0
seccomp_deny_dangerous = true
mount_namespace = true
read_only_filesystem = true
drop_capabilities = [21, 22]
# run_as_uid = 65534
# run_as_gid = 65534The launcher provides process groups, environment isolation, resource limits, wall-clock cleanup, PR_SET_NO_NEW_PRIVS, selected x86_64 seccomp denial, optional mount namespace/read-only root hardening, capability bounding-set drops, and optional UID/GID dropping. RLIMIT_NPROC is per-user/thread rather than child-only. Mount and capability controls require host privileges such as CAP_SYS_ADMIN; if the host denies them, startup fails closed. This is not a complete container, syscall allowlist, user-namespace mapping, or protection from a privileged host attacker.
Each tagged release produces:
- Linux x86_64 GNU/glibc binary
RELEASE-MANIFEST.jsoncontaining version, target, asset, and SHA-256SHA256SUMScovering the binary, manifest, and dependency metadataDEPENDENCY-METADATA.jsongenerated from the locked Cargo graph- A compressed release archive
- GitHub Actions build provenance when the release workflow runs with repository attestation permissions
Checksums are integrity metadata. They are not a signature. Verify the artifact before installation:
sha256sum -c SHA256SUMSThe default artifact is dynamically linked to glibc; it is not advertised as static.
cargo fmt --all -- --check
cargo check --all-targets
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-targets
cargo audit
cargo build --releaseThe repository includes a deterministic shell MCP fixture under tests/fixture.sh. Runtime hardening probes cover environment isolation, working-directory enforcement, NoNewPrivileges, timeout process-group cleanup, seccomp denial, network namespace fail-closed behavior, and identity-drop behavior.
MIT