Stateful Python REPL MCP Server
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.
Features
- Persistent State: variables, imports, and functions survive across calls (~0.1s warm calls vs ~3s per fresh
python3 spawn)
- Real Timeouts: runaway code (sync or async) is interrupted at
timeout seconds — 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 — no asyncio.run() wrapper
- Shell Composition: pre-injected
sh() helper — json.loads(sh("gh pr view 1 --json title")) replaces cmd | python3 -c pipelines
- 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 in mcp.failed / mcp.help()
- Claude Code Plugin: one install bundles the server, a usage skill, and a Bash-nudge hook
Installation
Claude Code (plugin — recommended)
# In Claude Code:
/plugin marketplace add iota-uz/repl-mcp
/plugin install python-repl@repl-mcp
Restart the session and all three components are active. Portable across machines — nothing is hand-edited in ~/.claude.json.
Migrating from a claude mcp add install? 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 Code (MCP server only)
claude mcp add python-repl -- uvx --from git+https://github.com/iota-uz/[email protected] repl-mcp
Pin to a tag (as above) so uvx caches the build instead of fetching GitHub on every session start.
Codex CLI
codex mcp add python-repl -- uvx --from git+https://github.com/iota-uz/[email protected] repl-mcp
Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"python-repl": {
"command": "uvx",
"args": ["--from", "git+https://github.com/iota-uz/[email protected]", "repl-mcp"]
}
}
}
Manual (development)
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)
Usage
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
mcp bridge sees only the project's .mcp.json servers. Host-level connectors (claude.ai Notion/GitHub, user-scope claude mcp add servers) are not reachable — call those tools directly.
mcp.call arguments must be JSON-serializable (they cross the kernel process boundary).
- Output truncates at 50KB (stdout) / 20KB (return values) — aggregate in-REPL.
reset=True clears variables but keeps sh/mcp.