Build tiny sandboxes from MCP tools, TypeScript functions, CLIs, and just-bash.
mcpbash gives you a small shell-first sandbox object. Instead of provisioning a VM or container, you map capabilities into commands and run them through a controlled runtime.
pnpm add mcpbash
# or
npm install mcpbash- Map MCP tools to shell commands
- Map TypeScript functions to shell commands
- Wrap existing binaries
- Choose an in-memory, read-only, or read-write filesystem
- Opt into
gitsupport when you need it - Control outbound MCP access with a simple network policy
- Resolve secrets at runtime instead of hardcoding them
This example creates one command, slugify, and runs it inside the sandbox.
import { createSandbox, fn } from "mcpbash"; const sandbox = await createSandbox({ filesystem: { mode: "memory" }, commands: { slugify: fn({ input: { text: "1ドル" }, handler: ({ text }: { text: string }) => text.toLowerCase().replace(/\s+/g, "-"), }), }, }); const result = await sandbox.run('slugify "Hello World"'); console.log(result.stdout); // hello-world console.log(result.exitCode); // 0 await sandbox.dispose();
This example is fully runnable. It starts a tiny local HTTP server that behaves like an MCP tool endpoint, maps that tool into the sandbox as repos.search, and calls it like a shell command.
import http from "node:http"; import { createSandbox, mcp } from "mcpbash"; async function startToolServer(): Promise<{ url: string; close(): Promise<void> }> { return new Promise((resolve) => { const server = http.createServer((req, res) => { if (req.method !== "POST" || req.url !== "/tools/search_repositories") { res.statusCode = 404; res.end("not found"); return; } let body = ""; req.on("data", (chunk) => { body += chunk.toString("utf8"); }); req.on("end", () => { const { input } = JSON.parse(body) as { input?: { q?: string } }; const query = input?.q ?? ""; res.setHeader("content-type", "application/json"); res.end( JSON.stringify({ result: { query, repos: [ `${query}-api`, `${query}-web`, `${query}-worker`, ], }, }) ); }); }); server.listen(0, "127.0.0.1", () => { const address = server.address(); if (!address || typeof address === "string") { throw new Error("failed to start tool server"); } resolve({ url: `http://127.0.0.1:${address.port}`, close: () => new Promise<void>((done, reject) => { server.close((error) => { if (error) reject(error); else done(); }); }), }); }); }); } const toolServer = await startToolServer(); try { const sandbox = await createSandbox({ filesystem: { mode: "memory" }, network: { allow: ["127.0.0.1"], }, commands: { "repos.search": mcp({ server: toolServer.url, tool: "search_repositories", input: { q: "1ドル" }, }), }, }); const result = await sandbox.run('repos.search "billing"'); console.log(result.stdout); // { // "query": "billing", // "repos": ["billing-api", "billing-web", "billing-worker"] // } await sandbox.dispose(); } finally { await toolServer.close(); }
See also: packages/mcpbash/examples/mcp-demo.ts
import { cli, createSandbox } from "mcpbash"; const sandbox = await createSandbox({ commands: { upper: cli({ command: process.execPath, args: [ "-e", "process.stdout.write((process.argv[1] ?? '').toUpperCase())", "1ドル", ], }), }, }); console.log((await sandbox.run('upper "hello"')).stdout); // HELLO
import { createSandbox } from "mcpbash"; const sandbox = await createSandbox({ filesystem: { mode: "readwrite", root: "./workspace", }, integrations: { git: true, }, }); await sandbox.run("git init -q"); await sandbox.fs.write("README.md", "# demo\n"); const status = await sandbox.git?.status(["--short"]); console.log(status?.stdout);
import { createSandbox, mcp, fn, cli, provider, secret } from "mcpbash";
createSandbox(...)creates a sandboxmcp(...)maps an MCP tool to a commandfn(...)maps a TypeScript handler to a commandcli(...)wraps a local binaryprovider(...)is an alias forcli(...)for external runtimessecret.env("NAME")resolves a secret at runtimesandbox.run(...)executes a commandsandbox.fsexposesread,write,list, andexistssandbox.gitexposesstatus,diff, andlogwhen git is enabled
memory: isolated in-memory sandboxreadonly: overlay an existing directory without writesreadwrite: back the sandbox with a real directory
packages/mcpbash/examples/mixed-demo.tspackages/mcpbash/examples/mcp-demo.tspackages/mcpbash/examples/git-demo.tspackages/mcpbash/examples/benchmark.ts
Run them:
pnpm install pnpm --filter mcpbash demo:mixed pnpm --filter mcpbash demo:mcp pnpm --filter mcpbash demo:git pnpm --filter mcpbash bench
See ADVANCED.md for:
- filesystem allow/deny rules
- secrets
- network policy
- MCP auth and OAuth patterns
- provider examples with Daytona, Upstash Box, and Docker
- current limitations
Local benchmark on April 9, 2026, on an Apple M4 Pro with Bun 1.3.5:
| Operation | Mean | P50 | P95 |
|---|---|---|---|
createSandbox() |
0.067 ms | 0.040 ms | 0.090 ms |
built-in shell command (echo) |
0.460 ms | 0.406 ms | 0.687 ms |
mapped fn(...) command |
0.350 ms | 0.319 ms | 0.550 ms |
mapped provider(...) command (true) |
2.143 ms | 2.038 ms | 3.298 ms |
mapped cli(...) command (node -e) |
4.160 ms | 3.878 ms | 7.314 ms |
The important takeaway is category-level: mcpbash stays on the local fast path. It does not provision a VM, container, or remote session just to dispatch a mapped command, so startup and dispatch stay in the low-millisecond range.
Run the benchmark locally:
pnpm --filter mcpbash bench
pnpm install
pnpm --filter mcpbash typecheck
pnpm --filter mcpbash build
pnpm --filter mcpbash test