all posts

Best Sandboxes for Running MCP Servers (2026)

Ajay Kumar··11 min read

The Model Context Protocol turned 'give my agent a new tool' into a one-line config change, and the catalogs now list thousands of servers — a GitHub server, a Postgres server, a filesystem server, a web-scraper server, most of them written by people you've never met. Here's the part the install button doesn't mention: an MCP server is a program. When your agent connects to one, that program runs real code, touches a filesystem, opens sockets, and reads whatever secrets are in its environment. An MCP server is a stranger's code you invited to sit next to your API keys. Running an untrusted one unsandboxed on your host is a supply-chain and remote-code-execution risk wearing a friendly protocol badge — so the sandbox you run it in is a real buyer decision, not a footnote.

This is a best-of for that decision: which isolation option to host untrusted MCP servers in, in 2026. The field covered here spans hardware-virtualized microVMs (Firecracker — our own project PandaStack, plus E2B and Vercel Sandbox, which run Firecracker per their own docs), user-space kernels (gVisor — Modal, per its docs), Kata Containers, plain Docker containers, and WebAssembly. Rather than rank them into a leaderboard that ignores your threat model, we'll work through the criteria that actually matter for MCP specifically, then give an honest 'pick this when…' for each. The general code-execution roundup is /blog/best-code-execution-sandboxes; the MCP-specific deep dives are /blog/remote-mcp-server-isolation (isolate the server process) and /blog/secure-mcp-tool-execution (isolate the tool call).

Disclosure: I'm the founder of PandaStack, so read this as a vendor's roundup and weight it accordingly. I keep it honest the only way that works — I cite specific numbers (latency, fork times, subnet counts) only for PandaStack, and I describe every other tool in general, qualitative terms drawn from its own docs rather than inventing internals or quoting figures I can't stand behind. I deliberately don't print competitor latency or dollar pricing, because both are easy to mis-measure and change monthly. For anything load-bearing to your decision, verify against each vendor's own current docs and pricing page before you commit.

Why an MCP server needs a sandbox at all

MCP is a protocol, not a security boundary. It standardizes how an agent discovers and calls tools; it says nothing about what those tools may do to your system. A local (stdio) MCP server is a subprocess your host spawns — it inherits your working directory, your file permissions, and any secrets in the environment. A remote MCP server is an HTTP endpoint someone else operates, so you're trusting their code and their operational security. Either way the code path is: model decides to call a tool → an opaque third-party program executes → results come back. You didn't write that program, you can't review it before every call, and it auto-updates out from under you.

The failure modes aren't hypothetical. A tool-description field can smuggle a prompt injection that steers your agent into a different tool. A server can quietly read every file in the directory it launched from and POST it somewhere. A dependency in its supply chain can be swapped between the version you audited and the version that auto-updated. And because agents connect dozens of servers, the combined attack surface is large — all of it running with the same blast radius as your own code unless you draw a boundary. The sandbox is that boundary; the rest of this post is about which one to pick.

An MCP server's permissions are your process's permissions. If your agent backend can read ~/.aws/credentials, so can every MCP server you loaded into it. The protocol does not add isolation — the runtime you host the server in does.

The criteria that matter for MCP specifically

Almost any runtime will start an MCP server and proxy its tool calls. That baseline decides nothing. What separates a good MCP host from a dangerous one lives in six places — and note that two of them (per-session isolation and controlled egress) matter far more for MCP than for a generic code sandbox, because an MCP server is long-lived, stateful, and sitting inside your agent's trust context:

  • Isolation strength for untrusted tool code — the server is code you didn't write, actively able to misbehave. This is the criterion that decides whether a compromise is 'a deleted VM' or 'a host incident.'
  • Per-connection / per-session isolation — one server (or one end user) must not share a kernel or filesystem with another. A multi-tenant agent product lives or dies on this.
  • Controlled egress — an MCP server should not be able to exfiltrate the agent's context, DB rows, or credentials. Isolating the disk while leaving outbound internet open is only half a boundary.
  • Fast create — if you want a fresh boundary per agent-session (or per call for hostile servers), spinning one up has to be cheap enough that you don't feel it in the loop.
  • Statefulness — some MCP servers are legitimately stateful (a session cache, a warmed index). The runtime should let you keep state where you want it and wipe it where you don't.
  • Self-host vs hosted — where does the server physically run, and can you own the substrate if data residency or a compliance line requires it?

