Replace the removed enterprise EE MCP (private apps/server/src/ee submodule,
license-gated /mcp route) with our docmost-mcp, vendored as an isolated ESM
workspace package and served by the server over HTTP — no enterprise license.
Backend:
- Add packages/mcp (@docmost/mcp): vendored docmost-mcp refactored into a
side-effect-free createDocmostMcpServer() factory (38 tools preserved),
stdio entry kept in stdio.ts, Streamable-HTTP session manager in http.ts.
- Add apps/server McpModule: @Post/@Get/@Delete('mcp') (served at /mcp via the
existing global-prefix exclude), @SkipTransform + reply.hijack to bridge raw
Fastify req/res into the SDK transport. The module dynamically imports the
ESM-only package from CommonJS via a Function-indirected import resolved with
require.resolve + file:// URL. Gated by the workspace ai.mcp toggle, a
service-account (MCP_DOCMOST_EMAIL/PASSWORD/API_URL) and optional MCP_TOKEN;
per-session idle eviction (MCP_SESSION_IDLE_MS).
- Drop the enterprise license check on mcpEnabled in workspace.service.
- Dockerfile: copy packages/mcp into the production image.
- .env.example: document MCP_DOCMOST_*, MCP_TOKEN, MCP_SESSION_IDLE_MS.
Frontend:
- Recreate the community "AI & MCP" workspace-settings panel (mcp-settings.tsx):
admin-only toggle on settings.ai.mcp with optimistic update, copyable
${APP_URL}/mcp URL; wired into workspace-settings page. Reuses existing i18n.
Fixes:
- Pin packages/mcp tiptap deps to 3.20.4 (matching the client) and inline
getStyleProperty, preventing a duplicate @tiptap/core@3.26.1 from leaking into
the client editor via pnpm shamefully-hoist (was breaking apps/client tsc).
152 lines
4.2 KiB
JavaScript
152 lines
4.2 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { convertProseMirrorToMarkdown } from "../../build/lib/markdown-converter.js";
|
|
|
|
// ProseMirror builders.
|
|
const text = (t, marks) => (marks ? { type: "text", text: t, marks } : { type: "text", text: t });
|
|
const paragraph = (...content) => ({ type: "paragraph", content });
|
|
const doc = (...content) => ({ type: "doc", content });
|
|
const listItem = (...content) => ({ type: "listItem", content });
|
|
const bulletList = (...items) => ({ type: "bulletList", content: items });
|
|
const orderedList = (...items) => ({ type: "orderedList", content: items });
|
|
|
|
test("nested bulletList with 3 children keeps all children indented under the parent", () => {
|
|
const input = doc(
|
|
bulletList(
|
|
listItem(
|
|
paragraph(text("Parent")),
|
|
bulletList(
|
|
listItem(paragraph(text("A"))),
|
|
listItem(paragraph(text("B"))),
|
|
listItem(paragraph(text("C"))),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
assert.equal(
|
|
convertProseMirrorToMarkdown(input),
|
|
"- Parent\n - A\n - B\n - C",
|
|
);
|
|
});
|
|
|
|
test("nested list under an ordered item indents 3 spaces", () => {
|
|
const input = doc(
|
|
orderedList(
|
|
listItem(
|
|
paragraph(text("Parent")),
|
|
bulletList(listItem(paragraph(text("Child")))),
|
|
),
|
|
),
|
|
);
|
|
|
|
assert.equal(
|
|
convertProseMirrorToMarkdown(input),
|
|
"1. Parent\n - Child",
|
|
);
|
|
});
|
|
|
|
test("link with title -> [t](url \"title\")", () => {
|
|
const input = doc(
|
|
paragraph(
|
|
text("click", [
|
|
{ type: "link", attrs: { href: "https://example.com", title: "the title" } },
|
|
]),
|
|
),
|
|
);
|
|
|
|
assert.equal(
|
|
convertProseMirrorToMarkdown(input),
|
|
'[click](https://example.com "the title")',
|
|
);
|
|
});
|
|
|
|
test("hardBreak -> trailing two-spaces+newline", () => {
|
|
const input = doc(
|
|
paragraph(text("line1"), { type: "hardBreak" }, text("line2")),
|
|
);
|
|
|
|
assert.equal(convertProseMirrorToMarkdown(input), "line1 \nline2");
|
|
});
|
|
|
|
test("table cell with two block children joined by a space (and a pipe escaped)", () => {
|
|
const input = doc({
|
|
type: "table",
|
|
content: [
|
|
{
|
|
type: "tableRow",
|
|
content: [
|
|
{
|
|
type: "tableCell",
|
|
content: [paragraph(text("a|b")), paragraph(text("c"))],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
// Single-column header row + separator. The cell joins its two paragraphs
|
|
// with a space ("a|b c") then escapes the pipe -> "a\|b c".
|
|
assert.equal(
|
|
convertProseMirrorToMarkdown(input),
|
|
"| a\\|b c |\n| --- |",
|
|
);
|
|
});
|
|
|
|
test("code block trailing newline trimmed", () => {
|
|
const input = doc({
|
|
type: "codeBlock",
|
|
attrs: { language: "js" },
|
|
content: [text("const a = 1;\n")],
|
|
});
|
|
|
|
// The single trailing newline inside the code is trimmed; fences add one.
|
|
assert.equal(
|
|
convertProseMirrorToMarkdown(input),
|
|
"```js\nconst a = 1;\n```",
|
|
);
|
|
});
|
|
|
|
test("textAlign value: delimiting double-quote escaped (attribute-safe, idempotent; < > left literal/inert)", () => {
|
|
const input = doc({
|
|
type: "paragraph",
|
|
attrs: { textAlign: 'right"><b' },
|
|
content: [text("body")],
|
|
});
|
|
|
|
// Attribute values escape only & and " so the value cannot break out of the
|
|
// quoted attribute. < and > are left literal: parse5/jsdom does NOT decode
|
|
// </> inside attribute values, so escaping them would corrupt the value
|
|
// and accumulate on every round-trip. The literal < > are inert inside quotes.
|
|
assert.equal(
|
|
convertProseMirrorToMarkdown(input),
|
|
'<div align="right"><b">body</div>',
|
|
);
|
|
});
|
|
|
|
test("highlight color: delimiting double-quote escaped (attribute-safe; < > inert, and import sanitizes the color)", () => {
|
|
const input = doc(
|
|
paragraph(
|
|
text("hi", [{ type: "highlight", attrs: { color: 'red"><script' } }]),
|
|
),
|
|
);
|
|
|
|
assert.equal(
|
|
convertProseMirrorToMarkdown(input),
|
|
'<mark style="background-color: red"><script">hi</mark>',
|
|
);
|
|
});
|
|
|
|
test("empty task item still emits its marker", () => {
|
|
const input = doc({
|
|
type: "taskList",
|
|
content: [
|
|
{ type: "taskItem", attrs: { checked: false }, content: [] },
|
|
{ type: "taskItem", attrs: { checked: true }, content: [] },
|
|
],
|
|
});
|
|
|
|
assert.equal(convertProseMirrorToMarkdown(input), "- [ ]\n- [x]");
|
|
});
|