feat(mcp): создание и редактирование drawio-ноды из XML (get/insert/update_drawio) #398

Open
opened 2026-07-06 23:38:42 +03:00 by agent_vscode · 0 comments
Collaborator

Контекст

drawio-диаграмма в Docmost — это ProseMirror-нода drawio, ссылающаяся на вложение diagram.drawio.svg. Это editable-SVG: пиксельный рендер + исходный mxfile-XML в атрибуте content корневого <svg>. Рендер XML→SVG делает сам drawio-редактор в браузере; серверного рендерера в проекте нет.

Файлы:

  • нода: packages/editor-ext/src/lib/drawio.ts (attrs: src, attachmentId, title, width, height, size, aspectRatio, align)
  • round-trip в UI: apps/client/src/features/editor/components/drawio/drawio-menu.tsx, .../drawio-view.tsx (fetch SVG → DrawIoEmbed → export xmlsvguploadFile)
  • DRAWIO_URL (embed редактора) по умолчанию https://embed.diagrams.netapps/client/src/lib/config.ts

Проблема

В MCP нет способа создать или отредактировать drawio-диаграмму:

  • insert_image/replace_image работают только с растровыми картинками, скачиваемыми по http-URL;
  • нет способа залить произвольные байты (SVG) как вложение;
  • нет чтения встроенного XML.

Цель (скоуп)

Дать агенту создавать и редактировать drawio-ноду, оперируя mxfile-XML как источником правды. Источник XML — вне скоупа (агент пишет руками либо берёт у внешнего drawio MCP, передаётся inline-текстом).

Три тула:

  1. get_drawio — чтение (нужно для осмысленного редактирования)
  2. insert_drawio — создание новой ноды из XML
  3. update_drawio — замена XML у существующей ноды

Вне скоупа

  • Серверный рендер диаграммы (WYSIWYG). Превью — плейсхолдер-баннер до первого открытия человеком в редакторе (тогда drawio перерисует растр).
  • Оркестрация: gitmost не вызывает внешний drawio MCP. XML передаётся inline-текстом.
  • Изменения в habr-mcp.

Контракты тулзов

get_drawio(pageId, attachmentId?)

  • без attachmentId → список drawio-нод страницы: [{ index, attachmentId, title, width, height, size }] (обход getPageJson)
  • с attachmentId → скачать SVG (fetchInternalFile), распарсить jsdom-ом, прочитать атрибут content → mxfile XML. Если содержимое <diagram> сжато (base64 → raw-deflate) — распаковать (zlib.inflateRawSync + decodeURIComponent)
    • ответ: { attachmentId, fileName, size, width, height, xml }
  • если атрибута content нет → ошибка «не editable drawio SVG» (без мутаций)

insert_drawio(pageId, xml, { placement?, align? })

  • placement: append (по умолчанию) / after-text / replace-text (как у insert_image)
  • шаги:
    1. валидировать XML (<mxfile> или <mxGraphModel>; голый mxGraphModel обернуть в mxfile). Мусор → ошибка до заливки
    2. собрать diagram.drawio.svg: <svg content="<escaped mxfile>" width/height data-drawio-stale="1"> + тело-плейсхолдер-баннер
    3. залить байты через POST /files/upload (новый uploadBuffer)
    4. собрать ноду { type: "drawio", attrs: { src, attachmentId, title: "diagram.drawio.svg", size, width, height, align } } и вставить существующим insertNode / mutatePageContent (page-locked)
  • ответ: { attachmentId, placement, renderStale: true, note }

update_drawio(pageId, attachmentId, xml, { align? })

  • шаги:
    1. валидировать XML (как выше)
    2. собрать новый diagram.drawio.svg
    3. залить новым вложением (не перезаписывать байты — Docmost на overwrite отдаёт HTTP 500, см. комментарий в replaceImage)
    4. под page-lock репойнтить все ноды с attrs.attachmentId === attachmentId (общий repointAttachment): обновить src/attachmentId/size/title, сохранить type/width/height/align/aspectRatio
  • ответ: { oldAttachmentId, newAttachmentId, renderStale: true, note }

Рефакторинг / переиспользование

  • расщепить uploadImageuploadBuffer(pageId, buffer, fileName, mime) + часть fetch-по-URL (packages/mcp/src/client.ts, ~L3006)
  • вынести цикл репойнта по attachmentId из replaceImage (~L3262) в общий repointAttachment
  • SVG-хелперы: readEmbeddedXml(svgBuffer) (jsdom + inflate), buildDrawioSvg(xml, { width, height }) (баннер + встроенный content)
  • jsdom уже в зависимостях packages/mcp; zlib встроенный
  • адресация только по attachmentId (у drawio-ноды нет attrs.id — в outline такие блоки идут с id: null)

