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 генерирует код на Питоне, перед его выполнением необходимо пройти два этапа валидации:

  1. Синтаксическая проверка — быстрая и безопасная верификация корректности кода без его запуска

  2. Безопасное выполнение — изолированный запуск кода с жёсткими ограничениями по ресурсам и функциональности

Традиционные подходы вроде простого 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 - первый этап защиты:

  1. Безопасный парсинг через 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

Этот подход даёт несколько критических преимуществ:

  1. Изоляция через файловую систему. Можно установить права доступа только на чтение для процесса.

  2. Аудит в реальном времени. Администратор может проанализировать содержимое файла перед выполнением.

  3. Совместимость с sandboxing-механизмами ОС. Такие системы как AppArmor, SELinux, Windows Sandbox могут применять политики к конкретным файлам.

  4. Отказоустойчивость — даже если процесс завершится аварийно, файл останется для анализа.

Важно гарантировать удаление временных файлов после их выполнения:

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.

Комментарии (0)