Safe Code Sandbox
Drives Docker to run untrusted, model-generated Python code in throwaway locked-down containers. The server validates client-specified limits (memory, timeout, CPUs) and builds a container with no network, no host filesystem, and a non-root user, copying code in as a tar after creating but before starting the container, then destroying it on every exit path. It manages container lifecycle (create, start, stop/remove) and reports run outcomes such as succeeded, failed, timed_out, out_of_memory, stopped, or sandbox_error.
Click on "Deploy 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., "@Safe Code Sandboxrun this Python code and tell me what it prints: print(sum(range(10)))"
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.
Safe Code Sandbox
An MCP server that runs untrusted, model-generated Python in a locked-down throwaway container, and reports back what happened.
AI agents write code. Running that code directly on a machine is unsafe: it can loop forever, eat all the memory, delete things, or be the payload of a prompt injection. This gives an agent somewhere to run it and get a result, without giving it the host.
The idea in one line
The model gets three tools. It never gets Docker.
MCP client (Claude Desktop, Cursor, your agent)
│ start_run / get_result / stop_run
▼
Safe Code Sandbox ── validates limits, builds a locked-down container
│
▼
Docker ── no network, no host paths, not root, destroyed after the runEvery restriction lives in server code, not in a prompt. A prompt is a request; code is a rule.
Related MCP server: Docker MCP Server
Tools
Tool | Does |
| Starts a run, returns a |
|
|
| Kills the run and removes the container |
Runs are asynchronous because a job can legitimately take an hour, and MCP has
no way to hold a call open that long. The run_id is the handle.
start_run(code="print(sum(range(10)))")
# → {"run_id": "8f2c...", "status": "running", "limits": {...}}
get_result("8f2c...")
# → {"status": "succeeded", "exit_code": 0, "stdout": "45\n", ...}Retrying a failed run is the agent's job, not the sandbox's. The sandbox reports; the agent decides.
Statuses
The distinctions here are the point. An agent deciding what to do next needs to know why a run stopped.
Status | Meaning | What a client should do |
| Exit code 0 | Use the output |
| Non-zero exit | Fix the code |
| Hit the wall-clock limit | Make it faster, or ask for more time |
| Killed by the memory cap | Ask for more memory, up to the max |
| Cancelled via | Nothing |
| The sandbox itself broke | Not your code; needs a human |
Limits
The client chooses, the server enforces.
Default | Maximum | |
Memory | 5 GB | 16 GB |
Timeout | 10 minutes | 1 hour |
CPUs | 2 | 4 |
Processes | 256 | not client-settable |
Asking for more than the maximum is rejected, not quietly reduced:
Requested 32 GB of memory, max is 16 GBClamping would be worse than failing. A client that asked for 32 GB and silently got 16 would read the resulting out-of-memory as a bug in its own code.
At most four runs execute concurrently, because four runs each individually allowed 16 GB is 64 GB.
Isolation
Short version: no network at all, no host filesystem, never root, all Linux capabilities dropped, memory and CPU and process count capped, scratch space mounted non-executable, container destroyed on every exit path.
assert_posture() re-checks the critical flags before each container is
created, so a refactor that drops one fails the run instead of silently
weakening the sandbox.
docs/SECURITY.md is the real document, including what this does not protect against. The headline limitation: containers share the host kernel, so a kernel escape is not prevented by any of the above. If you need a boundary that survives that, you want a VM, not a container.
Install
Requires Docker and Python 3.11 or newer.
git clone https://github.com/evrabhinav/safe-code-sandbox.git
cd safe-code-sandbox
python -m venv .venv
.venv/bin/pip install -e ".[dev]" # Windows: .venv\Scripts\pip
docker build -t safe-code-sandbox:py312 docker/The image is deliberately plain: a Python interpreter, a non-root user, and a
workspace owned by them. No package manager cache, no credentials, and no
CMD, so the image cannot decide what runs.
Run it
safe-code-sandbox # speaks MCP over stdioIn an MCP client's config:
{
"mcpServers": {
"sandbox": {
"command": "/path/to/.venv/bin/safe-code-sandbox"
}
}
}Design decisions
The server owns Docker; the model gets high-level tools. The model cannot name an image, a mount, a network or a capability.
Code travels in the request, not through a mount. The container is created but not started, the files are copied in as a tar, and then it starts. That ordering is what makes it possible to never expose a host path.
Fresh container per run. Nothing carries between runs, so one run cannot leave a payload for the next.
Limits rejected, not clamped. See above.
The sandbox does not retry. It reports precisely enough for the agent to decide. A sandbox that retried would hide the signal the agent needs.
Tests
.venv/bin/pytest83 tests, no Docker required. The Docker client is injected, so a fake drives the control flow: the four outcomes, the create-copy-start ordering, and the guarantee that a container is removed even when copying the code fails.
tests/test_sandbox.py is the one worth reading. It asserts each isolation
flag individually, names what each one stops, and checks that assert_posture
rejects a spec with any of them removed.
Known limitations
No integration tests against a real daemon. Docker was not available on the machine this was written on, so the control flow is covered by a fake and the container specification is asserted structurally. The flags are correct per Docker's API, but nobody has yet watched a real container get OOM-killed here. That is the first thing to add.
Python only. The entrypoint is python -I main.py.
No internet, not even opt-in. Which means no pip install inside a run.
No auth. Whoever can reach the MCP server can run code. Appropriate for a local tool, not for a shared deployment.
In-memory run registry. Reports are kept for an hour and lost on restart. Deliberate: a sandbox that accumulates other people's code on disk is a worse problem than one that forgets.
License
MIT. See LICENSE.
Available Tools
3 toolsget_resultA
Collect the result of a run, or find out it is still going.
While the run is in progress this returns status "running". Once it has finished it returns the full report: status, exit code, stdout, stderr and how long it took.
A status of "timed_out" or "out_of_memory" means the code hit a limit rather than crashing, so retrying with a larger budget may help.
| Name | Required | Description | Default |
|---|---|---|---|
| run_id | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does well: it enumerates the return states (running vs finished) and the payload fields (status, exit code, stdout, stderr, duration), and interprets two terminal statuses as resource limits rather than crashes. It omits whether output is truncated, how long results are retained, and whether repeated calls are safe.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the core purpose, then return payload, then the interpretation of failure statuses. Three short blocks, none redundant, every sentence earning its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
No output schema and no annotations, and the description does the work of documenting return shape and status semantics. The remaining gap is the parameter's origin/format and result retention, which matters for a polling tool but is secondary.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0% and run_id has no description in the schema, so the description must compensate. It never mentions run_id, its format, or that it comes from start_run, leaving the sole parameter entirely undocumented outside its name.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource ("Collect the result of a run") and adds the polling nuance ("or find out it is still going"). That inherently separates it from start_run/stop_run, though the description never names the siblings, so full differentiation credit is withheld.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives clear context for when the tool is meaningful: while the run is in progress it reports "running", after completion it returns the report. It also explains what a timed_out/out_of_memory status implies for follow-up (retry with a larger budget). It does not explicitly say to poll repeatedly or name start_run/stop_run as alternatives.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
start_runA
Run Python code in a locked-down, throwaway container.
The container has no network access, no access to any host directory, and does not run as root. It is deleted when the run ends.
Returns a run_id immediately; the code is still running. Poll get_result with that id to collect the output.
code: the program to execute, placed at main.py in the workspace. files: optional extra input files, as relative paths and contents. limits: optional memory_mb, timeout_seconds and cpus. Defaults are {default_mem} MB and {default_timeout} seconds; the ceilings are {max_mem} MB and {max_timeout} seconds. Asking for more than a ceiling is rejected rather than silently reduced.
| Name | Required | Description | Default |
|---|---|---|---|
| code | Yes | ||
| files | No | ||
| limits | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does so well: no network, no host directory access, non-root, container deleted at run end, and ceilings are rejected rather than silently clamped. The weakness is the unfilled template placeholders ("{default_mem} MB", "{default_timeout} seconds", "{max_mem}"), which void the most decision-relevant numbers an agent needs before choosing limits.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the sandbox contract, then the async return, then per-parameter notes in a clean parallel structure. Each sentence adds information; the only waste is the placeholder tokens, which occupy space without conveying the values they were meant to carry.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For an async 3-parameter job-submission tool with no output schema, the description covers isolation, lifetime, resource caps and the polling handoff. It does not say how long results remain retrievable or whether stdout/stderr and exit status are surfaced, which matters given no output schema documents the return shape.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, so the description must compensate, and it largely does: code is placed at main.py, files are relative paths plus contents, and limits enumerates memory_mb, timeout_seconds and cpus with the over-ceiling rejection rule that the schema does not express. The unresolved placeholder values for defaults and ceilings leave the limits semantics partially undetermined.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ("Run Python code") and immediately bounds the environment (locked-down, throwaway container). It also distinguishes its own async semantics from the sibling get_result by explaining the run_id/poll handoff, so an agent can tell it apart from get_result and stop_run without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Clear context for use: submit code, receive a run_id, then poll get_result to collect output. It does not, however, name stop_run as the alternative when a run must be cancelled, nor state any when-not conditions, so the routing is complete only for the happy path.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
stop_runA
Cancel a run in progress, killing and deleting its container.
Safe to call on a run that has already finished; it reports the status it ended with instead of failing.
| Name | Required | Description | Default |
|---|---|---|---|
| run_id | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the safety profile itself and does so well: it discloses the destructive side effect (container killed and deleted) and the idempotent non-failure behavior on completed runs. It omits authorization requirements and whether the run's results/artifacts survive, which keeps it below 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two short sentences with the destructive effect front-loaded and the edge-case behavior second. Nothing redundant, nothing padded.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter mutation with no output schema, the description covers purpose, side effects, and the repeat-call case, including what is returned when the run already finished. Only success-path return information and permission expectations are missing, which is acceptable for this complexity.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0% and run_id has no description, but the single parameter is self-evident from the tool's purpose. The description adds nothing about identifier format or source, so it neither compensates nor misleads.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource plus the concrete effect ('Cancel a run in progress, killing and deleting its container'). It is unmistakable against siblings start_run and get_result, since neither could be confused with cancellation.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives an explicit safe-use condition: calling it on an already-finished run returns the final status rather than failing. It stops short of naming alternatives, but for a terminal cancel operation there is no real competing tool to route to.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
v0.1.0- First observed
get_result - First observed
start_run - First observed
stop_run
TDQS
Scored across 3 tools
Each tool has a clearly distinct role in the run lifecycle: start_run launches execution, get_result polls or collects output, and stop_run cancels. There is little risk of confusing one for another, even though get_result and stop_run both report status in some cases.
All three tools follow a consistent verb_noun pattern: start_run, get_result, stop_run. The naming is predictable and readable.
Three tools are well-scoped for an asynchronous sandbox execution model: launch, poll, and cancel. Each tool earns its place without unnecessary surface area.
The core lifecycle is covered: start, poll for results, and cancel. Minor gaps include no list_runs or explicit artifact/output-file retrieval beyond stdout and stderr, but these are workable for the stated purpose.
Maintenance
Related MCP Connectors
Run AI agent tasks in isolated sandboxes with a cost cap, saved workflows and scoped connections.
Cloud Linux VMs for your agent: run code, Docker, files and HTTPS previews in a Sandboxer microVM.
Run Python code in a secure sandbox without local setup. Declare inline dependencies and execute s…
Build Apps and run code in 30 languages — sandboxed, with persistent sessions for agent loops.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables safe Python code execution in isolated Docker containers through the AgentRun framework. Provides automatic container lifecycle management and comprehensive error handling for secure and reproducible code execution.-
- AlicenseNot gradedqualityDmaintenanceEnables LLMs to safely execute code in isolated Docker containers with resource limits and security controls, supporting session management and automatic dependency installation.MIT
- AlicenseAqualityCmaintenanceEnables AI assistants to securely execute Python and C++ code snippets inside disposable, isolated Docker containers and receive structured results.31MIT
- FlicenseNot gradedqualityDmaintenanceEnables AI agents to safely execute Python, JavaScript, and Bash code in an isolated Docker sandbox with strict security constraints.1-