Skip to main content
Glama
axel-avb

1c-configuration

by axel-avb
README.md
# 1C Configuration MCP Server

MCP-сервер для навигации по конфигурации 1С:Предприятие, выгруженной в исходный
код (XML-метаданные + BSL-модули). Предназначен для подключения LLM-агентов
(Claude, Cursor и любых MCP-клиентов) к «внутреннему устройству» конфигурации:
справочники, документы, регистры, реквизиты, процедуры модулей и граф вызовов.

Транспорт: **streamable HTTP** (FastMCP 4.x), эндпоинт `http://<host>:8765/mcp`.

## Возможности

- Разбор XML-экспорта конфигурации (объекты, элементы, типы данных, ссылки
  между объектами) — `src/config_parser.py`.
- Разбор BSL-модулей: процедуры/функции, видимость (`Экспорт`), вызовы,
  ссылки на объекты конфигурации (`Справочники.X`, `Документы.Y`, ...) —
  `src/bs_parser.py`.
- SQLite-граф (объекты → элементы, модули → символы, вызовы, ссылки) —
  `src/graph.py`.
- Семантический поиск: внешний эмбеддер (OpenAI-совместимый `/embeddings`) →
  ANN-поиск в Qdrant → внешний реранкер (Cohere/Jina-совместимый `/rerank`) —
  `src/embedder.py`, `src/reranker.py`.
- Инкрементальный индекс: хэши контента в SQLite, переэмбедятся только
  изменившиеся узлы; удалённые объекты чистятся из обоих хранилищ —
  `src/indexer.py`.

## Требования

- Python **3.11+**
- Qdrant (сервер по HTTP или локальный режим на диске)
- Эмбеддер с OpenAI-совместимым API: Ollama (`/v1`), vLLM, TEI, Jina, OpenAI,
  Azure — любой, кто отвечает на `POST /embeddings` в формате OpenAI.
- Реранкер (необязаtельно): Cohere/Jina-совместимый `POST /rerank`
  Без реранкера: `search_config` работает, возвращается порядок ANN-поиска (graceful fallback).

### Деградация без внешних сервисов

| Компонент | Что недоступно | Что работает |
|---|---|---|
| Без эмбеддера | `search_config` (семантический поиск) | Все структурные инструменты: `list_objects`, `get_object_elements`, `get_symbol`, `get_callers/callees`, `get_references`, `graph_stats` (SQLite) |
| Без реранкера | Переупорядочивание кандидатов | `search_config` работает в порядке ANN-поиска |
| Без Qdrant | `search_config`, векторный upsert при индексации | SQLite-граф полностью функционален; индексация с `--no-vectors` |

Индексация без эмбеддера: `python -m src.indexer --no-vectors` — строит только
SQLite-граф (объекты, элементы, рёбра). Семантический поиск появится после
заполнения `embedder.base_url` и повторного прогона индексации.

## Установка

