sandbox-mcp
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@sandbox-mcpfetch https://example.com and count the number of links using Python"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
sandbox-mcp
Let an LLM fetch the web, write scripts, and run them — without letting it do those things anywhere near your agent's process.
Policy-driven MCP servers for capabilities that are unsafe to run in-process. What is allowed — domains, languages, resource ceilings, what needs approval — is a policy file, not code.
The one idea
Two servers, split on the axis that matters:
|
| |
Network | egress, allowlisted | none |
Code execution | none | yes |
Neither can both execute arbitrary code and reach the network.
A prompt injection in fetched content has no way to run. Code that runs has no way to phone home. That separation is the whole point, and it is why these are not one convenient server.
Related MCP server: mcp-safeguard
What makes this different
Most sandboxes describe their guarantees. This one attacks itself in CI — 40 tests run against real containers and attempt fork bombs, infinite loops, DNS exfiltration, privilege escalation, audit forgery, path traversal, and SSRF via redirect. Six real bugs were found that way, none of which were visible in review:
--network=nonemakes the MCP port unreachable — and the obvious replacement still resolves DNS, which exfiltrates data one subdomain label at a time.The wall-clock timeout could not kill anything: signalling across uids needs
CAP_KILL.Fork-bomb survivors outlived their call and degraded the sandbox until restart.
RLIMIT_NPROCcounts per-UID system-wide, not per process tree.PATHwas inherited wholesale from the host environment.The audit trail silently wrote nowhere on a uid-mismatched bind mount.
SECURITY.md states the threat model and the non-goals. The non-goals matter as much: a sandbox that oversells itself is worse than one that tells you where it stops.
Status: 0.6.0, early development but in daily use. Both servers, the approval broker, and the adversarial suite are complete, and an independent security audit has been run and acted on (AUDIT-fable5-2026-07-25.md). See CHANGELOG.md and ROADMAP.md.
Quick start: sandbox-exec
podman build -f Dockerfile.exec -t sandbox-exec:dev .
cp policies/exec.example.yaml ./exec.yaml
mkdir -p -m 700 audit approvals
# An *internal* network, not --network=none: the server still has to be
# reachable on its MCP port. See "Networking" below -- this combination is
# what actually blocks egress, and it is verified by the test suite.
podman network create --internal sandbox-net
podman run --rm \
--network=sandbox-net --dns=none \
--read-only \
--cap-drop=ALL --cap-add=SETUID --cap-add=SETGID --cap-add=CHOWN --cap-add=KILL \
--security-opt no-new-privileges \
--memory=512m --cpus=2 --pids-limit=256 \
--mount type=tmpfs,destination=/workspace,tmpfs-size=64m \
--mount type=tmpfs,destination=/tmp,tmpfs-size=16m \
-v ./exec.yaml:/policy/exec.yaml:ro \
-v ./audit:/audit \
-v ./approvals:/approvals \
-p 127.0.0.1:8801:8801 \
sandbox-exec:devRegister it with any MCP client by URL — for example:
hermes mcp add sandbox-exec --url http://127.0.0.1:8801/mcpNetworking
--network=none is the obvious choice and it does not work: it leaves the
MCP port unreachable, so nothing can call the server. What is needed is a
network the host can reach inward on while the container cannot reach
outward:
Flag | Why |
| No gateway or masquerading, so no route off-host. Published ports still reach the server. |
| An internal network still wires up a resolver, and DNS queries leave the host. Without this, |
| A tmpfs mounted over |
| Requires |
| The server runs as container-root but executed code runs as another uid, and signalling across uids needs this. Without it the timeout cannot kill anything and runaway processes survive — an infinite loop still dies on the CPU rlimit, but a sleeping one does not. |
Everything else is dropped: no DAC_OVERRIDE (root cannot bypass file
permissions), no NET_*, no SYS_*.
Verified by tests/security/test_container.py: TCP egress, DNS resolution,
and host-network access are each attempted and must fail, and a fork bomb
must not leave the sandbox degraded.
Quick start: sandbox-fetch
The fetcher is the inverse: it may reach the network, so it never executes anything, needs no capabilities at all, and runs unprivileged from PID 1.
podman build -f Dockerfile.fetch -t sandbox-fetch:dev .
cp policies/fetch.example.yaml ./fetch.yaml
mkdir -p -m 700 audit
podman run --rm \
--read-only \
--cap-drop=ALL \
--security-opt no-new-privileges \
--userns=keep-id:uid=10003,gid=10003 \
--memory=256m --cpus=1 --pids-limit=64 \
-e OLLAMA_BASE_URL=http://127.0.0.1:11434 \
-v ./fetch.yaml:/policy/fetch.yaml:ro \
-v ./audit:/audit \
-p 127.0.0.1:8802:8802 \
sandbox-fetch:dev--userns=keep-id is not cosmetic. Without it the bind-mounted audit
directory is owned by the host user — which maps to uid 0 inside a rootless
container — while the server runs as uid 10003, so every audit write fails.
Records still reach stderr, but the file you would actually go read stays
empty. The server warns about this at startup.
Tools
sandbox-exec — four tools:
tool | what |
|
|
| run source in an allowlisted language |
| shell, only if |
|
|
Nouns with an action argument, not seven verbs: list_files used to be matched
against the ordinary word "List" in an unrelated question, and the agent
answered about the sandbox instead. Schemas also cost ~1.8 KB each in every
prompt.
Command output is capped at limits.max_output_bytes (default 8000) and
lands directly in the caller's context, so it is the most expensive thing this
server produces. Write bulk results to the workspace and read back only what
you need. Truncation keeps the head and the tail, so a traceback at the end
of a noisy run survives.
sandbox-fetch — fetch_url(url). Returns extracted text, or a refusal
naming the rule that fired.
What the fetcher checks, in order
Every one of these re-runs on each redirect hop, not just on the URL you passed. A hop is a request the process actually makes, so a redirect landing on an internal address is a live SSRF no matter where the chain ends.
Check | Notes |
Scheme |
|
Credentials in URL | Always refused: |
Deny list, then allow list |
|
Address | Resolved, then rejected if private, loopback, link-local, multicast, reserved, or unspecified — including IPv4-mapped IPv6 like |
Size | Counted while streaming, never trusted from |
Injection review | Optional, and fails closed: an unreachable reviewer withholds the content. |
The User-Agent is policy-only and deliberately not caller-settable. A header the model controls, sent to an allowlisted host, is an exfiltration channel — the exact property the two-container split exists to prevent.
API routes: ask for the API, not the page
Scraped HTML carries menus, infoboxes, and edit links around the text you wanted. That wastes the caller's character budget, and markup remnants read like injected instructions to a reviewer. Where a site publishes JSON for the same content, policy can say so:
api_routes:
- match: "*.wikipedia.org"
when_path: "^/wiki/(?P<title>.+)$"
url: "https://{host}/api/rest_v1/page/summary/{title}"
extract: ["description", "extract"] # dotted paths, joined in order
on_miss: fallback # or `error` to require the APIMeasured on the same article: 559 characters of clean prose instead of about 8000 of page furniture.
There is no per-site code — a route is four lines of policy, so adding a site is a reviewable diff rather than a release. And a route is a rewrite, never a bypass: the rewritten URL goes through the full allowlist and SSRF check before anything is requested, substituted values are percent-encoded so a title cannot climb out of its path segment, and a template naming a placeholder that does not exist fails at startup rather than on first use.
Policy
See policies/exec.example.yaml and
policies/fetch.example.yaml. Mount it
read-only so the container cannot rewrite its own rules. A missing or
malformed policy is a startup failure — there is no permissive fallback.
Environment variables carry only operational settings (POLICY_PATH,
AUDIT_PATH, HOST, PORT); capability decisions live in the policy file,
where they are structured, reviewable, and not visible in podman inspect.
Approval
The executor has no network, so approval cannot be a webhook. It travels over a bind-mounted directory:
/approvals/pending/<id>.json server writes, then blocks
/approvals/decisions/<id>.json anything can write the verdictTimeout means deny. No transport is baked in — bridging that queue to Telegram, Slack, a TUI, or a web UI is a separate component, which is what keeps this project free of any of them. Approving by hand is just:
echo '{"approved":true,"reason":"looks fine"}' > approvals/decisions/<id>.jsonProviders: deny_all (default), file_queue, command, webhook.
A working broker for the other end ships in
contrib/approval-broker/ — one stdlib-only file,
with console and Telegram adapters. It lives outside src/ on purpose, so no
chat transport can end up in the sandbox image:
python3 contrib/approval-broker/approval_broker.py --queue ./approvalsSkills
The workspace is a tmpfs and dies with the container, so nothing an agent builds survives. A skills directory is durable storage for reusable scripts, off by default:
skills:
enabled: true
path: /skills
writable: false # see below
max_mb: 16Adds the skill tool: list and read always, save only when writable.
writable defaults to false deliberately. A persistent, writable store of
executable code is somewhere a prompt injection can leave something behind for
a later, unrelated call to pick up — a different threat from everything else
here, where every workspace is ephemeral. When it is false the save_skill
tool is not registered at all, rather than registered and failing; an absent
tool cannot be talked into trying. Skills are never executed implicitly: they
are inert files the caller may choose to run through the normal gated tools.
Mount it read-only unless you want the agent curating its own skills, and watch
skill_saved in the audit log if you do:
-v ./skills:/skills:roSecurity
Read SECURITY.md — it documents the threat model and the non-goals, which matter just as much. The short version: the container is the boundary; everything else narrows what happens inside it.
Guarantees are covered by an adversarial test suite rather than asserted:
make check # ruff (incl. bandit rules), mypy --strict, bandit, pip-audit
make test-security # fork bombs, infinite loops, network access, privilege escalationDevelopment
make install # venv + dev extras
make check # lint, types, static analysis, dep CVEs, non-container tests
make build # both images
make test-security # the adversarial suite against real containers
make scan-image # trivy, both imagesmake check must pass before anything is done. See
CONTRIBUTING.md — the one rule is that a change widening
what the sandbox permits arrives with an adversarial test that fails without
it.
Documentation
Threat model, and the non-goals | |
Setup, style, the rule for security changes | |
What is next, and what is deliberately not planned | |
Keep a Changelog, semver | |
The human end of the approval queue |
License
MIT — see LICENSE.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityBmaintenanceA security gateway that enforces policies, tracks data taints, and sandboxes tool calls between AI agents and MCP servers. It provides a secure chokepoint to prevent prompt injection and ensure OWASP ASI compliance through audit logging and deterministic execution.Last updated1Apache 2.0
- Alicense-qualityBmaintenanceAn MCP server that provides safeguard capabilities to protect against prompt injection and unsafe tool calls.Last updated5MIT
- Alicense-qualityBmaintenanceMCP server that provides tools to scan text and URLs for prompt injection attacks, protecting AI agents from adversarial inputs.Last updatedMIT
- AlicenseAqualityBmaintenanceSecure MCP server for quarantined web content extraction with two-layer prompt injection defense, enabling safe fetching of URLs and reading of files via sanitized markdown output.Last updated149AGPL 3.0
Related MCP Connectors
Security firewall for AI agents — scans MCP calls for injection, secrets, and risks.
Security scanner for MCP servers. Detect vulnerabilities, prompt injection, and tool poisoning.
MCP server teaching AI agents to implement TideCloak: auth, E2EE, IGA, security analysis
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Leovilhena/sandbox-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server