feat(mcp): markdown — формат по умолчанию для блочных операций getNode/patchNode/insertNode (PM JSON — опция для тонких работ) #413
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Мотивация
Три сходящиеся проблемы:
patchNode/insertNodeтребуют от агента сочинять ProseMirror JSON руками — главный источник их ошибок (~34 Yjs-encode падений, разбор в #409: вложенная нода безtypeи т.п.).editPageTextпо контракту не умеет менять форматирование — его собственное описание отправляет агента в page-JSON +patchNode(packages/mcp/src/tool-specs.ts:470-491).^[...]— сноски оседают литералами.Канонический конвертер (#293/#345/#351: P1–P4, байтовый fixpoint) достаточно зрел, чтобы доверить ему блочный уровень: markdown становится форматом по умолчанию для чтения и записи одного блока, PM JSON остаётся опцией «для тонких работ». Редактировать целую страницу маркдауном неудобно (full-body случай закрывает #411); блочный уровень сохраняет хирургичность: соседние блоки не пересылаются и не трогаются вовсе — их
attrs.idцелы автоматически (главное ограничение конвертера — ids регенерируются на импорте — на блочном уровне не мешает).Новых тулов не появляется, поверхность (~42) не растёт.
Дизайн
getNode(pageId, nodeId, format?)
formatпо умолчанию'markdown': обёртка{type:'doc',content:[node]}→convertProseMirrorToMarkdown.getPage): это чтение «под редактирование», потеря<span data-comment-id>при записи назад осиротит тред.format:'json'— текущее поведение (сабтри как есть).#<index>-ссылкам) — автоматический фолбэк в JSON с явным полемformat:"json"в ответе. Проверку делать через contentMatch схемы (schema.nodes.doc.contentMatch.matchType(...)), не рукописным списком.patchNode(pageId, nodeId, {markdown | node})
markdown|node(оба optional в схеме, XOR на рантайме; описание рекомендует markdown, node — для точной работы с атрибутами/марками).attrs.idцели (как сейчас нитуется вclient.ts:2101), остальные получают свежие ids (задокументировать).node-JSON. Простые таблицы — можно.insertNode(..., {markdown | node})
Сноски на markdown-пути
^[...]во фрагменте парсится каноническим токенизатором; фрагмент приходит как «блоки + footnotesList». Определения сливаются в хвостовой список страницы существующей машинериейinsertFootnote(footnoteContentKey-дедуп +canonicalizeFootnotes,client.ts:1961) — тот же инвариант и та же задокументированная оговорка (полная канонизация дропает орфанные определения — как уinsertFootnoteсегодня). Альтернатива «отклонять^[...]во фрагменте» отвергнута: при markdown-по-умолчанию это регресс UX относительно сегодняшнего литерала.Сырые JSON-пути (
node,updatePageJson,docmostTransform) в части литеральных^[...]не трогаем — материализация там осознанно вне скоупа (решение владельца); точечный тул —insertFootnote(#410).Не трогаем
editPageText(plain-text контракт остаётся нишей «дёшево поправить опечатку, не пересылая блок»; опциональный guard на^[— в #410), table-тулы,docmostTransform,updatePageJson.Скоуп правок
packages/mcp/src/tool-specs.ts— shapes/descriptions/catalogLine трёх спек.packages/mcp/src/index.ts— SERVER_INSTRUCTIONS (READ/EDIT-роутинг: «блок правится так: getNode(markdown) → правка → patchNode(markdown); node-JSON — для тонкой работы; таблицы со спанами/цветами — table-тулы») + прокидка новых аргументов в execute;test/unit/server-instructions.test.mjs.packages/mcp/src/client.ts—getNode/patchNode/insertNode.packages/mcp/src/lib/node-ops.ts—replaceNodeByIdWithMany/insertNodesRelative(сплайс массива). Координация: #409 (pre-validate там же) и #414 (дедупликация node-ops-форка).apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts— прокидкаformat/markdownв execute (схемы приедут из SHARED_TOOL_SPECS автоматически).packages/mcp/test/*,ai-chat-tools.service.spec.ts,shared-tool-specs.contract.spec.ts.Тесты (обязательные)
patchNode(markdown), канонически равен тому же контенту, прошедшему полный markdown-импорт (docsCanonicallyEqual) — иначе появляется «второй канон».^[...]во фрагменте → сноска в хвостовом списке + перенумерация.Breaking
Смена дефолта
getNode— breaking для внешних MCP-клиентов, ожидающих JSON-ответ. Выпускать одним breaking-релизом с #411/#412 (внешние конфиги ломаются один раз), миграционная заметка в CHANGELOG.План работ
formatв getNode (default markdown), фолбэки, описания, SERVER_INSTRUCTIONS.markdownв patchNode/insertNode, сплайс 1→N, guard'ы, слияние сносок, тест сходимости.Связано
#409 (валидация JSON-пути — комплементарно: md-путь устраняет сам класс ошибок «нода без type»), #410 (инцидент сносок; insertFootnote остаётся точечным тулом), #411 (full-body
updatePageMarkdown— парный случай к блочному), #412 (camelCase: имена здесь даны уже в целевом виде), #414 (дедупликация node-ops — те же файлы), #415 (честные описания getPage/exportPageMarkdown), #293/#345/#351 (зрелость конвертера).