```bash
git clone --recurse-submodules https://github.com/axel-avb/mcp-1c-metadata.git
# или, после обычного clone:
git submodule update --init --recursive

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

## Конфигурация

Приоритет (высший побеждает):

1. Переменные окружения (`ONEC_*`) — для секретов и деплоя.
2. JSON-файл конфигурации (путь из `ONEC_MCP_CONFIG`, по умолчанию `./config.json`).
3. Значения по умолчанию в `src/config.py`.

Шаблон: `config.json.example`. Скопируйте в `config.json` и заполните.

```json
{
  "config_root": "/path/to/onec/sources",
  "project_data_dir": "/path/to/onec/project-export",
  "xml_root": "/path/to/onec/code",
  "txt_root": "/path/to/onec/metadata",
  "qdrant": { "url": "http://localhost:6333", "collection": "onec_config" },
  "graph_db_path": "./data/graph.sqlite3",
  "embedder": {
    "base_url": "http://localhost:11434/v1",
    "api_key": "",
    "model": "bge-m3",
    "dimensions": 1024,
    "batch_size": 32
  },
  "reranker": {
    "endpoint": "",
    "api_key": "",
    "model": ""
  },
  "search": { "top_k": 5, "candidate_multiplier": 4 },
  "host": "0.0.0.0",
  "port": 8765,
  "auth_token": "",
  "payload_only": false,
  "node_id_in_payload": true
}
```

Если `auth_token` (или `ONEC_AUTH_TOKEN`) задан, сервер требует заголовок
`Authorization: Bearer <token>` на всех HTTP-запросах. Пустое значение —
аутентификация отключена.

### Переменные окружения

| Переменная | Описание |
|---|---|
| `ONEC_CONFIG_ROOT` | Корень исходников конфигурации 1С (XML/BSL) |
| `ONEC_QDRANT_URL` | URL Qdrant, напр. http://localhost:6333 |
| `ONEC_QDRANT_API_KEY` | API-ключ Qdrant (если включён) |
| `ONEC_QDRANT_COLLECTION` | Имя коллекции (по умолчанию onec_config) |
| `ONEC_GRAPH_DB_PATH` | Путь к SQLite-графу |
| `ONEC_EMBEDDER_BASE_URL` | Base URL эмбеддера (OpenAI-совместимый /embeddings) |
| `ONEC_EMBEDDER_API_KEY` | Ключ эмбеддера |
| `ONEC_EMBEDDER_MODEL` | Модель эмбеддинга (по умолчанию bge-m3) |
| `ONEC_EMBEDDER_DIMENSIONS` | Размерность вектора (должна совпадать с моделью) |
| `ONEC_EMBEDDER_BATCH_SIZE` | Пакетность эмбеддинга |
| `ONEC_RERANKER_ENDPOINT` | URL реранкера (Cohere/Jina-совместимый), пусто = выключен |
| `ONEC_RERANKER_API_KEY` | Ключ реранкера |
| `ONEC_RERANKER_MODEL` | Модель реранкера (необязательно) |
| `ONEC_SEARCH_TOP_K` | Сколько результатов возвращать |
| `ONEC_HOST` | Адрес HTTP-сервера (по умолчанию 0.0.0.0) |
| `ONEC_PORT` | Порт HTTP-сервера (по умолчанию: 8765) |
| `ONEC_AUTH_TOKEN` | Токен аутентификации: если задан, требуется заголовок `Authorization: Bearer <token>` |
| `ONEC_XML_ROOT` | Корень XML-выгрузки (`ConfigDumpInfo.xml` + per-object XML); по умолчанию `project_data_dir/code` |
| `ONEC_TXT_ROOT` | Корень TXT-отчёта (`ОтчетПоКонфигурации.txt`); по умолчанию `project_data_dir/metadata` |
| `ONEC_PAYLOAD_ONLY` | `true` — обновлять только payload в Qdrant без переэмбеддинга |
| `ONEC_NODE_ID_IN_PAYLOAD` | `true` (по умолчанию) — хранить `node_id` в payload Qdrant для `search_config` |

Полный точный список — в `_ENV_MAP` в `src/config.py`.

## Формат исходников конфигурации

Ожидается стандартная выгрузка «конфигурация в исходном коде»:

```
<config_root>/
  Config.xml                              # корневые метаданные
  Catalogs/Catalog.Номенклатура/
    Info.xml                              # метаданные объекта
    ObjectModule.bsl                      # модуль объекта
    ManagerModule.bsl                     # модуль менеджера
    Forms/FormНоменклатуры/FormModule.bsl # модуль формы
  Documents/Document.Реализация/...
  Constants/Constant.ИнформацияОКомпании/...
  Registers/AccumulationRegisters/Register.Обороты/...
  Registers/InformationRegisters/Register.ЦеноваяИнформация/...
```

Парсер терпим к вариациям: принимает и плоские элементы (`<Type>Catalog</Type>`),
и пары «свойство-значение». Имена объектов нормализуются к полной форме с
русским префиксом типа: `Каталог.Номенклатура`, `Документ.Реализация`.

В `tests/sample_config/` лежит минимальный образец для ручного прогона.

## Индексация

```bash
# инкрементальная (по умолчанию): переэмбедятся только изменившиеся узлы
python -m src.indexer

# полный пересбор
python -m src.indexer --full

# без векторов (только граф в SQLite) — для отладки парсеров
python -m src.indexer --no-vectors

# переиндексация одного объекта (русское или английское имя)
python -m src.indexer --object Справочник.Колледжи
python -m src.indexer --object Catalog.Колледжи

# эмбеддить только один слой (object|element|symbol); граф остаётся в SQLite
python -m src.indexer --kind symbol

# обновить только payload в Qdrant без переэмбеддинга (после смены флагов)
python -m src.indexer --payload-only

