{
  "markdown": "<!-- mcp-name: io.github.thesanjeetc/parselbox -->\n![Parselbox SDK](https://github.com/thesanjeetc/Parselbox/blob/main/assets/parselbox-dark.png#gh-dark-mode-only)\n![Parselbox SDK](https://github.com/thesanjeetc/Parselbox/blob/main/assets/parselbox-light.png#gh-light-mode-only)\n\n<div align=\"center\">\n\n>***Code. Filesystem. Context. Tools.***<br/>\n>**What if agents had one tool to rule them all?**\n\n</div>\n\n<h4 align=\"center\">\n  <a href=\"https://github.com/thesanjeetc/Parselbox/blob/main/LICENSE.md\">\n    <img alt=\"License\" src=\"https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge\">\n  </a>\n  <a href=\"https://pypi.org/project/parselbox/\">\n    <img alt=\"PyPI - Version\" src=\"https://img.shields.io/pypi/v/parselbox?style=for-the-badge\">\n  </a>\n  <a href=\"https://github.com/thesanjeetc/Parselbox/actions/workflows/ci.yaml\">\n    <img alt=\"CI\" src=\"https://img.shields.io/github/actions/workflow/status/thesanjeetc/Parselbox/ci.yaml?branch=main&style=for-the-badge&label=CI\">\n  </a>\n</h4>\n\nParselbox is an embeddable Python runtime 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 execution layer powered by [Deno](https://deno.com/) and [Pyodide](https://pyodide.org/en/stable/).\n\nhttps://github.com/user-attachments/assets/d4e43d16-3aa3-4e29-83c7-a1d3885b8045\n\n> [!TIP]\n> 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.\n\n## Why Parselbox?\n\nParselbox packages programmatic tool calling as a local, open-source execution layer. Drop it into an existing MCP configuration with `uvx`, or embed the Python API into any agent stack.\n\nBeyond code execution, Parselbox provides a stateful, disk-backed workspace with packages, controlled networking, background tasks and progressive discovery.\n\nDeno + Pyodide provide a single-process runtime with explicit filesystem and network permissions — no containers or microVMs required. It also enables JavaScript/npm interoperability, WASM/WASI integration and generative UI through MCP Apps.\n\n### Why an execution layer?\n\nMost agent stacks expose each capability as a separate tool. As integrations grow, tool schemas consume more context. The model must also carry intermediate results, transform data and route values between calls.\n\nAn execution layer moves orchestration out of the context window and into code. Capabilities are discovered on demand, then composed with full control flow. A single execution can coordinate multiple tools while variables, state and files remain inside the runtime.\n\nThe result is less context bloat, fewer model-to-tool round trips and complex workflows expressed as code rather than chains of individual calls.\n\n## Features\n\n#### 🔒 Secure Isolation\nNo containers, no VMs — just a single, lightweight Deno + Pyodide process (~160 MB). Deno permissions, memory caps, timeouts, network allowlists. Snapshot caching and crash recovery.\n\n#### 🛠️ Tools as Code\nMCP servers, REST + OpenAPI, GraphQL, shell, functions and classes — all native Python objects. Stateful across calls. Pydantic auto-conversion. Credentials stay on the host.\n\n#### 🐍 Polyglot Runtime\nFull 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.\n\n#### 📦 WASM Tools\n`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.\n\n#### ⚡ Background Tasks\nAppend `.task()` to any call — parallel fan-out with `asyncio.gather`, check progress, tail logs, drive interactive sessions with `send()`, await later.\n\n#### 📁 Filesystem Integration\nDisk-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.\n\n#### 🔍 Progressive Disclosure\n`help()`, `search()`, `inspect()`, `preview()` — agents discover only what they need, when they need it.\n\n#### 🎨 Generative UI\n`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.\n\n---\n\n## Contents\n\n- [Quick Start](#quick-start)\n  - [Parselbox API](#parselbox-api)\n  - [Parselbox MCP](#parselbox-mcp)\n  - [Parselbox Agents](#parselbox-agents)\n- [User Guide](#user-guide)\n  - [Tools as Code](#1-tools-as-code)\n  - [Background Tasks](#2-background-tasks)\n  - [Filesystem Integration](#3-filesystem-integration)\n  - [Packages & Networking](#4-packages--networking)\n  - [JavaScript Interop](#5-javascript-interop)\n  - [WASM Tools](#6-wasm-tools)\n  - [Progressive Disclosure](#7-progressive-disclosure)\n  - [Generative UI](#8-generative-ui)\n  - [Sandbox Hooks](#9-sandbox-hooks)\n- [Configuration Reference](#configuration-reference)\n- [Architecture](#architecture)\n- [Security](#security)\n- [Related Work](#related-work)\n\n## Quick Start\n\nParselbox uses [**Deno**](https://deno.com) for the secure sandbox runtime.\n\n**1. Install Deno**\n\n```bash\n# macOS / Linux\ncurl -fsSL https://deno.land/install.sh | sh\n\n# Windows (PowerShell)\nirm https://deno.land/install.ps1 | iex\n```\n\n**2. Install Parselbox**\n\n```bash\npip install parselbox\n```\n\n### Parselbox API\n\nWire 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.\n\n**Example:**\n\n```python\nimport asyncio\nimport os\nfrom textwrap import dedent\nfrom parselbox import Parselbox\nfrom parselbox.bridge import HTTPBridge, ShellBridge\n\nclass Analytics:\n    def summarize(self, repos: list) -> dict:\n        \"\"\"Aggregate repo stats.\"\"\"\n        stars = [r[\"stars\"] for r in repos]\n        return {\"count\": len(repos), \"avg_stars\": round(sum(stars) / len(stars))}\n\nconfig = {\"mcpServers\": {\"playwright\": {\"command\": \"npx\", \"args\": [\"@playwright/mcp@latest\"]}}}\n\nasync def main():\n    async with Parselbox(\n        mcp=config,\n        context={\n            \"analytics\": Analytics(),\n            \"github\": HTTPBridge(base_url=\"https://api.github.com\", token=os.environ[\"GITHUB_TOKEN\"]),\n            \"sh\": ShellBridge(\"bash\"),\n        },\n        network=True,\n        allow_runtime_packages=True,\n        packages=[\"numpy\", \"npm:lodash\"],\n        output_dir=\"./workspace\",\n    ) as sbx:\n        # Discover available tools\n        await sbx.execute_code(\"sbx.search('navigate|get')\")\n\n        # Scrape Hacker News for GitHub links in a real browser\n        await sbx.execute_code(dedent(\"\"\"\n            import re\n            playwright.browser_navigate(url=\"https://news.ycombinator.com\")\n            text = playwright.browser_snapshot()\n            repos = re.findall(r'github\\\\.com/([\\\\w.-]+/[\\\\w.-]+)', text)[:5]\n        \"\"\"))\n\n        # Fetch star counts in parallel, then summarize via the context bridge\n        await sbx.execute_code(dedent(\"\"\"\n            import asyncio\n            results = await asyncio.gather(*[github.get.task(f\"/repos/{r}\") for r in repos])\n            repo_data = [{\"name\": r[\"data\"][\"name\"], \"stars\": r[\"data\"][\"stargazers_count\"]}\n                         for r in results if r.get(\"ok\")]\n            analytics.summarize(repo_data)\n        \"\"\"))\n\n        # Chart it — matplotlib auto-installs on import\n        result = await sbx.execute_code(dedent(\"\"\"\n            import matplotlib.pyplot as plt\n            plt.barh([r[\"name\"] for r in repo_data], [r[\"stars\"] for r in repo_data])\n            plt.savefig(\"chart.png\")\n        \"\"\"))\n        print(result.files)                  # ['chart.png']\n        image = sbx.read_file(\"chart.png\")\n        # every result carries .output, .files, .stdout, .stderr, .error\n\n        # Serve the whole sandbox as an MCP server\n        await sbx.run_mcp()\n\nasyncio.run(main())\n```\n\n### Parselbox MCP\n\nThe Parselbox CLI runs a standalone MCP server — every sandbox option is available as a flag.\n\n#### STDIO\n\n> [!TIP]\n> **The \"loopback\" trick:**\n> 1. Add the Parselbox MCP alongside your existing MCP servers.\n> 2. Point `--mcp` at that same config file.\n> 3. On startup, Parselbox connects to the other servers, exposes their tools inside the sandbox, and starts its own MCP server.\n>\n> Don't worry — Parselbox detects and avoids connecting to itself. No infinite loops of doom.\n\n**Example:**\n\n```json\n{\n  \"mcpServers\": {\n    \"github\": {},\n    \"linear\": {},\n    \"parselbox\": {\n      \"command\": \"uvx\",\n      \"args\": [\"parselbox\", \"--mcp\", \"/absolute/path/to/mcp.json\"]\n    }\n  }\n}\n```\n\n#### HTTP\n\n```bash\nuvx parselbox --mcp mcp.json --transport http --port 9000\n```\n\n```json\n{\n  \"mcpServers\": {\n    \"parselbox\": {\n      \"type\": \"http\",\n      \"url\": \"http://localhost:9000/mcp\"\n    }\n  }\n}\n```\n\n#### Full Example\n\n```bash\nuvx parselbox \\\n  --mcp ./mcp.json \\\n  --transport http \\\n  --host 0.0.0.0 \\\n  --port 8080 \\\n  --file hello.txt \\\n  --mount ./datasets:/data:rw \\\n  --output-dir ./outputs \\\n  --packages pandas,matplotlib \\\n  --package-dir ./cache \\\n  --allow-runtime-packages \\\n  --network \\\n  --serve 3000 \\\n  --memory 2048 \\\n  --timeout 60 \\\n  --env MY_API_KEY=...\n```\n\n---\n\n### Parselbox Agents\n\n```python\nimport asyncio\nfrom parselbox import Parselbox\nfrom agents import Agent, Runner, function_tool\n\nsandbox = Parselbox(\n    mcp={\"mcpServers\": {\"playwright\": {\"command\": \"npx\", \"args\": [\"@playwright/mcp@latest\"]}}},\n    output_dir=\"./outputs\",\n    allow_runtime_packages=True,\n)\n\nagent = Agent(\n    name=\"Research Assistant\",\n    model=\"gpt-5.5\",\n    instructions=f\"You are a world-class research assistant.\\n\\n{sandbox.get_prompt()}\",\n    tools=[function_tool(sandbox.get_tool())],\n)\n\nasync def main():\n    async with sandbox:\n        result = await Runner.run(\n            agent,\n            \"Scrape Wikipedia's 'List of highest-grossing films' with the Playwright MCP. \"\n            \"Plot a bar chart of the top 10 and save it as ./plot.png\",\n            max_turns=30,\n        )\n        print(result.final_output)\n\nasyncio.run(main())\n```\n\n## User Guide\n\n### 1\\. Tools as Code\n\nThe context bridge exposes host Python objects inside the sandbox:\n\n- `context` — functions and namespaces as callable tools. Execution pauses, runs on host, returns result.\n- `globals` — static values (strings, numbers, dicts) copied into the sandbox.\n- `mcp` — MCP server config (dict or path). Appears as callable namespaces inside sandbox.\n\n**Plain classes** are auto-wrapped — every public method becomes a callable tool; methods starting with `_` stay private:\n\n```python\nfrom parselbox import Parselbox\n\nclass Calculator:\n    def add(self, a: float, b: float) -> float:\n        \"\"\"Add two numbers.\"\"\"\n        return a + b\n\nasync with Parselbox(context={\"calc\": Calculator()}) as sbx:\n    await sbx.execute_code(\"calc.add(a=10, b=20)\")\n```\n\nSubclass **`Bridge`** for nested namespaces (auto-crawled); annotate a parameter with a Pydantic model and passed dicts convert to it automatically:\n\n```python\nfrom parselbox import Parselbox\nfrom parselbox.bridge import Bridge\nfrom pydantic import BaseModel\n\nclass Coordinate(BaseModel):\n    x: float\n    y: float\n    z: float = 0.0\n\nclass Sensors(Bridge):\n    def temperature(self) -> float:\n        \"\"\"Read temperature in celsius.\"\"\"\n        return 23.5\n\nclass Robot(Bridge):\n    def __init__(self):\n        self.sensors = Sensors()\n\n    def move(self, to: Coordinate) -> dict:\n        \"\"\"Move robot to a position.\"\"\"\n        return {\"position\": [to.x, to.y, to.z], \"status\": \"reached\"}\n\nasync with Parselbox(context={\"robot\": Robot()}) as sbx:\n    await sbx.execute_code(\"robot.move(to={'x': 1, 'y': 2})\")\n    await sbx.execute_code(\"robot.sensors.temperature()\")\n```\n\nParselbox ships **bridges** for REST, GraphQL, and shell:\n\n```python\nfrom parselbox import Parselbox\nfrom parselbox.bridge import HTTPBridge, GraphQLBridge, ShellBridge\n\napi = HTTPBridge(\n    spec=\"https://petstore3.swagger.io/api/v3/openapi.json\",\n    base_url=\"https://petstore3.swagger.io/api/v3\",\n)\ngql = GraphQLBridge(\"https://countries.trevorblades.com/graphql\")\nsh = ShellBridge(\"ssh -T user@host\")\n\nmcp = {\"mcpServers\": {\"deepwiki\": {\"type\": \"http\", \"url\": \"https://mcp.deepwiki.com/mcp\"}}}\n\nasync with Parselbox(context={\"api\": api, \"gql\": gql, \"sh\": sh}, mcp=mcp, network=True) as sbx:\n    await sbx.execute_code('api.search(\"GET /pet/*\")')\n    await sbx.execute_code('api.get(\"/pet/1\")')\n\n    await sbx.execute_code('gql.graphql(query=\"{ continents { name } }\")')\n    await sbx.execute_code('gql.graphql(query=\"{ languages { code name } }\")')\n\n    await sbx.execute_code('term = sh.shell.task()')\n    await sbx.execute_code('term.send(\"df -h\")')\n\n    await sbx.execute_code(\"sbx.search('ask|read')\")\n    await sbx.execute_code(\"deepwiki.read_wiki_structure(repoName='pyodide/pyodide')\")\n    await sbx.execute_code(\"deepwiki.ask_question(question='What is Pyodide?', repoName='pyodide/pyodide')\")\n```\n\n> **Runnable:** [bridges.py](examples/parselbox-basics/bridges.py)\n\n### 2\\. Background Tasks\n\nEvery 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:\n\n```python\njob = sh.exec.task(command=\"ffmpeg -i in.mp4 out.mp4\")   # returns a task immediately\n\njob.status()                   # TaskStatus(state, elapsed, message, logfile)\njob.tail(5)                    # last lines of the task's live log\njob.send(\"q\")                  # message a running interactive process\nawait job.wait(timeout=120)    # block until done — or just `await job`\njob.cancel()\n\n# parallel fan-out\nimport asyncio\nresults = await asyncio.gather(*[api.get.task(f\"/items/{i}\") for i in range(5)])\n```\n\nMCP 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()`:\n\n```python\nfrom parselbox.bridge import Bridge\n\nclass Exporter(Bridge):\n    def run(self, rows: int) -> str:\n        for i in range(rows):\n            self.log(f\"row {i}/{rows}\")     # appended to task.logfile → tail()\n            for msg in self.recv():         # messages queued by task.send()\n                self.log(f\"got: {msg}\")\n        return \"done\"\n```\n\n**Interactive sessions** — `ShellBridge.shell()` keeps stdin open, so a task can drive a live process with `send()`:\n\n```python\nsession = sh.shell.task()               # a live shell — state persists within the session\nsession.send(\"x=21\")\nsession.send(\"echo $((x * 2))\")\n\nimport asyncio\nawait asyncio.sleep(1)                  # give it a beat\nsession.tail(1)                         # \"42\"\n\nsession.cancel()\n```\n\nAn optional first command launches any REPL as the session — e.g. `sh.shell.task(\"python3 -i\")`.\n\n> **Runnable:** [tasks.py](examples/parselbox-basics/tasks.py)\n\n### 3\\. Filesystem Integration\n\nParselbox 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.\n\n| Method         | Access Level   | Description                                                                       |\n| :------------- | :------------- | :-------------------------------------------------------------------------------- |\n| **files**      | Read / Write   | Temp directory at `/files/`. Input files copied here; server uploads stored here. |\n| **mounts**     | Configurable   | Maps host directories to `/mnt/{name}`. Mode: `ro` (default) or `rw`.             |\n| **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). |\n\n> [!NOTE]\n> - `/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.\n> - 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.\n> - Mounts with `target=\"skills\"` are reported by `sbx.info()` and discoverable via `bash(\"ls /mnt/skills/\")`.\n\n\n**Example:**\n\n```python\nfrom parselbox import Parselbox, Mount\n\nasync with Parselbox(\n    files=[\"data.csv\"],                         # Read/write at /files/data.csv\n    mounts=[\n        Mount(\"./datasets\", \"/data\", \"ro\"),     # Read-only at /mnt/data\n        Mount(\"./workspace\", \"/work\", \"rw\"),    # Read/write at /mnt/work\n    ],\n    output_dir=\"./outputs\"                      # Sandbox files persisted here\n) as sandbox:\n    # Write a file into the sandbox from the host\n    sandbox.write_file(\"greeting.txt\", \"Hello from host!\")\n\n    code = \"\"\"\n    content = open('/files/data.csv').read()               # input file\n    ref = open('/mnt/data/reference.json').read()          # read-only mount\n    open('/mnt/work/processed.txt', 'w').write(content)    # read/write mount\n    open('result.txt', 'w').write(\"Done!\")                 # working dir -> output_dir\n    \"\"\"\n    result = await sandbox.execute_code(code)\n\n    # New / modified files are detected and returned\n    print(result.files)   # ['result.txt', 'greeting.txt']\n    sandbox.read_file(\"result.txt\")\n```\n\nReach the same files from a shell with `bash()`:\n\n```python\nbash(\"echo 'hello from bash' > note.txt && cat note.txt\")   # shell over the workspace\n```\n\n> **Runnable:** [filesystem.py](examples/parselbox-basics/filesystem.py) · [bash.py](examples/parselbox-basics/bash.py)\n\n### 4\\. Packages & Networking\n\n#### Packages\n\nPyodide 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.\n\n```python\nfrom parselbox import Parselbox, Mount\n\n# preload Python + npm packages on startup\nParselbox(packages=[\"numpy\", \"pandas\", \"npm:lodash\"])\n\n# local wheel — mount its dir so Deno can read the host path\nParselbox(packages=[\"file:///host/wheels/pkg.whl\"],\n          mounts=[Mount(\"./wheels\", \"wheels\", \"ro\")])\n\n# remote wheel — needs network access\nParselbox(packages=[\"https://example.com/pkg.whl\"], network=True)\n\n# autoload as imports appear (only official domains when network=False)\nParselbox(allow_runtime_packages=True)\n```\n\n> [!NOTE]\n> 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.\n\n#### Networking\n\nAfter 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()`.\n\n```python\n# Block everything (default)\nParselbox(network=False)\n\n# Allow specific domains (Python API only)\nParselbox(network=[\"api.github.com:443\"])\n\n# Allow everything\nParselbox(network=True)\n```\n\n> [!NOTE]\n> The CLI `--network` flag is a boolean toggle only. Domain allowlists are available via the Python API.\n\n#### Proxy & Credential Injection\n\nPyodide 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:\n\n```python\nasync with Parselbox(\n    network=[\"127.0.0.1:8900\"],                 # sandbox can ONLY reach the proxy\n    env={\n        \"OPENAI_BASE_URL\": \"http://127.0.0.1:8900/v1\",\n        \"OPENAI_API_KEY\": \"phantom-token\",      # harmless; the real key lives on the proxy\n    },\n) as sbx:\n    await sbx.execute_code(\"import openai; openai.OpenAI().chat.completions.create(...)\")\n```\n\nMost 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.\n\n> **Runnable:** [basics.py](examples/parselbox-basics/basics.py)\n\n### 5\\. JavaScript Interop\n\nParselbox runs Python inside Deno's V8 engine via Pyodide, so Python and JavaScript share the same process memory — interop is seamless.\n\n#### `js()` — Execute JavaScript from Python\n\n```python\n# Basic — auto converts args and results\njs(\"return data.map(x => x * 2)\", data=[1, 2, 3])  # [2, 4, 6]\n\n# Callbacks — Python functions auto-proxied, no create_proxy needed\njs(\"return items.filter(fn)\", items=[1,2,3,4,5], fn=lambda x, *_: x > 3)  # [4, 5]\n\n# Async + Web APIs (Intl, Crypto, URL, TextEncoder)\njs(\"return crypto.randomUUID()\")\n```\n\nEach `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.\n\n#### `require()` — Import npm Packages, Local Modules and WASM\n\n```python\n# npm packages — returns proxy + auto-injects in js() scope (alias= to rename)\nlodash = require(\"lodash\")\nlodash.chunk([1, 2, 3, 4], 2)  # [[1, 2], [3, 4]]\n\n# Callbacks work with require'd packages\nlodash.sortBy(data, lambda x, *_: x[\"age\"])\n\n# Also available in js()\njs(\"return lodash.invert({a: 1, b: 2})\")\n\n# Local TypeScript — compiled by Deno, hot-reloads; can import npm internally\nrequire(\"./math_utils.ts\").fibonacci(10)\n\n# .wasm modules & WASI binaries load too — see WASM Tools\n\n# Instances keep their methods — chain them\ndayjs = require(\"dayjs\")\ndayjs(\"2026-06-15\").add(30, \"day\").format(\"YYYY-MM-DD\")   # \"2026-07-15\"\n\n# Class constructors auto-detect `new`\ncolor = require(\"color\")\ncolor(\"red\").darken(0.5).hex()   # \"#800000\"\n\n# Chains work with Python callbacks\nlodash(data).filter(lambda x, *_: x[\"pay\"] > 100).sortBy(lambda x, *_: -x[\"pay\"]).value()\n```\n\n#### Deno Streaming (Large Files)\n\nFor files too large to fit in memory, use Deno streams via `resolvePath()`:\n\n```python\njs(\"\"\"\n    const path = resolvePath(\"sample.txt\");\n    const info = await Deno.stat(path);\n    return { size: info.size, isFile: info.isFile };\n\"\"\")\n```\n\nPython callbacks work inside streaming pipelines — Deno reads, JS parses, Python classifies each line.\n\n#### Writing and Importing Modules\n\n```python\n# Python module — write it, import it\nopen(\"helpers.py\", \"w\").write(\"def double(x): return x * 2\")\nfrom helpers import double\ndouble(21)  # 42\n\n# TypeScript module — compiled by Deno\nopen(\"transform.ts\", \"w\").write(\"export function upper(s: string) { return s.toUpperCase(); }\")\nrequire(\"./transform.ts\").upper(\"hello\")  # \"HELLO\"\n```\n\nYou can even compile a language to WebAssembly in-sandbox, then `require()` the output.\n\n#### `bash()` — Shell Commands\n\nA 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.\n\n```python\nbash(\"echo hello > note.txt && cat note.txt | tr a-z A-Z\")   # \"HELLO\"\nbash(\"grep -rn hello . | wc -l\")\nbash(\"curl -s https://api.github.com/zen\")                   # network rules still apply\n```\n\n> **Runnable:** [javascript.py](examples/parselbox-basics/javascript.py) · [bash.py](examples/parselbox-basics/bash.py)\n\n### 6\\. WASM Tools\n\nPyodide 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.\n\nA missing capability is just a file.\n\n#### Two kinds of `.wasm`\n\n`require()` inspects the module and picks the right shape:\n\n```python\n# Library module (no imports) — its exports become methods\nrequire(\"./fib.wasm\").fib(20)                      # 6765\n\n# Command module (a WASI program) — becomes a callable command\npandoc = require(\"./pandoc.wasm\")\nr = pandoc([\"-f\", \"markdown\", \"-t\", \"html5\"], stdin=\"# Report\")\nr[\"stdout\"].decode()                               # '<h1 id=\"report\">Report</h1>'\n```\n\nA command returns `{\"exit\": int, \"stdout\": bytes, \"stderr\": str, \"missing\": [...]}` — `missing` lists any syscalls the binary asked for that aren't implemented, so gaps surface as data rather than a crash.\n\n> [!IMPORTANT]\n> **Emscripten builds are not WASI builds.** Much of npm's \"wasm\" (`sql.js`, `ffmpeg.wasm`, `tesseract.js`) is compiled with Emscripten and needs its own JavaScript glue — import those as **npm packages** (`require(\"sql.js\")`), not as bare `.wasm` files. Both routes work; `require()` tells you which one a binary needs.\n\n```python\nrun(args=None, stdin=\"\", env=None, preopens=None, argv0=None)\n```\n\n- **`stdin`** — `str` or `bytes`; **`stdout`** always comes back as `bytes`.\n- **`preopens`** — grant extra guest directories, e.g. `preopens={\"/usr\": \"vendor/usr\"}` for a binary that expects its own tree.\n- **`argv0`** — some binaries dispatch on their program name (lld becomes `wasm-ld` busybox-style).\n\n#### Binaries as `bash()` commands\n\nA WASI command binary (a `.wasm` exporting `_start`) in a mount's `bin/` directory becomes a shell command, usable alongside `bash()`'s JavaScript coreutils. Binaries are discovered per call, so a tool written mid-session works immediately.\n\n```python\nopen(\"bin/pandoc.wasm\", \"wb\").write(pandoc_bytes)\n\nbash(\"pandoc -f markdown -t plain notes.md | head -3 | tr a-z A-Z\")\n#     ^^ compiled pandoc                      ^^ just-bash builtins\n```\n\nMount a `bin/` folder read-only to ship a fixed toolset the agent can use but not modify — nothing installed on the host — or have it fetch a `.wasm` into `bin/` at runtime, which works even when the sandbox's network is restricted to a single allowlisted host.\n\nYou can even build one from source in-process — fetch a WASI clang + `wasm-ld` into `bin/`, compile C to `.wasm`, then `require()` the result. No host toolchain, nothing installed.\n\n> [!NOTE]\n> - Auto-detected as WASI `preview1` or `wasi_unstable` (preview0). Not supported: sockets, real sleeps, preview2 components.\n> - Compiled modules are cached per path (invalidated on rebuild), so a 50MB binary compiles once per session.\n\n> **Runnable:** [pandoc.py](examples/wasi/pandoc.py) — fetch a WASI binary · [compile_c.py](examples/wasi/compile_c.py) — compile C → wasm in-sandbox\n\n### 7\\. Progressive Disclosure\n\nThe `sbx` toolkit lets agents discover capabilities on demand instead of loading everything into context up front. Available as `sbx.*` inside the sandbox.\n\n| Function | Description |\n| :--- | :--- |\n| `sbx.help()` | Returns a full guide to using the sandbox. |\n| `sbx.info()` | Get sandbox environment info — context, packages, network, mounts, serve etc. |\n| `sbx.search(pattern)` | Search tools across all namespaces by name, description, or parameter. |\n| `sbx.inspect(tools)` | Get detailed schemas and documentation for tools. |\n| `sbx.preview(data)` | Summarize large or nested data structures — preserves keys, truncates content. |\n\nThe sandbox also exposes a `help()` builtin for per-object introspection:\n\n```python\n# Sandbox guide\nhelp()\n\n# Namespace tree view — shows all methods with hierarchy\nhelp(robot)\n# Remote namespace 'robot' — methods execute on the host and return results.\n# Methods:\n# ├── sensors\n# │   └── temperature()\n# └── move()\n\n# Tool details — description, parameters, output schema\nhelp(robot.move)\n# {\"description\": \"Move robot to position.\", \"parameters\": {...}, \"output\": {...}}\n\n# Works on local objects too\nhelp(len)\n```\n\n**Example:**\n\n```python\n# Discover what's available\nsbx.info()\n\n# Search for tools across all namespaces\nsbx.search(\"repo|query\")\n\n# Get tool signatures before calling\nsbx.inspect([\"github.search_repositories\", \"db.query\", \"robot.move\"])\n\n# Parallel execution with .task\nimport asyncio\nresults = await asyncio.gather(*[api.fetch.task(id=i) for i in ids])\n\n# Inspect unknown response structure\nsbx.preview(results)\n```\n\n> **Runnable:** [toolkit.py](examples/parselbox-basics/toolkit.py)\n\n### 8\\. Generative UI\n\nAgents can surface results two ways: **inline in the conversation** with `display()`, or as a **full web app** with `serve`.\n\n#### Inline widgets — `display()`\n\nAny HTML an agent passes to `display()` renders as a widget beneath its result, in hosts that support [MCP Apps](https://modelcontextprotocol.io).\n\n```python\nawait sbx.execute_code(\"\"\"\n    display(\"<h1>Q3 Revenue</h1><p class='text-lg'>Up <b>12%</b> to $4.1M</p>\")\n\"\"\")\n```\n\nTailwind and daisyUI are injected automatically, so plain markup is styled without a build step, and `pbx.call(\"/api/route\", body)` inside the HTML reaches `@api` handlers when `serve` is on. `display()` also accepts a path to an HTML file in the workspace. One view per execution — the last call wins.\n\n**On by default.** `run_mcp(ui=False)` turns it off, which stops advertising `display()` to the agent and drops the renderer from the tool. The rendered HTML is always on `result.view` regardless:\n\n```python\nresult = await sbx.execute_code('display(\"<b>done</b>\")')\nresult.view          # full HTML document, or None if display() wasn't called\n```\n\n#### Web apps — `serve`\n\nThe `serve` option starts a Deno HTTP server inside the sandbox — agents build full web apps on the fly.\n\n```python\n# SDK\nsandbox = Parselbox(serve=3000)\n\n# CLI\nuvx parselbox --serve 3000\n```\n\n**Static Files:** Any files written to the Pyodide working directory are automatically served:\n\n```python\nopen(\"index.html\", \"w\").write(\"<h1>Hello World</h1>\")\nopen(\"style.css\", \"w\").write(\"h1 { color: blue; }\")\n```\n\nServed at their own paths, with `/` resolving to `index.html`; uploaded and input files live under `/files/*`.\n\n**API Handlers:** Define endpoints using FastAPI-style decorators:\n\n```python\n@api.get(\"/items\")\ndef list_items(params):\n    limit = int(params.get(\"limit\", 10))\n    return items[:limit]\n\n@api.post(\"/items\")\ndef create_item(body):\n    return {\"id\": len(items) + 1, \"name\": body[\"name\"]}\n```\n\nRoutes are prefixed with `/api/` automatically. Verbs: `@api.get/post/put/patch/delete`.\n\nHandlers can call MCP tools, context functions, and any sandbox code:\n\n```python\n@api.get(\"/dashboard\")\nasync def dashboard(params):\n    import asyncio\n    sensors, orders = await asyncio.gather(\n        robot.sensors.temperature.task(),\n        store.get.task(\"/orders\", params={\"limit\": 5}),\n    )\n    return {\"temperature\": sensors, \"recent_orders\": orders}\n```\n\n**Built-in Endpoints:**\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/_upload` | POST | File upload (multipart form data) |\n| `/_live` | GET | SSE stream — connected browsers refresh when static files change (on by default) |\n| `/_routes` | GET | List registered API handlers |\n\n```bash\ncurl -F \"file=@photo.png\" http://localhost:3000/_upload\n# {\"uploaded\": [{\"name\": \"photo.png\", \"path\": \"/files/photo.png\", \"size\": 12345}]}\n```\n\n> **Runnable:** [display.py](examples/parselbox-basics/display.py) · [serve.py](examples/parselbox-basics/serve.py)\n\n### 9\\. Sandbox Hooks\n\nHooks intercept sandbox lifecycle events — log executions, approve tool calls, enforce policies. Pass them via the `hooks` parameter.\n\n```python\nfrom parselbox import Parselbox, Callback, ExecutionResult\nfrom parselbox.hooks import Hook\n\nclass AuditHook(Hook):\n    async def pre_execute(self, code: str):\n        print(f\"Executing: {code[:80]}...\")\n\n    async def post_execute(self, result: ExecutionResult):\n        print(f\"Result: {result.output}\")\n\n    async def pre_tool_call(self, callback: Callback):\n        if \"drop\" in str(callback.kwargs).lower():\n            raise PermissionError(\"DROP statements are blocked\")\n\n    async def post_tool_call(self, callback: Callback, result):\n        print(f\"Tool {callback.name} returned\")\n\nasync with Parselbox(\n    context={\"db\": db},\n    hooks=[AuditHook()],\n) as sbx:\n    await sbx.execute_code(\"db.query(sql='SELECT 1')\")\n```\n\n**`ElicitHook`** — a built-in hook that uses MCP elicitation for human-in-the-loop approval. Enable via `--elicit` (CLI) or `run_mcp(elicit=True)` (API). Only fires if the MCP client advertises elicitation capability — otherwise it's a no-op.\n\n```bash\n# CLI\nuvx parselbox --mcp mcp.json --elicit\n\n# API\nawait sandbox.run_mcp(elicit=True)\n```\n\n| Hook | Trigger | Use Cases |\n|:---|:---|:---|\n| `pre_execute` | Before code runs | Logging, policy checks, code sanitization |\n| `post_execute` | After code completes | Audit trails, result validation |\n| `pre_tool_call` | Before a context/MCP call | Approval gates, rate limiting, blocking |\n| `post_tool_call` | After a context/MCP call returns | Logging, result transformation |\n\n> **Runnable:** [hooks.py](examples/parselbox-basics/hooks.py)\n\n---\n\n## Configuration Reference\n\n`Parselbox` has the following configuration options:\n\n```python\nfrom parselbox import Parselbox, Mount\n\nsandbox = Parselbox(\n    context=dict(db=db, notify=send_alert),   # Proxied functions and namespaces\n    globals=dict(name=\"hi\", threshold=0.5),   # Static values copied into sandbox\n    files=[\"./input.txt\"],                    # Read/write files at /files/\n    mounts=[\n        Mount(\"./datasets\", \"/data\", \"ro\"),   # Read-only mount\n        Mount(\"./workspace\", \"/work\", \"rw\"),  # Read/write mount\n    ],\n    output_dir=\"./outputs\",                   # Persist sandbox files\n    packages=[\"numpy\", \"npm:lodash\"],         # Install on startup (Python + npm)\n    package_dir=\"./cache\",                    # Persist package cache across sessions\n    allow_runtime_packages=True,              # Auto-install from imports (default: False)\n    network=True,                             # True, False, or [\"domain:port\", ...] (API only)\n    mcp=\"./mcp.json\",                         # Connect MCP servers (path or dict)\n    serve=8080,                               # Enable web server on port\n    memory=2048,                              # WASM memory limit in MB (default: 2048)\n    timeout=60,                               # Execution timeout in seconds (default: 60, 0 disables)\n    hooks=[AuditHook()],                      # Lifecycle hooks\n    env={                                     # Custom env vars (available in Python os.environ)\n        \"OPENAI_BASE_URL\": \"http://proxy/v1\", # SDK base_url overrides for reverse proxy\n        \"OPENAI_API_KEY\": \"phantom\",          # Phantom tokens (real keys on proxy)\n        \"HTTP_PROXY\": \"http://proxy:8080\",    # Deno-level proxy (filtered from os.environ)\n        \"DENO_CERT\": \"/path/to/ca.pem\",       # Custom CA for MITM proxy\n    },\n)\n```\n\n---\n\n## Architecture\n\nParselbox runs agent code in one Deno process with Pyodide (CPython in WebAssembly) — no containers, no VMs. The permission-jailed **sandbox** works in an isolated temp workspace, with no network and no host access beyond the mounts you grant; the **host** holds the credentials. Every tool call is a round-trip between them:\n\n```\n  1. exec       HOST ──▶ SANDBOX    your code runs, permission-jailed\n  2. callback   HOST ◀── SANDBOX    code calls a tool as native Python\n  3. result     HOST ──▶ SANDBOX    host runs it with the real credentials\n```\n\nTools *look* like native Python inside the sandbox, but they execute on the host — so **credentials never enter the sandbox**.\n\n---\n\n## Security\n\nParselbox's boundary is **Deno's permission system** — the sandbox starts with nothing and gets only what you configure.\n\n- **Filesystem** — isolated temp workspace (wiped on exit); read/write only to paths you pass (`files`, `mounts` as `ro`/`rw`, `output_dir`). Package-cache writes lock after startup unless `allow_runtime_packages=True`.\n- **Network** — off by default (revoked before your code runs). Opt in with `network=True`, an allowlist `network=[\"host:port\", ...]`, or `allow_runtime_packages=True` (package domains only). For authenticated APIs, front it with a proxy — see [Proxy & Credential Injection](#proxy--credential-injection).\n- **Compiled tools (WASI)** — no sockets, so a binary has no network of its own; it sees only the mounts you grant (`ro` enforced by Deno), and a runaway is killed by the execution timeout.\n- **Resource limits** — WASM memory capped per instance at the V8 level (default 2048 MB), JS heap capped, per-execution timeout (default 60s → `KeyboardInterrupt`), auto-reconnect if the Deno process dies.\n- **Context bridge** — only the objects you pass are reachable, and only their public methods; MCP servers expose their full tool set.\n\n## Related Work\n\n- [Code execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp) (Anthropic)\n- [Code Mode](https://blog.cloudflare.com/code-mode/) (Cloudflare)\n- [smolagents](https://huggingface.co/docs/smolagents/en/tutorials/secure_code_execution) (Hugging Face)\n- [Deno + Pyodide Sandbox](https://til.simonwillison.net/deno/pyodide-sandbox) (Simon Willison)\n\nBuilt with [Deno](https://deno.com), [Pyodide](https://pyodide.org), and [just-bash](https://github.com/vercel-labs/just-bash) (Vercel).\n",
  "bytes": 37301,
  "sha": "64373233d0367e61de73b032ce4a30ac9cc3ff5c637db32355c26aa5772f72ad",
  "repo_slug": "thesanjeetc/parselbox",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_thesanjeetc_parselbox_3cfe3c83/readme"
}