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).
75 lines
2.9 KiB
JavaScript
75 lines
2.9 KiB
JavaScript
import axios from "axios";
|
|
export async function getCollabToken(baseUrl, apiToken) {
|
|
try {
|
|
const response = await axios.post(`${baseUrl}/auth/collab-token`, {}, {
|
|
headers: {
|
|
Authorization: `Bearer ${apiToken}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
});
|
|
// console.error('Collab Token Response:', response.data);
|
|
// Response is wrapped in { data: { token: ... } }
|
|
return response.data.data?.token || response.data.token;
|
|
}
|
|
catch (error) {
|
|
if (axios.isAxiosError(error)) {
|
|
// Attach the HTTP status to the plain Error so callers (e.g.
|
|
// getCollabTokenWithReauth) can still detect a 401/403 after the
|
|
// original AxiosError has been wrapped away.
|
|
// Avoid leaking the full server response body by default; include only
|
|
// status + statusText. Append the body only when DEBUG is set.
|
|
let message = `Failed to get collab token: ${error.response?.status} ${error.response?.statusText}`;
|
|
if (process.env.DEBUG) {
|
|
message += ` - ${JSON.stringify(error.response?.data)}`;
|
|
}
|
|
const err = new Error(message);
|
|
err.status = error.response?.status;
|
|
throw err;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
export async function performLogin(baseUrl, email, password) {
|
|
try {
|
|
const response = await axios.post(`${baseUrl}/auth/login`, {
|
|
email,
|
|
password,
|
|
});
|
|
// Extract token from Set-Cookie header
|
|
const cookies = response.headers["set-cookie"];
|
|
if (!cookies) {
|
|
throw new Error("No Set-Cookie header found in login response");
|
|
}
|
|
// Match the cookie name exactly to avoid matching a future
|
|
// authTokenRefresh cookie (startsWith would catch it).
|
|
const authCookie = cookies.find((c) => {
|
|
const kv = c.split(";")[0];
|
|
return kv.slice(0, kv.indexOf("=")) === "authToken";
|
|
});
|
|
if (!authCookie) {
|
|
throw new Error("No authToken cookie found in login response");
|
|
}
|
|
// Take everything after the FIRST "=" up to the first ";".
|
|
// Splitting on "=" would truncate base64 values containing "=" padding.
|
|
const kv = authCookie.split(";")[0];
|
|
const token = kv.slice(kv.indexOf("=") + 1);
|
|
return token;
|
|
}
|
|
catch (error) {
|
|
// Avoid leaking the full server response body by default; log only the
|
|
// HTTP status. Log the verbose body only when DEBUG is set.
|
|
if (axios.isAxiosError(error)) {
|
|
if (process.env.DEBUG) {
|
|
console.error("Login failed:", error.response?.data);
|
|
}
|
|
else {
|
|
console.error("Login failed:", error.response?.status);
|
|
}
|
|
}
|
|
else {
|
|
console.error("Login failed:", error.message);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|