feat MCP Sandboxing for python

This commit is contained in:
Irina Rueegg 2026-05-10 21:44:53 +02:00
parent 560e56b596
commit 3c677a13ab
3 changed files with 139 additions and 9 deletions

View File

@ -13,12 +13,9 @@ step-by-step methods so Streamlit can drive the loop via session_state:
agent.reject(feedback) # skip action, inject user feedback
"""
import ast
import inspect
import json
import os
import subprocess
import sys
import re
from pathlib import Path
import asyncio
import pprint
@ -76,6 +73,8 @@ async def dispatch_tool(tool_name: str, arguments: dict) -> str:
print(f"Trying to call tool '{tool_name}' in dispatch_tool through MCPToolAdapter...")
result = await adapter.call_tool(tool_name, arguments)
print(f"Raw result from tool '{tool_name}': {result}")
if result.isError:
texts = [block.text for block in result.content if block.type == "text"]
return f"Tool error: {' '.join(texts)}"
@ -102,7 +101,7 @@ analyze code, and execute Python scripts.
You can call tools to interact with the workspace and get feedback.
You can write and read files, list directory contents, search for patterns,
validate Python syntax, and run Python code.
Wou can access web search and page fetching tools to gather information from the internet.
You can access web search and page fetching tools to gather information from the internet.
You can use these capabilities to iteratively work towards completing the user's task.
</capabilities>
@ -239,7 +238,9 @@ def extract_json(text: str) -> str:
Returns the cleaned JSON string, or the original text as a fallback
(so json.loads can raise a meaningful error with context).
"""
import re
if text is None:
return ""
# 1. Strip markdown fences
fenced = re.sub(r"```(?:json)?\s*([\s\S]*?)\s*```", r"\1", text.strip())
@ -282,6 +283,9 @@ def extract_json(text: str) -> str:
def _strip_code_fences(text: str) -> str:
"""Remove markdown code fences (```json ... ```) from a string."""
if text is None:
return ""
text = text.strip()
if text.startswith("```"):
lines = text.split("\n")

View File

