A Model Context Protocol (MCP) server that gives an LLM a way to interact with a Python code interpreter. Built on the FastMCP framework, this lightweight, self-contained server enables LLMs (like Claude) to execute Python code in a conversational manner, supporting branching execution paths and package installation on demand without relying on the full Jupyter stack.
- Conversational Code Execution: Support for branching execution paths that match the non-linear nature of chat conversations
- Isolated Execution: Run Python code in isolated sessions with state preservation
- Graph-Based Code Model: Code blocks are organized in a directed acyclic graph (DAG) rather than linear cells
- Package Installation: Install Python packages on demand within isolated environments
- Rich Output Support: Capture and display standard output, errors, and rich content (like plots)
- Efficient Execution: Automatic detection of which code blocks need re-execution
- Session Management: Create and reset sessions with unique identifiers
- MCP Integration: Fully compatible with the Model Context Protocol framework
- Python 3.13+
uvpackage manager (required for dependency installation)fastmcpCLI (installed automatically with dependencies)
The following MCP tools are provided:
create_repl(): Create a new REPL session with a dedicated isolated environment. Pre-installs base packages (numpy, pandas, matplotlib, scipy, scikit-learn, seaborn, plotly) and returns a session ID.run_code(session_id, code, parent_cell_id): Execute code with optional branching from a previous code block. Returns execution results (stdout, stderr, display data, images) and the new cell ID.install_packages(session_id, packages): Install Python packages in the session environment usinguvpackage manager.reset_repl(session_id): Reset a session, terminating the helper process and clearing the graph state.
Important Note: To display matplotlib figures, code must call display_figure(fig) rather than plt.show() or plt.savefig(). The display_figure() function is automatically available in each session's namespace.
The system is built on FastMCP and designed around these core components:
- MCP Server (
server.py): Defines MCP tools using FastMCP decorators and manages user sessions with progress updates for long-running operations - Session Manager (
session_manager.py): Manages multiple active sessions, handles creation, retrieval, resurrection from disk, reset, and destruction - Session (
session.py): Handles individual session lifecycle, code execution coordination, helper process management, and ZeroMQ communication - Graph Manager (
graph_manager.py): Manages code blocks as a directed acyclic graph with persistence and hash-based state tracking for efficient re-execution - Helper Process (
helper.py): A dedicated subprocess for each active session that runs within an isolated virtual environment, executing code via IPython and communicating via ZeroMQ IPC sockets - Protocol (
protocol.py): Defines the Pydantic-based communication protocol for server-helper communication - Shell Config (
shell.py): IPython shell configuration with custom PlotCapturingShell that captures rich display outputs
Each session maintains:
- A dedicated directory with isolated virtual environment (managed by
uv) - A graph structure of related code blocks that supports branching execution paths
- Runtime state between code executions (variables, imports, etc.) preserved across executions
- A hash-based verification system to detect state changes and optimize execution (only re-executes full paths when code has changed)
- ZeroMQ IPC sockets for inter-process communication between server and helper processes
- Clone the repository
- Install dependencies:
uv install- Run the server:
Using FastMCP CLI (recommended):
# Development mode with MCP Inspector
make dev
# or
uv run fastmcp dev src/mcp_pyrepl/server.py
# Production mode (stdio transport)
make run
# or
uv run fastmcp run src/mcp_pyrepl/server.py
# HTTP transport
make run-http
# or
uv run python -m mcp_pyrepl.server --transport http --port 8484Using the installed entrypoint:
uv run mcp-pyrepl- Run tests:
make test
# or
uv run pytest- Clean build artifacts:
make cleansrc/mcp_pyrepl/
├── server.py # Main MCP server implementation
├── session.py # Session management and execution
├── session_manager.py # Manages multiple sessions
├── graph_manager.py # Manages code block graph and persistence
├── helper.py # Helper process for code execution
├── protocol.py # Communication protocol definitions
└── shell.py # IPython shell configuration
# Create a new session (base packages are pre-installed)
session_id = await create_repl()
# Install additional packages if needed
await install_packages(session_id, ["requests"])
# Run initial code (no parent)
result_1 = await run_code(
session_id=session_id,
code="import numpy as np\nimport matplotlib.pyplot as plt",
parent_cell_id=None
)
cell_1 = result_1[-1].text # Extract cell ID from response
# Run code that builds on the previous code
result_2 = await run_code(
session_id=session_id,
code="""x = np.linspace(0, 10, 100)
y = np.sin(x)""",
parent_cell_id=cell_1
)
cell_2 = result_2[-1].text # Extract cell ID from response
# Run a plotting cell that depends on the previous code
# Note: Use display_figure() instead of plt.show()
result_3 = await run_code(
session_id=session_id,
code="""fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_title('Sine Wave')
display_figure(fig)""",
parent_cell_id=cell_2
)# Create a branch from cell_2 with different analysis
result_3a = await run_code(
session_id=session_id,
code="""# Calculate some statistics
mean = np.mean(y)
std = np.std(y)
print(f"Mean: {mean}, Std Dev: {std}")""",
parent_cell_id=cell_2
)
cell_3a = result_3a[-1].text # Extract cell ID from response
# Create another branch with a different visualization
result_3b = await run_code(
session_id=session_id,
code="""# Create a different plot
fig, ax = plt.subplots()
ax.scatter(x, y)
ax.set_title('Sine Wave Scatter')
display_figure(fig)""",
parent_cell_id=cell_2
)
cell_3b = result_3b[-1].text # Extract cell ID from response# Reset a session, clearing all state and terminating the helper process
await reset_repl(session_id)
# The helper process will be recreated automatically on next code executionMIT
Contributions are welcome! Please feel free to submit a Pull Request.