Начало / MCP и 1С / Как написать MCP-сервер для базы 1С
Как написать MCP-сервер для базы 1С
Сервер на Python поверх стандартного интерфейса OData: только чтение, белый список, проверка входных данных
Самый короткий путь дать ассистенту данные базы — небольшой сервер на Python поверх стандартного интерфейса OData, который есть в платформе 1С:Предприятие 8.3. Сервер ничего не пишет в базу, отдаёт только разрешённые объекты и поля и сам собирает запросы — модель не может прислать свой.
MCP-сервер стоит между ИИ-клиентом и базой: принимает вызовы инструментов, сам собирает запрос OData с белым списком полей и обращается к публикации базы от отдельного пользователя.
Варианты архитектуры#
Таблица: Три способа
| Способ | Как работает | Когда выбрать |
|---|---|---|
| Сервер на Python поверх OData | Отдельная программа обращается к …/odata/standard.odata | Чтение справочников и документов без доработки конфигурации — этот способ ниже |
| Расширение с HTTP-сервисом | MCP-сервер реализован внутри базы на встроенном языке | Нужна логика на стороне 1С; готовая основа — проект 1c_mcp |
| Свой HTTP-сервис + MCP-прокси | Сервис отдаёт подготовленные данные, прокси переводит их в MCP | Сложные выборки, которые проще написать запросом 1С |
Шаг 1. Публикация OData#
- Опубликуйте базу на веб-сервере и в окне публикации отметьте «Публиковать стандартный интерфейс OData».
- Включите в состав интерфейса только нужные объекты. Состав задаётся методом
УстановитьСоставСтандартногоИнтерфейсаOData(); в типовых конфигурациях на БСП для этого обычно есть обработка настройки стандартного интерфейса OData. - Проверьте, что интерфейс отвечает:
http://сервер/база/odata/standard.odata/$metadata— описание опубликованных объектов. Из него берите точные имена видаCatalog_Номенклатураи реквизитов.
Шаг 2. Пользователь 1С#
Создайте отдельного пользователя, например mcp_reader, с правом только на чтение объектов, которые будет отдавать сервер, и правом доступа к стандартному интерфейсу OData. Пароль храните в переменных окружения сервера, а не в коде.
Шаг 3. Код сервера#
pip install "mcp[cli]""""MCP-сервер для базы 1С через стандартный интерфейс OData. Только чтение."""
import os
import re
from typing import Any
from urllib.parse import quote
import httpx2
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
# Адрес публикации, например http://srv/trade/odata/standard.odata
ODATA_URL = os.environ["ONEC_ODATA_URL"].rstrip("/")
# Отдельный пользователь 1С с правом только на чтение нужных объектов
AUTH = (os.environ["ONEC_USER"], os.environ["ONEC_PASSWORD"])
# Белый список: какие объекты читать и какие поля отдавать модели.
# Имена зависят от конфигурации — сверьте их с $metadata своей базы.
ENTITIES = {
"Catalog_Номенклатура": ["Ref_Key", "Code", "Description", "Артикул"],
"Catalog_Контрагенты": ["Ref_Key", "Code", "Description", "ИНН"],
}
MAX_ROWS = 50
GUID = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
mcp = MCPServer("1С: справочники", version="1.0.0",
instructions="Чтение справочников 1С. Изменять данные сервер не умеет.")
async def odata_get(path: str, params: dict[str, str]) -> dict:
async with httpx2.AsyncClient(auth=AUTH, timeout=20) as http:
r = await http.get(ODATA_URL + path, params={"$format": "json", **params})
if r.status_code == 401:
raise ToolError("1С отклонила логин или пароль сервера MCP")
if r.status_code == 404:
raise ToolError("Объект не найден или не включён в состав OData")
if r.status_code >= 400:
raise ToolError(f"1С вернула ошибку HTTP {r.status_code}")
return r.json()
def check_entity(entity: str) -> list[str]:
if entity not in ENTITIES:
raise ToolError(f"Нет доступа к {entity}. Доступны: {', '.join(ENTITIES)}")
return ENTITIES[entity]
@mcp.tool()
def list_sources() -> dict[str, list[str]]:
"""Какие справочники 1С можно читать и какие поля в них есть."""
return ENTITIES
@mcp.tool()
async def search(entity: str, text: str, limit: int = 10) -> list[dict[str, Any]]:
"""Найти элементы справочника по части наименования (без помеченных на удаление)."""
fields = check_entity(entity)
text = text.replace("'", "''") # экранирование кавычки в строке OData
data = await odata_get(f"/{quote(entity)}", {
"$filter": f"substringof('{text}', Description) and DeletionMark eq false",
"$select": ",".join(fields),
"$top": str(max(1, min(limit, MAX_ROWS))),
})
return data.get("value", [])
@mcp.tool()
async def get_item(entity: str, ref_key: str) -> dict[str, Any]:
"""Получить элемент справочника по Ref_Key (GUID из результата search)."""
fields = check_entity(entity)
if not GUID.match(ref_key):
raise ToolError("ref_key должен быть GUID вида 00000000-0000-0000-0000-000000000000")
data = await odata_get(f"/{quote(entity)}(guid'{ref_key}')", {"$select": ",".join(fields)})
return {k: data.get(k) for k in fields}
if __name__ == "__main__":
mcp.run()ENTITIES— белый список: только эти объекты и только эти поля попадут к модели. Закупочные цены, телефоны и другие лишние реквизиты отсекаются через$select.- Фильтр
$filterсобирает сервер. Строка от модели экранируется (кавычка удваивается),ref_keyпроверяется регулярным выражением. MAX_ROWSограничивает выборку,DeletionMark eq falseскрывает помеченные на удаление.- Ошибки 1С превращаются в понятный модели текст через
ToolError.
Шаг 4. Подключение к клиенту#
{
"mcpServers": {
"1c-catalogs": {
"command": "python",
"args": ["/абсолютный/путь/onec_odata_server.py"],
"env": {
"ONEC_ODATA_URL": "http://srv/trade/odata/standard.odata",
"ONEC_USER": "mcp_reader",
"ONEC_PASSWORD": "пароль"
}
}
}
}Что получилось#
Результаты вызовов из нашей проверки:
list_sources() → {"Catalog_Номенклатура": [...], "Catalog_Контрагенты": [...]}
search("Catalog_Номенклатура", "стол") → [{"Ref_Key": "0b8e2a4c-…-000000000001", "Code": "00-001",
"Description": "Стол обеденный", "Артикул": "A-100"}]
search("Catalog_Номенклатура", "O'Neil") → [{"Description": "Стул O'Neil", …}] (кавычка экранирована)
get_item("Catalog_Номенклатура", "<Ref_Key>") → {"Ref_Key": …, "Code": "00-001", "Description": "Стол обеденный", "Артикул": "A-100"}
search("Document_Заказ", "x") → ошибка: Нет доступа к Document_Заказ. Доступны: …
get_item("Catalog_Номенклатура", "1' or 1=1") → ошибка: ref_key должен быть GUID …
неверный пароль → ошибка: 1С отклонила логин или пароль сервера MCPЭлемент «Стол письменный», помеченный на удаление, в выдачу не попал, а реквизит «Цена закупки» отсечён белым списком полей.
Как расширить#
- Документы за период:
Document_ЗаказКлиентас фильтромDate ge datetime'2026-10-01T00:00:00'и$orderby=Date desc. Дату собирайте из параметра типаdate, а не из строки модели. - Остатки: виртуальные таблицы регистров накопления доступны через OData, но имена регистров и измерений зависят от конфигурации — сверяйтесь с
$metadata. - Удалённый режим: запустите сервер по HTTP (
mcp.run(transport="streamable-http")) и закройте его авторизацией.
Безопасность данных базы#
- Отдельный пользователь 1С только для MCP-сервера, с правами на чтение нужных объектов. Не используйте учётную запись администратора или сотрудника.
- Только нужные объекты и поля. Белый список справочников и реквизитов в коде сервера; персональные данные (паспорта, телефоны, зарплаты) не отдавайте без правового основания.
- Никакого произвольного запроса от модели. Фильтры собирает сервер, строковые значения экранируются, идентификаторы проверяются по формату.
- Лимит строк в каждом запросе.
- Только чтение. Запись в базу — отдельный сервер, отдельные права и подтверждение человека.
- HTTPS между MCP-сервером и публикацией базы, если они на разных машинах.
- Журнал вызовов: кто, когда и что запрашивал.
- Сначала копия базы. Проверяйте сервер на копии, а не на рабочей базе.