mcpBPMSoft
by Catter58
README.md
# MCP-сервер для BPMSoft
Подключение BPMSoft 1.8 к LLM-агенту (Claude Desktop, Cursor, любой клиент Model Context Protocol) — чтобы спросить «найди контакты из Москвы за последний месяц» или «зарегистрируй Иванова из Ромашки», а не вручную набирать OData-фильтры.
Сервер берёт на себя всё, что обычно мешает:
- авторизацию, CSRF-токены и сессии BPMSoft;
- разницу между OData v3 и v4 (форматы ID, имена полей, $batch);
- перевод «Город = Москва» в фильтр по справочнику — либо `City/Name eq 'Москва'`, либо `CityId = <UUID>` на записи;
- защиту от случайного массового удаления, переполнения контекста модели и SSRF;
- запуск бизнес-процессов через `ProcessEngineService`.
В коробке **32 операционных инструмента (+ `bpm_init` при включённом env-creds opt-in), 6 prompts, 4 ресурса** — от низкоуровневого CRUD до готовых сценариев «зарегистрировать контакт + контрагента» и «найти всё про Иванова».
---
## Содержание
- [Что это решает](#что-это-решает)
- [Быстрый старт за 5 минут](#быстрый-старт-за-5-минут)
- [Подключение к Claude Desktop](#подключение-к-claude-desktop)
- [Примеры диалогов](#примеры-диалогов)
- [Все инструменты — кратко](#все-инструменты--кратко)
- [Готовые сценарии (MCP prompts)](#готовые-сценарии-mcp-prompts)
- [Ресурсы (MCP resources)](#ресурсы-mcp-resources)
- [Конфигурация](#конфигурация)
- [Скрипты](#скрипты)
- [Отладка](#отладка)
- [Ограничения BPMSoft 1.8](#ограничения-bpmsoft-18)
- [Часто задаваемые вопросы](#часто-задаваемые-вопросы)
- [Лицензия](#лицензия)
---
## Что это решает
BPMSoft предоставляет OData-API, который мощный, но очень многословный. Запрос «контакты из Москвы, созданные на этой неделе» на чистом OData выглядит так:
```http
GET /0/odata/Contact?$filter=City/Name eq 'Москва' and CreatedOn ge 2026-04-26T00:00:00Z&$select=Id,Name,Email
Cookie: BPMSESSIONID=...; BPMCSRF=...
BPMCSRF: <csrf>
ForceUseSession: true
```
LLM-агенту, который пытается его собрать, нужно знать:
- где живёт OData (для .NET 8 — `/odata`, для .NET Framework — `/0/odata`, для v3 — отдельный путь);
- что строки в одинарных кавычках, GUID-ы у v3 в `guid'…'`, у v4 без обёртки;
- что лимит ответа 20 000 строк, $batch только в v4 и не больше 100 подзапросов;
- что lookup-поля у v4 заканчиваются на `Id`, а у v3 — нет;
- что русские названия полей доступны только через системную таблицу `SysEntitySchemaColumn`, а на части стендов её нет вовсе;
- и десяток других мелочей.
**Этот сервер прячет всё это за человеческим интерфейсом**:
```
Пользователь: Найди контакты из Москвы за последние 30 дней
Агент → tool: bpm_search_records
collection: "Contact"
criteria: [
{ field: "Город", op: "равно", value: "Москва" },
{ field: "Дата создания", op: "за последние N дней", value: 30 }
]
Сервер → Скомпилирует $filter, разрешит «Москва» в UUID города,
применит лимит max_records (от переполнения контекста),
вернёт сводку + первые 5 записей + cursor для следующих.
```
---
## Быстрый старт за 5 минут
**Требуется**: Node.js 18+, инстанс BPMSoft 1.8 с включённым OData (по умолчанию — да).
```bash
# 1. Склонировать репозиторий
git clone https://github.com/Catter58/mcpBPMSoft.git
cd mcpBPMSoft
# 2. Установить зависимости
npm install
# 3. Собрать
npm run build
# 4. Прописать URL целевого стенда в .env
cp .env.example .env
# открыть .env, заполнить BPMSOFT_URL
# 5. Запустить сервер
npm start
```
По умолчанию сервер поднимает **Streamable HTTP** транспорт на порту `8007`. MCP-клиент обращается к нему по HTTP с авторизационными заголовками своего пользователя — секреты на сервере не хранятся.
Для локальной отладки через stdio установите `MCP_TRANSPORT=stdio` в `.env`.
---
## Подключение к Claude Desktop
Сервер использует **Streamable HTTP** транспорт по умолчанию. Для Claude Desktop, который запускает процесс локально, удобен режим stdio. В файле `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) или `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
```json
{
"mcpServers": {
"bpmsoft": {
"command": "node",
"args": ["/абсолютный/путь/к/mcpBPMSoft/build/index.js"],
"env": {
"BPMSOFT_URL": "https://mycompany.bpmsoft.com",
"MCP_TRANSPORT": "stdio",
"BPMSOFT_ODATA_VERSION": "4",
"BPMSOFT_PLATFORM": "net8"
}
}
}
}
```
Перезапустить Claude Desktop. В чате появится бейдж `bpmsoft`, и инструменты будут доступны модели.
> **Авторизация в stdio-режиме.** При запуске через Claude Desktop аутентификация выполняется per-request: каждый вызов инструмента должен нести заголовок `BPMCSRF` и cookie `.ASPXAUTH`, `BPMSESSIONID`, `CsrfToken` — Claude Desktop передаёт их автоматически из сессии браузера пользователя. Секреты на сервере не хранятся.
Полный пример — в [`examples/claude_desktop_config.json`](examples/claude_desktop_config.json).
---
## Подключение к уже работающему HTTP-серверу
Если сервер запущен отдельно (`MCP_TRANSPORT=http`, порт `8007`), MCP-клиент подключается к нему по URL — **без** `command`/`args`. Транспорт задаётся полем `type: "http"` (не `transport`). В штатной схеме заголовки авторизации **прокидываются автоматически** — их не хардкодят в конфиге:
```json
{
"mcpServers": {
"bpmsoft": {
"type": "http",
"url": "http://bpmsoft-mcp-host:8007/mcp"
}
}
}
```
Авторизация per-request: заголовок `BPMCSRF` и cookie (`.ASPXAUTH`, `BPMSESSIONID`, `CsrfToken`) сервер извлекает из **каждого входящего** запроса (`extractAuthFromHeaders`, `src/auth/request-context.ts`) и пробрасывает в BPMSoft. Кто их подставляет:
- **Фронтящий reverse proxy** (штатно, паттерн mcp-proxy-server) — берёт живую сессию пользователя и добавляет заголовки к каждому проксируемому запросу.
- **Сам MCP-клиент**, если умеет форвардить сессионные заголовки/cookie.
Хардкодить токен в `headers` конфига **не нужно и вредно** — CSRF-токен и сессия протухают, статичное значение сломается. Явные `headers` уместны только для разовой ручной отладки (`curl`, Postman):
```bash
curl -X POST http://bpmsoft-mcp-host:8007/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "BPMCSRF: <csrf-token>" \
-H "Cookie: .ASPXAUTH=<...>; BPMSESSIONID=<...>; CsrfToken=<...>" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
Замечания:
- Сервер stateless и принимает **любой POST** — путь в URL игнорируется, `/mcp` взят по соглашению.
- Запрос без валидных заголовков → `AuthRequiredError`. Секреты на сервере не хранятся.
- `GET`/`DELETE` не поддерживаются (stateless JSON-режим) — вернётся `405`.
- В production ставьте перед сервером HTTPS reverse proxy и используйте `https://` URL — см. раздел о безопасности.
---
## Запуск в Docker
Production-образ (multi-stage `Dockerfile`): транспорт, host и порт уже зашиты в `ENV` (`MCP_TRANSPORT=http`, `MCP_HTTP_HOST=0.0.0.0`, `MCP_HTTP_PORT=8007`), контейнер работает под non-root `node`.
```bash
# сборка
docker build -t mcp-bpmsoft .
# запуск — BPMSOFT_URL единственный обязательный env
docker run -d --name mcp-bpmsoft \
-p 8007:8007 \
-e BPMSOFT_URL=https://bpm.example.ru \
mcp-bpmsoft
```
Опциональные env (значения по умолчанию — OData v4 / net8):
```bash
docker run -d --name mcp-bpmsoft \
-p 8007:8007 \
-e BPMSOFT_URL=https://bpm.example.ru \
-e BPMSOFT_ODATA_VERSION=4 \
-e BPMSOFT_PLATFORM=net8 \
mcp-bpmsoft
```
Замечания:
- `MCP_TRANSPORT`/`MCP_HTTP_HOST`/`MCP_HTTP_PORT` переопределять не нужно — образ уже настроен на HTTP. Меняете внешний порт маппингом (`-p 9000:8007`), а не env.
- Контейнер слушает `0.0.0.0:8007` внутри — наружу не публикуйте напрямую. В production перед контейнером ставьте HTTPS reverse proxy, а порт ограничьте фаерволом (см. раздел о безопасности).
- Логи идут в `stderr`: `docker logs -f mcp-bpmsoft`. Строка `MCP BPMSoft OData Server running on http (port 8007)` = сервер поднялся.
- Dev-образ с watch — `Dockerfile.dev`: `docker build -f Dockerfile.dev -t mcp-bpmsoft:dev .`
---
## Примеры диалогов
### Пример 1 — поиск с человеческим языком
```
Пользователь: покажи активные сделки на сумму больше 500 тысяч,
где менеджер — Иванова
Claude → bpm_search_records
collection: "Opportunity"
criteria: [
{ field: "Сумма", op: "больше", value: 500000 },
{ field: "Owner", op: "равно", value: "Иванова" },
{ field: "Stage", op: "не равно", value: "Закрыта (выиграна)" }
]
format: "markdown"
Сервер вернёт:
Скомпилированный $filter:
Amount gt 500000 and OwnerId eq <UUID Ивановой>
and StageId ne <UUID этапа закрыта>
Получено записей: 7
| Id | Title | Amount | Stage | Owner |
|-----|--------------------|---------|------------|----------|
| ... | Поставка софта | 1200000 | В работе | Иванова |
...
```
### Пример 2 — регистрация нового контакта
```
Пользователь: создай контакт Петров Пётр, телефон +7-999-1234567,
работает в ООО Орбита
Claude → bpm_register_contact
name: "Петров Пётр"
phone: "+7-999-1234567"
account_name: "ООО Орбита"
Сервер:
1. Ищет Account по Name='ООО Орбита' → не нашёл → создаёт.
2. Получает accountId.
3. Создаёт Contact с подставленным AccountId.
4. Возвращает оба UUID.
```
### Пример 3 — массовое обновление со страховкой
```
Пользователь: закрой все мои задачи старше года
Claude → bpm_search_records (узнать сколько таких)
collection: "Activity"
criteria: [
{ field: "Owner", op: "равно", value: "<текущий пользователь>" },
{ field: "CreatedOn", op: "меньше", value: "2025-05-02T00:00:00Z" },
{ field: "Status", op: "не равно", value: "Завершено" }
]
[ответ: 12 записей]
Пользователь: ага, закрывай
Claude → bpm_update_by_filter
collection: "Activity"
filter: "OwnerId eq <UUID> and CreatedOn lt 2025-05-02T00:00:00Z and StatusId ne <UUID Завершено>"
data: { Status: "Завершено" }
expected_count: 12 # страховка: если найдено иное число — операция отменится
```
### Пример 4 — запуск бизнес-процесса
```
Пользователь: запусти процесс «UsrCalculatePipeline» и покажи результат
Claude → bpm_run_process
process_name: "UsrCalculatePipeline"
parameters: { period_days: "30" }
result_parameter_name: "UsrPipelineSummary"
Сервер:
GET /ServiceModel/ProcessEngineService.svc/UsrCalculatePipeline/Execute
?period_days=30&ResultParameterName=UsrPipelineSummary
Парсит XML-обёртку <string>...</string>, JSON.parse содержимого.
Возвращает структуру в structuredContent.
```
### Пример 5 — комментарий в ленте записи
```
Пользователь: оставь в ленте этой сделки заметку «звонил, обещали вернуться в среду»
Claude → bpm_post_feed
collection: "Opportunity"
id: "<UUID сделки>"
message: "звонил, обещали вернуться в среду"
Сервер:
POST /odata/SocialMessage
body: { Message: "...", EntitySchemaName: "Opportunity", EntityId: "..." }
```
---
## Все инструменты — кратко
### Подключение (env-creds opt-in)
| Инструмент | Зачем |
|---|---|
| `bpm_init` | Подключиться к BPMSoft через логин/пароль (доступен только при `BPMSOFT_ALLOW_ENV_CREDS=true`) |
### Чтение
| Инструмент | Зачем |
|---|---|
| `bpm_get_records` | Получить записи коллекции с фильтром/select/expand/order/top/skip; **без `select` — только `Id` + колонка отображения**, все колонки — по `select='*'`; **safe-pagination** + **token-aware форматы** (compact/full/markdown); поддерживает opaque cursor |
| `bpm_get_record` | Одна запись по UUID (та же проекция по умолчанию) |
| `bpm_count_records` | Количество записей по фильтру |
| `bpm_whoami` | Кто выполняет вызов (SysAdminUnit + Contact) и текущее время в его поясе |
| `bpm_search_records` | **Поиск с критериями на русском** — массив `{field, op, value}`, операторы «равно», «содержит», «за последние N дней» и т.п.; компилирует в OData $filter |
### Запись
| Инструмент | Зачем |
|---|---|
| `bpm_create_record` | Создать запись; lookup-поля резолвятся по тексту, ключи можно на русском |
| `bpm_update_record` | Обновить по UUID |
| `bpm_delete_record` | Удалить по UUID |
| `bpm_update_by_filter` | Массовое обновление с обязательным `expected_count` (страховка) |
| `bpm_delete_by_filter` | Массовое удаление с обязательным `expected_count` |
### Схема и справочники
| Инструмент | Зачем |
|---|---|
| `bpm_get_collections` | Список доступных EntitySet (limit по умолчанию 100, в ответе total/has_more) |
| `bpm_get_schema` | Поля коллекции с русскими подписями, типами, lookup-связями |
| `bpm_lookup_value` | Найти UUID справочного значения; по умолчанию нечёткий каскад (регистр, кавычки, орг-формы АО/ООО игнорируются — «Ланит» найдёт «АО «ЛАНИТ»») |
| `bpm_get_enum_values` | Все значения справочника, к которому привязано lookup-поле (например, все ActivityCategory) |
| `bpm_workflow_catalog` | **Карта типичных сценариев + связи между сущностями + ограничения BPMSoft 1.8.** Хорошо вызывать в начале сессии. |
| `bpm_find_field` | Найти поле по фрагменту русского/английского названия в уже загруженных схемах |
| `bpm_describe_instance` | **Сводка по инстансу** за один вызов: главные сущности, их счётчики, кастомные коллекции/поля (`Usr*`). Кеш 5 мин. |
### Пакетные операции (только OData v4)
| Инструмент | Зачем |
|---|---|
| `bpm_batch_create` | Создать N записей одним $batch |
| `bpm_batch_update` | Обновить N записей одним $batch |
| `bpm_batch_delete` | Удалить N записей одним $batch |
Все три поддерживают `continue_on_error`.
### Файлы
| Инструмент | Зачем |
|---|---|
| `bpm_upload_file` | Загрузить локальный файл в `SysImage` + опционально привязать к записи |
| `bpm_download_file` | Скачать файл из `SysImage` |
| `bpm_field_upload` | PUT бинарных данных в произвольное поле сущности (`{Coll}({id})/{Field}`) |
| `bpm_field_download` | GET бинарных данных из поля сущности |
| `bpm_field_delete` | Очистить бинарное поле |
### Готовые workflow-инструменты
| Инструмент | Зачем |
|---|---|
| `bpm_register_contact` | Создать Account (или найти) + создать Contact + привязать |
| `bpm_log_activity` | Создать Activity с резолвом типа/владельца по тексту, опц. привязка к Contact/Account/Opportunity |
| `bpm_set_status` | Сменить статус по человеческому имени; сервер сам найдёт правильное status-поле и его справочник |
| `bpm_search_unified` | Сквозной поиск по подстроке в Contact/Account/Lead/Opportunity (плоский список) |
### Бизнес-процессы и лента
| Инструмент | Зачем |
|---|---|
| `bpm_run_process` | Запустить БП через `ProcessEngineService.svc`, опц. забрать результат через `result_parameter_name` |
| `bpm_exec_process_element` | Возобновить элемент уже выполняющегося процесса по UID |
| `bpm_post_feed` | Опубликовать сообщение в ленту записи (через коллекцию `SocialMessage`) |
---
## Готовые сценарии (MCP prompts)
LLM-клиенты с поддержкой prompts (Claude Desktop, Cursor) могут вызвать готовый сценарий за одну команду — модель получит сразу шаблон с инструкциями и нужными tool-ами:
| Prompt | Аргументы | Что делает |
|---|---|---|
| `getting_started` | — | Обзор сервера, главные сущности, типичные сценарии |
| `quick_search` | `query` | Сквозной поиск + детали при необходимости |
| `create_contact_flow` | `name, account?, email?, phone?` | Регистрация контакта в один заход |
| `weekly_report` | `period_days?` | Отчёт: новые контакты, активные сделки, завершённые задачи |
| `cleanup_duplicates_check` | `collection, field?` | Поиск потенциальных дубликатов (без удаления) |
| `pipeline_analysis` | `stage_field?` | Анализ воронки Opportunity: распределение по стадиям, средняя сумма |
---
## Ресурсы (MCP resources)
Браузабельные URI, которые модель может «прочитать» вместо tool-вызова — дешевле по токенам, удобно для карточек:
```
bpmsoft://collections — список всех EntitySet
bpmsoft://collection/{name} — карточка коллекции (поля + record_count)
bpmsoft://entity/{collection}/{id} — карточка одной записи
bpmsoft://schema/{name} — только схема коллекции (быстрее, без count)
```
---
## Конфигурация
Все параметры — через переменные окружения. Единственная обязательная переменная — `BPMSOFT_URL`; остальные имеют разумные defaults.
### Основные параметры
| Переменная | По умолчанию | Описание |
|---|---|---|
| `BPMSOFT_URL` | — *(обязательно)* | URL приложения, например `https://mycompany.bpmsoft.com` |
| `MCP_TRANSPORT` | `http` | Транспорт: `http` (Streamable HTTP, production) или `stdio` (локальная отладка) |
| `MCP_HTTP_PORT` | `8007` | Порт HTTP-сервера (при `MCP_TRANSPORT=http`) |
| `BPMSOFT_ODATA_VERSION` | `4` | `4` или `3` |
| `BPMSOFT_PLATFORM` | `net8` | `net8` или `netframework` |
| `BPMSOFT_PAGE_SIZE` | `5000` | Размер страницы при автопагинации |
| `BPMSOFT_MAX_BATCH_SIZE` | `100` | Лимит подзапросов в `$batch` |
| `BPMSOFT_LOOKUP_CACHE_TTL` | `300` | TTL кеша lookup в секундах |
| `BPMSOFT_REQUEST_TIMEOUT` | `30000` | Таймаут одного HTTP-запроса (мс) |
| `BPMSOFT_MAX_FILE_SIZE` | `10485760` | Лимит размера файла (10 МБ) |
| `BPMSOFT_DEBUG` | `off` | `1` — логировать `method url status duration`; `trace` — ещё и тела с маскированием паролей и токенов |
### Авторизация
**По умолчанию — per-request.** Сервер не хранит учётных данных. Каждый входящий MCP HTTP-запрос должен нести:
- заголовок `BPMCSRF` — CSRF-токен пользователя;
- заголовок `Cookie` с полями `.ASPXAUTH`, `BPMSESSIONID`, `CsrfToken`.
Сервер извлекает эти значения через AsyncLocalStorage и пробрасывает их в каждый OData-запрос к BPMSoft. Запрос без авторизационных данных завершается ошибкой `AuthRequiredError` (HTTP 401). Эта модель аналогична mcp-proxy-server.
#### Env-creds (скрытый opt-in)
> **Не рекомендуется для on-prem без явного согласия службы ИБ.**
Если выставить `BPMSOFT_ALLOW_ENV_CREDS=true`, сервер дополнительно:
- включает путь аутентификации через `BPMSOFT_USERNAME` / `BPMSOFT_PASSWORD` (login через `AuthService.svc`);
- регистрирует инструмент `bpm_init`, позволяющий передать логин/пароль через MCP-вызов.
По умолчанию (`BPMSOFT_ALLOW_ENV_CREDS` не задана или `false`) `bpm_init` **не регистрируется**, а env-creds путь не используется. В стандартной документации и подсказках этот режим не рекламируется.
### Deployment hardening (on-prem)
- **Default bind is loopback** (`MCP_HTTP_HOST=127.0.0.1`). Set explicitly only if the reverse proxy lives on a different interface; never bind to `0.0.0.0` in production.
- **Plain HTTP only** — the server must sit behind an HTTPS-terminating reverse proxy (nginx, Caddy, etc.). Never expose port 8007 directly to untrusted networks.
- **Firewall the host** so only the reverse proxy can reach port 8007, and so the only permitted outbound destination is the BPMSoft stand.
- Keep `BPMSOFT_ALLOW_ENV_CREDS` unset (default per-request auth); keep `BPMSOFT_DEBUG` unset in production.
- Run `npm audit` regularly to catch transitive dependency vulnerabilities.
- Note: upstream OData error messages are surfaced verbatim to callers and may echo request data (field names, filter values). Account for this when configuring aggregated log storage and retention.
---
## Скрипты
```bash
npm run build # компиляция TS -> JS в build/
npm start # запустить собранный сервер
npm run dev # tsc --watch на время разработки
npm test # vitest run (95 тестов: unit + интеграция через MSW)
npm run test:watch # тесты в watch-режиме
npm run lint # eslint (typescript-eslint, рекомендованный preset)
npm run lint:fix # автоисправление
npm run format # prettier --write
npm run format:check # prettier --check (для CI)
```
---
## Отладка
Самый быстрый способ увидеть, что именно уходит в BPMSoft — включить `BPMSOFT_DEBUG`:
```bash
BPMSOFT_DEBUG=1 npm start
# -> [HttpClient][req] GET https://.../odata/Contact?$top=10
# -> [HttpClient][res] GET .../odata/Contact -> 200 (143ms)
BPMSOFT_DEBUG=trace npm start
# -> также выводит headers и тела запросов/ответов;
# BPMCSRF/Cookie/UserPassword автоматически маскируются.
```
Для проверки подключения без MCP-клиента можно запустить сервер напрямую — он выводит в `stderr`:
```
[Server] Configuration loaded from environment variables
Target: https://mycompany.bpmsoft.com
OData: v4, Platform: net8
MCP BPMSoft OData Server running on http (port 8007)
Registered 31 operational tools
Registered 6 prompts, 4 resource templates
```
При `MCP_TRANSPORT=stdio` строка транспорта будет `running on stdio`.
---
## Ограничения BPMSoft 1.8
| Ограничение | Значение |
|---|---|
| Максимум строк в OData-ответе | 20 000 |
| Максимум подзапросов в `$batch` | 100 |
| Максимальный размер файла | 10 МБ (настраивается) |
| Длина query string в OData v3 | 4 000 символов |
| `$batch` в OData v3 | **не поддерживается** (используйте v4) |
| Создание системных пользователей | не поддерживается |
| Прямой HTTP-API для EntitySchemaQuery | не предусмотрен — используйте обёртку через бизнес-процесс (см. сценарий `esq-via-process` в `bpm_workflow_catalog`) |
---
## Часто задаваемые вопросы
**LLM путается в OData-синтаксисе. Что делать?**
Используйте `bpm_search_records` с criteria-массивом. Он принимает поля по русской подписи, операторы по-русски, сам экранирует значения и ставит правильный синтаксис. Сырые `$filter` нужны только для очень специфичных случаев.
**Сервер вернул 20 000 строк в одном ответе и контекст модели «лопнул».**
По умолчанию `bpm_get_records` ограничивает выдачу `max_records=1000` и пагинация **выключена**. Если хотите всё — передайте `auto_paginate: true` и увеличьте `max_records`. Для пошагового перебора возвращается `cursor` — передайте его в следующий вызов и получите следующую страницу без перенабора параметров.
**Почему в ответе только `Id` и `Name`, а не все колонки?**
Это защита контекста: без `select` сервер сам ставит `$select=Id,<колонка отображения>` (`Name`/`Title`/`Subject`/…). Нужны конкретные поля — перечислите их в `select`; нужна вся запись — передайте `select='*'`. То же и у `bpm_get_record`/`bpm_search_records`.
**В выдаче `CityId` — как понять, что это за город, без второго запроса?**
Никак не надо: рядом с каждой lookup-колонкой сервер кладёт её имя — `CityId` + `CityName`. Имена добираются одним запросом на справочник (не на запись) и настоящие колонки не затираются. Отключается параметром `resolve_lookups: false`.
**Модель просит «поставь мне задачу на сегодня». Что она должна вызвать?**
`bpm_whoami` — он возвращает и текущего пользователя с его контактом, и реальное время в его поясе. Личность вычисляет сам BPMSoft макросом DataService в сессии вызывающего, поэтому при per-request авторизации ответ соответствует владельцу cookie/BPMCSRF, а не тому, кто залогинился последним. Дальше `contact_id` подставляется в `OwnerId`/`AuthorId`, а «сегодня» в критериях считается оператором `сегодня`.
**Как задать «за сегодня» или «в этом месяце»?**
Оператором, а не датами: `{"field": "Дата создания", "op": "сегодня"}`. Есть `сегодня`, `вчера`, `завтра`, `на этой неделе`, `на прошлой неделе`, `в этом месяце`, `в прошлом месяце`, `в этом квартале`, `в этом году` (и английские синонимы). Границы считаются по календарю **часового пояса пользователя** и компилируются в полуинтервал `ge …T21:00:00Z and lt …T21:00:00Z` — для Москвы это ровно местные сутки, а не UTC-шные. Пояс берётся из `BPMSOFT_TIMEZONE`, иначе — пояс процесса.
**Можно посмотреть запрос, не выполняя его?**
`dry_run: true` у `bpm_get_records` и `bpm_search_records` вернёт собранный URL, `$filter`, `$select` и `$expand`, ничего не запрашивая. Удобно проверить, как разрешились имена полей и во что превратились критерии.
**Модель написала «Контакт» вместо «Contact».**
Сработает. Имя коллекции резолвится сервером по схеме: русская подпись объекта берётся из `SysSchema.Caption`, регистр не важен, опечатка возвращает `not_found` со списком похожих имён (`Contactt` → `Contact, ContactTag, Contract, …`), а не голый 404.
**Надо отфильтровать по справочному полю. Сначала доставать UUID?**
Нет. `bpm_search_records` для lookup-колонки со строковым значением сам сравнивает с именем связанной записи: `{"field": "Тип", "op": "равно", "value": "Сотрудник"}` компилируется в `Type/Name eq 'Сотрудник'`. UUID в значении по-прежнему сравнивается с самой FK-колонкой — так дешевле для сервера, а `пусто`/`не пусто` всегда проверяются на FK.
**Поиск по подстроке молча вернул пусто / упал сетевой ошибкой.**
Так вело себя до фикса: часть стендов не переваривает `tolower()` в `$filter` и вместо честного 400 отдаёт 200 и обрывает тело ответа. Теперь обрыв потока распознаётся как отказ от конструкции: сервер запоминает это на весь процесс, пересобирает фильтр без `tolower()` и повторяет запрос, а в `warnings` пишет, что поиск выполнен с учётом регистра.
**Как назвать поле — «Город» или «City»?**
Любым. Сервер сам тянет схему коллекции и сопоставляет колонку: сначала имя, затем caption из `SysSchema`/`SysEntitySchemaColumn`, затем встроенный словарь русских подписей типовых колонок Creatio. Последний нужен потому, что на части стендов представления с caption'ами колонок недоступны (404) и подписей нет вовсе. Имена колонок в `select` проверяются там же — `select: "Название,Город"` уходит в BPMSoft как `Id,Name,CityId`, а опечатка возвращает ошибку с подсказками вместо 400 от сервера.
**Lookup-значения — UUID или текст?**
Текст. Сервер сам резолвит «Москва» → `<UUID>` через `bpm_lookup_value`. Если найдено несколько кандидатов — вернёт список и попросит уточнить. Работает и на запись (`bpm_create_record`, `bpm_update_record`, `bpm_batch_*`, `bpm_update_by_filter`), причём ключ можно давать русской подписью («Город»). Пустая строка в lookup-поле очищает связь.
**OData v3 на .NET Framework — будет работать?**
Да, кроме `$batch` (его в v3 BPMSoft нет). При попытке `bpm_batch_*` на v3 получите явную ошибку, а не молчаливое 404.
**Как запустить кастомный бизнес-процесс?**
`bpm_run_process` с `process_name` (имя схемы процесса) и `parameters`. Если процесс возвращает результат — добавьте `result_parameter_name`. Сервер заберёт XML-обёртку и распакует JSON-payload в `structuredContent`.
**Что если на инстансе нет коллекции `SocialMessage`?**
`bpm_post_feed` вернёт ошибку 404 с понятным сообщением «На этом инстансе нет коллекции SocialMessage; функция ленты не настроена». Лента в BPMSoft может быть выключена настройками безопасности.
---
## Архитектура (для разработчиков)
```
src/
client/ HttpClient (binary-aware, contentKind, SSRF, 429+Retry-After)
ODataClient (v3/v4, $batch, nextLink, бинарные поля)
process/ ProcessEngineClient (XML envelope parsing)
metadata/ MetadataManager (fast-xml-parser, caption maps, suggestions)
lookup/ LookupResolver (caption-aware, LRU, fuzzy fallback)
utils/ errors, odata, suggest, filter-compiler, render, cursor
prompts/ registry + register
resources/ 4 resource templates
tools/ init, read, write, schema, describe-instance, enum,
workflow-catalog, batch, stream, process
workflows/ register-contact, log-activity, set-status, search-unified
tests/ 95 тестов (vitest + MSW): юнит + интеграция HTTP
```
Подробнее — в [`CLAUDE.md`](CLAUDE.md) (для разработчиков, добавляющих новые tool-ы)
---
## Лицензия
MIT — максимально свободные условия из стандартных OSS-лицензий. Использовать, изменять, распространять и встраивать в коммерческие продукты можно без ограничений; единственное требование — сохранить уведомление об авторских правах в копиях.
**Дополнительно (не юридически обязательно):** если планируете существенное переиспользование, интеграцию в коммерческий продукт или редистрибуцию под собственным брендом — автор будет признателен за короткое сообщение в GitHub: [@Catter58](https://github.com/Catter58). Это не требование лицензии, а просьба «дайте знать, чтобы я мог помочь и был в курсе».
Полный текст — в [`LICENSE`](LICENSE).
TDQS
A3.6/5.0
Scored across 32 tools
Disambiguation4/5
Tools have distinct purposes, but some overlap exists between bpm_get_records, bpm_search_records, and bpm_search_unified. However, descriptions clarify differences.
Naming Consistency5/5
All tools use a consistent bpm_verb_noun pattern in snake_case, making it easy to predict tool names.
Tool Count3/5
32 tools is high, but the server covers a complex domain (BPMSoft). While slightly above ideal, each tool serves a distinct purpose.
Completeness4/5
The tool set covers CRUD, batch, search, file handling, processes, and more. Minor gaps like lacking process management tools, but overall comprehensive.
Maintenance
ActivityActive
ResponsivenessNo issues