#
tokens: 5814/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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()
```