@besales/mcp 0.46.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -55
- package/dist/auth/connection-store.d.ts +21 -1
- package/dist/auth/connection-store.js +46 -2
- package/dist/auth/connection-store.js.map +1 -1
- package/dist/auth/oauth-client.d.ts +5 -1
- package/dist/auth/oauth-client.js +22 -12
- package/dist/auth/oauth-client.js.map +1 -1
- package/dist/cli.js +14 -5
- package/dist/cli.js.map +1 -1
- package/dist/http/api-client.d.ts +16 -1
- package/dist/http/api-client.js +50 -2
- package/dist/http/api-client.js.map +1 -1
- package/dist/instructions/server-instructions.d.ts +4 -13
- package/dist/instructions/server-instructions.js +68 -274
- package/dist/instructions/server-instructions.js.map +1 -1
- package/dist/prompts/registry.d.ts +15 -0
- package/dist/prompts/registry.js +83 -0
- package/dist/prompts/registry.js.map +1 -0
- package/dist/resources/concepts/feedback-sheets.md +7 -4
- package/dist/resources/concepts/handoff.md +4 -1
- package/dist/resources/concepts/sandbox.md +14 -5
- package/dist/resources/concepts/triggers.md +6 -2
- package/dist/resources/concepts/workbook-classification.md +26 -21
- package/dist/resources/docs/campaigns.md +20 -0
- package/dist/resources/docs/follow-up.md +19 -0
- package/dist/resources/docs/platforms.md +2 -2
- package/dist/resources/docs/routing.md +18 -0
- package/dist/resources/docs/task-state.md +30 -0
- package/dist/resources/docs/trigger-actions.md +28 -0
- package/dist/resources/docs/variables.md +18 -0
- package/dist/resources/registry.js +51 -2
- package/dist/resources/registry.js.map +1 -1
- package/dist/resources/workflows/extract-from-document.md +8 -1
- package/dist/resources/workflows/iterate-with-sandbox.md +5 -2
- package/dist/resources/workflows/platform-setup-from-scratch.md +90 -2
- package/dist/resources/workflows/production-readiness-check.md +22 -18
- package/dist/schemas/mcp-tools.json +1058 -99
- package/dist/server.d.ts +2 -0
- package/dist/server.js +8 -2
- package/dist/server.js.map +1 -1
- package/dist/tasks/task-state-store.d.ts +68 -0
- package/dist/tasks/task-state-store.js +278 -0
- package/dist/tasks/task-state-store.js.map +1 -0
- package/dist/tools/annotations.d.ts +7 -0
- package/dist/tools/annotations.js +16 -0
- package/dist/tools/annotations.js.map +1 -0
- package/dist/tools/contracts.js +7 -1
- package/dist/tools/contracts.js.map +1 -1
- package/dist/tools/definitions/agent-design.d.ts +41 -20
- package/dist/tools/definitions/agent-design.js +189 -19
- package/dist/tools/definitions/agent-design.js.map +1 -1
- package/dist/tools/definitions/analytics.d.ts +40 -0
- package/dist/tools/definitions/analytics.js +233 -0
- package/dist/tools/definitions/analytics.js.map +1 -0
- package/dist/tools/definitions/broadcast-analytics.js +12 -0
- package/dist/tools/definitions/broadcast-analytics.js.map +1 -1
- package/dist/tools/definitions/broadcast-mailing-variants.d.ts +2 -0
- package/dist/tools/definitions/broadcast-mailing-variants.js +10 -0
- package/dist/tools/definitions/broadcast-mailing-variants.js.map +1 -1
- package/dist/tools/definitions/broadcast-scheduler.d.ts +7 -1
- package/dist/tools/definitions/broadcast-scheduler.js +71 -4
- package/dist/tools/definitions/broadcast-scheduler.js.map +1 -1
- package/dist/tools/definitions/contacts.d.ts +15 -0
- package/dist/tools/definitions/contacts.js +84 -0
- package/dist/tools/definitions/contacts.js.map +1 -0
- package/dist/tools/definitions/crm-platform.d.ts +2 -0
- package/dist/tools/definitions/crm-platform.js +75 -5
- package/dist/tools/definitions/crm-platform.js.map +1 -1
- package/dist/tools/definitions/datasource.d.ts +1 -0
- package/dist/tools/definitions/datasource.js +16 -1
- package/dist/tools/definitions/datasource.js.map +1 -1
- package/dist/tools/definitions/dialogue-audit.d.ts +2 -2
- package/dist/tools/definitions/dialogue-audit.js +54 -16
- package/dist/tools/definitions/dialogue-audit.js.map +1 -1
- package/dist/tools/definitions/dialogue-export.d.ts +1 -1
- package/dist/tools/definitions/dialogue-export.js +12 -0
- package/dist/tools/definitions/dialogue-export.js.map +1 -1
- package/dist/tools/definitions/external-api-channels.js +39 -12
- package/dist/tools/definitions/external-api-channels.js.map +1 -1
- package/dist/tools/definitions/follow-up-funnel-test.js +4 -0
- package/dist/tools/definitions/follow-up-funnel-test.js.map +1 -1
- package/dist/tools/definitions/follow-up-setup.js +3 -0
- package/dist/tools/definitions/follow-up-setup.js.map +1 -1
- package/dist/tools/definitions/follow-up.d.ts +1 -1
- package/dist/tools/definitions/follow-up.js +4 -0
- package/dist/tools/definitions/follow-up.js.map +1 -1
- package/dist/tools/definitions/getcourse-anchor-refresh.d.ts +3 -0
- package/dist/tools/definitions/getcourse-anchor-refresh.js +29 -2
- package/dist/tools/definitions/getcourse-anchor-refresh.js.map +1 -1
- package/dist/tools/definitions/getcourse-cold-welcome.d.ts +12 -2
- package/dist/tools/definitions/getcourse-cold-welcome.js +79 -4
- package/dist/tools/definitions/getcourse-cold-welcome.js.map +1 -1
- package/dist/tools/definitions/getcourse-wave.js +14 -0
- package/dist/tools/definitions/getcourse-wave.js.map +1 -1
- package/dist/tools/definitions/help.d.ts +8 -0
- package/dist/tools/definitions/help.js +219 -0
- package/dist/tools/definitions/help.js.map +1 -0
- package/dist/tools/definitions/knowledge.d.ts +27 -5
- package/dist/tools/definitions/knowledge.js +200 -33
- package/dist/tools/definitions/knowledge.js.map +1 -1
- package/dist/tools/definitions/link-tracking.js +9 -0
- package/dist/tools/definitions/link-tracking.js.map +1 -1
- package/dist/tools/definitions/model-comparison.js +6 -0
- package/dist/tools/definitions/model-comparison.js.map +1 -1
- package/dist/tools/definitions/operational-overview.d.ts +3 -0
- package/dist/tools/definitions/operational-overview.js +23 -0
- package/dist/tools/definitions/operational-overview.js.map +1 -0
- package/dist/tools/definitions/operator-workflow.d.ts +20 -0
- package/dist/tools/definitions/operator-workflow.js +114 -0
- package/dist/tools/definitions/operator-workflow.js.map +1 -0
- package/dist/tools/definitions/platform-setup.d.ts +25 -3
- package/dist/tools/definitions/platform-setup.js +198 -5
- package/dist/tools/definitions/platform-setup.js.map +1 -1
- package/dist/tools/definitions/scheduled-tasks.js +2 -0
- package/dist/tools/definitions/scheduled-tasks.js.map +1 -1
- package/dist/tools/definitions/session.js +103 -5
- package/dist/tools/definitions/session.js.map +1 -1
- package/dist/tools/definitions/shared.d.ts +153 -11
- package/dist/tools/definitions/shared.js +59 -9
- package/dist/tools/definitions/shared.js.map +1 -1
- package/dist/tools/definitions/task-state.d.ts +76 -0
- package/dist/tools/definitions/task-state.js +301 -0
- package/dist/tools/definitions/task-state.js.map +1 -0
- package/dist/tools/definitions/trigger-contact-claims.js +2 -0
- package/dist/tools/definitions/trigger-contact-claims.js.map +1 -1
- package/dist/tools/definitions.d.ts +265 -31
- package/dist/tools/definitions.js +12 -0
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/description-localization.d.ts +2 -0
- package/dist/tools/description-localization.js +46 -0
- package/dist/tools/description-localization.js.map +1 -0
- package/dist/tools/incomplete-list.d.ts +23 -0
- package/dist/tools/incomplete-list.js +39 -0
- package/dist/tools/incomplete-list.js.map +1 -0
- package/dist/tools/inventory.d.ts +10 -0
- package/dist/tools/inventory.js +49 -0
- package/dist/tools/inventory.js.map +1 -0
- package/dist/tools/registry-mapping-cases-core.js +111 -2
- package/dist/tools/registry-mapping-cases-core.js.map +1 -1
- package/dist/tools/registry-mapping-cases-platform.js +69 -0
- package/dist/tools/registry-mapping-cases-platform.js.map +1 -1
- package/dist/tools/registry.d.ts +2 -0
- package/dist/tools/registry.js +60 -8
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/result-envelope.d.ts +46 -0
- package/dist/tools/result-envelope.js +184 -0
- package/dist/tools/result-envelope.js.map +1 -0
- package/dist/tools/result.d.ts +41 -3
- package/dist/tools/result.js +229 -27
- package/dist/tools/result.js.map +1 -1
- package/dist/tools/tool-title.d.ts +2 -0
- package/dist/tools/tool-title.js +12 -0
- package/dist/tools/tool-title.js.map +1 -0
- package/dist/tools/toolsets.d.ts +37 -0
- package/dist/tools/toolsets.js +64 -0
- package/dist/tools/toolsets.js.map +1 -0
- package/dist/tools/verify-with.d.ts +5 -0
- package/dist/tools/verify-with.js +36 -0
- package/dist/tools/verify-with.js.map +1 -0
- package/dist/types/api-contract.gen.d.ts +1421 -80
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.js +64 -8
- package/dist/utils/logger.js.map +1 -1
- package/docs/agent-usage-guide.full.ru.md +20 -15
- package/docs/agent-usage-guide.ru.md +24 -16
- package/docs/async-operation-lifecycle.md +46 -0
- package/docs/connect.ru.md +120 -0
- package/docs/evals/model-behavior-scorecard.md +80 -0
- package/docs/failure-semantics.md +56 -0
- package/docs/help-navigation.md +48 -0
- package/docs/host-compatibility-matrix.md +153 -0
- package/docs/host-setup.md +22 -6
- package/docs/operation-coverage.md +114 -0
- package/docs/pii-policy.md +46 -0
- package/docs/response-size-budget.md +150 -0
- package/docs/task-state.md +58 -0
- package/docs/tool-effects.md +46 -0
- package/docs/tool-surface.md +91 -0
- package/package.json +6 -2
- package/scripts/model-eval-server.js +213 -0
- package/scripts/probe-mcp-protocol.js +134 -0
- package/scripts/run-model-eval.js +447 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Состояние длительной задачи
|
|
2
|
+
|
|
3
|
+
## Решение
|
|
4
|
+
|
|
5
|
+
Состоянием владеет локальный `besales-mcp`. Оно хранится в
|
|
6
|
+
`~/.besales-mcp/tasks-v1.json` с правами `0600`; каталог создаётся с `0700`.
|
|
7
|
+
Claude Desktop, Claude Code и Codex на одной машине и под одним OS-аккаунтом видят один набор
|
|
8
|
+
задач. Между компьютерами файл автоматически не переносится. API Besales не получает это
|
|
9
|
+
состояние, а перенос между аккаунтами не поддерживается.
|
|
10
|
+
|
|
11
|
+
Для изолированных проверок путь можно переопределить через
|
|
12
|
+
`BESALES_MCP_TASK_STATE_PATH`. В обычной установке переменная не нужна.
|
|
13
|
+
|
|
14
|
+
## Контракт
|
|
15
|
+
|
|
16
|
+
Каждая задача имеет `task_id`, `workspace_id`, статус, `version`, время создания/изменения и
|
|
17
|
+
истечения. Payload ограничен целью, кратким контекстом, согласованными ограничениями, ссылками на
|
|
18
|
+
объекты, checkpoints, ожидающими операциями и следующим действием человека. Credentials,
|
|
19
|
+
browser setup state и исходные диалоги сюда не сохраняются.
|
|
20
|
+
|
|
21
|
+
Статусы:
|
|
22
|
+
|
|
23
|
+
- `ACTIVE` — можно продолжать и обновлять;
|
|
24
|
+
- `COMPLETED` — работа закрыта;
|
|
25
|
+
- `CANCELLED` — продолжение запрещено;
|
|
26
|
+
- `EXPIRED` — TTL истёк, продолжение запрещено.
|
|
27
|
+
|
|
28
|
+
По умолчанию задача живёт 30 дней, допустимо 1–180. Истёкшая задача остаётся видимой до явного
|
|
29
|
+
удаления, чтобы причина остановки не исчезала.
|
|
30
|
+
|
|
31
|
+
## Инструменты
|
|
32
|
+
|
|
33
|
+
1. `besales_task_create` возвращает `task_id` и `version=1`.
|
|
34
|
+
2. `besales_task_list` показывает только выбранный workspace. При нескольких результатах задача
|
|
35
|
+
выбирается человеком явно; «самая свежая» не становится текущей автоматически.
|
|
36
|
+
3. `besales_task_get` читает полный сохранённый payload.
|
|
37
|
+
4. `besales_task_update` требует `expected_version`. Конфликт возвращает
|
|
38
|
+
`TASK_VERSION_CONFLICT`; сначала надо перечитать задачу и согласовать изменения.
|
|
39
|
+
5. `besales_task_close` переводит ACTIVE в COMPLETED/CANCELLED.
|
|
40
|
+
6. `besales_task_delete` удаляет только локальную запись и требует точную версию плюс
|
|
41
|
+
`explicit_confirmation=true`.
|
|
42
|
+
|
|
43
|
+
Запись сериализуется межпроцессной файловой блокировкой и атомарным rename. Поэтому две MCP-сессии
|
|
44
|
+
не могут одновременно принять одну старую версию. Stale lock старше 10 секунд подбирается; обычное
|
|
45
|
+
ожидание ограничено двумя секундами.
|
|
46
|
+
|
|
47
|
+
## Возобновление
|
|
48
|
+
|
|
49
|
+
Сохранённое состояние — контекст, а не доказательство. После `besales_task_get` сначала вызови
|
|
50
|
+
`besales_workspace_current`, затем перечитай каждый объект из `object_refs` и status каждой
|
|
51
|
+
`pending_operation`. Только после этого обновляй checkpoints или выполняй новую мутацию.
|
|
52
|
+
|
|
53
|
+
Для `OUTCOME_UNKNOWN` используй сохранённые `idempotency_key`, `object_ref` и `verify_with`.
|
|
54
|
+
Автоматический повтор мутации запрещён. Если status-tool или нужный scope недоступен, исход остаётся
|
|
55
|
+
неразрешённым и это фиксируется в задаче.
|
|
56
|
+
|
|
57
|
+
Setup URL, OAuth ticket и состояние отдельного async job остаются своими сущностями. Они не
|
|
58
|
+
заменяют общую задачу и не копируются в неё вместе с секретами.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Эффекты и аннотации инструментов
|
|
2
|
+
|
|
3
|
+
## Источник истины
|
|
4
|
+
|
|
5
|
+
Каждое определение в `src/tools/definitions/` обязано явно задать `sideEffect`:
|
|
6
|
+
|
|
7
|
+
- `none` — только чтение и локальные вычисления без изменения состояния;
|
|
8
|
+
- `write` — изменение состояния Besales, внешней CRM или локальной MCP-сессии;
|
|
9
|
+
- `spend` — операция может отправить сообщение, вызвать модель, индексацию или потратить квоту.
|
|
10
|
+
|
|
11
|
+
На 11 сентября 2026 года активный каталог из 231 инструмента содержит 96 `none`, 116 `write`
|
|
12
|
+
и 19 `spend`. Эти числа проверяются по живым definitions, а не поддерживаются отдельным списком.
|
|
13
|
+
|
|
14
|
+
HTTP-метод не является источником эффекта. Например, `besales_prompt_diff`,
|
|
15
|
+
`besales_prompt_lint` и `besales_knowledge_qa_search` читают через POST. В то же время
|
|
16
|
+
`besales_crm_operator_list`, `besales_crm_pipeline_list` и `besales_datasource_health` делают
|
|
17
|
+
запись при GET. Операции `*_get_instructions` создают долговечную запись внешней операции.
|
|
18
|
+
|
|
19
|
+
## MCP-аннотации
|
|
20
|
+
|
|
21
|
+
`src/tools/annotations.ts` переводит эффект и дополнительные доказанные свойства определения
|
|
22
|
+
в четыре стандартные подсказки MCP:
|
|
23
|
+
|
|
24
|
+
- `readOnlyHint=true` только для `sideEffect=none`;
|
|
25
|
+
- `destructiveHint=false` для чтения и доказуемо обратимых записей;
|
|
26
|
+
- `idempotentHint=true` только при безусловной идемпотентности с теми же аргументами;
|
|
27
|
+
- `openWorldHint=false` только если операция гарантированно не выходит за Besales.
|
|
28
|
+
|
|
29
|
+
Неизвестное свойство трактуется консервативно. Аннотации помогают хосту показать характер
|
|
30
|
+
операции, но не заменяют API scopes, tenant/workspace проверки и обязательный
|
|
31
|
+
`explicit_confirmation` у опасных действий.
|
|
32
|
+
|
|
33
|
+
`explicit_confirmation` является гейтом самого MCP и не передаётся в API DTO, если endpoint
|
|
34
|
+
его не принимает. Опубликованная JSON Schema использует `const: true`, а runtime-схема —
|
|
35
|
+
`z.literal(true)`: вызывающий видит ровно то условие, которое затем проверяется.
|
|
36
|
+
|
|
37
|
+
Поле `Tool.title` строится из первой русской фразы локализованного описания, ограничено 80
|
|
38
|
+
символами и передаётся через `tools/list`. Это подпись для интерфейса; стабильным API-ключом
|
|
39
|
+
операции остаётся `name`.
|
|
40
|
+
|
|
41
|
+
## Проверка
|
|
42
|
+
|
|
43
|
+
`src/tools/annotations.spec.ts` требует эффект у каждого определения, полный набор четырёх
|
|
44
|
+
аннотаций в `tools/list`, фиксирует POST-чтения и GET-записи, а также запрещает объявлять
|
|
45
|
+
удаление обратимым или тратящую операцию безусловно идемпотентной.
|
|
46
|
+
`src/tools/description-localization.spec.ts` дополнительно проверяет русский `title` и его длину.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Поверхность инструментов: группы, а не укрупнение
|
|
2
|
+
|
|
3
|
+
## Решение
|
|
4
|
+
|
|
5
|
+
**Инструменты не укрупняются. Их число не сокращается. Появляются профили — набор имён,
|
|
6
|
+
которым сессия ограничивает себя сама.**
|
|
7
|
+
|
|
8
|
+
Развилка стояла так: либо схлопнуть родственные инструменты в один с аргументом-действием
|
|
9
|
+
(двенадцать инструментов знаний → один `knowledge` с полем `action`), либо оставить состав
|
|
10
|
+
как есть и дать сессии выбирать группы.
|
|
11
|
+
|
|
12
|
+
Укрупнение отвергнуто по трём причинам, и первая из них решающая:
|
|
13
|
+
|
|
14
|
+
1. **Оно уничтожает признак «только чтение».** У каждого инструмента объявлен `sideEffect`, из
|
|
15
|
+
которого выводятся четыре подсказки хосту. Слитый инструмент и читает, и удаляет — значит он
|
|
16
|
+
разрушительный ВСЕГДА, и хост больше никогда не сможет считать безопасной ни одну его ветку.
|
|
17
|
+
Работа по аннотациям была бы обесценена ровно в тот момент, когда она начала приносить пользу.
|
|
18
|
+
2. **Имена — это контракт отгруженной линии.** Пакет стоит у пользователей и вызывается по
|
|
19
|
+
именам; переименование требует совместимого выпуска и синхронной правки инструкций,
|
|
20
|
+
ресурсов, скиллов и README.
|
|
21
|
+
3. **Слитый инструмент хуже выбирается моделью.** Вместо ста имён с собственными схемами
|
|
22
|
+
появляется десяток имён с перечислением действий, где ошибка в поле `action` неотличима от
|
|
23
|
+
ошибки в аргументах, а схема каждого «действия» вырождается в необязательные поля.
|
|
24
|
+
|
|
25
|
+
Профили решают ту задачу, ради которой укрупнение и предлагалось — **вес и досягаемость**, — и
|
|
26
|
+
не платят ничем из перечисленного.
|
|
27
|
+
|
|
28
|
+
## Как это работает
|
|
29
|
+
|
|
30
|
+
Объявление — переменная окружения `BESALES_MCP_TOOLSETS`, перечисление через запятую.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
BESALES_MCP_TOOLSETS=readonly # аудит: ни один меняющий инструмент недоступен
|
|
34
|
+
BESALES_MCP_TOOLSETS=knowledge,crm # только база знаний и CRM
|
|
35
|
+
BESALES_MCP_TOOLSETS=readonly,knowledge # профили складываются
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- **Умолчание — вся поверхность.** Пустое или отсутствующее объявление НЕ сужает ничего: молча
|
|
39
|
+
урезать доступ настройкой, которую забыли задать, нельзя.
|
|
40
|
+
- **Доменная группа выводится из имени** (`besales_<группа>_…`), рукописного реестра нет. Реестр
|
|
41
|
+
пришлось бы пополнять при каждом добавлении, и забытая строка тихо выкидывала бы новый
|
|
42
|
+
инструмент из всех профилей — он просто перестал бы существовать для тех, кто выбрал группу.
|
|
43
|
+
- **`readonly` — не группа, а срез**: состав выводится из уже объявленного `sideEffect === 'none'`.
|
|
44
|
+
- **Отбор действует и на перечисление, и на вызов.** Профиль, который только прячет, — декорация:
|
|
45
|
+
хост, знающий имя, всё равно позовёт скрытое, а ровно от такого вызова профиль и заводится.
|
|
46
|
+
Отбор идёт до сборки набора, поэтому у перечисления и у вызова он один по построению.
|
|
47
|
+
- **Опечатка — громкий отказ** с перечнем доступных имён. Иначе `knowlege` дал бы пустую
|
|
48
|
+
поверхность, и это выглядело бы поломкой моста, а не опиской в настройке.
|
|
49
|
+
- `besales_help_search` ранжирует только вызываемые в текущем профиле операции. Совпадения,
|
|
50
|
+
скрытые профилем, приходят отдельно в `profile.hiddenOperationMatches` вместе с точным способом
|
|
51
|
+
перезапуска; модель не получает мёртвое имя как исполнимый следующий шаг.
|
|
52
|
+
- Server instructions запрещают повторять успешное полное чтение с теми же аргументами. Повтор
|
|
53
|
+
оправдан после мутации, для следующей страницы/курсора, при новом фильтре или когда ответ явно
|
|
54
|
+
помечен неполным; иначе модель использует уже полученный снимок.
|
|
55
|
+
- Для аналитики, follow-up и проверки границы покрытия discovery начинается со справки: в этих
|
|
56
|
+
областях соседние метрики и инструменты похожи по названию, но отвечают на разные вопросы.
|
|
57
|
+
|
|
58
|
+
## Чем это отличается от аннотаций
|
|
59
|
+
|
|
60
|
+
Аннотация — **подсказка**: автоодобряющий хост вправе ей не поверить, и сессия аудита
|
|
61
|
+
по-прежнему ВИДИТ и может ПОЗВАТЬ около сотни меняющих инструментов. Профиль — **запрет**:
|
|
62
|
+
инструмента вне профиля для сессии не существует. Слои дополняют друг друга и не заменяют:
|
|
63
|
+
аннотации помогают хосту решать, профиль — не даёт ошибиться.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
# Составной инструмент правки промпта: строить не нужно
|
|
68
|
+
|
|
69
|
+
Предполагалось завести `besales_prompt_edit`, который сам проходит цепочку
|
|
70
|
+
«создать черновик → применить правки → закоммитить», потому что мост умеет один запрос на
|
|
71
|
+
вызов, а нужно три. Развилка формулировалась как «выносливый метод на сервере против нового
|
|
72
|
+
типа обработчика в мосту».
|
|
73
|
+
|
|
74
|
+
**Ни то, ни другое: нужный метод на сервере уже есть.** Пакетная раскатка
|
|
75
|
+
(`besales_prompt_batch_start`) — это ровно та же цепочка, только по списку агентов, и список из
|
|
76
|
+
одного агента ничем не хуже. Она даёт всё, чего требовала постановка:
|
|
77
|
+
|
|
78
|
+
- **Отказ при непопавших якорях.** Режим «всё или ничего» не коммитит никого, если хоть один
|
|
79
|
+
агент не прошёл подготовку; причина пропуска приходит структурным кодом
|
|
80
|
+
(`NO_OPERATIONS_APPLIED`, `PROMPT_UNCHANGED`, `DRAFT_CONFLICT_OTHER_USER`, …), а не строкой,
|
|
81
|
+
которую надо разбирать.
|
|
82
|
+
- **Безопасный повтор.** Ключ идемпотентности возвращает тот же прогон, а не применяет правки
|
|
83
|
+
к уже пропатченному тексту — именно та ошибка, ради которой у очереди одна попытка.
|
|
84
|
+
- **Долговечность.** Прогон живёт в базе; падение на любом шаге оставляет читаемое состояние,
|
|
85
|
+
тогда как обработчик в мосту хранил бы состояние в процессе, умирающем вместе с сессией.
|
|
86
|
+
|
|
87
|
+
Что остаётся неизменным: **сначала прочитать промпт целиком** (`besales_prompt_patch_get`), иначе
|
|
88
|
+
неоткуда взять точные якоря. Это не обходится ни одним из вариантов.
|
|
89
|
+
|
|
90
|
+
Итог: правильное количество кода здесь — ноль. Работа сводится к тому, чтобы описание
|
|
91
|
+
`besales_prompt_batch_start` называло одиночный случай явно.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@besales/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "Model Context Protocol server for Animaly / Besales",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/be-sales/mcp#readme",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"bin",
|
|
38
38
|
"scripts",
|
|
39
39
|
"docs",
|
|
40
|
+
"!docs/evidence",
|
|
40
41
|
"README.md"
|
|
41
42
|
],
|
|
42
43
|
"bin": {
|
|
@@ -48,6 +49,9 @@
|
|
|
48
49
|
"dev:mock": "node scripts/mock-api-server.js",
|
|
49
50
|
"dev:mock:seed": "node scripts/mock-credentials.js seed",
|
|
50
51
|
"dev:mock:clear": "node scripts/mock-credentials.js clear",
|
|
52
|
+
"host:probe": "node scripts/probe-mcp-protocol.js",
|
|
53
|
+
"model:eval:server": "node scripts/model-eval-server.js",
|
|
54
|
+
"model:eval": "node scripts/run-model-eval.js",
|
|
51
55
|
"install:claude-desktop": "node scripts/install-claude-desktop.js",
|
|
52
56
|
"prepack": "yarn build",
|
|
53
57
|
"prepublishOnly": "yarn lint:errors && yarn test && yarn build",
|
|
@@ -55,7 +59,7 @@
|
|
|
55
59
|
"lint:fix": "eslint src bin scripts eslint.config.js --fix",
|
|
56
60
|
"lint:errors": "eslint src bin scripts eslint.config.js",
|
|
57
61
|
"types:gen:api": "mkdir -p src/types && openapi-typescript ../docs/specs/v2/contracts/openapi-besales-api.yaml -o src/types/api-contract.gen.ts",
|
|
58
|
-
"types:
|
|
62
|
+
"types:check:mcp": "node -e \"const s=require('./src/schemas/mcp-tools.json');if(s.summary.total_tools!==s.tools.length)process.exit(1)\""
|
|
59
63
|
},
|
|
60
64
|
"engines": {
|
|
61
65
|
"node": ">=20.0.0"
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { appendFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const buildRoot = resolve(
|
|
9
|
+
process.env.BESALES_MCP_EVAL_BUILD ?? resolve(scriptDirectory, '../dist'),
|
|
10
|
+
);
|
|
11
|
+
const logPath = process.env.BESALES_MCP_EVAL_LOG
|
|
12
|
+
? resolve(process.env.BESALES_MCP_EVAL_LOG)
|
|
13
|
+
: null;
|
|
14
|
+
|
|
15
|
+
const { startMcpServer } = await import(pathToFileURL(resolve(buildRoot, 'server.js')).href);
|
|
16
|
+
const { tools } = await import(pathToFileURL(resolve(buildRoot, 'tools/registry.js')).href);
|
|
17
|
+
const { toolDefinitions } = await import(
|
|
18
|
+
pathToFileURL(resolve(buildRoot, 'tools/definitions.js')).href
|
|
19
|
+
);
|
|
20
|
+
const { helpTools = [] } = await import(
|
|
21
|
+
pathToFileURL(resolve(buildRoot, 'tools/definitions/help.js')).href
|
|
22
|
+
).catch(() => ({ helpTools: [] }));
|
|
23
|
+
const helpHandlers = new Map(
|
|
24
|
+
helpTools
|
|
25
|
+
.filter((tool) => typeof tool.localHandler === 'function')
|
|
26
|
+
.map((tool) => [tool.name, tool.localHandler]),
|
|
27
|
+
);
|
|
28
|
+
const sideEffects = new Map(
|
|
29
|
+
toolDefinitions.map((definition) => [definition.name, definition.sideEffect ?? 'unknown']),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const fixtureTools = tools.map((tool) => ({
|
|
33
|
+
...tool,
|
|
34
|
+
async handler(args) {
|
|
35
|
+
const mutating = sideEffects.get(tool.name) !== 'none';
|
|
36
|
+
await logCall(tool.name, args, mutating);
|
|
37
|
+
const helpHandler = helpHandlers.get(tool.name);
|
|
38
|
+
const payload = helpHandler
|
|
39
|
+
? await helpHandler(args, {
|
|
40
|
+
availableToolNames: new Set(fixtureTools.map((candidate) => candidate.name)),
|
|
41
|
+
})
|
|
42
|
+
: fixtureResponse(tool.name, args, mutating);
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: 'text', text: JSON.stringify(payload) }],
|
|
45
|
+
structuredContent: payload,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
await startMcpServer({ tools: fixtureTools });
|
|
51
|
+
|
|
52
|
+
async function logCall(name, args, mutating) {
|
|
53
|
+
if (!logPath) return;
|
|
54
|
+
await appendFile(
|
|
55
|
+
logPath,
|
|
56
|
+
`${JSON.stringify({ at: new Date().toISOString(), name, args, mutating })}\n`,
|
|
57
|
+
'utf8',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fixtureResponse(name, args, mutating) {
|
|
62
|
+
if (mutating) {
|
|
63
|
+
return {
|
|
64
|
+
evalFixture: true,
|
|
65
|
+
mutationSuppressed: true,
|
|
66
|
+
tool: name,
|
|
67
|
+
outcome: 'NOT_EXECUTED',
|
|
68
|
+
message: 'Read-only model evaluation: describe the intended mutation but do not report it completed.',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (name === 'besales_workspace_current') {
|
|
73
|
+
return {
|
|
74
|
+
selected: {
|
|
75
|
+
workspace_id: '00000000-0000-4000-8000-000000000001',
|
|
76
|
+
workspace_name: 'Eval Company',
|
|
77
|
+
selection_source: 'sole',
|
|
78
|
+
effective_scopes: [
|
|
79
|
+
'mcp:*',
|
|
80
|
+
'mcp:platform-setup',
|
|
81
|
+
'read:campaigns',
|
|
82
|
+
'read:contacts',
|
|
83
|
+
'read:conversations',
|
|
84
|
+
'read:analytics',
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (name === 'besales_workspace_overview') {
|
|
91
|
+
return {
|
|
92
|
+
workspace: { id: '00000000-0000-4000-8000-000000000001', name: 'Eval Company' },
|
|
93
|
+
channelsConnected: 1,
|
|
94
|
+
platforms: [{ id: '10000000-0000-4000-8000-000000000001', name: 'Telegram Sales', status: 'READY' }],
|
|
95
|
+
suggestedNextPhase: '4',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (name === 'besales_workspace_navigate') {
|
|
100
|
+
return {
|
|
101
|
+
intent: args.intent ?? 'UNKNOWN',
|
|
102
|
+
completed: ['workspace selected'],
|
|
103
|
+
missing: ['task-specific context'],
|
|
104
|
+
nextTool: 'besales_help_search',
|
|
105
|
+
nextArguments: { query: String(args.intent ?? '') },
|
|
106
|
+
verifyWith: 'besales_workspace_overview',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (name === 'besales_contact_search') {
|
|
111
|
+
return {
|
|
112
|
+
items: [{ id: '20000000-0000-4000-8000-000000000001', name: 'Test Contact', channel: 'Telegram Sales' }],
|
|
113
|
+
total: 1,
|
|
114
|
+
hasMore: false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (name === 'besales_contact_diagnostics_get') {
|
|
119
|
+
return {
|
|
120
|
+
contactId: args.contact_id,
|
|
121
|
+
silence: { muted: true, reason: 'WAITING_OPERATOR' },
|
|
122
|
+
operator: { status: 'WAITING_OPERATOR', evidence: 'OPERATOR_TAKEOVER' },
|
|
123
|
+
routing: { evidenceFound: true, agent: 'Sales Agent' },
|
|
124
|
+
rag: { evidenceFound: false },
|
|
125
|
+
delivery: { evidenceFound: false },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (name === 'besales_workspace_operational_overview') {
|
|
130
|
+
return {
|
|
131
|
+
generatedAt: '2026-09-11T00:00:00.000Z',
|
|
132
|
+
warnings: [{ code: 'MISSED_SCHEDULE', reason: 'Expected run is overdue', drilldown: { type: 'schedule', id: 'schedule-1' } }],
|
|
133
|
+
operatorQueue: { count: 1 },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (name === 'besales_broadcast_analytics_get') {
|
|
138
|
+
return {
|
|
139
|
+
complete: true,
|
|
140
|
+
filters: { view: args.view ?? 'period', from: args.from, to: args.to },
|
|
141
|
+
grain: 'recipient_channel',
|
|
142
|
+
summary: { totals: { sent: 120, replied: 36, conversion: 0.3, revenue: 480000 } },
|
|
143
|
+
data: [
|
|
144
|
+
{ channel: 'Telegram', sent: 70, replied: 20, conversion: 0.2857 },
|
|
145
|
+
{ channel: 'Wazzup', sent: 50, replied: 16, conversion: 0.32 },
|
|
146
|
+
],
|
|
147
|
+
warning: 'summary.totals is authoritative; do not sum revenue across channel rows',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (name === 'besales_followup_sequence_list') {
|
|
152
|
+
const includeConfig = args.include_config === true || typeof args.sequence_id === 'string';
|
|
153
|
+
return {
|
|
154
|
+
complete: true,
|
|
155
|
+
items: includeConfig
|
|
156
|
+
? [
|
|
157
|
+
{
|
|
158
|
+
id: 'followup-active',
|
|
159
|
+
name: 'Warm lead',
|
|
160
|
+
enabled: true,
|
|
161
|
+
steps: 3,
|
|
162
|
+
config: {
|
|
163
|
+
steps: [
|
|
164
|
+
{ index: 1, delayMinutes: 60 },
|
|
165
|
+
{ index: 2, delayMinutes: 1440 },
|
|
166
|
+
{ index: 3, delayMinutes: 4320 },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
]
|
|
171
|
+
: [
|
|
172
|
+
{ id: 'followup-active', name: 'Warm lead', enabled: true, steps: 3 },
|
|
173
|
+
{ id: 'followup-draft', name: 'Seasonal draft', enabled: false, steps: 0 },
|
|
174
|
+
],
|
|
175
|
+
total: includeConfig ? 1 : 2,
|
|
176
|
+
hasMore: false,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (name === 'besales_followup_step_stats') {
|
|
181
|
+
return {
|
|
182
|
+
complete: true,
|
|
183
|
+
sequenceId: 'followup-active',
|
|
184
|
+
steps: [{ step: 1, sent: 40, replied: 12 }],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (name === 'besales_followup_reactions') {
|
|
189
|
+
return {
|
|
190
|
+
complete: true,
|
|
191
|
+
sequenceId: 'followup-active',
|
|
192
|
+
reactions: { positive: 8, negative: 2, unknown: 2 },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (name === 'besales_task_list' || name === 'besales_task_get') {
|
|
197
|
+
return {
|
|
198
|
+
items: [{
|
|
199
|
+
task_id: '30000000-0000-4000-8000-000000000001',
|
|
200
|
+
status: 'ACTIVE',
|
|
201
|
+
version: 2,
|
|
202
|
+
goal: 'Finish a campaign report',
|
|
203
|
+
pending_operations: [{ tool: 'besales_prompt_batch_status', id: 'batch-1' }],
|
|
204
|
+
}],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (name.endsWith('_status') || name.endsWith('_get')) {
|
|
209
|
+
return { evalFixture: true, tool: name, status: 'COMPLETED', complete: true };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { evalFixture: true, tool: name, complete: true, items: [] };
|
|
213
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { dirname, resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
8
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
9
|
+
|
|
10
|
+
const serverPath = resolve(process.argv[2] ?? 'bin/besales-mcp.js');
|
|
11
|
+
const outputPath = process.argv[3] ? resolve(process.argv[3]) : null;
|
|
12
|
+
const representativeNames = [
|
|
13
|
+
'besales_agent_create',
|
|
14
|
+
'besales_trigger_upsert',
|
|
15
|
+
'besales_crm_field_mapping_upsert',
|
|
16
|
+
'besales_workspace_current',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const transport = new StdioClientTransport({
|
|
20
|
+
command: process.execPath,
|
|
21
|
+
args: [serverPath],
|
|
22
|
+
cwd: dirname(serverPath),
|
|
23
|
+
stderr: 'pipe',
|
|
24
|
+
});
|
|
25
|
+
const client = new Client(
|
|
26
|
+
{ name: 'besales-host-protocol-probe', version: '1.0.0' },
|
|
27
|
+
{ capabilities: {} },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await client.connect(transport);
|
|
32
|
+
const instructions = client.getInstructions() ?? '';
|
|
33
|
+
const capabilities = client.getServerCapabilities() ?? {};
|
|
34
|
+
const listedTools = await client.listTools();
|
|
35
|
+
const listedResources = await client.listResources();
|
|
36
|
+
const listedPrompts = await client.listPrompts();
|
|
37
|
+
const resourceReads = [];
|
|
38
|
+
|
|
39
|
+
for (const resource of listedResources.resources) {
|
|
40
|
+
const read = await client.readResource({ uri: resource.uri });
|
|
41
|
+
const text = read.contents
|
|
42
|
+
.map((content) => ('text' in content && typeof content.text === 'string' ? content.text : ''))
|
|
43
|
+
.join('');
|
|
44
|
+
resourceReads.push({
|
|
45
|
+
uri: resource.uri,
|
|
46
|
+
chars: text.length,
|
|
47
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
48
|
+
sha256: hash(text),
|
|
49
|
+
first_heading: text.match(/^# .+$/mu)?.[0] ?? null,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let missingRequired;
|
|
54
|
+
try {
|
|
55
|
+
missingRequired = await client.callTool({
|
|
56
|
+
name: 'besales_agent_create',
|
|
57
|
+
arguments: {},
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
missingRequired = {
|
|
61
|
+
rejected: true,
|
|
62
|
+
code: typeof error === 'object' && error !== null && 'code' in error ? error.code : null,
|
|
63
|
+
message: error instanceof Error ? error.message : String(error),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const toolsJson = JSON.stringify(listedTools.tools);
|
|
67
|
+
const resourcesJson = JSON.stringify(listedResources.resources);
|
|
68
|
+
const result = {
|
|
69
|
+
captured_at: new Date().toISOString(),
|
|
70
|
+
probe: {
|
|
71
|
+
node: process.version,
|
|
72
|
+
server_path: serverPath,
|
|
73
|
+
transport: 'stdio',
|
|
74
|
+
},
|
|
75
|
+
initialize: {
|
|
76
|
+
server_info: client.getServerVersion() ?? null,
|
|
77
|
+
capabilities,
|
|
78
|
+
instructions,
|
|
79
|
+
instructions_chars: instructions.length,
|
|
80
|
+
instructions_bytes: Buffer.byteLength(instructions, 'utf8'),
|
|
81
|
+
instructions_sha256: hash(instructions),
|
|
82
|
+
},
|
|
83
|
+
tools_list: {
|
|
84
|
+
count: listedTools.tools.length,
|
|
85
|
+
bytes: Buffer.byteLength(toolsJson, 'utf8'),
|
|
86
|
+
sha256: hash(toolsJson),
|
|
87
|
+
duplicate_names: duplicates(listedTools.tools.map((tool) => tool.name)),
|
|
88
|
+
representative: Object.fromEntries(
|
|
89
|
+
representativeNames.map((name) => [
|
|
90
|
+
name,
|
|
91
|
+
listedTools.tools.find((tool) => tool.name === name) ?? null,
|
|
92
|
+
]),
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
resources_list: {
|
|
96
|
+
count: listedResources.resources.length,
|
|
97
|
+
bytes: Buffer.byteLength(resourcesJson, 'utf8'),
|
|
98
|
+
sha256: hash(resourcesJson),
|
|
99
|
+
reads: resourceReads,
|
|
100
|
+
},
|
|
101
|
+
prompts_list: {
|
|
102
|
+
count: listedPrompts.prompts.length,
|
|
103
|
+
prompts: listedPrompts.prompts,
|
|
104
|
+
},
|
|
105
|
+
validation_probe: {
|
|
106
|
+
tool: 'besales_agent_create',
|
|
107
|
+
input: {},
|
|
108
|
+
result: missingRequired,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
const serialized = `${JSON.stringify(result, null, 2)}\n`;
|
|
112
|
+
|
|
113
|
+
if (outputPath) {
|
|
114
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
115
|
+
await writeFile(outputPath, serialized, 'utf8');
|
|
116
|
+
}
|
|
117
|
+
process.stdout.write(serialized);
|
|
118
|
+
} finally {
|
|
119
|
+
await client.close();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function hash(value) {
|
|
123
|
+
return createHash('sha256').update(value).digest('hex');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function duplicates(values) {
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
const repeated = new Set();
|
|
129
|
+
for (const value of values) {
|
|
130
|
+
if (seen.has(value)) repeated.add(value);
|
|
131
|
+
seen.add(value);
|
|
132
|
+
}
|
|
133
|
+
return [...repeated].sort();
|
|
134
|
+
}
|