├── requirements.txt ├── MCP_1copilot ├── __init__.py ├── __main__.py ├── config.py ├── models.py ├── api_client.py └── mcp_server.py ├── example_config.json ├── LICENSE ├── README.md ├── .gitignore └── test_server.py /requirements.txt: -------------------------------------------------------------------------------- 1 | mcp>=1.0.0 2 | httpx>=0.25.0 3 | python-dotenv>=1.0.0 4 | pydantic>=2.0.0 5 | pydantic-settings>=2.0.0 -------------------------------------------------------------------------------- /MCP_1copilot/__init__.py: -------------------------------------------------------------------------------- 1 | """MCP сервер для интеграции с ИИ-ассистентом 1С.ai.""" 2 | 3 | __version__ = "0.1.0" 4 | __author__ = "artesk" 5 | 6 | from .mcp_server import OneCMcpServer 7 | from .api_client import OneCApiClient 8 | from .config import Config 9 | 10 | __all__ = ["OneCMcpServer", "OneCApiClient", "Config"] -------------------------------------------------------------------------------- /example_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "onec-ai-1c": { 4 | "command": "python", 5 | "args": ["-m", "MCP_1copilot"], 6 | "cwd": "путь_к_папке_с_проектом", 7 | "env": { 8 | "ONEC_AI_TOKEN": "ваш_токен_от_1c_ai", 9 | "ONEC_AI_UI_LANGUAGE": "russian", 10 | "ONEC_AI_TIMEOUT": "30", 11 | "MAX_ACTIVE_SESSIONS": "10", 12 | "PYTHONPATH": "путь_к_папке_с_проектом" 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /MCP_1copilot/__main__.py: -------------------------------------------------------------------------------- 1 | """Точка входа для запуска MCP сервера 1С.ai.""" 2 | 3 | import asyncio 4 | import sys 5 | from .mcp_server import main 6 | 7 | def cli_main(): 8 | """Консольная точка входа.""" 9 | try: 10 | asyncio.run(main()) 11 | except KeyboardInterrupt: 12 | print("\nЗавершение работы сервера...") 13 | sys.exit(0) 14 | except Exception as e: 15 | print(f"Ошибка запуска: {e}") 16 | sys.exit(1) 17 | 18 | if __name__ == "__main__": 19 | cli_main() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 1copilot_MCP - MCP Сервер для общения с агентом 1С:Напарник (https://code.1c.ai/) 2 | MCP (Model Context Protocol) сервер для интеграции с ИИ-ассистентом 1С.ai. Позволяет использовать специализированные знания модели 1С:Напарник в любой IDE с поддержкой MCP 3 | 4 | ## Возможности 5 | 6 | - Задавать вопросы по синтаксису и особенностям 1С 7 | - Получать помощь по методам и объектам платформы 1С 8 | - Использовать знания модели, дообученной на коде 1С 9 | 10 | ![image](https://github.com/user-attachments/assets/f41bada0-4eec-4a49-b84d-d751be1b0c92) 11 | 12 | 13 | ## Установка 14 | 15 | 1. Установите зависимости: 16 | ```bash 17 | pip install -r requirements.txt 18 | ``` 19 | 20 | 2. Настройте переменные окружения: 21 | ```bash 22 | export ONEC_AI_TOKEN="ваш_токен_от_1c_ai" 23 | ``` 24 | 25 | 3. Запустите сервер: 26 | ```bash 27 | python -m MCP_1copilot 28 | ``` 29 | 30 | ## Конфигурация для IDE 31 | 32 | ### Cursor 33 | 34 | 1. Откройте настройки Cursor (Ctrl+,) 35 | 2. Найдите раздел "Features" → "Model Context Protocol" 36 | 3. Добавьте конфигурацию (замените пути на ваши): 37 | 38 | ```json 39 | { 40 | "mcpServers": { 41 | "onec-ai-1c": { 42 | "command": "python", 43 | "args": ["-m", "MCP_1copilot"], 44 | "cwd": "путь_к_папке_с_проектом", 45 | "env": { 46 | "ONEC_AI_TOKEN": "ваш_токен_от_1c_ai", 47 | "ONEC_AI_UI_LANGUAGE": "russian", 48 | "ONEC_AI_TIMEOUT": "30", 49 | "MAX_ACTIVE_SESSIONS": "10", 50 | "PYTHONPATH": "путь_к_папке_с_проектом" 51 | } 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | 58 | ## Доступные инструменты 59 | 60 | 1. **🔍 ask_1c_ai** - Задать любой вопрос специализированному ИИ-ассистенту 1С.ai по платформе 1С:Предприятие 61 | 2. **📚 explain_1c_syntax** - Объяснить синтаксис объектов 1С (HTTPСоединение, HTTPЗапрос, ТаблицаЗначений, Запрос, РегистрСведений, и др.) 62 | 3. **🔧 check_1c_code** - Проверить и проанализировать код 1С:Предприятие на ошибки и производительность 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | cursor_config.json 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | # Pyre type checker 117 | .pyre/ 118 | 119 | # IDEs 120 | .vscode/ 121 | .idea/ 122 | *.swp 123 | *.swo 124 | *~ 125 | 126 | # OS 127 | .DS_Store 128 | .DS_Store? 129 | ._* 130 | .Spotlight-V100 131 | .Trashes 132 | ehthumbs.db 133 | Thumbs.db -------------------------------------------------------------------------------- /MCP_1copilot/config.py: -------------------------------------------------------------------------------- 1 | """Конфигурация для MCP сервера 1С.ai.""" 2 | 3 | import os 4 | from typing import Optional 5 | from pydantic import Field, AliasChoices 6 | from pydantic_settings import BaseSettings, SettingsConfigDict 7 | from dotenv import load_dotenv 8 | 9 | # Загружаем переменные окружения из .env файла 10 | load_dotenv() 11 | 12 | 13 | class Config(BaseSettings): 14 | """Конфигурация приложения.""" 15 | 16 | model_config = SettingsConfigDict( 17 | env_file=".env", 18 | env_file_encoding="utf-8", 19 | case_sensitive=True 20 | ) 21 | 22 | # API настройки 23 | onec_ai_token: str = Field( 24 | ..., 25 | validation_alias=AliasChoices('ONEC_AI_TOKEN', 'onec_ai_token'), 26 | description="Токен для доступа к API 1С.ai" 27 | ) 28 | base_url: str = Field( 29 | default="https://code.1c.ai", 30 | env="ONEC_AI_BASE_URL", 31 | description="Базовый URL API" 32 | ) 33 | timeout: int = Field( 34 | default=30, 35 | env="ONEC_AI_TIMEOUT", 36 | description="Таймаут для запросов в секундах" 37 | ) 38 | 39 | # Настройки модели 40 | ui_language: str = Field( 41 | default="russian", 42 | env="ONEC_AI_UI_LANGUAGE", 43 | description="Язык интерфейса" 44 | ) 45 | programming_language: str = Field( 46 | default="", 47 | env="ONEC_AI_PROGRAMMING_LANGUAGE", 48 | description="Язык программирования по умолчанию" 49 | ) 50 | script_language: str = Field( 51 | default="", 52 | env="ONEC_AI_SCRIPT_LANGUAGE", 53 | description="Скриптовый язык по умолчанию" 54 | ) 55 | 56 | # Настройки сессий 57 | max_active_sessions: int = Field( 58 | default=10, 59 | env="MAX_ACTIVE_SESSIONS", 60 | description="Максимальное количество активных сессий" 61 | ) 62 | session_ttl: int = Field( 63 | default=3600, 64 | env="SESSION_TTL", 65 | description="Время жизни сессии в секундах" 66 | ) 67 | 68 | 69 | 70 | 71 | def get_config() -> Config: 72 | """Получить конфигурацию приложения.""" 73 | return Config() -------------------------------------------------------------------------------- /MCP_1copilot/models.py: -------------------------------------------------------------------------------- 1 | """Pydantic модели для API 1С.ai и MCP.""" 2 | 3 | from typing import Optional, Any, Dict, List 4 | from pydantic import BaseModel, Field 5 | from datetime import datetime 6 | 7 | 8 | class ConversationRequest(BaseModel): 9 | """Запрос на создание новой дискуссии.""" 10 | tool_name: str = "custom" 11 | ui_language: str = "russian" 12 | programming_language: str = "" 13 | script_language: str = "" 14 | 15 | 16 | class ConversationResponse(BaseModel): 17 | """Ответ при создании дискуссии.""" 18 | uuid: str 19 | 20 | 21 | class MessageRequest(BaseModel): 22 | """Запрос на отправку сообщения в дискуссию.""" 23 | parent_uuid: Optional[str] = None 24 | tool_content: Dict[str, str] = Field(default_factory=dict) 25 | 26 | def __init__(self, instruction: str, **kwargs): 27 | super().__init__(**kwargs) 28 | self.tool_content = {"instruction": instruction} 29 | 30 | 31 | class MessageChunk(BaseModel): 32 | """Часть сообщения из SSE потока.""" 33 | uuid: str 34 | role: Optional[str] = None 35 | content: Optional[Dict[str, Any]] = None 36 | parent_uuid: Optional[str] = None 37 | create_time: Optional[str] = None 38 | finished: bool = False 39 | 40 | 41 | class ConversationSession(BaseModel): 42 | """Сессия дискуссии.""" 43 | conversation_id: str 44 | created_at: datetime = Field(default_factory=datetime.now) 45 | last_used: datetime = Field(default_factory=datetime.now) 46 | messages_count: int = 0 47 | 48 | def update_usage(self): 49 | """Обновить время последнего использования.""" 50 | self.last_used = datetime.now() 51 | self.messages_count += 1 52 | 53 | 54 | class ApiError(Exception): 55 | """Ошибка API 1С.ai.""" 56 | 57 | def __init__(self, message: str, status_code: Optional[int] = None): 58 | self.message = message 59 | self.status_code = status_code 60 | super().__init__(self.message) 61 | 62 | 63 | class McpToolRequest(BaseModel): 64 | """Запрос к MCP инструменту.""" 65 | question: str = Field(..., description="Вопрос для модели 1С.ai") 66 | programming_language: Optional[str] = Field( 67 | default=None, 68 | description="Язык программирования (опционально)" 69 | ) 70 | create_new_session: bool = Field( 71 | default=False, 72 | description="Создать новую сессию" 73 | ) 74 | 75 | 76 | class McpToolResponse(BaseModel): 77 | """Ответ от MCP инструмента.""" 78 | answer: str = Field(..., description="Ответ от модели 1С.ai") 79 | conversation_id: str = Field(..., description="ID дискуссии") 80 | success: bool = True 81 | error: Optional[str] = None -------------------------------------------------------------------------------- /test_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Простой тест MCP сервера 1С.ai.""" 3 | 4 | import asyncio 5 | from MCP_1copilot.config import get_config 6 | from MCP_1copilot.api_client import OneCApiClient 7 | 8 | 9 | async def test_api_client(): 10 | """Тест API клиента.""" 11 | print("🔧 Тестирование API клиента 1С.ai...") 12 | 13 | try: 14 | # Проверяем наличие токена 15 | config = get_config() 16 | if not config.onec_ai_token: 17 | print("❌ Не найден токен ONEC_AI_TOKEN в переменных окружения") 18 | print(" Установите токен: export ONEC_AI_TOKEN='ваш_токен'") 19 | return False 20 | 21 | print(f"✅ Конфигурация загружена:") 22 | print(f" - Базовый URL: {config.base_url}") 23 | print(f" - Язык интерфейса: {config.ui_language}") 24 | print(f" - Таймаут: {config.timeout}s") 25 | 26 | # Создаем API клиент 27 | async with OneCApiClient(config) as client: 28 | print("\n🚀 Тестирование создания дискуссии...") 29 | 30 | # Создаем дискуссию 31 | conversation_id = await client.create_conversation() 32 | print(f"✅ Дискуссия создана: {conversation_id}") 33 | 34 | # Отправляем тестовый вопрос 35 | print("\n💬 Отправка тестового вопроса...") 36 | test_question = "Что такое HTTPСоединение в 1С?" 37 | answer = await client.send_message(conversation_id, test_question) 38 | 39 | print(f"❓ Вопрос: {test_question}") 40 | print(f"✅ Ответ получен ({len(answer)} символов):") 41 | print("-" * 50) 42 | print(answer[:200] + "..." if len(answer) > 200 else answer) 43 | print("-" * 50) 44 | 45 | return True 46 | 47 | except Exception as e: 48 | print(f"❌ Ошибка тестирования: {str(e)}") 49 | return False 50 | 51 | 52 | def test_config(): 53 | """Тест конфигурации.""" 54 | print("🔧 Тестирование конфигурации...") 55 | 56 | try: 57 | config = get_config() 58 | print("✅ Конфигурация успешно загружена") 59 | return True 60 | except Exception as e: 61 | print(f"❌ Ошибка конфигурации: {str(e)}") 62 | return False 63 | 64 | 65 | async def main(): 66 | """Главная функция тестирования.""" 67 | print("🧪 Запуск тестов MCP сервера 1С.ai\n") 68 | 69 | # Тест конфигурации 70 | if not test_config(): 71 | return 72 | 73 | print() 74 | 75 | # Тест API клиента 76 | if not await test_api_client(): 77 | return 78 | 79 | print("\n🎉 Все тесты прошли успешно!") 80 | print("\n📋 Для запуска MCP сервера используйте:") 81 | print(" python -m MCP_1copilot") 82 | 83 | 84 | if __name__ == "__main__": 85 | asyncio.run(main()) -------------------------------------------------------------------------------- /MCP_1copilot/api_client.py: -------------------------------------------------------------------------------- 1 | """HTTP клиент для работы с API 1С.ai.""" 2 | 3 | import asyncio 4 | import json 5 | import logging 6 | from typing import Optional, AsyncGenerator, Dict 7 | from datetime import datetime, timedelta 8 | 9 | import httpx 10 | from .config import Config 11 | from .models import ( 12 | ConversationRequest, 13 | ConversationResponse, 14 | MessageRequest, 15 | MessageChunk, 16 | ConversationSession, 17 | ApiError 18 | ) 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | class OneCApiClient: 24 | """Клиент для работы с API 1С.ai.""" 25 | 26 | def __init__(self, config: Config): 27 | self.config = config 28 | self.base_url = config.base_url.rstrip('/') 29 | self.sessions: Dict[str, ConversationSession] = {} 30 | 31 | # Создаем HTTP клиент 32 | self.client = httpx.AsyncClient( 33 | timeout=config.timeout, 34 | headers={ 35 | "Accept": "*/*", 36 | "Accept-Charset": "utf-8", 37 | "Accept-Encoding": "gzip, deflate, br", 38 | "Accept-Language": "ru-ru,en-us;q=0.8,en;q=0.7", 39 | "Authorization": config.onec_ai_token, 40 | "Content-Type": "application/json; charset=utf-8", 41 | "Origin": config.base_url, 42 | "Referer": f"{config.base_url}/chat/", 43 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/620.1 (KHTML, like Gecko) JavaFX/22 Safari/620.1", 44 | } 45 | ) 46 | 47 | async def create_conversation( 48 | self, 49 | programming_language: Optional[str] = None, 50 | script_language: Optional[str] = None 51 | ) -> str: 52 | """Создать новую дискуссию.""" 53 | try: 54 | request_data = ConversationRequest( 55 | ui_language=self.config.ui_language, 56 | programming_language=programming_language or self.config.programming_language, 57 | script_language=script_language or self.config.script_language 58 | ) 59 | 60 | response = await self.client.post( 61 | f"{self.base_url}/chat_api/v1/conversations/", 62 | json=request_data.dict(), 63 | headers={"Session-Id": ""} 64 | ) 65 | 66 | if response.status_code != 200: 67 | raise ApiError( 68 | f"Ошибка создания дискуссии: {response.status_code}", 69 | response.status_code 70 | ) 71 | 72 | conversation_response = ConversationResponse(**response.json()) 73 | conversation_id = conversation_response.uuid 74 | 75 | # Сохраняем сессию 76 | self.sessions[conversation_id] = ConversationSession( 77 | conversation_id=conversation_id 78 | ) 79 | 80 | logger.info(f"Создана новая дискуссия: {conversation_id}") 81 | return conversation_id 82 | 83 | except httpx.RequestError as e: 84 | raise ApiError(f"Ошибка сети при создании дискуссии: {str(e)}") 85 | except Exception as e: 86 | raise ApiError(f"Неожиданная ошибка при создании дискуссии: {str(e)}") 87 | 88 | async def send_message(self, conversation_id: str, message: str) -> str: 89 | """Отправить сообщение в дискуссию и получить ответ.""" 90 | try: 91 | # Проверяем существование сессии 92 | if conversation_id not in self.sessions: 93 | self.sessions[conversation_id] = ConversationSession( 94 | conversation_id=conversation_id 95 | ) 96 | 97 | # Обновляем использование сессии 98 | self.sessions[conversation_id].update_usage() 99 | 100 | request_data = MessageRequest(instruction=message) 101 | 102 | # Отправляем сообщение 103 | url = f"{self.base_url}/chat_api/v1/conversations/{conversation_id}/messages" 104 | 105 | async with self.client.stream( 106 | "POST", 107 | url, 108 | json=request_data.dict(), 109 | headers={"Accept": "text/event-stream"} 110 | ) as response: 111 | 112 | if response.status_code != 200: 113 | raise ApiError( 114 | f"Ошибка отправки сообщения: {response.status_code}", 115 | response.status_code 116 | ) 117 | 118 | # Собираем ответ из SSE потока 119 | full_response = await self._parse_sse_response(response) 120 | 121 | logger.info(f"Получен ответ для дискуссии {conversation_id}") 122 | return full_response 123 | 124 | except httpx.RequestError as e: 125 | raise ApiError(f"Ошибка сети при отправке сообщения: {str(e)}") 126 | except Exception as e: 127 | raise ApiError(f"Неожиданная ошибка при отправке сообщения: {str(e)}") 128 | 129 | async def _parse_sse_response(self, response: httpx.Response) -> str: 130 | """Парсинг Server-Sent Events ответа.""" 131 | full_text = "" 132 | 133 | # Убеждаемся что кодировка UTF-8 134 | response.encoding = 'utf-8' 135 | 136 | async for line in response.aiter_lines(): 137 | if line.startswith("data: "): 138 | try: 139 | data_str = line[6:] # Убираем "data: " 140 | data = json.loads(data_str) 141 | 142 | chunk = MessageChunk(**data) 143 | 144 | # Если это ответ ассистента с контентом 145 | if (chunk.role == "assistant" and 146 | chunk.content and 147 | "text" in chunk.content): 148 | 149 | text = chunk.content["text"] 150 | if text: 151 | # Нормализуем текст для безопасности 152 | text = text.encode('utf-8', errors='ignore').decode('utf-8', errors='ignore') 153 | full_text = text # Берем полный текст из последнего чанка 154 | 155 | # Если сообщение завершено 156 | if chunk.finished: 157 | break 158 | 159 | except json.JSONDecodeError: 160 | continue 161 | except Exception as e: 162 | logger.warning(f"Ошибка парсинга SSE chunk: {e}") 163 | continue 164 | 165 | return full_text.strip() 166 | 167 | async def get_or_create_session( 168 | self, 169 | create_new: bool = False, 170 | programming_language: Optional[str] = None 171 | ) -> str: 172 | """Получить существующую сессию или создать новую.""" 173 | 174 | # Очищаем устаревшие сессии 175 | await self._cleanup_old_sessions() 176 | 177 | # Если требуется новая сессия или нет активных сессий 178 | if create_new or not self.sessions: 179 | return await self.create_conversation(programming_language) 180 | 181 | # Проверяем лимит активных сессий 182 | if len(self.sessions) >= self.config.max_active_sessions: 183 | # Удаляем самую старую сессию 184 | oldest_session_id = min( 185 | self.sessions.keys(), 186 | key=lambda k: self.sessions[k].last_used 187 | ) 188 | del self.sessions[oldest_session_id] 189 | logger.info(f"Удалена старая сессия: {oldest_session_id}") 190 | 191 | # Возвращаем самую свежую сессию 192 | recent_session_id = max( 193 | self.sessions.keys(), 194 | key=lambda k: self.sessions[k].last_used 195 | ) 196 | 197 | return recent_session_id 198 | 199 | async def _cleanup_old_sessions(self): 200 | """Очистка устаревших сессий.""" 201 | current_time = datetime.now() 202 | ttl_delta = timedelta(seconds=self.config.session_ttl) 203 | 204 | expired_sessions = [ 205 | session_id for session_id, session in self.sessions.items() 206 | if current_time - session.last_used > ttl_delta 207 | ] 208 | 209 | for session_id in expired_sessions: 210 | del self.sessions[session_id] 211 | logger.info(f"Удалена устаревшая сессия: {session_id}") 212 | 213 | async def close(self): 214 | """Закрыть HTTP клиент.""" 215 | await self.client.aclose() 216 | 217 | async def __aenter__(self): 218 | return self 219 | 220 | async def __aexit__(self, exc_type, exc_val, exc_tb): 221 | await self.close() -------------------------------------------------------------------------------- /MCP_1copilot/mcp_server.py: -------------------------------------------------------------------------------- 1 | """Основной MCP сервер для интеграции с 1С.ai.""" 2 | 3 | import asyncio 4 | import logging 5 | from typing import Any, Sequence, Optional 6 | 7 | from mcp.server.models import InitializationOptions 8 | from mcp.server import NotificationOptions, Server 9 | from mcp.types import ( 10 | Resource, 11 | Tool, 12 | TextContent, 13 | ImageContent, 14 | EmbeddedResource, 15 | LoggingLevel 16 | ) 17 | import mcp.types as types 18 | 19 | from .config import get_config, Config 20 | from .api_client import OneCApiClient 21 | from .models import ApiError, McpToolRequest, McpToolResponse 22 | 23 | # Настройка логирования только для ошибок при работе с MCP 24 | logger = logging.getLogger(__name__) 25 | logger.setLevel(logging.ERROR) 26 | 27 | 28 | class OneCMcpServer: 29 | """MCP сервер для интеграции с 1С.ai.""" 30 | 31 | @staticmethod 32 | def _sanitize_text(text: str) -> str: 33 | """Очистка текста от проблемных символов для корректного отображения.""" 34 | if not text: 35 | return text 36 | 37 | # Удаляем или заменяем проблемные Unicode символы 38 | import unicodedata 39 | 40 | # Нормализуем Unicode 41 | text = unicodedata.normalize('NFKC', text) 42 | 43 | # Удаляем управляющие символы кроме стандартных переносов строк 44 | cleaned = '' 45 | for char in text: 46 | if unicodedata.category(char) not in ['Cc', 'Cf'] or char in ['\n', '\r', '\t']: 47 | cleaned += char 48 | 49 | return cleaned 50 | 51 | def __init__(self, config: Optional[Config] = None): 52 | self.config = config 53 | self.api_client: Optional[OneCApiClient] = None 54 | 55 | # Создаем MCP сервер 56 | self.server = Server("onec-ai-1c-enterprise") 57 | 58 | # Регистрируем обработчики 59 | self._register_handlers() 60 | 61 | def _register_handlers(self): 62 | """Регистрация обработчиков MCP.""" 63 | 64 | @self.server.list_tools() 65 | async def handle_list_tools() -> list[Tool]: 66 | """Список доступных инструментов.""" 67 | return [ 68 | Tool( 69 | name="ask_1c_ai", 70 | description="🔍 Задать любой вопрос специализированному ИИ-ассистенту 1С.ai (1С:Напарник) по платформе 1С:Предприятие. Используется для вопросов о 1С, конфигурации, объектах платформы, встроенном языке, API, интеграции и разработке.", 71 | inputSchema={ 72 | "type": "object", 73 | "properties": { 74 | "question": { 75 | "type": "string", 76 | "description": "Вопрос для модели 1С.ai" 77 | }, 78 | "programming_language": { 79 | "type": "string", 80 | "description": "Язык программирования (опционально)", 81 | "default": "" 82 | }, 83 | "create_new_session": { 84 | "type": "boolean", 85 | "description": "Создать новую сессию для этого вопроса", 86 | "default": False 87 | } 88 | }, 89 | "required": ["question"] 90 | }, 91 | ), 92 | Tool( 93 | name="explain_1c_syntax", 94 | description="📚 Объяснить синтаксис, конструкции и объекты языка 1С:Предприятие. Используется для объяснения HTTPСоединение, HTTPЗапрос, ТаблицаЗначений, Запрос, РегистрСведений, Документ, Справочник, Для Каждого, Если, Процедура, Функция и других элементов 1С.", 95 | inputSchema={ 96 | "type": "object", 97 | "properties": { 98 | "syntax_element": { 99 | "type": "string", 100 | "description": "Элемент синтаксиса или объект 1С для объяснения (например: HTTPСоединение, HTTPЗапрос, ТаблицаЗначений, РегистрСведений, Документ, Справочник, Запрос, Для Каждого, Если, Процедура, Функция)" 101 | }, 102 | "context": { 103 | "type": "string", 104 | "description": "Дополнительный контекст использования (опционально)", 105 | "default": "" 106 | } 107 | }, 108 | "required": ["syntax_element"] 109 | }, 110 | ), 111 | Tool( 112 | name="check_1c_code", 113 | description="🔧 Проверить и проанализировать код 1С:Предприятие на ошибки, производительность и соответствие лучшим практикам. Используется для валидации кода на встроенном языке 1С.", 114 | inputSchema={ 115 | "type": "object", 116 | "properties": { 117 | "code": { 118 | "type": "string", 119 | "description": "Код 1С для проверки и анализа" 120 | }, 121 | "check_type": { 122 | "type": "string", 123 | "description": "Тип проверки: syntax (синтаксис), logic (логика), performance (производительность)", 124 | "enum": ["syntax", "logic", "performance"], 125 | "default": "syntax" 126 | } 127 | }, 128 | "required": ["code"] 129 | }, 130 | ), 131 | 132 | ] 133 | 134 | @self.server.call_tool() 135 | async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent]: 136 | """Обработка вызова инструментов.""" 137 | 138 | # Отладочное логирование (без эмодзи для избежания проблем с кодировкой) 139 | # print(f"MCP tool call: {name} with args: {arguments}", flush=True) 140 | 141 | # Инициализируем конфигурацию и API клиент если нужно 142 | if self.config is None: 143 | try: 144 | self.config = get_config() 145 | except Exception as e: 146 | return [types.TextContent( 147 | type="text", 148 | text=f"Ошибка конфигурации: {str(e)}\nУстановите переменную окружения ONEC_AI_TOKEN" 149 | )] 150 | 151 | if self.api_client is None: 152 | try: 153 | self.api_client = OneCApiClient(self.config) 154 | except Exception as e: 155 | return [types.TextContent( 156 | type="text", 157 | text=f"Ошибка подключения к API: {str(e)}" 158 | )] 159 | 160 | try: 161 | if name == "ask_1c_ai": 162 | return await self._handle_ask_1c_ai(arguments) 163 | elif name == "explain_1c_syntax": 164 | return await self._handle_explain_syntax(arguments) 165 | elif name == "check_1c_code": 166 | return await self._handle_check_code(arguments) 167 | else: 168 | return [types.TextContent( 169 | type="text", 170 | text=f"Неизвестный инструмент: {name}" 171 | )] 172 | 173 | except ApiError as e: 174 | logger.error(f"Ошибка API при вызове {name}: {e.message}") 175 | return [types.TextContent( 176 | type="text", 177 | text=f"Ошибка при обращении к 1С.ai: {e.message}" 178 | )] 179 | except Exception as e: 180 | logger.error(f"Неожиданная ошибка при вызове {name}: {str(e)}") 181 | return [types.TextContent( 182 | type="text", 183 | text=f"Произошла неожиданная ошибка: {str(e)}" 184 | )] 185 | 186 | async def _handle_ask_1c_ai(self, arguments: dict) -> list[types.TextContent]: 187 | """Обработка инструмента ask_1c_ai.""" 188 | question = arguments.get("question", "") 189 | programming_language = arguments.get("programming_language", "") 190 | create_new_session = arguments.get("create_new_session", False) 191 | 192 | if not question.strip(): 193 | return [types.TextContent( 194 | type="text", 195 | text="Ошибка: Вопрос не может быть пустым" 196 | )] 197 | 198 | # Получаем или создаем сессию 199 | conversation_id = await self.api_client.get_or_create_session( 200 | create_new=create_new_session, 201 | programming_language=programming_language or None 202 | ) 203 | 204 | # Отправляем вопрос 205 | answer = await self.api_client.send_message(conversation_id, question) 206 | 207 | # Очищаем ответ от проблемных символов 208 | clean_answer = self._sanitize_text(answer) 209 | 210 | return [types.TextContent( 211 | type="text", 212 | text=f"Ответ от 1С.ai:\n\n{clean_answer}\n\nСессия: {conversation_id}" 213 | )] 214 | 215 | async def _handle_explain_syntax(self, arguments: dict) -> list[types.TextContent]: 216 | """Обработка инструмента explain_1c_syntax.""" 217 | syntax_element = arguments.get("syntax_element", "") 218 | context = arguments.get("context", "") 219 | 220 | if not syntax_element.strip(): 221 | return [types.TextContent( 222 | type="text", 223 | text="Ошибка: Элемент синтаксиса не может быть пустым" 224 | )] 225 | 226 | # Формируем вопрос для модели 227 | question = f"Объясни синтаксис и использование: {syntax_element}" 228 | if context: 229 | question += f" в контексте: {context}" 230 | 231 | # Получаем сессию и отправляем вопрос 232 | conversation_id = await self.api_client.get_or_create_session() 233 | answer = await self.api_client.send_message(conversation_id, question) 234 | 235 | # Очищаем ответ от проблемных символов 236 | clean_answer = self._sanitize_text(answer) 237 | 238 | return [types.TextContent( 239 | type="text", 240 | text=f"Объяснение синтаксиса '{syntax_element}':\n\n{clean_answer}" 241 | )] 242 | 243 | async def _handle_check_code(self, arguments: dict) -> list[types.TextContent]: 244 | """Обработка инструмента check_1c_code.""" 245 | code = arguments.get("code", "") 246 | check_type = arguments.get("check_type", "syntax") 247 | 248 | if not code.strip(): 249 | return [types.TextContent( 250 | type="text", 251 | text="Ошибка: Код для проверки не может быть пустым" 252 | )] 253 | 254 | # Формируем вопрос в зависимости от типа проверки 255 | check_descriptions = { 256 | "syntax": "синтаксические ошибки", 257 | "logic": "логические ошибки и потенциальные проблемы", 258 | "performance": "проблемы производительности и оптимизации" 259 | } 260 | 261 | check_desc = check_descriptions.get(check_type, "ошибки") 262 | question = f"Проверь этот код 1С на {check_desc} и дай рекомендации:\n\n```1c\n{code}\n```" 263 | 264 | # Получаем сессию и отправляем вопрос 265 | conversation_id = await self.api_client.get_or_create_session() 266 | answer = await self.api_client.send_message(conversation_id, question) 267 | 268 | # Очищаем ответ от проблемных символов 269 | clean_answer = self._sanitize_text(answer) 270 | 271 | return [types.TextContent( 272 | type="text", 273 | text=f"Проверка кода на {check_desc}:\n\n{clean_answer}" 274 | )] 275 | 276 | 277 | 278 | async def run(self, transport: str = "stdio"): 279 | """Запуск MCP сервера.""" 280 | try: 281 | # Не проверяем конфигурацию при запуске - будем делать это при первом вызове инструмента 282 | 283 | # Запускаем сервер 284 | if transport == "stdio": 285 | from mcp.server.stdio import stdio_server 286 | async with stdio_server() as (read_stream, write_stream): 287 | await self.server.run( 288 | read_stream, 289 | write_stream, 290 | InitializationOptions( 291 | server_name="MCP_1copilot", 292 | server_version="0.1.0", 293 | capabilities=self.server.get_capabilities( 294 | notification_options=NotificationOptions(), 295 | experimental_capabilities={}, 296 | ), 297 | ), 298 | ) 299 | else: 300 | raise ValueError(f"Неподдерживаемый транспорт: {transport}") 301 | 302 | except Exception as e: 303 | logger.error(f"Ошибка при запуске сервера: {str(e)}") 304 | raise 305 | finally: 306 | # Закрываем API клиент 307 | if self.api_client: 308 | await self.api_client.close() 309 | 310 | 311 | async def main(): 312 | """Главная функция запуска сервера.""" 313 | try: 314 | server = OneCMcpServer() 315 | await server.run() 316 | except Exception as e: 317 | logger.error(f"Критическая ошибка: {str(e)}") 318 | raise 319 | 320 | 321 | if __name__ == "__main__": 322 | asyncio.run(main()) --------------------------------------------------------------------------------