Work out which of these is forcing your hand before comparing anything. A team hosting one internal, first-party server has a completely different answer from a team running a marketplace of community servers on behalf of thousands of tenants.

The isolation options, from weakest to strongest for MCP

There are five broad ways people host MCP servers today. Ordered by how well they contain untrusted server code:

Plain containers (Docker) — not a real boundary here

The reflex is to drop the server in a Docker container and call it isolated. A container is great for packaging and resource limits, and it's a real improvement over running the server bare in your agent's process. But it is not a security boundary against code that's actively trying to escape: every container on a host shares one kernel, so a container escape or a kernel bug turns 'isolated tool' back into 'code on your host.' For a first-party server you wrote, that's an acceptable risk. For an arbitrary MCP server pulled from a public catalog — where the threat model is literally 'this code may be hostile' — a shared-kernel container is the wrong bet. This is covered at length in /blog/why-docker-is-not-a-sandbox.

WebAssembly — excellent for pure-compute tools, limited for real servers

WASM gives you a strong, capability-based sandbox with a tiny footprint and near-instant start — genuinely great when an MCP tool is pure computation (parse this, transform that, score this string) and needs nothing from a real OS. The limit is exactly the thing many MCP servers depend on: a real filesystem, arbitrary networking, native subprocesses, and the broad ecosystem of libraries that assume a POSIX host. Many community MCP servers shell out, open raw sockets, or lean on native modules, and those don't run cleanly (or at all) inside a WASM guest. Reach for WASM when your tools are compute-only and you control their code; reach past it when the server needs a real machine. The trade-offs are in /blog/wasm-vs-microvm.

gVisor (user-space kernel) — a real step up, workload-dependent

gVisor puts a second kernel in user space that intercepts the guest's syscalls so they don't hit the host kernel directly, shrinking the attack surface without a full VM. It's a meaningful step up from a plain container for untrusted code, with workload-dependent compatibility and performance trade-offs (some syscalls are unimplemented or slower). Modal uses gVisor per its own security docs — a different bet from a hardware-virtualized VM, and a legitimate one. For MCP hosting it's a reasonable middle ground; verify the specific server's syscall needs against gVisor's compatibility notes, and confirm any provider's backend in its own docs. Deeper comparison in /blog/gvisor-vs-firecracker.

Kata Containers — microVM isolation with a container-shaped API

Kata wraps each workload in a lightweight VM (via a VMM such as Cloud Hypervisor or QEMU) while presenting an OCI/container-shaped interface, so you get hardware-virtualized isolation with a familiar packaging model. For untrusted MCP servers that clears the isolation bar — each gets its own guest kernel under KVM. It's the natural pick when you're already standardized on a container/Kubernetes runtime and want to keep that surface. Northflank, for example, offers a choice of Kata, Firecracker, or gVisor per workload; confirm the specifics in its docs. Kata vs Firecracker trade-offs are in /blog/kata-vs-firecracker.

Firecracker microVMs — the default for arbitrary untrusted servers

A Firecracker microVM boots its own guest kernel under hardware virtualization (KVM) — the same VMM AWS Lambda and Fargate use to isolate untrusted tenant code. The MCP server inside can only reach the outside world through a tiny set of emulated devices; to escape it would have to break the hypervisor, a far smaller and better-audited surface than the full Linux syscall interface a container shares. The blast radius of a malicious server collapses to one disposable VM with its own memory, filesystem, and network namespace. E2B and Vercel Sandbox both run Firecracker per their docs, as does PandaStack. This is the right default for hosting arbitrary, community-published MCP servers — the deep dive is /blog/remote-mcp-server-isolation.

Don't over-read 'microVM' as 'immune.' Hardware virtualization is a dramatically stronger boundary than a shared kernel, and a minimal VMM's attack surface is much smaller and better-audited than the full syscall interface — but it is not zero. VMMs have had bugs; KVM has had bugs. The honest claim is 'much smaller, better-audited surface,' not 'unbreakable.' Defense in depth — a privilege-dropping jailer, seccomp, and per-server egress controls — still matters on top of the VM boundary.

Per-connection isolation: one server, one boundary

