IPi
IPi gives MCP agents persistent IPython kernels, Jupyter notebooks, and durable local output artifacts. It is designed for Codex and Claude Code.
IPi is not an LLM harness. It has no provider client, agent turn loop, terminal UI, prompt manager, or context-window manager. The external harness owns the conversation; IPi owns notebook execution. IPi installs no host hooks.
The PyPI distribution and its command are ipinb; the Python import is ipi. MCP dependencies are included by default.
Run
IPi supports Linux and macOS and requires Python 3.11 or newer plus uv. Run
the published package without installing it permanently:
uvx ipinb
uvx ipinb with no arguments starts a stdio MCP server. Add it to the host's MCP configuration yourself, for example in Codex's .codex/config.toml:
[mcp_servers.ipi]
command = "uvx"
args = ["ipinb@0.2.0"]
or in Claude Code's .mcp.json:
{"mcpServers": {"ipi": {"command": "uvx", "args": ["ipinb@0.2.0"]}}}
Pin an exact version so that every process of one host session runs the same IPi. Do not use uvx ipi: the PyPI name ipi belongs to an unrelated package.
Set IPI_INSTALL_REQUIREMENT to an authenticated requirement when child
kernels must reconstruct a private Git-installed IPi host. The value is used
only at launch and is removed from published runtime state.
Tools And Storage
The same core MCP catalog serves stdio and HTTP:
create_kernel,list_kernels, andclose_kernelmanage explicit kernel resources.ipython(kernel_id, code)accepts Python and observes it for a bounded interval. Calls to one kernel may overlap and share globals.yield_time_ms=0always returns an accepted cell handle.ipdbprovides optional exception inspection and real breakpoint/stepping control. Ordinary failures finish normally and include one inspection hint.read_cellretrieves Python state and output or Markdown source and metadata, optionally waiting for Python output or completion;interruptrequests cancellation of one cell. Each reader owns its output cursor; cancelling observation does not cancel Python.list_cellsandmarkdowninspect and author retained notebook work.publish(kernel_id, cells={cell_id: caption})retains a full live executable Notebook 7 publication and optionally highlights terminal Python cells with captions.
Wait for a predecessor to succeed before submitting dependent code. interrupt is best effort and never escalates to killing a sibling or kernel. close_kernel deliberately discards one kernel's live state while preserving retained records and other kernels. There is no active-kernel selector or agent-facing whole-service shutdown.
For Codex, a kernel created by create_kernel gets CODEX_THREAD_ID from that call's _meta["x-codex-turn-metadata"]["thread_id"], so notebook code and its subprocesses see the same thread as Codex's own shell tools. It is set before bootstrap cells run; kernel plugin callbacks run earlier and do not see it. The prewarm kernel starts without a request and does not get it.
Bash is disabled by default. Set IPI_MCP_ENABLE_BASH_TOOL=1 before startup to expose it; 0 disables it. Optional Bash and plugin tools also require kernel_id. The ipdb tool requires kernel_id on every call. Add cell_id for execution-specific inspection or control; omit it to configure kernel-wide breakpoints and exception stops. See Debugging.
The notebook skill includes locally readable guides for execution and result recovery, kernel environments and SSH, debugging, publishing, and rich output. Agents load the relevant guide when needed. docs/ owns their content; matching copies ship in the skill and package, with packaging checks guarding against drift.
Execution requires an explicit kernel_id. The default observation window is five seconds for ipython and zero for read_cell; both accept yield_time_ms from zero through 30,000. Zero returns an accepted handle for ipython and an immediate snapshot for read_cell. Python read_cell replies contain state and output without source or submission metadata; Markdown replies include their source and metadata. Observation is independent of execution lifetime.
Managed kernels keep IPython history in memory because the notebook and registry are the durable execution record. Kernel creation is explicit, apart from optional discoverable startup prewarming. A closed or lost handle never creates a replacement.
Handles combine a persisted nine-digit decimal registry prefix with k or c and a durable counter, such as 123456789k0 and 123456789c1. Pass them unchanged to their originating registry; IDs are never reused within that registry. Tool responses provide one structured JSON payload with typed resource, handle, cell, inventory, or error envelopes. Output cursors belong to each reader. Large sources and events provide immutable MCP resource URIs and byte counts. Use read_cell(..., include_artifact_metadata=True) for artifact filesystem paths, SHA-256 checksums, and MIME types.
ipython returns its bounded cell record plus any images the cell displayed as MCP
image content, so plt.show() is one step instead of a path the model has to
fetch. [tools] inline_images = "off" disables that; images past
inline_image_max_px or inline_image_max_bytes are skipped rather than
resized, since IPi carries no imaging dependency.
Each notebook runtime gets a new session. Local sessions are stored under:
<initial-cwd>/.agents/sessions/<session-id>/
session.jsonl
kernels/123456789k1/notebook.ipynb
kernels/123456789k1/outputs/manifest.json
kernels/123456789k1/outputs/c1/g-<sha256-prefix>/input.py
kernels/123456789k1/outputs/c1/g-<sha256-prefix>/o1-stdout.txt
Every input and every retained Jupyter MIME output has a durable local
destination. Output available for the first MCP result is persisted before IPi
builds that bounded response. Returned and recorded paths identify immutable
response-time generations. Browser, yielded, and late output publishes a new
generation as it arrives; manifest.json atomically points to the latest
reduced state, including Jupyter clear and display updates. Unfinished agent or
browser cells are recorded as aborted during shutdown. Kernels and Python state
live until the MCP process exits; IPi does not resume them after restart.
Execution and plugins are trusted local Python, not a sandbox.
Generation directories normally use eight hexadecimal prefix characters and
extend on collision. Their .sha256 file and the manifest retain the complete
digest.
Importing Python Scripts From Paths
Kernels can import toolfuncs, an IPi dependency. toolfuncs.load(path) runs one Python script in the kernel without its __main__ block and returns a namespace of the functions, classes, and other callables that the script defines:
import toolfuncs
tool = toolfuncs.load("/absolute/project/scripts/report.py")
tool.summarize("results.csv")
When the script has PEP 723 metadata, load first installs the dependencies whose distributions are absent from the kernel with uv pip install --python sys.executable. It does not replace a distribution that is already installed; when the installed version does not satisfy the script, load warns and keeps it. This changes the disposable environment of the running kernel, like %pip, and needs uv on the kernel's PATH (on an SSH target too). Loading runs the source inside the kernel process, so review untrusted source and metadata first.
load is not a module import: it runs the script again on each call, returns only the callables, does not register a module in sys.modules, and importlib.reload does not apply. Functions that must be pickled, for example for a process pool, belong in an importable module.
Publishing a Notebook
Kernels always write an archival notebook.ipynb. Publish a specific live
notebook only when someone needs to open it:
publish(kernel_id=kernel_id, cells={cell_id: "Result"})
The tool starts one private Notebook 7 child attached to the existing IPi kernel, starts one Cloudflare quick tunnel, and returns the complete URL with its access token. The recipient can view and execute the notebook against the same live Python state. Browser interrupts are forwarded; browser launch, restart, and shutdown actions cannot create or destroy IPi-owned kernels.
Publication is lazy, explicit, and per kernel. There is no global daemon,
dashboard, rendezvous file, registration table, or [jupyter] configuration.
An unpublished kernel owns no Jupyter or tunnel process. A published kernel is
exempt from ordinary idle expiry until the kernel is
killed or its owning IPi process exits; teardown closes the Jupyter child and
tunnel and invalidates the URL. Repeating publish returns the same
URL while it remains live and repairs a failed child or tunnel otherwise.
The Notebook 7 stack is a lazy publish extra, not part of normal MCP-host startup. The first publication builds one process-cached wheel from the exact running IPi code and starts it in an isolated uv run environment. Set UV_CACHE_DIR to node-local storage when the home directory is remote. Jupyter configuration, runtime data, IPython state, logs, and collaboration data always live in a private operating-system temporary directory and are removed with the publication.
Quick tunnels require an installed cloudflared. Publication fails without
returning a local fallback if Jupyter or the public tunnel cannot become ready.
Treat the returned URL as a bearer credential and share it only with intended
recipients. See Notebook publishing for the
lifecycle and security contract.
HTTP
Stdio is the primary transport. FastMCP HTTP is also available:
uvx ipinb --transport http --host 127.0.0.1 --port 8000
The process registry owns kernels and cells independently of HTTP transport sessions. Reconnecting clients use the same explicit kernel_id and cell_id; resource IDs are not authentication credentials. Both transports use the same schemas, observation windows, and lifecycle. No server.session_mode setting is needed.
server.running_capacity defaults to eight execution lanes per kernel, server.queue_capacity to 64 waiting submissions, and server.kernel_capacity to eight live or starting kernels. Saturation rejects a new submission before acceptance. The registry retains terminal records and retry keys indefinitely by default. Set server.registry_root to retain and reopen the same on-disk registry across server restarts; live Python state is reported lost, never reconstructed implicitly. Only one process may own that directory at a time.
Unpublished, inactive kernels expire after server.kernel_idle_timeout_s (one hour by default; zero disables it). Published kernels are retained until explicit close or process exit. The repeatable -c/--config key=value option overrides strict configuration fields using dotted keys and TOML values.
Set IPI_MCP_HTTP_AUTH_TOKEN to require the token as either a bearer token or
the token query parameter. Authentication does not define runtime ownership.
Pass --connection-file PATH when another local process needs a machine-readable
endpoint. IPi removes a stale file before startup, waits until the HTTP origin
is accepting requests, and atomically publishes a private 0600 JSON manifest:
{
"headers": {"Authorization": "Bearer <token>"},
"transport": "http",
"url": "http://127.0.0.1:8000/mcp?token=%3Ctoken%3E",
"version": 1
}
The URL includes the query token only when query authentication is enabled, and
the header appears only when bearer authentication is enabled. Without a token,
headers is empty and the URL has no token. The last successfully published
manifest remains after shutdown as a diagnostic record; the next launch removes
it before starting. Treat the file as a secret whenever authentication is
enabled.
An installed cloudflared can expose the MCP endpoint directly. Public tunnels
require IPI_MCP_HTTP_AUTH_TOKEN:
IPI_MCP_HTTP_AUTH_TOKEN='<token>' \
uvx ipinb --transport http --host 127.0.0.1 --port 0 --tunnel
Tunnels are accountless Cloudflare quick tunnels, so the public
*.trycloudflare.com URL changes every time the tunnel starts. IPi
prints the public endpoint to stderr after the local origin and tunnel are both
ready; tunnel setup failure stops the public server instead of silently falling
back to a local-only URL. Combining --tunnel with --connection-file delays
manifest publication until the public endpoint is ready and records that public
URL.
SSH Kernels
create_kernel(cwd=...) accepts either a local absolute path or a per-kernel
SSH URI:
create_kernel(cwd="/Users/me/local-project")
create_kernel(cwd="ssh://gpu-box/opt/projects/model")
create_kernel(cwd="ssh://alice@gpu-box:2222/opt/projects/model")
The URI path is the absolute working directory on the target. Host aliases,
keys, agents, ports, ProxyJump, and host-key policy come from normal OpenSSH
configuration. Authentication is non-interactive: IPi never accepts a password
in the URI, prompts for credentials or host trust, weakens host-key checking, or
forwards the SSH agent. Establish host trust and verify ssh -T <alias> works
before creating the kernel.
Each remote kernel owns a private OpenSSH master, five target-loopback Jupyter
forwards, and a mode-0700 target session under /tmp. IPi resolves the latest
official portable uv release at launch, verifies its publisher checksum on
the host and target, stages the exact running IPi build, and removes the staged
runtime at shutdown. The target project does not need a host mirror and IPi
does not install target system software. The target must provide python3
3.10 or newer for the isolated, standard-library-only session owner that guards
the workspace until the exact staged supervisor takes ownership. Normal uv Python and dependency
resolution still needs network or pre-populated artifacts; failures retain the
bounded uv diagnostic.
Remote kernels run built-in plugins only. Project, user, and installed external
plugins are parsed as data but are not imported, executed, or staged for that
kernel. Creation emits one The following plugins have been ignored warning
with every ignored plugin and a redacted source. A single server can mix local
kernels with kernels on multiple SSH targets; listing, switching, recreation,
idle reaping, notebook attachment, PDB, images, and
cleanup retain each kernel's immutable location.
Windows, WSL path mapping, the ipi-windows/ipi-remote provisioners, and all
process-wide IPI_KERNEL_* settings have been removed. If a removed variable
is present, IPi exits with migration guidance to use an ssh:// cwd. See
Architecture for transport ownership and
Breaking Changes for migration details.
SSH troubleshooting and cleanup
Before launching, inspect the effective alias with ssh -G <alias> and prove
non-interactive authentication with ssh -o BatchMode=yes -T <alias> true.
Resolve an unknown host key or changed trust record through ordinary OpenSSH
administration first; IPi deliberately does not prompt, disable checking, or
edit known_hosts. A configured local agent may authenticate the client, but
IPi does not forward that agent to the target. Existing ProxyJump, port, and
identity settings remain OpenSSH's responsibility.
Resolving the latest portable uv requires host HTTPS access to GitHub. Later
Python and dependency resolution runs on the target and therefore needs its
normal uv cache or target network access. Both failures identify which side
needs access. A lost SSH connection may prevent immediate target verification;
the supervisor's owner lease then kills its kernel and removes its private
/tmp/ipi-session.* directory after roughly 60 seconds. Cleanup diagnostics
name the exact redacted SSH URI and owned paths. Audit only those exact paths
and processes; never delete a broad /tmp pattern. Passwords, Jupyter keys,
tokens, private-key paths, and signed query strings are not included in IPi
diagnostics.
Configuration And Plugins
IPi resolves the uv project or workspace member for each kernel cwd. Configuration merges ~/.ipi/config.toml, the workspace root's .ipi.toml, and the selected member's .ipi.toml, in that order. Outside a uv project, it uses <cwd>/.ipi.toml. Project plugins also come from the selected member. Each kernel retains its requested working directory. Missing config files are optional; invalid or inaccessible config fails.
Use ipinb --plugin DISTRIBUTION to add an already installed plugin for every local kernel location. Repeat it for more distributions. Existing per-location plugin selections and their version or source constraints remain in effect. This is useful for integrations owned by the server session when UV_PROJECT points outside its launch directory. Remote kernels continue to ignore external plugins with a warning.
[server]
kernel_idle_timeout_s = 0 # keep idle kernels alive; set a positive timeout to reap them
prewarm_kernel = true # start the default kernel in the background at startup
[tools]
inline_images = "all"
[kernel]
install_default_dependencies = true
additional_dependencies = []
scratch_dependencies = [] # personal packages only outside a uv project
locked = false # true requires an existing, current uv.lock
[publication]
shared_dependencies = []
scratch_shared_dependencies = [] # personal widget packages outside a uv project
[plugins]
installed = []
disabled = []
Fresh kernels make a small general-purpose module set available: typing_extensions, attrs, packaging, platformdirs, pydantic, requests, dateutil, psutil, tqdm, filelock, more_itertools, yaml, tenacity, and dotenv. The last six come from bare, unversioned overlay requirements for tqdm, filelock, more-itertools, PyYAML, tenacity, and python-dotenv. Set kernel.install_default_dependencies = false to omit those six, or add PEP 508 requirements with kernel.additional_dependencies. A caller's explicit additional_dependencies value wins when it names the same distribution.
Use publication.shared_dependencies for packages that must be installed in both the kernel and the lazily created Notebook 7 environment, such as packages that ship a JupyterLab frontend extension. Shared entries are automatically included in every kernel, so do not duplicate them under kernel.additional_dependencies. The publisher uses the exact package versions and sources selected by its kernel. Ordinary kernel-only packages remain in kernel.additional_dependencies; installing one live with %pip requires no publication change. Put personal scientific and widget defaults in kernel.scratch_dependencies and publication.scratch_shared_dependencies to keep them out of project kernels.
Local and SSH kernels use ephemeral uv run --isolated environments. uv selects the project, workspace membership, Python interpreter, sources, and normal default groups. IPi does not enable all extras or groups. Missing or stale locks are resolved temporarily without writing uv.lock, and IPi does not create or synchronize .venv. Set kernel.locked = true to require an existing current lock. Strict-lock mode checks the base project lock; kernel overrides can still select different package versions.
IPi requires uv 0.12.3 or later. Its runtime and additional dependencies use native uv overlays and may override project package pins. Each kernel has a private writable environment: live %pip changes affect only that kernel. Select the project with cwd and request extra packages with additional_dependencies; explicit venv attachment is not supported. Approved direnv settings apply to preparation and launch; IPi does not approve an .envrc.
When tools.view_image is omitted, the public tool is disabled on stdio and
enabled on HTTP, SSE, and streamable HTTP. Set it explicitly to true or
false to override that transport default. Kernel-side image-output guidance
follows the same resolution: it recommends view_image only when the tool is
actually registered, and otherwise lists absolute paths for the client's own
file/image reading.
IPi's bundled Notebook 7 extension shows the execution intent of each cell and collapses kernel setup and plugin bootstrap cells under one Setup group. Each execution and the Setup group has its own disclosure control, and a reader's manual choice is kept across later metadata updates. publish(kernel_id, cells={cell_id: caption}) opens the target execution once; the reader can collapse it again until a later promotion revision requests another reveal. The published notebook keeps JupyterLab's native contentVisibility windowing and output-trimming behavior instead of forcing every cell or thousands of discrete outputs to render eagerly.
Plugins are ordinary Plugin(server=..., environment=..., kernel=...) values.
They can come from built-ins, selected ipi.extensions entry points,
~/.ipi/extensions/, or <cwd>/.ipi/extensions/. See
Developing External IPi Plugins for the API and
Architecture for ownership and lifecycle details.
Coding agents that create or debug plugins can opt into the
developing-ipi-plugins skill;
it is not auto-read by the MCP server.
Development
uv sync --all-extras --all-groups
uv run ruff check .
uv run ruff format --check .
uv run basedpyright
uv run pytest -m "not kernel and not live_api" -q
uv run pytest -m kernel -q
uv run python benchmarks/model_output_tokens.py
Breaking API changes are recorded in BREAKING_CHANGES.md. Release procedure is documented in docs/releasing.md.
Optional notebook startup in Codex
Configure the notebook MCP server as optional and set a positive mcp_optional_startup_grace_ms in Codex to let the first turn proceed while it starts. Zero disables the grace deadline and waits for startup. Newly ready tools appear at subsequent native catalog boundaries. Ordinary chat remains available if optional notebook startup fails.
Metadata
Release files for ipinb 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ipinb-0.4.0.tar.gz | 733.0 kB | With provenance | Details for ipinb-0.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ipinb-0.4.0-py3-none-any.whl | Python 3 | none | any | Details for ipinb-0.4.0-py3-none-any.whl |
Total release size: 1.1 MB
Release files / ipinb-0.4.0.tar.gz
| Download URL | ipinb-0.4.0.tar.gz |
|---|---|
| Size | 733.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1e58f21160af661c00f60c63f4b8ec37813c91aa3d1485da9eab45496409192c
|
|
BLAKE2b-256 checksum How to use checksums |
da81c63d046eabf70896fc33727d7700739a7f6f98ee38df96617fe85f7df69c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logRelease files / ipinb-0.4.0-py3-none-any.whl
| Download URL | ipinb-0.4.0-py3-none-any.whl |
|---|---|
| Size | 330.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f37932ea99590452bc58ce58b3b62acd5ccb0c8c38b5983bae3a49cd1dd0203c
|
|
BLAKE2b-256 checksum How to use checksums |
fbf511c06503ccf3444f642cf52d9381a2299509d110f2e237c49c94a3aff3a2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logSecurity for ipinb 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for ipinb-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.