ultisuite-backend/services/ultimail-mcp/dist/index.js
R3D347HR4Y 621b0099d6
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
feat(deploy): enhance Nginx configuration and API integration for UltiAI
- Updated .env.example to include new configuration options for the UltiAI branding and API endpoints.
- Enhanced Nginx configuration to support new API routes for the MCP and WebSocket connections.
- Introduced sub-filters for branding adjustments in Nginx responses.
- Added new JavaScript patch for API endpoint adjustments.
- Implemented tests for new API functionalities and improved error handling in the AI gateway.
2026-06-15 00:22:23 +02:00

727 lines
29 KiB
JavaScript

import { randomUUID } from "node:crypto";
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const PORT = Number(process.env.MCP_PORT ?? 3100);
const API_BASE = (process.env.ULTID_API_URL ?? "http://localhost:8080/api/v1").replace(/\/$/, "");
const TOOL_GROUPS = {
mail: [
"mail_search",
"mail_read",
"mail_list",
"mail_send",
"mail_update_labels",
"mail_update_flags",
"mail_delete",
],
drive: [
"drive_list",
"drive_file_info",
"drive_create_file",
"drive_create_folder",
"drive_delete",
"drive_move",
"drive_rename",
"drive_share_create",
"drive_share_update",
"drive_share_delete",
],
contacts: [
"contacts_search",
"contacts_list_books",
"contacts_list",
"contacts_get",
"contacts_create",
"contacts_update",
"contacts_delete",
],
search: ["suite_search"],
docs: ["docs_read", "docs_save"],
agenda: [
"calendar_list",
"calendar_create",
"calendar_update",
"calendar_delete",
"calendar_list_events",
"calendar_freebusy",
"calendar_create_event",
"calendar_update_event",
"calendar_delete_event",
"calendar_respond_invitation",
"calendar_create_meet_link",
],
web_search: ["web_search"],
};
const ALL_TOOL_NAMES = [...new Set(Object.values(TOOL_GROUPS).flat())];
function parseEnabledTools(raw) {
const groups = String(raw ?? "")
.split(",")
.map((part) => part.trim().toLowerCase())
.filter(Boolean);
if (groups.length === 0)
return null;
const enabled = new Set();
for (const group of groups) {
for (const tool of TOOL_GROUPS[group] ?? []) {
enabled.add(tool);
}
}
return enabled.size > 0 ? enabled : null;
}
function encodePath(path) {
return path
.replace(/^\/+/, "")
.split("/")
.filter(Boolean)
.map((seg) => encodeURIComponent(seg))
.join("/");
}
function toolText(data) {
return {
content: [
{
type: "text",
text: typeof data === "string" ? data : JSON.stringify(data, null, 2),
},
],
};
}
/** Ultid list APIs use page + page_size, not limit + offset. */
function listPaginationParams(limit, offset) {
const qs = new URLSearchParams();
const pageSize = limit != null && Number.isFinite(limit) && limit > 0
? Math.min(Math.trunc(limit), 500)
: undefined;
if (pageSize != null) {
qs.set("page_size", String(pageSize));
const off = offset != null && Number.isFinite(offset) && offset > 0 ? Math.trunc(offset) : 0;
qs.set("page", String(Math.floor(off / pageSize) + 1));
}
else if (offset != null && Number.isFinite(offset) && offset > 0) {
qs.set("page_size", "50");
qs.set("page", String(Math.floor(Math.trunc(offset) / 50) + 1));
}
return qs;
}
function withListPagination(base, limit, offset) {
const qs = new URLSearchParams(base);
for (const [key, value] of listPaginationParams(limit, offset)) {
qs.set(key, value);
}
return qs;
}
function applyHiddenMailboxParams(qs, include_spam, include_trash) {
if (include_spam)
qs.set("include_spam", "true");
if (include_trash)
qs.set("include_trash", "true");
}
async function ultiFetch(token, path, init) {
const headers = new Headers(init?.headers);
headers.set("Accept", "application/json");
if (token)
headers.set("Authorization", `Bearer ${token}`);
const res = await fetch(`${API_BASE}${path}`, { ...init, headers });
const text = await res.text();
if (!res.ok) {
throw new Error(`ulti ${path} failed (${res.status}): ${text.slice(0, 500)}`);
}
if (!text)
return { ok: true, status: res.status };
try {
return JSON.parse(text);
}
catch {
return text;
}
}
function resolveToken(req) {
return (String(req.headers["x-ulti-token"] ?? req.headers.authorization ?? "").replace(/^Bearer\s+/i, "") || "");
}
function createServer(getToken, enabledTools) {
const server = new McpServer({
name: "ultimail-mcp",
version: "0.1.0",
});
const allow = (toolName) => !enabledTools || enabledTools.has(toolName);
if (allow("mail_search")) {
server.tool("mail_search", "Search mail messages (spam and trash excluded by default)", {
query: z.string(),
account_id: z.string().optional(),
limit: z.number().optional(),
offset: z.number().optional(),
include_spam: z.boolean().optional(),
include_trash: z.boolean().optional(),
}, async ({ query, account_id, limit, offset, include_spam, include_trash }) => {
const qs = withListPagination(new URLSearchParams({ q: query }), limit, offset);
if (account_id)
qs.set("account_id", account_id);
applyHiddenMailboxParams(qs, include_spam, include_trash);
const data = await ultiFetch(getToken(), `/mail/search?${qs}`);
return toolText(data);
});
}
if (allow("mail_read")) {
server.tool("mail_read", "Read a mail message by id", { message_id: z.string(), account_id: z.string().optional() }, async ({ message_id, account_id }) => {
const qs = account_id ? `?account_id=${encodeURIComponent(account_id)}` : "";
const data = await ultiFetch(getToken(), `/mail/messages/${message_id}${qs}`);
return toolText(data);
});
}
if (allow("mail_list")) {
server.tool("mail_list", "List mail messages in a folder (spam and trash excluded by default unless folder=spam/trash)", {
folder: z.string().optional(),
account_id: z.string().optional(),
limit: z.number().optional(),
offset: z.number().optional(),
include_spam: z.boolean().optional(),
include_trash: z.boolean().optional(),
}, async ({ folder, account_id, limit, offset, include_spam, include_trash }) => {
let qs = new URLSearchParams();
if (folder)
qs.set("folder", folder);
if (account_id)
qs.set("account_id", account_id);
qs = withListPagination(qs, limit, offset);
applyHiddenMailboxParams(qs, include_spam, include_trash);
const suffix = qs.size > 0 ? `?${qs}` : "";
const data = await ultiFetch(getToken(), `/mail/messages${suffix}`);
return toolText(data);
});
}
if (allow("mail_send")) {
server.tool("mail_send", "Send a mail message", {
account_id: z.string(),
to: z.array(z.string()),
cc: z.array(z.string()).optional(),
bcc: z.array(z.string()).optional(),
subject: z.string(),
body_text: z.string().optional(),
body_html: z.string().optional(),
in_reply_to: z.string().optional(),
reply_to_message_id: z.string().optional(),
schedule_at: z.string().optional(),
}, async (body) => {
const data = await ultiFetch(getToken(), "/mail/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
},
body: JSON.stringify(body),
});
return toolText(data);
});
}
if (allow("mail_update_labels")) {
server.tool("mail_update_labels", "Update labels on a mail message", { message_id: z.string(), labels: z.array(z.string()) }, async ({ message_id, labels }) => {
const data = await ultiFetch(getToken(), `/mail/messages/${message_id}/labels`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ labels }),
});
return toolText(data);
});
}
if (allow("mail_update_flags")) {
server.tool("mail_update_flags", "Update flags on a mail message", { message_id: z.string(), flags: z.array(z.string()) }, async ({ message_id, flags }) => {
const data = await ultiFetch(getToken(), `/mail/messages/${message_id}/flags`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ flags }),
});
return toolText(data);
});
}
if (allow("mail_delete")) {
server.tool("mail_delete", "Delete a mail message", { message_id: z.string() }, async ({ message_id }) => {
const data = await ultiFetch(getToken(), `/mail/messages/${message_id}`, {
method: "DELETE",
});
return toolText(data);
});
}
if (allow("drive_list")) {
server.tool("drive_list", "List drive files in a folder", {
path: z.string().optional(),
limit: z.number().optional(),
offset: z.number().optional(),
}, async ({ path, limit, offset }) => {
const encoded = encodePath(path ?? "");
const qs = listPaginationParams(limit, offset);
const suffix = qs.size > 0 ? `?${qs}` : "";
const data = await ultiFetch(getToken(), `/drive/files/${encoded}${suffix}`);
return toolText(data);
});
}
if (allow("drive_file_info")) {
server.tool("drive_file_info", "Get drive file or folder metadata", { path: z.string() }, async ({ path }) => {
const encoded = encodePath(path);
const data = await ultiFetch(getToken(), `/drive/files/info/${encoded}`);
return toolText(data);
});
}
if (allow("drive_create_file")) {
server.tool("drive_create_file", "Create a new office file (document, spreadsheet, presentation, drawing)", {
parent_path: z.string().optional(),
name: z.string(),
kind: z.enum(["document", "spreadsheet", "presentation", "drawing"]),
}, async ({ parent_path, name, kind }) => {
const data = await ultiFetch(getToken(), "/drive/files/new", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ parent_path, name, kind }),
});
return toolText(data);
});
}
if (allow("drive_create_folder")) {
server.tool("drive_create_folder", "Create a drive folder", { path: z.string() }, async ({ path }) => {
const encoded = encodePath(path);
const data = await ultiFetch(getToken(), `/drive/folders/${encoded}`, {
method: "POST",
});
return toolText(data);
});
}
if (allow("drive_delete")) {
server.tool("drive_delete", "Delete a drive file or folder", { path: z.string() }, async ({ path }) => {
const encoded = encodePath(path);
const data = await ultiFetch(getToken(), `/drive/files/${encoded}`, {
method: "DELETE",
});
return toolText(data);
});
}
if (allow("drive_move")) {
server.tool("drive_move", "Move a drive file or folder", {
source: z.string(),
destination: z.string(),
source_root: z.string().optional(),
source_root_id: z.string().optional(),
destination_root: z.string().optional(),
destination_root_id: z.string().optional(),
}, async (body) => {
const data = await ultiFetch(getToken(), "/drive/move", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return toolText(data);
});
}
if (allow("drive_rename")) {
server.tool("drive_rename", "Rename a drive file or folder", {
path: z.string(),
new_name: z.string(),
root: z.string().optional(),
root_id: z.string().optional(),
}, async (body) => {
const data = await ultiFetch(getToken(), "/drive/rename", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return toolText(data);
});
}
if (allow("drive_share_create")) {
server.tool("drive_share_create", "Create a drive share link or internal share", {
path: z.string(),
share_type: z.number().optional(),
permissions: z.number().optional(),
role: z.enum(["owner", "editor", "viewer"]).optional(),
mode: z.enum(["public", "internal", "contact"]).optional(),
share_with: z.string().optional(),
note: z.string().optional(),
root: z.string().optional(),
root_id: z.string().optional(),
}, async (body) => {
const data = await ultiFetch(getToken(), "/drive/shares", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return toolText(data);
});
}
if (allow("drive_share_update")) {
server.tool("drive_share_update", "Update an existing drive share", {
share_id: z.string(),
permissions: z.number().optional(),
role: z.enum(["owner", "editor", "viewer"]).optional(),
expire_date: z.string().optional(),
password: z.string().optional(),
}, async ({ share_id, ...body }) => {
const data = await ultiFetch(getToken(), `/drive/shares/${share_id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return toolText(data);
});
}
if (allow("drive_share_delete")) {
server.tool("drive_share_delete", "Delete a drive share", { share_id: z.string() }, async ({ share_id }) => {
const data = await ultiFetch(getToken(), `/drive/shares/${share_id}`, {
method: "DELETE",
});
return toolText(data);
});
}
if (allow("contacts_search")) {
server.tool("contacts_search", "Search contacts", {
query: z.string(),
book_id: z.string().optional(),
limit: z.number().optional(),
offset: z.number().optional(),
}, async ({ query, book_id, limit, offset }) => {
const qs = withListPagination(new URLSearchParams({ q: query }), limit, offset);
if (book_id)
qs.set("book_id", book_id);
const data = await ultiFetch(getToken(), `/contacts/search?${qs}`);
return toolText(data);
});
}
if (allow("contacts_list_books")) {
server.tool("contacts_list_books", "List address books", {}, async () => {
const data = await ultiFetch(getToken(), "/contacts/books");
return toolText(data);
});
}
if (allow("contacts_list")) {
server.tool("contacts_list", "List contacts in an address book", {
book_id: z.string(),
limit: z.number().optional(),
offset: z.number().optional(),
}, async ({ book_id, limit, offset }) => {
const qs = listPaginationParams(limit, offset);
const suffix = qs.size > 0 ? `?${qs}` : "";
const data = await ultiFetch(getToken(), `/contacts/books/${book_id}${suffix}`);
return toolText(data);
});
}
if (allow("contacts_get")) {
server.tool("contacts_get", "Get a contact by path (book_id/contact_uid)", { path: z.string() }, async ({ path }) => {
const encoded = encodePath(path);
const data = await ultiFetch(getToken(), `/contacts/${encoded}`);
return toolText(data);
});
}
if (allow("contacts_create")) {
server.tool("contacts_create", "Create a contact in an address book", {
book_id: z.string(),
contact: z.record(z.string(), z.unknown()),
}, async ({ book_id, contact }) => {
const data = await ultiFetch(getToken(), `/contacts/books/${book_id}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(contact),
});
return toolText(data);
});
}
if (allow("contacts_update")) {
server.tool("contacts_update", "Update a contact (requires etag from get)", {
path: z.string(),
if_match: z.string(),
contact: z.record(z.string(), z.unknown()),
}, async ({ path, if_match, contact }) => {
const encoded = encodePath(path);
const data = await ultiFetch(getToken(), `/contacts/${encoded}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"If-Match": if_match,
},
body: JSON.stringify(contact),
});
return toolText(data);
});
}
if (allow("contacts_delete")) {
server.tool("contacts_delete", "Delete a contact by path", { path: z.string() }, async ({ path }) => {
const encoded = encodePath(path);
const data = await ultiFetch(getToken(), `/contacts/${encoded}`, {
method: "DELETE",
});
return toolText(data);
});
}
if (allow("suite_search")) {
server.tool("suite_search", "Unified search across mail, drive, contacts (mail spam/trash excluded by default)", {
query: z.string(),
types: z.string().optional(),
limit: z.number().optional(),
offset: z.number().optional(),
include_spam: z.boolean().optional(),
include_trash: z.boolean().optional(),
}, async ({ query, types, limit, offset, include_spam, include_trash }) => {
const qs = withListPagination(new URLSearchParams({ q: query }), limit, offset);
if (types)
qs.set("types", types);
applyHiddenMailboxParams(qs, include_spam, include_trash);
const data = await ultiFetch(getToken(), `/search?${qs}`);
return toolText(data);
});
}
if (allow("web_search")) {
server.tool("web_search", "Search the public web using the configured search provider (Brave, Bing, SearXNG, DuckDuckGo or custom JSON API).", {
query: z.string(),
count: z.number().optional(),
}, async ({ query, count }) => {
const qs = new URLSearchParams({ q: query });
if (count != null && Number.isFinite(count) && count > 0) {
qs.set("count", String(Math.min(Math.trunc(count), 20)));
}
const data = await ultiFetch(getToken(), `/search/web?${qs}`);
return toolText(data);
});
}
if (allow("docs_read")) {
server.tool("docs_read", "Read UltiDocs document JSON (.ultidoc sidecar path)", { path: z.string() }, async ({ path }) => {
const encoded = encodePath(path);
const data = await ultiFetch(getToken(), `/drive/download/${encoded}`);
return toolText(data);
});
}
if (allow("docs_save")) {
server.tool("docs_save", "Save UltiDocs TipTap content to sidecar path", {
path: z.string(),
document: z.record(z.string(), z.unknown()),
}, async ({ path, document }) => {
const data = await ultiFetch(getToken(), "/richtext/save", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path, document }),
});
return toolText(data);
});
}
if (allow("calendar_list")) {
server.tool("calendar_list", "List calendars", {}, async () => {
const data = await ultiFetch(getToken(), "/calendar/");
return toolText(data);
});
}
if (allow("calendar_create")) {
server.tool("calendar_create", "Create a calendar", {
id: z.string().optional(),
display_name: z.string(),
color: z.string().optional(),
}, async (body) => {
const data = await ultiFetch(getToken(), "/calendar/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return toolText(data);
});
}
if (allow("calendar_update")) {
server.tool("calendar_update", "Update calendar display name or color", {
cal_id: z.string(),
display_name: z.string().optional(),
color: z.string().optional(),
}, async ({ cal_id, ...body }) => {
const data = await ultiFetch(getToken(), `/calendar/${cal_id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return toolText(data);
});
}
if (allow("calendar_delete")) {
server.tool("calendar_delete", "Delete a calendar", { cal_id: z.string() }, async ({ cal_id }) => {
const data = await ultiFetch(getToken(), `/calendar/${cal_id}`, {
method: "DELETE",
});
return toolText(data);
});
}
if (allow("calendar_list_events")) {
server.tool("calendar_list_events", "List events in a calendar", {
cal_id: z.string(),
limit: z.number().optional(),
offset: z.number().optional(),
}, async ({ cal_id, limit, offset }) => {
const qs = listPaginationParams(limit, offset);
const suffix = qs.size > 0 ? `?${qs}` : "";
const data = await ultiFetch(getToken(), `/calendar/${cal_id}/events${suffix}`);
return toolText(data);
});
}
if (allow("calendar_freebusy")) {
server.tool("calendar_freebusy", "Query free/busy for attendees in a time range", {
start: z.string(),
end: z.string(),
attendees: z.array(z.string()),
}, async (body) => {
const data = await ultiFetch(getToken(), "/calendar/freebusy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return toolText(data);
});
}
if (allow("calendar_create_event")) {
server.tool("calendar_create_event", "Create a calendar event", {
cal_id: z.string(),
event: z.record(z.string(), z.unknown()),
}, async ({ cal_id, event }) => {
const data = await ultiFetch(getToken(), `/calendar/${cal_id}/events`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(event),
});
return toolText(data);
});
}
if (allow("calendar_update_event")) {
server.tool("calendar_update_event", "Update a calendar event (requires If-Match etag)", {
event_path: z.string(),
if_match: z.string(),
event: z.record(z.string(), z.unknown()),
}, async ({ event_path, if_match, event }) => {
const encoded = encodePath(event_path);
const data = await ultiFetch(getToken(), `/calendar/events/${encoded}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"If-Match": if_match,
},
body: JSON.stringify(event),
});
return toolText(data);
});
}
if (allow("calendar_delete_event")) {
server.tool("calendar_delete_event", "Delete a calendar event", { event_path: z.string() }, async ({ event_path }) => {
const encoded = encodePath(event_path);
const data = await ultiFetch(getToken(), `/calendar/events/${encoded}`, {
method: "DELETE",
});
return toolText(data);
});
}
if (allow("calendar_respond_invitation")) {
server.tool("calendar_respond_invitation", "Respond to a calendar invitation", {
event_path: z.string(),
email: z.string().optional(),
response: z.enum(["accepted", "declined", "tentative", "needs-action"]),
if_match: z.string().optional(),
}, async ({ event_path, ...body }) => {
const encoded = encodePath(event_path);
const data = await ultiFetch(getToken(), `/calendar/events/response/${encoded}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return toolText(data);
});
}
if (allow("calendar_create_meet_link")) {
server.tool("calendar_create_meet_link", "Create a video meeting link for an event", {
event_path: z.string(),
if_match: z.string().optional(),
}, async ({ event_path, if_match }) => {
const encoded = encodePath(event_path);
const data = await ultiFetch(getToken(), `/calendar/events/meet-link/${encoded}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ if_match }),
});
return toolText(data);
});
}
if (enabledTools && enabledTools.size === 0) {
console.warn("ultimail-mcp: no tools enabled for groups", [...enabledTools]);
}
else if (enabledTools) {
const disabled = ALL_TOOL_NAMES.filter((name) => !enabledTools.has(name));
if (disabled.length > 0) {
console.log("ultimail-mcp disabled tools:", disabled.join(", "));
}
}
return server;
}
const app = express();
app.use(express.json({ limit: "4mb" }));
const streamableTransports = new Map();
const sseTransports = new Map();
app.get("/health", (_req, res) => {
res.json({ ok: true, tools: ALL_TOOL_NAMES });
});
app.all("/mcp", async (req, res) => {
const token = resolveToken(req);
const enabledTools = parseEnabledTools(String(req.headers["x-ulti-enabled-tools"] ?? ""));
try {
const sessionId = String(req.headers["mcp-session-id"] ?? "");
let transport;
if (sessionId && streamableTransports.has(sessionId)) {
transport = streamableTransports.get(sessionId);
}
else if (!sessionId && req.method === "POST" && isInitializeRequest(req.body)) {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sid) => {
if (transport)
streamableTransports.set(sid, transport);
},
});
transport.onclose = () => {
const sid = transport?.sessionId;
if (sid)
streamableTransports.delete(sid);
};
const server = createServer(() => token, enabledTools);
await server.connect(transport);
}
else {
res.status(400).json({
jsonrpc: "2.0",
error: { code: -32000, message: "Bad Request: No valid session ID provided" },
id: null,
});
return;
}
await transport.handleRequest(req, res, req.body);
}
catch (error) {
console.error("streamable mcp error:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
}
}
});
app.get("/sse", async (req, res) => {
const token = resolveToken(req);
const enabledTools = parseEnabledTools(String(req.headers["x-ulti-enabled-tools"] ?? ""));
const transport = new SSEServerTransport("/messages", res);
sseTransports.set(transport.sessionId, transport);
res.on("close", () => sseTransports.delete(transport.sessionId));
const server = createServer(() => token, enabledTools);
await server.connect(transport);
});
app.post("/messages", async (req, res) => {
const sessionId = String(req.query.sessionId ?? "");
const transport = sseTransports.get(sessionId);
if (!transport || !(transport instanceof SSEServerTransport)) {
res.status(404).json({ error: "session not found" });
return;
}
await transport.handlePostMessage(req, res, req.body);
});
app.listen(PORT, () => {
console.log(`ultimail-mcp listening on :${PORT} (streamable /mcp, legacy /sse)`);
});