@ -1,9 +1,32 @@
import ast
from datetime import datetime
import subprocess
import io
from pyflakes.api import check
from pyflakes.reporter import Reporter
from mcp.server.fastmcp import FastMCP
from pathlib import Path
import venv
import shutil
# ── Sandbox venv ────────────────────────────────────────────────────────────
SERVER_BASE_DIR = Path(__file__).parent.resolve()
SANDBOX_DIR = SERVER_BASE_DIR / ".mcp_sandbox"
WORKSPACE_DIR = SERVER_BASE_DIR.parent.parent.parent.parent / "workspace"
def get_sandbox_paths():
"""Bestimmt die Executables innerhalb der Venv ohne os-Modul."""
if not SANDBOX_DIR.exists():
venv.create(SANDBOX_DIR, with_pip=True)
bin_folder = "Scripts" if Path("C:/").exists() else "bin" # Einfacher Check für Windows
python_exe = SANDBOX_DIR / bin_folder / "python"
pip_exe = SANDBOX_DIR / bin_folder / "pip"
return str(python_exe), str(pip_exe)
PYTHON_EXE, PIP_EXE = get_sandbox_paths()
# ── Configuration ────────────────────────────────────────────────────────────
EXEC_TIMEOUT = 10 # seconds before killing the subprocess
@ -41,6 +64,10 @@ BLOCKED_BUILTINS = {
"globals", "locals", "vars", "memoryview", "type"
}
FORBIDDEN_SEQUENCES = ["../", "..\\", "/etc/", "/dev/",
"C:\\Windows", "C:\\Program Files", "C:\\Users",
"compile(", "__import__", "os.", "sys.", "subprocess."]
# ── Static Analysis ────────────────────────────────────────────────────
def check_code_safety(code: str) -> str | None:
"""
@ -78,6 +105,10 @@ def check_code_safety(code: str) -> str | None:
if isinstance(node.func, ast.Name):
if node.func.id in BLOCKED_BUILTINS:
return f"Blocked builtin: Use of builtin '{node.func.id}' is not allowed."
for seq in FORBIDDEN_SEQUENCES:
if seq in code:
return f"Blocked: Suspect path sequence '{seq}' detected."
return None # No violations found
@ -200,7 +231,84 @@ def lint_code(code: str) -> str:
@mcp.tool()
def run_python_sandboxed(code: str) -> str:
def list_sandbox_packages() -> str:
"""
Lists all Python-Packages, that are installed in the Sandbox and their Version.
Helpful to determine if packages like 'pygame', 'numpy' or similair are already available
"""
try:
result = subprocess.run(
[PIP_EXE, "list"],
capture_output=True,
text=True,
timeout=10
)
if result.returncode != 0:
return f"Error while listing the packages: {result.stderr}"
if not result.stdout.strip():
return "The Sandbox environment is empty (only Standard-Libraries are available)."
return f"Installed Packages: {result.stdout}"
except Exception as e:
return f"Error trying to list packages from the Sandbox venv: {str(e)}"
@mcp.tool()
def install_package_into_sandbox(package_name: str) -> str:
"""
Install a Python package into the sandbox environment using pip.
Args:
package_name: The name of the package to install (e.g., "requests").
Returns:
A success message or an error message if installation fails.
"""
clean_name = "".join(e for e in package_name if e.isalnum() or e in "-_.")
if clean_name in BLOCKED_IMPORTS:
return f"Error: Installation of package '{clean_name}' is blocked due to security policies."
if clean_name in BLOCKED_BUILTINS:
return f"Error: Installation of package '{clean_name}' is blocked due to security policies."
if not clean_name:
return "Error: Invalid package name provided."
try:
result = subprocess.run(
[PIP_EXE, "install", clean_name],
capture_output=True,
text=True,
timeout=EXEC_TIMEOUT
)
if result.returncode == 0:
return f"Package '{clean_name}' installed successfully in the sandbox."
else:
return (f"Error installing package '{clean_name}':\n"
f"{result.stdout}\n{result.stderr}")
except subprocess.TimeoutExpired:
return f"Error: Package installation exceeded time limit of {EXEC_TIMEOUT} seconds and was terminated."
except Exception as e:
return f"Error during package installation: {e}"
@mcp.tool()
def reset_sandbox() -> str:
"""Löscht die gesamte Sandbox und erstellt sie neu (Full Reset)."""
if SANDBOX_DIR.exists():
shutil.rmtree(SANDBOX_DIR)
get_sandbox_paths()
return "Sandbox wurde komplett zurückgesetzt."
@mcp.tool()
def run_python_code_sandboxed(code: str) -> str:
"""
Run Python code in a sandboxed environment.
@ -220,10 +328,24 @@ def run_python_sandboxed(code: str) -> str:
static_safety = check_code_safety(code)
if static_safety:
return f"Code rejected:{static_safety}"
run_id = datetime.now().strftime("%Y%m%d_%H%M%S")
jail_dir = WORKSPACE_DIR / f"sandbox_run_{run_id}"
try:
jail_dir.mkdir(parents=True, exist_ok=True)
custom_env = {
"PYTHONPATH": str(WORKSPACE_DIR),
"PATH": str(Path(PYTHON_EXE).parent),
"HOME": str(jail_dir),
"TMPDIR": str(jail_dir)
}
result = subprocess.run(
["python", "-c", code],
[PYTHON_EXE, "-c", code],
cwd=str(WORKSPACE_DIR),
env=custom_env,
capture_output=True,
text=True,
timeout=EXEC_TIMEOUT)
@ -243,6 +365,10 @@ def run_python_sandboxed(code: str) -> str:
except Exception as e:
return f"Error during code execution: {e}"
finally:
if jail_dir.exists():
shutil.rmtree(jail_dir)
@mcp.tool()
def python_code_validation(code: str) -> str:

View File

@ -200,7 +200,7 @@ def create_new_directory(path: str) -> str:
if resolved.exists():
return f"Error: File '{path}' already exists."
if resolved.suffix != None:
if resolved.suffix != None and resolved.suffix != "":
return f"Error: can only create directories, got '{resolved.suffix}'."
try: