9a8671c3af
Механика конкурентной записи (withPageLock → acquireCollabSession → mutate) держалась на цепочке конвенций в комментариях. Новый write-метод без знания правил компилировался и уходил в прод (класс #260/#152/#159). Закрепляем ключевые инварианты кодом. - page-lock.ts: экспортированы UUID_RE/isUuid (тот же regex, что resolvePageId, UUID v1–8/v7). withPageLock FAIL-FAST кидает при не-UUID ключе ДО любой работы (комментарий-инвариант #260/#449) — забытый resolve/slugId больше не даёт тихую потерю сериализации под другим ключом. client.ts импортирует isUuid оттуда (убран локальный дубль — resolver и assert не разъедутся). - mutatePage/replacePage seams стали async и сами вызывают resolvePageId — ключ лока/кэша канонический даже если вызывающий забыл (для уже-UUID это cached no-op; все 7 текущих вызывающих и так резолвят). replaceImage (один внешний лок + mutateLiveContentUnlocked) не тронут, deadlock невозможен. - collab-session.ts: машинно-проверяемые маркеры MUTATE-CRITICAL-WINDOW BEGIN/END вокруг синхронного блока fromYdoc→applyDocToFragment (INVARIANT 1). Тест no-await-critical-window читает исходник и краснеет на await/yield в окне (проверено нейтером). Случайный await больше не тихо клоббит живые правки. - Документация осознанной позиции: single-instance/sticky-sessions — требование деплоя (Dockerfile + README EN/RU + .env.example), т.к. мьютекс и stash — per-process. Окно устаревших прав (кэш-сессия пишет под токеном момента connect до MCP_COLLAB_SESSION_MAX_AGE_MS=10мин) — задокументированный trade-off в .env.example; push-инвалидации нет (осознанно). Тесты: page-lock fail-fast (slugId/пусто/non-string → throw; канонический UUID принят), no-away-guard, обновлённые фикстуры на валидные UUID. #449-специфичные 37/37 зелёные; mcp tsc чисто. closes #449. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
415 lines
37 KiB
Markdown
415 lines
37 KiB
Markdown
# Docmost MCP Server
|
|
|
|
[English](README.md) · **Русский**
|
|
|
|
Сервер Model Context Protocol (MCP) для [Docmost](https://docmost.com/), который
|
|
позволяет ИИ-агентам **читать, искать, писать, реструктурировать, рецензировать, вести
|
|
версии, комментировать, иллюстрировать и публиковать** документацию — безопасно, на живом
|
|
инстансе и без enterprise-лицензии.
|
|
|
|
> **Написан агентом для агентов.** Человек правит документ глазами и руками: читает,
|
|
> заходит в редактор, перепечатывает. Агент работает иначе — ему гораздо проще *написать
|
|
> небольшую функцию, которая чинит текст*, чем перечитывать и заново выдавать весь
|
|
> документ. Поэтому сервер построен вокруг того, как модели на самом деле удобно
|
|
> редактировать: адресовать блок по id, сделать find/replace или передать трансформ
|
|
> `(doc, ctx) => doc` и позволить модели *запрограммировать* правку. `docmostTransform` —
|
|
> это и есть такой интерфейс. Другие Docmost-MCP «заточены под человека» — они дают
|
|
> «открыть страницу» и «заменить страницу»; этот даёт примитивы редактирования, в которых
|
|
> модель сильна.
|
|
|
|
Сервер предоставляет **41 инструмент**, построенный вокруг трёх идей, которые другие
|
|
Docmost-MCP не сочетают:
|
|
|
|
1. **Точечные, экономичные по токенам правки.** Адресуйте отдельный блок по id и патчите
|
|
его или делайте find/replace вместо того, чтобы гонять весь документ ~100 КБ через
|
|
модель.
|
|
2. **Безопасная запись на живой документ.** Каждая мутация проходит через слой
|
|
коллаборации реального времени (тот же WebSocket, что использует веб-редактор),
|
|
сериализуется по странице, поэтому никогда не затирает параллельную правку человека и
|
|
подтверждается как сохранённая до возврата из инструмента.
|
|
3. **Настоящая страховка.** История версий, дифф, эквивалентный Docmost, восстановление
|
|
одним вызовом и предпросмотр (dry-run) для скриптовых правок — чтобы агент мог
|
|
редактировать смело, а вы всегда могли увидеть и откатить сделанное.
|
|
|
|
---
|
|
|
|
## Почему именно этот сервер (в сравнении с альтернативами)
|
|
|
|
Существует несколько Docmost-MCP. Ниже — сравнение по возможностям.
|
|
«Официальный» — встроенный MCP Docmost; остальные — community-проекты на GitHub.
|
|
|
|
| Возможность | **Этот сервер** | Официальный (встроенный) | MrMartiniMo/docmost-mcp | cyborgx0x/mcp-docmost | aleksvin8888 / isak-landin |
|
|
| --- | :---: | :---: | :---: | :---: | :---: |
|
|
| **Нужна enterprise-лицензия** | **Нет** | **Да** | Нет | Нет | Нет |
|
|
| Аутентификация | email + пароль, **авто-переавторизация** | API-ключ | email + пароль | cookie `authToken` (копировать из DevTools) | API Docmost / **напрямую PostgreSQL** |
|
|
| Чтение страницы как Markdown | ✅ | ✅ | ✅ | ✅ | ✅ (только чтение) |
|
|
| **Markdown round-trip** (экспорт/импорт, сохраняет якоря комментариев) | ✅ | — | — | — | — |
|
|
| Чтение **lossless ProseMirror JSON** (с id блоков) | ✅ | — | — | — | — |
|
|
| **Компактная структура страницы** (дешёвый поиск id блока) | ✅ | — | — | — | — |
|
|
| **Получение одного блока** (по id или индексу) | ✅ | — | — | — | — |
|
|
| Создание / перемещение / удаление страниц | ✅ | ✅ | ✅ | ✅ | — |
|
|
| **Поблочные правки** (patch/insert/delete по id) | ✅ | — | — | — | — |
|
|
| **Хирургический find/replace** (с сохранением структуры) | ✅ | — | — | — | — |
|
|
| **Скриптовый JS-трансформ** (песочница, dry-run дифф) | ✅ | — | — | — | — |
|
|
| **Структурное редактирование таблиц** (CRUD строк/ячеек) | ✅ | — | — | — | — |
|
|
| **История версий** страницы | ✅ | — | — | ✅ | — |
|
|
| **Дифф двух версий** | ✅ | — | — | — | — |
|
|
| **Восстановление версии** (обратимое) | ✅ | — | — | — | — |
|
|
| **Комментарии** (CRUD + inline-привязка) | ✅ | — | — | ✅ | — |
|
|
| **Поллинг новых комментариев** с момента времени | ✅ | — | — | — | — |
|
|
| **Изображения** (вставка / замена) | ✅ | — | — | — | — |
|
|
| **Публичные ссылки** (создать / отозвать / список) | ✅ | — | — | — | — |
|
|
| Экспорт в HTML / PDF | — | — | — | ✅ | — |
|
|
| **Безопасная запись через real-time-collab** (без затирания, с подтверждением) | ✅ | n/a | ✅ | — | n/a (только чтение) |
|
|
|
|
### Что это даёт на практике
|
|
|
|
- **Никакого enterprise-налога.** Официальный MCP Docmost — enterprise-функция: нужна
|
|
активная enterprise-лицензия. Этот сервер — MIT и работает с *любым* self-hosted Docmost
|
|
через стандартный API + сокет коллаборации, имея лишь email и пароль аккаунта.
|
|
|
|
- **Экономия токенов при редактировании.** Большинство Docmost-MCP (и официальный)
|
|
предлагают только запись «заменить всю страницу» — агент вынужден скачать весь документ,
|
|
изменить и загрузить обратно, оплачивая весь документ **дважды** на каждой мелкой
|
|
правке. Этот сервер позволяет агенту изменить ровно один блок (`patchNode` /
|
|
`insertNode` / `deleteNode`), сделать find/replace с сохранением структуры
|
|
(`editPageText`) или скопировать страницу на стороне сервера (`copyPageContent`) —
|
|
**причём документ ни разу не проходит через модель**.
|
|
|
|
- **Записи, которые не воюют с редактором.** Наивная запись через REST конфликтует с тем,
|
|
что в этот момент печатает человек, и может молча затереть его правки или упасть на
|
|
дебаунс-сохранении Docmost. Этот сервер применяет каждое изменение через живой документ
|
|
коллаборации (Hocuspocus/Yjs), читая и записывая **синхронно в пределах одного тика
|
|
синхронизации**, чтобы никакая параллельная правка не вклинилась, сериализует записи
|
|
**по странице** мьютексом и **ждёт подтверждения сохранения от сервера** до возврата.
|
|
Если сокет отвалился посреди записи, инструмент возвращает ошибку, а не ложный успех.
|
|
|
|
- **Агентоориентированная модель редактирования.** Серверы «под человека» дают «открыть
|
|
страницу» и «заменить страницу», потому что это отражает то, как работает человек. Модель
|
|
редактирует лучше, *программируя* правку — адресуя блоки по id, делая find/replace или
|
|
передавая трансформ `(doc, ctx) => doc` (`docmostTransform`, с dry-run диффом перед
|
|
коммитом). Этот сервер построен вокруг этого — поэтому у него есть примитивы
|
|
редактирования, которых у остальных просто нет.
|
|
|
|
- **Страховка при редактировании, которой нет у других.** `listPageHistory` →
|
|
`diffPageVersions` → `restorePageVersion` дают агенту (и вам) полный цикл «посмотреть
|
|
и откатить». Дифф использует *тот же* конвейер `recreateTransform → ChangeSet →
|
|
simplifyChanges`, что и встроенный просмотр истории Docmost, так что результат совпадает
|
|
с продуктом.
|
|
|
|
- **Удобство вместо выковыривания cookie.** Некоторые community-серверы аутентифицируются,
|
|
заставляя вас копировать сессионный cookie из DevTools браузера (он истекает), либо лезут
|
|
**напрямую в базу PostgreSQL**. Этот сервер логинится по учётным данным и **прозрачно
|
|
переавторизуется на 401/403** (с дедупликацией
|
|
параллельных логинов), поэтому долгоживущие агенты не падают, когда токен истёк. Он также
|
|
соблюдает контроль доступа Docmost, потому что ходит через API и сервер коллаборации как
|
|
обычный пользователь.
|
|
|
|
---
|
|
|
|
## Инструменты
|
|
|
|
Все 41 инструмент, сгруппированы по задачам, для которых вы их возьмёте.
|
|
|
|
### Чтение и поиск
|
|
|
|
- **`getWorkspace`** — Информация о текущем воркспейсе Docmost.
|
|
- **`listSpaces`** — Все пространства воркспейса.
|
|
- **`listPages`** — Недавние страницы пространства, по убыванию `updatedAt` (по умолчанию
|
|
50, максимум 100). Для поиска в больших пространствах используйте `search`.
|
|
- **`search`** — Полнотекстовый поиск по страницам и контенту (ограничен `limit`, максимум
|
|
100).
|
|
- **`getPage`** — Контент страницы как чистый **Markdown** (канонично для текста; теряет
|
|
лишь id блоков, якоря разрешённых комментариев и фиксированный набор атрибутов без
|
|
markdown-представления — спаны/colwidth/фон ячеек таблиц, отступы (indent),
|
|
`callout.icon`, `orderedList.type` и `internal`/`target`/`rel`/`class` у ссылок;
|
|
используйте `getPageJson`, когда они нужны).
|
|
- **`getPageJson`** — **Lossless ProseMirror/TipTap JSON** страницы, включая `attrs.id`
|
|
каждого блока и `slugId`, используемый в URL. Именно его потребляют инструменты
|
|
поблочного редактирования.
|
|
- **`getOutline`** — Компактная структура страницы из блоков верхнего уровня (`{index,
|
|
type, id, level, firstText}`; для таблиц добавляются число строк/столбцов и тексты ячеек
|
|
заголовка, для списков — число пунктов) **без** тела документа. Дешёвый способ найти раздел или таблицу и получить
|
|
id блока перед `getNode` / `patchNode` / `insertNode`.
|
|
- **`getNode`** — Получить полное ProseMirror-поддерево одного блока (lossless), не
|
|
вытягивая всю страницу. Адресуйте его по id блока (из `getOutline` / `getPageJson`)
|
|
или формой `#<index>` для блока верхнего уровня — используйте `#<index>` для
|
|
таблиц/строк/ячеек, у которых нет id.
|
|
|
|
### Жизненный цикл страниц
|
|
|
|
- **`createPage`** — Создать страницу из Markdown и поместить в иерархию (опционально
|
|
`parentPageId`) одним вызовом. Использует import API Docmost для чистой конвертации
|
|
Markdown→ProseMirror.
|
|
- **`renamePage`** — Изменить только заголовок страницы, не трогая и не пересылая контент.
|
|
- **`movePage`** — Сменить родителя страницы (вложить или вынести в корень); поддерживает
|
|
позиционирование по fractional-index. Возвращает успех только при *положительно
|
|
подтверждённом* результате.
|
|
- **`deletePage`** — Удалить одну страницу.
|
|
- **`copyPageContent`** — Заменить тело одной страницы копией тела другой, **полностью на
|
|
стороне сервера** — документ не проходит через модель. У целевой страницы сохраняются
|
|
собственные заголовок и slug (URL не меняется).
|
|
|
|
### Редактирование
|
|
|
|
- **`editPageText`** — Хирургический find/replace внутри текста страницы. Сохраняет
|
|
**всю** структуру: id блоков, marks, ссылки, коллауты, таблицы. Предпочтительный
|
|
инструмент для правки формулировок, опечаток, чисел и имён.
|
|
- **`patchNode`** — Заменить один блок, адресованный по `attrs.id` (из `getPageJson`),
|
|
без пересылки документа.
|
|
- **`insertNode`** — Вставить блок до/после другого (по `attrs.id` или по якорному тексту)
|
|
либо добавить в конец.
|
|
- **`deleteNode`** — Удалить один блок по его `attrs.id`.
|
|
- **`updatePageJson`** — Заменить весь контент страницы документом ProseMirror (массовые
|
|
перезаписи или когда у узлов нет id). `content` опционален — опустите его, чтобы изменить
|
|
только заголовок. Сохраняет переданные id блоков, поэтому якоря заголовков и история
|
|
остаются стабильными.
|
|
- **`updatePageMarkdown`** — Заменить тело страницы (и опционально заголовок) новым
|
|
**обычным Markdown**. Всё тело переимпортируется (id блоков перегенерируются — для
|
|
хирургических правок или сохранения id используйте `editPageText` / `patchNode` /
|
|
`updatePageJson`). Markdown в диалекте Docmost разбирается, включая inline-сноски `^[...]`.
|
|
- **`docmostTransform`** — Агентоориентированный интерфейс редактирования: вместо
|
|
перепечатывания документа агент **пишет функцию, которая его чинит**. Редактирует
|
|
страницу, запуская произвольный **JS-трансформ `(doc, ctx) => doc`** на её *живом*
|
|
документе ProseMirror. Работает в **песочнице** (без `require`/`process`/`fs`/сети,
|
|
таймаут 5 с). **По умолчанию dry-run**: возвращает предпросмотр диффа без записи;
|
|
установите `dryRun:false`, чтобы применить атомарно. `ctx` даёт доступ к комментариям
|
|
страницы и набору хелперов (`walk`, `getList`, `blockText`, `insertMarkerAfter`,
|
|
`setCalloutRange`, `commentsToFootnotes`, …) для многошаговых согласованных перезаписей —
|
|
например перенумерации или превращения inline-комментариев в нумерованные сноски.
|
|
|
|
### Таблицы
|
|
|
|
- **`tableGet`** — Прочитать таблицу как матрицу: `{rows, cols, cells (text[][]),
|
|
cellIds}` (id абзаца на ячейку или `null`). Адресуйте таблицу через `#<index>` (из
|
|
`getOutline`) или любой id блока внутри неё. Используйте `cellIds` вместе с `patchNode`
|
|
для правок ячеек с форматированием.
|
|
- **`tableInsertRow`** — Вставить строку из текстовых ячеек, дополненную до числа
|
|
столбцов таблицы (передать ячеек больше числа столбцов — ошибка). `index` — 0-based
|
|
позиция вставки (0 вставляет перед заголовком); опустите, чтобы добавить в конец.
|
|
- **`tableDeleteRow`** — Удалить строку по 0-based `index`. Отказывается удалять
|
|
единственную строку таблицы; удаление строки 0 делает заголовком следующую строку.
|
|
- **`tableUpdateCell`** — Задать текстовое содержимое ячейки `[row, col]` (0-based). Для
|
|
форматирования используйте `patchNode` по id абзаца ячейки из `tableGet`.
|
|
|
|
### Markdown: экспорт и импорт
|
|
|
|
- **`exportPageMarkdown`** — Экспортировать страницу в один самодостаточный
|
|
**Markdown в диалекте Docmost**: мета-заголовок, тело с inline-якорями комментариев и
|
|
диаграммами и завершающий блок тредов комментариев. Round-trip скачать → отредактировать →
|
|
импортировать перегенерирует id блоков и **молча отбрасывает** набор атрибутов без
|
|
markdown-представления (спаны/colwidth/фон ячеек таблиц, отступы (indent), `callout.icon`,
|
|
`orderedList.type`, `internal`/`target`/`rel`/`class` у ссылок); держите их в ProseMirror
|
|
JSON, если они должны выжить. Чтобы заменить тело страницы из обычного авторского Markdown,
|
|
используйте `updatePageMarkdown`.
|
|
|
|
> **Удалено в этом релизе:** `importPageMarkdown` (парсер round-trip для
|
|
> экспортированного Docmost-Markdown-файла) **больше не отдаётся на внешней MCP-поверхности**.
|
|
> Чтобы заменить тело страницы из Markdown, используйте **`updatePageMarkdown`** (замена
|
|
> тела обычным Markdown). См. заметку о миграции в CHANGELOG.
|
|
|
|
### Изображения
|
|
|
|
- **`insertImage`** — Загрузить локальное изображение и вставить за один шаг: добавить в
|
|
конец, поставить вместо текстового плейсхолдера (`replaceText`) или после заданного блока
|
|
(`afterText`). Сохраняет id всех остальных блоков.
|
|
- **`replaceImage`** — Заменить существующее изображение. Загружает новый файл как **новое
|
|
вложение** (чистый URL, который рендерится и сбрасывает кэш браузера), затем
|
|
перенаправляет все узлы, ссылавшиеся на старое вложение (рекурсивно, включая
|
|
коллауты/таблицы), через живой документ, сохраняя комментарии, выравнивание и alt-текст.
|
|
(Перезапись «по месту» намеренно не используется — некоторые версии Docmost портят
|
|
вложение при перезаписи.)
|
|
- **`stashPage`** — Сериализовать страницу целиком (её полный ProseMirror JSON) в
|
|
эфемерный blob в оперативной памяти и вернуть ТОЛЬКО короткий анонимный URL — тело
|
|
никогда не попадает в контекст модели, поэтому это способ передать большую страницу
|
|
(вместе с её изображениями) внешнему потребителю без усечения. Каждое внутреннее
|
|
файловое/графическое вложение зеркалируется в тот же sandbox, а его `src` переписывается
|
|
на URL sandbox; внешние http(s)-изображения остаются нетронутыми. Возвращает
|
|
`{ uri, size, sha256, images:{ mirrored, failed } }` (`sha256` — это также ETag blob'а).
|
|
Blob'ы хранятся только в оперативной памяти, истекают через короткий TTL (~1 ч) и
|
|
привязаны к тому экземпляру сервера, который их создал.
|
|
|
|
### Комментарии
|
|
|
|
- **`createComment`** — Добавить комментарий к странице, опционально **привязав inline** к
|
|
точному фрагменту текста (первое вхождение оборачивается comment-маркой).
|
|
- **`listComments`** — Список комментариев страницы (контент возвращается как Markdown).
|
|
- **`updateComment`** — Изменить существующий комментарий.
|
|
- **`deleteComment`** — Удалить комментарий.
|
|
- **`resolveComment`** — Закрыть (resolve) или переоткрыть тред комментария (обратимо). Resolve
|
|
доступен только для корневых комментариев; тред и ответы сохраняются, в отличие от `deleteComment`.
|
|
- **`checkNewComments`** — Найти комментарии, созданные после заданной метки времени
|
|
ISO-8601, по пространству, опционально в рамках поддерева страниц — идеально для агента,
|
|
который следит за обратной связью в документе.
|
|
|
|
### Версии и история
|
|
|
|
- **`listPageHistory`** — Сохранённые версии страницы (Docmost авто-снапшотит при каждом
|
|
сохранении), новые сверху, курсорная пагинация. id каждого элемента — это `historyId`.
|
|
- **`diffPageVersions`** — Дифф двух версий (или версии против живой страницы).
|
|
Возвращает вставленный/удалённый текст, счётчики целостности (изображения, ссылки,
|
|
таблицы, коллауты, маркеры сносок) и человекочитаемую Markdown-сводку — посчитано тем же
|
|
конвейером, что использует встроенный просмотр истории Docmost.
|
|
- **`restorePageVersion`** — Записать сохранённую версию обратно как текущий контент. У
|
|
Docmost нет эндпоинта восстановления, поэтому создаётся **новый** снапшот — само
|
|
восстановление тоже обратимо.
|
|
|
|
### Публикация
|
|
|
|
- **`sharePage`** — Сделать страницу публично доступной (идемпотентно) и вернуть её
|
|
публичный URL (`<app>/share/<key>/p/<slugId>`); опционально индексирование поисковиками.
|
|
- **`unsharePage`** — Отозвать публичный доступ к странице.
|
|
- **`listShares`** — Все публичные ссылки воркспейса с заголовками и публичными URL.
|
|
|
|
---
|
|
|
|
## Как выбрать инструмент редактирования
|
|
|
|
Та же подсказка отдаётся в рантайме через поле `instructions` MCP-сервера, так что
|
|
подходящие клиенты направляют модель автоматически.
|
|
|
|
- **Правки текста** (формулировки, опечатки, числа): `editPageText`.
|
|
- **Один блок** (абзац/заголовок/коллаут/ячейка таблицы): `patchNode` / `insertNode` /
|
|
`deleteNode`, адресуя узел по его `attrs.id` из `getPageJson`.
|
|
- **Изображения**: `insertImage` / `replaceImage`.
|
|
- **Новая страница**: `createPage`.
|
|
- **Массовая перезапись или узлы без id**: `updatePageJson` (ProseMirror) или
|
|
`updatePageMarkdown` (замена тела обычным Markdown).
|
|
- **Многошаговая / скриптовая перезапись** (перенумерация, сноски, согласованные правки):
|
|
`docmostTransform` — предпросмотр через `dryRun`, затем применение.
|
|
- **Скопировать контент целой страницы из другой** (на стороне сервера):
|
|
`copyPageContent`.
|
|
- **Переименовать страницу** (только заголовок): `renamePage`.
|
|
- **Чтение**: `getPage` (Markdown) / `getPageJson` (lossless ProseMirror с id).
|
|
- **Просмотр изменений**: `listPageHistory` → `diffPageVersions` →
|
|
`restorePageVersion`.
|
|
- **Комментарии**: `createComment` (с опциональной inline-привязкой) / `listComments` /
|
|
`updateComment` / `resolveComment` / `deleteComment` / `checkNewComments`.
|
|
- **Дешёвая навигация по странице** (найти раздел/таблицу, получить id блока): `getOutline`
|
|
→ `getNode`.
|
|
- **Таблицы** (добавить/удалить строку, задать ячейку): `tableGet` / `tableInsertRow` /
|
|
`tableDeleteRow` / `tableUpdateCell`.
|
|
- **Экспорт страницы в самодостаточный Markdown** (с якорями комментариев): `exportPageMarkdown`.
|
|
- **Заменить тело страницы из Markdown**: `updatePageMarkdown`.
|
|
|
|
---
|
|
|
|
## Как это устроено (технические детали)
|
|
|
|
- **Безопасная запись через коллаборацию реального времени.** Мутации контента применяются
|
|
через WebSocket коллаборации Docmost (Hocuspocus + Yjs). Сервер подключается, ждёт
|
|
первичной синхронизации, чтобы локальный документ отражал авторитетный серверный (включая
|
|
правки, которых ещё нет в дебаунс-снапшоте REST), затем **читает → трансформирует →
|
|
пишет синхронно** в одном тике, чтобы никакое удалённое обновление не вклинилось, и
|
|
**ждёт подтверждения сохранения** до возврата.
|
|
- **Сериализация записи по странице.** Асинхронный мьютекс по разрешённому **UUID**
|
|
страницы (никогда не по slugId) гарантирует, что две записи MCP в одну страницу никогда
|
|
не пересекаются; разные страницы друг друга не блокируют. Хелпер блокировки падает сразу
|
|
(fail-fast), если ему передали не-UUID ключ, — путь записи, забывший разрезолвить id, не
|
|
сможет молча взять лок под расщеплённым ключом.
|
|
|
|
**Требование к деплою — один инстанс или sticky-сессии.** Этот мьютекс — процесс-локальный
|
|
`Map`, а кэш collab-сессий и хранилище `stashPage` живут только в RAM одного процесса. За
|
|
**мультиреплика**-балансировщиком **без sticky-сессий** две реплики могут одновременно
|
|
«держать» лок одной страницы, и сериализация по странице молча теряется. Запускайте
|
|
MCP/приложение **одним инстансом** либо прибивайте трафик страницы к одной реплике
|
|
(sticky-сессии / consistent hashing по id страницы). Кросс-процессной блокировки (например,
|
|
Postgres advisory-lock) намеренно пока нет — осознанное задокументированное ограничение.
|
|
См. комментарий в `Dockerfile` и блок `MCP collaboration write path` в `.env.example`.
|
|
|
|
**Окно устаревших прав.** Кэшированная collab-сессия пишет под токеном, захваченным в
|
|
момент connect (а кэш collab-токена переиспользует токен в пределах своего TTL), поэтому
|
|
**отозванный** доступ к странице может лагать до `MCP_COLLAB_SESSION_MAX_AGE_MS` (жёсткий
|
|
срок жизни сессии, по умолчанию 10 мин), пока следующая переавторизация его не подхватит.
|
|
Уменьшите значение, чтобы сократить лаг ценой большего числа переподключений. Это
|
|
ограниченное окно — принятый trade-off; push-инвалидации кэша при смене прав нет.
|
|
- **Прозрачная переавторизация.** Логин по email/паролю; истёкшие токены обновляются
|
|
автоматически на первом 401/403 (покрывая JSON, multipart-загрузку и путь токена
|
|
коллаборации), с дедупликацией параллельных логинов, так что пачка вызовов вызывает один
|
|
повторный логин.
|
|
- **Точные чтения.** `getPageJson` возвращает точное дерево ProseMirror с id блоков;
|
|
`getPage` возвращает канонический Markdown, теряющий лишь фиксированный, документированный
|
|
набор атрибутов.
|
|
- **Полная схема Docmost.** Конвертация Markdown↔ProseMirror поддерживает коллауты
|
|
(включая вложенные), списки задач (маркированные *и* нумерованные чек-листы), таблицы,
|
|
блоки формул, эмбеды, выделение, под/надстрочный текст и прочее, с защитными лимитами
|
|
против патологического ввода.
|
|
- **Структурные таблицы и Markdown round-trip.** Таблицы можно редактировать как
|
|
матрицу (чтение, вставка/удаление строк, задание ячеек по `[row, col]`) без пересылки
|
|
документа, а страницу — экспортировать и заново импортировать как самодостаточный
|
|
Markdown-файл в диалекте Docmost, сохраняющий inline-якоря комментариев и диаграммы
|
|
(id блоков перегенерируются, а фиксированный набор атрибутов без markdown-представления
|
|
отбрасывается — см. `exportPageMarkdown`).
|
|
- **Ответы, оптимизированные по токенам.** Ответы API урезаются до полей, действительно
|
|
нужных агентам, а большие коллекции (пространства, страницы, комментарии, история)
|
|
пагинируются.
|
|
- **Закалённый рантайм.** Глобальные обработчики не дают случайной ошибке сокета уронить
|
|
stdio-сервер; `movePage` требует положительно подтверждённого успеха; движок диффа
|
|
откатывается к грубому поблочному диффу, а не падает на патологическом документе.
|
|
|
|
---
|
|
|
|
## Установка
|
|
|
|
```bash
|
|
npm install
|
|
npm run build
|
|
```
|
|
|
|
## Конфигурация
|
|
|
|
Серверу нужны три переменные окружения:
|
|
|
|
- `DOCMOST_API_URL` — полный URL к API вашего Docmost (например,
|
|
`https://docs.example.com/api`).
|
|
- `DOCMOST_EMAIL` — email аккаунта для аутентификации.
|
|
- `DOCMOST_PASSWORD` — пароль аккаунта.
|
|
|
|
## Использование с Claude Desktop / произвольным MCP-клиентом
|
|
|
|
Добавьте сервер в конфигурацию MCP (например, `claude_desktop_config.json`):
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"docmost-local": {
|
|
"command": "node",
|
|
"args": ["./build/index.js"],
|
|
"env": {
|
|
"DOCMOST_API_URL": "http://localhost:3000/api",
|
|
"DOCMOST_EMAIL": "test@docmost.com",
|
|
"DOCMOST_PASSWORD": "test"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Разработка
|
|
|
|
```bash
|
|
# Режим наблюдения
|
|
npm run watch
|
|
|
|
# Сборка
|
|
npm run build
|
|
|
|
# Тесты (unit + mock; live end-to-end набор требует запущенного Docmost)
|
|
npm test
|
|
npm run test:e2e
|
|
```
|
|
|
|
## Происхождение и благодарности
|
|
|
|
Проект начинался как форк
|
|
[MrMartiniMo/docmost-mcp](https://github.com/MrMartiniMo/docmost-mcp) (автор Moritz Krause)
|
|
и существенно его расширяет — добавлены поблочное редактирование узлов, хирургические
|
|
правки текста, песочница `docmostTransform`, история версий / дифф / восстановление,
|
|
комментарии, вставка/замена изображений, публичные ссылки, серверное копирование страниц,
|
|
двойное чтение JSON/Markdown, прозрачная переавторизация и значительное упрочнение.
|
|
Инструменты комментариев портированы из upstream PR #3 от Max Nikitin. Спасибо обоим.
|
|
|
|
## Лицензия
|
|
|
|
MIT
|