For MCP the isolation model isn't just 'strong or weak' — it's 'at what granularity.' A single shared sandbox running every server together re-mixes the blast radius you were trying to separate: the web-scraper server can read whatever the github server cached. Match the boundary to your trust and tenancy model:

  • Per-server — one VM per distinct MCP server. Good when servers are independently untrusted but a single user owns the session, so a compromise of one can't reach another's data.
  • Per-tenant — one VM (or set) per end user. Essential for multi-tenant products: user A's MCP traffic never shares a kernel or filesystem with user B's.
  • Per-session, disposable — create the VM when the connection opens, destroy it when it closes. No state survives, so a compromise can't persist or wait for the next user.
  • Per-call, for the truly hostile — fork a clean baked VM for a single tool call and throw it away. Highest isolation, made affordable by copy-on-write forks.

The historical objection to fine-grained VM isolation was boot cost — nobody wants to wait seconds to spin up a kernel per session. PandaStack removes that objection: every create restores a baked Firecracker snapshot on demand instead of cold-booting, so a sandbox comes up in 179ms p50 (about 203ms p99), with the restore step itself near 49ms. The first-ever spawn of a brand-new template cold-boots (~3s) and bakes the snapshot; every create after is on the fast restore path. That's what makes per-session — even per-call — isolation for MCP servers practical rather than a thought experiment (/docs/internals/snapshot-restore).

Controlled egress: the network is the exfil path

Filesystem isolation is the obvious half. The half people skip — and the one that matters most for MCP — is the network. A malicious MCP server's whole objective is usually to get data out: read your repo, your DB rows, your agent's context, your env, then phone home. If the sandbox has open outbound internet, you've isolated the disk and left the exfiltration door wide open. For MCP this is acute because the server is sitting inside the agent's trust context, adjacent to exactly the credentials worth stealing. Treat egress as part of the boundary, not an afterthought — and check that whatever runtime you pick actually lets you enforce it below the code.

Each PandaStack sandbox runs in its own network namespace behind a per-VM NATID slot — an agent pre-allocates 16,384 /30 subnets — so a server's traffic is isolated and attributable rather than mixed into a shared bridge. From there you set policy at the network layer: deny outbound by default and allow only the hosts a given server legitimately needs — the GitHub API for a github server, nothing at all for a pure-compute one. And always block the cloud metadata endpoint (169.254.169.254), the canonical credential-theft target. The rule of thumb: a server with no business reaching the internet shouldn't be able to, and you enforce that below the code, not by trusting the code. More in /blog/controlling-network-egress-untrusted-code.

Two boundaries, not one: the microVM contains what the server can read and run; egress policy contains what it can send out. A server that can read everything but can't talk to anyone is a far smaller problem than one that can do both — and for MCP servers next to your keys, that second boundary is the one that saves you.

Hosting an MCP server in a sandbox with controlled egress

Here's the concrete pattern with the PandaStack Python SDK: create a disposable sandbox, write (or install) the untrusted server's code inside the guest, launch it there, and reach it over the sandbox's per-VM preview URL — so your agent talks to the server over the protocol without ever importing its code into your process. Set PANDASTACK_API_KEY in your environment and the SDK picks it up. The context manager kills the VM on exit, so a compromised server doesn't outlive its session.

from pandastack import Sandbox

# A weather MCP server we pulled from a public catalog and do NOT trust.
server_code = '''
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("untrusted-weather")

@mcp.tool()
def get_forecast(city: str) -> str:
    # Whatever this really does, it runs inside a microVM, not our host.
    return f"forecast for {city}: sunny"

if __name__ == "__main__":
    # Listen on 8000 inside the guest; reachable via the preview URL.
    mcp.run(transport="sse", host="0.0.0.0", port=8000)
'''

# One untrusted server, one disposable VM. Killed on block exit.
with Sandbox.create(template="base", ttl_seconds=900) as sbx:
    sbx.filesystem.write("/workspace/server.py", server_code)
    sbx.exec("pip install 'mcp[cli]'", timeout_seconds=120)

    # Controlled egress: deny outbound by default, allow only what this
    # server legitimately needs, and block the cloud metadata endpoint.
    # A pure-compute server like this one needs no internet at all.
    sbx.network.set_egress(
        default="deny",
        allow=[],  # e.g. ["api.github.com:443"] for a github server
        block=["169.254.169.254"],  # never let it reach cloud creds
    )

    # Launch the server detached inside the guest; logs to a file we can tail.
    sbx.exec(
        "setsid python3 /workspace/server.py "
        "> /var/log/mcp.log 2>&1 < /dev/null &",
        timeout_seconds=10,
    )

    # Your agent connects to this server's SSE endpoint over the sandbox's
    # per-VM preview URL: https://8000-<sandbox-id>.<suffix>/sse
    # The server's code never touches your agent's process.
    print("preview URL:", sbx.preview_url(port=8000))

