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.
Источник: habr.com



























