Should Each MCP Server Run in Its Own Sandbox? How to Set It Up
Updated October 27, 2026 .md
Short answer: Yes, for any server you didn't write or that holds a secret. Run each one in its own --no-env sandbox with only its own -e variables, bridge stdio servers to Streamable HTTP on 127.0.0.1, host each port privately, and connect your client to every URL with its own __sbx_auth cookie header.
An agent with five MCP servers on your laptop runs five programs as you. Each one can read your home directory, your shell's environment and every other server's config file, and each one can reach your local network. Most servers need none of that. This page sets up the opposite: one microVM per server, so a server only has what you gave it.
If you want to try a server before you trust it, test it first with the MCP Inspector in a sandbox. If you want an agent to create sandboxes through MCP, that's Sandboxer's own MCP server, a different thing.
What one sandbox per server buys you#
| Shared by servers on one machine | One sandbox per server |
|---|---|
| Filesystem: any server can read the others' files and config | Each VM has its own disk. Server B's sandbox had no /home/user/project/notes.txt, which server A serves |
| Secrets: every variable in the environment, every token file on disk | Each sandbox holds only its own -e values. Server B saw EVERYTHING_SERVER_TOKEN, never FILES_SERVER_TOKEN |
| Network: servers can talk to each other on localhost | Sandbox to sandbox is blocked. From B, A's SSH port and port 8000 both timed out |
| One access path for all of them | Each hosted URL has its own token. A's URL answered 401 to B's cookie |
| A crash, a runaway process or a memory leak hits all of them | Each server has its own 2 vCPU and 4 GB (small) |
The env row matters more than it looks. The reference everything server has a get-env tool that returns its whole process environment to the client. Plenty of real servers can read files or run shell commands, which amounts to the same thing. Whatever a server can read, the model can be talked into returning.
When it isn't worth it: a server you wrote that only calls one API with a key you'd give the agent anyway. A sandbox per server costs a hosted URL to manage and one network round trip per call (about 130 ms in our test, below).
What we ran#
On production, October 27, 2026, from a laptop in Miami. Two small sandboxes, each made with --no-env:
- Sandbox A: the reference filesystem server 2.0.1. It speaks stdio only, so supergateway 4.2.1 serves it as Streamable HTTP.
- Sandbox B: the reference everything server 2.0.1, behind supergateway too, so both bind
127.0.0.1the same way. - The client: the MCP TypeScript SDK 1.32.2 on the laptop, and Claude Code 2.1.311.
1. One sandbox per server, each with only its own secret#
sbx new --name mcp-files --type small --no-env -e FILES_SERVER_TOKEN=not-a-real-secret-files
sbx new --name mcp-everything --type small --no-env -e EVERYTHING_SERVER_TOKEN=not-a-real-secret-everything--no-env means the sandbox gets none of your account's variables, secret files, GitHub token or agent logins (environments). The -e value is the one secret that server needs. Use the variable name your server's README asks for. Ours are fake.
A --no-env sandbox still gets a SANDBOXER_TOKEN. It can read only that one sandbox and host its ports. It can't run commands, create sandboxes or read your account (pass secrets safely shows what it can and can't do). So a server that leaks its environment leaks its own secret and a token that can't touch anything else.
Sandboxes stop after an hour by default. For a server you want up all day, add --ttl 28800 (8 hours) or more.
2. Run the server as a service on 127.0.0.1#
This is sandbox A's setup. We piped it in with sbx exec -i <id> --timeout 600 "bash -s" < setup-files.sh:
set -e
sudo npm i -g --silent supergateway@4.2.1 @modelcontextprotocol/server-filesystem@2.0.1
mkdir -p /home/user/project && echo "only the files server can read this" > /home/user/project/notes.txt
sudo tee /etc/systemd/system/mcp-files.service >/dev/null <<'UNIT'
[Unit]
Description=MCP filesystem server over Streamable HTTP
After=network-online.target
[Service]
User=user
EnvironmentFile=/etc/sandboxer/env
ExecStart=/usr/bin/supergateway --stdio "mcp-server-filesystem /home/user/project" --outputTransport streamableHttp --host 127.0.0.1 --port 8000 --stateful --sessionTimeout 600000 --logLevel none
Restart=always
[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload && sudo systemctl enable --now mcp-files
sleep 3
sudo ss -ltnp | grep ':8000 'Created symlink /etc/systemd/system/multi-user.target.wants/mcp-files.service → /etc/systemd/system/mcp-files.service.
LISTEN 0 511 127.0.0.1:8000 0.0.0.0:* users:(("node",pid=636,fd=21))It took 10 seconds. Sandbox B is the same script with @modelcontextprotocol/server-everything@2.0.1 and --stdio "mcp-server-everything stdio". Three lines in that unit are easy to miss:
EnvironmentFile=/etc/sandboxer/env. systemd starts services with a clean environment, so without this line the server never sees its-evariable.--host 127.0.0.1. Each sandbox has a public IPv6 address. Its firewall is closed except SSH (security), but binding to loopback means the server isn't listening there at all.--stateful. Without it, supergateway starts a fresh copy of the stdio server for every request. Its log showedChild exitedafter each call, and calls took 270–410 ms. With it, one server process serves the whole session, and calls took 131 ms, about one round trip from Miami.
We also set --logLevel none. At the default level, supergateway writes every request and response body to the journal, including file contents. Leave it on while you debug (journalctl -u mcp-files), then turn it off.
3. Give each server a private URL#
sbx host <files-id> 8000 --localhost --title "MCP files"
sbx host <everything-id> 8000 --localhost --title "MCP everything"https://quick-clear-canyon-8000.box.sandboxer.com?_token=<token>
https://clean-chief-hare-8000.box.sandboxer.com?_token=<token>--localhost makes the sandbox relay the hosted port to 127.0.0.1:8000 (hosting). Hosted URLs are private by default. A browser trades _token for a cookie, but an MCP client sends no cookies of its own. So take the token out of the URL and send it on every request as Cookie: __sbx_auth=<token>. Sandboxer's proxy strips it before the request reaches the server.
Keep each token in its own environment variable on your laptop. We used FILES_MCP_AUTH and EVERYTHING_MCP_AUTH. The proxy checks each URL against its own token:
INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
H=(-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream')
A=https://quick-clear-canyon-8000.box.sandboxer.com/mcp
curl -s -o /dev/null -w "no token: %{http_code}\n" -X POST $A "${H[@]}" -d "$INIT"
curl -s -o /dev/null -w "B's cookie: %{http_code}\n" -X POST $A "${H[@]}" -H "Cookie: __sbx_auth=$EVERYTHING_MCP_AUTH" -d "$INIT"
curl -s -w "\nown cookie: %{http_code}\n" -X POST $A "${H[@]}" -H "Cookie: __sbx_auth=$FILES_MCP_AUTH" -d "$INIT"no token: 401
B's cookie: 401
event: message
data: {"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"secure-filesystem-server","version":"2.0.1"}},"jsonrpc":"2.0","id":1}
own cookie: 2004. Connect one client to both#
With the TypeScript SDK, pass the header in requestInit:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
// One entry per sandbox. Each hosted URL has its own token, read from the environment.
const servers = {
files: { url: "https://quick-clear-canyon-8000.box.sandboxer.com/mcp", token: process.env.FILES_MCP_AUTH },
everything: { url: "https://clean-chief-hare-8000.box.sandboxer.com/mcp", token: process.env.EVERYTHING_MCP_AUTH },
};
async function connect(name, { url, token }) {
const transport = new StreamableHTTPClientTransport(new URL(url), {
requestInit: { headers: { Cookie: `__sbx_auth=${token}` } },
});
const client = new Client({ name: "laptop", version: "1.0.0" });
await client.connect(transport);
return client;
}
const files = await connect("files", servers.files);
const everything = await connect("everything", servers.everything);
const text = (r) => r.content.map((c) => c.text).join("\n");
console.log((await files.listTools()).tools.length, (await everything.listTools()).tools.length);
console.log(text(await files.callTool({ name: "read_text_file", arguments: { path: "/home/user/project/notes.txt" } })));
console.log(text(await everything.callTool({ name: "echo", arguments: { message: "hi" } })));
// Which secrets can the everything server see? Print names only, never values.
const env = JSON.parse(text(await everything.callTool({ name: "get-env", arguments: {} })));
console.log(Object.keys(env).filter((k) => /TOKEN|SECRET|KEY/.test(k)).join(" "));Our run, with timings added:
files: connected in 557 ms secure-filesystem-server 2.0.1
everything: connected in 615 ms mcp-servers/everything 2.0.1
files tools (399 ms): 14
everything tools (323 ms): 16
files read_text_file (275 ms): only the files server can read this
everything echo (305 ms): Echo: hi
everything get-env, names only (305 ms): SANDBOXER_TOKEN EVERYTHING_SERVER_TOKENThat was before --stateful. With it, five calls in a row took 208 131 131 130 131 ms on A and 298 131 131 132 131 ms on B. The ping from the laptop to the node was 123 ms, so nearly all of that is distance.
The last line is the point of the whole setup. The everything server can dump its environment, and its environment holds its own fake token and a sandbox token that can't act. FILES_SERVER_TOKEN isn't there, because it's in a different VM.
For Claude Code, write ${VAR} into the header and let Claude Code fill it in from your environment. The single quotes keep the shell from expanding it, so the token never lands in ~/.claude.json:
claude mcp add-json --scope user files '{"type":"http","url":"https://quick-clear-canyon-8000.box.sandboxer.com/mcp","headers":{"Cookie":"__sbx_auth=${FILES_MCP_AUTH}"}}'
claude mcp add-json --scope user everything '{"type":"http","url":"https://clean-chief-hare-8000.box.sandboxer.com/mcp","headers":{"Cookie":"__sbx_auth=${EVERYTHING_MCP_AUTH}"}}'
claude mcp listfiles: https://quick-clear-canyon-8000.box.sandboxer.com/mcp (HTTP) - ✔ Connected
everything: https://clean-chief-hare-8000.box.sandboxer.com/mcp (HTTP) - ✔ ConnectedThe config file held "Cookie": "__sbx_auth=${FILES_MCP_AUTH}", not the token. With a wrong value in FILES_MCP_AUTH, the check said ✘ Failed to connect — Dynamic Client Registration rejected (HTTP 401). Claude Code reads a 401 as a cue to try OAuth, which a hosted port doesn't offer. If you see that message, check the variable. Other clients that let you set headers on an HTTP server work the same way; see local or hosted MCP for where Codex and Cursor keep theirs.
What it costs#
Each small sandbox is $0.018/hour while it runs and bills no running time while stopped or paused (pricing). Two servers:
| How you run them | Running time billed | Cost |
|---|---|---|
| An 8-hour workday | 2 × 8 h | $0.29 a day |
| Around the clock, 30 days | 2 × 720 h | $25.92 a month |
| With idle auto-pause | Only while they're in use | Less, by how long they sit idle |
Idle auto-pause is on by default. A sandbox with no activity pauses with its memory kept. The next request with the URL's token wakes it, and the proxy holds that request while it resumes, up to 45 seconds (wake on use). For an MCP server that's a slow first call, not an error. We set sandbox B to pause after 1 minute (sbx idle <id> 1). It paused, and a new client connected through the paused URL in 1,188 ms. The next call took 293 ms. Afterwards the sandbox reported memoryRestored: true, and the server still had the PID it started with. sbx idle <id> 60 sets the delay in minutes. Paused and stopped sandboxes still count toward your plan's included storage.
FAQ#
Can I run several MCP servers in one sandbox instead?#
Yes. Group servers that you trust equally and that need the same secrets, like two servers for one internal API. You save a sandbox, but each of them can read the other's files and environment.
Why not just give the server the API key through the client config?#
For a stdio server on your laptop, the client passes that key in the server's environment. The server process still runs as you and can read every other server's config. A sandbox limits what it can reach, not just what it's handed.
Do I need supergateway if my server already speaks Streamable HTTP?#
No. Run the server's own HTTP mode and host its port. If it can't bind to 127.0.0.1, host it without --localhost. Only hosted ports are reachable through the proxy, and the sandbox's IPv6 firewall stays closed except SSH.
Can the agent reach a server's sandbox some other way?#
Only through the hosted URL you gave it. Sandboxes can't reach each other, and each URL needs its own token. sbx host hide <id> 8000 takes a URL down. Hosting the port again gives the old token back, and --private mints a new one.
What happens to an open MCP session when the sandbox pauses?#
It carries on. We opened a session to sandbox B, left it until the sandbox paused, then called echo on the same session. The call took 916 ms instead of 131 ms and returned Echo: same session after pause. Memory was restored, so the server process and its session were still there.
Related#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.