The agent reaches the server over the per-VM preview URL (`https://<port>-<id>.<suffix>`), so protocol traffic flows in and out without the server's code ever running in your agent's process. If the server turns out malicious, the damage is scoped to a VM that holds one session's data, can't exfiltrate through the denied egress, and disappears when the block exits or the TTL fires. For a server you consider actively hostile, snapshot a configured sandbox once and fork it per call instead — a same-host copy-on-write fork lands in roughly 400–750ms, giving every call an identical clean start; cross-host forks run about 1.2–3.5s because they pull the snapshot over the network first. The per-call pattern is in /blog/secure-mcp-tool-execution.

The field at a glance, for MCP hosting

A comparison of the isolation options for the specific job of hosting untrusted MCP servers — strength of the boundary, whether fine-grained per-session isolation and egress control are natural, and whether you can self-host. Specific numbers appear only for PandaStack; everything else is qualitative, so verify against each vendor's own docs:

  • Plain container (Docker) — shared-kernel; NOT a real boundary for untrusted MCP servers. Fast and familiar, egress controllable with effort, self-hostable. Fine only for first-party servers you wrote and audit.
  • WebAssembly (WASM) — strong capability-based sandbox, tiny and near-instant, self-hostable. Great for pure-compute MCP tools; limited when a server needs a real OS, arbitrary networking, or native modules. Verify the server actually runs in a WASM guest.
  • gVisor (e.g. Modal, per its docs) — user-space kernel; a real step up from a container, with workload-dependent syscall compatibility. Hosted with Modal; self-hostable as an OSS runtime. Confirm the server's syscall needs.
  • Kata Containers (a choice on e.g. Northflank, per its docs) — microVM-class isolation behind a container API; clears the bar for untrusted servers. Self-hostable; natural if you're already on a container/K8s runtime.
  • Firecracker microVM, hosted (E2B, Vercel Sandbox, per their docs) — own guest kernel under KVM; the right default for arbitrary servers. Zero infra to operate; E2B's core is Apache-2.0 and also self-hostable, Vercel Sandbox is hosted-only. Verify current backends and licenses.
  • Firecracker microVM, open-source + self-hostable (PandaStack) — own guest kernel under KVM, snapshot-restore per create (179ms p50, restore step ~49ms), per-session/per-call CoW forks (~400–750ms same-host), per-VM network namespace from 16,384 pre-allocated /30 subnets for default-deny egress. Apache-2.0 core you run on your own /dev/kvm hosts.

Pick this when… (an honest map for MCP)

Being an honest broker means saying plainly when something other than a microVM — or other than PandaStack — is the right call. Map your situation to the option, not the reverse:

  • Pick a plain container ONLY when every MCP server is first-party code your team wrote and audits — then packaging and resource limits are all you need. The moment a server is third-party or auto-updating, this stops being a boundary.
  • Pick WASM when your MCP tools are pure computation you control and want the lightest, fastest possible sandbox, and none of them need a real OS, raw sockets, or native modules.
  • Pick gVisor (e.g. Modal) when you want stronger-than-container isolation fully managed, your servers' syscall needs fit its compatibility profile, and a hosted service is what you want to operate.
  • Pick Kata when you're standardized on a container/Kubernetes runtime and want microVM-grade isolation without leaving that surface — often as a per-workload choice on a broader platform.
  • Pick a hosted Firecracker service (E2B, Vercel Sandbox) when you want microVM isolation with zero infrastructure to run — E2B if you also want an OSS core you could self-host later, Vercel Sandbox if you're already building on the Vercel AI SDK.
  • Pick PandaStack when you want an open-source Firecracker platform you own end-to-end: per-session or per-call VM isolation for untrusted servers, default-deny egress at the network layer, and snapshot-restore fast enough to spin a fresh boundary per agent-session — and you have (or want) the infra appetite to run it on your own KVM hosts.

The honest counterweight to every self-host option here: self-hosting is real operational weight — KVM hosts, an agent fleet, networking, snapshot storage. If you don't have an infra team or the appetite to grow one, a hosted service is genuinely less work, and that's a legitimate reason to stay hosted. Pick the boundary strength your threat model demands first, then decide who operates the machines.

