Show HN: Parselbox – an embeddable Python sandbox for AI agents Parselbox, an embeddable Python sandbox for AI agents, launched on Hacker News, enabling agents to call MCP servers, APIs, and shells as native Python objects in a single Deno and Pyodide process (~160 MB). The tool provides a disk-backed workspace, built-in networking, package installation, and supports parallel task execution, with credentials staying on the host. Code. Filesystem. Context. Tools. What if agents had one tool to rule them all? Parselbox is an embeddable Python sandbox where AI agents call tools as code — MCP servers, APIs, and shells become native Python objects. Disk-backed workspace, packages, and networking built in; a single process powered by Deno https://deno.com/ and Pyodide https://pyodide.org/en/stable/ . demo.mp4 Tip Drop the Parselbox MCP parselbox-mcp alongside existing MCP server configurations. Agents instantly get a Python runtime, MCP tools as code, support for skills and a disk-backed workspace. No containers, no VMs — just a single, lightweight Deno + Pyodide process ~160 MB . Deno permissions, memory caps, timeouts, network allowlists. Snapshot caching and crash recovery. MCP servers, REST + OpenAPI, GraphQL, shell, functions and classes — all native Python objects. Stateful across calls. Pydantic auto-conversion. Credentials stay on the host. Full CPython with js interop — use JS packages as native Python. require for npm, local TypeScript, and .wasm modules. Virtual bash for shell. Auto-install packages on import. require any .wasm — library exports become Python methods, WASI programs become callable commands; drop one in bin/ to run it from bash too. In-process, inherits the sandbox's mounts and permissions, installs nothing on the host. Append .task to any call — parallel fan-out with asyncio.gather , check progress, tail logs, drive interactive sessions with send , await later. Disk-backed workspace — host mounts ro / rw , input files at /files/ , outputs persisted to real directories. New and modified files are detected and returned per call. help , search , inspect , preview — agents discover only what they need, when they need it. display renders HTML inline in the chat MCP Apps , with Tailwind + daisyUI injected. Or serve a full app — built-in HTTP server with static files, live reload, file upload, and @api routes that compose across tools. Parselbox uses Deno https://deno.com for the secure sandbox runtime. 1. Install Deno macOS / Linux curl -fsSL https://deno.land/install.sh | sh Windows PowerShell irm https://deno.land/install.ps1 | iex 2. Install Parselbox pip install parselbox Wire any tool into the sandbox — MCP servers, REST/GraphQL, shells, host objects — and the agent calls them as native Python, composing them with real control flow over a disk-backed workspace and both the Python and npm package ecosystems. Example: python import asyncio import os from textwrap import dedent from parselbox import Parselbox from parselbox.bridge import HTTPBridge, ShellBridge class Analytics: def summarize self, repos: list - dict: """Aggregate repo stats.""" stars = r "stars" for r in repos return {"count": len repos , "avg stars": round sum stars / len stars } config = {"mcpServers": {"playwright": {"command": "npx", "args": "@playwright/mcp@latest" }}} async def main : async with Parselbox mcp=config, context={ "analytics": Analytics , "github": HTTPBridge base url="https://api.github.com", token=os.environ "GITHUB TOKEN" , "sh": ShellBridge "bash" , }, network=True, allow runtime packages=True, packages= "numpy", "npm:lodash" , output dir="./workspace", as sbx: Discover available tools await sbx.execute code "sbx.search 'navigate|get' " Scrape Hacker News for GitHub links in a real browser await sbx.execute code dedent """ import re playwright.browser navigate url="https://news.ycombinator.com" text = playwright.browser snapshot repos = re.findall r'github\\.com/ \\w.- +/ \\w.- + ', text :5 """ Fetch star counts in parallel, then summarize via the context bridge await sbx.execute code dedent """ import asyncio results = await asyncio.gather github.get.task f"/repos/{r}" for r in repos repo data = {"name": r "data" "name" , "stars": r "data" "stargazers count" } for r in results if r.get "ok" analytics.summarize repo data """ Chart it — matplotlib auto-installs on import result = await sbx.execute code dedent """ import matplotlib.pyplot as plt plt.barh r "name" for r in repo data , r "stars" for r in repo data plt.savefig "chart.png" """ print result.files 'chart.png' image = sbx.read file "chart.png" every result carries .output, .files, .stdout, .stderr, .error Serve the whole sandbox as an MCP server await sbx.run mcp asyncio.run main The Parselbox CLI runs a standalone MCP server — every sandbox option is available as a flag. Tip The "loopback" trick: - Add the Parselbox MCP alongside your existing MCP servers. - Point --mcp at that same config file. - On startup, Parselbox connects to the other servers, exposes their tools inside the sandbox, and starts its own MCP server. Don't worry — Parselbox detects and avoids connecting to itself. No infinite loops of doom. Example: { "mcpServers": { "github": {}, "linear": {}, "parselbox": { "command": "uvx", "args": "parselbox", "--mcp", "/absolute/path/to/mcp.json" } } } uvx parselbox --mcp mcp.json --transport http --port 9000 { "mcpServers": { "parselbox": { "type": "http", "url": "http://localhost:9000/mcp" } } } uvx parselbox \ --mcp ./mcp.json \ --transport http \ --host 0.0.0.0 \ --port 8080 \ --file hello.txt \ --mount ./datasets:/data:rw \ --output-dir ./outputs \ --packages pandas,matplotlib \ --package-dir ./cache \ --allow-runtime-packages \ --network \ --serve 3000 \ --memory 2048 \ --timeout 60 \ --env MY API KEY=... python import asyncio from parselbox import Parselbox from agents import Agent, Runner, function tool sandbox = Parselbox mcp={"mcpServers": {"playwright": {"command": "npx", "args": "@playwright/mcp@latest" }}}, output dir="./outputs", allow runtime packages=True, agent = Agent name="Research Assistant", model="gpt-5.5", instructions=f"You are a world-class research assistant.\n\n{sandbox.get prompt }", tools= function tool sandbox.get tool , async def main : async with sandbox: result = await Runner.run agent, "Scrape Wikipedia's 'List of highest-grossing films' with the Playwright MCP. " "Plot a bar chart of the top 10 and save it as ./plot.png", max turns=30, print result.final output asyncio.run main The context bridge exposes host Python objects inside the sandbox: context — functions and namespaces as callable tools. Execution pauses, runs on host, returns result. globals — static values strings, numbers, dicts copied into the sandbox. mcp — MCP server config dict or path . Appears as callable namespaces inside sandbox. Plain classes are auto-wrapped — every public method becomes a callable tool; methods starting with stay private: python from parselbox import Parselbox class Calculator: def add self, a: float, b: float - float: """Add two numbers.""" return a + b async with Parselbox context={"calc": Calculator } as sbx: await sbx.execute code "calc.add a=10, b=20 " Subclass Bridge for nested namespaces auto-crawled ; annotate a parameter with a Pydantic model and passed dicts convert to it automatically: python from parselbox import Parselbox from parselbox.bridge import Bridge from pydantic import BaseModel class Coordinate BaseModel : x: float y: float z: float = 0.0 class Sensors Bridge : def temperature self - float: """Read temperature in celsius.""" return 23.5 class Robot Bridge : def init self : self.sensors = Sensors def move self, to: Coordinate - dict: """Move robot to a position.""" return {"position": to.x, to.y, to.z , "status": "reached"} async with Parselbox context={"robot": Robot } as sbx: await sbx.execute code "robot.move to={'x': 1, 'y': 2} " await sbx.execute code "robot.sensors.temperature " Parselbox ships bridges for REST, GraphQL, and shell: python from parselbox import Parselbox from parselbox.bridge import HTTPBridge, GraphQLBridge, ShellBridge api = HTTPBridge spec="https://petstore3.swagger.io/api/v3/openapi.json", base url="https://petstore3.swagger.io/api/v3", gql = GraphQLBridge "https://countries.trevorblades.com/graphql" sh = ShellBridge "ssh -T user@host" mcp = {"mcpServers": {"deepwiki": {"type": "http", "url": "https://mcp.deepwiki.com/mcp"}}} async with Parselbox context={"api": api, "gql": gql, "sh": sh}, mcp=mcp, network=True as sbx: await sbx.execute code 'api.search "GET /pet/ " ' await sbx.execute code 'api.get "/pet/1" ' await sbx.execute code 'gql.graphql query="{ continents { name } }" ' await sbx.execute code 'gql.graphql query="{ languages { code name } }" ' await sbx.execute code 'term = sh.shell.task ' await sbx.execute code 'term.send "df -h" ' await sbx.execute code "sbx.search 'ask|read' " await sbx.execute code "deepwiki.read wiki structure repoName='pyodide/pyodide' " await sbx.execute code "deepwiki.ask question question='What is Pyodide?', repoName='pyodide/pyodide' " Runnable: bridges.py Every context and MCP call also has a .task form that runs on the host without blocking the sandbox — for parallel fan-out, long-running jobs, and interactive sessions: job = sh.exec.task command="ffmpeg -i in.mp4 out.mp4" returns a task immediately job.status TaskStatus state, elapsed, message, logfile job.tail 5 last lines of the task's live log job.send "q" message a running interactive process await job.wait timeout=120 block until done — or just await job job.cancel parallel fan-out import asyncio results = await asyncio.gather api.get.task f"/items/{i}" for i in range 5 MCP tools stream their progress and log notifications into the task's logfile. A custom Bridge method emits the same way with self.log , and reads whatever the sandbox queued via send with self.recv : python from parselbox.bridge import Bridge class Exporter Bridge : def run self, rows: int - str: for i in range rows : self.log f"row {i}/{rows}" appended to task.logfile → tail for msg in self.recv : messages queued by task.send self.log f"got: {msg}" return "done" Interactive sessions — ShellBridge.shell keeps stdin open, so a task can drive a live process with send : session = sh.shell.task a live shell — state persists within the session session.send "x=21" session.send "echo $ x 2 " import asyncio await asyncio.sleep 1 give it a beat session.tail 1 "42" session.cancel An optional first command launches any REPL as the session — e.g. sh.shell.task "python3 -i" . Runnable: tasks.py Parselbox runs on Pyodide's virtual filesystem, with the working directory, input files, mounts, and packages backed by real host directories — access gated by Deno's permission controls at startup. | Method | Access Level | Description | |---|---|---| files | Read / Write | Temp directory at /files/ . Input files copied here; server uploads stored here. | mounts | Configurable | Maps host directories to /mnt/{name} . Mode: ro default or rw . | output dir | Read / Write | Maps working directory to a host directory to persist files. If not provided, defaults to a temp directory wiped on close . | Note /workspace is always backed by a real host directory — output dir persistent or an ephemeral temp dir wiped on close — enabling Deno streaming, resolvePath , and require for local modules.- Cross the boundary with sandbox.read file path str for text, bytes for binary and sandbox.write file path, content ; a persistent output dir is also readable directly. - Mounts with target="skills" are reported by sbx.info and discoverable via bash "ls /mnt/skills/" . Example: python from parselbox import Parselbox, Mount async with Parselbox files= "data.csv" , Read/write at /files/data.csv mounts= Mount "./datasets", "/data", "ro" , Read-only at /mnt/data Mount "./workspace", "/work", "rw" , Read/write at /mnt/work , output dir="./outputs" Sandbox files persisted here as sandbox: Write a file into the sandbox from the host sandbox.write file "greeting.txt", "Hello from host " code = """ content = open '/files/data.csv' .read input file ref = open '/mnt/data/reference.json' .read read-only mount open '/mnt/work/processed.txt', 'w' .write content read/write mount open 'result.txt', 'w' .write "Done " working dir - output dir """ result = await sandbox.execute code code New / modified files are detected and returned print result.files 'result.txt', 'greeting.txt' sandbox.read file "result.txt" Reach the same files from a shell with bash : bash "echo 'hello from bash' note.txt && cat note.txt" shell over the workspace Runnable: filesystem.py · bash.py Pyodide supports pure-Python packages and many C-extension packages, which must be pre-built for Pyodide https://pyodide.org/en/stable/usage/packages-in-pyodide.html — numpy, pandas, and more ship included. python from parselbox import Parselbox, Mount preload Python + npm packages on startup Parselbox packages= "numpy", "pandas", "npm:lodash" local wheel — mount its dir so Deno can read the host path Parselbox packages= "file:///host/wheels/pkg.whl" , mounts= Mount "./wheels", "wheels", "ro" remote wheel — needs network access Parselbox packages= "https://example.com/pkg.whl" , network=True autoload as imports appear only official domains when network=False Parselbox allow runtime packages=True Note Package installs write straight to disk — a temp dir by default wiped on exit . Set package dir to persist them across sessions, so the next boot is instant with no re-download. After initial package loading, network is blocked by default. Access is configured with Deno's permission controls via --allow-net / --deny-net . All HTTP from sandboxed code requests, httpx, fetch routes through Deno's fetch . Block everything default Parselbox network=False Allow specific domains Python API only Parselbox network= "api.github.com:443" Allow everything Parselbox network=True Note The CLI --network flag is a boolean toggle only. Domain allowlists are available via the Python API. Pyodide is not a security boundary — sandboxed code can read env vars via js 'Deno.env.get "KEY" ' , so never pass real credentials in env . Instead, run a credential-injecting proxy on the host and lock the sandbox to it: async with Parselbox network= "127.0.0.1:8900" , sandbox can ONLY reach the proxy env={ "OPENAI BASE URL": "http://127.0.0.1:8900/v1", "OPENAI API KEY": "phantom-token", harmless; the real key lives on the proxy }, as sbx: await sbx.execute code "import openai; openai.OpenAI .chat.completions.create ... " Most SDKs take a base url override. For SDK-agnostic interception, set HTTP PROXY / HTTPS PROXY / DENO CERT instead and route everything through a MITM proxy — Deno's fetch honours them at the process level. Runnable: basics.py Parselbox runs Python inside Deno's V8 engine via Pyodide, so Python and JavaScript share the same process memory — interop is seamless. js Basic — auto converts args and results js "return data.map x = x 2 ", data= 1, 2, 3 2, 4, 6 Callbacks — Python functions auto-proxied, no create proxy needed js "return items.filter fn ", items= 1,2,3,4,5 , fn=lambda x, : x 3 4, 5 Async + Web APIs Intl, Crypto, URL, TextEncoder js "return crypto.randomUUID " Each js call runs in a fresh, stateless scope. Python callables are auto-proxied and cleaned up after the call. Binary converts too — Uint8Array / ArrayBuffer results become Python bytes , and bytes arguments become Uint8Array s. npm packages — returns proxy + auto-injects in js scope alias= to rename lodash = require "lodash" lodash.chunk 1, 2, 3, 4 , 2 1, 2 , 3, 4 Callbacks work with require'd packages lodash.sortBy data, lambda x, : x "age" Also available in js js "return lodash.invert {a: 1, b: 2} " Local TypeScript — compiled by Deno, hot-reloads; can import npm internally require "./math utils.ts" .fibonacci 10 .wasm modules & WASI binaries load too — see WASM Tools Instances keep their methods — chain them dayjs = require "dayjs" dayjs "2026-06-15" .add 30, "day" .format "YYYY-MM-DD" "2026-07-15" Class constructors auto-detect new color = require "color" color "red" .darken 0.5 .hex " 800000" Chains work with Python callbacks lodash data .filter lambda x, : x "pay" 100 .sortBy lambda x, : -x "pay" .value For files too large to fit in memory, use Deno streams via resolvePath : js js """ const path = resolvePath "sample.txt" ; const info = await Deno.stat path ; return { size: info.size, isFile: info.isFile }; """ Python callbacks work inside streaming pipelines — Deno reads, JS parses, Python classifies each line. python Python module — write it, import it open "helpers.py", "w" .write "def double x : return x 2" from helpers import double double 21 42 TypeScript module — compiled by Deno open "transform.ts", "w" .write "export function upper s: string { return s.toUpperCase ; }" require "./transform.ts" .upper "hello" "HELLO" You can even compile a language to WebAssembly in-sandbox, then require the output. A pure-JavaScript bash just-bash https://github.com/vercel-labs/just-bash over the same workspace. Pipes and coreutils work, and curl is backed by fetch . Each call is isolated cd / export don't persist ; filesystem changes do. bash "echo hello note.txt && cat note.txt | tr a-z A-Z" "HELLO" bash "grep -rn hello . | wc -l" bash "curl -s https://api.github.com/zen" network rules still apply Runnable: javascript.py · bash.py Pyodide can only load packages built for it — so pandoc , ruby or shellcheck are out of reach, and there is no apt-get in a single-process sandbox. Parselbox closes that gap with WASI : any program compiled to WebAssembly becomes a tool, with no host install. A missing capability is just a file. require inspects the module and picks the right shape: js Library module no imports — its exports become methods require "./fib.wasm" .fib 20 6765 Command module a WASI program — becomes a callable command pandoc = require "./pandoc.wasm" r = pandoc "-f", "markdown", "-t", "html5" , stdin=" Report" r "stdout" .decode '