├── src ├── 1c_ext │ ├── DataProcessors │ │ ├── mcp_УправлениеСервером │ │ │ ├── Ext │ │ │ │ ├── ManagerModule.bsl │ │ │ │ └── ObjectModule.bsl │ │ │ └── Forms │ │ │ │ ├── Форма │ │ │ │ └── Ext │ │ │ │ │ └── Form │ │ │ │ │ └── Module.bsl │ │ │ │ └── Форма.xml │ │ ├── mcp_РесурсОписаниеСинтаксисаВстроенногоЯзыка │ │ │ ├── Ext │ │ │ │ └── ManagerModule.bsl │ │ │ └── Templates │ │ │ │ ├── syntax_1c.xml │ │ │ │ └── syntax_1c │ │ │ │ └── Ext │ │ │ │ └── Template.txt │ │ ├── mcp_ИнструментДанныеОКонфигурации.xml │ │ ├── mcp_УправлениеСервером.xml │ │ ├── mcp_РесурсОписаниеСинтаксисаВстроенногоЯзыка.xml │ │ └── mcp_ИнструментДанныеОКонфигурации │ │ │ └── Ext │ │ │ ├── ObjectModule.bsl │ │ │ └── ManagerModule.bsl │ ├── Roles │ │ └── mcp_ОсновнаяРоль.xml │ ├── Languages │ │ └── Русский.xml │ ├── Subsystems │ │ ├── mcp_MCPСервер │ │ │ └── Subsystems │ │ │ │ ├── mcp_КонтейнерыПромптов.xml │ │ │ │ ├── mcp_КонтейнерыИнструментов.xml │ │ │ │ └── mcp_КонтейнерыРесурсов.xml │ │ └── mcp_MCPСервер.xml │ ├── CommonModules │ │ ├── mcp_Содержимое.xml │ │ ├── mcp_Выполнение.xml │ │ ├── mcp_Метаданные.xml │ │ ├── mcp_ОбщегоНазначения.xml │ │ ├── mcp_КонтейнерыПовтИсп │ │ │ └── Ext │ │ │ │ └── Module.bsl │ │ ├── mcp_КонтейнерыПовтИсп.xml │ │ ├── mcp_Выполнение │ │ │ └── Ext │ │ │ │ └── Module.bsl │ │ ├── mcp_Содержимое │ │ │ └── Ext │ │ │ │ └── Module.bsl │ │ ├── mcp_ОбщегоНазначения │ │ │ └── Ext │ │ │ │ └── Module.bsl │ │ └── mcp_Метаданные │ │ │ └── Ext │ │ │ └── Module.bsl │ ├── Configuration.xml │ ├── HTTPServices │ │ ├── mcp_APIBackend.xml │ │ └── mcp_APIBackend │ │ │ └── Ext │ │ │ └── Module.bsl │ └── agents.md └── py_server │ ├── auth │ ├── __init__.py │ └── oauth2.py │ ├── __main__.py │ ├── requirements.txt │ ├── __init__.py │ ├── stdio_server.py │ ├── env.example │ ├── config.py │ ├── main.py │ ├── mcp_server.py │ ├── onec_client.py │ ├── README.md │ └── http_server.py ├── mcp_client_settings ├── kilocode │ └── mcp.json ├── claude_desktop │ └── claude_desktop_config.json └── cursor │ └── mcp.json ├── docker-compose.yml ├── .dockerignore ├── Dockerfile ├── .env.docker.example ├── start_http_server.bat ├── .gitignore └── README.md /src/1c_ext/DataProcessors/mcp_УправлениеСервером/Ext/ManagerModule.bsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/1c_ext/DataProcessors/mcp_УправлениеСервером/Ext/ObjectModule.bsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/py_server/auth/__init__.py: -------------------------------------------------------------------------------- 1 | """Модуль авторизации OAuth2.""" 2 | 3 | from .oauth2 import OAuth2Service, OAuth2Store 4 | 5 | __all__ = ["OAuth2Service", "OAuth2Store"] 6 | 7 | -------------------------------------------------------------------------------- /src/py_server/__main__.py: -------------------------------------------------------------------------------- 1 | """Точка входа для запуска модуля как пакета.""" 2 | 3 | import asyncio 4 | from .main import main 5 | 6 | if __name__ == "__main__": 7 | asyncio.run(main()) -------------------------------------------------------------------------------- /src/py_server/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi>=0.115.0 2 | uvicorn[standard]>=0.30.0 3 | httpx>=0.27.0 4 | pydantic>=2.5.0 5 | pydantic-settings>=2.0.0 6 | python-dotenv>=1.0.0 7 | mcp>=1.8.0 -------------------------------------------------------------------------------- /mcp_client_settings/kilocode/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "1c-server-direct": { 4 | "type": "streamable-http", 5 | "url": "http://localhost/имя_вашей_публикации/hs/mcp/", 6 | "disabled": false, 7 | "alwaysAllow": [] 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/1c_ext/DataProcessors/mcp_УправлениеСервером/Forms/Форма/Ext/Form/Module.bsl: -------------------------------------------------------------------------------- 1 | 2 | &НаСервере 3 | Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка) 4 | 5 | ЗначениеВРеквизитФормы(mcp_КонтейнерыПовтИсп.Инструменты(), "ТаблицаИнструментов"); 6 | ЗначениеВРеквизитФормы(mcp_КонтейнерыПовтИсп.Ресурсы() , "ТаблицаРесурсов"); 7 | ЗначениеВРеквизитФормы(mcp_КонтейнерыПовтИсп.Промпты() , "ТаблицаПромптов"); 8 | 9 | КонецПроцедуры 10 | -------------------------------------------------------------------------------- /src/py_server/__init__.py: -------------------------------------------------------------------------------- 1 | """MCP-прокси сервер для взаимодействия с 1С.""" 2 | 3 | from .config import Config, get_config 4 | from .mcp_server import MCPProxy 5 | from .http_server import run_http_server 6 | from .stdio_server import run_stdio_server 7 | from .onec_client import OneCClient 8 | 9 | __version__ = "1.0.0" 10 | 11 | __all__ = [ 12 | "Config", 13 | "get_config", 14 | "MCPProxy", 15 | "run_http_server", 16 | "run_stdio_server", 17 | "OneCClient" 18 | ] -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mcp-proxy: 3 | image: 1c-mcp-proxy 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | container_name: 1c-mcp-proxy 8 | ports: 9 | - "${MCP_PORT:-8000}:8000" 10 | env_file: 11 | - .env 12 | restart: unless-stopped 13 | healthcheck: 14 | test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8000/health', timeout=5.0).raise_for_status()"] 15 | interval: 30s 16 | timeout: 10s 17 | retries: 3 18 | start_period: 5s 19 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | .gitignore 4 | 5 | # Python 6 | __pycache__ 7 | *.py[cod] 8 | *$py.class 9 | *.so 10 | .Python 11 | venv/ 12 | env/ 13 | ENV/ 14 | *.egg-info 15 | .pytest_cache 16 | .coverage 17 | 18 | # IDE 19 | .vscode 20 | .idea 21 | *.swp 22 | *.swo 23 | 24 | # Docker 25 | Dockerfile 26 | .dockerignore 27 | docker-compose.yml 28 | 29 | # Documentation 30 | *.md 31 | !src/py_server/README.md 32 | 33 | # 1C files 34 | src/1c_ext/ 35 | build/ 36 | 37 | # Settings examples 38 | mcp_client_settings/ 39 | *.bat 40 | .env 41 | .env.* 42 | !.env.docker.example 43 | -------------------------------------------------------------------------------- /mcp_client_settings/claude_desktop/claude_desktop_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "1c-server-stdio": { 4 | "command": "C:\\Python\\python", 5 | "args": ["-m", "src.py_server"], 6 | "cwd": "Y:\\1c_mcp", 7 | "env": { 8 | "PYTHONPATH":"Y:\\1c_mcp", 9 | "MCP_ONEC_URL":"http://192.168.10.202/cg", 10 | "MCP_ONEC_USERNAME":"Администратор", 11 | "MCP_ONEC_PASSWORD":"", 12 | 13 | "MCP_ONEC_SERVICE_ROOT":"mcp", 14 | 15 | "MCP_SERVER_NAME":"1C-Metadata", 16 | "MCP_SERVER_VERSION":"1.0.0" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /mcp_client_settings/cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "1c-server-direct": { 4 | "url": "http://localhost/your_base_name/hs/mcp/" 5 | }, 6 | "1c-server-stdio": { 7 | "command": "d:/rep/1c_mcp/venv/Scripts/python.exe", 8 | "args": ["-m", "src.py_server"], 9 | "cwd": "D:/rep/1c_mcp", 10 | "env": { 11 | "PYTHONPATH":"D:/rep/1c_mcp", 12 | "PYTHONIOENCODING":"utf-8", 13 | 14 | "MCP_ONEC_URL":"http://localhost/cg", 15 | "MCP_ONEC_USERNAME":"Администратор", 16 | "MCP_ONEC_PASSWORD":"", 17 | 18 | "MCP_ONEC_SERVICE_ROOT":"mcp" 19 | } 20 | }, 21 | "1c-server-http": { 22 | "url": "http://127.0.0.1:8000/mcp/" 23 | }, 24 | "1c-server-http-sse": { 25 | "url": "http://127.0.0.1:8000/sse" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/py_server/stdio_server.py: -------------------------------------------------------------------------------- 1 | """Stdio сервер для MCP.""" 2 | 3 | import asyncio 4 | import logging 5 | 6 | import mcp.server.stdio 7 | from .mcp_server import MCPProxy 8 | from .config import Config 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | async def run_stdio_server(config: Config): 15 | """Запуск stdio сервера. 16 | 17 | Args: 18 | config: Конфигурация сервера 19 | """ 20 | logger.info("Запуск MCP сервера в режиме stdio") 21 | 22 | # Создаем прокси 23 | mcp_proxy = MCPProxy(config) 24 | 25 | try: 26 | # Запускаем сервер через stdio 27 | async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): 28 | await mcp_proxy.server.run( 29 | read_stream, 30 | write_stream, 31 | mcp_proxy.get_initialization_options() 32 | ) 33 | except Exception as e: 34 | logger.error(f"Ошибка в stdio сервере: {e}") 35 | raise -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile для 1C MCP Proxy 2 | FROM python:3.13-slim 3 | 4 | # Метаданные 5 | LABEL maintainer="1C MCP Proxy" 6 | LABEL description="MCP-прокси для решения инфраструктурных проблем подключения к MCP-серверу, реализованному в 1С:Предприятие" 7 | 8 | # Рабочая директория 9 | WORKDIR /app 10 | 11 | # Копирование файлов зависимостей 12 | COPY src/py_server/requirements.txt /app/src/py_server/requirements.txt 13 | 14 | # Установка зависимостей 15 | RUN pip install --no-cache-dir -r /app/src/py_server/requirements.txt 16 | 17 | # Копирование исходного кода 18 | COPY src/py_server /app/src/py_server 19 | 20 | # Порт по умолчанию 21 | EXPOSE 8000 22 | 23 | # Healthcheck 24 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ 25 | CMD python -c "import httpx; httpx.get('http://localhost:8000/health', timeout=5.0).raise_for_status()" || exit 1 26 | 27 | # Запуск в HTTP режиме 28 | ENTRYPOINT ["python", "-m", "src.py_server"] 29 | CMD ["http", "--host", "0.0.0.0", "--port", "8000"] 30 | -------------------------------------------------------------------------------- /src/1c_ext/Roles/mcp_ОсновнаяРоль.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mcp_ОсновнаяРоль 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.env.docker.example: -------------------------------------------------------------------------------- 1 | # Конфигурация MCP-прокси для Docker 2 | 3 | # Настройки подключения к 1С (обязательные) 4 | # ВАЖНО: Если 1С на том же хосте, используйте host.docker.internal вместо localhost 5 | # Для Linux: используйте IP хоста (например, 172.17.0.1) или запустите с --network host 6 | MCP_ONEC_URL=http://host.docker.internal/your_base_name 7 | MCP_ONEC_USERNAME=your_username 8 | MCP_ONEC_PASSWORD=your_password 9 | 10 | # Настройки HTTP-сервиса 1С (опциональные) 11 | MCP_ONEC_SERVICE_ROOT=mcp 12 | 13 | # Настройки HTTP-сервера (опциональные) 14 | MCP_HOST=0.0.0.0 15 | MCP_PORT=8000 16 | 17 | # Настройки MCP-сервера (опциональные) 18 | MCP_SERVER_NAME=1C-MCP-Proxy 19 | MCP_SERVER_VERSION=1.0.0 20 | 21 | # Настройки логирования (опциональные) 22 | MCP_LOG_LEVEL=INFO 23 | 24 | # Настройки авторизации OAuth2 (опциональные) 25 | MCP_AUTH_MODE=none 26 | 27 | # Публичный URL прокси для OAuth2 (обязательно при AUTH_MODE=oauth2) 28 | # MCP_PUBLIC_URL=https://your-mcp-proxy.example.com 29 | 30 | # TTL для OAuth2 токенов (опциональные) 31 | MCP_OAUTH2_CODE_TTL=120 32 | MCP_OAUTH2_ACCESS_TTL=3600 33 | MCP_OAUTH2_REFRESH_TTL=1209600 34 | -------------------------------------------------------------------------------- /src/1c_ext/Languages/Русский.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Adopted 7 | Русский 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /start_http_server.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 > nul 3 | setlocal 4 | 5 | echo ======================================== 6 | echo 1C MCP HTTP Server 7 | echo ======================================== 8 | echo. 9 | 10 | REM Проверка наличия Python 11 | python --version >nul 2>&1 12 | if errorlevel 1 ( 13 | echo ❌ Python не найден! Убедитесь, что Python установлен и добавлен в PATH. 14 | pause 15 | exit /b 1 16 | ) 17 | 18 | echo ✅ Python найден: 19 | python --version 20 | 21 | REM Переход в директорию скрипта 22 | cd /d "%~dp0" 23 | 24 | REM Активация виртуального окружения, если оно существует 25 | if exist "venv\Scripts\activate.bat" ( 26 | echo 🐍 Активация виртуального окружения... 27 | call venv\Scripts\activate.bat 28 | ) 29 | 30 | echo. 31 | echo 🚀 Запуск HTTP сервера... 32 | echo ⏹️ Для остановки нажмите Ctrl+C 33 | echo. 34 | echo 📡 Сервер будет доступен на http://127.0.0.1:8000 35 | echo 🏠 Главная страница: http://127.0.0.1:8000/ 36 | echo ❤️ Health check: http://127.0.0.1:8000/health 37 | echo ℹ️ Server info: http://127.0.0.1:8000/info 38 | echo 🔌 SSE для MCP: http://127.0.0.1:8000/sse 39 | echo 🚀 Streamable HTTP для MCP: http://127.0.0.1:8000/mcp 40 | echo. 41 | 42 | REM Запуск сервера 43 | python -m src.py_server http 44 | 45 | echo. 46 | echo ⏹️ Сервер остановлен 47 | pause -------------------------------------------------------------------------------- /src/1c_ext/DataProcessors/mcp_РесурсОписаниеСинтаксисаВстроенногоЯзыка/Ext/ManagerModule.bsl: -------------------------------------------------------------------------------- 1 | #Область ПрограммныйИнтерфейс 2 | 3 | // Добавляет ресурсы в таблицу ресурсов 4 | // 5 | // Параметры: 6 | // ТаблицаРесурсов - ТаблицаЗначений - таблица ресурсов для заполнения 7 | // 8 | Процедура ДобавитьРесурсы(ТаблицаРесурсов) Экспорт 9 | 10 | mcp_Метаданные.ДобавитьРесурс( 11 | ТаблицаРесурсов, 12 | "file://resource/syntax_1c.txt", 13 | "1csyntax", 14 | "Описание синтаксиса встроенного языка 1С:Предприятие в формате Markdown" 15 | ); 16 | 17 | КонецПроцедуры 18 | 19 | // Читает ресурс по адресу 20 | // 21 | // Параметры: 22 | // Адрес - Строка - адрес ресурса 23 | // 24 | // Возвращаемое значение: 25 | // Строка - содержимое ресурса 26 | // 27 | Функция ПрочитатьРесурс(Адрес) Экспорт 28 | 29 | Если СтрНачинаетсяС(Адрес, "file://resource/") Тогда 30 | 31 | ПозицияНачалаИмени = СтрДлина("file://resource/") + 1; 32 | ПозицияНачалаРасширения = СтрНайти(Адрес, "."); 33 | ИмяМакета = Сред(Адрес, ПозицияНачалаИмени, ПозицияНачалаРасширения - ПозицияНачалаИмени); 34 | МакетСинтаксиса = Обработки.mcp_РесурсОписаниеСинтаксисаВстроенногоЯзыка.ПолучитьМакет(ИмяМакета); 35 | Возврат МакетСинтаксиса.ПолучитьТекст(); 36 | 37 | КонецЕсли; 38 | 39 | ВызватьИсключение СтрШаблон("Неизвестный адрес ресурса: %1", Адрес); 40 | 41 | КонецФункции 42 | 43 | #КонецОбласти 44 | -------------------------------------------------------------------------------- /src/py_server/env.example: -------------------------------------------------------------------------------- 1 | # Конфигурация MCP-прокси сервера 2 | 3 | # Настройки подключения к 1С (обязательные) 4 | MCP_ONEC_URL=http://localhost/your_base_name 5 | MCP_ONEC_USERNAME=your_username 6 | MCP_ONEC_PASSWORD=your_password 7 | 8 | # Настройки HTTP-сервиса 1С (опциональные) 9 | MCP_ONEC_SERVICE_ROOT=mcp 10 | 11 | # Настройки HTTP-сервера (опциональные) 12 | MCP_HOST=127.0.0.1 13 | MCP_PORT=8000 14 | 15 | # Настройки MCP-сервера (опциональные) 16 | MCP_SERVER_NAME=1C-MCP-Proxy 17 | MCP_SERVER_VERSION=1.0.0 18 | 19 | # Настройки логирования (опциональные) 20 | MCP_LOG_LEVEL=INFO 21 | 22 | # Настройки CORS (опциональные) 23 | # MCP_CORS_ORIGINS=["http://localhost:3000", "https://yourdomain.com"] 24 | 25 | # Настройки авторизации OAuth2 (опциональные) 26 | # Режим авторизации: none (по умолчанию) или oauth2 27 | MCP_AUTH_MODE=none 28 | 29 | # Публичный URL прокси для OAuth2 (обязательно при AUTH_MODE=oauth2) 30 | # Используется для формирования ссылок в PRM документе и redirect_uri 31 | # MCP_PUBLIC_URL=https://your-mcp-proxy.example.com 32 | 33 | # TTL (время жизни) для OAuth2 токенов в секундах (опциональные) 34 | # Authorization code TTL (по умолчанию 120 секунд = 2 минуты) 35 | MCP_OAUTH2_CODE_TTL=120 36 | 37 | # Access token TTL (по умолчанию 3600 секунд = 1 час) 38 | MCP_OAUTH2_ACCESS_TTL=3600 39 | 40 | # Refresh token TTL (по умолчанию 1209600 секунд = 14 дней) 41 | MCP_OAUTH2_REFRESH_TTL=1209600 -------------------------------------------------------------------------------- /src/1c_ext/DataProcessors/mcp_РесурсОписаниеСинтаксисаВстроенногоЯзыка/Templates/syntax_1c.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | -------------------------------------------------------------------------------- /src/1c_ext/Subsystems/mcp_MCPСервер/Subsystems/mcp_КонтейнерыПромптов.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mcp_КонтейнерыПромптов 6 | 7 | 8 | ru 9 | Контейнеры промптов 10 | 11 | 12 | 13 | true 14 | false 15 | false 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/1c_ext/DataProcessors/mcp_УправлениеСервером/Forms/Форма.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | Форма 6 | 7 | 8 | ru 9 | Форма 10 | 11 | 12 | 13 | Managed 14 | false 15 | 16 | PlatformApplication 17 | MobilePlatformApplication 18 | 19 | 20 | 21 |
22 |
-------------------------------------------------------------------------------- /src/1c_ext/CommonModules/mcp_Содержимое.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mcp_Содержимое 6 | 7 | 8 | ru1 9 | Содержимое 10 | 11 | 12 | 13 | false 14 | false 15 | true 16 | true 17 | false 18 | false 19 | false 20 | DontUse 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/1c_ext/CommonModules/mcp_Выполнение.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mcp_Выполнение 6 | 7 | 8 | ru 9 | Выполнение (MCP) 10 | 11 | 12 | 13 | false 14 | false 15 | true 16 | true 17 | false 18 | false 19 | false 20 | DontUse 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/1c_ext/CommonModules/mcp_Метаданные.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mcp_Метаданные 6 | 7 | 8 | ru 9 | Метаданные (MCP) 10 | 11 | 12 | 13 | false 14 | false 15 | true 16 | true 17 | false 18 | false 19 | false 20 | DontUse 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/1c_ext/CommonModules/mcp_ОбщегоНазначения.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mcp_ОбщегоНазначения 6 | 7 | 8 | ru 9 | Общего назначения (MCP) 10 | 11 | 12 | 13 | false 14 | false 15 | true 16 | true 17 | false 18 | false 19 | false 20 | DontUse 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/1c_ext/CommonModules/mcp_КонтейнерыПовтИсп/Ext/Module.bsl: -------------------------------------------------------------------------------- 1 | #Область ПрограммныйИнтерфейс 2 | 3 | // Возвращает таблицу инструментов (с кешированием) 4 | // 5 | // Возвращаемое значение: 6 | // ТаблицаЗначений - таблица инструментов с колонками: 7 | // * ИмяОбработкиКонтейнера - Строка 8 | // * Имя - Строка 9 | // * Описание - Строка 10 | // * СхемаПараметров - Строка 11 | // 12 | Функция Инструменты() Экспорт 13 | 14 | ТаблицаИнструментов = mcp_Метаданные.ТаблицаИнструментов(); 15 | mcp_Метаданные.ЗаполнитьТаблицуИнструментов(ТаблицаИнструментов); 16 | 17 | Возврат ТаблицаИнструментов; 18 | 19 | КонецФункции 20 | 21 | // Возвращает таблицу ресурсов (с кешированием) 22 | // 23 | // Возвращаемое значение: 24 | // ТаблицаЗначений - таблица ресурсов с колонками: 25 | // * ИмяОбработкиКонтейнера - Строка 26 | // * Адрес - Строка 27 | // * Имя - Строка 28 | // * Описание - Строка 29 | // 30 | Функция Ресурсы() Экспорт 31 | 32 | ТаблицаРесурсов = mcp_Метаданные.ТаблицаРесурсов(); 33 | mcp_Метаданные.ЗаполнитьТаблицуРесурсов(ТаблицаРесурсов); 34 | 35 | Возврат ТаблицаРесурсов; 36 | 37 | КонецФункции 38 | 39 | // Возвращает таблицу промптов (с кешированием) 40 | // 41 | // Возвращаемое значение: 42 | // ТаблицаЗначений - таблица промптов с колонками: 43 | // * ИмяОбработкиКонтейнера - Строка 44 | // * Имя - Строка 45 | // * Описание - Строка 46 | // * Параметры - Строка 47 | // 48 | Функция Промпты() Экспорт 49 | 50 | ТаблицаПромптов = mcp_Метаданные.ТаблицаПромптов(); 51 | mcp_Метаданные.ЗаполнитьТаблицуПромптов(ТаблицаПромптов); 52 | 53 | Возврат ТаблицаПромптов; 54 | 55 | КонецФункции 56 | 57 | #КонецОбласти 58 | 59 | -------------------------------------------------------------------------------- /src/1c_ext/Subsystems/mcp_MCPСервер/Subsystems/mcp_КонтейнерыИнструментов.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mcp_КонтейнерыИнструментов 6 | 7 | 8 | ru 9 | Контейнеры инструментов 10 | 11 | 12 | 13 | true 14 | false 15 | false 16 | 17 | 18 | 19 | DataProcessor.mcp_ИнструментДанныеОКонфигурации 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/1c_ext/Subsystems/mcp_MCPСервер/Subsystems/mcp_КонтейнерыРесурсов.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mcp_КонтейнерыРесурсов 6 | 7 | 8 | ru 9 | Контейнеры ресурсов 10 | 11 | 12 | 13 | true 14 | false 15 | false 16 | 17 | 18 | 19 | DataProcessor.mcp_РесурсОписаниеСинтаксисаВстроенногоЯзыка 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/1c_ext/CommonModules/mcp_КонтейнерыПовтИсп.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mcp_КонтейнерыПовтИсп 6 | 7 | 8 | ru 9 | Контейнеры инструментов, ресурсов, промптов 10 | 11 | 12 | 13 | false 14 | false 15 | true 16 | true 17 | false 18 | false 19 | false 20 | DuringSession 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/1c_ext/Subsystems/mcp_MCPСервер.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mcp_MCPСервер 6 | 7 | 8 | ru 9 | MCP-cервер 10 | 11 | 12 | 13 | true 14 | true 15 | false 16 | 17 | 18 | 19 | HTTPService.mcp_APIBackend 20 | DataProcessor.mcp_УправлениеСервером 21 | 22 | 23 | 24 | mcp_КонтейнерыИнструментов 25 | mcp_КонтейнерыРесурсов 26 | mcp_КонтейнерыПромптов 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/py_server/config.py: -------------------------------------------------------------------------------- 1 | """Конфигурация MCP-прокси сервера.""" 2 | 3 | import os 4 | from typing import Optional, Literal 5 | from pydantic import Field 6 | from pydantic_settings import BaseSettings 7 | 8 | 9 | class Config(BaseSettings): 10 | """Настройки MCP-прокси сервера.""" 11 | 12 | # Настройки сервера 13 | host: str = Field(default="127.0.0.1", description="Хост для HTTP-сервера") 14 | port: int = Field(default=8000, description="Порт для HTTP-сервера") 15 | 16 | # Настройки подключения к 1С 17 | onec_url: str = Field(..., description="URL базы 1С") 18 | onec_username: str = Field(..., description="Имя пользователя 1С") 19 | onec_password: str = Field(..., description="Пароль пользователя 1С") 20 | onec_service_root: str = Field(default="mcp", description="Корневой URL HTTP-сервиса в 1С") 21 | 22 | # Настройки MCP 23 | server_name: str = Field(default="1C Configuration Data Tools", description="Имя MCP-сервера") 24 | server_version: str = Field(default="1.0.0", description="Версия MCP-сервера") 25 | 26 | # Настройки логирования 27 | log_level: str = Field(default="INFO", description="Уровень логирования") 28 | 29 | # Настройки безопасности 30 | cors_origins: list[str] = Field(default=["*"], description="Разрешенные CORS origins") 31 | 32 | # Настройки авторизации OAuth2 33 | auth_mode: Literal["none", "oauth2"] = Field(default="none", description="Режим авторизации: none или oauth2") 34 | public_url: Optional[str] = Field(default=None, description="Публичный URL прокси для OAuth2 (если не задан, формируется из запроса)") 35 | oauth2_code_ttl: int = Field(default=120, description="TTL authorization code в секундах") 36 | oauth2_access_ttl: int = Field(default=3600, description="TTL access token в секундах") 37 | oauth2_refresh_ttl: int = Field(default=1209600, description="TTL refresh token в секундах (14 дней)") 38 | 39 | class Config: 40 | env_file = ".env" 41 | env_prefix = "MCP_" 42 | 43 | 44 | def get_config() -> Config: 45 | """Получить конфигурацию.""" 46 | return Config() -------------------------------------------------------------------------------- /src/1c_ext/CommonModules/mcp_Выполнение/Ext/Module.bsl: -------------------------------------------------------------------------------- 1 | #Область ПрограммныйИнтерфейс 2 | 3 | // Выполняет инструмент из таблицы инструментов 4 | // 5 | // Параметры: 6 | // СтрокаТаблицыИнструментов - СтрокаТаблицыЗначений - строка таблицы инструментов 7 | // Параметры - Структура - параметры для выполнения инструмента 8 | // 9 | // Возвращаемое значение: 10 | // Произвольный - результат выполнения инструмента 11 | // 12 | Функция ВыполнитьИнструмент(СтрокаТаблицыИнструментов, Параметры) Экспорт 13 | 14 | ИмяОбработки = СтрокаТаблицыИнструментов.ИмяОбработкиКонтейнера; 15 | ИмяИнструмента = СтрокаТаблицыИнструментов.Имя; 16 | 17 | МенеджерОбработки = Обработки[ИмяОбработки]; 18 | 19 | Возврат МенеджерОбработки.ВыполнитьИнструмент(ИмяИнструмента, Параметры); 20 | 21 | КонецФункции 22 | 23 | // Читает ресурс из таблицы ресурсов 24 | // 25 | // Параметры: 26 | // СтрокаТаблицыРесурсов - СтрокаТаблицыЗначений - строка таблицы ресурсов 27 | // Адрес - Строка - адрес ресурса 28 | // 29 | // Возвращаемое значение: 30 | // Произвольный - содержимое ресурса 31 | // 32 | Функция ПрочитатьРесурс(СтрокаТаблицыРесурсов, Адрес) Экспорт 33 | 34 | ИмяОбработки = СтрокаТаблицыРесурсов.ИмяОбработкиКонтейнера; 35 | 36 | МенеджерОбработки = Обработки[ИмяОбработки]; 37 | 38 | Возврат МенеджерОбработки.ПрочитатьРесурс(Адрес); 39 | 40 | КонецФункции 41 | 42 | // Получает промпт из таблицы промптов 43 | // 44 | // Параметры: 45 | // СтрокаТаблицыПромптов - СтрокаТаблицыЗначений - строка таблицы промптов 46 | // Имя - Строка - имя промпта 47 | // Параметры - Структура - параметры для получения промпта 48 | // 49 | // Возвращаемое значение: 50 | // Произвольный - содержимое промпта 51 | // 52 | Функция ПолучитьПромпт(СтрокаТаблицыПромптов, Имя, Параметры) Экспорт 53 | 54 | ИмяОбработки = СтрокаТаблицыПромптов.ИмяОбработкиКонтейнера; 55 | ИмяПромпта = СтрокаТаблицыПромптов.Имя; 56 | 57 | МенеджерОбработки = Обработки[ИмяОбработки]; 58 | 59 | Возврат МенеджерОбработки.ПолучитьПромпт(ИмяПромпта, Параметры); 60 | 61 | КонецФункции 62 | 63 | #КонецОбласти 64 | -------------------------------------------------------------------------------- /src/1c_ext/DataProcessors/mcp_ИнструментДанныеОКонфигурации.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9447f8fd-a995-49ab-a533-4d5138400f85 7 | a67c76f3-b972-4f6b-bc76-f3bf709bff1c 8 | 9 | 10 | f2803de5-00ff-45e7-999e-2cee5a341474 11 | 51838fdd-3fb0-4042-9a47-1d548b385091 12 | 13 | 14 | 15 | mcp_ИнструментДанныеОКонфигурации 16 | 17 | 18 | ru 19 | Данные о конфигурации (инструмент) 20 | 21 | 22 | 23 | true 24 | 25 | 26 | false 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/1c_ext/DataProcessors/mcp_УправлениеСервером.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | c09f7fb9-0161-4caa-b47f-0de8e861cefd 7 | b39a5350-907f-4b30-95ce-47f4a9bcbb1b 8 | 9 | 10 | 5277467a-7d45-457a-a40d-b551cc70f61b 11 | 65418df2-831a-4325-8e75-7a741015e060 12 | 13 | 14 | 15 | mcp_УправлениеСервером 16 | 17 | 18 | ru 19 | Управление сервером 20 | 21 | 22 | 23 | true 24 | DataProcessor.mcp_УправлениеСервером.Form.Форма 25 | 26 | false 27 | 28 | 29 | 30 | 31 |
Форма
32 |
33 |
34 |
-------------------------------------------------------------------------------- /src/1c_ext/DataProcessors/mcp_РесурсОписаниеСинтаксисаВстроенногоЯзыка.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 46394879-3a46-4bba-b6ab-f00cab14ac3c 7 | da4d285b-cb0b-4d41-ad17-76efbae451cf 8 | 9 | 10 | f0a606cb-1c10-4347-b58f-e017b23cd926 11 | d547205e-78cd-4d06-ba49-e4642895c7a4 12 | 13 | 14 | 15 | mcp_РесурсОписаниеСинтаксисаВстроенногоЯзыка 16 | 17 | 18 | ru 19 | Описание синтаксиса встроенного языка (ресурс) 20 | 21 | 22 | 23 | true 24 | 25 | 26 | false 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | */__pycache__/ 4 | **/__pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | *.pyc 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | .cursor/ 15 | # build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env* 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # IDEs 136 | .vscode/ 137 | .idea/ 138 | *.swp 139 | *.swo 140 | *~ 141 | 142 | # OS generated files 143 | .DS_Store 144 | .DS_Store? 145 | ._* 146 | .Spotlight-V100 147 | .Trashes 148 | ehthumbs.db 149 | Thumbs.db 150 | 151 | # Windows 152 | *.lnk 153 | 154 | # 1C specific files 155 | *.cf 156 | *.epf 157 | *.erf 158 | *.dt 159 | *.1cd 160 | *.lgf 161 | *.lgp 162 | *.lgd 163 | *.1Cv8.1CD 164 | *.1Cv8.1CL 165 | 166 | # 1C temporary files 167 | *.tmp 168 | *.bak 169 | *.old 170 | 171 | # Configuration files with sensitive data 172 | .env 173 | .env.local 174 | .env.production 175 | .env.staging 176 | config.ini 177 | config.cfg 178 | settings.ini 179 | settings.cfg 180 | 181 | # Logs 182 | *.log 183 | logs/ 184 | log/ 185 | 186 | # Temporary files 187 | temp/ 188 | tmp/ 189 | *.temp 190 | *.tmp 191 | 192 | # Database files 193 | *.db 194 | *.sqlite 195 | *.sqlite3 196 | 197 | # Backup files 198 | *.backup 199 | *.bak 200 | 201 | # Archive files (usually not needed in repo) 202 | *.zip 203 | *.tar.gz 204 | *.rar 205 | *.7z 206 | 207 | # Node.js (if any frontend tools are used) 208 | node_modules/ 209 | npm-debug.log* 210 | yarn-debug.log* 211 | yarn-error.log* 212 | 213 | # Package files 214 | *.deb 215 | *.rpm 216 | *.msi 217 | *.exe 218 | *.dmg 219 | 220 | src/1c_ext/ConfigDumpInfo.xml -------------------------------------------------------------------------------- /src/1c_ext/Configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9cd510cd-abfc-11d4-9434-004095e12fc7 7 | 3b8e6b17-7228-44b5-a965-ed927e018f33 8 | 9 | 10 | 9fcd25a0-4822-11d4-9414-008048da11f9 11 | 2fa2d15d-e72e-4628-bffa-cf8a22779f6e 12 | 13 | 14 | e3687481-0a87-462c-a166-9f34594f9bba 15 | 043cdc90-202b-46cc-890c-e9bcd93b50f7 16 | 17 | 18 | 9de14907-ec23-4a07-96f0-85521cb6b53b 19 | a4298964-e396-44aa-820e-fd1293207b8f 20 | 21 | 22 | 51f2d5d8-ea4d-4064-8892-82951750031e 23 | 65c48c89-48b3-4679-9859-dec14c059e28 24 | 25 | 26 | e68182ea-4237-4383-967f-90c1e3370bc7 27 | fd695762-444f-4bb0-8715-372304e85616 28 | 29 | 30 | fb282519-d103-4dd3-bc12-cb271d631dfc 31 | bd2ec323-f379-41c8-bda5-39dacc283166 32 | 33 | 34 | 35 | Adopted 36 | MCP_Сервер 37 | 38 | 39 | ru 40 | MCP-сервер 41 | 42 | 43 | 44 | AddOn 45 | true 46 | mcp_ 47 | Version8_3_20 48 | ManagedApplication 49 | 50 | PlatformApplication 51 | 52 | Russian 53 | 54 | Role.mcp_ОсновнаяРоль 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | Русский 66 | mcp_MCPСервер 67 | mcp_ОсновнаяРоль 68 | mcp_ОбщегоНазначения 69 | mcp_КонтейнерыПовтИсп 70 | mcp_Метаданные 71 | mcp_Выполнение 72 | mcp_Содержимое 73 | mcp_APIBackend 74 | mcp_УправлениеСервером 75 | mcp_ИнструментДанныеОКонфигурации 76 | mcp_РесурсОписаниеСинтаксисаВстроенногоЯзыка 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/1c_ext/HTTPServices/mcp_APIBackend.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mcp_APIBackend 6 | 7 | 8 | ru 9 | Бэкэнд API для MCP 10 | 11 | 12 | 13 | mcp 14 | AutoUse 15 | 20 16 | 17 | 18 | 19 | 20 | rpc 21 | 22 | 23 | ru 24 | Rpc 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | POST 34 | 35 | 36 | ru 37 | POST 38 | 39 | 40 | 41 | POST 42 | rpcPOST 43 | 44 | 45 | 46 | 47 | 48 | 49 | health 50 | 51 | 52 | ru 53 | Health 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | GET 63 | 64 | 65 | ru 66 | GET 67 | 68 | 69 | 70 | GET 71 | healthGET 72 | 73 | 74 | 75 | 76 | 77 | 78 | mcp 79 | 80 | 81 | ru1 82 | Mcp 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | POST 92 | 93 | 94 | ru1 95 | POST 96 | 97 | 98 | 99 | POST 100 | mcpPOST 101 | 102 | 103 | 104 | 105 | GET 106 | 107 | 108 | ru1 109 | GET 110 | 111 | 112 | 113 | GET 114 | mcpGET 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/1c_ext/CommonModules/mcp_Содержимое/Ext/Module.bsl: -------------------------------------------------------------------------------- 1 | #Область ПрограммныйИнтерфейс 2 | 3 | // Возвращает структуру текстового содержимого для MCP 4 | // 5 | // Параметры: 6 | // Текст - Строка - текстовое содержимое 7 | // 8 | // Возвращаемое значение: 9 | // Структура - содержит поля: 10 | // * type - Строка - "text" 11 | // * text - Строка - текстовое содержимое 12 | // 13 | Функция ТекстовоеСодержимое(Знач Текст) Экспорт 14 | 15 | Результат = Новый Структура; 16 | Результат.Вставить("type", "text"); 17 | Результат.Вставить("text", Строка(Текст)); 18 | 19 | Возврат Результат; 20 | 21 | КонецФункции 22 | 23 | // Возвращает структуру текстового содержимого ресурса для MCP 24 | // 25 | // Параметры: 26 | // Текст - Строка - текстовое содержимое 27 | // MimeТип - Строка - MIME-тип содержимого, по умолчанию "text/plain" 28 | // 29 | // Возвращаемое значение: 30 | // Структура - содержит поля: 31 | // * type - Строка - "text" 32 | // * text - Строка - текстовое содержимое 33 | // * mimeType - Строка - MIME-тип содержимого 34 | // 35 | Функция ТекстовоеСодержимоеРесурса(Знач Текст, Знач MimeТип = "text/plain") Экспорт 36 | 37 | Результат = Новый Структура; 38 | Результат.Вставить("type", "text"); 39 | Результат.Вставить("text", Строка(Текст)); 40 | Результат.Вставить("mimeType", MimeТип); 41 | 42 | Возврат Результат; 43 | 44 | КонецФункции 45 | 46 | // Возвращает структуру содержимого изображения для MCP 47 | // 48 | // Параметры: 49 | // Данные - Строка, ДвоичныеДанные - данные изображения (строка в base64 или двоичные данные) 50 | // MimeТип - Строка - MIME-тип изображения (например, "image/png") 51 | // 52 | // Возвращаемое значение: 53 | // Структура - содержит поля: 54 | // * type - Строка - "image" 55 | // * data - Строка - данные изображения в base64 56 | // * mimeType - Строка - MIME-тип изображения 57 | // 58 | Функция ИзображениеСодержимое(Знач Данные, Знач MimeТип) Экспорт 59 | 60 | ДанныеBase64 = ""; 61 | 62 | Если ТипЗнч(Данные) = Тип("Строка") Тогда 63 | // Данные уже в формате base64 64 | ДанныеBase64 = Данные; 65 | ИначеЕсли ТипЗнч(Данные) = Тип("ДвоичныеДанные") Тогда 66 | // Преобразуем двоичные данные в base64 67 | ДанныеBase64 = Base64Строка(Данные); 68 | Иначе 69 | ВызватьИсключение СтрШаблон("Неподдерживаемый тип данных для изображения: %1. Ожидается Строка или ДвоичныеДанные", ТипЗнч(Данные)); 70 | КонецЕсли; 71 | 72 | Результат = Новый Структура; 73 | Результат.Вставить("type", "image"); 74 | Результат.Вставить("data", ДанныеBase64); 75 | Результат.Вставить("mimeType", MimeТип); 76 | 77 | Возврат Результат; 78 | 79 | КонецФункции 80 | 81 | // Возвращает структуру двоичного содержимого для MCP 82 | // 83 | // Параметры: 84 | // Данные - Строка, ДвоичныеДанные - двоичные данные (строка в base64 или двоичные данные) 85 | // MimeТип - Строка - MIME-тип содержимого 86 | // 87 | // Возвращаемое значение: 88 | // Структура - содержит поля: 89 | // * type - Строка - "blob" 90 | // * blob - Строка - двоичные данные в base64 91 | // * mimeType - Строка - MIME-тип содержимого 92 | // 93 | Функция ДвоичноеСодержимое(Знач Данные, Знач MimeТип) Экспорт 94 | 95 | ДанныеBase64 = ""; 96 | 97 | Если ТипЗнч(Данные) = Тип("Строка") Тогда 98 | // Данные уже в формате base64 99 | ДанныеBase64 = Данные; 100 | ИначеЕсли ТипЗнч(Данные) = Тип("ДвоичныеДанные") Тогда 101 | // Преобразуем двоичные данные в base64 102 | ДанныеBase64 = Base64Строка(Данные); 103 | Иначе 104 | ВызватьИсключение СтрШаблон("Неподдерживаемый тип данных для двоичного содержимого: %1. Ожидается Строка или ДвоичныеДанные", ТипЗнч(Данные)); 105 | КонецЕсли; 106 | 107 | Результат = Новый Структура; 108 | Результат.Вставить("type", "blob"); 109 | Результат.Вставить("blob", ДанныеBase64); 110 | Результат.Вставить("mimeType", MimeТип); 111 | 112 | Возврат Результат; 113 | 114 | КонецФункции 115 | 116 | // Возвращает структуру сообщения промпта для MCP 117 | // 118 | // Параметры: 119 | // Роль - Строка - роль отправителя сообщения ("user", "assistant", "system") 120 | // Текст - Строка - текст сообщения 121 | // 122 | // Возвращаемое значение: 123 | // Структура - содержит поля: 124 | // * role - Строка - роль отправителя 125 | // * content - Структура - содержимое сообщения с полями type и text 126 | // 127 | Функция СообщениеПромпта(Знач Роль, Знач Текст) Экспорт 128 | 129 | СодержимоеСообщения = Новый Структура; 130 | СодержимоеСообщения.Вставить("type", "text"); 131 | СодержимоеСообщения.Вставить("text", Строка(Текст)); 132 | 133 | Результат = Новый Структура; 134 | Результат.Вставить("role", Роль); 135 | Результат.Вставить("content", СодержимоеСообщения); 136 | 137 | Возврат Результат; 138 | 139 | КонецФункции 140 | 141 | #КонецОбласти 142 | -------------------------------------------------------------------------------- /src/1c_ext/DataProcessors/mcp_РесурсОписаниеСинтаксисаВстроенногоЯзыка/Templates/syntax_1c/Ext/Template.txt: -------------------------------------------------------------------------------- 1 | ### Синтаксис встроенного языка **1С:Предприятие** 2 | 3 | > Ключевые слова не чувствительны к регистру и почти всегда имеют русскую **и** английскую форму. Ниже в примерах показан только русский вариант, но английские синонимы работают так же. 4 | 5 | --- 6 | 7 | ## Комментарии 8 | 9 | ```1c 10 | // однострочный комментарий 11 | А = 5; // комментарий после кода 12 | ``` 13 | 14 | --- 15 | 16 | ## Примитивные типы и литералы 17 | 18 | | Тип | Литерал(ы) | Пример | | 19 | | -------------- | ---------------------------------------------- | ----------------------- | -------------------------- | 20 | | `NULL` | `NULL` | `А = NULL;` | | 21 | | `Неопределено` | `Неопределено` / `Undefined` | `А = Неопределено;` | | 22 | | `Булево` | `Истина`, `Ложь` | `Флаг = Истина;` | | 23 | | `Число` | десятичные, `.` как разделитель | `Сумма = -1234.56;` | | 24 | | `Дата` | `'ГГГГММДДччммсс'`, допускаются сокращения | `ДатаДок = '20250101';` | | 25 | | `Строка` | `"…"`, поддерживает многострочный синтаксис \` | \` | `Имя = "ООО ""Василек""";` | 26 | 27 | --- 28 | 29 | ## Объявление переменных 30 | 31 | ```1c 32 | Перем ГлобальнаяПеременная Экспорт; // видна из других модулей 33 | Перем Лок1, Лок2; // две переменные модуля 34 | ``` 35 | 36 | *Первое появление имени слева от `=` тоже создает переменную.* 37 | 38 | --- 39 | 40 | ## Оператор присваивания 41 | 42 | ```1c 43 | Перем А; 44 | А = 10 + 5; 45 | ``` 46 | 47 | --- 48 | 49 | ## Выражения и операторы 50 | 51 | ### Арифметика 52 | 53 | `+ − * / %` 54 | 55 | ### Конкатенация строк 56 | 57 | ```1c 58 | ФИО = Фамилия + " " + Имя; 59 | ``` 60 | 61 | ### Сравнение / логика 62 | 63 | ``` 64 | > >= < <= = <> // сравнение 65 | И ИЛИ НЕ // AND OR NOT 66 | ``` 67 | 68 | ### Тернарный «короткий IF» 69 | 70 | ```1c 71 | Статус = ?(Скидка > 10, "VIP", "Обычный"); 72 | ``` 73 | 74 | --- 75 | 76 | ## Управляющие конструкции 77 | 78 | ### Условие 79 | 80 | ```1c 81 | Если Цена > 0 Тогда 82 | Сообщить("Ок"); 83 | ИначеЕсли Цена = 0 Тогда 84 | Сообщить("Бесплатно"); 85 | Иначе 86 | Сообщить("Отрицательная цена?!"); 87 | КонецЕсли 88 | ``` 89 | 90 | ### Циклы 91 | 92 | ```1c 93 | // Для … По … 94 | Для Сч = 1 По 10 Цикл 95 | Сообщить(Сч); 96 | КонецЦикла; 97 | 98 | // Для каждого … Из … 99 | Для каждого Стр Из Таблица Цикл 100 | // … 101 | КонецЦикла; 102 | 103 | // Пока … 104 | Пока Сч < 100 Цикл 105 | Сч = Сч * 2; 106 | КонецЦикла; 107 | ``` 108 | 109 | `Прервать;` — выйти из цикла, `Продолжить;` — к следующей итерации. 110 | 111 | --- 112 | 113 | ## Процедуры и функции 114 | 115 | ### Объявление процедуры 116 | 117 | ```1c 118 | Процедура ОбработатьДок(Документ, Режим = "Просмотр") Экспорт 119 | // тело 120 | КонецПроцедуры 121 | ``` 122 | 123 | ### Объявление функции 124 | 125 | ```1c 126 | Функция ПолучитьСумму(Число1, Знач Число2 = 0) Экспорт 127 | Возврат Число1 + Число2; 128 | КонецФункции 129 | ``` 130 | 131 | *`Знач` — передача по значению; без него — по ссылке.* 132 | 133 | --- 134 | 135 | ## Исключения 136 | 137 | ```1c 138 | Попытка 139 | Результат = 1 / 0; 140 | Исключение 141 | Предупреждение("Деление на ноль!"); 142 | КонецПопытки; 143 | ``` 144 | 145 | Принудительный выброс: 146 | 147 | ```1c 148 | ВызватьИсключение "Ошибка бизнес-логики"; 149 | ``` 150 | 151 | --- 152 | 153 | ## Создание объектов 154 | 155 | ```1c 156 | Массив = Новый Массив(3); // встроенные коллекции 157 | Док = Новый(Тип("Документ.Счет")); // прикладные объекты через второй синтаксис 158 | ``` 159 | 160 | --- 161 | 162 | ## Доступ к свойствам 163 | 164 | ```1c 165 | Имя = Контрагент.Наименование; // точка 166 | Имя = Контрагент["Наименование"]; // по строке 167 | ``` 168 | 169 | --- 170 | 171 | ## Метки и переход 172 | 173 | ```1c 174 | Перейти ~МеткаКонца; 175 | // … 176 | ~МеткаКонца: Сообщить("Перешли"); 177 | ``` 178 | 179 | --- 180 | 181 | ## Препроцессор и директивы компиляции 182 | 183 | ### Инструкции препроцессора 184 | 185 | ```1c 186 | #Если ВебКлиент Тогда 187 | // код только для web-клиента 188 | #Иначе 189 | // остальная режимы работы платформы 190 | #КонецЕсли 191 | ``` 192 | 193 | ### Директивы перед методами/переменными 194 | 195 | ```1c 196 | &НаКлиенте 197 | Процедура ОткрытьФорму() … КонецПроцедуры 198 | 199 | &НаСервереБезКонтекста 200 | Функция Сервисная() … КонецФункции 201 | ``` 202 | 203 | *Возможные метки: `НаКлиенте`, `НаСервере`, `НаСервереБезКонтекста`, `НаКлиентеНаСервереБезКонтекста`.* 204 | 205 | --- 206 | 207 | ## Коллекция Массив и доступ по индексу 208 | 209 | ```1c 210 | Массив = Новый Массив; // пустой 211 | Массив2 = Новый Массив(10); // сразу 10 “пустых” ячеек 212 | Первый = Массив[0]; // индексы с 0 213 | Для каждого Элемент Из Массив Цикл … КонецЦикла; 214 | ``` 215 | 216 | --- 217 | 218 | -------------------------------------------------------------------------------- /src/1c_ext/DataProcessors/mcp_ИнструментДанныеОКонфигурации/Ext/ObjectModule.bsl: -------------------------------------------------------------------------------- 1 | #Область Переменные 2 | 3 | Перем ЯзыкСинтаксиса; 4 | 5 | #КонецОбласти 6 | 7 | #Область ПрограммныйИнтерфейс 8 | 9 | Функция ОписаниеСтруктурыОбъектаМетаданных(МетаданныеОбъекта) Экспорт 10 | 11 | ЯзыкСинтаксиса = ?( 12 | Метаданные.ВариантВстроенногоЯзыка = Метаданные.СвойстваОбъектов.ВариантВстроенногоЯзыка.Английский, 13 | "en", 14 | "ru"); 15 | 16 | МассивСтрок = Новый Массив; 17 | 18 | // Заголовок объекта 19 | МетаТип = СтрРазделить(МетаданныеОбъекта.ПолноеИмя(), ".")[0]; 20 | ВывестиЗаголовокОбъектаМетаданных(МассивСтрок, МетаданныеОбъекта, МетаТип); 21 | 22 | СтруктураСтандартныеРеквизиты = Новый Структура("СтандартныеРеквизиты"); 23 | ЗаполнитьЗначенияСвойств(СтруктураСтандартныеРеквизиты, МетаданныеОбъекта); 24 | Если СтруктураСтандартныеРеквизиты.СтандартныеРеквизиты <> Неопределено Тогда 25 | ВывестиСписокРеквизитовМетаданных(МассивСтрок, МетаданныеОбъекта.СтандартныеРеквизиты, НСтр("ru='Стандартные реквизиты';en='Standard attributes'")); 26 | КонецЕсли; 27 | 28 | // Обработка измерений и ресурсов для регистров 29 | ЭтоРегистр = ЭтоРегистрМД(МетаданныеОбъекта); 30 | Если ЭтоРегистр Тогда 31 | ВывестиСписокРеквизитовМетаданных(МассивСтрок, МетаданныеОбъекта.Измерения, НСтр("ru='Измерения';en='Dimensions'")); 32 | ВывестиСписокРеквизитовМетаданных(МассивСтрок, МетаданныеОбъекта.Ресурсы, НСтр("ru='Ресурсы';en='Resources'")); 33 | КонецЕсли; 34 | 35 | ВывестиСписокРеквизитовМетаданных(МассивСтрок, МетаданныеОбъекта.Реквизиты, НСтр("ru='Реквизиты';en='Attributes'")); 36 | 37 | Если Метаданные.Справочники.Содержит(МетаданныеОбъекта) И МетаданныеОбъекта.Владельцы.Количество() > 0 Тогда 38 | ВывестиВладельцевМетаданных(МассивСтрок, МетаданныеОбъекта); 39 | КонецЕсли; 40 | 41 | Если Не ЭтоРегистр Тогда 42 | ВывестиТабличныеЧастиМетаданных(МассивСтрок, МетаданныеОбъекта.ТабличныеЧасти); 43 | КонецЕсли; 44 | 45 | // Объединяем строки через символ перевода строки 46 | Результат = СтрСоединить(МассивСтрок, Символы.ПС); 47 | 48 | Возврат Результат; 49 | 50 | КонецФункции 51 | 52 | #КонецОбласти 53 | 54 | #Область СлужебныеПроцедурыИФункции 55 | 56 | #Область ИнструментСтруктураОбъекта 57 | 58 | Функция ЭтоРегистрМД(МетаданныеОбъекта) 59 | 60 | Возврат Метаданные.РегистрыБухгалтерии.Содержит(МетаданныеОбъекта) 61 | ИЛИ Метаданные.РегистрыНакопления.Содержит(МетаданныеОбъекта) 62 | ИЛИ Метаданные.РегистрыРасчета.Содержит(МетаданныеОбъекта) 63 | ИЛИ Метаданные.РегистрыСведений.Содержит(МетаданныеОбъекта); 64 | 65 | КонецФункции 66 | 67 | Функция ПолучитьСтрокуТипаМетаданных(ТипРеквизита) 68 | 69 | МассивСтрокТипов = Новый Массив; 70 | 71 | Для Каждого Тип Из ТипРеквизита.Типы() Цикл 72 | ТипМД = Метаданные.НайтиПоТипу(Тип); 73 | 74 | Если ТипМД = Неопределено Тогда 75 | МассивСтрокТипов.Добавить(Строка(Тип)); 76 | Иначе 77 | СтрокаТипа = ТипМД.ПолноеИмя(); 78 | 79 | Если Метаданные.Перечисления.Содержит(ТипМД) Тогда 80 | МассивЗначений = Новый Массив; 81 | Счетчик = 0; 82 | Для Каждого ЗначениеПеречисления Из ТипМД.ЗначенияПеречисления Цикл 83 | Если Счетчик < 10 Тогда 84 | МассивЗначений.Добавить(ЗначениеПеречисления.Имя); 85 | Счетчик = Счетчик + 1; 86 | Иначе 87 | МассивЗначений.Добавить("..."); 88 | Прервать; 89 | КонецЕсли; 90 | КонецЦикла; 91 | 92 | Если МассивЗначений.Количество() > 0 Тогда 93 | СтрокаЗначений = СтрСоединить(МассивЗначений, ", "); 94 | СтрокаТипа = СтрокаТипа + " (" + СтрокаЗначений + ")"; 95 | КонецЕсли; 96 | КонецЕсли; 97 | МассивСтрокТипов.Добавить(СтрокаТипа); 98 | КонецЕсли; 99 | КонецЦикла; 100 | 101 | Возврат СтрСоединить(МассивСтрокТипов, ", "); 102 | 103 | КонецФункции 104 | 105 | Процедура ВывестиЗаголовокОбъектаМетаданных(МассивСтрок, МетаданныеОбъекта, МетаТип) 106 | 107 | МассивСтрок.Добавить(НСтр("ru='Структура объекта ';en='Object structure '") + МетаТип + "." + МетаданныеОбъекта.Имя + ":"); 108 | МассивСтрок.Добавить(НСтр("ru='Синоним: ';en='Synonym: '") + """" + МетаданныеОбъекта.Синоним + """"); 109 | МассивСтрок.Добавить(""); 110 | 111 | КонецПроцедуры 112 | 113 | Процедура ВывестиСписокРеквизитовМетаданных(МассивСтрок, Реквизиты, ИмяРаздела) 114 | 115 | Если Реквизиты.Количество() > 0 Тогда 116 | МассивСтрок.Добавить(ИмяРаздела + ":"); 117 | 118 | Для каждого Реквизит Из Реквизиты Цикл 119 | ТипРекв_Стр = ПолучитьСтрокуТипаМетаданных(Реквизит.Тип); 120 | МассивСтрок.Добавить(Символы.Таб + Реквизит.Имя + " - " + ТипРекв_Стр + " - """ + Реквизит.Синоним + """"); 121 | КонецЦикла; 122 | 123 | МассивСтрок.Добавить(""); 124 | КонецЕсли; 125 | 126 | КонецПроцедуры 127 | 128 | Процедура ВывестиТабличныеЧастиМетаданных(МассивСтрок, ТабличныеЧасти) 129 | 130 | Если ТабличныеЧасти.Количество() > 0 Тогда 131 | МассивСтрок.Добавить(НСтр("ru='Табличные части:';en='Tabular sections:'")); 132 | 133 | Для каждого ТабЧасть Из ТабличныеЧасти Цикл 134 | МассивСтрок.Добавить(Символы.Таб + НСтр("ru='ТЧ ';en='TS '") + """" + ТабЧасть.Имя + """ - """ + ТабЧасть.Синоним + """:"); 135 | 136 | Для каждого Реквизит Из ТабЧасть.Реквизиты Цикл 137 | ТипРекв_Стр = ПолучитьСтрокуТипаМетаданных(Реквизит.Тип); 138 | МассивСтрок.Добавить(Символы.Таб + Символы.Таб + Реквизит.Имя + " - " + ТипРекв_Стр + " - """ + Реквизит.Синоним + """"); 139 | КонецЦикла; 140 | КонецЦикла; 141 | 142 | МассивСтрок.Добавить(""); 143 | КонецЕсли; 144 | 145 | КонецПроцедуры 146 | 147 | Процедура ВывестиВладельцевМетаданных(МассивСтрок, МетаданныеОбъекта) 148 | 149 | МассивСтрок.Добавить(НСтр("ru='Владельцы:';en='Owners:'")); 150 | 151 | Для каждого Владелец Из МетаданныеОбъекта.Владельцы Цикл 152 | МассивСтрок.Добавить(Символы.Таб + Владелец.ПолноеИмя()); 153 | КонецЦикла; 154 | 155 | МассивСтрок.Добавить(""); 156 | 157 | КонецПроцедуры 158 | 159 | #КонецОбласти 160 | 161 | #КонецОбласти 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Разработка MCP-серверов в 1С 2 | 3 | Инструмент для создания MCP (Model Context Protocol) серверов на платформе 1С:Предприятие. Позволяет разрабатывать расширения 1С, которые предоставляют данные и функциональность базы для AI-ассистентов (чаты с языковыми моделями, Claude Desktop, Cursor и других AI-клиентов). 4 | 5 | Подробное видео о проекте и его возможностях: 6 | [VK Видео](https://vkvideo.ru/video-219359576_456239024) | [Youtube](https://youtu.be/ZEla85JsfCo) | [Rutube](https://rutube.ru/video/ba1c64432d1a5b6cfd2335b83bf47071/) 7 | 8 | Проект содержит готовое расширение для 1С, которое берет на себя всю техническую рутину протокола MCP. Вам остается только реализовать бизнес-логику ваших инструментов. Инструменты для работы с метаданными конфигурации 1С доступны "из коробки". 9 | 10 | ## Как это работает: Концепция MCP 11 | 12 | Качество ответа языковой модели (LLM) напрямую зависит от качества предоставленного ей контекста. Подготовка такого контекста вручную может быть трудоемкой. 13 | 14 | **Model Context Protocol (MCP)** — это открытый стандарт, который позволяет модели самой запрашивать необходимые данные через специальные "инструменты" (tools), предоставляемые вашим сервером. Таким образом, контекст для решения задачи собирается автоматически. 15 | 16 | ## Компоненты проекта 17 | 18 | 1. **`src/1c_ext/`** - **Расширение 1С:** Ядро решения. Реализует MCP-сервер и инструменты. 19 | 2. **`src/py_server/`** - **Python-прокси:** Опциональный, но рекомендуемый компонент для решения инфраструктурных задач. 20 | 21 | ## Быстрый старт 22 | 23 | ### Шаг 1: Установка расширения 1С 24 | 25 | Подключите готовое, собранное расширение из каталога `build/MCP_Сервер.cfe` в вашу конфигурацию. 26 | 27 | ### Шаг 2: Публикация HTTP-сервиса 28 | 29 | Опубликуйте на веб-сервере HTTP-сервис `mcp_APIBackend`, который находится в расширении. 30 | 31 | > **Важно:** Для прямого подключения AI-клиента к 1С (без прокси) требуется публиковать базу без необходимости аутентификации ("вшить" реквизиты доступа к базе в default.vrd), что является небезопасным. Решение этой проблемы описано в разделе "Варианты подключения". 32 | 33 | ### Шаг 3: Подключение AI-клиента 34 | 35 | Подключите MCP-клиент (например, Cursor) к опубликованному HTTP-сервису (`.../ваша_база/hs/mcp/`). Примеры настроек для разных клиентов находятся в папке [`mcp_client_settings/`](./mcp_client_settings/). 36 | 37 | ## Варианты подключения 38 | 39 | ### Вариант 1: Прямое подключение к 1С 40 | 41 | - **Как работает:** `AI-клиент ←→ HTTP-сервис 1С` 42 | - **Ограничения:** 43 | - Требует публикации HTTP-сервиса без аутентификации 1С. 44 | - Невозможно подключить клиенты, требующие транспорт `stdio`. 45 | - Ограниченная совместимость с некоторыми HTTP-клиентами из-за нюансов протокола. 46 | 47 | ### Вариант 2: Подключение через Python-прокси (Рекомендуется) 48 | 49 | - **Как работает:** `AI-клиент ←→ MCP-прокси (Python) ←→ HTTP-сервис 1С` 50 | - **Зачем он нужен?** Прокси решает ключевые инфраструктурные проблемы: 51 | - **Проблема транспорта:** Позволяет подключать клиенты, работающие по `stdio`. 52 | - **Проблема аутентификации:** Реализует протокол `OAuth2` (стандартный способ аутентификации в MCP) и передает авторизацию в 1С через `Basic Auth`. Это позволяет **не отключать** аутентификацию 1С на веб-сервере. Прокси поддерживает два режима: работа от имени одного фиксированного пользователя или "проброс" аутентификации каждого пользователя под его собственными учетными данными. 53 | 54 | - **Настройка и запуск прокси:** 55 | - Детальная инструкция по требованиям, установке, настройке и запуску находится в [документации Python-прокси](./src/py_server/README.md). 56 | 57 | ### Вариант 3: Запуск прокси в Docker 58 | 59 | Для изолированного запуска прокси-сервера в контейнере: 60 | 61 | ```bash 62 | # Скопируйте пример конфигурации 63 | cp .env.docker.example .env 64 | 65 | # Отредактируйте .env (укажите URL, логин, пароль) 66 | # Запустите контейнер 67 | docker-compose up -d 68 | ``` 69 | 70 | > **Важно:** Если 1С на том же хосте, используйте `host.docker.internal` вместо `localhost` в `MCP_ONEC_URL`. 71 | 72 | Подробнее в [документации Python-прокси](./src/py_server/README.md#docker). 73 | 74 | ## Разработка собственных инструментов 75 | 76 | Функциональность расширяется добавлением собственных инструментов для взаимодействия с данными 1С. 77 | 78 | - **Шаг 1:** В расширении создайте новую обработку и включите ее в подсистему `mcp_КонтейнерыИнструментов`. 79 | 80 | - **Шаг 2:** В модуле менеджера этой обработки реализуйте два экспортных метода: 81 | 82 | ```bsl 83 | // Метод для описания инструментов и их параметров 84 | Процедура ДобавитьИнструменты(Инструменты) Экспорт 85 | // ... здесь вы описываете, какие инструменты предоставляет ваша обработка 86 | КонецПроцедуры 87 | 88 | // Метод для выполнения логики инструмента 89 | Функция ВыполнитьИнструмент(ИмяИнструмента, Аргументы) Экспорт 90 | // ... здесь вы реализуете логику, которая будет вызвана AI-моделью 91 | КонецФункции 92 | ``` 93 | 94 | Подробное руководство по разработке находится в файле [`src/1c_ext/agents.md`](./src/1c_ext/agents.md). 95 | 96 | ## Другие примитивы MCP 97 | 98 | Помимо **Инструментов (Tools)**, проект также поддерживает **Ресурсы (Resources)** для предоставления статического контекста (например, файлов) и **Промпты (Prompts)** для заготовленных шаблонов сообщений. 99 | 100 | ## Документация 101 | 102 | - **[Документация Python-прокси](./src/py_server/README.md)** — детальное описание настройки и запуска прокси-сервера. 103 | - **[Руководство по разработке в 1С](./src/1c_ext/agents.md)** — подробности реализации инструментов, ресурсов и промптов на стороне 1С. Можно подключать к генераторам кода (AI-агентам). 104 | - **[Примеры настроек клиентов](./mcp_client_settings/)** — готовые конфигурации для разных AI-клиентов. 105 | 106 | ## Лицензия 107 | 108 | MIT License 109 | 110 | ## Поддержка и развитие 111 | 112 | Проект активно развивается. Вопросы и предложения по улучшению приветствуются через Issues. -------------------------------------------------------------------------------- /src/1c_ext/DataProcessors/mcp_ИнструментДанныеОКонфигурации/Ext/ManagerModule.bsl: -------------------------------------------------------------------------------- 1 | Процедура ДобавитьИнструменты(Инструменты) Экспорт 2 | 3 | ДобавитьИнструментСписокМетаданных(Инструменты); 4 | ДобавитьИнструментСтруктураОбъекта(Инструменты); 5 | 6 | КонецПроцедуры 7 | 8 | Функция ВыполнитьИнструмент(ИмяИнструмента, Аргументы) Экспорт 9 | 10 | Если ИмяИнструмента = "list_metadata_objects" Тогда 11 | Возврат СписокМетаданных(Аргументы); 12 | ИначеЕсли ИмяИнструмента = "get_metadata_structure" Тогда 13 | Возврат СтруктураОбъектаМетаданных(Аргументы); 14 | Иначе 15 | ВызватьИсключение "Неизвестный инструмент: " + ИмяИнструмента; 16 | КонецЕсли; 17 | 18 | КонецФункции 19 | 20 | #Область ИнструментСписокМетаданных 21 | 22 | Процедура ДобавитьИнструментСписокМетаданных(Инструменты) 23 | 24 | // Создаем описания параметров для инструмента list_metadata_objects 25 | МассивПараметров = Новый Массив; 26 | 27 | // Параметр metaType - тип объекта метаданных 28 | СписокТиповМетаданных = "Catalogs,Documents,InformationRegisters,AccumulationRegisters,AccountingRegisters,CalculationRegisters,ChartsOfCharacteristicTypes,ChartsOfAccounts,ChartsOfCalculationTypes,BusinessProcesses,Tasks,ExchangePlans,FilterCriteria,Reports,DataProcessors,Enums,CommonModules,SessionParameters,CommonTemplates,CommonPictures,XDTOPackages,WebServices,HTTPServices,WSReferences,Styles,Languages,FunctionalOptions,FunctionalOptionsParameters,DefinedTypes,CommonAttributes,CommonCommands,CommandGroups,Constants,CommonForms,Roles,Subsystems,EventSubscriptions,ScheduledJobs,SettingsStorages,Sequences,DocumentJournals,ExternalDataSources,Interfaces"; 29 | 30 | МассивПараметров.Добавить(mcp_Метаданные.ПараметрИнструмента( 31 | "metaType", 32 | "string", 33 | "Тип объекта метаданных", 34 | , 35 | Истина, 36 | СписокТиповМетаданных 37 | )); 38 | 39 | // Параметр nameMask - маска имени в формате регулярного выражения 40 | МассивПараметров.Добавить(mcp_Метаданные.ПараметрИнструмента( 41 | "nameMask", 42 | "string", 43 | "Маска имени объекта. Проверяется на вхождение подстроки в имя или синоним объекта." 44 | )); 45 | 46 | // Параметр maxItems - максимальное количество возвращаемых результатов 47 | МассивПараметров.Добавить(mcp_Метаданные.ПараметрИнструмента( 48 | "maxItems", 49 | "number", 50 | "Максимальное количество возвращаемых результатов", 51 | 100 52 | )); 53 | 54 | // Создаем JSON-схему параметров 55 | СхемаПараметров = mcp_Метаданные.СхемаПараметровИнструмента(МассивПараметров); 56 | 57 | // Добавляем инструмент в таблицу 58 | mcp_Метаданные.ДобавитьИнструмент( 59 | Инструменты, 60 | "list_metadata_objects", 61 | "Получение списка объектов метаданных конфигурации с возможностью фильтрации по типу и имени", 62 | СхемаПараметров 63 | ); 64 | 65 | КонецПроцедуры 66 | 67 | Функция СписокМетаданных(Аргументы) 68 | 69 | // Значения по умолчанию 70 | МетаТип = Аргументы.metaType; 71 | МаскаИмени = ?(Аргументы.Свойство("nameMask"), Аргументы.nameMask, ""); 72 | Лимит = ?(Аргументы.Свойство("maxItems"), Аргументы.maxItems, 100); 73 | 74 | МаскаИмениВРег = ВРег(МаскаИмени); 75 | 76 | // Получаем коллекцию нужного типа 77 | Попытка 78 | КоллекцияМД = Метаданные[МетаТип]; 79 | Исключение 80 | ВызватьИсключение "Неизвестный тип метаданных в metaType: " + МетаТип; 81 | КонецПопытки; 82 | 83 | МассивСтрок = Новый Массив; 84 | Счетчик = 0; 85 | 86 | Для Каждого Элемент Из КоллекцияМД Цикл 87 | // Проверяем маску имени, если она задана 88 | Если ЗначениеЗаполнено(МаскаИмени) Тогда 89 | // Простая проверка на вхождение подстроки 90 | Если СтрНайти(ВРег(Элемент.Имя), МаскаИмениВРег) = 0 91 | И СтрНайти(ВРег(Элемент.Синоним), МаскаИмениВРег) = 0 Тогда 92 | Продолжить; 93 | КонецЕсли; 94 | КонецЕсли; 95 | 96 | // Формируем строку в формате "ПолноеИмя (Синоним)" 97 | СтрокаРезультата = Элемент.ПолноеИмя() + " (" + Элемент.Синоним + ")"; 98 | МассивСтрок.Добавить(СтрокаРезультата); 99 | 100 | Счетчик = Счетчик + 1; 101 | Если Счетчик >= Лимит Тогда 102 | Прервать; 103 | КонецЕсли; 104 | КонецЦикла; 105 | 106 | // Объединяем строки через символ перевода строки 107 | Результат = СтрСоединить(МассивСтрок, Символы.ПС); 108 | 109 | Возврат Результат; 110 | 111 | КонецФункции 112 | 113 | #КонецОбласти 114 | 115 | #Область ИнструментСтруктураОбъекта 116 | 117 | Процедура ДобавитьИнструментСтруктураОбъекта(Инструменты) 118 | 119 | // Создаем описания параметров для инструмента get_metadata_structure 120 | МассивПараметров = Новый Массив; 121 | 122 | // Параметр metaType - тип объекта метаданных (только те, у которых есть структура) 123 | СписокТиповМетаданных = "Catalogs,Documents,InformationRegisters,AccumulationRegisters,AccountingRegisters,CalculationRegisters,Reports,DataProcessors,ChartsOfCharacteristicTypes,ChartsOfAccounts,ChartsOfCalculationTypes,BusinessProcesses,Tasks,ExchangePlans"; 124 | 125 | МассивПараметров.Добавить(mcp_Метаданные.ПараметрИнструмента( 126 | "metaType", 127 | "string", 128 | "Тип объекта метаданных", 129 | , 130 | Истина, 131 | СписокТиповМетаданных 132 | )); 133 | 134 | // Параметр name - точное имя объекта (не зависимое от регистра) 135 | МассивПараметров.Добавить(mcp_Метаданные.ПараметрИнструмента( 136 | "name", 137 | "string", 138 | "Точное имя объекта метаданных (без учета регистра)", 139 | , 140 | Истина 141 | )); 142 | 143 | // Создаем JSON-схему параметров 144 | СхемаПараметров = mcp_Метаданные.СхемаПараметровИнструмента(МассивПараметров); 145 | 146 | // Добавляем инструмент в таблицу 147 | mcp_Метаданные.ДобавитьИнструмент( 148 | Инструменты, 149 | "get_metadata_structure", 150 | "Получение структуры объекта метаданных (реквизиты, табличные части, измерения, ресурсы)", 151 | СхемаПараметров 152 | ); 153 | 154 | КонецПроцедуры 155 | 156 | Функция СтруктураОбъектаМетаданных(Аргументы) 157 | 158 | МетаТип = Аргументы.metaType; 159 | ИмяОбъекта = Аргументы.name; 160 | 161 | // Получаем коллекцию нужного типа 162 | Попытка 163 | КоллекцияМД = Метаданные[МетаТип]; 164 | Исключение 165 | ВызватьИсключение "Неизвестный тип метаданных в metaType: " + МетаТип; 166 | КонецПопытки; 167 | 168 | // Ищем объект метаданных по имени (без учета регистра) 169 | МетаданныеОбъекта = КоллекцияМД.Найти(ИмяОбъекта); 170 | Если МетаданныеОбъекта = Неопределено Тогда 171 | ВызватьИсключение "Объект метаданных не найден: " + МетаТип + "." + ИмяОбъекта; 172 | КонецЕсли; 173 | 174 | ОбработкаОбъект = Создать(); 175 | 176 | Возврат ОбработкаОбъект.ОписаниеСтруктурыОбъектаМетаданных(МетаданныеОбъекта); 177 | 178 | КонецФункции 179 | 180 | #КонецОбласти 181 | 182 | 183 | -------------------------------------------------------------------------------- /src/1c_ext/agents.md: -------------------------------------------------------------------------------- 1 | # Архитектура MCP-расширения для 1С 2 | 3 | ## Обзор архитектуры 4 | 5 | MCP-сервер в 1С реализован как расширение с модульной архитектурой на основе контейнеров: 6 | 7 | ``` 8 | Python MCP Proxy ←→ HTTP-сервис mcp_APIBackend ←→ Обработки-контейнеры 9 | ``` 10 | 11 | **Ключевые принципы:** 12 | - Универсальный HTTP-сервис (не требует изменений при добавлении инструментов) 13 | - Обработки-контейнеры реализуют стандартный программный интерфейс 14 | - Автоматическое обнаружение контейнеров через подсистемы 15 | - JSON-схемы параметров для типизации 16 | 17 | ## HTTP-сервис `mcp_APIBackend` 18 | 19 | **Файл:** `HTTPServices/mcp_APIBackend/Ext/Module.bsl` 20 | 21 | Реализует JSON-RPC API с двумя endpoints: 22 | - `GET /mcp/health` - проверка состояния 23 | - `POST /mcp/rpc` - универсальный JSON-RPC обработчик 24 | 25 | **Поддерживаемые методы:** 26 | - `tools/list`, `tools/call` - инструменты 27 | - `resources/list`, `resources/read` - ресурсы 28 | - `prompts/list`, `prompts/get` - промпты 29 | 30 | ## Общие модули 31 | 32 | ### `mcp_Метаданные` - Схемы и таблицы 33 | 34 | **Назначение:** Создание таблиц описаний и JSON-схем 35 | 36 | **Ключевые функции:** 37 | ```bsl 38 | // Создание таблиц 39 | ТаблицаИнструментов() // Колонки: ИмяОбработкиКонтейнера, Имя, Описание, СхемаПараметров 40 | ТаблицаРесурсов() // Колонки: ИмяОбработкиКонтейнера, Адрес, Имя, Описание 41 | ТаблицаПромптов() // Колонки: ИмяОбработкиКонтейнера, Имя, Описание, Параметры 42 | 43 | // Добавление элементов в таблицы 44 | ДобавитьИнструмент(Таблица, Имя, Описание, СхемаПараметров) 45 | ДобавитьРесурс(Таблица, Адрес, Имя, Описание) 46 | ДобавитьПромпт(Таблица, Имя, Описание, Параметры) 47 | 48 | // Заполнение из контейнеров (через подсистемы) 49 | ЗаполнитьТаблицуИнструментов(ТаблицаИнструментов) 50 | ЗаполнитьТаблицуРесурсов(ТаблицаРесурсов) 51 | ЗаполнитьТаблицуПромптов(ТаблицаПромптов) 52 | 53 | // Создание JSON-схем параметров 54 | ПараметрИнструмента(Имя, Тип, Описание, ЗначениеПоУмолчанию, Обязательный, СписокДопустимыхЗначений) 55 | ПараметрИнструментаМассив(Имя, ТипЭлементаМассива, Описание, Обязательный, СписокДопустимыхЗначенийЭлемента) 56 | СхемаПараметровИнструмента(МассивОписанийПараметров) // Возвращает JSON 57 | 58 | // Для промптов 59 | ПараметрПромпта(Имя, Описание, Обязательный) 60 | ПараметрыПромпта(МассивОписанийПараметров) 61 | ``` 62 | 63 | ### `mcp_Выполнение` - Выполнение операций 64 | 65 | **Назначение:** Вызов методов контейнеров 66 | 67 | **Ключевые функции:** 68 | ```bsl 69 | ВыполнитьИнструмент(СтрокаТаблицыИнструментов, Параметры) 70 | ПрочитатьРесурс(СтрокаТаблицыРесурсов, Адрес) 71 | ПолучитьПромпт(СтрокаТаблицыПромптов, Имя, Параметры) 72 | ``` 73 | 74 | ### `mcp_КонтейнерыПовтИсп` - Кеширование 75 | 76 | **Назначение:** Кешированное получение таблиц 77 | 78 | **Ключевые функции:** 79 | ```bsl 80 | Инструменты() // ТаблицаЗначений 81 | Ресурсы() // ТаблицаЗначений 82 | Промпты() // ТаблицаЗначений 83 | ``` 84 | 85 | ### `mcp_ОбщегоНазначения` - Утилиты 86 | 87 | **Назначение:** JSON, HTTP утилиты 88 | 89 | **Ключевые функции:** 90 | ```bsl 91 | JSONВСтруктуру(СтрокаJSON) // Парсинг JSON в структуру 92 | СтруктураВJSON(Объект) // Сериализация в JSON 93 | РазобратьURL(URL) // Парсинг URL 94 | ``` 95 | 96 | ## Обработки-контейнеры 97 | 98 | ### Программный интерфейс для инструментов 99 | 100 | **Обязательные методы менеджера обработки:** 101 | 102 | ```bsl 103 | // Добавление описаний инструментов в таблицу 104 | Процедура ДобавитьИнструменты(Инструменты) Экспорт 105 | // Создать параметры через mcp_Метаданные.ПараметрИнструмента() 106 | // Создать схему через mcp_Метаданные.СхемаПараметровИнструмента() 107 | // Добавить через mcp_Метаданные.ДобавитьИнструмент() 108 | КонецПроцедуры 109 | 110 | // Выполнение инструмента по имени 111 | Функция ВыполнитьИнструмент(ИмяИнструмента, Аргументы) Экспорт 112 | // Проверить ИмяИнструмента и вызвать соответствующую функцию 113 | // Аргументы - Структура с параметрами 114 | // Возврат: строка, массив структур с type/text, или структура с type/text 115 | КонецФункции 116 | ``` 117 | 118 | ### Программный интерфейс для ресурсов 119 | 120 | **Опциональные методы менеджера обработки:** 121 | 122 | ```bsl 123 | // Добавление описаний ресурсов 124 | Процедура ДобавитьРесурсы(Ресурсы) Экспорт 125 | // Добавить через mcp_Метаданные.ДобавитьРесурс(Ресурсы, Адрес, Имя, Описание) 126 | КонецПроцедуры 127 | 128 | // Чтение ресурса по адресу 129 | Функция ПрочитатьРесурс(Адрес) Экспорт 130 | // Адрес - строка (URI) 131 | // Возврат: строка, массив структур с type/text/mimeType, или структура 132 | КонецФункции 133 | ``` 134 | 135 | ### Программный интерфейс для промптов 136 | 137 | **Опциональные методы менеджера обработки:** 138 | 139 | ```bsl 140 | // Добавление описаний промптов 141 | Процедура ДобавитьПромпты(Промпты) Экспорт 142 | // Создать параметры через mcp_Метаданные.ПараметрПромпта() 143 | // Создать описание через mcp_Метаданные.ПараметрыПромпта() 144 | // Добавить через mcp_Метаданные.ДобавитьПромпт() 145 | КонецПроцедуры 146 | 147 | // Получение промпта 148 | Функция ПолучитьПромпт(ИмяПромпта, Параметры) Экспорт 149 | // Параметры - Структура 150 | // Возврат: структура с полями description, messages или массив сообщений 151 | КонецФункции 152 | ``` 153 | 154 | ## Подсистемы 155 | 156 | **Главная подсистема:** `mcp_MCPСервер` 157 | 158 | **Подсистемы для группировки контейнеров:** 159 | - `mcp_КонтейнерыИнструментов` - обработки с инструментами 160 | - `mcp_КонтейнерыРесурсов` - обработки с ресурсами 161 | - `mcp_КонтейнерыПромптов` - обработки с промптами 162 | 163 | **Принцип работы:** модуль `mcp_Метаданные` автоматически обходит состав подсистем и вызывает методы всех включенных обработок. 164 | 165 | ## Создание нового инструмента 166 | 167 | 1. **Создать обработку** с префиксом `mcp_ИнструментИмя` 168 | 2. **Включить в подсистему** `mcp_КонтейнерыИнструментов` 169 | 3. **Реализовать методы:** 170 | ```bsl 171 | Процедура ДобавитьИнструменты(Инструменты) Экспорт 172 | Функция ВыполнитьИнструмент(ИмяИнструмента, Аргументы) Экспорт 173 | ``` 174 | 4. **Никаких изменений** в HTTP-сервисе или других модулях не требуется 175 | 176 | ## Пример реализации инструмента 177 | 178 | ```bsl 179 | Процедура ДобавитьИнструменты(Инструменты) Экспорт 180 | МассивПараметров = Новый Массив; 181 | 182 | // Добавляем параметры 183 | МассивПараметров.Добавить(mcp_Метаданные.ПараметрИнструмента( 184 | "text", "string", "Текст для обработки", , Истина)); 185 | МассивПараметров.Добавить(mcp_Метаданные.ПараметрИнструмента( 186 | "maxLength", "number", "Максимальная длина", 100)); 187 | 188 | // Создаем схему 189 | СхемаПараметров = mcp_Метаданные.СхемаПараметровИнструмента(МассивПараметров); 190 | 191 | // Добавляем инструмент 192 | mcp_Метаданные.ДобавитьИнструмент( 193 | Инструменты, "process_text", "Обработка текста", СхемаПараметров); 194 | КонецПроцедуры 195 | 196 | Функция ВыполнитьИнструмент(ИмяИнструмента, Аргументы) Экспорт 197 | Если ИмяИнструмента = "process_text" Тогда 198 | Текст = Аргументы.text; 199 | МаксДлина = ?(Аргументы.Свойство("maxLength"), Аргументы.maxLength, 100); 200 | 201 | // Логика обработки 202 | Результат = Лев(Текст, МаксДлина); 203 | Возврат Результат; 204 | КонецЕсли; 205 | 206 | ВызватьИсключение "Неизвестный инструмент: " + ИмяИнструмента; 207 | КонецФункции 208 | ``` 209 | 210 | ## Отступы и стиль кода 211 | 212 | - **Стандартный отступ:** одна табуляция 213 | - **Именование:** префикс `mcp_` для всех объектов расширения 214 | - **Экспортные методы:** обязательно с `Экспорт` 215 | - **Обработка ошибок:** через `ВызватьИсключение` -------------------------------------------------------------------------------- /src/py_server/main.py: -------------------------------------------------------------------------------- 1 | """Основной файл запуска MCP-прокси сервера.""" 2 | 3 | import asyncio 4 | import logging 5 | import os 6 | import sys 7 | from typing import Optional 8 | import argparse 9 | from pathlib import Path 10 | 11 | from dotenv import load_dotenv 12 | 13 | from .config import get_config 14 | from .http_server import run_http_server 15 | from .stdio_server import run_stdio_server 16 | 17 | 18 | def setup_logging(level: str = "INFO"): 19 | """Настройка логирования. 20 | 21 | Args: 22 | level: Уровень логирования 23 | """ 24 | logging.basicConfig( 25 | level=getattr(logging, level.upper()), 26 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 27 | handlers=[ 28 | logging.StreamHandler(sys.stderr) # Логи должны идти в stderr, не в stdout! 29 | ] 30 | ) 31 | 32 | 33 | def create_parser() -> argparse.ArgumentParser: 34 | """Создание парсера аргументов командной строки.""" 35 | parser = argparse.ArgumentParser( 36 | description="MCP-прокси сервер для взаимодействия с 1С", 37 | formatter_class=argparse.RawDescriptionHelpFormatter, 38 | epilog=""" 39 | Примеры использования: 40 | 41 | # Запуск в режиме stdio (по умолчанию) 42 | python -m src.py_server 43 | python -m src.py_server stdio 44 | 45 | # Запуск HTTP-сервера на порту 8000 46 | python -m src.py_server http --port 8000 47 | 48 | # Запуск с конфигурацией из .env файла 49 | python -m src.py_server --env-file .env 50 | 51 | Переменные окружения: 52 | MCP_ONEC_URL - URL базы 1С (обязательно) 53 | MCP_ONEC_USERNAME - Имя пользователя 1С (обязательно) 54 | MCP_ONEC_PASSWORD - Пароль пользователя 1С (обязательно) 55 | MCP_ONEC_SERVICE_ROOT - Корневой URL HTTP-сервиса (по умолчанию: mcp) 56 | MCP_HOST - Хост HTTP-сервера (по умолчанию: 127.0.0.1) 57 | MCP_PORT - Порт HTTP-сервера (по умолчанию: 8000) 58 | MCP_LOG_LEVEL - Уровень логирования (по умолчанию: INFO) 59 | MCP_AUTH_MODE - Режим авторизации: none или oauth2 (по умолчанию: none) 60 | MCP_PUBLIC_URL - Публичный URL для OAuth2 (опционально) 61 | MCP_OAUTH2_CODE_TTL - TTL authorization code в секундах (по умолчанию: 120) 62 | MCP_OAUTH2_ACCESS_TTL - TTL access token в секундах (по умолчанию: 3600) 63 | MCP_OAUTH2_REFRESH_TTL - TTL refresh token в секундах (по умолчанию: 1209600) 64 | """ 65 | ) 66 | 67 | # Режим работы как позиционный аргумент с значением по умолчанию 68 | parser.add_argument( 69 | "mode", 70 | nargs="?", 71 | default="stdio", 72 | choices=["stdio", "http"], 73 | help="Режим работы сервера (по умолчанию: stdio)" 74 | ) 75 | 76 | # Общие аргументы доступны всегда 77 | parser.add_argument( 78 | "--env-file", 79 | type=str, 80 | help="Путь к .env файлу с конфигурацией" 81 | ) 82 | parser.add_argument( 83 | "--onec-url", 84 | type=str, 85 | help="URL базы 1С" 86 | ) 87 | parser.add_argument( 88 | "--onec-username", 89 | type=str, 90 | help="Имя пользователя 1С" 91 | ) 92 | parser.add_argument( 93 | "--onec-password", 94 | type=str, 95 | help="Пароль пользователя 1С" 96 | ) 97 | parser.add_argument( 98 | "--onec-service-root", 99 | type=str, 100 | help="Корневой URL HTTP-сервиса в 1С" 101 | ) 102 | parser.add_argument( 103 | "--log-level", 104 | type=str, 105 | choices=["DEBUG", "INFO", "WARNING", "ERROR"], 106 | help="Уровень логирования" 107 | ) 108 | 109 | # HTTP-специфичные аргументы 110 | parser.add_argument( 111 | "--host", 112 | type=str, 113 | help="Хост для HTTP-сервера (только для режима http)" 114 | ) 115 | parser.add_argument( 116 | "--port", 117 | type=int, 118 | help="Порт для HTTP-сервера (только для режима http)" 119 | ) 120 | 121 | # OAuth2 аргументы 122 | parser.add_argument( 123 | "--auth-mode", 124 | type=str, 125 | choices=["none", "oauth2"], 126 | help="Режим авторизации: none или oauth2" 127 | ) 128 | parser.add_argument( 129 | "--public-url", 130 | type=str, 131 | help="Публичный URL прокси для OAuth2" 132 | ) 133 | 134 | return parser 135 | 136 | 137 | async def main(): 138 | """Основная функция.""" 139 | # Принудительная настройка кодировки UTF-8 для Windows 140 | if sys.platform == "win32": 141 | import locale 142 | 143 | # Устанавливаем кодировку для Python I/O 144 | os.environ['PYTHONIOENCODING'] = 'utf-8' 145 | 146 | # Устанавливаем локаль 147 | try: 148 | locale.setlocale(locale.LC_ALL, 'ru_RU.UTF-8') 149 | except: 150 | try: 151 | locale.setlocale(locale.LC_ALL, 'Russian_Russia.1251') 152 | except: 153 | pass # Игнорируем ошибки локали 154 | 155 | # Убеждаемся, что stderr работает без буферизации 156 | sys.stderr.flush() 157 | 158 | parser = create_parser() 159 | args = parser.parse_args() 160 | 161 | # Загружаем .env файл если указан 162 | if args.env_file: 163 | env_path = Path(args.env_file) 164 | if env_path.exists(): 165 | load_dotenv(env_path) 166 | else: 167 | print(f"Предупреждение: файл {args.env_file} не найден", file=sys.stderr) 168 | else: 169 | # Пытаемся загрузить .env из текущей директории 170 | load_dotenv() 171 | 172 | # ИСПРАВЛЕНИЕ: Устанавливаем переменные окружения из аргументов ДО создания Config 173 | if args.onec_url: 174 | os.environ["MCP_ONEC_URL"] = args.onec_url 175 | if args.onec_username: 176 | os.environ["MCP_ONEC_USERNAME"] = args.onec_username 177 | if args.onec_password: 178 | os.environ["MCP_ONEC_PASSWORD"] = args.onec_password 179 | if args.onec_service_root: 180 | os.environ["MCP_ONEC_SERVICE_ROOT"] = args.onec_service_root 181 | if args.host: 182 | os.environ["MCP_HOST"] = args.host 183 | if args.port: 184 | os.environ["MCP_PORT"] = str(args.port) 185 | if args.log_level: 186 | os.environ["MCP_LOG_LEVEL"] = args.log_level 187 | if args.auth_mode: 188 | os.environ["MCP_AUTH_MODE"] = args.auth_mode 189 | if args.public_url: 190 | os.environ["MCP_PUBLIC_URL"] = args.public_url 191 | 192 | # Получаем конфигурацию (теперь валидация пройдет успешно) 193 | try: 194 | config = get_config() 195 | 196 | # Убираем старые переопределения - теперь они не нужны 197 | # Все значения уже установлены через переменные окружения 198 | 199 | except Exception as e: 200 | print(f"Ошибка конфигурации: {e}", file=sys.stderr) 201 | print("\nПроверьте, что указаны все обязательные параметры:", file=sys.stderr) 202 | print("- MCP_ONEC_URL (URL базы 1С)", file=sys.stderr) 203 | print("- MCP_ONEC_USERNAME (имя пользователя)", file=sys.stderr) 204 | print("- MCP_ONEC_PASSWORD (пароль)", file=sys.stderr) 205 | sys.exit(1) 206 | 207 | # Настройка логирования 208 | setup_logging(config.log_level) 209 | logger = logging.getLogger(__name__) 210 | 211 | # Отладочная информация через logger (подчиняется уровню логирования) 212 | logger.debug(f"Режим работы: {args.mode}") 213 | logger.debug(f"Аргументы: {args}") 214 | logger.debug(f"Python версия: {sys.version}") 215 | logger.debug(f"Рабочая директория: {os.getcwd()}") 216 | 217 | logger.debug(f"Запуск MCP-прокси сервера в режиме: {args.mode}") 218 | logger.debug(f"Подключение к 1С: {config.onec_url}") 219 | logger.debug(f"Пользователь: {config.onec_username}") 220 | 221 | try: 222 | if args.mode == "stdio": 223 | await run_stdio_server(config) 224 | elif args.mode == "http": 225 | logger.debug(f"HTTP-сервер будет запущен на {config.host}:{config.port}") 226 | await run_http_server(config) 227 | else: 228 | logger.error(f"Неизвестный режим: {args.mode}") 229 | sys.exit(1) 230 | 231 | except KeyboardInterrupt: 232 | logger.debug("Получен сигнал прерывания, завершение работы...") 233 | except Exception as e: 234 | logger.error(f"Критическая ошибка: {e}") 235 | sys.exit(1) 236 | 237 | 238 | if __name__ == "__main__": 239 | asyncio.run(main()) -------------------------------------------------------------------------------- /src/1c_ext/CommonModules/mcp_ОбщегоНазначения/Ext/Module.bsl: -------------------------------------------------------------------------------- 1 | // Репозиторий проекта: 2 | // https://github.com/vladimir-kharin/1c_mcp 3 | // 4 | // Харин Владимир (С) 2025. https://vharin.ru 5 | // Telegram - https://t.me/vladimir_kharin 6 | 7 | #Область ПрограммныйИнтерфейс 8 | 9 | // Преобразует строку в формате JSON в структуру. 10 | // 11 | // Параметры: 12 | // СтрокаJSON - Строка - строка в формате JSON. 13 | // 14 | // Возвращаемое значение: 15 | // Структура - 16 | Функция JSONВСтруктуру(СтрокаJSON) Экспорт 17 | // Выделяем в строке JSON часть именно JSON 18 | // Находим позицию первого открывающего фигурного скобки 19 | ПозицияНачала = СтрНайти(СтрокаJSON, "{"); 20 | Если ПозицияНачала = 0 Тогда 21 | // Если не найдено, возвращаем Неопределено или можно вызвать ошибку 22 | Возврат Неопределено; 23 | КонецЕсли; 24 | 25 | // Находим позицию последнего закрывающего фигурного скобки, используя поиск с конца 26 | ПозицияКонца = СтрНайти(СтрокаJSON, "}", НаправлениеПоиска.СКонца); 27 | Если ПозицияКонца = 0 Тогда 28 | // Если закрывающая скобка не найдена, возвращаем Неопределено 29 | Возврат Неопределено; 30 | КонецЕсли; 31 | 32 | // Вычисляем длину фрагмента с JSON: от первого "{" до последнего "}" включительно 33 | ДлинаФрагмента = ПозицияКонца - ПозицияНачала + 1; 34 | 35 | // Извлекаем корректную часть JSON из строки 36 | ЧистыйJSON = Сред(СтрокаJSON, ПозицияНачала, ДлинаФрагмента); 37 | 38 | ЧтениеJSON = Новый ЧтениеJSON; 39 | ЧтениеJSON.УстановитьСтроку(ЧистыйJSON); 40 | 41 | Попытка 42 | // Пытаемся прочитать JSON в структуру 43 | Результат = ПрочитатьJSON(ЧтениеJSON); 44 | ЧтениеJSON.Закрыть(); 45 | Возврат Результат; 46 | Исключение 47 | // Если не удалось, то читаем в соответствие 48 | ЧтениеJSON.Закрыть(); 49 | 50 | ЧтениеJSON = Новый ЧтениеJSON; 51 | ЧтениеJSON.УстановитьСтроку(ЧистыйJSON); 52 | 53 | Соответствие = ПрочитатьJSON(ЧтениеJSON, Истина); 54 | ЧтениеJSON.Закрыть(); 55 | 56 | // Преобразуем соответствие в структуру с нормализацией ключей 57 | Возврат СоответствиеВСтруктуруСНормализациейКлючей(Соответствие); 58 | КонецПопытки; 59 | КонецФункции 60 | 61 | // Разбирает URL на составляющие: схема, хост, порт и путь 62 | // 63 | // Параметры: 64 | // URL - Строка - URL для разбора 65 | // 66 | // Возвращаемое значение: 67 | // Структура - Разобранные части URL (схема, хост, порт, путь) 68 | Функция РазобратьURL(Знач URL) Экспорт 69 | РезультатРазбора = Новый Структура("Схема, Хост, Порт, Путь"); 70 | 71 | // Проверка и удаление протокола 72 | Если НРег(Лев(URL, 8)) = "https://" Тогда 73 | РезультатРазбора.Схема = "https"; 74 | URL = Сред(URL, 9); 75 | ИначеЕсли НРег(Лев(URL, 7)) = "http://" Тогда 76 | РезультатРазбора.Схема = "http"; 77 | URL = Сред(URL, 8); 78 | Иначе 79 | ВызватьИсключение "Неверный формат URL: отсутствует протокол http или https"; 80 | КонецЕсли; 81 | 82 | // Разделение хоста, порта и пути 83 | ПозицияСлеш = СтрНайти(URL, "/"); 84 | Если ПозицияСлеш > 0 Тогда 85 | ХостИПорт = Лев(URL, ПозицияСлеш - 1); 86 | РезультатРазбора.Путь = Сред(URL, ПозицияСлеш); 87 | Иначе 88 | ХостИПорт = URL; 89 | РезультатРазбора.Путь = "/"; 90 | КонецЕсли; 91 | 92 | // Разделение хоста и порта 93 | ПозицияДвоеточие = СтрНайти(ХостИПорт, ":"); 94 | Если ПозицияДвоеточие > 0 Тогда 95 | РезультатРазбора.Хост = Лев(ХостИПорт, ПозицияДвоеточие - 1); 96 | РезультатРазбора.Порт = Число(Сред(ХостИПорт, ПозицияДвоеточие + 1)); 97 | Иначе 98 | РезультатРазбора.Хост = ХостИПорт; 99 | РезультатРазбора.Порт = ?(РезультатРазбора.Схема = "https", 443, 80); 100 | КонецЕсли; 101 | 102 | Возврат РезультатРазбора; 103 | КонецФункции 104 | 105 | // Преобразует структура в строку JSON 106 | // 107 | // Параметры: 108 | // Объект - Структура - которую необходимо преобразовать в строку JSON. 109 | // 110 | // Возвращаемое значение: 111 | // Строка - в формате JSON. 112 | Функция СтруктураВJSON(Объект) Экспорт 113 | ЗаписьJSON = Новый ЗаписьJSON; 114 | ЗаписьJSON.УстановитьСтроку(); 115 | ЗаписатьJSON(ЗаписьJSON, Объект); 116 | 117 | Возврат ЗаписьJSON.Закрыть(); 118 | КонецФункции 119 | 120 | #КонецОбласти 121 | 122 | #Область СлужебныеПроцедурыИФункции 123 | 124 | // Проверяет, является ли символ буквой. 125 | // 126 | // Параметры: 127 | // Символ - Строка - символ для проверки. 128 | // 129 | // Возвращаемое значение: 130 | // Булево - Истина, если символ является буквой. 131 | Функция ЭтоБуква(Символ) 132 | 133 | КодСимвола = КодСимвола(Символ); 134 | 135 | // Латинские буквы A-Z (65-90) и a-z (97-122) 136 | Если (КодСимвола >= 65 И КодСимвола <= 90) 137 | Или (КодСимвола >= 97 И КодСимвола <= 122) Тогда 138 | Возврат Истина; 139 | КонецЕсли; 140 | 141 | // Кириллические буквы А-Я (1040-1103), включая Ё (1025, 1105) 142 | Если (КодСимвола >= 1040 И КодСимвола <= 1103) 143 | Или КодСимвола = 1025 144 | Или КодСимвола = 1105 Тогда 145 | Возврат Истина; 146 | КонецЕсли; 147 | 148 | Возврат Ложь; 149 | 150 | КонецФункции 151 | 152 | // Проверяет, является ли символ цифрой. 153 | // 154 | // Параметры: 155 | // Символ - Строка - символ для проверки. 156 | // 157 | // Возвращаемое значение: 158 | // Булево - Истина, если символ является цифрой. 159 | Функция ЭтоЦифра(Символ) 160 | 161 | КодСимвола = КодСимвола(Символ); 162 | 163 | // Цифры 0-9 (48-57) 164 | Возврат КодСимвола >= 48 И КодСимвола <= 57; 165 | 166 | КонецФункции 167 | 168 | // Нормализует ключ для использования в структуре. 169 | // Заменяет недопустимые символы на "_" и обеспечивает, 170 | // что первый символ - буква или "_". 171 | // 172 | // Параметры: 173 | // Ключ - Строка - исходный ключ. 174 | // 175 | // Возвращаемое значение: 176 | // Строка - нормализованный ключ. 177 | Функция НормализоватьКлюч(Ключ) 178 | 179 | Если НЕ ЗначениеЗаполнено(Ключ) Тогда 180 | Возврат "_"; 181 | КонецЕсли; 182 | 183 | НормализованныйКлюч = ""; 184 | 185 | Для Индекс = 1 По СтрДлина(Ключ) Цикл 186 | Символ = Сред(Ключ, Индекс, 1); 187 | 188 | Если Индекс = 1 Тогда 189 | // Первый символ должен быть буквой или "_" 190 | Если ЭтоБуква(Символ) Или Символ = "_" Тогда 191 | НормализованныйКлюч = НормализованныйКлюч + Символ; 192 | Иначе 193 | НормализованныйКлюч = НормализованныйКлюч + "_"; 194 | КонецЕсли; 195 | Иначе 196 | // Последующие символы могут быть буквами, цифрами или "_" 197 | Если ЭтоБуква(Символ) Или ЭтоЦифра(Символ) Или Символ = "_" Тогда 198 | НормализованныйКлюч = НормализованныйКлюч + Символ; 199 | Иначе 200 | НормализованныйКлюч = НормализованныйКлюч + "_"; 201 | КонецЕсли; 202 | КонецЕсли; 203 | КонецЦикла; 204 | 205 | Возврат НормализованныйКлюч; 206 | 207 | КонецФункции 208 | 209 | // Преобразует соответствие в структуру с нормализацией ключей. 210 | // Рекурсивно обрабатывает вложенные объекты. 211 | // 212 | // Параметры: 213 | // Значение - Произвольный - значение для преобразования. 214 | // 215 | // Возвращаемое значение: 216 | // Произвольный - преобразованное значение. 217 | Функция СоответствиеВСтруктуруСНормализациейКлючей(Значение) 218 | 219 | Если ТипЗнч(Значение) = Тип("Соответствие") Тогда 220 | 221 | Структура = Новый Структура; 222 | 223 | Для Каждого КлючЗначение Из Значение Цикл 224 | 225 | НормализованныйКлюч = НормализоватьКлюч(Строка(КлючЗначение.Ключ)); 226 | 227 | // Рекурсивно обрабатываем значение 228 | ОбработанноеЗначение = СоответствиеВСтруктуруСНормализациейКлючей(КлючЗначение.Значение); 229 | 230 | Структура.Вставить(НормализованныйКлюч, ОбработанноеЗначение); 231 | 232 | КонецЦикла; 233 | 234 | Возврат Структура; 235 | 236 | ИначеЕсли ТипЗнч(Значение) = Тип("Массив") Тогда 237 | 238 | Массив = Новый Массив; 239 | 240 | Для Каждого Элемент Из Значение Цикл 241 | // Рекурсивно обрабатываем элементы массива 242 | Массив.Добавить(СоответствиеВСтруктуруСНормализациейКлючей(Элемент)); 243 | КонецЦикла; 244 | 245 | Возврат Массив; 246 | 247 | Иначе 248 | 249 | // Простое значение - возвращаем как есть 250 | Возврат Значение; 251 | 252 | КонецЕсли; 253 | 254 | КонецФункции 255 | 256 | #КонецОбласти 257 | 258 | -------------------------------------------------------------------------------- /src/py_server/mcp_server.py: -------------------------------------------------------------------------------- 1 | """Основной MCP-сервер, который проксирует запросы в 1С.""" 2 | 3 | import asyncio 4 | import logging 5 | import contextvars 6 | from contextlib import asynccontextmanager 7 | from typing import Any, Dict, List, Optional, AsyncIterator, Tuple 8 | 9 | from mcp.server import Server 10 | from mcp.server.models import InitializationOptions 11 | from mcp.server.lowlevel import NotificationOptions 12 | from mcp import types 13 | 14 | from .onec_client import OneCClient 15 | from .config import Config 16 | 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | # Context var для per-session креденшилов 1С (login, password) 21 | current_onec_credentials: contextvars.ContextVar[Optional[Tuple[str, str]]] = contextvars.ContextVar( 22 | 'current_onec_credentials', 23 | default=None 24 | ) 25 | 26 | 27 | class MCPProxy: 28 | """MCP-прокси сервер для взаимодействия с 1С.""" 29 | 30 | def __init__(self, config: Config): 31 | """Инициализация прокси. 32 | 33 | Args: 34 | config: Конфигурация сервера 35 | """ 36 | self.config = config 37 | self.onec_client: Optional[OneCClient] = None 38 | 39 | # Создаем MCP сервер 40 | self.server = Server( 41 | name=config.server_name, 42 | lifespan=self._lifespan 43 | ) 44 | 45 | # Регистрируем обработчики 46 | self._register_handlers() 47 | 48 | @asynccontextmanager 49 | async def _lifespan(self, server: Server) -> AsyncIterator[Dict[str, Any]]: 50 | """Управление жизненным циклом сервера.""" 51 | logger.debug(f"Инициализация MCP сервера '{self.config.server_name}' v{self.config.server_version}") 52 | 53 | # Определяем креденшилы для текущей сессии 54 | # При auth_mode=oauth2 берём из context var (per-session), иначе из конфигурации 55 | session_creds = None 56 | if self.config.auth_mode == "oauth2": 57 | session_creds = current_onec_credentials.get() 58 | if session_creds: 59 | username, password = session_creds 60 | logger.debug(f"Использую per-session креденшилы для пользователя: {username}") 61 | else: 62 | # Fallback на дефолтные (для совместимости) 63 | username = self.config.onec_username 64 | password = self.config.onec_password 65 | logger.debug("Per-session креденшилы не найдены, использую дефолтные из конфигурации") 66 | else: 67 | # Режим none - используем дефолтные креды 68 | username = self.config.onec_username 69 | password = self.config.onec_password 70 | logger.debug(f"Режим auth_mode=none, использую дефолтные креденшилы: {username}") 71 | 72 | # Инициализация при запуске 73 | self.onec_client = OneCClient( 74 | base_url=self.config.onec_url, 75 | username=username, 76 | password=password, 77 | service_root=self.config.onec_service_root 78 | ) 79 | 80 | logger.debug(f"Подключение к 1С: {self.config.onec_url}") 81 | logger.debug(f"HTTP-сервис: {self.config.onec_service_root}") 82 | 83 | try: 84 | # Проверяем подключение к 1С 85 | await self.onec_client.check_health() 86 | logger.debug("Успешное подключение к 1С (проверка health)") 87 | 88 | logger.debug("MCP сервер готов к работе") 89 | yield {"onec_client": self.onec_client} 90 | finally: 91 | # Очистка при завершении 92 | if self.onec_client: 93 | await self.onec_client.close() 94 | logger.debug("Соединение с 1С закрыто") 95 | 96 | def _register_handlers(self): 97 | """Регистрация обработчиков MCP.""" 98 | 99 | @self.server.list_tools() 100 | async def handle_list_tools() -> List[types.Tool]: 101 | """Получить список доступных инструментов.""" 102 | ctx = self.server.request_context 103 | onec_client: OneCClient = ctx.lifespan_context["onec_client"] 104 | 105 | try: 106 | tools = await onec_client.list_tools() 107 | logger.debug(f"Получено инструментов: {len(tools)}") 108 | return tools 109 | except Exception as e: 110 | logger.error(f"Ошибка при получении списка инструментов: {e}") 111 | return [] 112 | 113 | @self.server.call_tool() 114 | async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[types.TextContent]: 115 | """Вызвать инструмент.""" 116 | ctx = self.server.request_context 117 | onec_client: OneCClient = ctx.lifespan_context["onec_client"] 118 | 119 | try: 120 | logger.debug(f"Вызов инструмента: {name} с аргументами: {arguments}") 121 | result = await onec_client.call_tool(name, arguments) 122 | 123 | if result.isError: 124 | logger.error(f"Ошибка выполнения инструмента {name}") 125 | 126 | return result.content 127 | except Exception as e: 128 | logger.error(f"Ошибка при вызове инструмента {name}: {e}") 129 | return [types.TextContent( 130 | type="text", 131 | text=f"Ошибка выполнения инструмента: {str(e)}" 132 | )] 133 | 134 | @self.server.list_resources() 135 | async def handle_list_resources() -> List[types.Resource]: 136 | """Получить список доступных ресурсов.""" 137 | ctx = self.server.request_context 138 | onec_client: OneCClient = ctx.lifespan_context["onec_client"] 139 | 140 | try: 141 | resources = await onec_client.list_resources() 142 | logger.debug(f"Получено ресурсов: {len(resources)}") 143 | return resources 144 | except Exception as e: 145 | logger.error(f"Ошибка при получении списка ресурсов: {e}") 146 | return [] 147 | 148 | @self.server.read_resource() 149 | async def handle_read_resource(uri: str) -> types.ReadResourceResult: 150 | """Прочитать ресурс.""" 151 | ctx = self.server.request_context 152 | onec_client: OneCClient = ctx.lifespan_context["onec_client"] 153 | 154 | try: 155 | logger.debug(f"Чтение ресурса: {uri}") 156 | result = await onec_client.read_resource(uri) 157 | return result 158 | except Exception as e: 159 | logger.error(f"Ошибка при чтении ресурса {uri}: {e}") 160 | # Возвращаем ReadResourceResult с ошибкой 161 | return types.ReadResourceResult( 162 | contents=[ 163 | types.TextResourceContents( 164 | uri=str(uri), 165 | mimeType="text/plain", 166 | text=f"Ошибка чтения ресурса: {str(e)}" 167 | ) 168 | ] 169 | ) 170 | 171 | @self.server.list_prompts() 172 | async def handle_list_prompts() -> List[types.Prompt]: 173 | """Получить список доступных промптов.""" 174 | ctx = self.server.request_context 175 | onec_client: OneCClient = ctx.lifespan_context["onec_client"] 176 | 177 | try: 178 | prompts = await onec_client.list_prompts() 179 | logger.debug(f"Получено промптов: {len(prompts)}") 180 | return prompts 181 | except Exception as e: 182 | logger.error(f"Ошибка при получении списка промптов: {e}") 183 | return [] 184 | 185 | @self.server.get_prompt() 186 | async def handle_get_prompt(name: str, arguments: Optional[Dict[str, str]] = None) -> types.GetPromptResult: 187 | """Получить промпт.""" 188 | ctx = self.server.request_context 189 | onec_client: OneCClient = ctx.lifespan_context["onec_client"] 190 | 191 | try: 192 | logger.debug(f"Получение промпта: {name} с аргументами: {arguments}") 193 | result = await onec_client.get_prompt(name, arguments) 194 | return result 195 | except Exception as e: 196 | logger.error(f"Ошибка при получении промпта {name}: {e}") 197 | return types.GetPromptResult( 198 | description=f"Ошибка получения промпта: {str(e)}", 199 | messages=[] 200 | ) 201 | 202 | def get_capabilities(self) -> Dict[str, Any]: 203 | """Получить capabilities сервера.""" 204 | return { 205 | "tools": { 206 | "listChanged": True 207 | }, 208 | "resources": { 209 | "subscribe": True, 210 | "listChanged": True 211 | }, 212 | "prompts": { 213 | "listChanged": True 214 | }, 215 | "logging": {} 216 | } 217 | 218 | def get_initialization_options(self) -> InitializationOptions: 219 | """Получить опции инициализации.""" 220 | return InitializationOptions( 221 | server_name=self.config.server_name, 222 | server_version=self.config.server_version, 223 | capabilities=self.server.get_capabilities( 224 | notification_options=NotificationOptions( 225 | tools_changed=True, 226 | resources_changed=True, 227 | prompts_changed=True 228 | ), 229 | experimental_capabilities={} 230 | ) 231 | ) -------------------------------------------------------------------------------- /src/py_server/onec_client.py: -------------------------------------------------------------------------------- 1 | """Клиент для взаимодействия с 1С.""" 2 | 3 | import json 4 | import logging 5 | from typing import Any, Dict, List, Optional 6 | import httpx 7 | from mcp import types 8 | from mcp.server.lowlevel.helper_types import ReadResourceContents 9 | import base64 10 | 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class OneCClient: 16 | """Клиент для взаимодействия с HTTP-сервисом 1С.""" 17 | 18 | def __init__(self, base_url: str, username: str, password: str, service_root: str = "mcp"): 19 | """Инициализация клиента. 20 | 21 | Args: 22 | base_url: Базовый URL 1С (например, http://localhost/base) 23 | username: Имя пользователя 24 | password: Пароль 25 | service_root: Корневой URL HTTP-сервиса (по умолчанию "mcp") 26 | """ 27 | self.base_url = base_url.rstrip('/') 28 | self.service_root = service_root.strip('/') 29 | self.auth = httpx.BasicAuth(username, password) 30 | self.client = httpx.AsyncClient( 31 | auth=self.auth, 32 | timeout=30.0, 33 | headers={"Content-Type": "application/json"} 34 | ) 35 | 36 | # Формируем базовый URL для HTTP-сервиса 37 | self.service_base_url = f"{self.base_url}/hs/{self.service_root}" 38 | logger.debug(f"Базовый URL HTTP-сервиса: {self.service_base_url}") 39 | 40 | async def check_health(self) -> bool: 41 | """Проверить состояние HTTP-сервиса 1С. 42 | 43 | Returns: 44 | True, если сервис доступен и здоров, иначе вызывает исключение. 45 | """ 46 | try: 47 | url = f"{self.service_base_url}/health" 48 | logger.debug(f"Запрос состояния здоровья: {url}") 49 | 50 | response = await self.client.get(url) 51 | response.raise_for_status() 52 | 53 | # Проверяем JSON ответ от 1C healthGET 54 | try: 55 | response_json = response.json() 56 | if response_json.get("status") == "ok": 57 | logger.debug("Сервис 1С доступен и здоров (статус OK).") 58 | return True 59 | else: 60 | logger.warning(f"1C health check вернул неожиданный статус: {response_json}") 61 | raise httpx.HTTPStatusError(f"1C service reported not healthy: {response_json}", request=response.request, response=response) 62 | except json.JSONDecodeError as e: 63 | logger.error(f"Ошибка парсинга JSON ответа health-check 1С: {response.text}") 64 | raise httpx.HTTPStatusError(f"Invalid JSON response from 1C health check: {e}", request=response.request, response=response) 65 | 66 | except httpx.HTTPError as e: 67 | logger.error(f"Ошибка HTTP при проверке состояния 1С: {e}") 68 | raise 69 | 70 | async def call_rpc(self, method: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 71 | """Выполнить JSON-RPC запрос к 1С. 72 | 73 | Args: 74 | method: Имя метода 75 | params: Параметры метода 76 | 77 | Returns: 78 | Результат выполнения метода 79 | """ 80 | try: 81 | url = f"{self.service_base_url}/rpc" 82 | 83 | # Формируем JSON-RPC запрос 84 | rpc_request = { 85 | "jsonrpc": "2.0", 86 | "id": 1, 87 | "method": method, 88 | "params": params or {} 89 | } 90 | 91 | logger.debug(f"JSON-RPC запрос: {rpc_request}") 92 | 93 | response = await self.client.post(url, json=rpc_request) 94 | response.raise_for_status() 95 | 96 | rpc_response = response.json() 97 | logger.debug(f"JSON-RPC ответ: {rpc_response}") 98 | 99 | # Проверяем на ошибки JSON-RPC 100 | if "error" in rpc_response: 101 | error = rpc_response["error"] 102 | raise Exception(f"JSON-RPC ошибка {error.get('code', 'unknown')}: {error.get('message', 'Unknown error')}") 103 | 104 | return rpc_response.get("result", {}) 105 | 106 | except httpx.HTTPError as e: 107 | logger.error(f"Ошибка HTTP при вызове RPC: {e}") 108 | raise 109 | except json.JSONDecodeError as e: 110 | logger.error(f"Ошибка парсинга JSON ответа RPC: {e}") 111 | raise 112 | 113 | async def list_tools(self) -> List[types.Tool]: 114 | """Получить список доступных инструментов. 115 | 116 | Returns: 117 | Список инструментов MCP 118 | """ 119 | result = await self.call_rpc("tools/list") 120 | tools_data = result.get("tools", []) 121 | 122 | tools = [] 123 | for tool_data in tools_data: 124 | tool = types.Tool( 125 | name=tool_data["name"], 126 | description=tool_data.get("description", ""), 127 | inputSchema=tool_data.get("inputSchema", {}) 128 | ) 129 | tools.append(tool) 130 | 131 | return tools 132 | 133 | async def call_tool(self, name: str, arguments: Dict[str, Any]) -> types.CallToolResult: 134 | """Вызвать инструмент. 135 | 136 | Args: 137 | name: Имя инструмента 138 | arguments: Аргументы инструмента 139 | 140 | Returns: 141 | Результат выполнения инструмента 142 | """ 143 | result = await self.call_rpc("tools/call", { 144 | "name": name, 145 | "arguments": arguments 146 | }) 147 | 148 | # Преобразуем результат в формат MCP 149 | content = [] 150 | if "content" in result: 151 | for item in result["content"]: 152 | content_type = item.get("type") 153 | 154 | if content_type == "text": 155 | content.append(types.TextContent( 156 | type="text", 157 | text=item.get("text", "") 158 | )) 159 | 160 | elif content_type == "image": 161 | content.append(types.ImageContent( 162 | type="image", 163 | data=item.get("data", ""), 164 | mimeType=item.get("mimeType", "image/png") 165 | )) 166 | 167 | else: 168 | # Неизвестный тип - логируем предупреждение и обрабатываем как текст 169 | logger.warning(f"Неизвестный тип контента: {content_type}, обрабатываем как текст") 170 | content.append(types.TextContent( 171 | type="text", 172 | text=str(item.get("text", item)) 173 | )) 174 | 175 | return types.CallToolResult( 176 | content=content, 177 | isError=result.get("isError", False) 178 | ) 179 | 180 | async def list_resources(self) -> List[types.Resource]: 181 | """Получить список доступных ресурсов. 182 | 183 | Returns: 184 | Список ресурсов MCP 185 | """ 186 | result = await self.call_rpc("resources/list") 187 | resources_data = result.get("resources", []) 188 | 189 | resources = [] 190 | for resource_data in resources_data: 191 | resource = types.Resource( 192 | uri=resource_data["uri"], 193 | name=resource_data.get("name", ""), 194 | description=resource_data.get("description", ""), 195 | mimeType=resource_data.get("mimeType") 196 | ) 197 | resources.append(resource) 198 | 199 | return resources 200 | 201 | async def read_resource(self, uri: str) -> List[ReadResourceContents]: 202 | """Прочитать ресурс. 203 | 204 | Args: 205 | uri: URI ресурса 206 | 207 | Returns: 208 | Список частей содержимого ресурса (текст/бинарные данные) 209 | """ 210 | # MCP декоратор может передать сюда AnyUrl; приводим к строке перед JSON-RPC 211 | uri_str = str(uri) 212 | result = await self.call_rpc("resources/read", {"uri": uri_str}) 213 | 214 | # Преобразуем результат в Iterable[ReadResourceContents] для декоратора read_resource 215 | contents: List[ReadResourceContents] = [] 216 | if "contents" in result: 217 | for item in result["contents"]: 218 | content_type = item.get("type") 219 | mime_type = item.get("mimeType") 220 | if content_type == "text": 221 | contents.append(ReadResourceContents( 222 | content=item.get("text", ""), 223 | mime_type=mime_type or "text/plain" 224 | )) 225 | elif content_type == "blob": 226 | blob_b64 = item.get("blob", "") or "" 227 | try: 228 | data_bytes = base64.b64decode(blob_b64) 229 | except Exception: 230 | # В случае некорректной base64 — вернем как текст для диагностики 231 | contents.append(ReadResourceContents( 232 | content=f"Invalid base64 blob: length={len(blob_b64)}", 233 | mime_type="text/plain" 234 | )) 235 | else: 236 | contents.append(ReadResourceContents( 237 | content=data_bytes, 238 | mime_type=mime_type or "application/octet-stream" 239 | )) 240 | else: 241 | # Fallback: сериализуем как текст 242 | contents.append(ReadResourceContents( 243 | content=f"Unknown resource content type '{content_type}': {json.dumps(item, ensure_ascii=False)}", 244 | mime_type="text/plain" 245 | )) 246 | else: 247 | # Если сервер вернул не ожидаемую структуру — вернем весь результат текстом 248 | contents.append(ReadResourceContents( 249 | content=json.dumps(result, ensure_ascii=False), 250 | mime_type="application/json" 251 | )) 252 | 253 | return contents 254 | 255 | async def list_prompts(self) -> List[types.Prompt]: 256 | """Получить список доступных промптов. 257 | 258 | Returns: 259 | Список промптов MCP 260 | """ 261 | result = await self.call_rpc("prompts/list") 262 | prompts_data = result.get("prompts", []) 263 | 264 | prompts = [] 265 | for prompt_data in prompts_data: 266 | arguments = [] 267 | if "arguments" in prompt_data: 268 | for arg_data in prompt_data["arguments"]: 269 | arguments.append(types.PromptArgument( 270 | name=arg_data["name"], 271 | description=arg_data.get("description", ""), 272 | required=arg_data.get("required", False) 273 | )) 274 | 275 | prompt = types.Prompt( 276 | name=prompt_data["name"], 277 | description=prompt_data.get("description", ""), 278 | arguments=arguments 279 | ) 280 | prompts.append(prompt) 281 | 282 | return prompts 283 | 284 | async def get_prompt(self, name: str, arguments: Optional[Dict[str, str]] = None) -> types.GetPromptResult: 285 | """Получить промпт. 286 | 287 | Args: 288 | name: Имя промпта 289 | arguments: Аргументы промпта 290 | 291 | Returns: 292 | Результат промпта 293 | """ 294 | result = await self.call_rpc("prompts/get", { 295 | "name": name, 296 | "arguments": arguments or {} 297 | }) 298 | 299 | # Преобразуем результат в формат MCP 300 | messages = [] 301 | if "messages" in result: 302 | for msg_data in result["messages"]: 303 | content = types.TextContent( 304 | type="text", 305 | text=msg_data.get("content", {}).get("text", "") 306 | ) 307 | 308 | message = types.PromptMessage( 309 | role=msg_data.get("role", "user"), 310 | content=content 311 | ) 312 | messages.append(message) 313 | 314 | return types.GetPromptResult( 315 | description=result.get("description", ""), 316 | messages=messages 317 | ) 318 | 319 | async def close(self): 320 | """Закрыть клиент.""" 321 | await self.client.aclose() -------------------------------------------------------------------------------- /src/py_server/auth/oauth2.py: -------------------------------------------------------------------------------- 1 | """OAuth2 хранилище и сервис для авторизации.""" 2 | 3 | import asyncio 4 | import hashlib 5 | import base64 6 | import secrets 7 | import logging 8 | from dataclasses import dataclass 9 | from datetime import datetime, timedelta 10 | from typing import Optional, Dict, Tuple 11 | from urllib.parse import urlencode 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | @dataclass 17 | class AuthCodeData: 18 | """Данные authorization code.""" 19 | login: str 20 | password: str 21 | redirect_uri: str 22 | code_challenge: str 23 | exp: datetime 24 | 25 | 26 | @dataclass 27 | class AccessTokenData: 28 | """Данные access token.""" 29 | login: str 30 | password: str 31 | exp: datetime 32 | 33 | 34 | @dataclass 35 | class RefreshTokenData: 36 | """Данные refresh token.""" 37 | login: str 38 | password: str 39 | exp: datetime 40 | rotation_counter: int = 0 41 | 42 | 43 | class OAuth2Store: 44 | """In-memory хранилище для OAuth2 токенов и кодов.""" 45 | 46 | def __init__(self): 47 | """Инициализация хранилища.""" 48 | self.auth_codes: Dict[str, AuthCodeData] = {} 49 | self.access_tokens: Dict[str, AccessTokenData] = {} 50 | self.refresh_tokens: Dict[str, RefreshTokenData] = {} 51 | self._cleanup_task: Optional[asyncio.Task] = None 52 | 53 | async def start_cleanup_task(self, interval: int = 60): 54 | """Запустить периодическую очистку устаревших токенов. 55 | 56 | Args: 57 | interval: Интервал очистки в секундах 58 | """ 59 | self._cleanup_task = asyncio.create_task(self._cleanup_loop(interval)) 60 | logger.debug(f"Запущена задача очистки OAuth2 токенов (интервал: {interval}s)") 61 | 62 | async def stop_cleanup_task(self): 63 | """Остановить задачу очистки.""" 64 | if self._cleanup_task: 65 | self._cleanup_task.cancel() 66 | try: 67 | await self._cleanup_task 68 | except asyncio.CancelledError: 69 | pass 70 | logger.debug("Задача очистки OAuth2 токенов остановлена") 71 | 72 | async def _cleanup_loop(self, interval: int): 73 | """Периодическая очистка устаревших токенов.""" 74 | while True: 75 | try: 76 | await asyncio.sleep(interval) 77 | self._cleanup_expired() 78 | except asyncio.CancelledError: 79 | break 80 | except Exception as e: 81 | logger.error(f"Ошибка при очистке токенов: {e}") 82 | 83 | def _cleanup_expired(self): 84 | """Удалить устаревшие токены и коды.""" 85 | now = datetime.now() 86 | 87 | # Очистка authorization codes 88 | expired_codes = [code for code, data in self.auth_codes.items() if data.exp < now] 89 | for code in expired_codes: 90 | del self.auth_codes[code] 91 | 92 | # Очистка access tokens 93 | expired_access = [token for token, data in self.access_tokens.items() if data.exp < now] 94 | for token in expired_access: 95 | del self.access_tokens[token] 96 | 97 | # Очистка refresh tokens 98 | expired_refresh = [token for token, data in self.refresh_tokens.items() if data.exp < now] 99 | for token in expired_refresh: 100 | del self.refresh_tokens[token] 101 | 102 | if expired_codes or expired_access or expired_refresh: 103 | logger.debug(f"Очищено токенов: codes={len(expired_codes)}, access={len(expired_access)}, refresh={len(expired_refresh)}") 104 | 105 | def save_auth_code(self, code: str, data: AuthCodeData): 106 | """Сохранить authorization code.""" 107 | self.auth_codes[code] = data 108 | logger.debug(f"Сохранён authorization code для {data.login}, expires в {data.exp}") 109 | 110 | def get_auth_code(self, code: str) -> Optional[AuthCodeData]: 111 | """Получить и удалить authorization code (одноразовый).""" 112 | data = self.auth_codes.pop(code, None) 113 | if data and data.exp < datetime.now(): 114 | logger.debug(f"Authorization code истёк: {code}") 115 | return None 116 | return data 117 | 118 | def save_access_token(self, token: str, data: AccessTokenData): 119 | """Сохранить access token.""" 120 | self.access_tokens[token] = data 121 | logger.debug(f"Сохранён access token для {data.login}, expires в {data.exp}") 122 | 123 | def get_access_token(self, token: str) -> Optional[AccessTokenData]: 124 | """Получить access token.""" 125 | data = self.access_tokens.get(token) 126 | if data and data.exp < datetime.now(): 127 | logger.debug(f"Access token истёк: {token[:16]}...") 128 | del self.access_tokens[token] 129 | return None 130 | return data 131 | 132 | def save_refresh_token(self, token: str, data: RefreshTokenData): 133 | """Сохранить refresh token.""" 134 | self.refresh_tokens[token] = data 135 | logger.debug(f"Сохранён refresh token для {data.login}, expires в {data.exp}") 136 | 137 | def get_refresh_token(self, token: str) -> Optional[RefreshTokenData]: 138 | """Получить и удалить refresh token (ротация).""" 139 | data = self.refresh_tokens.pop(token, None) 140 | if data and data.exp < datetime.now(): 141 | logger.debug(f"Refresh token истёк: {token[:16]}...") 142 | return None 143 | return data 144 | 145 | 146 | class OAuth2Service: 147 | """Сервис OAuth2 для авторизации.""" 148 | 149 | def __init__(self, store: OAuth2Store, code_ttl: int = 120, access_ttl: int = 3600, refresh_ttl: int = 1209600): 150 | """Инициализация сервиса. 151 | 152 | Args: 153 | store: Хранилище токенов 154 | code_ttl: TTL authorization code в секундах 155 | access_ttl: TTL access token в секундах 156 | refresh_ttl: TTL refresh token в секундах 157 | """ 158 | self.store = store 159 | self.code_ttl = code_ttl 160 | self.access_ttl = access_ttl 161 | self.refresh_ttl = refresh_ttl 162 | 163 | def generate_prm_document(self, public_url: str) -> dict: 164 | """Сгенерировать Protected Resource Metadata документ (RFC 9728). 165 | 166 | Args: 167 | public_url: Публичный URL прокси 168 | 169 | Returns: 170 | PRM документ 171 | """ 172 | public_url = public_url.rstrip('/') 173 | return { 174 | "resource": public_url, 175 | "authorization_servers": [public_url], 176 | "authorization_endpoint": f"{public_url}/authorize", 177 | "token_endpoint": f"{public_url}/token", 178 | "code_challenge_methods_supported": ["S256"] 179 | } 180 | 181 | def generate_authorization_code(self, login: str, password: str, redirect_uri: str, code_challenge: str) -> str: 182 | """Сгенерировать authorization code. 183 | 184 | Args: 185 | login: Логин пользователя 1С 186 | password: Пароль пользователя 1С 187 | redirect_uri: URI для редиректа 188 | code_challenge: PKCE challenge 189 | 190 | Returns: 191 | Authorization code 192 | """ 193 | code = secrets.token_urlsafe(32) 194 | exp = datetime.now() + timedelta(seconds=self.code_ttl) 195 | 196 | self.store.save_auth_code(code, AuthCodeData( 197 | login=login, 198 | password=password, 199 | redirect_uri=redirect_uri, 200 | code_challenge=code_challenge, 201 | exp=exp 202 | )) 203 | 204 | return code 205 | 206 | def validate_pkce(self, code_verifier: str, code_challenge: str) -> bool: 207 | """Валидировать PKCE S256. 208 | 209 | Args: 210 | code_verifier: Verifier от клиента 211 | code_challenge: Challenge из authorization code 212 | 213 | Returns: 214 | True если валидно 215 | """ 216 | # Вычисляем SHA256 от verifier 217 | verifier_hash = hashlib.sha256(code_verifier.encode('ascii')).digest() 218 | # Base64url encoding (без padding) 219 | computed_challenge = base64.urlsafe_b64encode(verifier_hash).decode('ascii').rstrip('=') 220 | 221 | return computed_challenge == code_challenge 222 | 223 | def exchange_code_for_tokens(self, code: str, redirect_uri: str, code_verifier: str) -> Optional[Tuple[str, str, int, str]]: 224 | """Обменять authorization code на токены. 225 | 226 | Args: 227 | code: Authorization code 228 | redirect_uri: Redirect URI (должен совпадать) 229 | code_verifier: PKCE verifier 230 | 231 | Returns: 232 | Tuple (access_token, token_type, expires_in, refresh_token) или None 233 | """ 234 | # Получаем код (одноразовый) 235 | code_data = self.store.get_auth_code(code) 236 | if not code_data: 237 | logger.warning("Недействительный или истёкший authorization code") 238 | return None 239 | 240 | # Проверяем redirect_uri 241 | if code_data.redirect_uri != redirect_uri: 242 | logger.warning(f"Несовпадение redirect_uri: ожидался {code_data.redirect_uri}, получен {redirect_uri}") 243 | return None 244 | 245 | # Валидируем PKCE 246 | if not self.validate_pkce(code_verifier, code_data.code_challenge): 247 | logger.warning("PKCE валидация не прошла") 248 | return None 249 | 250 | # Генерируем токены 251 | access_token = secrets.token_urlsafe(32) 252 | refresh_token = secrets.token_urlsafe(32) 253 | 254 | access_exp = datetime.now() + timedelta(seconds=self.access_ttl) 255 | refresh_exp = datetime.now() + timedelta(seconds=self.refresh_ttl) 256 | 257 | self.store.save_access_token(access_token, AccessTokenData( 258 | login=code_data.login, 259 | password=code_data.password, 260 | exp=access_exp 261 | )) 262 | 263 | self.store.save_refresh_token(refresh_token, RefreshTokenData( 264 | login=code_data.login, 265 | password=code_data.password, 266 | exp=refresh_exp, 267 | rotation_counter=0 268 | )) 269 | 270 | logger.debug(f"Выданы токены для пользователя {code_data.login}") 271 | return (access_token, "Bearer", self.access_ttl, refresh_token) 272 | 273 | def refresh_tokens(self, refresh_token: str) -> Optional[Tuple[str, str, int, str]]: 274 | """Обновить токены по refresh token. 275 | 276 | Args: 277 | refresh_token: Refresh token 278 | 279 | Returns: 280 | Tuple (access_token, token_type, expires_in, new_refresh_token) или None 281 | """ 282 | # Получаем refresh token (с ротацией - удаляется) 283 | refresh_data = self.store.get_refresh_token(refresh_token) 284 | if not refresh_data: 285 | logger.warning("Недействительный или истёкший refresh token") 286 | return None 287 | 288 | # Генерируем новые токены 289 | new_access_token = secrets.token_urlsafe(32) 290 | new_refresh_token = secrets.token_urlsafe(32) 291 | 292 | access_exp = datetime.now() + timedelta(seconds=self.access_ttl) 293 | refresh_exp = datetime.now() + timedelta(seconds=self.refresh_ttl) 294 | 295 | self.store.save_access_token(new_access_token, AccessTokenData( 296 | login=refresh_data.login, 297 | password=refresh_data.password, 298 | exp=access_exp 299 | )) 300 | 301 | self.store.save_refresh_token(new_refresh_token, RefreshTokenData( 302 | login=refresh_data.login, 303 | password=refresh_data.password, 304 | exp=refresh_exp, 305 | rotation_counter=refresh_data.rotation_counter + 1 306 | )) 307 | 308 | logger.debug(f"Обновлены токены для пользователя {refresh_data.login} (rotation #{refresh_data.rotation_counter + 1})") 309 | return (new_access_token, "Bearer", self.access_ttl, new_refresh_token) 310 | 311 | def validate_access_token(self, token: str) -> Optional[Tuple[str, str]]: 312 | """Валидировать access token и получить креды 1С. 313 | 314 | Args: 315 | token: Access token 316 | 317 | Returns: 318 | Tuple (login, password) или None 319 | """ 320 | token_data = self.store.get_access_token(token) 321 | if not token_data: 322 | return None 323 | 324 | return (token_data.login, token_data.password) 325 | 326 | -------------------------------------------------------------------------------- /src/py_server/README.md: -------------------------------------------------------------------------------- 1 | # MCP-прокси сервер для 1С 2 | 3 | ## Что это 4 | 5 | Прокси-сервер между MCP-клиентами (Claude Desktop, Cursor) и 1С:Предприятие. Транслирует MCP-протокол в JSON-RPC вызовы к HTTP-сервису 1С. 6 | 7 | **Возможности:** 8 | - Два транспорта: stdio (для нативных клиентов) и HTTP (для веб) 9 | - Проксирование всех MCP-примитивов: Tools, Resources, Prompts 10 | - Опциональная OAuth2 авторизация с per-user креденшилами 11 | - Асинхронная архитектура для множественных подключений 12 | 13 | ## Быстрый старт 14 | 15 | ### Требования 16 | 17 | - **Python 3.13** (рекомендуется) или 3.11+ 18 | - 1С:Предприятие 8.3.20+ с опубликованным HTTP-сервисом 19 | 20 | ### Установка 21 | 22 | ```bash 23 | # Создание виртуального окружения 24 | python -m venv venv 25 | 26 | # Активация 27 | venv\Scripts\activate # Windows 28 | source venv/bin/activate # Linux/Mac 29 | 30 | # Установка зависимостей 31 | pip install -r requirements.txt 32 | ``` 33 | 34 | ### Выбор режима работы 35 | 36 | #### Stdio режим 37 | 38 | Для локальных MCP-клиентов (Claude Desktop, Cursor). 39 | 40 | Настройки указываются в конфигурации клиента через переменные окружения. 41 | 42 | **Минимальная конфигурация клиента:** 43 | ```json 44 | { 45 | "mcpServers": { 46 | "1c-server": { 47 | "command": "python", 48 | "args": ["-m", "src.py_server"], 49 | "env": { 50 | "MCP_ONEC_URL": "http://localhost/base", 51 | "MCP_ONEC_USERNAME": "admin", 52 | "MCP_ONEC_PASSWORD": "password" 53 | } 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | Примеры конфигураций для разных клиентов: [`../../mcp_client_settings/`](../../mcp_client_settings/) 60 | 61 | #### HTTP режим 62 | 63 | Для веб-приложений и множественных клиентов. 64 | 65 | Настройки указываются в файле `.env` в корне проекта или через переменные окружения: 66 | 67 | ```bash 68 | # Скопируйте пример 69 | copy src\py_server\env.example .env # Windows 70 | cp src/py_server/env.example .env # Linux/Mac 71 | ``` 72 | 73 | **Минимальный .env:** 74 | ```ini 75 | MCP_ONEC_URL=http://localhost/base 76 | MCP_ONEC_USERNAME=admin 77 | MCP_ONEC_PASSWORD=password 78 | ``` 79 | 80 | **Запуск:** 81 | ```bash 82 | python -m src.py_server http --port 8000 83 | ``` 84 | 85 | ### Docker 86 | 87 | Запуск в контейнере для изоляции и упрощения развертывания. 88 | 89 | **Быстрый старт:** 90 | ```bash 91 | # 1. Скопировать конфигурацию 92 | cp .env.docker.example .env 93 | 94 | # 2. Отредактировать .env (обязательно: MCP_ONEC_URL, MCP_ONEC_USERNAME, MCP_ONEC_PASSWORD) 95 | 96 | # 3. Запустить через docker-compose 97 | docker-compose up -d 98 | 99 | # Проверка 100 | curl http://localhost:8000/health 101 | ``` 102 | 103 | **Или напрямую через Docker:** 104 | ```bash 105 | # Сборка образа 106 | docker build -t 1c-mcp-proxy . 107 | 108 | # Запуск с переменными окружения 109 | docker run -d \ 110 | -p 8000:8000 \ 111 | -e MCP_ONEC_URL=http://host.docker.internal/base \ 112 | -e MCP_ONEC_USERNAME=admin \ 113 | -e MCP_ONEC_PASSWORD=password \ 114 | --name mcp-proxy \ 115 | 1c-mcp-proxy 116 | ``` 117 | 118 | **Важно про сеть:** 119 | - Если 1С на **том же хосте**: используйте `host.docker.internal` (Mac/Windows) или IP хоста `172.17.0.1` (Linux) вместо `localhost` 120 | - Если 1С на **другом сервере**: указывайте его реальный адрес как обычно 121 | 122 | **Логи:** 123 | ```bash 124 | docker-compose logs -f 125 | ``` 126 | 127 | **Остановка:** 128 | ```bash 129 | docker-compose down 130 | ``` 131 | 132 | ## Режимы работы 133 | 134 | ### Stdio режим 135 | 136 | - Общение через stdin/stdout 137 | - Используется локальными MCP-клиентами 138 | - Логи идут в stderr 139 | 140 | ### HTTP режим 141 | 142 | **Endpoints:** 143 | - `/mcp/` - Streamable HTTP транспорт (основной) 144 | - `/sse` - SSE транспорт (устаревший, но поддерживается) 145 | - `/health` - проверка состояния 146 | - `/info` - информация о сервере 147 | - `/` - список endpoints 148 | 149 | **Проверка работы:** 150 | ```bash 151 | curl http://localhost:8000/health 152 | ``` 153 | 154 | ## Режимы авторизации 155 | 156 | ### Без OAuth2 (по умолчанию) 157 | 158 | ```bash 159 | MCP_AUTH_MODE=none # по умолчанию 160 | ``` 161 | 162 | **Поведение:** 163 | - Все обращения к 1С выполняются от одного пользователя 164 | - Креденшилы задаются в конфигурации: `MCP_ONEC_USERNAME` и `MCP_ONEC_PASSWORD` 165 | - Используется Basic Auth для всех запросов к 1С 166 | 167 | ### С OAuth2 168 | 169 | ```bash 170 | MCP_AUTH_MODE=oauth2 171 | MCP_PUBLIC_URL=http://your-server:8000 172 | ``` 173 | 174 | **Поведение:** 175 | - Каждый клиент авторизуется своими креденшилами 1С 176 | - Креденшилы передаются через OAuth2 flow 177 | - `MCP_ONEC_USERNAME` и `MCP_ONEC_PASSWORD` не используются (опциональны для резервного подключения) 178 | 179 | **Поддерживаемые OAuth2 flows:** 180 | - **Password Grant** - передача username/password напрямую 181 | - **Authorization Code + PKCE** - авторизация через HTML-форму 182 | - **Dynamic Client Registration** - автоматическая регистрация клиентов 183 | 184 | **Дополнительные endpoints (для OAuth2):** 185 | - `/.well-known/oauth-protected-resource` - Protected Resource Metadata 186 | - `/.well-known/oauth-authorization-server` - Authorization Server Metadata 187 | - `/register` - регистрация клиентов 188 | - `/authorize` - HTML форма авторизации 189 | - `/token` - получение/обновление токенов 190 | 191 | Детали OAuth2: см. раздел "Примеры использования" и `agents.md` 192 | 193 | ## Конфигурация 194 | 195 | Все настройки задаются через переменные окружения с префиксом `MCP_` или через CLI аргументы. 196 | 197 | ### Подключение к 1С 198 | 199 | | Переменная | Описание | По умолчанию | Обязательная | 200 | |------------|----------|--------------|--------------| 201 | | `MCP_ONEC_URL` | URL базы 1С | - | ✅ Всегда | 202 | | `MCP_ONEC_USERNAME` | Имя пользователя | - | ✅ При `AUTH_MODE=none` | 203 | | `MCP_ONEC_PASSWORD` | Пароль | - | ✅ При `AUTH_MODE=none` | 204 | | `MCP_ONEC_SERVICE_ROOT` | Корень HTTP-сервиса | `mcp` | ❌ | 205 | 206 | ### HTTP-сервер 207 | 208 | | Переменная | Описание | По умолчанию | Обязательная | 209 | |------------|----------|--------------|--------------| 210 | | `MCP_HOST` | Хост для прослушивания | `127.0.0.1` | ❌ | 211 | | `MCP_PORT` | Порт | `8000` | ❌ | 212 | | `MCP_CORS_ORIGINS` | CORS origins (JSON array) | `["*"]` | ❌ | 213 | 214 | ### MCP 215 | 216 | | Переменная | Описание | По умолчанию | Обязательная | 217 | |------------|----------|--------------|--------------| 218 | | `MCP_SERVER_NAME` | Имя сервера | `1C Configuration Data Tools` | ❌ | 219 | | `MCP_SERVER_VERSION` | Версия | `1.0.0` | ❌ | 220 | | `MCP_LOG_LEVEL` | Уровень логирования | `INFO` | ❌ | 221 | 222 | Допустимые уровни: `DEBUG`, `INFO`, `WARNING`, `ERROR` 223 | 224 | ### OAuth2 225 | 226 | | Переменная | Описание | По умолчанию | Обязательная | 227 | |------------|----------|--------------|--------------| 228 | | `MCP_AUTH_MODE` | Режим: `none` или `oauth2` | `none` | ❌ | 229 | | `MCP_PUBLIC_URL` | Публичный URL прокси | (определяется из запроса) | ✅ При `AUTH_MODE=oauth2` для HTTP режима | 230 | | `MCP_OAUTH2_CODE_TTL` | TTL authorization code (сек) | `120` | ❌ | 231 | | `MCP_OAUTH2_ACCESS_TTL` | TTL access token (сек) | `3600` | ❌ | 232 | | `MCP_OAUTH2_REFRESH_TTL` | TTL refresh token (сек) | `1209600` | ❌ | 233 | 234 | ### CLI аргументы 235 | 236 | Переопределяют переменные окружения: 237 | 238 | ```bash 239 | python -m src.py_server http \ 240 | --onec-url http://server/base \ 241 | --onec-username admin \ 242 | --onec-password secret \ 243 | --auth-mode oauth2 \ 244 | --public-url http://proxy:8000 \ 245 | --port 8000 \ 246 | --log-level DEBUG 247 | ``` 248 | 249 | Полный список аргументов: 250 | ```bash 251 | python -m src.py_server --help 252 | ``` 253 | 254 | ## Архитектура 255 | 256 | ### Общая схема 257 | 258 | ``` 259 | ┌─────────────────┐ 260 | │ MCP Client │ (Claude Desktop, Cursor) 261 | │ (stdio/HTTP) │ 262 | └────────┬────────┘ 263 | │ MCP Protocol 264 | ↓ 265 | ┌────────────────────┐ 266 | │ Python Proxy │ 267 | │ - mcp_server │ Проксирование MCP → JSON-RPC 268 | │ - http_server │ HTTP/SSE транспорты + OAuth2 269 | │ - stdio_server │ Stdio транспорт 270 | │ - onec_client │ HTTP-клиент для 1С 271 | └────────┬───────────┘ 272 | │ JSON-RPC over HTTP 273 | │ Basic Auth (username:password) 274 | ↓ 275 | ┌────────────────────┐ 276 | │ 1C HTTP Service │ /hs/mcp/rpc 277 | │ (расширение) │ 278 | └────────────────────┘ 279 | ``` 280 | 281 | ### Модули 282 | 283 | - **`main.py`** - CLI парсинг и запуск 284 | - **`config.py`** - конфигурация через Pydantic 285 | - **`mcp_server.py`** - ядро MCP-сервера (проксирование) 286 | - **`onec_client.py`** - асинхронный HTTP-клиент для 1С 287 | - **`http_server.py`** - HTTP/SSE транспорт + OAuth2 288 | - **`stdio_server.py`** - stdio транспорт 289 | - **`auth/oauth2.py`** - OAuth2 авторизация (Store + Service) 290 | 291 | ### Проксирование MCP-примитивов 292 | 293 | Все MCP-запросы транслируются в JSON-RPC к 1С: 294 | 295 | **Tools (инструменты):** 296 | - `tools/list` → список доступных инструментов 297 | - `tools/call` → вызов инструмента с аргументами 298 | 299 | **Resources (ресурсы):** 300 | - `resources/list` → список доступных ресурсов 301 | - `resources/read` → чтение содержимого ресурса 302 | 303 | **Prompts (промпты):** 304 | - `prompts/list` → список доступных промптов 305 | - `prompts/get` → получение промпта с параметрами 306 | 307 | ## Примеры использования 308 | 309 | ### Проверка подключения к 1С 310 | 311 | ```bash 312 | # HTTP режим 313 | curl http://localhost:8000/health 314 | 315 | # Ожидаемый ответ 316 | { 317 | "status": "healthy", 318 | "onec_connection": "ok", 319 | "auth": {"mode": "none"} 320 | } 321 | ``` 322 | 323 | ### Информация о сервере 324 | 325 | ```bash 326 | curl http://localhost:8000/info 327 | ``` 328 | 329 | ### OAuth2: Password Grant (упрощённый) 330 | 331 | ```bash 332 | # 1. Получить токен 333 | curl -X POST http://localhost:8000/token \ 334 | -d "grant_type=password" \ 335 | -d "username=admin" \ 336 | -d "password=secret" 337 | 338 | # Ответ: 339 | # { 340 | # "access_token": "simple_...", 341 | # "token_type": "Bearer", 342 | # "expires_in": 86400, 343 | # "scope": "mcp" 344 | # } 345 | 346 | # 2. Использовать токен для доступа 347 | curl http://localhost:8000/mcp/ \ 348 | -H "Authorization: Bearer " 349 | ``` 350 | 351 | ### OAuth2: Authorization Code + PKCE (стандартный) 352 | 353 | ```bash 354 | # 1. Discovery 355 | curl http://localhost:8000/.well-known/oauth-authorization-server 356 | 357 | # 2. Регистрация клиента 358 | curl -X POST http://localhost:8000/register \ 359 | -H "Content-Type: application/json" \ 360 | -d '{"client_name": "My Client"}' 361 | 362 | # 3. Авторизация (в браузере) 363 | # http://localhost:8000/authorize?response_type=code&client_id=mcp-public-client&... 364 | 365 | # 4. Обмен кода на токены 366 | curl -X POST http://localhost:8000/token \ 367 | -d "grant_type=authorization_code" \ 368 | -d "code=" \ 369 | -d "redirect_uri=http://localhost/callback" \ 370 | -d "code_verifier=" 371 | ``` 372 | 373 | ### Логирование 374 | 375 | ```bash 376 | # DEBUG режим для отладки 377 | python -m src.py_server http --log-level DEBUG 378 | 379 | # Логи показывают: 380 | # - Все HTTP запросы к 1С 381 | # - OAuth2 операции (генерация/валидация токенов) 382 | # - MCP операции (tools/resources/prompts) 383 | # - Ошибки подключения 384 | ``` 385 | 386 | ## Интеграция с 1С 387 | 388 | Прокси ожидает HTTP-сервис в 1С по адресу: 389 | ``` 390 | {MCP_ONEC_URL}/hs/{MCP_ONEC_SERVICE_ROOT}/ 391 | ``` 392 | 393 | Например: `http://localhost/base/hs/mcp/` 394 | 395 | ### Endpoints 1С 396 | 397 | 1. **`GET /health`** 398 | - Проверка доступности сервиса 399 | - Ответ: `{"status": "ok"}` 400 | - Используется для валидации креденшилов в OAuth2 401 | 402 | 2. **`POST /rpc`** 403 | - JSON-RPC endpoint для всех MCP-операций 404 | - Content-Type: `application/json` 405 | - Basic Auth: `username:password` 406 | 407 | ### Формат JSON-RPC запроса 408 | 409 | ```json 410 | { 411 | "jsonrpc": "2.0", 412 | "id": 1, 413 | "method": "tools/list", 414 | "params": {} 415 | } 416 | ``` 417 | 418 | ### Формат JSON-RPC ответа 419 | 420 | ```json 421 | { 422 | "jsonrpc": "2.0", 423 | "id": 1, 424 | "result": { 425 | "tools": [ 426 | { 427 | "name": "get_metadata", 428 | "description": "Получить метаданные объекта", 429 | "inputSchema": {...} 430 | } 431 | ] 432 | } 433 | } 434 | ``` 435 | 436 | Подробности реализации 1С-стороны: `../1c_ext/agents.md` 437 | 438 | ## Документация 439 | 440 | ### Для разработчиков 441 | 442 | - **`agents.md`** - полная документация архитектуры для AI-агентов 443 | - Детальное описание всех модулей 444 | - Протоколы взаимодействия 445 | - OAuth2 flows 446 | - Точки расширения 447 | 448 | ### Конфигурация 449 | 450 | - **`env.example`** - пример `.env` файла со всеми параметрами 451 | 452 | --- 453 | 454 | **MIT License** 455 | 456 | Проект активно развивается. Вопросы и предложения приветствуются через Issues. 457 | -------------------------------------------------------------------------------- /src/1c_ext/CommonModules/mcp_Метаданные/Ext/Module.bsl: -------------------------------------------------------------------------------- 1 | #Область ПрограммныйИнтерфейс 2 | 3 | #Область СозданиеТаблицОписанияОбъектов 4 | 5 | // Создает пустую таблицу для описания инструментов 6 | // 7 | // Возвращаемое значение: 8 | // ТаблицаЗначений - таблица с колонками: 9 | // * ИмяОбработкиКонтейнера - Строка 10 | // * Имя - Строка 11 | // * Описание - Строка 12 | // * СхемаПараметров - Строка 13 | // 14 | Функция ТаблицаИнструментов() Экспорт 15 | 16 | ТаблицаИнструментов = Новый ТаблицаЗначений; 17 | ТаблицаИнструментов.Колонки.Добавить("ИмяОбработкиКонтейнера", Новый ОписаниеТипов("Строка")); 18 | ТаблицаИнструментов.Колонки.Добавить("Имя", Новый ОписаниеТипов("Строка")); 19 | ТаблицаИнструментов.Колонки.Добавить("Описание", Новый ОписаниеТипов("Строка")); 20 | ТаблицаИнструментов.Колонки.Добавить("СхемаПараметров", Новый ОписаниеТипов("Строка")); 21 | 22 | Возврат ТаблицаИнструментов; 23 | 24 | КонецФункции 25 | 26 | // Создает пустую таблицу для описания ресурсов 27 | // 28 | // Возвращаемое значение: 29 | // ТаблицаЗначений - таблица с колонками: 30 | // * ИмяОбработкиКонтейнера - Строка 31 | // * Адрес - Строка 32 | // * Имя - Строка 33 | // * Описание - Строка 34 | // 35 | Функция ТаблицаРесурсов() Экспорт 36 | 37 | ТаблицаРесурсов = Новый ТаблицаЗначений; 38 | ТаблицаРесурсов.Колонки.Добавить("ИмяОбработкиКонтейнера", Новый ОписаниеТипов("Строка")); 39 | ТаблицаРесурсов.Колонки.Добавить("Адрес", Новый ОписаниеТипов("Строка")); 40 | ТаблицаРесурсов.Колонки.Добавить("Имя", Новый ОписаниеТипов("Строка")); 41 | ТаблицаРесурсов.Колонки.Добавить("Описание", Новый ОписаниеТипов("Строка")); 42 | 43 | Возврат ТаблицаРесурсов; 44 | 45 | КонецФункции 46 | 47 | // Создает пустую таблицу для описания промптов 48 | // 49 | // Возвращаемое значение: 50 | // ТаблицаЗначений - таблица с колонками: 51 | // * ИмяОбработкиКонтейнера - Строка 52 | // * Имя - Строка 53 | // * Описание - Строка 54 | // * Параметры - Строка 55 | // 56 | Функция ТаблицаПромптов() Экспорт 57 | 58 | ТаблицаПромптов = Новый ТаблицаЗначений; 59 | ТаблицаПромптов.Колонки.Добавить("ИмяОбработкиКонтейнера", Новый ОписаниеТипов("Строка")); 60 | ТаблицаПромптов.Колонки.Добавить("Имя", Новый ОписаниеТипов("Строка")); 61 | ТаблицаПромптов.Колонки.Добавить("Описание", Новый ОписаниеТипов("Строка")); 62 | ТаблицаПромптов.Колонки.Добавить("Параметры", Новый ОписаниеТипов("Строка")); 63 | 64 | Возврат ТаблицаПромптов; 65 | 66 | КонецФункции 67 | 68 | #КонецОбласти 69 | 70 | #Область ДобавлениеСтрок 71 | 72 | // Добавляет строку в таблицу инструментов 73 | // 74 | // Параметры: 75 | // ТаблицаИнструментов - ТаблицаЗначений - таблица инструментов 76 | // Имя - Строка - имя инструмента 77 | // Описание - Строка - описание инструмента 78 | // СхемаПараметров - Строка - JSON-схема параметров 79 | // 80 | Процедура ДобавитьИнструмент(ТаблицаИнструментов, Имя, Описание, СхемаПараметров) Экспорт 81 | 82 | НоваяСтрока = ТаблицаИнструментов.Добавить(); 83 | НоваяСтрока.Имя = Имя; 84 | НоваяСтрока.Описание = Описание; 85 | НоваяСтрока.СхемаПараметров = СхемаПараметров; 86 | 87 | КонецПроцедуры 88 | 89 | // Добавляет строку в таблицу ресурсов 90 | // 91 | // Параметры: 92 | // ТаблицаРесурсов - ТаблицаЗначений - таблица ресурсов 93 | // Адрес - Строка - адрес ресурса 94 | // Имя - Строка - имя ресурса 95 | // Описание - Строка - описание ресурса 96 | // 97 | Процедура ДобавитьРесурс(ТаблицаРесурсов, Адрес, Имя, Описание) Экспорт 98 | 99 | НоваяСтрока = ТаблицаРесурсов.Добавить(); 100 | НоваяСтрока.Адрес = Адрес; 101 | НоваяСтрока.Имя = Имя; 102 | НоваяСтрока.Описание = Описание; 103 | 104 | КонецПроцедуры 105 | 106 | // Добавляет строку в таблицу промптов 107 | // 108 | // Параметры: 109 | // ТаблицаПромптов - ТаблицаЗначений - таблица промптов 110 | // Имя - Строка - имя промпта 111 | // Описание - Строка - описание промпта 112 | // Параметры - Строка - JSON-описание параметров 113 | // 114 | Процедура ДобавитьПромпт(ТаблицаПромптов, Имя, Описание, Параметры) Экспорт 115 | 116 | НоваяСтрока = ТаблицаПромптов.Добавить(); 117 | НоваяСтрока.Имя = Имя; 118 | НоваяСтрока.Описание = Описание; 119 | НоваяСтрока.Параметры = Параметры; 120 | 121 | КонецПроцедуры 122 | 123 | // Заполняет таблицу инструментов из обработок-контейнеров 124 | // 125 | // Параметры: 126 | // ТаблицаИнструментов - ТаблицаЗначений - таблица инструментов для заполнения 127 | // 128 | Процедура ЗаполнитьТаблицуИнструментов(ТаблицаИнструментов) Экспорт 129 | 130 | СоставПодсистемыКонтейнеры = Метаданные.Подсистемы.mcp_MCPСервер.Подсистемы.mcp_КонтейнерыИнструментов.Состав; 131 | 132 | Для Каждого МД Из СоставПодсистемыКонтейнеры Цикл 133 | 134 | Если НЕ Метаданные.Обработки.Содержит(МД) Тогда 135 | Продолжить; 136 | КонецЕсли; 137 | 138 | МенеджерОбработки = Обработки[МД.Имя]; 139 | 140 | // Создаем отдельную таблицу для текущей обработки 141 | ТекущаяТаблицаИнструментов = ТаблицаИнструментов.СкопироватьКолонки(); 142 | 143 | // Вызываем метод добавления инструментов в обработке 144 | МенеджерОбработки.ДобавитьИнструменты(ТекущаяТаблицаИнструментов); 145 | 146 | // Заполняем имя обработки-контейнера и переносим в общую таблицу 147 | Для Каждого Строка Из ТекущаяТаблицаИнструментов Цикл 148 | СтрокаОбщейТаблицы = ТаблицаИнструментов.Добавить(); 149 | ЗаполнитьЗначенияСвойств(СтрокаОбщейТаблицы, Строка); 150 | СтрокаОбщейТаблицы.ИмяОбработкиКонтейнера = МД.Имя; 151 | КонецЦикла; 152 | 153 | КонецЦикла; 154 | 155 | КонецПроцедуры 156 | 157 | // Заполняет таблицу ресурсов из обработок-контейнеров 158 | // 159 | // Параметры: 160 | // ТаблицаРесурсов - ТаблицаЗначений - таблица ресурсов для заполнения 161 | // 162 | Процедура ЗаполнитьТаблицуРесурсов(ТаблицаРесурсов) Экспорт 163 | 164 | СоставПодсистемыКонтейнеры = Метаданные.Подсистемы.mcp_MCPСервер.Подсистемы.mcp_КонтейнерыРесурсов.Состав; 165 | 166 | Для Каждого МД Из СоставПодсистемыКонтейнеры Цикл 167 | 168 | Если НЕ Метаданные.Обработки.Содержит(МД) Тогда 169 | Продолжить; 170 | КонецЕсли; 171 | 172 | МенеджерОбработки = Обработки[МД.Имя]; 173 | 174 | // Проверяем наличие метода ДобавитьРесурсы 175 | Попытка 176 | // Создаем отдельную таблицу для текущей обработки 177 | ТекущаяТаблицаРесурсов = ТаблицаРесурсов.СкопироватьКолонки(); 178 | 179 | // Вызываем метод добавления ресурсов в обработке 180 | МенеджерОбработки.ДобавитьРесурсы(ТекущаяТаблицаРесурсов); 181 | 182 | // Заполняем имя обработки-контейнера и переносим в общую таблицу 183 | Для Каждого Строка Из ТекущаяТаблицаРесурсов Цикл 184 | СтрокаОбщейТаблицы = ТаблицаРесурсов.Добавить(); 185 | ЗаполнитьЗначенияСвойств(СтрокаОбщейТаблицы, Строка); 186 | СтрокаОбщейТаблицы.ИмяОбработкиКонтейнера = МД.Имя; 187 | КонецЦикла; 188 | Исключение 189 | // Метод ДобавитьРесурсы не реализован в обработке - пропускаем 190 | КонецПопытки; 191 | 192 | КонецЦикла; 193 | 194 | КонецПроцедуры 195 | 196 | // Заполняет таблицу промптов из обработок-контейнеров 197 | // 198 | // Параметры: 199 | // ТаблицаПромптов - ТаблицаЗначений - таблица промптов для заполнения 200 | // 201 | Процедура ЗаполнитьТаблицуПромптов(ТаблицаПромптов) Экспорт 202 | 203 | СоставПодсистемыКонтейнеры = Метаданные.Подсистемы.mcp_MCPСервер.Подсистемы.mcp_КонтейнерыПромптов.Состав; 204 | 205 | Для Каждого МД Из СоставПодсистемыКонтейнеры Цикл 206 | 207 | Если НЕ Метаданные.Обработки.Содержит(МД) Тогда 208 | Продолжить; 209 | КонецЕсли; 210 | 211 | МенеджерОбработки = Обработки[МД.Имя]; 212 | 213 | // Проверяем наличие метода ДобавитьПромпты 214 | Попытка 215 | // Создаем отдельную таблицу для текущей обработки 216 | ТекущаяТаблицаПромптов = ТаблицаПромптов.СкопироватьКолонки(); 217 | 218 | // Вызываем метод добавления промптов в обработке 219 | МенеджерОбработки.ДобавитьПромпты(ТекущаяТаблицаПромптов); 220 | 221 | // Заполняем имя обработки-контейнера и переносим в общую таблицу 222 | Для Каждого Строка Из ТекущаяТаблицаПромптов Цикл 223 | СтрокаОбщейТаблицы = ТаблицаПромптов.Добавить(); 224 | ЗаполнитьЗначенияСвойств(СтрокаОбщейТаблицы, Строка); 225 | СтрокаОбщейТаблицы.ИмяОбработкиКонтейнера = МД.Имя; 226 | КонецЦикла; 227 | Исключение 228 | // Метод ДобавитьПромпты не реализован в обработке - пропускаем 229 | КонецПопытки; 230 | 231 | КонецЦикла; 232 | 233 | КонецПроцедуры 234 | 235 | #КонецОбласти 236 | 237 | #Область СхемаПараметровИнструментов 238 | 239 | // Создает описание простого параметра для схемы инструмента 240 | // 241 | // Параметры: 242 | // Имя - Строка - имя параметра 243 | // Тип - Строка - тип параметра (string, number, boolean, etc.) 244 | // Описание - Строка - описание параметра 245 | // ЗначениеПоУмолчанию - Произвольный - значение по умолчанию (Неопределено, если нет) 246 | // Обязательный - Булево - признак обязательности параметра 247 | // СписокДопустимыхЗначений - Строка - список допустимых значений через запятую (пустая строка, если нет ограничений) 248 | // 249 | // Возвращаемое значение: 250 | // Структура - описание простого параметра 251 | // 252 | Функция ПараметрИнструмента(Имя, Тип = Неопределено, Описание = Неопределено, ЗначениеПоУмолчанию = Неопределено, Обязательный = Ложь, СписокДопустимыхЗначений = "") Экспорт 253 | 254 | ОписаниеПараметра = Новый Структура; 255 | ОписаниеПараметра.Вставить("ТипЭлемента", "ПростойПараметр"); 256 | ОписаниеПараметра.Вставить("Имя", Имя); 257 | ОписаниеПараметра.Вставить("Описание", Описание); 258 | ОписаниеПараметра.Вставить("Тип", Тип); 259 | ОписаниеПараметра.Вставить("ЗначениеПоУмолчанию", ЗначениеПоУмолчанию); 260 | ОписаниеПараметра.Вставить("Обязательный", Обязательный); 261 | ОписаниеПараметра.Вставить("СписокДопустимыхЗначений", СписокДопустимыхЗначений); 262 | 263 | Возврат ОписаниеПараметра; 264 | 265 | КонецФункции 266 | 267 | // Создает описание параметра-массива для схемы инструмента 268 | // 269 | // Параметры: 270 | // Имя - Строка - имя параметра 271 | // ТипЭлементаМассива - Строка - тип элементов массива 272 | // Описание - Строка - описание параметра 273 | // Обязательный - Булево - признак обязательности параметра 274 | // СписокДопустимыхЗначенийЭлемента - Строка - список допустимых значений для элементов через запятую 275 | // 276 | // Возвращаемое значение: 277 | // Структура - описание параметра-массива 278 | // 279 | Функция ПараметрИнструментаМассив(Имя, ТипЭлементаМассива = Неопределено, Описание = Неопределено, Обязательный = Ложь, СписокДопустимыхЗначенийЭлемента = "") Экспорт 280 | 281 | ОписаниеПараметра = Новый Структура; 282 | ОписаниеПараметра.Вставить("ТипЭлемента", "Массив"); 283 | ОписаниеПараметра.Вставить("Имя", Имя); 284 | ОписаниеПараметра.Вставить("Описание", Описание); 285 | ОписаниеПараметра.Вставить("ТипЭлементаМассива", ТипЭлементаМассива); 286 | ОписаниеПараметра.Вставить("Обязательный", Обязательный); 287 | ОписаниеПараметра.Вставить("СписокДопустимыхЗначенийЭлемента", СписокДопустимыхЗначенийЭлемента); 288 | 289 | Возврат ОписаниеПараметра; 290 | 291 | КонецФункции 292 | 293 | // Создает JSON-схему параметров инструмента из массива описаний параметров 294 | // 295 | // Параметры: 296 | // МассивОписанийПараметров - Массив из Структура - массив описаний параметров 297 | // 298 | // Возвращаемое значение: 299 | // Строка - JSON-схема параметров 300 | // 301 | Функция СхемаПараметровИнструмента(МассивОписанийПараметров) Экспорт 302 | 303 | Схема = Новый Структура; 304 | Схема.Вставить("type", "object"); 305 | 306 | Свойства = Новый Структура; 307 | ОбязательныеПараметры = Новый Массив; 308 | 309 | Для Каждого ОписаниеПараметра Из МассивОписанийПараметров Цикл 310 | 311 | ИмяПараметра = ОписаниеПараметра.Имя; 312 | СвойствоПараметра = Новый Структура; 313 | 314 | Если ОписаниеПараметра.ТипЭлемента = "ПростойПараметр" Тогда 315 | 316 | Если ЗначениеЗаполнено(ОписаниеПараметра.Тип) Тогда 317 | СвойствоПараметра.Вставить("type", ОписаниеПараметра.Тип); 318 | КонецЕсли; 319 | 320 | Если ЗначениеЗаполнено(ОписаниеПараметра.Описание) Тогда 321 | СвойствоПараметра.Вставить("description", ОписаниеПараметра.Описание); 322 | КонецЕсли; 323 | 324 | Если ЗначениеЗаполнено(ОписаниеПараметра.ЗначениеПоУмолчанию) Тогда 325 | СвойствоПараметра.Вставить("default", ОписаниеПараметра.ЗначениеПоУмолчанию); 326 | КонецЕсли; 327 | 328 | Если ЗначениеЗаполнено(ОписаниеПараметра.СписокДопустимыхЗначений) Тогда 329 | МассивЗначений = МассивДопустимыхЗначений(ОписаниеПараметра.СписокДопустимыхЗначений); 330 | СвойствоПараметра.Вставить("enum", МассивЗначений); 331 | КонецЕсли; 332 | 333 | ИначеЕсли ОписаниеПараметра.ТипЭлемента = "Массив" Тогда 334 | 335 | СвойствоПараметра.Вставить("type", "array"); 336 | 337 | Если ЗначениеЗаполнено(ОписаниеПараметра.Описание) Тогда 338 | СвойствоПараметра.Вставить("description", ОписаниеПараметра.Описание); 339 | КонецЕсли; 340 | 341 | ЭлементМассива = Новый Структура; 342 | 343 | Если ЗначениеЗаполнено(ОписаниеПараметра.ТипЭлементаМассива) Тогда 344 | ЭлементМассива.Вставить("type", ОписаниеПараметра.ТипЭлементаМассива); 345 | КонецЕсли; 346 | 347 | Если ЗначениеЗаполнено(ОписаниеПараметра.СписокДопустимыхЗначенийЭлемента) Тогда 348 | МассивЗначений = МассивДопустимыхЗначений(ОписаниеПараметра.СписокДопустимыхЗначенийЭлемента); 349 | ЭлементМассива.Вставить("enum", МассивЗначений); 350 | КонецЕсли; 351 | 352 | СвойствоПараметра.Вставить("items", ЭлементМассива); 353 | 354 | КонецЕсли; 355 | 356 | Свойства.Вставить(ИмяПараметра, СвойствоПараметра); 357 | 358 | Если ОписаниеПараметра.Обязательный Тогда 359 | ОбязательныеПараметры.Добавить(ИмяПараметра); 360 | КонецЕсли; 361 | 362 | КонецЦикла; 363 | 364 | Схема.Вставить("properties", Свойства); 365 | 366 | Если ОбязательныеПараметры.Количество() > 0 Тогда 367 | Схема.Вставить("required", ОбязательныеПараметры); 368 | КонецЕсли; 369 | 370 | Возврат mcp_ОбщегоНазначения.СтруктураВJSON(Схема); 371 | 372 | КонецФункции 373 | 374 | #КонецОбласти 375 | 376 | #Область ПараметрыПромптов 377 | 378 | // Создает описание параметра промпта 379 | // 380 | // Параметры: 381 | // Имя - Строка - имя параметра 382 | // Описание - Строка - описание параметра 383 | // Обязательный - Булево - признак обязательности параметра 384 | // 385 | // Возвращаемое значение: 386 | // Структура - описание параметра промпта 387 | // 388 | Функция ПараметрПромпта(Имя, Описание = Неопределено, Обязательный = Ложь) Экспорт 389 | 390 | ОписаниеПараметра = Новый Структура; 391 | ОписаниеПараметра.Вставить("Имя", Имя); 392 | ОписаниеПараметра.Вставить("Описание", Описание); 393 | ОписаниеПараметра.Вставить("Обязательный", Обязательный); 394 | 395 | Возврат ОписаниеПараметра; 396 | 397 | КонецФункции 398 | 399 | // Создает JSON-описание параметров промпта из массива описаний параметров 400 | // 401 | // Параметры: 402 | // МассивОписанийПараметров - Массив из Структура - массив описаний параметров промпта 403 | // 404 | // Возвращаемое значение: 405 | // Строка - JSON-описание параметров промпта 406 | // 407 | Функция ПараметрыПромпта(МассивОписанийПараметров) Экспорт 408 | 409 | МассивПараметров = Новый Массив; 410 | 411 | Для Каждого ОписаниеПараметра Из МассивОписанийПараметров Цикл 412 | 413 | ПараметрПромпта = Новый Структура; 414 | ПараметрПромпта.Вставить("name", ОписаниеПараметра.Имя); 415 | 416 | Если ЗначениеЗаполнено(ОписаниеПараметра.Описание) Тогда 417 | ПараметрПромпта.Вставить("description", ОписаниеПараметра.Описание); 418 | КонецЕсли; 419 | 420 | ПараметрПромпта.Вставить("required", ОписаниеПараметра.Обязательный); 421 | 422 | МассивПараметров.Добавить(ПараметрПромпта); 423 | 424 | КонецЦикла; 425 | 426 | Возврат mcp_ОбщегоНазначения.СтруктураВJSON(МассивПараметров); 427 | 428 | КонецФункции 429 | 430 | #КонецОбласти 431 | 432 | #КонецОбласти 433 | 434 | #Область СлужебныеПроцедурыИФункции 435 | 436 | // Преобразует строку со списком допустимых значений в массив 437 | // 438 | // Параметры: 439 | // СписокДопустимыхЗначений - Строка - список значений через запятую 440 | // 441 | // Возвращаемое значение: 442 | // Массив - массив обработанных значений 443 | // 444 | Функция МассивДопустимыхЗначений(СписокДопустимыхЗначений) 445 | 446 | МассивИсходныхЗначений = СтрРазделить(СписокДопустимыхЗначений, ",", Ложь); 447 | МассивЗначений = Новый Массив; 448 | Для Каждого Значение Из МассивИсходныхЗначений Цикл 449 | МассивЗначений.Добавить(СокрЛП(Значение)); 450 | КонецЦикла; 451 | 452 | Возврат МассивЗначений; 453 | 454 | КонецФункции 455 | 456 | #КонецОбласти 457 | -------------------------------------------------------------------------------- /src/1c_ext/HTTPServices/mcp_APIBackend/Ext/Module.bsl: -------------------------------------------------------------------------------- 1 | #Область ОбщийИнтерфейс 2 | 3 | Функция rpcPOST(Запрос) 4 | // Обрабатываем через унифицированную функцию 5 | Возврат ОбработатьJSONRPCЗапрос(Запрос); 6 | КонецФункции 7 | 8 | Функция healthGET(Запрос) 9 | ОтветДанные = Новый Структура; 10 | ОтветДанные.Вставить("status", "ok"); 11 | 12 | JSONСтрока = mcp_ОбщегоНазначения.СтруктураВJSON(ОтветДанные); 13 | 14 | Ответ = Новый HTTPСервисОтвет(200); 15 | Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8"); 16 | Ответ.УстановитьТелоИзСтроки(JSONСтрока, КодировкаТекста.UTF8); 17 | 18 | Возврат Ответ; 19 | КонецФункции 20 | 21 | Функция mcpPOST(Запрос) 22 | // Обрабатываем MCP Streamable HTTP запросы через унифицированную функцию 23 | Возврат ОбработатьJSONRPCЗапрос(Запрос); 24 | КонецФункции 25 | 26 | Функция mcpGET(Запрос) 27 | // GET не поддерживается для MCP эндпоинта согласно спецификации 28 | Ответ = Новый HTTPСервисОтвет(405); 29 | Ответ.Заголовки.Вставить("Allow", "POST"); 30 | Возврат Ответ; 31 | КонецФункции 32 | 33 | #КонецОбласти 34 | 35 | #Область УнифицированнаяОбработкаJSONRPC 36 | 37 | Функция ОбработатьJSONRPCЗапрос(Запрос) 38 | // Унифицированная обработка JSON-RPC запросов для /rpc и /mcp эндпоинтов 39 | 40 | Ответ = Новый HTTPСервисОтвет(200); 41 | Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8"); 42 | 43 | // Добавляем CORS заголовки для совместимости с браузерными клиентами 44 | Ответ.Заголовки.Вставить("Access-Control-Allow-Origin", "*"); 45 | 46 | Попытка 47 | // Получаем тело запроса 48 | ТелоЗапроса = Запрос.ПолучитьТелоКакСтроку(КодировкаТекста.UTF8); 49 | 50 | // Парсим JSON-RPC запрос 51 | ЗапросДанные = mcp_ОбщегоНазначения.JSONВСтруктуру(ТелоЗапроса); 52 | 53 | // Для notifications (запросы без id) сразу возвращаем 204 No Content 54 | Если НЕ ЗапросДанные.Свойство("id") Тогда 55 | Возврат СформироватьОтвет204(); 56 | КонецЕсли; 57 | 58 | ИдентификаторЗапроса = ЗапросДанные.id; 59 | 60 | // Проверяем версию JSON-RPC 61 | Если ЗапросДанные.Свойство("jsonrpc") И ЗапросДанные.jsonrpc <> "2.0" Тогда 62 | Возврат СформироватьJSONОшибку(Ответ, ИдентификаторЗапроса, -32600, "Неподдерживаемая версия JSON-RPC"); 63 | КонецЕсли; 64 | 65 | // Получаем метод 66 | Метод = ""; 67 | Если ЗапросДанные.Свойство("method") Тогда 68 | Метод = ЗапросДанные.method; 69 | КонецЕсли; 70 | 71 | // Получаем параметры 72 | Параметры = Новый Структура; 73 | Если ЗапросДанные.Свойство("params") Тогда 74 | Параметры = ЗапросДанные.params; 75 | КонецЕсли; 76 | 77 | // Маршрутизация по методам 78 | Результат = Неопределено; 79 | 80 | Если Метод = "initialize" Тогда 81 | Результат = ОбработатьInitialize(Параметры); 82 | ИначеЕсли Метод = "tools/list" Тогда 83 | Результат = ПолучитьСписокИнструментов(Параметры); 84 | ИначеЕсли Метод = "tools/call" Тогда 85 | Результат = ВызватьИнструмент(Параметры); 86 | ИначеЕсли Метод = "resources/list" Тогда 87 | Результат = ПолучитьСписокРесурсов(Параметры); 88 | ИначеЕсли Метод = "resources/read" Тогда 89 | Результат = ПолучитьРесурс(Параметры); 90 | ИначеЕсли Метод = "prompts/list" Тогда 91 | Результат = ПолучитьСписокПромптов(Параметры); 92 | ИначеЕсли Метод = "prompts/get" Тогда 93 | Результат = ПолучитьПромпт(Параметры); 94 | Иначе 95 | // Неизвестный метод 96 | Возврат СформироватьJSONОшибку(Ответ, ИдентификаторЗапроса, -32601, "Неизвестный метод: " + Метод); 97 | КонецЕсли; 98 | 99 | // Формируем успешный ответ 100 | Возврат СформироватьJSONУспех(Ответ, ИдентификаторЗапроса, Результат); 101 | 102 | Исключение 103 | ИнформацияОбОшибке = ИнформацияОбОшибке(); 104 | ОписаниеОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке); 105 | 106 | Возврат СформироватьJSONОшибку(Ответ, ИдентификаторЗапроса, -32603, "Внутренняя ошибка сервера: " + ОписаниеОшибки); 107 | КонецПопытки; 108 | КонецФункции 109 | 110 | Функция ОбработатьInitialize(Параметры) 111 | // Обрабатывает метод initialize для MCP handshake 112 | 113 | Результат = Новый Структура; 114 | 115 | // Устанавливаем версию протокола 116 | Результат.Вставить("protocolVersion", "2025-03-26"); 117 | 118 | // Описываем возможности сервера 119 | ВозможностиИнструментов = Новый Структура; 120 | ВозможностиИнструментов.Вставить("listChanged", Ложь); 121 | ВозможностиИнструментов.Вставить("call", Истина); 122 | 123 | ВозможностиРесурсов = Новый Структура; 124 | ВозможностиРесурсов.Вставить("listChanged", Ложь); 125 | ВозможностиРесурсов.Вставить("subscribe", Ложь); 126 | 127 | ВозможностиПромптов = Новый Структура; 128 | ВозможностиПромптов.Вставить("listChanged", Ложь); 129 | 130 | Возможности = Новый Структура; 131 | Возможности.Вставить("tools", ВозможностиИнструментов); 132 | Возможности.Вставить("resources", ВозможностиРесурсов); 133 | Возможности.Вставить("prompts", ВозможностиПромптов); 134 | 135 | Результат.Вставить("capabilities", Возможности); 136 | 137 | // Информация о сервере 138 | ИнформацияСервера = Новый Структура; 139 | ИнформацияСервера.Вставить("name", "1C MCP Server"); 140 | ИнформацияСервера.Вставить("version", "1.0.0"); 141 | 142 | Результат.Вставить("serverInfo", ИнформацияСервера); 143 | 144 | Возврат Результат; 145 | КонецФункции 146 | 147 | Функция СформироватьJSONУспех(HTTPОтвет, ИдентификаторЗапроса, Результат) 148 | // Формирует успешный JSON-RPC ответ 149 | 150 | ОтветУспех = Новый Структура; 151 | ОтветУспех.Вставить("jsonrpc", "2.0"); 152 | ОтветУспех.Вставить("id", ИдентификаторЗапроса); 153 | ОтветУспех.Вставить("result", Результат); 154 | 155 | HTTPОтвет.УстановитьТелоИзСтроки(mcp_ОбщегоНазначения.СтруктураВJSON(ОтветУспех), КодировкаТекста.UTF8); 156 | 157 | Возврат HTTPОтвет; 158 | КонецФункции 159 | 160 | Функция СформироватьJSONОшибку(HTTPОтвет, ИдентификаторЗапроса, КодОшибки, СообщениеОшибки) 161 | // Формирует JSON-RPC ответ с ошибкой 162 | 163 | ОтветОшибка = СформироватьОтветОшибку(КодОшибки, СообщениеОшибки, ИдентификаторЗапроса); 164 | HTTPОтвет.УстановитьТелоИзСтроки(mcp_ОбщегоНазначения.СтруктураВJSON(ОтветОшибка), КодировкаТекста.UTF8); 165 | 166 | Возврат HTTPОтвет; 167 | КонецФункции 168 | 169 | Функция СформироватьОтвет204() 170 | // Формирует ответ 204 No Content для notifications 171 | 172 | Ответ = Новый HTTPСервисОтвет(204); 173 | Ответ.Заголовки.Вставить("Access-Control-Allow-Origin", "*"); 174 | 175 | Возврат Ответ; 176 | КонецФункции 177 | 178 | #КонецОбласти 179 | 180 | #Область РаботаСИнструментами 181 | 182 | Функция ПолучитьСписокИнструментов(Параметры) 183 | // Получает список доступных инструментов из контейнеров 184 | // Возвращает структуру с полем "tools" - массив инструментов 185 | // Каждый инструмент содержит: 186 | // - name (строка): имя инструмента 187 | // - description (строка): описание инструмента 188 | // - inputSchema (структура): JSON схема входных параметров 189 | 190 | Результат = Новый Структура; 191 | Инструменты = Новый Массив; 192 | 193 | // Получаем таблицу инструментов из контейнеров 194 | ТаблицаИнструментов = mcp_КонтейнерыПовтИсп.Инструменты(); 195 | 196 | // Преобразуем таблицу в массив структур для JSON-RPC ответа 197 | Для Каждого СтрокаИнструмента Из ТаблицаИнструментов Цикл 198 | 199 | Инструмент = Новый Структура; 200 | Инструмент.Вставить("name", СтрокаИнструмента.Имя); 201 | Инструмент.Вставить("description", СтрокаИнструмента.Описание); 202 | 203 | // Парсим JSON схему параметров 204 | СхемаПараметров = Новый Структура; 205 | Если ЗначениеЗаполнено(СтрокаИнструмента.СхемаПараметров) Тогда 206 | Попытка 207 | СхемаПараметров = mcp_ОбщегоНазначения.JSONВСтруктуру(СтрокаИнструмента.СхемаПараметров); 208 | Исключение 209 | ИнформацияОбОшибке = ИнформацияОбОшибке(); 210 | ТекстОшибки = СтрШаблон("Ошибка чтения JSON схемы параметров для инструмента '%1': %2", 211 | СтрокаИнструмента.Имя, 212 | ПодробноеПредставлениеОшибки(ИнформацияОбОшибке)); 213 | ВызватьИсключение ТекстОшибки; 214 | КонецПопытки; 215 | КонецЕсли; 216 | 217 | Инструмент.Вставить("inputSchema", СхемаПараметров); 218 | 219 | Инструменты.Добавить(Инструмент); 220 | 221 | КонецЦикла; 222 | 223 | Результат.Вставить("tools", Инструменты); 224 | 225 | Возврат Результат; 226 | КонецФункции 227 | 228 | Функция ВызватьИнструмент(Параметры) 229 | // Выполняет инструмент из контейнеров 230 | // Параметры содержат: 231 | // - name (строка): имя инструмента для вызова 232 | // - arguments (структура): аргументы для инструмента 233 | // 234 | // Возвращает структуру с полями: 235 | // - content (массив): содержимое результата 236 | // Каждый элемент содержит: 237 | // - type ("text" или "image"): тип содержимого 238 | // - text (строка): текст для типа "text" 239 | // - data (строка): данные изображения в base64 для типа "image" 240 | // - mimeType (строка): MIME-тип для изображений 241 | // - isError (булево): признак ошибки 242 | 243 | ИмяИнструмента = ""; 244 | Если Параметры.Свойство("name") Тогда 245 | ИмяИнструмента = Параметры.name; 246 | КонецЕсли; 247 | 248 | АргументыИнструмента = Новый Структура; 249 | Если Параметры.Свойство("arguments") Тогда 250 | АргументыИнструмента = Параметры.arguments; 251 | КонецЕсли; 252 | 253 | // Проверяем обязательные параметры 254 | Если НЕ ЗначениеЗаполнено(ИмяИнструмента) Тогда 255 | ВызватьИсключение "Не указано имя инструмента для вызова"; 256 | КонецЕсли; 257 | 258 | // Получаем таблицу инструментов и ищем нужный 259 | ТаблицаИнструментов = mcp_КонтейнерыПовтИсп.Инструменты(); 260 | СтрокаИнструмента = ТаблицаИнструментов.Найти(ИмяИнструмента, "Имя"); 261 | 262 | Если СтрокаИнструмента = Неопределено Тогда 263 | ВызватьИсключение СтрШаблон("Инструмент '%1' не найден", ИмяИнструмента); 264 | КонецЕсли; 265 | 266 | Результат = Новый Структура; 267 | Содержимое = Новый Массив; 268 | ПризнакОшибки = Ложь; 269 | 270 | Попытка 271 | // Выполняем инструмент через модуль выполнения 272 | РезультатВыполнения = mcp_Выполнение.ВыполнитьИнструмент(СтрокаИнструмента, АргументыИнструмента); 273 | 274 | // Преобразуем результат в формат MCP 275 | Если ТипЗнч(РезультатВыполнения) = Тип("Массив") Тогда 276 | // Результат уже в формате массива элементов содержимого 277 | Для Каждого Элемент Из РезультатВыполнения Цикл 278 | Если ТипЗнч(Элемент) = Тип("Структура") И Элемент.Свойство("type") Тогда 279 | Содержимое.Добавить(Элемент); 280 | Иначе 281 | // Преобразуем произвольный элемент в текстовый 282 | ЭлементСодержимого = Новый Структура; 283 | ЭлементСодержимого.Вставить("type", "text"); 284 | ЭлементСодержимого.Вставить("text", Строка(Элемент)); 285 | Содержимое.Добавить(ЭлементСодержимого); 286 | КонецЕсли; 287 | КонецЦикла; 288 | Иначе 289 | // Результат - одиночное значение, преобразуем в текст 290 | ЭлементСодержимого = Новый Структура; 291 | ЭлементСодержимого.Вставить("type", "text"); 292 | ЭлементСодержимого.Вставить("text", Строка(РезультатВыполнения)); 293 | Содержимое.Добавить(ЭлементСодержимого); 294 | КонецЕсли; 295 | 296 | Исключение 297 | ПризнакОшибки = Истина; 298 | ИнформацияОбОшибке = ИнформацияОбОшибке(); 299 | 300 | ЭлементОшибки = Новый Структура; 301 | ЭлементОшибки.Вставить("type", "text"); 302 | ЭлементОшибки.Вставить("text", 303 | СтрШаблон("Ошибка выполнения инструмента '%1': %2", 304 | ИмяИнструмента, 305 | ПодробноеПредставлениеОшибки(ИнформацияОбОшибке))); 306 | 307 | Содержимое.Добавить(ЭлементОшибки); 308 | КонецПопытки; 309 | 310 | Результат.Вставить("content", Содержимое); 311 | Результат.Вставить("isError", ПризнакОшибки); 312 | 313 | Возврат Результат; 314 | КонецФункции 315 | 316 | #КонецОбласти 317 | 318 | #Область РаботаСРесурсами 319 | 320 | Функция ПолучитьСписокРесурсов(Параметры) 321 | // Получает список доступных ресурсов из контейнеров 322 | // Возвращает структуру с полем "resources" - массив ресурсов 323 | // Каждый ресурс содержит: 324 | // - uri (строка): URI ресурса 325 | // - name (строка): имя ресурса 326 | // - description (строка): описание ресурса 327 | // - mimeType (строка): MIME-тип ресурса (опционально) 328 | 329 | Результат = Новый Структура; 330 | Ресурсы = Новый Массив; 331 | 332 | // Получаем таблицу ресурсов из контейнеров 333 | ТаблицаРесурсов = mcp_КонтейнерыПовтИсп.Ресурсы(); 334 | 335 | // Преобразуем таблицу в массив структур для JSON-RPC ответа 336 | Для Каждого СтрокаРесурса Из ТаблицаРесурсов Цикл 337 | 338 | Ресурс = Новый Структура; 339 | Ресурс.Вставить("uri", СтрокаРесурса.Адрес); 340 | Ресурс.Вставить("name", СтрокаРесурса.Имя); 341 | Ресурс.Вставить("description", СтрокаРесурса.Описание); 342 | 343 | // mimeType не хранится в таблице ресурсов, поэтому не указываем 344 | // При необходимости можно будет добавить в схему таблицы 345 | 346 | Ресурсы.Добавить(Ресурс); 347 | 348 | КонецЦикла; 349 | 350 | Результат.Вставить("resources", Ресурсы); 351 | 352 | Возврат Результат; 353 | КонецФункции 354 | 355 | Функция ПолучитьРесурс(Параметры) 356 | // Читает содержимое ресурса из контейнеров 357 | // Параметры содержат: 358 | // - uri (строка): URI ресурса для получения 359 | // 360 | // Возвращает структуру с полем "contents" - массив содержимого 361 | // Каждый элемент содержит: 362 | // - type ("text" или "blob"): тип содержимого 363 | // - mimeType (строка): MIME-тип содержимого 364 | // - text (строка): текстовое содержимое для типа "text" 365 | // - blob (строка): двоичные данные в base64 для типа "blob" 366 | 367 | URIРесурса = ""; 368 | Если Параметры.Свойство("uri") Тогда 369 | URIРесурса = Параметры.uri; 370 | КонецЕсли; 371 | 372 | // Проверяем обязательные параметры 373 | Если НЕ ЗначениеЗаполнено(URIРесурса) Тогда 374 | ВызватьИсключение "Не указан URI ресурса для чтения"; 375 | КонецЕсли; 376 | 377 | // Получаем таблицу ресурсов и ищем нужный 378 | ТаблицаРесурсов = mcp_КонтейнерыПовтИсп.Ресурсы(); 379 | СтрокаРесурса = ТаблицаРесурсов.Найти(URIРесурса, "Адрес"); 380 | 381 | Если СтрокаРесурса = Неопределено Тогда 382 | ВызватьИсключение СтрШаблон("Ресурс '%1' не найден", URIРесурса); 383 | КонецЕсли; 384 | 385 | Результат = Новый Структура; 386 | Содержимое = Новый Массив; 387 | 388 | Попытка 389 | // Читаем ресурс через модуль выполнения 390 | СодержимоеРесурса = mcp_Выполнение.ПрочитатьРесурс(СтрокаРесурса, URIРесурса); 391 | 392 | // Преобразуем результат в формат MCP 393 | Если ТипЗнч(СодержимоеРесурса) = Тип("Массив") Тогда 394 | // Содержимое уже в формате массива элементов 395 | Для Каждого Элемент Из СодержимоеРесурса Цикл 396 | Если ТипЗнч(Элемент) = Тип("Структура") И Элемент.Свойство("type") Тогда 397 | // Добавляем uri, если его нет 398 | Если НЕ Элемент.Свойство("uri") Тогда 399 | Элемент.Вставить("uri", URIРесурса); 400 | КонецЕсли; 401 | Содержимое.Добавить(Элемент); 402 | Иначе 403 | // Преобразуем произвольный элемент в текстовый 404 | ЭлементСодержимого = Новый Структура; 405 | ЭлементСодержимого.Вставить("type", "text"); 406 | ЭлементСодержимого.Вставить("uri", URIРесурса); 407 | ЭлементСодержимого.Вставить("mimeType", "text/plain"); 408 | ЭлементСодержимого.Вставить("text", Строка(Элемент)); 409 | Содержимое.Добавить(ЭлементСодержимого); 410 | КонецЕсли; 411 | КонецЦикла; 412 | Иначе 413 | // Содержимое - одиночное значение, преобразуем в текст 414 | ЭлементСодержимого = Новый Структура; 415 | ЭлементСодержимого.Вставить("type", "text"); 416 | ЭлементСодержимого.Вставить("uri", URIРесурса); 417 | ЭлементСодержимого.Вставить("mimeType", "text/plain"); 418 | ЭлементСодержимого.Вставить("text", Строка(СодержимоеРесурса)); 419 | Содержимое.Добавить(ЭлементСодержимого); 420 | КонецЕсли; 421 | 422 | Исключение 423 | ИнформацияОбОшибке = ИнформацияОбОшибке(); 424 | ТекстОшибки = СтрШаблон("Ошибка чтения ресурса '%1': %2", 425 | URIРесурса, 426 | ПодробноеПредставлениеОшибки(ИнформацияОбОшибке)); 427 | ВызватьИсключение ТекстОшибки; 428 | КонецПопытки; 429 | 430 | Результат.Вставить("contents", Содержимое); 431 | 432 | Возврат Результат; 433 | КонецФункции 434 | 435 | #КонецОбласти 436 | 437 | #Область РаботаСПромптами 438 | 439 | Функция ПолучитьСписокПромптов(Параметры) 440 | // Получает список доступных промптов из контейнеров 441 | // Возвращает структуру с полем "prompts" - массив промптов 442 | // Каждый промпт содержит: 443 | // - name (строка): имя промпта 444 | // - description (строка): описание промпта 445 | // - arguments (массив): аргументы промпта (опционально) 446 | // Каждый аргумент содержит: 447 | // - name (строка): имя аргумента 448 | // - description (строка): описание аргумента 449 | // - required (булево): признак обязательности 450 | 451 | Результат = Новый Структура; 452 | Промпты = Новый Массив; 453 | 454 | // Получаем таблицу промптов из контейнеров 455 | ТаблицаПромптов = mcp_КонтейнерыПовтИсп.Промпты(); 456 | 457 | // Преобразуем таблицу в массив структур для JSON-RPC ответа 458 | Для Каждого СтрокаПромпта Из ТаблицаПромптов Цикл 459 | 460 | Промпт = Новый Структура; 461 | Промпт.Вставить("name", СтрокаПромпта.Имя); 462 | Промпт.Вставить("description", СтрокаПромпта.Описание); 463 | 464 | // Парсим JSON параметров промпта 465 | АргументыПромпта = Новый Массив; 466 | Если ЗначениеЗаполнено(СтрокаПромпта.Параметры) Тогда 467 | Попытка 468 | АргументыПромпта = mcp_ОбщегоНазначения.JSONВСтруктуру(СтрокаПромпта.Параметры); 469 | // Если это не массив, а объект - генерируем ошибку 470 | Если ТипЗнч(АргументыПромпта) <> Тип("Массив") Тогда 471 | ТекстОшибки = СтрШаблон("Параметры промпта '%1' должны быть массивом, а получен %2", 472 | СтрокаПромпта.Имя, 473 | ТипЗнч(АргументыПромпта)); 474 | ВызватьИсключение ТекстОшибки; 475 | КонецЕсли; 476 | Исключение 477 | ИнформацияОбОшибке = ИнформацияОбОшибке(); 478 | ТекстОшибки = СтрШаблон("Ошибка чтения JSON параметров для промпта '%1': %2", 479 | СтрокаПромпта.Имя, 480 | ПодробноеПредставлениеОшибки(ИнформацияОбОшибке)); 481 | ВызватьИсключение ТекстОшибки; 482 | КонецПопытки; 483 | КонецЕсли; 484 | 485 | Промпт.Вставить("arguments", АргументыПромпта); 486 | 487 | Промпты.Добавить(Промпт); 488 | 489 | КонецЦикла; 490 | 491 | Результат.Вставить("prompts", Промпты); 492 | 493 | Возврат Результат; 494 | КонецФункции 495 | 496 | Функция ПолучитьПромпт(Параметры) 497 | // Получает промпт из контейнеров 498 | // Параметры содержат: 499 | // - name (строка): имя промпта 500 | // - arguments (структура): аргументы промпта 501 | // 502 | // Возвращает структуру с полями: 503 | // - description (строка): описание промпта 504 | // - messages (массив): сообщения промпта 505 | // Каждое сообщение содержит: 506 | // - role (строка): роль ("user", "assistant", "system") 507 | // - content (структура): содержимое сообщения 508 | // - type ("text"): тип содержимого 509 | // - text (строка): текст сообщения 510 | 511 | ИмяПромпта = ""; 512 | Если Параметры.Свойство("name") Тогда 513 | ИмяПромпта = Параметры.name; 514 | КонецЕсли; 515 | 516 | АргументыПромпта = Новый Структура; 517 | Если Параметры.Свойство("arguments") Тогда 518 | АргументыПромпта = Параметры.arguments; 519 | КонецЕсли; 520 | 521 | // Проверяем обязательные параметры 522 | Если НЕ ЗначениеЗаполнено(ИмяПромпта) Тогда 523 | ВызватьИсключение "Не указано имя промпта для получения"; 524 | КонецЕсли; 525 | 526 | // Получаем таблицу промптов и ищем нужный 527 | ТаблицаПромптов = mcp_КонтейнерыПовтИсп.Промпты(); 528 | СтрокаПромпта = ТаблицаПромптов.Найти(ИмяПромпта, "Имя"); 529 | 530 | Если СтрокаПромпта = Неопределено Тогда 531 | ВызватьИсключение СтрШаблон("Промпт '%1' не найден", ИмяПромпта); 532 | КонецЕсли; 533 | 534 | Результат = Новый Структура; 535 | Сообщения = Новый Массив; 536 | 537 | Попытка 538 | // Получаем промпт через модуль выполнения 539 | СодержимоеПромпта = mcp_Выполнение.ПолучитьПромпт(СтрокаПромпта, ИмяПромпта, АргументыПромпта); 540 | 541 | // Устанавливаем описание из таблицы или из результата 542 | ОписаниеПромпта = СтрокаПромпта.Описание; 543 | Если ТипЗнч(СодержимоеПромпта) = Тип("Структура") И СодержимоеПромпта.Свойство("description") Тогда 544 | ОписаниеПромпта = СодержимоеПромпта.description; 545 | КонецЕсли; 546 | 547 | // Преобразуем результат в формат MCP 548 | Если ТипЗнч(СодержимоеПромпта) = Тип("Структура") И СодержимоеПромпта.Свойство("messages") Тогда 549 | // Результат уже в формате MCP с полем messages 550 | Если ТипЗнч(СодержимоеПромпта.messages) = Тип("Массив") Тогда 551 | Сообщения = СодержимоеПромпта.messages; 552 | КонецЕсли; 553 | ИначеЕсли ТипЗнч(СодержимоеПромпта) = Тип("Массив") Тогда 554 | // Результат - массив сообщений 555 | Сообщения = СодержимоеПромпта; 556 | Иначе 557 | // Результат - одиночное значение, создаем сообщение пользователя 558 | Сообщение = Новый Структура; 559 | Сообщение.Вставить("role", "user"); 560 | 561 | СодержимоеСообщения = Новый Структура; 562 | СодержимоеСообщения.Вставить("type", "text"); 563 | СодержимоеСообщения.Вставить("text", Строка(СодержимоеПромпта)); 564 | 565 | Сообщение.Вставить("content", СодержимоеСообщения); 566 | Сообщения.Добавить(Сообщение); 567 | КонецЕсли; 568 | 569 | Результат.Вставить("description", ОписаниеПромпта); 570 | 571 | Исключение 572 | ИнформацияОбОшибке = ИнформацияОбОшибке(); 573 | ТекстОшибки = СтрШаблон("Ошибка получения промпта '%1': %2", 574 | ИмяПромпта, 575 | ПодробноеПредставлениеОшибки(ИнформацияОбОшибке)); 576 | ВызватьИсключение ТекстОшибки; 577 | КонецПопытки; 578 | 579 | Результат.Вставить("messages", Сообщения); 580 | 581 | Возврат Результат; 582 | КонецФункции 583 | 584 | #КонецОбласти 585 | 586 | #Область ВспомогательныеМетоды 587 | 588 | Функция СформироватьОтветОшибку(КодОшибки, СообщениеОшибки, ИдентификаторЗапроса) 589 | ОтветОшибка = Новый Структура; 590 | ОтветОшибка.Вставить("jsonrpc", "2.0"); 591 | ОтветОшибка.Вставить("id", ИдентификаторЗапроса); 592 | 593 | Ошибка = Новый Структура; 594 | Ошибка.Вставить("code", КодОшибки); 595 | Ошибка.Вставить("message", СообщениеОшибки); 596 | 597 | ОтветОшибка.Вставить("error", Ошибка); 598 | 599 | Возврат ОтветОшибка; 600 | КонецФункции 601 | 602 | #КонецОбласти 603 | 604 | -------------------------------------------------------------------------------- /src/py_server/http_server.py: -------------------------------------------------------------------------------- 1 | """HTTP-сервер с поддержкой SSE и Streamable HTTP для MCP.""" 2 | 3 | import asyncio 4 | import json 5 | import logging 6 | from typing import Dict, Any, Optional 7 | from contextlib import asynccontextmanager 8 | from urllib.parse import urlencode, parse_qs 9 | 10 | from fastapi import FastAPI, Request, Response, HTTPException, Form 11 | from fastapi.responses import StreamingResponse, HTMLResponse, RedirectResponse, JSONResponse 12 | from fastapi.middleware.cors import CORSMiddleware 13 | import uvicorn 14 | import httpx 15 | 16 | from mcp.server.sse import SseServerTransport 17 | from mcp.server.streamable_http_manager import StreamableHTTPSessionManager 18 | from mcp.server.models import InitializationOptions 19 | from starlette.applications import Starlette 20 | from starlette.routing import Mount, Route 21 | from starlette.types import Scope, Receive, Send 22 | from starlette.middleware.base import BaseHTTPMiddleware 23 | 24 | from .mcp_server import MCPProxy, current_onec_credentials 25 | from .config import Config 26 | from .auth import OAuth2Service, OAuth2Store 27 | 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | 32 | class OAuth2BearerMiddleware(BaseHTTPMiddleware): 33 | """Middleware для проверки Bearer токенов в режиме OAuth2.""" 34 | 35 | def __init__(self, app, oauth2_service: Optional[OAuth2Service], auth_mode: str): 36 | super().__init__(app) 37 | self.oauth2_service = oauth2_service 38 | self.auth_mode = auth_mode 39 | self.protected_paths = ["/mcp/", "/sse"] 40 | 41 | async def dispatch(self, request: Request, call_next): 42 | """Проверка авторизации для защищённых путей.""" 43 | # Пропускаем, если auth_mode != oauth2 44 | if self.auth_mode != "oauth2": 45 | return await call_next(request) 46 | 47 | # Проверяем, является ли путь защищённым 48 | path = request.url.path 49 | is_protected = any(path.startswith(protected) for protected in self.protected_paths) 50 | 51 | if not is_protected: 52 | return await call_next(request) 53 | 54 | # Извлекаем Bearer token 55 | auth_header = request.headers.get("Authorization", "") 56 | if not auth_header.startswith("Bearer "): 57 | return JSONResponse( 58 | status_code=401, 59 | content={"error": "invalid_token"}, 60 | headers={"WWW-Authenticate": 'Bearer error="invalid_token"'} 61 | ) 62 | 63 | token = auth_header[7:] # Убираем "Bearer " 64 | 65 | # Валидируем токен (поддерживаем два формата) 66 | creds = None 67 | 68 | # 1. Простой формат: simple_base64(username:password) 69 | if token.startswith("simple_"): 70 | try: 71 | import base64 72 | creds_string = base64.b64decode(token[7:]).decode() 73 | username, password = creds_string.split(":", 1) 74 | creds = (username, password) 75 | logger.debug(f"Простой токен валидирован для пользователя: {username}") 76 | except Exception as e: 77 | logger.warning(f"Ошибка декодирования простого токена: {e}") 78 | creds = None 79 | 80 | # 2. OAuth2 формат: через хранилище 81 | if not creds: 82 | creds = self.oauth2_service.validate_access_token(token) 83 | 84 | if not creds: 85 | return JSONResponse( 86 | status_code=401, 87 | content={"error": "invalid_token"}, 88 | headers={"WWW-Authenticate": 'Bearer error="invalid_token"'} 89 | ) 90 | 91 | # Устанавливаем креденшилы в context var для этой сессии 92 | login, password = creds 93 | current_onec_credentials.set((login, password)) 94 | 95 | # Передаём управление дальше 96 | response = await call_next(request) 97 | return response 98 | 99 | 100 | class MCPHttpServer: 101 | """HTTP-сервер для MCP с поддержкой SSE и Streamable HTTP.""" 102 | 103 | def __init__(self, config: Config): 104 | """Инициализация HTTP-сервера. 105 | 106 | Args: 107 | config: Конфигурация сервера 108 | """ 109 | self.config = config 110 | self.mcp_proxy = MCPProxy(config) 111 | 112 | # Создаем session manager для Streamable HTTP после создания MCP прокси 113 | self.streamable_session_manager = StreamableHTTPSessionManager(self.mcp_proxy.server) 114 | 115 | # Инициализация OAuth2 (если включено) 116 | self.oauth2_store: Optional[OAuth2Store] = None 117 | self.oauth2_service: Optional[OAuth2Service] = None 118 | if config.auth_mode == "oauth2": 119 | self.oauth2_store = OAuth2Store() 120 | self.oauth2_service = OAuth2Service( 121 | self.oauth2_store, 122 | code_ttl=config.oauth2_code_ttl, 123 | access_ttl=config.oauth2_access_ttl, 124 | refresh_ttl=config.oauth2_refresh_ttl 125 | ) 126 | logger.info("OAuth2 авторизация включена") 127 | 128 | self.app = FastAPI( 129 | title="1C MCP Proxy", 130 | description="MCP-прокси для взаимодействия с 1С", 131 | version=config.server_version, 132 | lifespan=self._lifespan 133 | ) 134 | 135 | # Настройка CORS 136 | self.app.add_middleware( 137 | CORSMiddleware, 138 | allow_origins=config.cors_origins, 139 | allow_credentials=True, 140 | allow_methods=["*"], 141 | allow_headers=["*"], 142 | ) 143 | 144 | # Добавляем OAuth2 middleware 145 | self.app.add_middleware( 146 | OAuth2BearerMiddleware, 147 | oauth2_service=self.oauth2_service, 148 | auth_mode=config.auth_mode 149 | ) 150 | 151 | # Монтируем транспорты 152 | self._mount_transports() 153 | 154 | # Регистрация основных маршрутов 155 | self._register_routes() 156 | 157 | @asynccontextmanager 158 | async def _lifespan(self, app: FastAPI): 159 | """Управление жизненным циклом приложения.""" 160 | logger.debug("Запуск HTTP-сервера MCP") 161 | 162 | # Запускаем задачу очистки OAuth2 токенов (если включено) 163 | if self.oauth2_store: 164 | await self.oauth2_store.start_cleanup_task(interval=60) 165 | 166 | # Запускаем session manager для Streamable HTTP 167 | async with self.streamable_session_manager.run(): 168 | yield 169 | 170 | # Останавливаем задачу очистки OAuth2 171 | if self.oauth2_store: 172 | await self.oauth2_store.stop_cleanup_task() 173 | 174 | logger.debug("Остановка HTTP-сервера MCP") 175 | 176 | def _create_sse_starlette_app(self) -> Starlette: 177 | """Создание Starlette приложения для обработки SSE.""" 178 | # Создаем SSE транспорт для обработки сообщений 179 | sse_transport = SseServerTransport("/messages/") 180 | 181 | async def handle_sse(request): 182 | """Обработчик SSE подключений.""" 183 | logger.debug("Новое SSE подключение") 184 | 185 | try: 186 | # Подключаем SSE с использованием транспорта 187 | async with sse_transport.connect_sse( 188 | request.scope, 189 | request.receive, 190 | request._send 191 | ) as streams: 192 | # Запускаем MCP сервер с потоками 193 | await self.mcp_proxy.server.run( 194 | streams[0], 195 | streams[1], 196 | self.mcp_proxy.get_initialization_options() 197 | ) 198 | except Exception as e: 199 | logger.error(f"Ошибка в SSE обработчике: {e}") 200 | raise 201 | finally: 202 | logger.debug("SSE подключение закрыто") 203 | 204 | # Создаем маршруты для Starlette приложения 205 | # Когда это приложение монтируется на /sse: 206 | # - Route("/", ...) становится GET /sse (SSE подключение) 207 | # - Mount("/messages/", ...) становится POST /sse/messages/ (отправка сообщений) 208 | routes = [ 209 | Route("/", endpoint=handle_sse), # SSE endpoint: GET /sse 210 | Mount("/messages/", app=sse_transport.handle_post_message), # Messages: POST /sse/messages/ 211 | ] 212 | 213 | return Starlette(routes=routes) 214 | 215 | def _create_streamable_http_asgi(self): 216 | """Создание ASGI обработчика для Streamable HTTP.""" 217 | 218 | async def asgi(scope: Scope, receive: Receive, send: Send) -> None: 219 | """ASGI обработчик для Streamable HTTP соединений.""" 220 | logger.debug("Новое Streamable HTTP подключение") 221 | 222 | try: 223 | # Используем правильный API handle_request для ASGI 224 | await self.streamable_session_manager.handle_request(scope, receive, send) 225 | except Exception as e: 226 | logger.error(f"Ошибка в Streamable HTTP обработчике: {e}") 227 | raise 228 | finally: 229 | logger.debug("Streamable HTTP подключение закрыто") 230 | 231 | return asgi 232 | 233 | def _mount_transports(self): 234 | """Монтирование транспортов MCP.""" 235 | 236 | # Монтируем SSE транспорт на /sse 237 | sse_app = self._create_sse_starlette_app() 238 | self.app.mount("/sse", sse_app) 239 | 240 | # Монтируем Streamable HTTP транспорт на /mcp/ (с trailing slash для устранения 307 редиректов) 241 | streamable_app = self._create_streamable_http_asgi() 242 | self.app.mount("/mcp/", streamable_app) 243 | 244 | def _register_routes(self): 245 | """Регистрация основных маршрутов.""" 246 | 247 | @self.app.get("/") 248 | async def root(): 249 | """Корневой маршрут - перенаправляет на info.""" 250 | endpoints = { 251 | "info": "/info", 252 | "health": "/health", 253 | "sse": "/sse", 254 | "streamable_http": "/mcp/" 255 | } 256 | if self.config.auth_mode == "oauth2": 257 | endpoints["oauth2"] = { 258 | "well_known_prm": "/.well-known/oauth-protected-resource", 259 | "well_known_as": "/.well-known/oauth-authorization-server", 260 | "register": "/register", 261 | "authorize": "/authorize", 262 | "token": "/token" 263 | } 264 | return { 265 | "message": "1C MCP Proxy Server", 266 | "endpoints": endpoints 267 | } 268 | 269 | @self.app.get("/info") 270 | async def info(): 271 | """Информационный маршрут.""" 272 | return { 273 | "name": self.config.server_name, 274 | "version": self.config.server_version, 275 | "description": "MCP-прокси для взаимодействия с 1С", 276 | "endpoints": { 277 | "sse": "/sse", 278 | "messages": "/sse/messages/", 279 | "streamable_http": "/mcp/", 280 | "health": "/health", 281 | "info": "/info" 282 | }, 283 | "transports": { 284 | "sse": { 285 | "endpoint": "/sse", 286 | "messages": "/sse/messages/" 287 | }, 288 | "streamable_http": { 289 | "endpoint": "/mcp/" 290 | } 291 | } 292 | } 293 | 294 | @self.app.get("/health") 295 | async def health(): 296 | """Проверка здоровья сервера.""" 297 | try: 298 | # Проверяем подключение к 1С через прокси 299 | if hasattr(self.mcp_proxy, 'onec_client') and self.mcp_proxy.onec_client: 300 | await self.mcp_proxy.onec_client.check_health() 301 | result = {"status": "healthy", "onec_connection": "ok"} 302 | else: 303 | result = {"status": "starting", "onec_connection": "not_initialized"} 304 | 305 | # Добавляем информацию об авторизации 306 | result["auth"] = {"mode": self.config.auth_mode} 307 | return result 308 | except Exception as e: 309 | logger.error(f"Ошибка проверки здоровья: {e}") 310 | return { 311 | "status": "unhealthy", 312 | "onec_connection": "error", 313 | "error_details": str(e), 314 | "auth": {"mode": self.config.auth_mode} 315 | } 316 | 317 | # OAuth2 endpoints (если включено) 318 | if self.config.auth_mode == "oauth2": 319 | self._register_oauth2_routes() 320 | 321 | def _register_oauth2_routes(self): 322 | """Регистрация OAuth2 маршрутов.""" 323 | 324 | @self.app.get("/.well-known/oauth-protected-resource") 325 | async def well_known_prm(request: Request): 326 | """Protected Resource Metadata (RFC 9728).""" 327 | # Определяем публичный URL 328 | if self.config.public_url: 329 | public_url = self.config.public_url 330 | else: 331 | # Формируем из текущего запроса 332 | scheme = request.url.scheme 333 | netloc = request.headers.get("host", f"{request.client.host}:{request.url.port}") 334 | public_url = f"{scheme}://{netloc}" 335 | 336 | return self.oauth2_service.generate_prm_document(public_url) 337 | 338 | @self.app.get("/.well-known/oauth-authorization-server") 339 | async def well_known_as_metadata(request: Request): 340 | """Authorization Server Metadata (RFC 8414).""" 341 | # Определяем публичный URL 342 | if self.config.public_url: 343 | base_url = self.config.public_url 344 | else: 345 | # Формируем из текущего запроса 346 | scheme = request.url.scheme 347 | netloc = request.headers.get("host", f"{request.client.host}:{request.url.port}") 348 | base_url = f"{scheme}://{netloc}" 349 | 350 | return { 351 | "issuer": base_url, 352 | "authorization_endpoint": f"{base_url}/authorize", 353 | "token_endpoint": f"{base_url}/token", 354 | "registration_endpoint": f"{base_url}/register", # Добавляем registration endpoint 355 | "grant_types_supported": [ 356 | "authorization_code", 357 | "refresh_token", 358 | "password" # Добавляем Password Grant для простоты 359 | ], 360 | "response_types_supported": ["code"], 361 | "code_challenge_methods_supported": ["S256"], 362 | "token_endpoint_auth_methods_supported": ["none"], # Публичный клиент, без client_secret 363 | "revocation_endpoint_auth_methods_supported": ["none"] 364 | } 365 | 366 | @self.app.post("/register") 367 | async def register_client(request: Request): 368 | """Dynamic Client Registration (RFC 7591) - упрощённая версия. 369 | 370 | Всегда возвращает фиксированный client_id для публичного клиента. 371 | Игнорирует параметры регистрации, т.к. у нас нет реальной БД клиентов. 372 | """ 373 | # Читаем тело запроса (но не используем, т.к. всё равно вернём фиксированные данные) 374 | try: 375 | body = await request.json() 376 | logger.debug(f"Client registration request: {body}") 377 | except: 378 | body = {} 379 | 380 | # Определяем публичный URL для redirect_uris 381 | if self.config.public_url: 382 | base_url = self.config.public_url 383 | else: 384 | scheme = request.url.scheme 385 | netloc = request.headers.get("host", f"{request.client.host}:{request.url.port}") 386 | base_url = f"{scheme}://{netloc}" 387 | 388 | # Возвращаем фиксированные данные публичного клиента 389 | client_data = { 390 | "client_id": "mcp-public-client", 391 | "client_secret": "", # Пустой для публичного клиента 392 | "client_id_issued_at": 1640000000, # Фиксированная дата 393 | "grant_types": ["authorization_code", "refresh_token", "password"], 394 | "response_types": ["code"], 395 | "redirect_uris": [ 396 | f"{base_url}/callback", 397 | "http://localhost/callback", 398 | "http://127.0.0.1/callback" 399 | ], 400 | "token_endpoint_auth_method": "none", # Публичный клиент 401 | "application_type": "web" 402 | } 403 | 404 | # Если клиент передал свои redirect_uris, добавляем их 405 | if "redirect_uris" in body: 406 | for uri in body.get("redirect_uris", []): 407 | if uri not in client_data["redirect_uris"]: 408 | client_data["redirect_uris"].append(uri) 409 | 410 | logger.info(f"Client registration: вернули фиксированный client_id='mcp-public-client'") 411 | 412 | return client_data 413 | 414 | @self.app.get("/authorize") 415 | async def authorize_get( 416 | request: Request, 417 | response_type: str = None, 418 | client_id: str = None, 419 | redirect_uri: str = None, 420 | state: str = None, 421 | code_challenge: str = None, 422 | code_challenge_method: str = None 423 | ): 424 | """Authorization endpoint - показывает форму логина.""" 425 | # Валидация параметров 426 | if not all([response_type, client_id, redirect_uri, code_challenge, code_challenge_method]): 427 | return HTMLResponse( 428 | content="