# другой файл конфигурации
python -m src.indexer --config /path/to/config.json
```

Вывод — статистика: число объектов, BSL-файлов, узлов/рёбер графа и
обновлённых векторов. Индексация идемпотентна; при удалении объектов из
конфигурации их узлы и векторы удаляются из хранилищ.

## Запуск MCP-сервера

```bash
python -m src.server
# INFO: Starting MCP server '1c-configuration' with transport 'streamable-http'
#       on http://0.0.0.0:8765/mcp
```

Сервер читает ту же конфигурацию и держит в памяти граф, клиент эмбеддера и
клиент реранкера. Векторные запросы к Qdrant выполняются на лету.

### Инструменты (tools)

**Инвентарь и структура**

| Инструмент | Назначение |
|---|---|
| `get_metadata(mode, category?, object_name?, object_match?, limit?, offset?)` | Инвентарь: `summary` (счётчики), `categories` (типы), `objects` (список с фильтром) |
| `inspect_metadata_object(object_ref, detail?, sections?)` | Досье объекта одним вызовом: счётчики, структура, формы, BSL-модули, использование |
| `get_metadata_object_structure(object_ref, sections?, tabular_part?)` | Структура объекта по секциям (attributes/tabular_parts/forms/commands/layouts/resources/dimensions) |
| `get_metadata_element_type(object_ref, element_type, container_ref?)` | Типизированные дети объекта (реквизиты/ресурсы/измерения/…) |
| `get_metadata_details(ref_type, ref, owner_ref?, mode?)` | Разрешение ссылки в карточку узла (object/element/symbol) |
| `list_objects(type?)` | Объекты по типам с числом элементов |
| `get_object_elements(object_name, include_children=true)` | Элементы объекта: реквизиты, табличные части, команды |

**Поиск**

| Инструмент | Назначение |
|---|---|
| `search_config(query, top_k=5, kind?)` | Семантический поиск по объектам/элементам/процедурам (embed → Qdrant → rerank) |
| `find_metadata_objects(search_by, search_text?, within_object?, limit?)` | Найти объекты по описанию или по имени дочернего элемента («где поле X») |
| `find_metadata_elements(element_type, element_name?, owner_object?, mode?, limit?)` | Дочерние элементы по всему проекту с контекстом владельца |
| `find_metadata_usages(target_ref, mode?)` | Кто ссылается на объект / какие модули его используют |

**BSL**

| Инструмент | Назначение |
|---|---|
| `search_bsl_code(query, top_k=5)` | Семантический поиск по телам процедур/функций |
| `get_symbol(name, object_name?)` | Процедура/функция: сигнатура, видимость, модуль, тело |
| `get_callers(name, object_name?)` | Кто вызывает процедуру (входные рёбра) |
| `get_callees(name, object_name?)` | Что вызывает процедура (выходные рёбра) |
| `get_bsl_call_graph(routine_ref, mode?, depth?, owner_ref?)` | Граф вызовов: `callees`/`callers`/`subtree` (BFS с глубиной) |
| `get_bsl_routine_body(routine_ref, owner_ref?, body_offset?, body_limit?)` | Тело рутины с пагинацией |
| `get_bsl_modules(mode, owner_ref?, module_ref?, routine_name?)` | Модули объекта и их рутины |
| `search_bsl_routines(name?, mode?, object_name?, exported_only?, limit?)` | Поиск рутин по имени/экспорту/сигнатуре |

**Ссылки и служебные**

| Инструмент | Назначение |
|---|---|
| `get_references(object_name, direction="both")` | Ссылки на объект и от объекта |
| `reindex(full=false)` | Пересбор индекса в фоне |
| `reindex_status` | Статус фоновой переиндексации |
| `graph_stats` | Статистика графа: узлы/рёбра по видам |

Дорожная карта оставшегося (полная — `PLAN.md` §13):

- **Слой B** (нужна интеграция парсеров из сабмодуля): `find_predefined_values`,
  `get_event_subscriptions`, `get_access_rights`.
- **Слой C** (отложено, нет модели): `get_extension_object_diff`,
  `get_form_structure`/`find_form_links`, `find_dependency_paths`.

### Расход токенов на инициализацию модели

Полный `tools/list` для всех 23 инструментов — **~16 800 символов** (~4.2 тыс.
токенов при латинском тексте). Эта сумма уходит на каждый старт/повторную
инициализацию клиента (загрузка схемы инструментов в контекст).

Большую часть объёма составляет JSON-schema (генерируется FastMCP автоматически),
а не docstring'и. В `pre-4-cut` из docstring'ов убраны многословные перечисления
значений (`sections`, `element_type`, `search_by`, списки типов) — они
дублировали схему; экономия ~1К символов без потери функциональности.

Примеры вызовов (что видит LLM-агент):

```text
list_objects(type="catalog")
  → «Каталоги (catalog) — 2
       • Каталог.Номенклатура — Номенклатура [5 elem.]
       • Каталог.ПрайсЛист [12 elem.]»

search_config(query="где хранится цена товара", top_k=5)
  → [0.87] element: Цена [Число(10,2)] (объект: Каталог.Номенклатура)
    [0.74] object: РегистрСведений.ЦеноваяИнформация ...

get_callers(name="РассчитатьСуммуРеализации")
  → Callers of РассчитатьСуммуРеалиции:
       • Обработать — Документ.Реализация (Documents/Document.Реализация/ObjectModule.bsl)
```

## Подключение MCP-клиента

Эндпоинт: `http://<host>:<port>/mcp` (transport: streamable HTTP).

### Claude Desktop / Claude Code

