Skip to main content
Glama

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 run

Every 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

start_run(code, files?, limits?)

Starts a run, returns a run_id immediately

get_result(run_id)

"running", or the full report once finished

stop_run(run_id)

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

succeeded

Exit code 0

Use the output

failed

Non-zero exit

Fix the code

timed_out

Hit the wall-clock limit

Make it faster, or ask for more time

out_of_memory

Killed by the memory cap

Ask for more memory, up to the max

stopped

Cancelled via stop_run

Nothing

sandbox_error

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 GB

Clamping 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 stdio

In 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/pytest

83 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 tools
get_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.

NameRequiredDescriptionDefault
run_idYes

TDQS

A3.8/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters2/5

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.

Purpose4/5

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.

Usage Guidelines4/5

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.

NameRequiredDescriptionDefault
codeYes
filesNo
limitsNo

TDQS

A4.3/5.0
Behavior4/5

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.

Conciseness4/5

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.

Completeness4/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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.

NameRequiredDescriptionDefault
run_idYes

TDQS

A4.2/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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.

  1. 3 tool updatesv0.1.0
    • First observedget_result
    • First observedstart_run
    • First observedstop_run

TDQS

A4.2/5.0

Scored across 3 tools

Disambiguation5/5

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.

Naming Consistency5/5

All three tools follow a consistent verb_noun pattern: start_run, get_result, stop_run. The naming is predictable and readable.

Tool Count5/5

Three tools are well-scoped for an asynchronous sandbox execution model: launch, poll, and cancel. Each tool earns its place without unnecessary surface area.

Completeness4/5

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

ActivityMaintained
ResponsivenessNo recent community issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables 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.
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables LLMs to safely execute code in isolated Docker containers with resource limits and security controls, supporting session management and automatic dependency installation.
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables AI agents to safely execute Python, JavaScript, and Bash code in an isolated Docker sandbox with strict security constraints.
    1
    -