Ошибка

Отсутствуют обязательные параметры OAuth2

", 429 | status_code=400 430 | ) 431 | 432 | if response_type != "code": 433 | return HTMLResponse( 434 | content="

Ошибка

Поддерживается только response_type=code

", 435 | status_code=400 436 | ) 437 | 438 | if code_challenge_method != "S256": 439 | return HTMLResponse( 440 | content="

Ошибка

Поддерживается только code_challenge_method=S256

", 441 | status_code=400 442 | ) 443 | 444 | # Сохраняем параметры в query для формы 445 | query_params = urlencode({ 446 | "redirect_uri": redirect_uri, 447 | "state": state or "", 448 | "code_challenge": code_challenge 449 | }) 450 | 451 | # HTML форма для ввода креденшилов 1С 452 | html_content = f""" 453 | 454 | 455 | 456 | 457 | Авторизация 1С MCP 458 | 468 | 469 | 470 |

Вход в 1С

471 |

Введите учётные данные пользователя 1С:

472 |
473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 |
481 | 482 | 483 | """ 484 | return HTMLResponse(content=html_content) 485 | 486 | @self.app.post("/authorize") 487 | async def authorize_post( 488 | request: Request, 489 | username: str = Form(...), 490 | password: str = Form(...), 491 | redirect_uri: str = None, 492 | state: str = None, 493 | code_challenge: str = None 494 | ): 495 | """Обработка формы логина и выдача authorization code.""" 496 | if not all([redirect_uri, code_challenge]): 497 | return HTMLResponse( 498 | content="

Ошибка

Отсутствуют обязательные параметры

", 499 | status_code=400 500 | ) 501 | 502 | # Валидация креденшилов через вызов к 1С health endpoint 503 | try: 504 | async with httpx.AsyncClient(timeout=10.0) as client: 505 | health_url = f"{self.config.onec_url}/hs/{self.config.onec_service_root}/health" 506 | response = await client.get( 507 | health_url, 508 | auth=httpx.BasicAuth(username, password) 509 | ) 510 | 511 | if response.status_code != 200: 512 | # Неверные креденшилы 513 | error_html = f""" 514 | 515 | 516 | 517 | 518 | Ошибка авторизации 519 | 524 | 525 | 526 |