Плейсхолдер-баннер

  • тело нового SVG = рамка + короткая метка (EN): Diagram edited via agent — open in editor to re-render
  • маркер data-drawio-stale="1" на корне <svg>
  • renderStale: true в ответах тулзов
  • обоснование: сервер не рендерит; drawio перерисует растр при первом открытии+сохранении человеком. content-XML при этом всегда корректный и редактируемый

Регистрация

  • три тула инлайн в packages/mcp/src/index.ts (stdio) + зеркально в http.ts; добавить в строку MCP-гайда
  • MCP-only (как replace_image)
  • все комментарии в коде — на английском

Крайние случаи / риски

  • сжатый content при чтении — обязательный inflate-фолбэк
  • нет content → понятная ошибка, без мутаций
  • orphan старого вложения на update (Docmost не умеет удалять одиночное вложение — та же особенность, что у replace_image)
  • конкурентность: page-lock + мутация live-документа
  • лимиты размера входного XML (напр. пара МБ) + ранняя валидация до заливки
  • SSRF/traversal на чтении уже закрыт resolveInternalFilePath
  • сохранять width/height/align/aspectRatio при update

Критерии приёмки

  • get_drawio без аргумента перечисляет drawio-ноды; с attachmentId возвращает XML
  • get_drawio читает и сжатый, и несжатый content
  • get_drawio отдаёт понятную ошибку на не-editable SVG
  • insert_drawio создаёт ноду; XML открывается в редакторе drawio и показывает диаграмму
  • insert_drawio отклоняет битый XML до заливки (без orphan)
  • placement append/after/replace работает как у insert_image
  • update_drawio заменяет XML, репойнтит все ссылающиеся ноды, сохраняет width/height/align
  • ответы содержат renderStale: true; превью — баннер до открытия+сохранения человеком
  • тесты на всё перечисленное

На будущее (отдельные задачи)

  • серверный рендер превью (WYSIWYG) — заменить баннер на реальный растр; контракты тулзов не меняются
  • create_drawio(pageId, description) — оркестрация: gitmost как MCP-клиент внешнего drawio MCP, чтобы большой XML не шёл через контекст агента
  • drawio-осведомлённость в habr-mcp (экспорт превью drawio-ноды)
