A Model Context Protocol (MCP) server giving AI agents a persistent Python REPL with honest execution semantics. Code runs in a subprocess kernel (Jupyter-style): variables survive across calls, runaway code is interruptible without losing state, and crashes never take the server down. Other MCP servers from your project's .mcp.json are callable in-code via a pre-injected mcp bridge.
- Persistent State: variables, imports, and functions survive across calls (~0.1s warm calls vs ~3s per fresh
python3spawn) - Real Timeouts: runaway code (sync or async) is interrupted at
timeoutseconds — KeyboardInterrupt, namespace state preserved. Cells that swallow the interrupt are killed and the kernel respawns with an explicit "variables cleared" notice - Crash Isolation: a segfault/OOM in REPL code kills only the kernel child; the server respawns it instantly
- Top-level
await:await client.get(url)directly — noasyncio.run()wrapper - Shell Composition: pre-injected
sh()helper —json.loads(sh("gh pr view 1 --json title"))replacescmd | python3 -cpipelines - Full Filesystem Access:
open(), absolute paths, and~all work; cwd is your project - MCP Bridge:
mcp.call("server", "tool", **args)reaches the servers in your project's.mcp.json— connected lazily on first use, with failures visible inmcp.failed/mcp.help() - Claude Code Plugin: one install bundles the server, a usage skill, and a Bash-nudge hook
# In Claude Code:
/plugin marketplace add iota-uz/repl-mcp
/plugin install python-repl@repl-mcpRestart the session and all three components are active. Portable across machines — nothing is hand-edited in ~/.claude.json.
Migrating from a
claude mcp addinstall? Remove the old entry first:claude mcp remove python-repl -s user. Keeping both registers two REPL server processes with duplicate tools and can skew versions between them.
What the plugin bundles:
| Component | What it does |
|---|---|
| MCP server | execute_python tool, launched via uvx pinned to the release tag (cached after first run; the REPL's working directory is your project, not the plugin cache) |
Skill (python-repl) |
Teaches Claude when to reach for the REPL (instead of python3 -c / heredocs via Bash) and its gotchas — truncation limits, lazy mcp bridge, package installs |
| Nudge hook (PostToolUse) | When Claude runs inline Python through Bash (python3 -c, python3 - <<EOF, cmd | python3), injects a non-blocking reminder to use execute_python. Silent on python3 script.py, python3 -m ..., pytest |
To update later: /plugin marketplace update repl-mcp then /plugin update python-repl@repl-mcp.
claude mcp add python-repl -- uvx --from git+https://github.com/iota-uz/repl-mcp@v2.0.0 repl-mcpPin to a tag (as above) so uvx caches the build instead of fetching GitHub on every session start.
codex mcp add python-repl -- uvx --from git+https://github.com/iota-uz/repl-mcp@v2.0.0 repl-mcpAdd to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"python-repl": {
"command": "uvx",
"args": ["--from", "git+https://github.com/iota-uz/repl-mcp@v2.0.0", "repl-mcp"]
}
}
}git clone https://github.com/iota-uz/repl-mcp && cd repl-mcp
uv sync --extra dev
uv run repl-mcp # stdio transport (the only transport)One tool: execute_python(code, reset=False, timeout=120).
# State persists across calls
execute_python(code="import httpx; data = (await httpx.AsyncClient().get(url)).json()")
execute_python(code="len(data['items'])") # → 42
# Shell composition
execute_python(code="prs = json.loads(sh('gh pr list --json number,title'))")
# MCP bridge (lazy-connects to your project's .mcp.json on first use)
execute_python(code="print(mcp.help())")
execute_python(code="mcp.call('github', 'create_issue', owner='me', repo='proj', title='Bug')")
# Runaway code? Interrupted at timeout, state survives:
execute_python(code="while True: pass", timeout=5)
# → KeyboardInterrupt: execution interrupted. Namespace state ... preserved.
# Missing package? Install into the running env:
execute_python(code="sh('uv pip install openpyxl')")Notes:
- The
mcpbridge sees only the project's.mcp.jsonservers. Host-level connectors (claude.ai Notion/GitHub, user-scopeclaude mcp addservers) are not reachable — call those tools directly. mcp.callarguments must be JSON-serializable (they cross the kernel process boundary).- Output truncates at 50KB (stdout) / 20KB (return values) — aggregate in-REPL.
reset=Trueclears variables but keepssh/mcp.
MCP client ── stdio ──► PARENT (FastMCP, pure async) CHILD (owns namespace)
execute_python ── EXECUTE ──────► exec / await cell
◄──── RESULT ────── captured output
timeout: SIGINT ────────────────► KeyboardInterrupt
crash: respawn + clear notice (state survives)
MCP sessions (lazy) ◄─ MCP_CALL ─ in-code mcp.* proxy
The server's event loop never blocks on REPL code; in-cell mcp.* calls are serviced on an independent channel while the cell runs. See CLAUDE.md for the full development guide.
- Removed (zero observed usage across real agent transcripts):
workspace/git/ast_utils/codepre-injected utilities (useopen()/pathlib/sh('git …')),%magiccommands andobject?queries, theinjectparameter,mcp.tools.<server>.<tool>dot-style access anddiscover_tools()(usemcp.call/mcp.list_tools), SSE transport (stdio only) - Changed: execution moved to a subprocess kernel —
timeoutis now actually enforced; kernel restarts are reported explicitly - Added: top-level
await,mcp.failed, lazy MCP connect - Install footprint dropped ~350MB (tree-sitter removed)
uv run pytest tests/ -v # full suiteSee CLAUDE.md for architecture details, test map, gotchas, and the release process.
MIT