Ошибка авторизации

527 |

Неверный логин или пароль 1С

528 |

← Вернуться назад

529 | 530 | 531 | """ 532 | return HTMLResponse(content=error_html, status_code=401) 533 | except Exception as e: 534 | logger.error(f"Ошибка проверки креденшилов 1С: {e}") 535 | return HTMLResponse( 536 | content=f"

Ошибка

Не удалось подключиться к 1С: {e}

", 537 | status_code=503 538 | ) 539 | 540 | # Генерируем authorization code 541 | code = self.oauth2_service.generate_authorization_code( 542 | login=username, 543 | password=password, 544 | redirect_uri=redirect_uri, 545 | code_challenge=code_challenge 546 | ) 547 | 548 | # Формируем redirect URL 549 | params = {"code": code} 550 | if state: 551 | params["state"] = state 552 | 553 | redirect_url = f"{redirect_uri}?{urlencode(params)}" 554 | 555 | logger.info(f"Authorization code выдан для пользователя {username}, redirect: {redirect_uri}") 556 | return RedirectResponse(url=redirect_url, status_code=302) 557 | 558 | @self.app.post("/token") 559 | async def token_endpoint( 560 | request: Request, 561 | grant_type: str = Form(...), 562 | code: str = Form(None), 563 | redirect_uri: str = Form(None), 564 | code_verifier: str = Form(None), 565 | refresh_token: str = Form(None), 566 | username: str = Form(None), 567 | password: str = Form(None) 568 | ): 569 | """Token endpoint для обмена code на токены, refresh или password grant.""" 570 | 571 | # Password Grant - самый простой вариант 572 | if grant_type == "password": 573 | if not username or not password: 574 | return JSONResponse( 575 | status_code=400, 576 | content={"error": "invalid_request", "error_description": "Missing username or password"} 577 | ) 578 | 579 | # Валидация креденшилов через 1С 580 | try: 581 | async with httpx.AsyncClient(timeout=10.0) as client: 582 | health_url = f"{self.config.onec_url}/hs/{self.config.onec_service_root}/health" 583 | response = await client.get( 584 | health_url, 585 | auth=httpx.BasicAuth(username, password) 586 | ) 587 | 588 | if response.status_code != 200: 589 | return JSONResponse( 590 | status_code=400, 591 | content={"error": "invalid_grant", "error_description": "Invalid username or password"} 592 | ) 593 | except Exception as e: 594 | logger.error(f"Ошибка проверки креденшилов 1С для password grant: {e}") 595 | return JSONResponse( 596 | status_code=503, 597 | content={"error": "server_error", "error_description": "Unable to validate credentials"} 598 | ) 599 | 600 | # Генерируем простой токен (base64 от username:password с префиксом) 601 | import base64 602 | creds_string = f"{username}:{password}" 603 | simple_token = "simple_" + base64.b64encode(creds_string.encode()).decode() 604 | 605 | logger.info(f"Password grant выдан для пользователя {username}") 606 | 607 | return { 608 | "access_token": simple_token, 609 | "token_type": "Bearer", 610 | "expires_in": 86400, # 24 часа (для простоты) 611 | "scope": "mcp" 612 | } 613 | 614 | # Authorization Code Grant 615 | if grant_type == "authorization_code": 616 | # Обмен code на токены 617 | if not all([code, redirect_uri, code_verifier]): 618 | return JSONResponse( 619 | status_code=400, 620 | content={"error": "invalid_request", "error_description": "Missing required parameters"} 621 | ) 622 | 623 | result = self.oauth2_service.exchange_code_for_tokens(code, redirect_uri, code_verifier) 624 | if not result: 625 | return JSONResponse( 626 | status_code=400, 627 | content={"error": "invalid_grant", "error_description": "Invalid or expired authorization code"} 628 | ) 629 | 630 | access_token, token_type, expires_in, refresh = result 631 | return { 632 | "access_token": access_token, 633 | "token_type": token_type, 634 | "expires_in": expires_in, 635 | "refresh_token": refresh, 636 | "scope": "mcp" 637 | } 638 | 639 | elif grant_type == "refresh_token": 640 | # Обновление токенов 641 | if not refresh_token: 642 | return JSONResponse( 643 | status_code=400, 644 | content={"error": "invalid_request", "error_description": "Missing refresh_token"} 645 | ) 646 | 647 | result = self.oauth2_service.refresh_tokens(refresh_token) 648 | if not result: 649 | return JSONResponse( 650 | status_code=400, 651 | content={"error": "invalid_grant", "error_description": "Invalid or expired refresh token"} 652 | ) 653 | 654 | access_token, token_type, expires_in, new_refresh = result 655 | return { 656 | "access_token": access_token, 657 | "token_type": token_type, 658 | "expires_in": expires_in, 659 | "refresh_token": new_refresh, 660 | "scope": "mcp" 661 | } 662 | 663 | else: 664 | return JSONResponse( 665 | status_code=400, 666 | content={"error": "unsupported_grant_type", "error_description": f"Grant type '{grant_type}' not supported"} 667 | ) 668 | 669 | async def start(self): 670 | """Запуск HTTP-сервера.""" 671 | config = uvicorn.Config( 672 | app=self.app, 673 | host=self.config.host, 674 | port=self.config.port, 675 | log_level=self.config.log_level.lower(), 676 | access_log=True 677 | ) 678 | 679 | server = uvicorn.Server(config) 680 | logger.debug(f"Запуск HTTP-сервера на {self.config.host}:{self.config.port}") 681 | await server.serve() 682 | 683 | 684 | async def run_http_server(config: Config): 685 | """Запуск HTTP-сервера. 686 | 687 | Args: 688 | config: Конфигурация сервера 689 | """ 690 | server = MCPHttpServer(config) 691 | await server.start() --------------------------------------------------------------------------------