MCP Server
An MCP server whose tools execute inside a Landlock and seccomp sandbox, one per call. Deny by default: each tool declares the capabilities it needs and gets nothing else. No Docker required.
Install and run
$ cd python && pip install -e '.[mcp]'
# local, stdio transport
$ sandlock-mcp --workspace /tmp/sandbox
# remote, SSE transport
$ sandlock-mcp --transport sse --host 0.0.0.0 --port 8080 \
--workspace /tmp/sandbox
The mcp-remote extra additionally pulls in uvicorn and starlette for the SSE transport.
| Flag | Default | Description |
|---|---|---|
--workspace | auto-created temp dir | The one directory tools may write to |
--transport | stdio | stdio or sse |
--host | 127.0.0.1 | Bind address for the SSE transport |
--port | 8080 | Bind port for the SSE transport |
Client configuration
{
"mcpServers": {
"sandlock": {
"command": "sandlock-mcp",
"args": ["--workspace", "/tmp/sandbox"]
}
}
}
{
"mcpServers": {
"sandlock": {
"url": "http://remote-host:8080/sse"
}
}
}
Built-in tools
Each tool runs in its own sandbox, built fresh for the call, with only the capabilities that tool declares.
| Tool | Arguments | Capabilities granted |
|---|---|---|
shell |
command |
Write access to the workspace. No network. Read-only filesystem otherwise. |
python |
code |
256 MB of memory. No filesystem or network access. |
read_file |
path |
None beyond the read-only baseline. Path is relative to the workspace root. |
write_file |
path, content |
Write access to the workspace. Creates parent directories. |
list_files |
subdir (optional) |
None beyond the read-only baseline. |
Custom tools
McpSandbox is the mechanism underneath, and it is usable directly. It is deny-by-default: a tool added with no capabilities gets a read-only sandbox with no network, and anything more has to be named.
from sandlock.mcp import McpSandbox
mcp = McpSandbox(workspace="/tmp/agent")
mcp.add_tool("read_file", read_fn) # read-only
mcp.add_tool("write_file", write_fn,
capabilities={"fs_writable": [workspace]}) # write to workspace
result = await mcp.call_tool("write_file", {"path": "out.txt", "content": "hi"})
Tools must be top-level functions. Each call runs the tool in a fresh interpreter inside the sandbox, so the function has to be importable by name. Lambdas, methods, and nested or closure functions are rejected with a clear error rather than failing mysteriously at call time.
The worker is executed as a plain script rather than through -m, so it runs with only stdlib imports and never pulls the sandlock package, or its FFI shared library, into the sandbox with it.
Why this matters for agents
A tool-using agent is an arbitrary code execution primitive with a model deciding the arguments. The usual mitigation is to run tools in a container, which is heavy enough that most implementations reuse one, and reuse means one call's side effects are visible to the next.
A Sandlock sandbox costs about 5 ms to start, so a fresh sandbox per call is affordable. Combined with deny-by-default capabilities, the blast radius of a tool call is the capabilities that tool declared, and nothing else.
For agents that also need to reach an API, the same techniques from the CLI apply: an HTTP ACL restricting the agent to one method and path, and credential injection so the key stays in the supervisor and never enters the agent's address space.