Skip to main content
Glama

MCP Docker Sandbox Interpreter

by svngoku
main.py15.2 kB
import docker import os from contextlib import asynccontextmanager from collections.abc import AsyncIterator from dataclasses import dataclass from typing import Optional, Dict, Any from mcp.server.fastmcp import FastMCP, Context class DockerSandbox: def __init__(self): try: self.client = docker.from_env() print("Successfully connected to Docker daemon.") except docker.errors.DockerException as e: print(f"Error connecting to Docker: {e}") print("Ensure Docker Desktop or Docker Engine is running and DOCKER_HOST is set correctly.") raise # Re-raise the exception to prevent server startup self.container = None self._container_id: Optional[str] = None def create_container(self, image: str = "alpine:latest") -> str: """Creates and starts a Docker container.""" if self.container: print(f"Warning: Container already exists. Reusing container ID: {self._container_id}") return self._container_id print(f"Attempting to create container with image: {image}") try: print("Running container with specified parameters...") # Create with temporary elevated permissions to allow installation self.container = self.client.containers.run( image, command="tail -f /dev/null", # Keep container running detach=True, tty=True, mem_limit="512m", cpu_quota=50000, # Limits CPU usage (e.g., 50% of one core) pids_limit=100, # Limit number of processes # Temporarily allow network and root access for setup network_mode="bridge", # No user restriction for install step read_only=False, # Temporarily allow writes tmpfs={"/tmp": "rw,size=64m,noexec,nodev,nosuid"}, # Writable /tmp ) self._container_id = self.container.id print(f"Container created successfully. ID: {self._container_id}") # Install Python within the container print(f"Installing Python in container {self._container_id}...") # Use sh -c with full environment to ensure proper installation install_cmd = "sh -c 'apk update && apk add --no-cache python3 py3-pip && ln -sf /usr/bin/python3 /usr/bin/python'" install_result = self.container.exec_run(cmd=install_cmd) if install_result.exit_code != 0: install_output = install_result.output.decode('utf-8', errors='replace') print(f"Python installation failed: {install_output}") raise Exception(f"Failed to install Python: {install_output}") # Debug PATH and installed binaries print("Checking environment after installation...") debug_cmd = "sh -c 'echo PATH=$PATH && ls -la /usr/bin/python* && ls -la /usr/local/bin/python*'" debug_result = self.container.exec_run(cmd=debug_cmd) debug_output = debug_result.output.decode('utf-8', errors='replace') print(f"Environment debug info: {debug_output}") # Try multiple approaches to verify Python installation print("Verifying Python installation...") # Try various paths where Python might be installed potential_python_paths = [ "/usr/bin/python3", "/usr/bin/python", "/usr/local/bin/python3", "/usr/local/bin/python" ] # Check for Python binary in common locations python_path = None for path in potential_python_paths: test_cmd = f"test -x {path}" test_result = self.container.exec_run(cmd=test_cmd) if test_result.exit_code == 0: python_path = path print(f"Found Python at: {python_path}") break if not python_path: # As a last resort, try to find Python using find find_cmd = "find /usr -name 'python3*' -type f -executable" find_result = self.container.exec_run(cmd=find_cmd) find_output = find_result.output.decode('utf-8', errors='replace') if find_output.strip(): python_path = find_output.splitlines()[0].strip() print(f"Found Python using find: {python_path}") else: print("Python executable not found in any common location") raise Exception("Python executable not found after installation. Check container environment.") # Then check the Python version using the found path version_cmd = f"{python_path} --version" version_result = self.container.exec_run(cmd=version_cmd) if version_result.exit_code != 0: version_output = version_result.output.decode('utf-8', errors='replace') print(f"Python version check failed: {version_output}") raise Exception(f"Failed to execute Python after installation") python_version = version_result.output.decode('utf-8', errors='replace').strip() print(f"Python successfully installed at {python_path}, version: {python_version}") # Store the Python path for future use self._python_path = python_path # Execute the 'Wake up neo' message using shell as a safer option print(f"Executing startup command in {self._container_id}...") startup_cmd = "sh -c 'echo \"Wake up neo\"'" exec_result_startup = self.container.exec_run( cmd=startup_cmd, user="nobody", # Now run as non-root workdir="/tmp" ) startup_output = exec_result_startup.output.decode('utf-8', errors='replace') if exec_result_startup.output else "" print(f"Startup command output ({self._container_id}):\\n{startup_output}") return self._container_id except docker.errors.ImageNotFound: print(f"Warning: Docker image '{image}' not found locally. Attempting to pull...") try: self.client.images.pull(image) print(f"Image '{image}' pulled successfully. Retrying container creation...") # Retry creation return self.create_container(image) except docker.errors.APIError as pull_error: print(f"Error pulling image '{image}': {pull_error}") raise docker.errors.DockerException(f"Failed to pull image '{image}': {pull_error}") from pull_error except docker.errors.APIError as e: print(f"API error during container creation: {e}") raise docker.errors.DockerException(f"Failed to create container: {e}") from e except Exception as e: # Catch potential unexpected errors print(f"An unexpected error occurred during container creation: {e}") raise def run_code(self, code: str, language: str = "python") -> Dict[str, Any]: """Runs code in the container.""" if not self.container: # Return error directly, let caller handle logging via ctx return {"error": "Container not initialized. Call 'initialize_sandbox' first."} cmd = [] if language == "python": cmd = ["python3", "-c", code] # Use python3 command elif language == "javascript": return {"error": "JavaScript execution not supported in minimal container"} # Add more languages as needed else: return {"error": f"Unsupported language: {language}"} print(f"Executing code in container {self._container_id}: {cmd}") try: # Ensure container is running self.container.reload() if self.container.status != 'running': print(f"Warning: Container {self._container_id} is not running. Status: {self.container.status}. Restarting...") self.container.start() # Execute with timeout (e.g., 10 seconds) # Note: exec_run doesn't have a direct timeout, manage externally or via command (e.g., `timeout` utility) # For simplicity here, we rely on resource limits primarily. exec_result = self.container.exec_run( cmd=cmd, user="nobody", # Run command as non-root workdir="/tmp" # Execute in the writable temp directory ) output = exec_result.output.decode('utf-8', errors='replace') if exec_result.output else "" exit_code = exec_result.exit_code print(f"Execution finished. Exit code: {exit_code}. Output:\\n{output}") if exit_code != 0: # Return specific error message return {"exit_code": exit_code, "output": output, "error": f"Execution failed with exit code {exit_code}"} else: # Success case return {"exit_code": exit_code, "output": output} except docker.errors.APIError as e: print(f"Error executing code in container {self._container_id}: {e}") return {"error": f"API error during execution: {e}"} except Exception as e: print(f"Unexpected error during code execution: {e}") return {"error": f"Unexpected error: {e}"} def cleanup(self): """Stops and removes the container.""" if self.container: container_id = self._container_id print(f"Cleaning up container: {container_id}") try: self.container.stop(timeout=5) # Give 5 seconds to stop gracefully self.container.remove(force=True) # Force remove if stop fails print(f"Container {container_id} stopped and removed.") except docker.errors.NotFound: print(f"Container {container_id} already removed.") except docker.errors.APIError as e: print(f"Error during container cleanup {container_id}: {e}") finally: self.container = None self._container_id = None else: print("No active container to clean up.") # Define a context structure to hold our sandbox instance @dataclass class SandboxContext: sandbox: DockerSandbox # Lifespan manager for the sandbox @asynccontextmanager async def sandbox_lifespan(server: FastMCP) -> AsyncIterator[SandboxContext]: """Manage DockerSandbox lifecycle.""" print("Lifespan: Initializing Docker Sandbox...") sandbox = DockerSandbox() try: # Yield the context containing the initialized sandbox print("Lifespan: Docker Sandbox initialized, yielding context.") yield SandboxContext(sandbox=sandbox) finally: # Cleanup when the server shuts down print("Lifespan: Shutting down Docker Sandbox...") sandbox.cleanup() # Create the FastMCP server instance with the lifespan manager mcp = FastMCP( "Docker Code Sandbox", lifespan=sandbox_lifespan, # Add dependencies if needed for deployment # dependencies=["docker"] ) # --- MCP Tools Definition --- @mcp.tool() async def initialize_sandbox(ctx: Context, image: str = "alpine:latest") -> Dict[str, Any]: """ Initializes a secure Docker container sandbox for code execution. Reuses existing container if already initialized. Args: image: The Docker image to use (e.g., 'python:3.12-alpine', 'node:20-slim'). Defaults to python:3.12-alpine. Returns: A dictionary containing the container ID or an error message. """ sandbox: DockerSandbox = ctx.request_context.lifespan_context.sandbox try: await ctx.info(f"Attempting to initialize sandbox with image: {image}") container_id = sandbox.create_container(image) await ctx.info(f"Sandbox initialized successfully. Container ID: {container_id}") return {"status": "success", "container_id": container_id} except docker.errors.DockerException as e: await ctx.error(f"Docker error during sandbox initialization: {e}") return {"status": "error", "error_type": "DockerException", "message": str(e)} except Exception as e: await ctx.error(f"Unexpected error during sandbox initialization: {e}") return {"status": "error", "error_type": "UnexpectedError", "message": f"An unexpected error occurred: {e}"} @mcp.tool() async def execute_code(ctx: Context, code: str, language: str = "python") -> Dict[str, Any]: """ Executes the given code string inside the initialized Docker sandbox. Args: code: The code string to execute. language: The programming language ('python', 'javascript', etc.). Defaults to python. Returns: A dictionary containing the execution result (stdout/stderr) and exit code, or an error message. """ sandbox: DockerSandbox = ctx.request_context.lifespan_context.sandbox if not sandbox.container: await ctx.warning("Sandbox not initialized. Cannot execute code.") return {"status": "error", "error_type": "StateError", "message": "Sandbox not initialized. Call 'initialize_sandbox' first."} await ctx.info(f"Executing {language} code in container {sandbox._container_id}...") result = sandbox.run_code(code, language) if "error" in result: await ctx.error(f"Code execution failed: {result['error']}") return { "status": "error", "error_type": "ExecutionError", "message": result["error"], "output": result.get("output"), "exit_code": result.get("exit_code") } else: await ctx.info(f"Execution completed. Exit code: {result.get('exit_code')}") return { "status": "success", "exit_code": result.get("exit_code"), "output": result.get("output") } @mcp.tool() async def stop_sandbox(ctx: Context) -> Dict[str, str]: """ Stops and removes the currently active Docker container sandbox. """ sandbox: DockerSandbox = ctx.request_context.lifespan_context.sandbox await ctx.info("Attempting to stop and remove sandbox...") try: sandbox.cleanup() await ctx.info("Sandbox stopped and removed successfully.") return {"status": "success", "message": "Sandbox stopped and removed."} except Exception as e: await ctx.error(f"Error stopping sandbox: {e}") return {"status": "error", "error_type": "CleanupError", "message": f"An error occurred during cleanup: {e}"} if __name__ == "__main__": # Use print here as logger isn't configured before server runs print("Starting server via __main__ (using FASTMCP_SERVER_HOST/PORT env vars or defaults)...") mcp.run()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/svngoku/mcp-docker-code-interpreter'

If you have feedback or need assistance with the MCP directory API, please join our Discord server