MCP-серверы становятся необходимой частью инфраструктуры локальных LLM, обеспечивая безопасное взаимодействие между моделью и внешними инструментами. Такой сервер может быть полезен, например, для разработки на Питоне. Веб-версия QWEN3 уже продемонстрировала способность не только генерировать код, но и автоматически проверять его синтаксис и выполнять в безопасной среде прямо из браузера. А бесплатное приложение Google Antigravity, позволяет управлять ИИ агентами-программистами, контролируя командами на естественном языке весь процесс разработки и тестирования. Но, во-первых, приложение недоступно для российских аккаунтов, а во-вторых, весь наш код и процесс взаимодействия становится полностью доступен по сети для провайдера сервиса.
Локально MCP-сервер можно использовать например в редакторе VS Code с использованием расширений подключения к языковой модели, таких, например, как Continue. Мы хотим чтобы модель могла сама проверять и выполнять сгенерированный код на Питоне в изолированной среде. Надо ли говорить, что инструмент для вайб-кодинга создавался с помощью того же вайб-кодинга). Может показаться что с помощью ИИ это легко, но в данном случае задача слишком непростая чтобы обойтись парой часов, и ошибок в процессе разработки ИИ допускал предостаточно.
При разработке с использованием ИИ, локальная нейросеть предлагает Python-скрипт для решения задачи, но нужна уверенность в его корректности и безопасности. Прямой запуск такого кода на рабочей машине это риск для системы и данных. Значит MCP-сервер должен учитывать это. Посмотрим как устроен такой сервер, какие подводные камни могут встретиться и как интегрировать его с локальной LLM.
Статья является документированным описанием проекта MCP-сервера, инструмента LLM, предоставляющего две функции: проверку синтаксиса и безопасное выполнение кода в изолированной песочнице. Исходники выложены на github.
Структура проекта и установка
В корневой папке проекта создайте файл pyproject.toml и подпапку python_code_sandbox. В ней два исходника и пустой файл __init__.py:
python-mcp-sandbox/
├── python_code_sandbox/
│ ├── __init__.py
│ ├── python_code_sandbox.py
│ └── safe_executor.py
└── pyproject.toml
Здесь python_code_sandbox.py — основной модуль сервера, содержащий функции проверки синтаксиса и запуска кода, safe_executor.py — модуль, реализующий изолированное выполнение кода в песочнице, pyproject.toml — файл конфигурации сборки проекта.
Файл pyproject.toml
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "python-code-sandbox"
version = "0.1.0"
description = "FastMCP server for testing and syntax checking of generated python code"
authors = [{ name="Alexander Kazantsev", email="akazant@gmail.com" }]
dependencies = [
"asteval",
"fastmcp",
"pywin32; platform_system=='Windows'"
]
requires-python = ">=3.8"
[project.scripts]
python-code-sandbox = "python_code_sandbox.python_code_sandbox:main"
requires = ["setuptools>=42", "wheel"]— версии инструментов сборки, необходимых для установки пакета;python-code-sandbox = "python_code_sandbox.python_code_sandbox:main"— точка входа, которая будет вызывать функциюmain()из модуляpython_code_sandbox.py
Установка в режиме разработки
Для установки проекта в режиме разработки выполните следующую команду в корневой директории:
pip install -e .
Эта команда устанавливает пакет в редактируемом режиме (-e flag), что означает:
Python будет использовать исходные файлы напрямую из рабочей директории
Изменения в коде сразу будут доступны без повторной установки
Модули будут доступны как полноценные Python-пакеты
Удобная отладка — можно работать с кодом как с обычным проектом, но при этом использовать его как установленный пакет.
Тогда сервер можно запустить выполнением модуля python_code_sandbox.py
Проблема и архитектурное решение
Когда LLM генерирует код на Питоне, перед его выполнением необходимо пройти два этапа валидации:
Синтаксическая проверка — быстрая и безопасная верификация корректности кода без его запуска
Безопасное выполнение — изолированный запуск кода с жёсткими ограничениями по ресурсам и функциональности
Традиционные подходы вроде простого exec() или запуска в отдельном процессе недостаточно безопасны. Злонамеренный код может получить доступ к файловой системе, сетевым ресурсам или исчерпать системные ресурсы. Нужна дополнительная защита.
Архитектура MCP-сервера выглядит следующим образом:
Синтаксический анализатор на основе модуля ast
Система безопасности, сканирующая код на опасные конструкции
Изолированный исполнитель с ограничениями по CPU, памяти и функциям
Кросс-платформенная реализация для Windows и Unix-систем
Этап 1: Проверка синтаксиса
Первый и самый безопасный этап. Используем встроенный модуль ast (Abstract Syntax Tree), который парсит код в древовидную структуру без его выполнения. Это позволяет мгновенно выявить ошибки вроде пропущенных двоеточий, скобок или проблем с отступами.
Модуль: python_code_sandbox/python_code_sandbox.py
Функция: check_syntax(code: str) -> str
def check_syntax(code: str) -> str:
try:
ast.parse(code)
return json.dumps({"valid": True})
except (SyntaxError, IndentationError) as e:
context_lines = code.splitlines()
error_line = ""
if e.lineno and 0 < e.lineno <= len(context_lines):
error_line = context_lines[e.lineno - 1]
return json.dumps({
"valid": False,
"error": str(e).split('(', 1)[0].strip(),
"line": e.lineno,
"offset": e.offset,
"context": error_line.strip() if error_line else ""
})
except Exception as e:
return json.dumps({
"valid": False,
"error": f"Internal syntax checker error: {str(e)}",
"line": None,
"offset": None,
"context": ""
})
Функция check_syntax - первый этап защиты:
Безопасный парсинг через AST:
ast.parse(code)
Модуль ast компилирует Python-код в абстрактное синтаксическое дерево без его выполнения. Для безопасности код никогда не запускается, только анализируется структурно.
2. Обработка синтаксических ошибок: except (SyntaxError, IndentationError) as e:
Отдельно обрабатываем самые частые ошибки:SyntaxError - общие синтаксические ошибки (пропущенные скобки, двоеточия, точки с запятой и т.д.),IndentationError - ошибки в отступах, которые в Python критичны для корректной работы кода
3. Контекст ошибки для удобной отладки:
context_lines = code.splitlines()
if e.lineno and 0 < e.lineno <= len(context_lines):
error_line = context_lines[e.lineno - 1]
Функция не просто сообщает об ошибке, но и предоставляет контекст -- номер строки с ошибкой. Тогда LLM сможет точно понять, где нужно исправить код.
4. Возврат структурированного результата:
return json.dumps({
"valid": False,
"error": str(e).split('(', 1)[0].strip(),
"line": e.lineno,
"offset": e.offset,
"context": error_line.strip() if error_line else ""
})
Результат возвращается в формате JSON с унифицированной структурой:
valid - флаг корректности синтаксиса,error - краткое описание ошибки без технических деталей,line - номер строки (1-индексированный),offset - позиция символа в строке,context - фрагмент кода с ошибкой.
5. Обработка внутренних ошибок:
except Exception as e:
return json.dumps({
"valid": False,
"error": f"Internal syntax checker error: {str(e)}",
"line": None,
"offset": None,
"context": ""
})
На случай непредвиденных ошибок в самом анализаторе, функция возвращает информативное сообщение об ошибке.
Преимущества этого подхода:
Абсолютная безопасность, код не выполняется;
Точная локализация ошибок, получаем номер строки, позицию символа и контекст;
Минимальные накладные расходы, разбор синтаксиса за миллисекунды;
Унифицированный интерфейс, результат в формате JSON, понятном для LLM;
Когда LLM генерирует код, этот этап позволяет быстро вернуть ошибку до попытки запуска, экономя ресурсы и предотвращая потенциальные проблемы.
Этап 2: Безопасное выполнение в песочнице
Если синтаксис корректен, код передаётся в изолированную песочницу, которая кроме формального запуска кода обеспечивает многоуровневую защиту:
Временные файлы vs буфер памяти: выбор метода запуска
При создании песочницы важно решить как передавать код в изолированный процесс. Существует два основных подхода:
Буфер памяти (stdin/аргументы командной строки):
Плюсы: Быстрее (нет операций ввода-вывода), проще реализация;
Минусы: Меньше контроля со стороны ОС, риски экранирования специальных символов, сложнее аудит.
Временные файлы:
Плюсы: Полный контроль со стороны файловой системы, возможность применения ACL и sandboxing на уровне ОС, простой аудит и отладка;
Минусы: Немного медленнее из-за операций ввода-вывода, необходимость управления временными файлами.
Для нашей задачи выбраны временные файлы, несмотря на небольшие накладные расходы. Безопасность важнее производительности при работе с потенциально вредоносным кодом. Когда код записан в файл, операционная система может применить свои встроенные механизмы безопасности:
Модуль: python_code_sandbox/safe_executor.py
Метод: SafeExecutor.run()
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as f:
f.write(sandbox_script)
script_path = f.name
Этот подход даёт несколько критических преимуществ:
Изоляция через файловую систему. Можно установить права доступа только на чтение для процесса.
Аудит в реальном времени. Администратор может проанализировать содержимое файла перед выполнением.
Совместимость с sandboxing-механизмами ОС. Такие системы как AppArmor, SELinux, Windows Sandbox могут применять политики к конкретным файлам.
Отказоустойчивость — даже если процесс завершится аварийно, файл останется для анализа.
Важно гарантировать удаление временных файлов после их выполнения:
finally:
try:
os.unlink(script_path)
except FileNotFoundError:
pass # Файл уже удален
except Exception as e:
logging.warning(f"Could not delete temporary script {script_path}: {e}")
Модуль safe_executor.py
является ядром нашей песочницы и реализует многоуровневую защиту. Разберём его по частям.
1. Импорты и инициализация:
import json
import subprocess
import sys
import textwrap
import platform
import os
from typing import Dict, Any
import tempfile
import logging
log_file = os.path.join(tempfile.gettempdir(), "mcp_sandbox_executor.log")
logging.basicConfig(
filename=log_file,
level=logging.CRITICAL,
format="%(asctime)s - %(levelname)s - %(message)s"
)
IS_UNIX = platform.system() != "Windows"
Логирование перенаправлено в файл, чтобы не мешать MCP-протоколу
Автоматическое определение платформы для кросс-платформенной работы
2. Класс SafeExecutor — основной интерфейс:
class SafeExecutor:
"""Кросс-платформенный запуск кода в изоляции"""
@staticmethod
def run(
code: str,
timeout: float,
cpu_limit_sec: float = 10.0,
memory_limit_mb: int = 100
) -> Dict[str, Any]:
Статический метод для удобства вызова;
Параметры лимитов ресурсов по умолчанию обеспечивают безопасность даже при неправильном вызове.
3. Генерация скрипта песочницы:
sandbox_script = SafeExecutor._generate_sandbox_script(code)
Этот метод создаёт Python-скрипт, который будет выполняться в изолированном процессе. Внутри скрипта реализована вторая линия защиты.
4. Создание временного файла:
как говорилось выше, временные файлы обеспечивают лучшую изоляцию и аудит.
5. Подготовка окружения:
clean_env = os.environ.copy()
clean_env.pop("PYTHONPATH", None)
clean_env["PYTHONUNBUFFERED"] = "1"
Удаляем PYTHONPATH, чтобы предотвратить загрузку внешних модулей;
Устанавливаем PYTHONUNBUFFERED для немедленного вывода результатов.
6. Кросс-платформенная изоляция:
Для Unix (через resource limits):
def preexec_set_limits():
import resource
if cpu_limit_sec > 0:
cpu_sec_int = int(cpu_limit_sec)
resource.setrlimit(resource.RLIMIT_CPU, (cpu_sec_int, cpu_sec_int))
if memory_limit_mb > 0:
mem_bytes = int(memory_limit_mb * 1024 * 1024)
resource.setrlimit(resource.RLIMIT_AS, (mem_bytes, mem_bytes))
process = subprocess.Popen(
[exe, script_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.DEVNULL,
env=clean_env,
text=True,
universal_newlines=True,
preexec_fn=preexec_set_limits
)
RLIMIT_CPU- ограничение процессорного времени;RLIMIT_AS- ограничение виртуальной памяти;preexec_fnвыполняется в дочернем процессе перед запуском кода.
Для Windows (через Job Objects):
try:
import win32job
import win32process
import win32con
job = win32job.CreateJobObject(None, "")
extended_info = win32job.QueryInformationJobObject(job, win32job.JobObjectExtendedLimitInformation)
extended_info['BasicLimitInformation']['LimitFlags'] = (
win32job.JOB_OBJECT_LIMIT_PROCESS_MEMORY |
win32job.JOB_OBJECT_LIMIT_JOB_MEMORY |
win32job.JOB_OBJECT_LIMIT_ACTIVE_PROCESS |
win32job.JOB_OBJECT_LIMIT_PROCESS_TIME
)
# Установка лимитов...
win32job.SetInformationJobObject(job, win32job.JobObjectExtendedLimitInformation, extended_info)
process = subprocess.Popen(
[exe, script_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.DEVNULL,
env=clean_env,
text=True,
universal_newlines=True,
startupinfo=startupinfo,
creationflags=win32process.CREATE_SUSPENDED | subprocess.CREATE_NEW_PROCESS_GROUP
)
win32job.AssignProcessToJobObject(job, process._handle)
win32process.ResumeThread(process._handle)
job_handle = job
Job Objects позволяют ограничивать ресурсы на уровне ядра Windows;
Процесс создаётся приостановленным, затем добавляется в Job Object;
Это обеспечивает более надёжную изоляцию, чем на Unix.
7. Обработка таймаутов:
try:
stdout, stderr = process.communicate(timeout=timeout)
exit_code = process.returncode
except subprocess.TimeoutExpired:
process.kill()
try:
stdout, stderr = process.communicate(timeout=1)
except subprocess.TimeoutExpired:
stdout, stderr = "", "Process killed due to timeout."
return {
"stdout": "",
"stderr": f"Execution timed out after {timeout} seconds",
"exit_code": 124
}
Комбинируем общий таймаут с таймаутом получения вывода;
Принудительно завершаем процесс при превышении времени;
Предоставляем информативное сообщение об ошибке.
8. Генерация скрипта песочницы (вторая линия защиты):
@staticmethod
def _generate_sandbox_script(code: str) -> str:
dangerous_names = [
'__import__', 'eval', 'exec', 'compile',
'getattr', 'setattr', 'globals', 'locals',
'help', 'dir', 'vars', 'breakpoint', 'memoryview'
]
dangerous_modules = [
'subprocess', 'shutil',
'requests', 'urllib', 'pathlib', 'inspect', 'types',
'ctypes', 'pickle', 'marshal', 'builtins',
'resource', 'signal', 'getpass', 'os'
]
return textwrap.dedent(f'''
import sys
# РАННЕЕ ОТКЛЮЧЕНИЕ ОТЛАДЧИКА
sys.settrace(None)
if hasattr(sys, 'gettrace') and sys.gettrace() is not None:
sys.settrace(None)
# Удаляем следы debugpy, если есть
for mod in list(sys.modules):
if mod.startswith(('debugpy', 'pydevd', '_pydev')):
del sys.modules[mod]
# Импортируем необходимые модули
import json
import io
import builtins
# Удаляем опасные модули из sys.modules
for mod in {dangerous_modules!r}:
if mod in sys.modules:
del sys.modules[mod]
# Создаем безопасный словарь встроенных функций
SAFE_BUILTINS = {{
name: getattr(builtins, name)
for name in dir(builtins)
if name not in {dangerous_names!r} and not name.startswith('_')
}}
# Запрещаем импорт
def restricted_import(name, globals=None, locals=None, fromlist=(), level=0):
raise ImportError("All imports disabled in sandbox")
safe_globals = {{
'__builtins__': SAFE_BUILTINS,
'__import__': restricted_import,
}}
# Запрещаем open
def disabled_open(*args, **kwargs):
raise OSError("open() disabled in sandbox")
safe_globals['open'] = disabled_open
# Буферы для перехвата вывода
stdout_buffer = io.StringIO()
stderr_buffer = io.StringIO()
# Перенаправляем print
def safe_print(*args, **kwargs):
kwargs['file'] = stdout_buffer
kwargs['flush'] = True
print(*args, **kwargs)
safe_globals['print'] = safe_print
exit_code = 0
try:
# Выполняем пользовательский код
exec({repr(code)}, safe_globals)
except BaseException as e:
stderr_buffer.write(f"{{type(e).__name__}}: {{e}}")
exit_code = 1
finally:
result = {{
"stdout": stdout_buffer.getvalue(),
"stderr": stderr_buffer.getvalue(),
"exit_code": exit_code
}}
sys.stdout.write(json.dumps(result))
sys.stdout.flush()
''')
Этот скрипт реализует третью линию защиты:
Отключение отладчика предотвращает использование отладочных инструментов для обхода ограничений;
Очищение
sys.modulesудаляет опасные модули из кэша импортов;Безопасные встроенные функции. Cоздаёт ограниченный набор доступных функций;
Запрет импортов переопределяет import для блокировки всех импортов
Запрет файловых операций блокирует функцию
open()Перехват вывода собирает весь
stdout/stderrдля возврата в MCP-сервер.
9. Обработка результатов:
try:
return json.loads(stdout)
except (json.JSONDecodeError, TypeError):
return {
"stdout": stdout,
"stderr": stderr or "Failed to parse sandbox output or sandbox did not return JSON.",
"exit_code": exit_code if exit_code != 0 else 1
}
Пытаемся распарсить JSON-результат из песочницы;
При ошибке возвращаем сырые данные с информативным сообщением;
Гарантируем, что всегда возвращается словарь с ожидаемой структурой.
Модуль safe_executor.py реализует настоящую "матрёшку" изоляции:
Уровень 1: Ограничения ОС (CPU, память, процессы);
Уровень 2: Изолированный процесс с ограниченным окружением;
Уровень 3: Безопасное окружение выполнения внутри процесса.
Такой подход гарантирует, что даже если злоумышленник найдёт способ обойти один уровень защиты, остальные уровни продолжат работать.
Перед запуском код сканируется на наличие опасных конструкций с помощью AST-анализа:
Модуль: python_code_sandbox/python_code_sandbox.py
Класс: SecurityChecker
class SecurityChecker:
"""Проверяет код на опасные конструкции через AST-анализ"""
DANGEROUS_NAMES = {
'open', '__import__', 'eval', 'exec', 'compile',
'getattr', 'setattr', 'globals', 'locals', 'input',
'help', 'dir', 'vars', 'breakpoint', 'memoryview'
}
DANGEROUS_MODULES = {
'os', 'sys', 'subprocess', 'shutil', 'socket',
'requests', 'urllib', 'pathlib', 'inspect', 'types',
'ctypes', 'pickle', 'marshal', 'builtins', 'platform',
'resource', 'signal'
}
@classmethod
def scan(cls, code: str) -> list[str]:
"""Возвращает список нарушений безопасности"""
try:
tree = ast.parse(code)
except SyntaxError:
return ["Syntax error (should have been caught earlier)"]
visitor = cls._ASTVisitor()
visitor.visit(tree)
return visitor.violations
Этот анализ блокирует попытки использовать опасные функции и модули на этапе компиляции, не давая вредоносному коду даже начать выполняться.
Подводные камни и тонкости реализации
При создании такого сервера можно столкнуться с множеством нюансов, о которых стоит рассказать.
Кросс-платформенность: адаптация для Windows
Желательно обеспечить одинаковую функциональность на Windows и Unix. На Unix resource.setrlimit() работает отлично, но Windows требует использования Job Objects через pywin32. Это создаёт зависимость, которую нужно аккуратно обрабатывать:
try:
import win32job
# Полноценная реализация с Job Objects
except ImportError:
logging.error("pywin32 not available on Windows, resource limits cannot be set.")
# Запасной вариант без ограничений
Важно иметь fallback-механизмы и честно предупреждать пользователя о пониженной безопасности.
Обработка таймаутов и прерываний
Когда процесс зависает, обычный timeout в subprocess может оказаться недостаточным. Нужно комбинировать:
Общий таймаут реального времени;
CPU time limit на уровне ОС;
Принудительное завершение процесса и всех его потомков.
except subprocess.TimeoutExpired:
process.kill()
try:
stdout, stderr = process.communicate(timeout=1)
except subprocess.TimeoutExpired:
stdout, stderr = "", "Process killed due to timeout."
Логирование без конфликтов со стандартным выводом
MCP-протокол использует stdin/stdout для обмена сообщениями. Поэтому логирование нужно перенаправлять в файл:
log_file = os.path.join(tempfile.gettempdir(), "mcp_sandbox.log")
logging.basicConfig(
filename=log_file,
level=logging.CRITICAL,
format="%(asctime)s - %(levelname)s - %(message)s"
)
Это предотвращает коллизии и позволяет отлаживать сервер без нарушения протокола обмена.
Безопасность против обхода ограничений
Злонамеренный код может попытаться обойти ограничения через:
Динамическую генерацию кода (compile(), eval())
Прямые системные вызовы через ctypes
Манипуляции с ресурсами через модуль resource
Поэтому в песочнице мы:
Удаляем опасные модули из sys.modules
Переопределяем встроенные функции
Отключаем отладчик и следы отладочных инструментов
Запрещаем импорты на уровне интерпретатора
# РАННЕЕ ОТКЛЮЧЕНИЕ ОТЛАДЧИКА
sys.settrace(None)
if hasattr(sys, 'gettrace') and sys.gettrace() is not None:
sys.settrace(None)
# Удаляем следы debugpy, если есть
for mod in list(sys.modules):
if mod.startswith(('debugpy', 'pydevd', '_pydev')):
del sys.modules[mod]
Интеграция с LM Studio
Для интеграции с LM Studio сервер добавляется в файл mcp.json через графический интерфейс. Пример содержимого файла:
{
"mcpServers": {
"web-search": {
"command": "node",
"args": [
"F:\\MCP Server\\web-search-mcp-v0.3.0\\dist\\index.js"
]
},
"python-code-sandbox": {
"command": "python",
"args": [
"-m",
"python_code_sandbox.python_code_sandbox"
]
}
}
}
Можно заметить, что в этом файле, кроме нашего Python-сервера, также установлен сторонний сервер поиска в интернете, написанный для Node.js. Это показывает огромный потенциал, заложенный в идею MCP-серверов и инструментов LLM, возможность создания экосистемы специализированных сервисов, каждый из которых решает свою конкретную задачу в безопасной и контролируемой среде.
После установки наш сервер автоматически определит платформу и настроит соответствующие механизмы изоляции. Для Windows без pywin32 будут работать базовые ограничения по таймауту, но без жёстких лимитов по CPU и памяти.
Заключение
Создание безопасной песочницы для выполнения кода сгенерированного AI -- задача нетривиальная, но важная для доверия к локальным LLM. Представленный MCP-сервер обеспечивает многоуровневую защиту, начиная от синтаксического анализа и заканчивая жёсткой изоляцией процессов с ограничениями по ресурсам.
Ключевые архитектурные решения, такие как использование временных файлов вместо буферов памяти и установка как локального модуля через pip install -e . делают систему более надёжной и удобной для разработки. Временные файлы обеспечивают лучшую интеграцию с механизмами безопасности операционной системы, а режим разработки позволяет мгновенно видеть результат изменений в коде. Этот подход позволяет спокойно экспериментировать с кодом, сгенерированным локальной нейросетью, не опасаясь за безопасность системы. Возможно не только проверить синтаксис но и безопасно протестировать функциональность кода в контролируемых условиях. Для критически важных систем рекомендуется запускать сгенерированный код в виртуальной машине или в контейнере.
Полный исходный код проекта доступен на GitHub. Сервер может стать полезным инструментом в арсенале разработчика для повседневной работы с локальными LLM.