# Дублирование определений инструментов: in-app агент vs standalone MCP-пакет Статус: **зафиксировано в беклоге, код не менялся.** Это forward-looking стоимость поддержки, НЕ баг — код корректен сегодня. Фиксируем, чтобы при росте набора инструментов (см. §16) долг не разъезжался молча. ## Суть Один и тот же набор инструментов поверх одного `DocmostClient` описан **тремя независимыми рукописными слоями**. Каждое добавление инструмента или правка его model-facing описания требует синхронной правки в 2–3 местах, а parity-баги (расхождение копий) приходится чинить/переоткрывать дважды. ## Где дублируется (три слоя) 1. **Standalone MCP-сервер** — `packages/mcp/src/index.ts` (~38 `registerTool`). Для внешних MCP-клиентов (stdio/http). На каждый инструмент: zod-схема + длинное model-facing описание + тонкий `execute`, вызывающий `DocmostClient`. 2. **Встроенный AI-чат** — `apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts` (~39 `tool({...})` через `ai`-SDK). Своя zod-схема + своё описание + свой `execute` поверх ТОГО ЖЕ клиента (`@docmost/mcp` грузится в `tools/docmost-client.loader.ts:188` через динамический `import()`). 3. **Ручная копия сигнатур** — интерфейс `DocmostClientLike` в `apps/server/src/core/ai-chat/tools/docmost-client.loader.ts:9` (в комментарии прямо: «Signatures here mirror that file exactly»), скопирован руками из `packages/mcp/src/client.ts`. ## Что именно продублировано (с подтверждением по коду) - **zod-схема + описание** каждого инструмента — в слоях 1 и 2 целиком. - **Квирк «node как объект ИЛИ JSON-строка»** реализован дважды (НЕ в общем клиенте): - in-app: `ai-chat-tools.service.ts:686` (patchNode), `:745` (insertNode), `:800` (updatePageJson); - standalone: `index.ts:526` (patch_node), `:578` (insert_node), `:350` (update_page_json). - **Guardrail/семантика `transformPage` (dryRun)** описана в обоих: `ai-chat-tools.service.ts:~935` и `index.ts:~1006`. ## Почему разделение слоёв 1 и 2 само по себе оправдано У путей разный транспорт и auth-контекст, и это правильно держать раздельно: in-app путь чеканит per-user JWT + provenance collab-токен (подписанная agent-claim, `docmost-client.loader.ts:159` — `getCollabToken`; см. план §6.5), а standalone обслуживает внешних клиентов по stdio/http. **Но** это оправдывает два тонких адаптера (`execute` + auth-обвязка), а НЕ две рукописные копии МЕТАДАННЫХ (схема + описание + квирки). Метаданные можно объявить один раз и переиспользовать обоими транспортами. ## Доказательство стоимости (наблюдалось при фиксе edit_page_text) При исправлении ложного «успеха» `edit_page_text` (refuse форматных правок + `verify`-отчёт): - **Поведение** легло в общий `DocmostClient` → автоматически дошло до обоих агентов ОДНОЙ правкой. Это «хороший» случай — логика в едином источнике. - **Описание** инструмента пришлось править ДВАЖДЫ: в `index.ts` (кодером) и отдельно в `ai-chat-tools.service.ts:617`, где описание продолжало рекламировать «Markdown wrappers tolerated via strip-and-retry» — ровно ту формулировку, что ввела исходного агента в заблуждение. Копия молча разъехалась и какое-то время встроенный агент получал устаревшую подсказку. Это и есть материализованный parity-баг. ## Фикс Единый реестр спеков (полное устранение дублирования).** Вынести в `packages/mcp` один источник на инструмент: `name` + zod-схема + model-facing описание + общий хелпер нормализации node-строки (для patch/insert/update). И `index.ts`, и `ai-chat-tools.service.ts` импортируют спеки и добавляют только свой `execute`/auth. `DocmostClientLike` — выводить из типа реального клиента (type-only import / генерация), а не копировать руками. - Ограничение: `@docmost/mcp` — ESM-only, сервер грузит его через трюк `new Function('import(specifier)')` (`docmost-client.loader.ts:174`), потому что `module:commonjs` даунлевелит `import()` в `require()`. Реестр спеков (данные + zod) должен пересекать ту же ESM/CJS-границу — выполнимо тем же динамическим импортом; `ai`-SDK `tool()` и MCP `registerTool()` имеют разную форму, поэтому реестр экспортирует транспорт-агностичные `{name, schema, description}`, а каждая сторона оборачивает их сама. `zod` — общая зависимость обоих пакетов, типы переносятся.