572f0a2ab9
Инцидент: агент заполнял большую таблицу десятками table_update_cell, каждый — полный цикл connect/auth/load/initial-sync/store/unload; часть падала с 25s connect- timeout, event loop lag до 1.7с. Вариант B: кэшировать живой HocuspocusProvider per page на серию правок — пока провайдер жив (connections>0), сервер не входит в store->unload->reload, дебаунс реально коалесцирует записи (N ячеек -> 1-2 store). Новый модуль collab-session.ts: класс CollabSession (connecting->ready->dead) + реестр (ключ wsUrl+pageId+token) с idle-TTL/max-age/LRU-evict. mutatePageContent (collaboration.ts) и mutateLiveContentUnlocked (client.ts, replaceImage) переведены на acquireCollabSession; one-shot Promise-машина (~360 строк дублирования) удалена. 5 инвариантов (подтверждены внутренним ревью): (1) read->write атомарна — между fromYdoc и applyDocToFragment нет await; (2) per-edit ack сохранён дословно (гард connectionLost от false-success при реконнекте); (3) disconnect=смерть сессии (без авто-реконнекта, in-flight реджектится теми же текстами ошибок); (4) изоляция identity (токен в ключе); (5) валидация при reuse. replaceImage работает под внешним page-локом без дедлока (acquire лок не берёт). Тексты ошибок == develop. Env: MCP_COLLAB_SESSION_IDLE_MS (0=выкл кэш, точное легаси), _MAX_AGE_MS, _MAX_ENTRIES. Teardown: destroyAllSessions обвязан в stdio (exit/SIGINT/SIGTERM) + реэкспорт из index для встраивающего хоста. closes #400 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
64 lines
2.3 KiB
JavaScript
64 lines
2.3 KiB
JavaScript
#!/usr/bin/env node
|
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
import { createDocmostMcpServer } from "./index.js";
|
|
import { destroyAllSessions } from "./lib/collab-session.js";
|
|
|
|
// Standalone stdio entrypoint. This restores the original behavior of the
|
|
// package when run as a CLI (`docmost-mcp`): it reads credentials from the
|
|
// environment and serves the MCP protocol over stdin/stdout. The factory in
|
|
// index.ts stays side-effect-free; all the process/transport lifecycle lives
|
|
// here.
|
|
|
|
const API_URL = process.env.DOCMOST_API_URL;
|
|
const EMAIL = process.env.DOCMOST_EMAIL;
|
|
const PASSWORD = process.env.DOCMOST_PASSWORD;
|
|
|
|
if (!API_URL || !EMAIL || !PASSWORD) {
|
|
console.error(
|
|
"Error: DOCMOST_API_URL, DOCMOST_EMAIL, and DOCMOST_PASSWORD environment variables are required.",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
async function run() {
|
|
// Global safety nets so a stray rejection/exception cannot silently kill
|
|
// the stdio server. Per-tool errors still flow through the SDK and are not
|
|
// affected by these handlers; these only catch errors raised OUTSIDE a tool
|
|
// call (e.g. a transient ws/collab socket "error" event). Such errors must
|
|
// NOT tear down the whole stdio server, so we log only and keep running.
|
|
// Genuine startup failures are still fatal via run().catch(...) below.
|
|
process.on("unhandledRejection", (reason) => {
|
|
console.error("Unhandled promise rejection:", reason);
|
|
});
|
|
process.on("uncaughtException", (error) => {
|
|
console.error("Uncaught exception:", error);
|
|
});
|
|
|
|
// Teardown hook (issue #400): destroy every cached live CollabSession on exit
|
|
// so a hanging session does not keep a doc loaded on the server (which would
|
|
// also defer the server's afterUnloadDocument cleanup). `exit` runs the
|
|
// synchronous idempotent teardown; SIGINT/SIGTERM also run it, then exit.
|
|
process.on("exit", () => {
|
|
destroyAllSessions();
|
|
});
|
|
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
process.on(signal, () => {
|
|
destroyAllSessions();
|
|
process.exit(0);
|
|
});
|
|
}
|
|
|
|
const server = createDocmostMcpServer({
|
|
apiUrl: API_URL!,
|
|
email: EMAIL!,
|
|
password: PASSWORD!,
|
|
});
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
}
|
|
|
|
run().catch((error) => {
|
|
console.error("Fatal error running server:", error);
|
|
process.exit(1);
|
|
});
|