# ipybox > ipybox Unified execution environment for Python code, shell commands, and programmatic MCP tool calls. ipybox executes code blocks in a stateful IPython kernel. A code block can contain any combination of Python code, shell commands, and programmatic MCP tool calls. Kernels can be sandboxed with sandbox-runtime, enforcing filesystem and network restrictions at OS level. It generates Python APIs for MCP server tools via mcpygen, and supports application-level approval of individual tool calls and shell commands during code execution. ipybox runs locally on your computer, enabling protected access to your local data and tools. # Documentation # ipybox Unified execution environment for Python code, shell commands, and programmatic MCP tool calls. ## Overview ipybox executes code blocks in a stateful IPython kernel. A code block can contain any combination of Python code, shell commands, and programmatic MCP tool calls. Kernels can be sandboxed with [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime), enforcing filesystem and network restrictions at OS level. It generates Python APIs for MCP server tools via [mcpygen](https://gradion-ai.github.io/mcpygen/), and supports application-level approval of individual tool calls and shell commands during code execution. ipybox runs locally on your computer, enabling protected access to your local data and tools. ## Capabilities | Capability | Description | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | **Stateful execution** | Definitions and variables persist across executions in IPython kernels | | **Unified execution** | Combine Python code, shell commands, and programmatic MCP tool calls in a code block | | **Shell command execution** | Run shell commands via `!cmd` syntax, capture output into Python variables | | **Programmatic MCP tool calls** | MCP tools called via generated Python APIs ("code mode"), not JSON directly | | **Python tool API generation** | Typed functions and Pydantic models generated from MCP tool schemas via [mcpygen](https://gradion-ai.github.io/mcpygen/) | | **Application-level approval** | Individual approval of tool calls and shell commands during code execution | | **Lightweight sandboxing** | Optional kernel isolation via Anthropic's [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime) | | **Local execution** | No cloud dependencies, everything runs locally on your machine | ## Usage | Component | Description | | -------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | **[Python SDK](https://gradion-ai.github.io/ipybox/api/code_executor/index.md)** | Python API for building applications on ipybox | | **[MCP server](https://gradion-ai.github.io/ipybox/mcpserver/index.md)** | ipybox as MCP server for code actions and programmatic tool calling | | **[Claude Code plugin](https://gradion-ai.github.io/ipybox/ccplugin/index.md)** | Plugin that bundles the ipybox MCP server and a code action skill | Freeact agent [Freeact](https://gradion-ai.github.io/freeact/) is a general-purpose agent built on ipybox. # Installation ## Python package Install ipybox using `pip`: ``` pip install ipybox ``` or `uv`: ``` uv add ipybox ``` ## MCP server ipybox can also be run as an [MCP server](https://gradion-ai.github.io/ipybox/mcpserver/index.md) using `uvx`: ``` uvx ipybox --workspace /path/to/workspace ``` See the [MCP server documentation](https://gradion-ai.github.io/ipybox/mcpserver/index.md) for configuration details. ## sandbox-runtime To use ipybox's [sandboxing](https://gradion-ai.github.io/ipybox/sandbox/index.md) features, you need to install Anthropic's [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime) separately. This provides the `srt` command for IPython kernel and MCP server isolation. Install via npm: ``` npm install -g @anthropic-ai/sandbox-runtime@0.0.21 ``` ### Mac OS On Mac OS, `sandbox-runtime` requires `ripgrep`. Install it using Homebrew: ``` brew install ripgrep ``` No other dependencies are needed on Mac OS, as `sandbox-runtime` uses the native `sandbox-exec` command for sandboxing. ### Linux On Linux, install the required system packages: ``` apt-get install bubblewrap socat ripgrep ``` Info [Sandboxing](https://gradion-ai.github.io/ipybox/sandbox/index.md) with `srt` currently doesn't work with ipybox on Linux, a fix is work in progress. You can still use ipybox on Linux with `sandbox=False`, or run the ipybox [MCP server](https://gradion-ai.github.io/ipybox/mcpserver/index.md) as a Docker container. # Quickstart This guide walks through Python code execution, shell commands, programmatic MCP tool calling, and application-level approval with ipybox. ## Installation ``` pip install ipybox ``` ## Basic execution CodeExecutor runs Python code and shell commands in an IPython kernel: ``` async with CodeExecutor() as executor: # Execute Python code result = await executor.execute("print('hello from Python')") print(result.text) # Execute a shell command result = await executor.execute("!echo hello from shell") print(result.text) # Mix Python and shell in one block code = """ name = "ipybox" !echo hello from {name} # Capture shell output into a Python variable files = !ls /tmp print(f"found {len(files)} entries in /tmp") """ result = await executor.execute(code) print(result.text) ``` Shell commands use IPython's `!` syntax and mix freely with Python code. `result = !cmd` captures shell output into a Python variable. Python variables are interpolated into shell commands via `{variable}` syntax. For multi-line shell scripts, use `%%bash` or `%%sh` cell magics. ## Programmatic MCP tool calling ipybox can generate typed Python APIs from MCP server tool schemas via [mcpygen](https://gradion-ai.github.io/mcpygen/). The generated APIs can be imported and called like regular Python functions. This example uses the [Brave Search MCP server](https://github.com/brave/brave-search-mcp-server). Sign up for a free API key at [api.search.brave.com](https://api.search.brave.com) and set it as an environment variable: ``` export BRAVE_API_KEY=your_api_key_here ``` `generate_mcp_sources()` connects to the MCP server, discovers its tools, and generates a typed Python package: ``` await generate_mcp_sources( server_name="brave_search", server_params=SERVER_PARAMS, root_dir=Path("mcptools"), ) ``` See [API Generation](https://gradion-ai.github.io/ipybox/apigen/index.md) for details on server parameters, generated package structure, and supported transports. The generated API can then be imported and called in code submitted to `execute()`, which auto-approves all tool calls: ``` CODE = """ from mcptools.brave_search.brave_image_search import Params, Result, run result: Result = run(Params(query="neural topic models", count=3)) for image in result.items: print(f"- [{image.title}]({image.properties.url})") """ ``` ``` async with CodeExecutor() as executor: result = await executor.execute(CODE) print(result.text) ``` ## Streaming vs execute `execute()` runs code to completion and auto-approves any tool calls and shell commands. For incremental output and control over [approvals](#approval), use `stream()` instead. `stream()` yields events as execution progresses: - `ApprovalRequest` when code triggers a programmatic MCP tool call or a shell command - CodeExecutionChunk for incremental output (when `chunks=True`) - CodeExecutionResult with the final output ## Approval Both MCP tool calls and shell commands can require application-level approval before execution. `approve_tool_calls` (default `True`) requires approval for MCP tool calls. `approve_shell_cmds` (default `False`) requires approval for `!cmd` shell commands and `%%bash`/`%%sh` cell magics. The following example executes a code block that calls an MCP tool and runs a shell command, both requiring approval: ``` SEARCH_AND_ECHO = """ from mcptools.brave_search.brave_image_search import Params, Result, run result: Result = run(Params(query="neural topic models", count=3)) !echo "Found {len(result.items)} images" """ ``` ``` async with CodeExecutor( approve_tool_calls=True, # default approve_shell_cmds=True, ) as executor: async for item in executor.stream(SEARCH_AND_ECHO): match item: case ApprovalRequest(tool_name="shell" | "shell_magic", tool_args=args): print(f"Shell: {args['cmd']}") await item.accept() case ApprovalRequest(tool_name=name, tool_args=args): print(f"Tool call: {name}({args})") await item.accept() case CodeExecutionResult(text=text): print(text) ``` Both approval types yield an `ApprovalRequest`. The `tool_name` field distinguishes them: `"shell"` for `!` commands, `"shell_magic"` for `%%bash`/`%%sh` cell magics, and the MCP tool name for tool calls. Call `accept()` to continue or `reject()` to block execution. ## Next steps - [Code Execution](https://gradion-ai.github.io/ipybox/codeexec/index.md) - Shell commands, mixing, approval, and streaming - [API Generation](https://gradion-ai.github.io/ipybox/apigen/index.md) - Generating typed Python APIs from MCP tools - [Sandboxing](https://gradion-ai.github.io/ipybox/sandbox/index.md) - Kernel isolation with filesystem and network restrictions # Architecture CodeExecutor coordinates three components: an IPython kernel for stateful execution of Python code and shell commands, a tool server for MCP tool dispatch, and an approval channel for application-level approval of tool calls and shell commands. The application submits code to `CodeExecutor`, which forwards it to an IPython kernel running inside an optional sandbox. Inline shell commands use IPython's `!` syntax[1](#fn:1) and mix freely with Python code in a single block. When code calls a [generated](https://gradion-ai.github.io/ipybox/apigen/index.md) Python tool API function, the request routes to the tool server, which manages local (stdio) MCP servers and connections to remote (HTTP) MCP servers. Before executing any tool call, the tool server sends an approval request back through `CodeExecutor` to the application, blocking until it accepts or rejects. Shell commands go through the same approval channel when shell command approval is enabled. MCP tool execution runs outside the kernel sandbox in the tool server. Shell commands execute as kernel subprocesses inside the sandbox when enabled. mcpygen The code generation and tool execution infrastructure is provided by [mcpygen](https://gradion-ai.github.io/mcpygen/) and re-exported by ipybox. `CodeExecutor` coordinates sandboxed execution of Python code and shell commands, MCP tool execution, and approval of tool calls and shell commands. ______________________________________________________________________ 1. `%%bash`/`%%sh` cell magics are also supported and run an entire code block as a shell script. [↩](#fnref:1 "Jump back to footnote 1 in the text") # Python tool API generation ``` from ipybox import generate_mcp_sources ``` `generate_mcp_sources()` generates a typed Python tool API from MCP server tool schemas. API generation is provided by [mcpygen](https://gradion-ai.github.io/mcpygen/) and re-exported by ipybox. Each tool becomes a module with a Pydantic `Params` class, a `Result` class or `str` return type, and a `run()` function. ## Stdio servers For MCP servers that run as local processes, specify `command`, `args`, and optional `env`: ``` brave_mcp_params = { "command": "npx", "args": ["-y", "@brave/brave-search-mcp-server", "--transport", "stdio"], "env": {"BRAVE_API_KEY": "${BRAVE_API_KEY}"}, } await generate_mcp_sources( server_name="brave_search", server_params=brave_mcp_params, root_dir=Path("mcptools"), ) ``` ## HTTP servers For remote MCP servers over HTTP, specify `url` and optional `headers`: ``` github_mcp_params = { "url": "https://api.githubcopilot.com/mcp/", "headers": {"Authorization": "Bearer ${GITHUB_API_KEY}"}, } await generate_mcp_sources( server_name="github", server_params=github_mcp_params, root_dir=Path("mcptools"), ) ``` ipybox auto-detects the transport type from the URL. URLs containing `/mcp` use streamable HTTP, URLs containing `/sse` use SSE. You can also set `type` explicitly to `"streamable_http"` or `"sse"`. ## Environment variable substitution You can use `${VAR_NAME}` placeholders in `server_params` values. ipybox replaces them with the corresponding environment variable when connecting to the MCP server. This keeps secrets out of your code. ## Generated package structure The Brave Search MCP server [example above](#stdio-servers) generates a package structure like this: ``` mcptools/ └── brave_search/ ├── __init__.py ├── brave_web_search.py ├── brave_local_search.py ├── brave_image_search.py └── ... ``` For each MCP server tool, a separate Python module is generated, named after the tool. ## Using the generated API Each module provides a typed interface for programmatic MCP tool calls: ``` from mcptools.brave_search.brave_image_search import Params, Result, run # Params validates input params = Params(query="neural topic models", count=3) # run() calls the MCP tool and returns a Result (or str for untyped tools) result: Result = run(params) for image in result.items: print(image.title) ``` The `Params` class is generated from the tool's input schema. Tools with an output schema get a typed `Result` class; others return `str`. The MCP tool itself is called via its `run()` function. # Code execution CodeExecutor runs Python code, shell commands, and programmatic MCP tool calls in a stateful IPython kernel through a unified execution interface: all three can be combined in a single code block. Both tool calls and shell commands support application-level approval before execution. ``` from ipybox import ( ApprovalRequest, CodeExecutionChunk, CodeExecutionResult, CodeExecutor, ) ``` ## Basic execution Use `execute()` for non-interactive execution where MCP tool calls and shell commands, if any, are auto-approved: ``` async with CodeExecutor() as executor: result = await executor.execute("print('hello world')") assert result.text == "hello world" ``` For streaming output or application-level approval control, use `stream()` instead. ## Shell commands Shell commands use IPython's `!` syntax: ``` async with CodeExecutor() as executor: # Run a shell command result = await executor.execute("!echo hello from shell") assert result.text == "hello from shell" # Capture shell output into a Python variable code = """ files = !ls /tmp print(f"found {len(files)} entries") """ result = await executor.execute(code) # Variable interpolation in shell commands code = """ name = "world" !echo hello {name} """ result = await executor.execute(code) assert result.text == "hello world" ``` `!cmd` runs a shell command and prints its output. `result = !cmd` captures the output as a list of lines. Python variables are interpolated into shell commands via `{variable}` syntax. Shell commands and Python code mix freely in a single code block, for example to install packages with `!pip install` and use them immediately. ## Cell magics For multi-line shell scripts, use `%%bash` or `%%sh` cell magics: ``` code = """ %%bash for i in 1 2 3; do echo $i done """ async with CodeExecutor() as executor: result = await executor.execute(code) assert result.text == "1\n2\n3" ``` `%%bash` must be the first line of the code block and passes the remaining lines to bash as a script. `%%sh` works the same way with `sh`. Unlike `!cmd`, cell magics cannot be mixed with Python code and do not support Python variable interpolation. ## Tool calls ipybox can [generate typed Python APIs](https://gradion-ai.github.io/ipybox/apigen/index.md) from MCP server tool schemas. The generated code executes within the kernel, while MCP servers run on a separate [tool server](https://gradion-ai.github.io/ipybox/architecture/index.md). ## Approval ### Tool calls When code calls a generated tool API, ipybox suspends execution and yields an `ApprovalRequest`. Call `accept()` to continue: ``` code = """ from mcptools.brave_search.brave_image_search import Params, Result, run result: Result = run(Params(query="neural topic models", count=3)) print(f"num results = {len(result.items)}") """ async with CodeExecutor(approve_tool_calls=True) as executor: # default async for item in executor.stream(code): match item: case ApprovalRequest(tool_name=name, tool_args=args): assert name == "brave_image_search" assert args["query"] == "neural topic models" assert args["count"] == 3 await item.accept() case CodeExecutionResult(): assert item.text == "num results = 3" ``` `ApprovalRequest` includes `tool_name` and `tool_args` for inspection. Calling `reject()` raises a CodeExecutionError containing an `ApprovalRejectedError` traceback from the kernel. `approve_tool_calls` (default `True`) is set explicitly in the example above. Set it to `False` to skip approval and execute tool calls directly when using `stream()`. The `execute()` method always auto-approves tool calls regardless of this setting. ### Shell commands Enable `approve_shell_cmds=True` to require application-level approval for shell commands: ``` code = """ name = "world" !echo hello {name} """ async with CodeExecutor(approve_shell_cmds=True) as executor: async for item in executor.stream(code): match item: case ApprovalRequest(tool_name="shell", tool_args=args): assert args == {"cmd": "echo hello world"} await item.accept() case CodeExecutionResult(): assert item.text == "hello world" ``` Each `!cmd` triggers an `ApprovalRequest` with `tool_name="shell"` and `tool_args={"cmd": "..."}`, using the same approval interface as tool calls. Variable interpolation happens before the approval request, so the application sees the fully expanded command. `%%bash` and `%%sh` cell magics also trigger approval when `approve_shell_cmds=True`, with `tool_name="shell_magic"` and `tool_args={"cmd": "..."}` containing the cell body: ``` code = """ %%bash echo hello from bash """ async with CodeExecutor(approve_shell_cmds=True) as executor: async for item in executor.stream(code): match item: case ApprovalRequest(tool_name="shell_magic", tool_args=args): assert "echo hello from bash" in args["cmd"] await item.accept() case CodeExecutionResult(): assert item.text == "hello from bash" ``` #### Preventing bypass Code could bypass shell command approval through various process-creation APIs (`subprocess`, `os.system()`, `os.exec*()`, `os.spawn*()`, `os.posix_spawn()`, `pty.spawn()`). Set `require_shell_escape=True` to guard these, forcing all shell execution through `!cmd` or `%%bash`/`%%sh` where it triggers the approval flow: ``` async with CodeExecutor(approve_shell_cmds=True, require_shell_escape=True) as executor: # Direct subprocess calls are blocked to prevent bypassing approval try: await executor.execute('import subprocess; subprocess.run(["echo", "hi"])') except Exception as e: assert "RuntimeError" in str(e) # Shell commands via !cmd still work and go through approval async for item in executor.stream("!echo hello"): match item: case ApprovalRequest(): await item.accept() case CodeExecutionResult(): assert item.text == "hello" ``` With `require_shell_escape=True`, direct process-creation calls raise a `RuntimeError`. Shell commands via `!cmd` and `%%bash`/`%%sh` still work and go through the approval channel. Requires `approve_shell_cmds=True`. Note These guards are Python-level guards that close the most obvious gaps. They catch accidental bypass (e.g., an LLM agent reaching for `subprocess.run`) but are not a security boundary: code running in the kernel can undo guards, call C functions via `ctypes`, or use CPython internal modules. These bypasses can be prevented at the OS level. A future version will add [sandbox](https://gradion-ai.github.io/ipybox/sandbox/index.md)-level enforcement for shell command approval. ## Stream output chunks Enable `chunks=True` to receive output incrementally as it's produced: ``` code = """ from time import sleep print("chunk 1") sleep(0.5) print("chunk 2") """ async with CodeExecutor() as executor: async for item in executor.stream(code, chunks=True): match item: case CodeExecutionChunk(): assert item.text.strip() in ["chunk 1", "chunk 2"] case CodeExecutionResult(): assert item.text == "chunk 1\nchunk 2" ``` CodeExecutionChunk events contain partial output. The final CodeExecutionResult contains the complete, aggregated output. ## Capturing plots Plots are automatically captured as PNG files. Set `images_dir` to specify the output directory: ``` code = """ import matplotlib.pyplot as plt plt.plot([1, 2, 3], [1, 4, 9]) plt.show() """ with tempfile.TemporaryDirectory() as images_dir: async with CodeExecutor(images_dir=Path(images_dir)) as executor: result = await executor.execute(code) assert len(result.images) == 1 assert result.images[0].exists() assert result.images[0].suffix == ".png" ``` Generated images are available in `result.images` as a list of `Path` objects. ## Custom timeouts Configure approval and execution timeouts: ``` # set custom approval timeout, default is no timeout async with CodeExecutor(approval_timeout=10) as executor: # set custom execution timeout, default is no timeout async for item in executor.stream("...", timeout=10): ... ``` - `approval_timeout`: How long to wait for `accept()`/`reject()` (default: no timeout) - `timeout` in `stream()`: Maximum total execution time **excluding approval waits**. Tool execution time and kernel execution time still count toward this budget (default: no timeout). ## Kernel environment The IPython kernel does not inherit environment variables from the parent process. Pass them with `kernel_env`: ``` # IPython kernel does not inherit environment variables from parent process # Kernel environment must be explicitly set using the kernel_env parameter async with CodeExecutor(kernel_env={"TEST_VAR": "test_val"}) as executor: result = await executor.execute("import os; print(os.environ['TEST_VAR'])") assert result.text == "test_val" ``` Note Environment variables referenced in `server_params` via `${VAR_NAME}` are taken from the parent process and do not need to be passed to `kernel_env`. ## Kernel reset `reset()` clears all variables and definitions: ``` async with CodeExecutor() as executor: await executor.execute("x = 42") result = await executor.execute("print(x)") assert result.text == "42" await executor.reset() code = """ try: print(x) except NameError: print("x not defined") """ result = await executor.execute(code) assert result.text == "x not defined" ``` This also stops any MCP servers started during execution. They restart lazily on their next tool call. ## Resetting working directory If `working_dir` is set, the kernel starts in that directory and ipybox resets it back after each execution if code changed it. When a reset happens, ipybox prints a message in the cell output. ``` base_dir = Path.cwd() with tempfile.TemporaryDirectory() as changed_dir: async with CodeExecutor(working_dir=base_dir) as executor: result = await executor.execute(f"import os; os.chdir({changed_dir!r})") assert result.text == f"[ipybox] cwd reset to {base_dir}" result = await executor.execute("import os; print(os.getcwd())") assert result.text == str(base_dir) ``` If `working_dir` is not set, ipybox preserves the default IPython behavior: code can change the current working directory and that change persists until code changes it again or the kernel is reset. ``` with tempfile.TemporaryDirectory() as changed_dir: async with CodeExecutor() as executor: await executor.execute(f"import os; os.chdir({changed_dir!r})") result = await executor.execute("import os; print(os.getcwd())") assert result.text == changed_dir ``` # Sandboxing ipybox uses Anthropic's [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime) to isolate code execution. When enabled, the IPython kernel runs with restricted filesystem and network access. ``` from ipybox import CodeExecutionError, CodeExecutor, generate_mcp_sources ``` ## Default sandbox Enable sandboxing with `sandbox=True`. ``` async with CodeExecutor(sandbox=True) as executor: result = await executor.execute("print('hello world')") assert result.text == "hello world" code = """ import requests try: requests.get('https://example.org') except Exception as e: print(e) """ # Default sandbox config blocks internet access result = await executor.execute(code) assert "Failed to resolve 'example.org'" in result.text ``` The default sandbox configuration allows: - Reading all files except `.env` - Writing to the current directory and subdirectories (plus IPython directories) - Local network access to the tool execution server Default sandbox configuration ``` { "allowPty": true, "enableWeakerNestedSandbox": false, "filesystem": { "denyRead": [".env"], "allowWrite": [".", "~/Library/Jupyter", "~/.ipython"], "denyWrite": [] }, "network": { "allowedDomains": [], "deniedDomains": [], "allowLocalBinding": true } } ``` Internet access is blocked as demonstrated in the example above. See the [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime) documentation for all configuration options. ## Custom sandbox To allow access to `example.org`, provide a custom sandbox configuration file: examples/sandbox-kernel.json ``` { "enableWeakerNestedSandbox": false, "filesystem": { "denyRead": [".env"], "allowWrite": [".", "~/Library/Jupyter", "~/.ipython"], "denyWrite": [] }, "network": { "allowedDomains": ["example.org"], "deniedDomains": [], "allowLocalBinding": true } } ``` and pass it as `sandbox_config` argument: ``` code = """ import requests result = requests.get('https://example.org') print(result.text) """ async with CodeExecutor( sandbox=True, sandbox_config=Path("examples/sandbox-kernel.json"), log_level="WARNING", ) as executor: result = await executor.execute(code) assert "Example Domain" in result.text ``` ## Sandboxing MCP servers ### Filesystem MCP server stdio MCP servers like the [filesystem MCP server](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) can be configured to run in a sandbox using `srt` as command: ``` server_params = { "command": "srt", "args": [ "--settings", "examples/sandbox-filesystem-mcp.json", "npx", "-y", "@modelcontextprotocol/server-filesystem", ".", ], } ``` The sandbox configuration is: examples/sandbox-filesystem-mcp.json ``` { "enableWeakerNestedSandbox": false, "filesystem": { "denyRead": [".env"], "allowWrite": [".", "~/.npm"], "denyWrite": [] }, "network": { "allowedDomains": ["registry.npmjs.org"], "deniedDomains": [], "allowLocalBinding": true } } ``` The server itself is configured with permissions to access all files in the current directory (`"."`), but the sandbox additionally blocks read access to `.env`. The sandbox also allows access to `registry.npmjs.org` for downloading the server package via `npx`, and `~/.npm` for the local `npm` cache. ``` await generate_mcp_sources("filesystem", server_params, Path("mcptools")) list_dir_code = """ from mcptools.filesystem.list_directory import run, Params result = run(Params(path=".")) print(result.content) """ read_env_code = """ from mcptools.filesystem.read_file import run, Params result = run(Params(path=".env")) print(result.content) """ async with CodeExecutor(sandbox=True) as executor: # allowed by MCP server and sandbox result = await executor.execute(list_dir_code) assert "README.md" in result.text try: # allowed by MCP server but blocked by sandbox result = await executor.execute(read_env_code) assert False, "Read access to .env not blocked" except CodeExecutionError as e: assert "operation not permitted" in str(e) ``` Info MCP server sandboxing is independent of kernel sandboxing and usually not needed when using trusted servers, but provides an additional security layer when needed. ### Fetch MCP server The [fetch MCP server](https://github.com/modelcontextprotocol/servers/tree/main/src/fetch) retrieves web content and converts it to markdown. Install the server and SOCKS proxy support (used by sandbox-runtime for network filtering) as project dependencies: ``` uv add mcp-server-fetch uv add "httpx[socks]>=0.28.1" ``` Note Running via `uvx` is currently not supported because `srt` restricts access to system configuration required by `uvx`. Configure the server to run in a sandbox using `python -m mcp_server_fetch`: ``` server_params = { "command": "srt", "args": [ "--settings", "examples/sandbox-fetch-mcp.json", "python", "-m", "mcp_server_fetch", ], } ``` The sandbox configuration allows access to `example.com` for fetching content and `registry.npmjs.org` for the [readability](https://github.com/mozilla/readability) dependency: examples/sandbox-fetch-mcp.json ``` { "enableWeakerNestedSandbox": false, "filesystem": { "denyRead": [".env"], "allowWrite": [".", "~/.npm", "/tmp/**", "/private/tmp/**"], "denyWrite": [] }, "network": { "allowedDomains": ["registry.npmjs.org", "example.com"], "deniedDomains": [], "allowLocalBinding": true } } ``` ``` await generate_mcp_sources("fetch", server_params, Path("mcptools")) fetch_code = """ from mcptools.fetch import fetch result = fetch.run(fetch.Params(url="https://example.com")) print(result) """ async with CodeExecutor(sandbox=True) as executor: result = await executor.execute(fetch_code) print(result.text) assert "This domain is for use in documentation examples" in result.text ``` # MCP server [ipybox](https://gradion-ai.github.io/ipybox/index.md) is a local, sandboxed execution environment for running Python code, shell commands and programmatic MCP tool calls. Code executes in a stateful IPython kernel where variables and definitions persist across executions. When run as an MCP server, it exposes these capabilities to MCP clients like Claude Code or Claude Desktop. Agents can register MCP servers, then execute Python code that uses them programmatically: 1. Agent calls [`register_mcp_server`](#register_mcp_server) to [generate a typed Python API](https://gradion-ai.github.io/ipybox/apigen/index.md) for the tools of an MCP server 1. Agent calls [`execute_ipython_cell`](#execute_ipython_cell) to [execute Python code](https://gradion-ai.github.io/ipybox/codeexec/index.md) that imports and uses the generated API Application example An application example of this MCP server is the [programmatic tool calling plugin](https://gradion-ai.github.io/ipybox/ccplugin/index.md) for Claude Code. ## Configuration ``` { "mcpServers": { "ipybox": { "command": "uvx", "args": [ "ipybox", "--workspace", "/path/to/workspace" ] } } } ``` ## Workspace The `--workspace` option specifies the ipybox working directory, default is `"."`. Generated [Python tool APIs](https://gradion-ai.github.io/ipybox/apigen/index.md) are written to `mcptools/` in the workspace, and [code execution](#execute_ipython_cell) uses the workspace as working directory. ## Environment variables Environment variables can be passed to ipybox either via an `"env"` key in the MCP [configuration](#configuration) or in an `.env` file in the workspace directory: /path/to/workspace/.env ``` API_KEY_1=... API_KEY_2=... KERNEL_ENV_SECRET_1=... KERNEL_ENV_SECRET_2=... ``` These variables are available to MCP servers registered with ipybox but are not passed to the IPython kernel by default. To make them available to the kernel, use the `KERNEL_ENV_` prefix. For example, `KERNEL_ENV_SECRET_1` is available as `SECRET_1` in the kernel. ## Usage example This example shows a typical workflow using the [Brave Search MCP server](https://github.com/brave/brave-search-mcp-server). First, configure the ipybox MCP server with a [BRAVE_API_KEY](https://gradion-ai.github.io/ipybox/quickstart/#programmatic-mcp-tool-calling): ``` { "mcpServers": { "ipybox": { "command": "uvx", "args": ["ipybox", "--workspace", "/path/to/workspace"], "env": { "BRAVE_API_KEY": "your-api-key" } } } } ``` or add the API key to a `.env` file in the workspace directory: /path/to/workspace/.env ``` BRAVE_API_KEY=your-api-key ``` An agent then registers the Brave Search MCP server by calling `register_mcp_server` with the following argument: ``` { "server_name": "brave_search", "server_params": { "command": "npx", "args": ["-y", "@brave/brave-search-mcp-server", "--transport", "stdio"], "env": {"BRAVE_API_KEY": "${BRAVE_API_KEY}"} } } ``` The `${BRAVE_API_KEY}` placeholder is replaced with the actual value from the MCP configuration or the `.env` file. ipybox connects to the Brave Search MCP server and generates a Python tool API under `mcptools/brave_search/`. After registration, the agent calls `execute_ipython_cell` with Python code that uses the generated API: ``` from mcptools.brave_search.brave_web_search import Params, run result = run(Params(query="Python asyncio tutorial", count=3)) print(result) ``` The printed result is returned and added to the agent's context window. ## Tools The ipybox MCP server exposes four tools. ### `register_mcp_server` Connects to an MCP server and [generates a Python API](https://gradion-ai.github.io/ipybox/apigen/index.md) for its tools under `mcptools/{server_name}/`. Parameters: - `server_name` — Application-defined MCP server name (valid Python identifier) - `server_params` — Server config: `{"command", "args", "env"}` for stdio or `{"url", "headers"}` for HTTP ### `execute_ipython_cell` Executes Python code in a stateful IPython kernel. Executed code can use the generated Python tool API of [registered MCP servers](#register_mcp_server). MCP tool calls from executed code are [auto-approved](https://gradion-ai.github.io/ipybox/codeexec/#basic-execution). Parameters: - `code` — Python code to execute - `timeout` — Maximum execution time in seconds (default: 120) - `max_output_chars` — Output character limit (default: 5000) Returns the execution output. ### `install_package` Installs a Python package via `pip`. Supports version specifiers and git URLs. Parameters: - `package_name` — Package spec (e.g., `requests`, `numpy>=1.20.0`, or `git+https://...`) ### `reset` Creates a new kernel, clearing all variables and imports. Installed packages and generated `mcptools/` persist. ## Sandboxing To isolate code execution via Anthropic's [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime), enable [sandboxing](https://gradion-ai.github.io/ipybox/sandbox/index.md) with the `--sandbox` option: ``` { "mcpServers": { "ipybox": { "command": "uvx", "args": [ "ipybox", "--workspace", "/path/to/workspace", "--sandbox", "--sandbox-config", "/path/to/sandbox-config.json" ] } } } ``` The default sandbox configuration permits reading all files except `.env` and writing to the current directory and subdirectories (plus IPython directories). Access to internet is blocked. An optional custom sandbox configuration can be passed with the `--sandbox-config` option. Info Sandboxing with [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime) currently works on Mac OS only. On Linux and Windows, you can either run ipybox without sandboxing or as a [Docker container](#docker). ## Docker ipybox can be run as a Docker container. Clone the [project](https://github.com/gradion-ai/ipybox) and build the image: ``` git clone https://github.com/gradion-ai/ipybox.git cd ipybox ./docker-build.sh ``` The build script creates a container user with your UID/GID, ensuring files generated by ipybox in the mounted workspace are owned by you and can be edited on the host. Then configure the MCP server: ``` { "mcpServers": { "ipybox": { "command": "docker", "args": [ "run", "-i", "--rm", "-v", "/path/to/workspace:/app/workspace", "ipybox" ] } } } ``` The workspace `/path/to/workspace` on the host is mounted to `/app/workspace` inside the container. # Programmatic tool calling plugin for Claude Code This plugin installs the [ipybox MCP server](https://gradion-ai.github.io/ipybox/mcpserver/index.md) in Claude Code along with a `codeact`[1](#fn:1) skill. It enables Claude Code to act by generating and executing code rather than JSON tool calls. The generated code calls MCP tools and previously saved code actions programmatically. With this "code as tool" approach, saved code actions become tools themselves, available for use in future code actions. Over time, a library of code actions can be built, each composing other code actions and MCP tools. The `codeact` skill provides guidance for Claude Code to discover MCP tools and code actions via agentic filesystem search, inspect the interfaces of relevant tools, generate code actions that use them, and save successful code actions for later reuse. Tool search and inspection is performed on demand based on the current task. This progressive disclosure approach frees Claude Code from pre-loading tool sources into its context window, saving tokens and improving performance. Code actions are stored with interface separated from implementation. This separation lets Claude Code inspect only the interface without being distracted by implementation details, further reducing token consumption during tool discovery. Note This plugin is a demonstration of the "code as tool" approach. It is a prototype that requires further optimization and refinement. freeact An agent that natively provides this functionality is the [freeact](https://gradion-ai.github.io/freeact/) code action agent. It uses Gemini 3 instead of Claude and supports unified approval of code actions and tool calls. The `codeact` skill distributes responsibilities between ipybox and Claude Code: ipybox: - Generates typed Python tool APIs from MCP server tool schemas - Executes Python code that uses the generated MCP tool APIs - Uses a stateful and sandboxed IPython kernel for code execution Claude Code: - Discovers and selects MCP tools and stored code actions via agentic search - Generates and executes code in ipybox that calls selected MCP tools and code actions - Adds output parsers and structured result types to MCP tools that lack output schemas - Saves successful code actions with a structure optimized for discovery and reuse ## Environment setup The [example below](#usage-example) uses the [GitHub MCP server](https://github.com/github/github-mcp-server). Create a `.env` file with your GitHub personal access token (PAT) in the current working directory: .env ``` GITHUB_API_KEY=your-github-personal-access-token ``` The plugin automatically loads environment variables from this file. ## Plugin installation Add the ipybox repository as plugin marketplace to Claude Code: ``` claude plugin marketplace add https://github.com/gradion-ai/ipybox ``` Then install one of the available plugins: | Plugin | Description | | ----------------- | --------------------------------------------------------------------------------------------------- | | `codeact-default` | Runs ipybox without a sandbox | | `codeact-sandbox` | Runs ipybox with a [sandboxed](https://gradion-ai.github.io/ipybox/sandbox/index.md) IPython kernel | | `codeact-docker` | Runs ipybox as a Docker container | ``` claude plugin install codeact-default@ipybox ``` Warning Only **one** plugin from this marketplace should be active at a time. Having multiple plugins active simultaneously may cause conflicts. ### Sandbox configuration When using `codeact-sandbox`, you can optionally provide a `sandbox-config.json` file in the workspace directory to customize the sandbox configuration. If not provided, ipybox runs with the [default sandbox](https://gradion-ai.github.io/ipybox/sandbox/#default-sandbox). ## Usage example This example demonstrates a complete workflow: - [Registering an MCP server](#register-the-github-mcp-server) - [Using its tools programmatically](#use-the-github-mcp-server-programmatically) - [Generating an output parser](#generate-an-output-parser) - [Chaining tools in a single code action](#chaining-tools-in-a-single-code-action) - [Saving the code action for reuse](#saving-code-actions-as-tools) It uses two tools from the GitHub MCP server: - `search_repositories` - `list_commits` ### Register the GitHub MCP server User prompt ``` Register this MCP server at ipybox under name github: { "url": "https://api.githubcopilot.com/mcp/", "headers": { "Authorization": "Bearer ${GITHUB_API_KEY}" } } ``` The `${GITHUB_API_KEY}` placeholder is automatically replaced with the value from your `.env` file. This registers the GitHub MCP server and generates a typed Python API for its tools under `mcptools/github/`. Each tool becomes a module named after the tool (`search_repositories.py`, `list_commits.py`, etc.). ### Use the GitHub MCP server programmatically The codeact skill can be activated in Claude Code with phrases like "use the codeact skill" or similar: User prompt ``` Use the codeact skill to get the latest 5 commits of the 3 github repos of torvalds with the most stars. For each repo, output name, stars and the first line of commit messages, and the link to the commit ``` Claude Code first lists directories under `mcptools/` to see which tools are available. It then reads the tool files [search_repositories.py](https://github.com/gradion-ai/ipybox/blob/main/docs/generated/mcptools/github/search_repositories_orig.py) and [list_commits.py](https://github.com/gradion-ai/ipybox/blob/main/docs/generated/mcptools/github/list_commits.py) to understand their interfaces, as these appear relevant to the task. Claude Code generates two code actions. The first searches for the top 3 repos of Linus Torvalds sorted by stars: Code action ``` import json from mcptools.github import search_repositories, list_commits # Search for torvalds' repositories sorted by stars (descending) repos_result = search_repositories.run(search_repositories.Params( query="user:torvalds", sort=search_repositories.Sort.stars, order=search_repositories.Order.desc, perPage=10, minimal_output=False )) repos_data = json.loads(repos_result) print(f"Found {repos_data['total_count']} repos") # Get top 3 repos top_repos = repos_data['items'][:3] for repo in top_repos: print(f"- {repo['name']}: {repo['stargazers_count']} stars") ``` Here, Claude Code makes assumptions about the tool response structure. The GitHub MCP server tools do not provide output schemas, so Claude Code guesses the structure from its training data. These assumptions may or may not be correct depending on the MCP server's popularity, but in this example they work. MCP tools that provide output schemas get a generated `Result` class with typed fields, but the GitHub MCP server tools return unstructured strings. The second code action uses the repository information stored in `top_repos` to get the latest 5 commits of each repo: Code action ``` # Get latest 5 commits for each of the top 3 repos for repo in top_repos: repo_name = repo['name'] stars = repo['stargazers_count'] print(f"\n{'='*60}") print(f"📦 {repo_name} | ⭐ {stars:,} stars") print(f"{'='*60}") # Get commits commits_result = list_commits.run(list_commits.Params( owner="torvalds", repo=repo_name, perPage=5 )) commits_data = json.loads(commits_result) for i, commit in enumerate(commits_data[:5], 1): sha = commit['sha'] short_sha = sha[:7] message = commit['commit']['message'].split('\n')[0] # First line only url = commit['html_url'] print(f"\n{i}. {message}") print(f" 🔗 {url}") ``` This prints the requested information. However, intermediate results were added to the agent's context window. To encourage Claude Code to chain `search_repositories` and `list_commits` in a single code action, we generate an output parser for `search_repositories`. ### Generate an output parser To compensate for the lack of output schemas, we generate an output parser for the `search_repositories` tool: User prompt ``` Generate an output parser for search_repositories ``` This adds a `run_parsed()` function to [search_repositories.py](https://github.com/gradion-ai/ipybox/blob/main/docs/generated/mcptools/github/search_repositories.py), returning a structured `ParseResult`. Claude Code infers this type by interacting with the tool using example inputs. The codeact skill encourages Claude Code to prioritize `run_parsed()` over `run()` when selecting tools. The parsing implementation details are stored in [mcpparse/github/search_repositories.py](https://github.com/gradion-ai/ipybox/blob/main/docs/generated/mcpparse/github/search_repositories.py). Keeping implementation separate from interface prevents polluting the interfaces that Claude Code inspects. ### Chaining tools in a single code action Running the same task again (optionally after restarting Claude Code), Claude Code now chains the tools in a single code action because it knows the structure of the `search_repositories` result up-front. It uses the new `run_parsed()` function and navigates the structured output based on the `ParseResult` type: User prompt ``` Use the codeact skill to get the latest 5 commits of the 3 github repos of torvalds with the most stars. For each repo, output name, stars and the first line of commit messages, and the link to the commit ``` Code action ``` import json from mcptools.github import search_repositories, list_commits # Get Torvalds' repos sorted by stars repos_result = search_repositories.run_parsed( search_repositories.Params( query="user:torvalds", sort=search_repositories.Sort.stars, order=search_repositories.Order.desc, perPage=3 ) ) # Get top 3 repos top_repos = repos_result.repositories[:3] print(f"Found {len(top_repos)} repos\n") for repo in top_repos: print(f"📦 {repo.name} ⭐ {repo.stargazers_count:,} stars") print("-" * 60) # Get latest 5 commits commits_raw = list_commits.run( list_commits.Params( owner="torvalds", repo=repo.name, perPage=5 ) ) # Parse the commits JSON commits = json.loads(commits_raw) for commit in commits[:5]: sha = commit["sha"][:7] message = commit["commit"]["message"].split("\n")[0] url = commit["html_url"] print(f" {sha}: {message}") print(f" {url}") print() ``` ### Saving code actions as tools Code actions can be saved and reused as tools in later code actions. To save the previous code action, use a prompt like: User prompt ``` Save this as code action under github category with name commits_of_top_repos. Make username, top_n_repos and last_n_commits parameters ``` This creates a new package `gentools/github/commits_of_top_repos/` with an [api.py](https://github.com/gradion-ai/ipybox/blob/main/docs/generated/gentools/github/commits_of_top_repos/api.py) module that defines the typed interface and an [impl.py](https://github.com/gradion-ai/ipybox/blob/main/docs/generated/gentools/github/commits_of_top_repos/impl.py) module that contains the implementation. The interface in `api.py` exposes the tool's parameters and return types and is relevant for understanding tool usage. The implementation in `impl.py` is not inspected by Claude Code during tool selection, saving token usage. ### Using saved code actions as tools After restarting Claude Code or clearing its context window, to enforce re-discovery of tools, the same task now uses the saved code action: User prompt ``` Use the codeact skill to get the latest 5 commits of the 3 github repos of torvalds with the most stars. For each repo, output name, stars and the first line of commit messages, and the link to the commit ``` Code action ``` from gentools.github.commits_of_top_repos import run results = run(username="torvalds", top_n_repos=3, last_n_commits=5) for repo in results: print(f"\n## {repo.name} ({repo.stars:,} ⭐)") print("-" * 60) for commit in repo.commits: print(f"• {commit.message}") print(f" {commit.url}") ``` To conclude, the usage pattern in this example supports building a library of code actions. Each saved code action becomes a tool available for use in future code actions, enabling composition and reuse. ______________________________________________________________________ 1. Executable Code Actions Elicit Better LLM Agents:  [↩](#fnref:1 "Jump back to footnote 1 in the text") # API Reference ## ipybox.CodeExecutor ``` CodeExecutor( tool_server_host: str = "localhost", tool_server_port: int | None = None, kernel_gateway_host: str = "localhost", kernel_gateway_port: int | None = None, kernel_env: dict[str, str] | None = None, working_dir: Path | None = None, images_dir: Path | None = None, approval_timeout: float | None = None, connect_timeout: float = 30, sandbox: bool = False, sandbox_config: Path | None = None, log_level: str = "WARNING", approve_tool_calls: bool = True, approve_shell_cmds: bool = False, require_shell_escape: bool = False, ) ``` Executes Python code in an IPython kernel with programmatic MCP tool call support. `CodeExecutor` launches an embedded KernelGateway for running Python code and an embedded `ToolServer` for executing MCP tools. Code is executed in an IPython kernel, providing a stateful environment where variables and definitions persist across executions. MCP tools can be called from executed code using the API generated by `generate_mcp_sources`. When code calls an MCP tool, the tool server receives the request and emits an approval request. The client must accept or reject the request before the tool executes. Example Generate a Python tool API and execute code that uses it: ``` from pathlib import Path from ipybox import ApprovalRequest, CodeExecutionResult, CodeExecutor from ipybox import generate_mcp_sources # Generate a Python tool API for the fetch MCP server server_params = {"command": "uvx", "args": ["mcp-server-fetch"]} await generate_mcp_sources("fetch", server_params, Path("mcptools")) # Execute code that calls the generated API code = """ from mcptools.fetch import fetch result = fetch.run(fetch.Params(url="https://example.com")) print(result) """ async with CodeExecutor() as executor: async for item in executor.stream(code): match item: case ApprovalRequest(): print(f"Tool call: {item}") await item.accept() case CodeExecutionResult(): print(item.text) ``` Configure a code executor with optional sandboxing. Parameters: | Name | Type | Description | Default | | ---------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tool_server_host` | `str` | Hostname for the ToolServer. | `'localhost'` | | `tool_server_port` | \`int | None\` | Port for the tool server. If None, a free port is selected automatically. | | `kernel_gateway_host` | `str` | Hostname for the KernelGateway. | `'localhost'` | | `kernel_gateway_port` | \`int | None\` | Port for the kernel gateway. If None, a free port is selected automatically. | | `kernel_env` | \`dict[str, str] | None\` | Environment variables to set for the IPython kernel. Kernels do not inherit environment variables from the parent process. | | `working_dir` | \`Path | None\` | Working directory for the kernel gateway and IPython kernel. If set, ipybox restores the kernel to this directory after each execution. If None, cwd changes persist until code changes them again or the kernel is reset. | | `images_dir` | \`Path | None\` | Directory for saving images generated during code execution. Defaults to images in the current directory. | | `approval_timeout` | \`float | None\` | Timeout in seconds for approval requests. If an approval request is not accepted or rejected within this time, the tool call fails. If None, no timeout is applied. | | `connect_timeout` | `float` | Timeout in seconds for starting MCP servers. | `30` | | `sandbox` | `bool` | Whether to run the kernel gateway inside Anthropic's sandbox-runtime. When enabled, IPython kernels run in a secure sandbox with no network access except to the local tool server. | `False` | | `sandbox_config` | \`Path | None\` | Path to a JSON file with sandbox configuration. See the Configuration section of the sandbox-runtime README for available options. | | `log_level` | `str` | Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). | `'WARNING'` | | `approve_tool_calls` | `bool` | Whether MCP tool calls require approval before execution. When True, each tool call yields an ApprovalRequest that must be accepted or rejected. | `True` | | `approve_shell_cmds` | `bool` | Whether to require approval for ! shell commands and %%bash/%%sh cell magics. When enabled, each shell command triggers an ApprovalRequest before execution. | `False` | | `require_shell_escape` | `bool` | Whether to block direct process-creation calls (subprocess, os.system, os.exec\*, os.spawn\*, os.posix_spawn\*, pty.spawn), forcing shell commands through the ! handler. Requires approve_shell_cmds=True. | `False` | ### cancel ``` cancel() ``` Cancel the currently running execution. Signals the active stream or execute call to interrupt the kernel and return cleanly without raising an exception. Safe to call from any coroutine in the same event loop. Has no effect if no execution is in progress. ### execute ``` execute( code: str, timeout: float | None = None ) -> CodeExecutionResult ``` Execute Python code with automatic approval of all MCP tool calls. Convenience method that executes code, auto-approves any MCP tool calls, and returns the final result directly. Parameters: | Name | Type | Description | Default | | --------- | ------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `code` | `str` | Python code to execute. | *required* | | `timeout` | \`float | None\` | Maximum time in seconds to wait for execution to complete. If None, no timeout is applied. Approval wait time is excluded from this timeout budget. | Returns: | Type | Description | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `CodeExecutionResult` | The execution result containing output text and generated images. If the execution was cancelled, returns a result with text=None and empty images. | Raises: | Type | Description | | -------------------- | --------------------------------------------------------------------- | | `CodeExecutionError` | If code execution raises an error. | | `TimeoutError` | If code execution exceeds the timeout (excluding approval wait time). | ### reset ``` reset() ``` Reset execution state. Restarts the IPython kernel and stops all started MCP servers. Kernel state (variables, definitions, imports) is lost. MCP servers are lazily restarted on their next tool call. ### start ``` start() ``` Start the executor. Starts the tool server, kernel gateway, and connects to the IPython kernel. ### stop ``` stop() ``` Stop the executor. Stops the tool server, kernel gateway, and disconnects from the IPython kernel. ### stream ``` stream( code: str, timeout: float | None = None, chunks: bool = False, ) -> AsyncIterator[ ApprovalRequest | CodeExecutionChunk | CodeExecutionResult ] ``` Execute Python code in the IPython kernel with MCP tool call approval. Code can call MCP tools using the API generated by `generate_mcp_sources`. Each tool call yields an `ApprovalRequest`. If accepted, the tool executes on the `ToolServer` and returns the result to the kernel. If rejected, the tool call fails with an error. Use cancel to stop execution and return cleanly from a concurrent coroutine. Parameters: | Name | Type | Description | Default | | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `code` | `str` | Python code to execute. | *required* | | `timeout` | \`float | None\` | Maximum time in seconds to wait for execution to complete. If None, no timeout is applied. Approval wait time is excluded from this timeout budget. | | `chunks` | `bool` | Whether to yield CodeExecutionChunk objects during execution. When False, only ApprovalRequest and CodeExecutionResult are yielded. | `False` | Yields: | Type | Description | | -------------------------------- | ------------------ | | \`AsyncIterator\[ApprovalRequest | CodeExecutionChunk | | \`AsyncIterator\[ApprovalRequest | CodeExecutionChunk | | \`AsyncIterator\[ApprovalRequest | CodeExecutionChunk | Raises: | Type | Description | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CodeExecutionError` | If code execution raises an error (syntax errors, runtime errors, rejected or timed-out approval requests, MCP tool errors). The error message contains the stack trace from the kernel. | | `TimeoutError` | If code execution exceeds the timeout (excluding approval wait time). | ## ipybox.CodeExecutionChunk ``` CodeExecutionChunk(text: str) ``` A chunk of output text generated during streaming code execution. Only yielded by CodeExecutor.stream when `chunks=True`. Attributes: | Name | Type | Description | | ------ | ----- | ----------------------- | | `text` | `str` | A chunk of output text. | ## ipybox.CodeExecutionResult ``` CodeExecutionResult(text: str | None, images: list[Path]) ``` The result of a successful code execution. Attributes: | Name | Type | Description | | -------- | ------------ | ------------------------------------------- | | `text` | \`str | None\` | | `images` | `list[Path]` | Paths to images generated during execution. | ## ipybox.CodeExecutionError Bases: `Exception` Raised when code execution in an IPython kernel fails. ## ipybox.kernel_mgr.server.KernelGateway ``` KernelGateway( host: str = "localhost", port: int = 8888, working_dir: Path | None = None, sandbox: bool = False, sandbox_config: Path | None = None, log_level: str = "INFO", log_to_stderr: bool = False, env: dict[str, str] | None = None, ) ``` Manages a Jupyter Kernel Gateway process. The kernel gateway provides a REST and WebSocket API for creating and communicating with IPython kernels. Use KernelClient to create and connect to an IPython kernel and execute code. When sandboxing is enabled, the gateway runs inside Anthropic's [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime), providing secure isolation for code execution. Example ``` async with KernelGateway(host="localhost", port=8888) as gateway: # Gateway is running, connect with KernelClient await gateway.join() # Wait until gateway stops ``` Parameters: | Name | Type | Description | Default | | ---------------- | ---------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `host` | `str` | Hostname or IP address to bind the gateway to. | `'localhost'` | | `port` | `int` | Port number the gateway listens on. | `8888` | | `working_dir` | \`Path | None\` | Working directory for the kernel gateway process. If None, inherit the current process working directory. | | `sandbox` | `bool` | Whether to run the gateway inside the sandbox-runtime. | `False` | | `sandbox_config` | \`Path | None\` | Path to a JSON file with sandbox configuration. See the Configuration section of the sandbox-runtime README for available options. | | `log_level` | `str` | Logging level for the gateway process. | `'INFO'` | | `log_to_stderr` | `bool` | Whether to redirect gateway logs to stderr. | `False` | | `env` | \`dict[str, str] | None\` | Environment variables to set for kernels created by the gateway. Kernels do not inherit environment variables from the parent process, so any required variables must be explicitly provided. | ### join ``` join() ``` Waits for the kernel gateway process to exit. ### start ``` start() ``` Starts the kernel gateway process. Raises: | Type | Description | | -------------- | ---------------------------------- | | `RuntimeError` | If the gateway is already running. | ### stop ``` stop(timeout: float = 10) ``` Stops the kernel gateway process. Terminates the gateway and all child processes. If the process doesn't stop within the timeout, it is forcefully killed. Parameters: | Name | Type | Description | Default | | --------- | ------- | --------------------------------------------------------- | ------- | | `timeout` | `float` | Maximum time in seconds to wait for graceful termination. | `10` | ## ipybox.kernel_mgr.client.KernelClient ``` KernelClient( host: str = "localhost", port: int = 8888, working_dir: Path | None = None, images_dir: Path | None = None, ping_interval: float = 10, approve_shell_cmds: bool = False, require_shell_escape: bool = False, tool_server_host: str = "localhost", tool_server_port: int = 0, ) ``` Client for executing code in an IPython kernel. Connects to a KernelGateway to create and communicate with an IPython kernel. Code execution is stateful: definitions and variables from previous executions are available to subsequent executions. Example ``` async with KernelClient(host="localhost", port=8888) as client: # Simple execution result = await client.execute("print('hello')") print(result.text) # Streaming execution async for item in client.stream("print('hello')"): match item: case str(): print(f"Chunk: {item}") case ExecutionResult(): print(f"Result: {item}") ``` Parameters: | Name | Type | Description | Default | | ---------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `host` | `str` | Hostname or IP address of the kernel gateway. | `'localhost'` | | `port` | `int` | Port number of the kernel gateway. | `8888` | | `working_dir` | \`Path | None\` | Working directory to set in the IPython kernel and restore after each cell execution. If None, ipybox leaves the kernel working directory unchanged between executions. | | `images_dir` | \`Path | None\` | Directory for saving images generated during code execution. Defaults to images in the current directory. | | `ping_interval` | `float` | Interval in seconds for WebSocket pings that keep the connection to the IPython kernel alive. | `10` | | `approve_shell_cmds` | `bool` | Whether to require approval for ! shell commands and %%bash/%%sh cell magics. When enabled, each shell command triggers an approval request before execution. | `False` | | `require_shell_escape` | `bool` | Whether to block direct process-creation calls (subprocess, os.system, os.exec\*, os.spawn\*, os.posix_spawn\*, pty.spawn), forcing shell commands through the ! handler or %%bash/%%sh cell magics. Requires approve_shell_cmds=True. | `False` | | `tool_server_host` | `str` | Hostname of the tool server (used when approve_shell_cmds is True). | `'localhost'` | | `tool_server_port` | `int` | Port of the tool server (used when approve_shell_cmds is True). | `0` | ### kernel_id ``` kernel_id ``` The ID of the running IPython kernel. Raises: | Type | Description | | -------------- | ----------------------------- | | `RuntimeError` | If not connected to a kernel. | ### connect ``` connect(retries: int = 10, retry_interval: float = 1.0) ``` Creates an IPython kernel and connects to it. Parameters: | Name | Type | Description | Default | | ---------------- | ------- | -------------------------------------------- | ------- | | `retries` | `int` | Number of connection retries. | `10` | | `retry_interval` | `float` | Delay between connection retries in seconds. | `1.0` | Raises: | Type | Description | | -------------- | ------------------------------------------------------ | | `RuntimeError` | If connection cannot be established after all retries. | ### disconnect ``` disconnect() ``` Disconnects from and deletes the running IPython kernel. ### drain ``` drain(timeout: float = 1.0) -> int ``` Drain pending kernel messages for up to `timeout` seconds. Returns: | Type | Description | | ----- | ------------------------------- | | `int` | The number of messages drained. | ### execute ``` execute( code: str, timeout: float | None = None ) -> ExecutionResult ``` Executes code in this client's IPython kernel and returns the result. Waits for execution to complete and returns the final result. Use stream for incremental output. Parameters: | Name | Type | Description | Default | | --------- | ------- | ----------------------- | ----------------------------------------------------------------------------------------- | | `code` | `str` | Python code to execute. | *required* | | `timeout` | \`float | None\` | Maximum time in seconds to wait for the execution result. If None, no timeout is applied. | Returns: | Type | Description | | ----------------- | ----------------------------------------------------------------- | | `ExecutionResult` | The execution result containing output text and generated images. | Raises: | Type | Description | | ---------------- | ----------------------------------------------- | | `ExecutionError` | If code execution raises an error. | | `TimeoutError` | If code execution duration exceeds the timeout. | ### interrupt ``` interrupt() ``` Interrupt the running IPython kernel. ### interrupt_and_drain ``` interrupt_and_drain( drain_timeout: float = 1.0, settle_delay: float = 0.2 ) ``` Interrupt the kernel and drain pending messages. ### reset ``` reset() ``` Resets the IPython kernel to a clean state. Deletes the running kernel and creates a new one. ### stream ``` stream( code: str, timeout: float | None = None ) -> AsyncIterator[str | ExecutionResult] ``` Executes code in this client's IPython kernel. Yields output chunks as strings during execution, and yields the final ExecutionResult as the last item. Parameters: | Name | Type | Description | Default | | --------- | ------- | ----------------------- | ----------------------------------------------------------------------------------------- | | `code` | `str` | Python code to execute. | *required* | | `timeout` | \`float | None\` | Maximum time in seconds to wait for the execution result. If None, no timeout is applied. | Yields: | Name | Type | Description | | ----------------- | -------------------- | ------------------- | | `str` | \`AsyncIterator\[str | ExecutionResult\]\` | | `ExecutionResult` | \`AsyncIterator\[str | ExecutionResult\]\` | Raises: | Type | Description | | ---------------- | ----------------------------------------------- | | `ExecutionError` | If code execution raises an error. | | `TimeoutError` | If code execution duration exceeds the timeout. | ## ipybox.kernel_mgr.client.ExecutionResult ``` ExecutionResult(text: str | None, images: list[Path]) ``` The result of a successful code execution. Attributes: | Name | Type | Description | | -------- | ------------ | --------------------------------------------------- | | `text` | \`str | None\` | | `images` | `list[Path]` | List of paths to images generated during execution. | ## ipybox.kernel_mgr.client.ExecutionError Bases: `Exception` Raised when code executed in an IPython kernel raises an error.