## Контекст drawio-диаграмма в Docmost — это ProseMirror-нода `drawio`, ссылающаяся на вложение `diagram.drawio.svg`. Это **editable-SVG**: пиксельный рендер + исходный mxfile-XML в атрибуте `content` корневого `<svg>`. Рендер XML→SVG делает сам drawio-редактор в браузере; серверного рендерера в проекте нет. Файлы: - нода: `packages/editor-ext/src/lib/drawio.ts` (attrs: `src`, `attachmentId`, `title`, `width`, `height`, `size`, `aspectRatio`, `align`) - round-trip в UI: `apps/client/src/features/editor/components/drawio/drawio-menu.tsx`, `.../drawio-view.tsx` (fetch SVG → `DrawIoEmbed` → export `xmlsvg` → `uploadFile`) - `DRAWIO_URL` (embed редактора) по умолчанию `https://embed.diagrams.net` — `apps/client/src/lib/config.ts` ## Проблема В MCP нет способа создать или отредактировать drawio-диаграмму: - `insert_image`/`replace_image` работают только с растровыми картинками, скачиваемыми по http-URL; - нет способа залить произвольные байты (SVG) как вложение; - нет чтения встроенного XML. ## Цель (скоуп) Дать агенту **создавать** и **редактировать** drawio-ноду, оперируя mxfile-XML как источником правды. Источник XML — вне скоупа (агент пишет руками либо берёт у внешнего drawio MCP, передаётся inline-текстом). Три тула: 1. `get_drawio` — чтение (нужно для осмысленного редактирования) 2. `insert_drawio` — создание новой ноды из XML 3. `update_drawio` — замена XML у существующей ноды ### Вне скоупа - Серверный рендер диаграммы (WYSIWYG). Превью — плейсхолдер-баннер до первого открытия человеком в редакторе (тогда drawio перерисует растр). - Оркестрация: gitmost **не** вызывает внешний drawio MCP. XML передаётся inline-текстом. - Изменения в habr-mcp. ## Контракты тулзов ### `get_drawio(pageId, attachmentId?)` - без `attachmentId` → список drawio-нод страницы: `[{ index, attachmentId, title, width, height, size }]` (обход `getPageJson`) - с `attachmentId` → скачать SVG (`fetchInternalFile`), распарсить jsdom-ом, прочитать атрибут `content` → mxfile XML. Если содержимое `<diagram>` сжато (base64 → raw-deflate) — распаковать (`zlib.inflateRawSync` + `decodeURIComponent`) - ответ: `{ attachmentId, fileName, size, width, height, xml }` - если атрибута `content` нет → ошибка «не editable drawio SVG» (без мутаций) ### `insert_drawio(pageId, xml, { placement?, align? })` - `placement`: append (по умолчанию) / after-text / replace-text (как у `insert_image`) - шаги: 1. валидировать XML (`<mxfile>` или `<mxGraphModel>`; голый mxGraphModel обернуть в mxfile). Мусор → ошибка **до** заливки 2. собрать `diagram.drawio.svg`: `<svg content="<escaped mxfile>" width/height data-drawio-stale="1">` + тело-плейсхолдер-баннер 3. залить байты через `POST /files/upload` (новый `uploadBuffer`) 4. собрать ноду `{ type: "drawio", attrs: { src, attachmentId, title: "diagram.drawio.svg", size, width, height, align } }` и вставить существующим `insertNode` / `mutatePageContent` (page-locked) - ответ: `{ attachmentId, placement, renderStale: true, note }` ### `update_drawio(pageId, attachmentId, xml, { align? })` - шаги: 1. валидировать XML (как выше) 2. собрать новый `diagram.drawio.svg` 3. залить **новым** вложением (не перезаписывать байты — Docmost на overwrite отдаёт HTTP 500, см. комментарий в `replaceImage`) 4. под page-lock репойнтить все ноды с `attrs.attachmentId === attachmentId` (общий `repointAttachment`): обновить `src/attachmentId/size/title`, сохранить `type/width/height/align/aspectRatio` - ответ: `{ oldAttachmentId, newAttachmentId, renderStale: true, note }` ## Рефакторинг / переиспользование - расщепить `uploadImage` → `uploadBuffer(pageId, buffer, fileName, mime)` + часть fetch-по-URL (`packages/mcp/src/client.ts`, ~L3006) - вынести цикл репойнта по `attachmentId` из `replaceImage` (~L3262) в общий `repointAttachment` - SVG-хелперы: `readEmbeddedXml(svgBuffer)` (jsdom + inflate), `buildDrawioSvg(xml, { width, height })` (баннер + встроенный `content`) - `jsdom` уже в зависимостях `packages/mcp`; `zlib` встроенный - адресация только по `attachmentId` (у drawio-ноды нет `attrs.id` — в outline такие блоки идут с `id: null`) ## Плейсхолдер-баннер - тело нового SVG = рамка + короткая метка (EN): `Diagram edited via agent — open in editor to re-render` - маркер `data-drawio-stale="1"` на корне `<svg>` - `renderStale: true` в ответах тулзов - обоснование: сервер не рендерит; drawio перерисует растр при первом открытии+сохранении человеком. `content`-XML при этом всегда корректный и редактируемый ## Регистрация - три тула инлайн в `packages/mcp/src/index.ts` (stdio) + зеркально в `http.ts`; добавить в строку MCP-гайда - MCP-only (как `replace_image`) - все комментарии в коде — на английском ## Крайние случаи / риски - сжатый `content` при чтении — обязательный inflate-фолбэк - нет `content` → понятная ошибка, без мутаций - orphan старого вложения на update (Docmost не умеет удалять одиночное вложение — та же особенность, что у `replace_image`) - конкурентность: page-lock + мутация live-документа - лимиты размера входного XML (напр. пара МБ) + ранняя валидация до заливки - SSRF/traversal на чтении уже закрыт `resolveInternalFilePath` - сохранять `width/height/align/aspectRatio` при update ## Критерии приёмки - [ ] `get_drawio` без аргумента перечисляет drawio-ноды; с `attachmentId` возвращает XML - [ ] `get_drawio` читает и сжатый, и несжатый `content` - [ ] `get_drawio` отдаёт понятную ошибку на не-editable SVG - [ ] `insert_drawio` создаёт ноду; XML открывается в редакторе drawio и показывает диаграмму - [ ] `insert_drawio` отклоняет битый XML **до** заливки (без orphan) - [ ] placement append/after/replace работает как у `insert_image` - [ ] `update_drawio` заменяет XML, репойнтит все ссылающиеся ноды, сохраняет `width/height/align` - [ ] ответы содержат `renderStale: true`; превью — баннер до открытия+сохранения человеком - [ ] тесты на всё перечисленное ## На будущее (отдельные задачи) - серверный рендер превью (WYSIWYG) — заменить баннер на реальный растр; контракты тулзов не меняются - `create_drawio(pageId, description)` — оркестрация: gitmost как MCP-клиент внешнего drawio MCP, чтобы большой XML не шёл через контекст агента - drawio-осведомлённость в habr-mcp (экспорт превью drawio-ноды)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#398