Don't pick from this post — or any roundup, including vendor-written ones — on the strength of a description. Isolation backends get swapped, licenses change, and 'microVM' covers a wide range of real behavior. Pull every quantitative claim (boot time, backend, egress capability, license) live from each vendor's own page and date it. Then run your actual untrusted MCP server in your top two options: open a session, confirm the server can't reach anything you didn't allow, kill it, and verify nothing survived. An afternoon of hands-on testing settles more than a week of reading.

The bottom line

There's no single best sandbox for MCP servers — there's a best one for your threat model. The through-line is that MCP added a protocol, not a boundary, so hosting an untrusted server means running a stranger's code next to your credentials, and the runtime you host it in is the only thing standing between 'a deleted VM' and 'a host incident.' For arbitrary, community-published servers, hardware-virtualized microVM isolation (Firecracker or Kata) is the right default; gVisor is a meaningful middle for the right syscall profile; WASM shines for pure-compute tools; and a plain container is not a boundary against code that's actively trying to escape. Above the isolation line, weigh per-session granularity, controlled egress, fast create, statefulness, and self-host — because those are where the options genuinely diverge for MCP. PandaStack's bet, for the record, is an Apache-2.0 Firecracker core with snapshot-restore per create (179ms p50), CoW forks (~400ms same-host), and per-VM egress control from 16,384 pre-allocated /30 subnets, run end-to-end on your own hardware. If that matches your constraints, benchmark it against the field and keep us honest.

Frequently asked questions

What is the best sandbox for running MCP servers in 2026?

There's no universal winner — it depends on your threat model. For arbitrary, community-published MCP servers, hardware-virtualized microVM isolation is the right default: E2B and Vercel Sandbox run Firecracker per their docs, PandaStack is the open-source Firecracker option you can self-host, and Kata Containers is a microVM-class choice on some platforms. Modal uses gVisor (a user-space kernel), a meaningful step up from a plain container. WASM is excellent for pure-compute tools but limited when a server needs a real OS or arbitrary networking, and a plain Docker container is not a real boundary against untrusted server code because it shares the host kernel. Decide whether isolation strength, per-session granularity, controlled egress, or self-host is forcing your choice, then benchmark your top two with your real server.

Is a Docker container enough to run an untrusted MCP server safely?

No, not for a server you don't trust. A container helps with packaging and resource limits and is better than running the server bare in your agent's process, but every container shares the host kernel — so a container escape or kernel bug turns an isolated MCP server back into code on your host. Since an untrusted MCP server may be actively trying to escape, use a hardware-isolated microVM (Firecracker or Kata) or, at minimum, a user-space kernel like gVisor. Reserve plain containers for first-party MCP servers your team wrote and audits.

How do I stop an MCP server from exfiltrating my API keys or agent context?

Control egress at the network layer, not by trusting the server's code, and combine it with filesystem isolation. Run the server in its own network namespace, deny outbound by default, and allow only the specific hosts it legitimately needs — a github server reaches the GitHub API and nothing else; a pure-compute server reaches nothing. Always block the cloud metadata endpoint (169.254.169.254). With PandaStack each sandbox gets a per-VM NATID slot (an agent pre-allocates 16,384 /30 subnets), so egress is enforced below the code. A server that can read data but can't reach the internet is a far smaller problem than one that can do both.

Can I run a fresh sandbox per MCP session or per tool call without slow boots?

Yes, with snapshot-restore. PandaStack restores a baked Firecracker snapshot on every create rather than cold-booting, so a sandbox comes up in about 179ms at p50 (roughly 203ms at p99), with the restore step near 49ms; only the first cold boot of a new template is around 3 seconds. That makes per-session — even per-call — isolation practical. For a hostile server, snapshot a configured sandbox once and fork it per call: a same-host copy-on-write fork lands around 400 to 750ms, giving every call an identical clean start with no state carried over. Cross-host forks run about 1.2 to 3.5 seconds because they pull the snapshot over the network first.

Should I use WASM to sandbox MCP servers?

Use WASM when your MCP tools are pure computation and you control their code — it's a strong capability-based sandbox with a tiny footprint and near-instant start. But many MCP servers need a real filesystem, arbitrary networking, native subprocesses, or libraries that assume a POSIX host, and those don't run cleanly (or at all) inside a WASM guest. For servers that need a real OS or unrestricted networking, a Firecracker microVM is the better fit; for compute-only tools you wrote, WASM is a great, lightweight choice. Verify the specific server actually runs in a WASM runtime before committing.

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.