# How to host MCP servers for your agents in sandboxes Start MCP servers in a sandbox. Give your agent the Streamable HTTP URL and bearer token of each server. Keep API keys out of the agent. **On Runtime, one call starts a catalog MCP server in a microVM. Its API key never enters the machine.** The catalog holds 15 servers. Each one is licensed under MIT or Apache-2.0 and has a pinned version: GitHub, Postgres, a Playwright browser, filesystem, fetch, git, time, memory, sequential thinking, Notion, Context7, Brave Search, Exa, Firecrawl and Supabase ([JavaScript SDK](/docs/javascript#mcp-servers-in-a-sandbox), 26 October 2026). A key stored as a Runtime secret reaches the server as a placeholder. The host's proxy adds the real value to outgoing requests. ## Why run MCP servers in a sandbox The Model Context Protocol defines two standard transports. With stdio, the client starts the server as a subprocess. With Streamable HTTP, the server is an independent process, and many clients connect to it at one endpoint ([MCP specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports)). A stdio server that your agent starts runs with the agent's rights, on the agent's machine, and holds every key you configured. Moving the server into a sandbox changes three things: - The server's code, and anything a tool call makes it do, runs in a Firecracker microVM with its own kernel. It does not run beside your agent. - Its network is the sandbox's network. You choose which hosts it can reach. - Its credentials stay with Runtime's proxy. If a prompt injection makes the server print its environment, the output shows only a placeholder with no value. ## The short answer Store the key once as a secret for the hosts it belongs to. Start the servers. Then give the URLs and headers to your agent's MCP client: ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); await runtime.secrets.set("GITHUB_TOKEN", { value: process.env.GITHUB_TOKEN ?? "", hosts: ["api.github.com"], }); const sbx = await runtime.sandboxes.create({ name: "mcp-gateway", idlePauseSeconds: 900, labels: { role: "mcp" }, }); await sbx.mcp.start([ { id: "github", secrets: { GITHUB_PERSONAL_ACCESS_TOKEN: "GITHUB_TOKEN" } }, { id: "fetch" }, { name: "tickets", command: ["python3", "/workspace/tickets_server.py"] }, // your own stdio server ]); const gateway = await sbx.mcp.ready(); // waits while servers install for (const server of gateway.servers) console.log(server.name, server.status, server.url); console.log(gateway.headers); // { Authorization: "Bearer ..." }, on every request ``` ```python check import os from withruntime import Runtime runtime = Runtime() runtime.secrets.set("GITHUB_TOKEN", value=os.environ.get("GITHUB_TOKEN", ""), hosts=["api.github.com"]) sbx = runtime.sandboxes.create(name="mcp-gateway", idle_pause_seconds=900, labels={"role": "mcp"}) sbx.mcp.start([ {"id": "github", "secrets": {"GITHUB_PERSONAL_ACCESS_TOKEN": "GITHUB_TOKEN"}}, {"id": "fetch"}, {"name": "tickets", "command": ["python3", "/workspace/tickets_server.py"]}, ]) gateway = sbx.mcp.ready() for server in gateway["servers"]: print(server["name"], server["status"], server["url"]) print(gateway["headers"]) ``` Your own stdio server runs the same way as a catalog server. Upload its files first, then give it a name and a command. Each server gets its own Streamable HTTP URL through a private preview link. Every request also needs the gateway's bearer token. ## Connect a client Any MCP client that supports Streamable HTTP accepts the URL and the header. A first request written by hand is an `initialize` POST: ```ts check import { Sandbox } from "withruntime"; const sbx = await Sandbox.getOrCreate("mcp-gateway"); const gateway = await sbx.mcp.ready(); const github = gateway.servers.find((server) => server.name === "github"); const reply = await fetch(github!.url!, { method: "POST", headers: { ...gateway.headers, "content-type": "application/json", accept: "application/json, text/event-stream", }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "my-agent", version: "1.0.0" }, }, }), }); console.log(reply.status, reply.headers.get("content-type")); ``` From a terminal, `runtime sandboxes mcp start github fetch` prints each server's URL, the `Authorization` header and a `claude mcp add` line. Paste that line into Claude Code: ```bash no-run runtime sandboxes mcp catalog runtime sandboxes mcp "${id}" start github fetch --secret github.GITHUB_PERSONAL_ACCESS_TOKEN=GITHUB_TOKEN runtime sandboxes mcp "${id}" ``` ## Limit what each server can reach `runtime.mcp.catalog()` lists the settings and the hosts that each server calls. After the servers are installed, limit the sandbox to those hosts. A server that a prompt injection tricks into fetching another host is refused by the host: ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); const catalog = await runtime.mcp.catalog(); const github = catalog.find((entry) => entry.id === "github")!; console.log(github.license, github.version, github.egress); await using sbx = await runtime.sandboxes.create(); await sbx.mcp.start([{ id: "github", secrets: { GITHUB_PERSONAL_ACCESS_TOKEN: "GITHUB_TOKEN" } }]); await sbx.mcp.ready(); // installed while the web was open await sbx.network.set({ internet: true, allow: github.egress }); ``` The rule applies immediately, including to open connections. The servers run as the sandbox's user, behind its network rules. When the servers start, a host that the rules refuse appears as a warning. The rules do not change to allow it. The Postgres server's `DATABASE_URI` is the one setting passed as given, because an HTTPS proxy cannot replace a database password. ## One gateway per user or per agent If each customer connects their own accounts, give each customer a named sandbox with their own servers. `idlePauseSeconds` pauses a gateway that nobody calls. The pause keeps its memory and running servers. | Need | How Runtime covers it | | -------------------------------------- | ------------------------------------------------------------------------------------ | | Well-known servers, nothing to install | `sbx.mcp.start([{ id }])` from a catalog of 14, versions pinned | | Your own server | `{ name, command }` runs any stdio server in the sandbox | | Keys a server must not hold | Runtime secrets: a placeholder inside the sandbox, and the proxy adds the real value | | Only the hosts a server needs | `allow` lists the catalog's hosts; the rule also binds root in the guest | | Authenticated URLs | A private preview link plus the gateway's bearer token | | Idle gateways | `idlePauseSeconds`; a request to a URL wakes it | | A gateway per customer | `Sandbox.getOrCreate(name)` returns the same sandbox every time | | Isolation between customers | A Firecracker microVM with its own kernel for each one | ## What it costs Take 100 customers. Each has a 1 vCPU, 2 GiB gateway. The gateway is busy 2 hours a day for 30 days, using 0.1 of a vCPU on average. It is paused for the other 660 hours, with 0.5 GB of its own stored data: ``` CPU: 100 × 60 h × 0.1 vCPU × $0.025 = $15.00 Memory: 100 × 60 h × 2 GiB × $0.0075 = $90.00 Paused: 100 × 0.5 GB × $0.08 × 660 h / 720 h = $3.67 Total: $108.67 ``` That is about $1.09 per customer per month. A gateway that is running but waiting for requests costs $0.01625 an hour: its memory, plus a CPU floor of a twentieth of a vCPU. A paused gateway costs only storage ([pricing](/docs/pricing)). Secrets are included. An account can hold up to 50. Every account gets 100 free hours of a 1 GB machine in its first 30 days, with no card needed. ## Start ```bash no-run npx withruntime sandboxes run --keep -- echo ready ``` The first run prints a link. Open it in your browser to approve the connection. Then `runtime sandboxes mcp start fetch` starts a server you can connect to. Related: [what the Model Context Protocol is](/glossary/model-context-protocol), [egress control](/glossary/egress-control), [a browser automation agent](/use-cases/browser-automation-agent), [Claude Code in a sandbox](/integrations/claude-code), [pause and resume a sandbox](/how-to/pause-and-resume-a-sandbox). ## Sources Checked 26 October 2026. - [MCP specification, transports](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) The facts on this page were checked on 26 October 2026.