# MCP Python REPL Server Implementation Plan
## ⚠️ CRITICAL: READ CLAUDE.md FIRST! ⚠️
**DO NOT START IMPLEMENTATION WITHOUT READING CLAUDE.md**
This file describes WHAT to build. CLAUDE.md describes HOW to build it correctly.
If you haven't read CLAUDE.md yet, STOP and read it now.
## Overview
This plan outlines the implementation of an MCP server providing Python REPL capabilities to Claude Code. The implementation follows a phased approach allowing incremental development and testing.
## Important Implementation Guidelines
- Each file should be self-contained with all necessary imports at the top
- Use type hints throughout for better code clarity
- All MCP tool handlers must be async functions
- Error handling should return structured MCP error responses, not raise exceptions
- The test_project is a complete standalone project - do NOT modify its structure, only interact with it
## Phase 1: Project Setup and Core Structure
### 1.1 Initialize Project
- Use `uv init` to create project structure
- Set up pyproject.toml with:
- Name: `mcp-py3repl`
- Dependencies: `mcp`, `pydantic`, `python-dotenv`, `rich`
- Dev dependencies: `pytest`, `black`, `ruff`
- Python version: `>=3.11` (for tomllib support)
**CRITICAL**: The MCP package installation requires:
```bash
uv add "mcp[server]"
```
This installs the MCP server framework with all necessary protocol support.
### 1.2 Create Directory Structure
```
src/
├── mcp_py3repl/
│ ├── __init__.py
│ ├── server.py # Main MCP server entry point
│ ├── models.py # Pydantic models for requests/responses
│ ├── session_manager.py # REPL session management
│ ├── code_executor.py # Python code execution engine
│ ├── package_manager.py # uv integration for packages
│ ├── file_handler.py # Project file operations
│ ├── env_handler.py # Environment variable management
│ └── dev_tools.py # Formatting, linting, testing
tests/
├── test_mcp_tools.py
└── test_project/ # Complete standalone Python project (see below)
```
### 1.3 Test Project Structure
**IMPORTANT**: The test_project is a complete, standalone Python project with its own pyproject.toml, virtual environment, and all dependencies pre-installed. This project exists solely to test the MCP server's ability to interact with real Python projects.
```
tests/test_project/
├── pyproject.toml # Complete project config with ALL dependencies
├── .env # Test environment variables
├── .env.example # Template showing required env vars
├── alembic.ini # Database migration config
├── alembic/
│ ├── env.py # Configures Alembic to use SQLModel
│ └── versions/ # Migration files (pre-created)
├── app/
│ ├── main.py # FastAPI app with routers mounted
│ ├── config.py # Pydantic Settings with env validation
│ ├── database.py # SQLModel engine and session setup
│ ├── models/
│ │ ├── user.py # User model with email validation
│ │ ├── post.py # Post model with relationships
│ │ └── __init__.py # Exports all models
│ ├── api/
│ │ ├── users.py # CRUD endpoints for users
│ │ ├── posts.py # CRUD endpoints for posts
│ │ └── __init__.py # Router exports
│ └── services/
│ ├── rabbitmq.py # Mock RabbitMQ client
│ └── __init__.py
├── tests/
│ ├── conftest.py # pytest fixtures for test DB/client
│ ├── test_models.py # Model validation tests
│ ├── test_api.py # API endpoint tests
│ ├── test_database.py # Database operation tests
│ └── test_services.py # Service mock tests
└── test.db # SQLite database for testing
```
#### Test Project Dependencies (pre-installed in test_project/pyproject.toml):
```toml
[project]
dependencies = [
"fastapi",
"uvicorn",
"sqlmodel",
"alembic",
"pydantic",
"pydantic-settings",
"python-dotenv",
"httpx", # For TestClient
"pytest",
"pytest-cov",
"black",
"ruff",
"rich",
]
```
## Phase 2: Core MCP Server Implementation
### 2.1 Server Foundation (server.py)
- Create MCP server instance using `@mcp.server("mcp-py3repl")`
- Define all tool handlers as async functions
- Implement proper error handling with detailed error messages
- Set up logging for debugging
Example server structure:
```python
from mcp import MCPServer, Tool
from mcp.types import TextContent, ToolResponse, ErrorData
import logging
logger = logging.getLogger(__name__)
# Create server instance
server = MCPServer("mcp-py3repl")
# Global managers
session_manager = None
package_manager = None
@server.tool()
async def python_execute(code: str, session_id: str | None = None) -> ToolResponse:
"""Execute Python code in REPL session"""
try:
result = await session_manager.execute_code(code, session_id)
return ToolResponse(
content=[TextContent(text=result.output)],
metadata={"execution_time": result.execution_time}
)
except Exception as e:
logger.error(f"Execution error: {e}")
return ToolResponse(
error=ErrorData(code="EXECUTION_ERROR", message=str(e))
)
# Main entry point
async def run():
global session_manager, package_manager
session_manager = SessionManager()
package_manager = PackageManager()
# Create default session
await session_manager.create_session("default")
# Start server
await server.run()
```
### 2.2 Data Models (models.py)
Define Pydantic models for:
- SessionInfo: id, name, working_directory, created_at, active
- ExecutionResult: output, error, return_value, execution_time
- PackageInfo: name, version, dev_dependency
- InspectionResult: type, docstring, attributes, methods
- LintResult: errors, warnings, fixed_code
## Phase 3: Session Management
### 3.1 Session Manager (session_manager.py)
- Create SessionManager class with:
- Dictionary to store active sessions
- Default session creation on startup
- Session switching logic
- Working directory management per session
- sys.path manipulation for project imports
### 3.2 Code Executor (code_executor.py)
- Use `code.InteractiveInterpreter` for REPL functionality
- Implement:
- Safe code execution with timeout
- Output capture (stdout/stderr)
- Exception handling with full traceback
- Variable persistence across executions
- Return value capture and formatting
Key implementation details:
```python
import code
import sys
import io
import contextlib
from typing import Any, Dict
class CodeExecutor:
def __init__(self):
self.interpreter = code.InteractiveInterpreter()
self.globals: Dict[str, Any] = {}
def execute(self, code_str: str) -> ExecutionResult:
# Capture stdout/stderr
stdout_capture = io.StringIO()
stderr_capture = io.StringIO()
with contextlib.redirect_stdout(stdout_capture), \
contextlib.redirect_stderr(stderr_capture):
try:
# Try exec for statements
exec(compile(code_str, '<input>', 'exec'), self.globals)
except SyntaxError:
# Try eval for expressions
try:
result = eval(code_str, self.globals)
if result is not None:
print(repr(result))
except Exception as e:
print(f"Error: {type(e).__name__}: {e}")
return ExecutionResult(
output=stdout_capture.getvalue(),
error=stderr_capture.getvalue(),
# Additional fields...
)
```
## Phase 4: Core Tools Implementation
### 4.1 Python Execution Tools
- `python_execute`: Execute code in active session
- Support multiline code
- Capture output and return values
- Handle imports and variable persistence
- `python_inspect`: Introspect objects
- Use `inspect` module for detailed info
- Format output with Rich for readability
### 4.2 Session Management Tools
- `session_create`: Create new REPL session
- Auto-detect .venv and activate it
- Set working directory
- Load .env if present
- `session_list`: Return all sessions with status
- `session_switch`: Change active session
## Phase 5: Package Management
### 5.1 Package Manager (package_manager.py)
- Implement uv command wrapper using subprocess
- `package_install`:
- Run `uv add` or `uv add --dev`
- Parse output for success/failure
- Update pyproject.toml automatically
- `package_remove`: Use `uv remove`
- `package_list`: Parse `uv pip list` output
Implementation example:
```python
import subprocess
import json
from pathlib import Path
from typing import List, Dict
class PackageManager:
def __init__(self, project_dir: Path):
self.project_dir = project_dir
async def install_packages(self, packages: List[str], dev: bool = False) -> Dict[str, Any]:
cmd = ["uv", "add"]
if dev:
cmd.append("--dev")
cmd.extend(packages)
result = subprocess.run(
cmd,
cwd=self.project_dir,
capture_output=True,
text=True
)
if result.returncode == 0:
return {
"success": True,
"message": f"Installed: {', '.join(packages)}",
"output": result.stdout
}
else:
return {
"success": False,
"error": result.stderr,
"message": "Installation failed"
}
```
## Phase 6: File and Environment Handling
### 6.1 File Handler (file_handler.py)
- `file_read_project`:
- Validate paths are within project
- Auto-detect encoding
- Handle binary files gracefully
- `file_write_project`:
- Create directories if needed
- Backup existing files
- Validate write permissions
### 6.2 Environment Handler (env_handler.py)
- `load_env_file`: Use python-dotenv
- `set_env_var`: Update os.environ
- `list_env_vars`: Return all environment variables
- `create_env_template`:
- Import and introspect pydantic Settings class
- Generate .env.example with field descriptions
Critical implementation for pydantic Settings introspection:
```python
import importlib
import inspect
from pydantic import BaseSettings
from pydantic.fields import FieldInfo
async def create_env_template(self, settings_class_path: str, output_path: str = ".env.example"):
# Dynamic import: "app.config.Settings" -> module="app.config", class="Settings"
module_path, class_name = settings_class_path.rsplit(".", 1)
# Import module
module = importlib.import_module(module_path)
settings_class = getattr(module, class_name)
# Verify it's a pydantic Settings class
if not issubclass(settings_class, BaseSettings):
raise ValueError(f"{settings_class_path} is not a pydantic Settings class")
# Generate template
lines = ["# Auto-generated environment template\n"]
for field_name, field_info in settings_class.__fields__.items():
# Get field type and description
field_type = field_info.annotation
description = field_info.field_info.description or f"Type: {field_type}"
# Add to template
lines.append(f"# {description}")
lines.append(f"{field_name.upper()}=\n")
# Write template
template_path = self.project_dir / output_path
template_path.write_text("\n".join(lines))
```
## Phase 7: Development Tools
### 7.1 Development Tools (dev_tools.py)
- `format_code`:
- Try black first, fallback to ruff format
- Handle syntax errors gracefully
- `lint_file`:
- Use ruff check with auto-fix option
- Parse and format linting results
- `run_tests`:
- Execute pytest with proper working directory
- Capture and parse test results
- Support test filtering
## Phase 8: Testing Strategy
### 8.1 Test Implementation
- Use the pre-configured test_project as integration test target
- Test each tool individually with mock data
- Test error conditions and edge cases
- Verify session isolation
- Test concurrent session handling
### 8.2 Key Test Scenarios with test_project
```python
# Example test scenarios using the test_project
async def test_fastapi_project_workflow():
# Create session in test_project directory
session = await session_create(working_directory="tests/test_project")
# Load environment variables
await load_env_file() # Should load DATABASE_URL, SECRET_KEY, etc.
# Execute code that uses the project
result = await python_execute("""
from app.config import settings
from app.models.user import User
print(settings.database_url)
user = User(email="test@example.com", username="testuser")
""")
# Run the project's tests
test_result = await run_tests(test_path="tests/", verbose=True)
# Format project code
formatted = await format_code(code="def hello( ): return 'world'")
```
## Implementation Notes
### Critical Details
1. **Virtual Environment**: Always check for .venv and activate it using:
```python
import sys
from pathlib import Path
def activate_venv(project_dir: Path):
venv_path = project_dir / ".venv"
if venv_path.exists():
# Update sys.path
site_packages = venv_path / "lib" / f"python{sys.version_info.major}.{sys.version_info.minor}" / "site-packages"
if site_packages.exists():
sys.path.insert(0, str(site_packages))
# Update sys.prefix
sys.prefix = str(venv_path)
# Add venv bin to PATH
import os
venv_bin = venv_path / "bin"
os.environ["PATH"] = f"{venv_bin}:{os.environ.get('PATH', '')}"
# Update sys.executable
python_exe = venv_bin / "python"
if python_exe.exists():
sys.executable = str(python_exe)
```
2. **Error Handling**: Wrap all tool handlers with try/except, return structured errors
3. **Path Safety**: Always validate file paths are within project directory:
```python
requested_path = Path(file_path).resolve()
project_root = Path.cwd()
if not requested_path.is_relative_to(project_root):
raise ValueError("Path outside project")
```
4. **Subprocess Execution**: Use subprocess.run with capture_output=True, text=True
5. **Async Considerations**: All MCP tool handlers must be async functions
### Entry Point Configuration
The MCP server requires proper entry point setup for both development and production use:
#### 1. Package Entry Point (src/mcp_py3repl/__main__.py)
Create `src/mcp_py3repl/__main__.py` that starts the MCP server:
```python
import asyncio
import sys
from pathlib import Path
# Add src to path so imports work during development
sys.path.insert(0, str(Path(__file__).parent.parent))
from mcp_py3repl.server import run
if __name__ == "__main__":
asyncio.run(run())
```
#### 2. Package Initialization (src/mcp_py3repl/__init__.py)
Create `src/mcp_py3repl/__init__.py`:
```python
"""MCP Python REPL Server - Interactive Python execution for Claude Code"""
__version__ = "0.1.0"
```
#### 3. Running the Server
The server can be started in two ways:
- Development: `python -m mcp_py3repl` from the project root
- Production: Configure in Claude Desktop's settings as per README
## Success Criteria
- All tools from README are implemented and functional
- Test project (with its own dependencies) runs successfully
- MCP server can interact with test_project's FastAPI app, models, and tests
- Error messages are helpful and actionable
- Sessions are properly isolated
- Package management updates pyproject.toml correctly
- Environment variables are loaded and set correctly
- Test project's black/ruff formatting works through MCP tools
## Test Project Pre-setup Instructions
**IMPORTANT**: Before starting implementation, the test_project needs to be properly initialized:
1. Navigate to tests/test_project
2. Run `uv venv` to create virtual environment
3. Run `uv sync` to install all dependencies from pyproject.toml
4. The test project should have all FastAPI, SQLModel, pydantic dependencies ready
## Tool Registration Pattern
All tools must follow this exact pattern for MCP registration:
```python
from mcp import Tool
from mcp.types import TextContent, ToolResponse, ErrorData
@server.tool()
async def tool_name(param1: str, param2: int | None = None) -> ToolResponse:
"""Tool description for MCP discovery"""
try:
# Implementation
result = await some_operation(param1, param2)
return ToolResponse(
content=[TextContent(text=str(result))],
metadata={"extra_info": "value"}
)
except Exception as e:
return ToolResponse(
error=ErrorData(
code="ERROR_CODE",
message=str(e)
)
)
```
## Common Pitfalls to Avoid
1. **DO NOT** modify the test_project structure - it's a reference implementation
2. **DO NOT** assume imports work without activating venv first
3. **DO NOT** use relative imports in tool implementations
4. **DO NOT** forget to make all tool handlers async
5. **DO NOT** raise exceptions in tool handlers - return ErrorData instead
6. **DO NOT** forget to validate file paths are within project directory
7. **DO NOT** forget to handle async/await properly in all tools