# Directory Structure ``` ├── .gitignore ├── .python-version ├── main.py ├── pyproject.toml ├── README.md ├── requirements.txt └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 1 | 3.13 2 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | .cursorindexingignore 12 | .specstory/** 13 | .cursor/* ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Docker Sandbox Interpreter 2 | 3 | A secure Docker-based code execution environment for the Model Context Protocol (MCP). 4 | 5 | ## Overview 6 | 7 | This project provides a secure sandbox for executing code through MCP (Model Context Protocol). It allows AI assistants to safely run code without requiring direct access to the host system, by executing all code within isolated Docker containers. 8 | 9 | ```mermaid 10 | graph LR 11 | A[Claude/Cursor] -->|Sends Code| B[MCP Server] 12 | B -->|Executes Code| C[Docker Sandbox] 13 | C -->|Returns Results| A 14 | ``` 15 | 16 | ## Features 17 | 18 | - **Secure Execution**: Code runs in isolated Docker containers with strict security limitations 19 | - **Multi-Language Support**: Currently supports Python with easy extensibility for other languages 20 | - **Resource Limitations**: CPU and memory restrictions to prevent abuse 21 | - **MCP Integration**: Fully compatible with the Model Context Protocol 22 | - **Automatic Setup**: Handles container creation, dependency installation, and cleanup 23 | 24 | ## Requirements 25 | 26 | - Docker (Desktop or Engine) 27 | - Python 3.10+ 28 | - MCP SDK (`pip install mcp`) 29 | - Docker Python SDK (`pip install docker`) 30 | 31 | ## Installation 32 | 33 | 1. Clone this repository: 34 | ```bash 35 | git clone https://github.com/yourusername/mcp-docker-interpreter.git 36 | cd mcp-docker-interpreter 37 | ``` 38 | 39 | 2. Create and activate a virtual environment: 40 | ```bash 41 | python -m venv .venv 42 | source .venv/bin/activate # On Windows: .venv\Scripts\activate 43 | ``` 44 | 45 | 3. Install dependencies: 46 | ```bash 47 | pip install -r requirements.txt 48 | ``` 49 | 50 | ## Usage 51 | 52 | ### Starting the MCP Server 53 | 54 | Start the server by running: 55 | 56 | ```bash 57 | # For Colima users: 58 | export DOCKER_HOST="unix:///Users/username/.colima/default/docker.sock" 59 | 60 | # Run the server 61 | uv run mcp dev main.py 62 | ``` 63 | 64 | ### Connecting to an AI Assistant 65 | 66 | You can connect this MCP server to AI assistants that support the Model Context Protocol: 67 | 68 | #### Cursor 69 | 70 | In Cursor, add the following to your MCP settings: 71 | 72 | ```json 73 | { 74 | "mcpServers": { 75 | "docker-sandbox": { 76 | "command": "python", 77 | "args": ["/absolute/path/to/your/main.py"], 78 | "env": { 79 | "DOCKER_HOST": "unix:///path/to/your/docker.sock" 80 | } 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | Replace the paths with your actual file paths. 87 | 88 | #### Claude Desktop 89 | 90 | Similar to Cursor, add the configuration to Claude Desktop's MCP settings. 91 | 92 | ### MCP Tools 93 | 94 | This MCP server exposes three main tools: 95 | 96 | 1. **initialize_sandbox**: Creates a new Docker container for code execution 97 | ``` 98 | Arguments: 99 | - image: The Docker image to use (default: "alpine:latest") 100 | ``` 101 | 102 | 2. **execute_code**: Runs code in the initialized sandbox 103 | ``` 104 | Arguments: 105 | - code: The code string to execute 106 | - language: Programming language (default: "python") 107 | ``` 108 | 109 | 3. **stop_sandbox**: Stops and removes the container 110 | ``` 111 | No arguments needed 112 | ``` 113 | 114 | ## How It Works 115 | 116 | 1. When `initialize_sandbox` is called, the system: 117 | - Creates a Docker container based on Alpine Linux 118 | - Installs Python and other dependencies 119 | - Sets up security restrictions 120 | 121 | 2. When `execute_code` is called: 122 | - Code is executed within the isolated container 123 | - Standard output and errors are captured 124 | - Results are returned to the calling application 125 | 126 | 3. When `stop_sandbox` is called: 127 | - The container is stopped and removed 128 | - All resources are released 129 | 130 | ## Security Considerations 131 | 132 | This sandbox implements several security measures: 133 | 134 | - Containers have restricted CPU and memory usage 135 | - Containers are run with minimal privileges 136 | - Network access is disabled by default 137 | - Containers are disposable and cleaned up after use 138 | 139 | ## Development 140 | 141 | ### Project Structure 142 | 143 | ``` 144 | mcp-docker-interpreter/ 145 | ├── main.py # Main implementation of MCP server and Docker sandbox 146 | ├── requirements.txt # Project dependencies 147 | └── README.md # This file 148 | ``` 149 | 150 | ### Adding New Language Support 151 | 152 | To add support for a new programming language, modify the `run_code` method in the `DockerSandbox` class to handle the new language. 153 | 154 | ## Troubleshooting 155 | 156 | ### Common Issues 157 | 158 | 1. **Docker connection error**: 159 | - Ensure Docker is running 160 | - Check that the DOCKER_HOST environment variable is correctly set for your Docker installation 161 | 162 | 2. **Container creation fails**: 163 | - Verify you have permission to create Docker containers 164 | - Ensure the specified base image is accessible 165 | 166 | 3. **Code execution fails**: 167 | - Check that the language runtime is properly installed in the container 168 | - Verify the code is valid for the specified language 169 | 170 | ## License 171 | 172 | [MIT License](LICENSE) 173 | 174 | ## Acknowledgements 175 | 176 | - This project uses the [Model Context Protocol](https://modelcontextprotocol.io/) 177 | - Built with [Docker SDK for Python](https://docker-py.readthedocs.io/) 178 | ``` -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- ``` 1 | docker>=7.0.0 2 | mcp>=1.6.0 ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "mcp-docker-interpretor" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "docker>=7.1.0", 9 | "mcp[cli]>=1.6.0", 10 | ] 11 | ``` -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- ```python 1 | import docker 2 | import os 3 | from contextlib import asynccontextmanager 4 | from collections.abc import AsyncIterator 5 | from dataclasses import dataclass 6 | from typing import Optional, Dict, Any 7 | from mcp.server.fastmcp import FastMCP, Context 8 | 9 | class DockerSandbox: 10 | def __init__(self): 11 | try: 12 | self.client = docker.from_env() 13 | print("Successfully connected to Docker daemon.") 14 | except docker.errors.DockerException as e: 15 | print(f"Error connecting to Docker: {e}") 16 | print("Ensure Docker Desktop or Docker Engine is running and DOCKER_HOST is set correctly.") 17 | raise # Re-raise the exception to prevent server startup 18 | self.container = None 19 | self._container_id: Optional[str] = None 20 | 21 | def create_container(self, image: str = "alpine:latest") -> str: 22 | """Creates and starts a Docker container.""" 23 | if self.container: 24 | print(f"Warning: Container already exists. Reusing container ID: {self._container_id}") 25 | return self._container_id 26 | 27 | print(f"Attempting to create container with image: {image}") 28 | try: 29 | print("Running container with specified parameters...") 30 | # Create with temporary elevated permissions to allow installation 31 | self.container = self.client.containers.run( 32 | image, 33 | command="tail -f /dev/null", # Keep container running 34 | detach=True, 35 | tty=True, 36 | mem_limit="512m", 37 | cpu_quota=50000, # Limits CPU usage (e.g., 50% of one core) 38 | pids_limit=100, # Limit number of processes 39 | # Temporarily allow network and root access for setup 40 | network_mode="bridge", 41 | # No user restriction for install step 42 | read_only=False, # Temporarily allow writes 43 | tmpfs={"/tmp": "rw,size=64m,noexec,nodev,nosuid"}, # Writable /tmp 44 | ) 45 | self._container_id = self.container.id 46 | print(f"Container created successfully. ID: {self._container_id}") 47 | 48 | # Install Python within the container 49 | print(f"Installing Python in container {self._container_id}...") 50 | # Use sh -c with full environment to ensure proper installation 51 | install_cmd = "sh -c 'apk update && apk add --no-cache python3 py3-pip && ln -sf /usr/bin/python3 /usr/bin/python'" 52 | install_result = self.container.exec_run(cmd=install_cmd) 53 | 54 | if install_result.exit_code != 0: 55 | install_output = install_result.output.decode('utf-8', errors='replace') 56 | print(f"Python installation failed: {install_output}") 57 | raise Exception(f"Failed to install Python: {install_output}") 58 | 59 | # Debug PATH and installed binaries 60 | print("Checking environment after installation...") 61 | debug_cmd = "sh -c 'echo PATH=$PATH && ls -la /usr/bin/python* && ls -la /usr/local/bin/python*'" 62 | debug_result = self.container.exec_run(cmd=debug_cmd) 63 | debug_output = debug_result.output.decode('utf-8', errors='replace') 64 | print(f"Environment debug info: {debug_output}") 65 | 66 | # Try multiple approaches to verify Python installation 67 | print("Verifying Python installation...") 68 | 69 | # Try various paths where Python might be installed 70 | potential_python_paths = [ 71 | "/usr/bin/python3", 72 | "/usr/bin/python", 73 | "/usr/local/bin/python3", 74 | "/usr/local/bin/python" 75 | ] 76 | 77 | # Check for Python binary in common locations 78 | python_path = None 79 | for path in potential_python_paths: 80 | test_cmd = f"test -x {path}" 81 | test_result = self.container.exec_run(cmd=test_cmd) 82 | if test_result.exit_code == 0: 83 | python_path = path 84 | print(f"Found Python at: {python_path}") 85 | break 86 | 87 | if not python_path: 88 | # As a last resort, try to find Python using find 89 | find_cmd = "find /usr -name 'python3*' -type f -executable" 90 | find_result = self.container.exec_run(cmd=find_cmd) 91 | find_output = find_result.output.decode('utf-8', errors='replace') 92 | if find_output.strip(): 93 | python_path = find_output.splitlines()[0].strip() 94 | print(f"Found Python using find: {python_path}") 95 | else: 96 | print("Python executable not found in any common location") 97 | raise Exception("Python executable not found after installation. Check container environment.") 98 | 99 | # Then check the Python version using the found path 100 | version_cmd = f"{python_path} --version" 101 | version_result = self.container.exec_run(cmd=version_cmd) 102 | if version_result.exit_code != 0: 103 | version_output = version_result.output.decode('utf-8', errors='replace') 104 | print(f"Python version check failed: {version_output}") 105 | raise Exception(f"Failed to execute Python after installation") 106 | 107 | python_version = version_result.output.decode('utf-8', errors='replace').strip() 108 | print(f"Python successfully installed at {python_path}, version: {python_version}") 109 | 110 | # Store the Python path for future use 111 | self._python_path = python_path 112 | 113 | # Execute the 'Wake up neo' message using shell as a safer option 114 | print(f"Executing startup command in {self._container_id}...") 115 | startup_cmd = "sh -c 'echo \"Wake up neo\"'" 116 | exec_result_startup = self.container.exec_run( 117 | cmd=startup_cmd, 118 | user="nobody", # Now run as non-root 119 | workdir="/tmp" 120 | ) 121 | startup_output = exec_result_startup.output.decode('utf-8', errors='replace') if exec_result_startup.output else "" 122 | print(f"Startup command output ({self._container_id}):\\n{startup_output}") 123 | 124 | return self._container_id 125 | except docker.errors.ImageNotFound: 126 | print(f"Warning: Docker image '{image}' not found locally. Attempting to pull...") 127 | try: 128 | self.client.images.pull(image) 129 | print(f"Image '{image}' pulled successfully. Retrying container creation...") 130 | # Retry creation 131 | return self.create_container(image) 132 | except docker.errors.APIError as pull_error: 133 | print(f"Error pulling image '{image}': {pull_error}") 134 | raise docker.errors.DockerException(f"Failed to pull image '{image}': {pull_error}") from pull_error 135 | except docker.errors.APIError as e: 136 | print(f"API error during container creation: {e}") 137 | raise docker.errors.DockerException(f"Failed to create container: {e}") from e 138 | except Exception as e: # Catch potential unexpected errors 139 | print(f"An unexpected error occurred during container creation: {e}") 140 | raise 141 | 142 | def run_code(self, code: str, language: str = "python") -> Dict[str, Any]: 143 | """Runs code in the container.""" 144 | if not self.container: 145 | # Return error directly, let caller handle logging via ctx 146 | return {"error": "Container not initialized. Call 'initialize_sandbox' first."} 147 | 148 | cmd = [] 149 | if language == "python": 150 | cmd = ["python3", "-c", code] # Use python3 command 151 | elif language == "javascript": 152 | return {"error": "JavaScript execution not supported in minimal container"} 153 | # Add more languages as needed 154 | else: 155 | return {"error": f"Unsupported language: {language}"} 156 | 157 | print(f"Executing code in container {self._container_id}: {cmd}") 158 | try: 159 | # Ensure container is running 160 | self.container.reload() 161 | if self.container.status != 'running': 162 | print(f"Warning: Container {self._container_id} is not running. Status: {self.container.status}. Restarting...") 163 | self.container.start() 164 | 165 | # Execute with timeout (e.g., 10 seconds) 166 | # Note: exec_run doesn't have a direct timeout, manage externally or via command (e.g., `timeout` utility) 167 | # For simplicity here, we rely on resource limits primarily. 168 | exec_result = self.container.exec_run( 169 | cmd=cmd, 170 | user="nobody", # Run command as non-root 171 | workdir="/tmp" # Execute in the writable temp directory 172 | ) 173 | 174 | output = exec_result.output.decode('utf-8', errors='replace') if exec_result.output else "" 175 | exit_code = exec_result.exit_code 176 | 177 | print(f"Execution finished. Exit code: {exit_code}. Output:\\n{output}") 178 | 179 | if exit_code != 0: 180 | # Return specific error message 181 | return {"exit_code": exit_code, "output": output, "error": f"Execution failed with exit code {exit_code}"} 182 | else: 183 | # Success case 184 | return {"exit_code": exit_code, "output": output} 185 | 186 | except docker.errors.APIError as e: 187 | print(f"Error executing code in container {self._container_id}: {e}") 188 | return {"error": f"API error during execution: {e}"} 189 | except Exception as e: 190 | print(f"Unexpected error during code execution: {e}") 191 | return {"error": f"Unexpected error: {e}"} 192 | 193 | 194 | def cleanup(self): 195 | """Stops and removes the container.""" 196 | if self.container: 197 | container_id = self._container_id 198 | print(f"Cleaning up container: {container_id}") 199 | try: 200 | self.container.stop(timeout=5) # Give 5 seconds to stop gracefully 201 | self.container.remove(force=True) # Force remove if stop fails 202 | print(f"Container {container_id} stopped and removed.") 203 | except docker.errors.NotFound: 204 | print(f"Container {container_id} already removed.") 205 | except docker.errors.APIError as e: 206 | print(f"Error during container cleanup {container_id}: {e}") 207 | finally: 208 | self.container = None 209 | self._container_id = None 210 | else: 211 | print("No active container to clean up.") 212 | 213 | 214 | 215 | # Define a context structure to hold our sandbox instance 216 | @dataclass 217 | class SandboxContext: 218 | sandbox: DockerSandbox 219 | 220 | # Lifespan manager for the sandbox 221 | @asynccontextmanager 222 | async def sandbox_lifespan(server: FastMCP) -> AsyncIterator[SandboxContext]: 223 | """Manage DockerSandbox lifecycle.""" 224 | print("Lifespan: Initializing Docker Sandbox...") 225 | sandbox = DockerSandbox() 226 | try: 227 | # Yield the context containing the initialized sandbox 228 | print("Lifespan: Docker Sandbox initialized, yielding context.") 229 | yield SandboxContext(sandbox=sandbox) 230 | finally: 231 | # Cleanup when the server shuts down 232 | print("Lifespan: Shutting down Docker Sandbox...") 233 | sandbox.cleanup() 234 | 235 | # Create the FastMCP server instance with the lifespan manager 236 | mcp = FastMCP( 237 | "Docker Code Sandbox", 238 | lifespan=sandbox_lifespan, 239 | # Add dependencies if needed for deployment 240 | # dependencies=["docker"] 241 | ) 242 | 243 | # --- MCP Tools Definition --- 244 | @mcp.tool() 245 | async def initialize_sandbox(ctx: Context, image: str = "alpine:latest") -> Dict[str, Any]: 246 | """ 247 | Initializes a secure Docker container sandbox for code execution. 248 | Reuses existing container if already initialized. 249 | 250 | Args: 251 | image: The Docker image to use (e.g., 'python:3.12-alpine', 'node:20-slim'). Defaults to python:3.12-alpine. 252 | Returns: 253 | A dictionary containing the container ID or an error message. 254 | """ 255 | sandbox: DockerSandbox = ctx.request_context.lifespan_context.sandbox 256 | try: 257 | await ctx.info(f"Attempting to initialize sandbox with image: {image}") 258 | container_id = sandbox.create_container(image) 259 | await ctx.info(f"Sandbox initialized successfully. Container ID: {container_id}") 260 | return {"status": "success", "container_id": container_id} 261 | except docker.errors.DockerException as e: 262 | await ctx.error(f"Docker error during sandbox initialization: {e}") 263 | return {"status": "error", "error_type": "DockerException", "message": str(e)} 264 | except Exception as e: 265 | await ctx.error(f"Unexpected error during sandbox initialization: {e}") 266 | return {"status": "error", "error_type": "UnexpectedError", "message": f"An unexpected error occurred: {e}"} 267 | 268 | 269 | @mcp.tool() 270 | async def execute_code(ctx: Context, code: str, language: str = "python") -> Dict[str, Any]: 271 | """ 272 | Executes the given code string inside the initialized Docker sandbox. 273 | 274 | Args: 275 | code: The code string to execute. 276 | language: The programming language ('python', 'javascript', etc.). Defaults to python. 277 | 278 | Returns: 279 | A dictionary containing the execution result (stdout/stderr) and exit code, or an error message. 280 | """ 281 | sandbox: DockerSandbox = ctx.request_context.lifespan_context.sandbox 282 | if not sandbox.container: 283 | await ctx.warning("Sandbox not initialized. Cannot execute code.") 284 | return {"status": "error", "error_type": "StateError", "message": "Sandbox not initialized. Call 'initialize_sandbox' first."} 285 | 286 | await ctx.info(f"Executing {language} code in container {sandbox._container_id}...") 287 | result = sandbox.run_code(code, language) 288 | 289 | if "error" in result: 290 | await ctx.error(f"Code execution failed: {result['error']}") 291 | return { 292 | "status": "error", 293 | "error_type": "ExecutionError", 294 | "message": result["error"], 295 | "output": result.get("output"), 296 | "exit_code": result.get("exit_code") 297 | } 298 | else: 299 | await ctx.info(f"Execution completed. Exit code: {result.get('exit_code')}") 300 | return { 301 | "status": "success", 302 | "exit_code": result.get("exit_code"), 303 | "output": result.get("output") 304 | } 305 | 306 | @mcp.tool() 307 | async def stop_sandbox(ctx: Context) -> Dict[str, str]: 308 | """ 309 | Stops and removes the currently active Docker container sandbox. 310 | """ 311 | sandbox: DockerSandbox = ctx.request_context.lifespan_context.sandbox 312 | await ctx.info("Attempting to stop and remove sandbox...") 313 | try: 314 | sandbox.cleanup() 315 | await ctx.info("Sandbox stopped and removed successfully.") 316 | return {"status": "success", "message": "Sandbox stopped and removed."} 317 | except Exception as e: 318 | await ctx.error(f"Error stopping sandbox: {e}") 319 | return {"status": "error", "error_type": "CleanupError", "message": f"An error occurred during cleanup: {e}"} 320 | 321 | 322 | if __name__ == "__main__": 323 | # Use print here as logger isn't configured before server runs 324 | print("Starting server via __main__ (using FASTMCP_SERVER_HOST/PORT env vars or defaults)...") 325 | mcp.run() ```