```json
{
  "mcpServers": {
    "1c": {
      "url": "http://localhost:8765/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}
```

Заголовок `headers` нужен только если задан `auth_token`.

### Python (FastMCP Client)

```python
from fastmcp import Client

async def main():
    async with Client("http://localhost:8765/mcp") as c:
        print(await c.call_tool("list_objects", {}))
        print(await c.call_tool("search_config", {"query": "цена товара"}))
```

### Проверка вручную

```bash
# список инструментов (MCP JSON-RPC)
curl -s http://localhost:8765/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

## Docker

`docker-compose.yml` поднимает Qdrant + сервер. Исходники конфигурации
монтируются в `/data/config`, эмбеддер ожидается по адресу
`ONEC_EMBEDDER_BASE_URL` (например, Ollama на хосте: `http://host.docker.internal:11434/v1`).

```bash
# 1. заполнить .env (см. .env.example) и config.json
# 2. загрузить модель эмбеддинга в Ollama (или другой сервис):
#    ollama pull bge-m3
docker compose up -d --build

# индексация при старте выполняется автоматически (entrypoint.sh)
docker compose logs -f mcp
```

Вручную: `docker compose run --rm mcp python -m src.indexer --full`.

## Архитектура

```
XML/BSL-экспорт 1С
        │
        ▼
src/config_parser.py ── объекты, элементы, ссылки (типы данных)
src/bs_parser.py ────── процедуры, вызовы, ссылки из кода
        │
        ▼
src/indexer.py ───────── инкрементальная сборка
   ├──► src/graph.py    SQLite: узлы (object/element/symbol/module),
   │                    рёбра (HAS_ELEMENT, CHILD_ELEMENT, REFERENCE,
   │                    DEFINES, CALLS, USES)
   └──► Qdrant          векторы (эмбеддер: OpenAI-совместимый API)
        │
        ▼
src/server.py (FastMCP, streamable HTTP :8765/mcp)
   list_objects / get_object_elements / search_config (embed→Qdrant→rerank)
   get_symbol / get_callers / get_callees / get_references / reindex / graph_stats
```

Виды рёбер графа:

- `HAS_ELEMENT` — объект → элемент (прямые дети)
- `CHILD_ELEMENT` — элемент → вложенный элемент
- `REFERENCE` — элемент → объект (типы данных, напр. реквизит → справочник)
- `DEFINES` — модуль → символ
- `CALLS` — символ → символ (1С имеет плоское глобальное пространство имён,
  поэтому вызов может вести к нескольким одноимённым символам — это норма)
- `USES` — модуль → объект (`Справочники.X` и т.п. в коде)

## Тесты

```bash
# unit-тесты (парсеры/маппинг/легаси-флаг/чек-суммы)
pytest tests/

# компиляция всех модулей
python -m compileall src
```

## Ограничения

- BSL-парсер строковый/regex'овый, а не полный грамматический: достаточен для
  графа вызовов и поиска, но не для строгой валидации кода.
- **Условная компиляция не раскрывается.** BSL-парсер не обрабатывает директивы
  препроцессора `#Если`/`#Иначе`/`#КонецЕсли`, поэтому процедура, объявленная в
  обеих ветках (напр. серверная `Печать` для обычных неуправляемых форм внутри
  `#Если ТолстыйКлиентОбычноеПриложение ... #Иначе ... #КонецЕсли`), даёт две
  декларации с одинаковым `stable_id`. На поиск/граф не влияет (upsert по id
  перезаписывает), но в счётчике символов возможен дубль.
- Вызовы разрешаются по имени (глобальное пространство имён 1С): при
  одноимённых процедурах в разных модулях `CALLS`-рёбра ведут на всех
  кандидатов; инструмент `get_symbol` принимает `object_name` для уточнения.
- Размерность вектора (`embedder.dimensions`) должна совпадать с моделью,
  иначе Qdrant отклонит upsert.
- **Объекты вне `.txt`-отчёта** (бизнес-процессы, общие модули/формы, веб-сервисы
  и др.) индексируются как module/symbol, но без object-узла и `HAS_MODULE`-связи
  (см. `PLAN.md` §12).
- **Легаси-формы** (`FormType=Ordinary`, `Form.bin`) — BSL-код извлекается
  вендоренным бинарным парсером (`parsers/`, из github.com/axel-avb/v8_ordinary_unpack);
  процедуры попадают в граф вызовов с `is_legacy=True`. Схема формы (элементы
  управления) — в text-описании парсера, не раскладывается в узлы графа.
- **Qdrant RAM** — единственное реальное ограничение по объёму: ~6 КБ/вектор
  (1536 dims float32), для ЕРП 2.x это десятки ГБ RAM (см. `PLAN.md` §12).

Maintenance

ActivityMaintained
ResponsivenessNo issues