From a84e2f72c3d2bca9ee3862142ee222711584bf9b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 1 Apr 2026 11:19:10 +0300 Subject: [PATCH 01/82] feat(llm/mcp): first implementation --- apps/server/package.json | 1 + apps/server/src/app.ts | 9 +- apps/server/src/routes/mcp.ts | 55 +++++++ apps/server/src/services/mcp/mcp_server.ts | 73 +++++++++ pnpm-lock.yaml | 167 +++++++++++++-------- 5 files changed, 242 insertions(+), 63 deletions(-) create mode 100644 apps/server/src/routes/mcp.ts create mode 100644 apps/server/src/services/mcp/mcp_server.ts diff --git a/apps/server/package.json b/apps/server/package.json index cb7f728fd7..6daa90a45b 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -33,6 +33,7 @@ "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/google": "3.0.54", "@ai-sdk/openai": "3.0.49", + "@modelcontextprotocol/sdk": "^1.12.1", "ai": "6.0.142", "better-sqlite3": "12.8.0", "html-to-text": "9.0.5", diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index bb9097ba63..ffd33600dd 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -14,6 +14,7 @@ import favicon from "serve-favicon"; import assets from "./routes/assets.js"; import custom from "./routes/custom.js"; import error_handlers from "./routes/error_handlers.js"; +import mcpRoutes from "./routes/mcp.js"; import routes from "./routes/routes.js"; import config from "./services/config.js"; import { startScheduledCleanup } from "./services/erase.js"; @@ -58,8 +59,8 @@ export default async function buildApp() { app.use(compression({ // Skip compression for SSE endpoints to enable real-time streaming filter: (req, res) => { - // Skip compression for LLM chat streaming endpoint - if (req.path === "/api/llm-chat/stream") { + // Skip compression for SSE-capable endpoints + if (req.path === "/api/llm-chat/stream" || req.path === "/mcp") { return false; } return compression.filter(req, res); @@ -90,6 +91,10 @@ export default async function buildApp() { app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); + // MCP is registered before session/auth middleware — it uses its own + // localhost-only guard and does not require Trilium authentication. + mcpRoutes.register(app); + app.use(express.static(path.join(publicDir, "root"))); app.use(`/manifest.webmanifest`, express.static(path.join(publicAssetsDir, "manifest.webmanifest"))); app.use(`/robots.txt`, express.static(path.join(publicAssetsDir, "robots.txt"))); diff --git a/apps/server/src/routes/mcp.ts b/apps/server/src/routes/mcp.ts new file mode 100644 index 0000000000..9e5f82b73a --- /dev/null +++ b/apps/server/src/routes/mcp.ts @@ -0,0 +1,55 @@ +/** + * MCP (Model Context Protocol) HTTP route handler. + * + * Mounts the Streamable HTTP transport at `/mcp` with a localhost-only guard. + * No authentication is required — access is restricted to loopback addresses. + */ + +import type express from "express"; + +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; + +import { createMcpServer } from "../services/mcp/mcp_server.js"; +import log from "../services/log.js"; + +const LOCALHOST_ADDRESSES = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]); + +function localhostOnly(req: express.Request, res: express.Response, next: express.NextFunction) { + if (LOCALHOST_ADDRESSES.has(req.socket.remoteAddress ?? "")) { + next(); + } else { + res.status(403).json({ error: "MCP is only available from localhost" }); + } +} + +async function handleMcpRequest(req: express.Request, res: express.Response) { + try { + const server = createMcpServer(); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined // stateless + }); + + res.on("close", () => { + transport.close(); + server.close(); + }); + + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (err) { + log.error(`MCP request error: ${err}`); + if (!res.headersSent) { + res.status(500).json({ error: "Internal MCP error" }); + } + } +} + +export function register(app: express.Application) { + app.post("/mcp", localhostOnly, handleMcpRequest); + app.get("/mcp", localhostOnly, handleMcpRequest); + app.delete("/mcp", localhostOnly, handleMcpRequest); + + log.info("MCP server registered at /mcp (localhost only)"); +} + +export default { register }; diff --git a/apps/server/src/services/mcp/mcp_server.ts b/apps/server/src/services/mcp/mcp_server.ts new file mode 100644 index 0000000000..876e71cf3d --- /dev/null +++ b/apps/server/src/services/mcp/mcp_server.ts @@ -0,0 +1,73 @@ +/** + * MCP (Model Context Protocol) server for Trilium Notes. + * + * Exposes existing LLM tools via the MCP protocol so external AI agents + * (e.g. Claude Desktop) can interact with Trilium. + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +import appInfo from "../app_info.js"; +import cls from "../cls.js"; +import sql from "../sql.js"; +import { noteTools } from "../llm/tools/note_tools.js"; +import { attributeTools } from "../llm/tools/attribute_tools.js"; +import { hierarchyTools } from "../llm/tools/hierarchy_tools.js"; +import { skillTools } from "../llm/skills/index.js"; + +import type { Tool } from "@ai-sdk/provider-utils"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +/** + * Register an AI SDK tool on the MCP server. + * + * Bridges between the Vercel AI SDK `tool()` shape and the MCP SDK's + * `registerTool()` API. Write operations are wrapped in CLS + transaction + * context so that Becca entity tracking works correctly. + */ +function registerAiTool( + server: McpServer, + name: string, + aiTool: Tool, + { mutates = false }: { mutates?: boolean } = {} +) { + server.registerTool(name, { + description: aiTool.description, + inputSchema: aiTool.inputSchema + }, async (args: any): Promise => { + const run = () => aiTool.execute!(args, {} as any); + const result = mutates + ? await cls.init(() => sql.transactional(run)) + : await run(); + return { content: [{ type: "text", text: JSON.stringify(result) }] }; + }); +} + +export function createMcpServer(): McpServer { + const server = new McpServer({ + name: "trilium-notes", + version: appInfo.appVersion + }); + + // Note tools + registerAiTool(server, "search_notes", noteTools.search_notes); + registerAiTool(server, "read_note", noteTools.read_note); + registerAiTool(server, "update_note_content", noteTools.update_note_content, { mutates: true }); + registerAiTool(server, "append_to_note", noteTools.append_to_note, { mutates: true }); + registerAiTool(server, "create_note", noteTools.create_note, { mutates: true }); + + // Attribute tools + registerAiTool(server, "get_attributes", attributeTools.get_attributes); + registerAiTool(server, "get_attribute", attributeTools.get_attribute); + registerAiTool(server, "set_attribute", attributeTools.set_attribute, { mutates: true }); + registerAiTool(server, "delete_attribute", attributeTools.delete_attribute, { mutates: true }); + + // Hierarchy tools + registerAiTool(server, "get_child_notes", hierarchyTools.get_child_notes); + registerAiTool(server, "get_subtree", hierarchyTools.get_subtree); + + // Skill tools + registerAiTool(server, "load_skill", skillTools.load_skill); + + return server; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0930c06901..2568451254 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -565,6 +565,9 @@ importers: '@ai-sdk/openai': specifier: 3.0.49 version: 3.0.49(zod@4.3.6) + '@modelcontextprotocol/sdk': + specifier: ^1.12.1 + version: 1.29.0(zod@4.3.6) ai: specifier: 6.0.142 version: 6.0.142(zod@4.3.6) @@ -3414,6 +3417,12 @@ packages: '@hapi/topo@5.1.0': resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + '@hono/node-server@1.19.12': + resolution: {integrity: sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -4018,6 +4027,16 @@ packages: '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: '>=4.0.0' + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@mswjs/interceptors@0.37.6': resolution: {integrity: sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==} engines: {node: '>=18'} @@ -9546,6 +9565,10 @@ packages: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@1.0.0: resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} engines: {node: '>=6'} @@ -10280,6 +10303,10 @@ packages: hoist-non-react-statics@2.5.5: resolution: {integrity: sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==} + hono@4.12.9: + resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==} + engines: {node: '>=16.9.0'} + hookable@6.0.1: resolution: {integrity: sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==} @@ -10918,10 +10945,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isexe@3.1.1: - resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} - engines: {node: '>=16'} - isexe@3.1.5: resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} engines: {node: '>=18'} @@ -10975,6 +10998,9 @@ packages: resolution: {integrity: sha512-5hFWIigKqC+e/lRyQhfnirrAqUdIPMB7SJRqflJaO29dW7q5DFvH1XCSTmv6PQ6pb++0k6MJlLRoS0Wv4s38Wg==} engines: {node: '>=10.13.0 < 13 || >=13.7.0'} + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + jotai-scope@0.7.2: resolution: {integrity: sha512-Gwed97f3dDObrO43++2lRcgOqw4O2sdr4JCjP/7eHK1oPACDJ7xKHGScpJX9XaflU+KBHXF+VhwECnzcaQiShg==} peerDependencies: @@ -11082,6 +11108,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -12792,6 +12821,10 @@ packages: resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==} hasBin: true + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -16033,6 +16066,11 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: '>=4.0.0' + zod@4.1.12: resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} @@ -16906,6 +16944,8 @@ snapshots: '@ckeditor/ckeditor5-core': 47.6.1 '@ckeditor/ckeditor5-upload': 47.6.1 ckeditor5: 47.6.1 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-ai@47.6.1(bufferutil@4.0.9)(utf-8-validate@6.0.5)': dependencies: @@ -17047,14 +17087,14 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.6.1 '@ckeditor/ckeditor5-widget': 47.6.1 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-cloud-services@47.6.1': dependencies: '@ckeditor/ckeditor5-core': 47.6.1 '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-code-block@47.6.1(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': dependencies: @@ -17241,6 +17281,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-classic@47.6.1': dependencies: @@ -17250,6 +17292,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-decoupled@47.6.1': dependencies: @@ -17259,6 +17303,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-inline@47.6.1': dependencies: @@ -17304,8 +17350,6 @@ snapshots: ckeditor5: 47.6.1 es-toolkit: 1.39.5 fuzzysort: 3.1.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-engine@47.6.1': dependencies: @@ -17348,8 +17392,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.6.1 '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-export-word@47.6.1': dependencies: @@ -17383,8 +17425,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.6.1 '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-footnotes@47.6.1': dependencies: @@ -17415,8 +17455,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.6.1 '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-heading@47.6.1': dependencies: @@ -17427,8 +17465,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.6.1 '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-highlight@47.6.1': dependencies: @@ -17438,8 +17474,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.6.1 '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-horizontal-line@47.6.1': dependencies: @@ -17449,8 +17483,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.6.1 '@ckeditor/ckeditor5-widget': 47.6.1 ckeditor5: 47.6.1 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-html-embed@47.6.1': dependencies: @@ -17460,8 +17492,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.6.1 '@ckeditor/ckeditor5-widget': 47.6.1 ckeditor5: 47.6.1 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-html-support@47.6.1': dependencies: @@ -17477,8 +17507,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.6.1 ckeditor5: 47.6.1 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-icons@47.6.1': {} @@ -17496,8 +17524,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.6.1 ckeditor5: 47.6.1 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-import-word@47.6.1': dependencies: @@ -17510,8 +17536,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.6.1 '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-indent@47.6.1': dependencies: @@ -17523,8 +17547,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.6.1 '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-inspector@5.0.0': {} @@ -17535,8 +17557,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.6.1 '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-line-height@47.6.1': dependencies: @@ -17561,8 +17581,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.6.1 ckeditor5: 47.6.1 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-list-multi-level@47.6.1': dependencies: @@ -17587,8 +17605,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-markdown-gfm@47.6.1': dependencies: @@ -17626,8 +17642,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.6.1 '@ckeditor/ckeditor5-widget': 47.6.1 ckeditor5: 47.6.1 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-mention@47.6.1(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)': dependencies: @@ -17651,8 +17665,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.6.1 ckeditor5: 47.6.1 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-minimap@47.6.1': dependencies: @@ -17661,6 +17673,8 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.6.1 '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-operations-compressor@47.6.1': dependencies: @@ -17713,8 +17727,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.6.1 '@ckeditor/ckeditor5-widget': 47.6.1 ckeditor5: 47.6.1 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-pagination@47.6.1': dependencies: @@ -17741,6 +17753,8 @@ snapshots: '@ckeditor/ckeditor5-paste-from-office': 47.6.1 '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-paste-from-office@47.6.1': dependencies: @@ -17748,6 +17762,8 @@ snapshots: '@ckeditor/ckeditor5-core': 47.6.1 '@ckeditor/ckeditor5-engine': 47.6.1 ckeditor5: 47.6.1 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-real-time-collaboration@47.6.1(bufferutil@4.0.9)(utf-8-validate@6.0.5)': dependencies: @@ -17822,8 +17838,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.6.1 '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-slash-command@47.6.1': dependencies: @@ -17836,8 +17850,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.6.1 '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-source-editing-enhanced@47.6.1': dependencies: @@ -17885,8 +17897,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-table@47.6.1': dependencies: @@ -17899,8 +17909,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.6.1 ckeditor5: 47.6.1 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-template@47.6.1': dependencies: @@ -18010,8 +18018,6 @@ snapshots: '@ckeditor/ckeditor5-engine': 47.6.1 '@ckeditor/ckeditor5-utils': 47.6.1 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-widget@47.6.1': dependencies: @@ -18031,8 +18037,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.6.1 ckeditor5: 47.6.1 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@codemirror/autocomplete@6.18.6': dependencies: @@ -19327,6 +19331,10 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 + '@hono/node-server@1.19.12(hono@4.12.9)': + dependencies: + hono: 4.12.9 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -20040,6 +20048,28 @@ snapshots: '@mixmark-io/domino@2.2.0': {} + '@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.12(hono@4.12.9) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.2(express@5.2.1) + hono: 4.12.9 + jose: 6.2.2 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.2(zod@4.3.6) + transitivePeerDependencies: + - supports-color + '@mswjs/interceptors@0.37.6': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -24649,6 +24679,10 @@ snapshots: optionalDependencies: ajv: 8.13.0 + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv-keywords@3.5.2(ajv@6.14.0): dependencies: ajv: 6.14.0 @@ -25472,8 +25506,6 @@ snapshots: ckeditor5-collaboration@47.6.1: dependencies: '@ckeditor/ckeditor5-collaboration-core': 47.6.1 - transitivePeerDependencies: - - supports-color ckeditor5-premium-features@47.6.1(bufferutil@4.0.9)(ckeditor5@47.6.1)(utf-8-validate@6.0.5): dependencies: @@ -27373,6 +27405,10 @@ snapshots: eventsource-parser@3.0.6: {} + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + execa@1.0.0: dependencies: cross-spawn: 6.0.6 @@ -28353,6 +28389,8 @@ snapshots: hoist-non-react-statics@2.5.5: {} + hono@4.12.9: {} + hookable@6.0.1: {} hookified@1.15.0: {} @@ -28618,7 +28656,6 @@ snapshots: iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 - optional: true icss-utils@5.1.0(postcss@8.5.8): dependencies: @@ -28977,8 +29014,6 @@ snapshots: isexe@2.0.0: {} - isexe@3.1.1: {} - isexe@3.1.5: {} isexe@4.0.0: {} @@ -29061,6 +29096,8 @@ snapshots: dependencies: '@panva/asn1.js': 1.0.0 + jose@6.2.2: {} + jotai-scope@0.7.2(jotai@2.11.0(@types/react@19.1.7)(react@19.2.4))(react@19.2.4): dependencies: jotai: 2.11.0(@types/react@19.1.7)(react@19.2.4) @@ -29183,6 +29220,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-schema@0.4.0: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -31304,6 +31343,8 @@ snapshots: dependencies: pngjs: 6.0.0 + pkce-challenge@5.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -31845,7 +31886,7 @@ snapshots: dependencies: bytes: 3.1.2 http-errors: 2.0.1 - iconv-lite: 0.7.0 + iconv-lite: 0.7.2 unpipe: 1.0.0 raw-loader@0.5.1: {} @@ -34849,7 +34890,7 @@ snapshots: which@5.0.0: dependencies: - isexe: 3.1.1 + isexe: 3.1.5 which@6.0.1: dependencies: @@ -35128,6 +35169,10 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 + zod-to-json-schema@3.25.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod@4.1.12: {} zod@4.3.6: {} From a5793ff768b50f0c8acc9578162026604b06e9c0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 1 Apr 2026 11:29:29 +0300 Subject: [PATCH 02/82] chore(mcp): add MCP config for localhost --- .mcp.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .mcp.json diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000000..9fd17b8d67 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "trilium": { + "type": "http", + "url": "http://localhost:8080/mcp" + } + } +} From 23ccbf9642680e425a06a8da3354b43fdd389a3b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 1 Apr 2026 11:30:47 +0300 Subject: [PATCH 03/82] chore(llm): add instructions for MCP use --- .github/copilot-instructions.md | 6 ++++++ CLAUDE.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b4dfb29f7f..108341ed8e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -213,6 +213,12 @@ When adding query parameters to ETAPI endpoints (`apps/server/src/etapi/`), main 10. **Attribute inheritance can be complex** - When checking for labels/relations, use `note.getOwnedAttribute()` for direct attributes or `note.getAttribute()` for inherited ones. Don't assume attributes are directly on the note. +## MCP Server +- Trilium exposes an MCP (Model Context Protocol) server at `http://localhost:8080/mcp`, configured in `.mcp.json` +- The MCP server is **only available when the Trilium server is running** (`pnpm run server:start`) +- It provides tools for reading, searching, and modifying notes directly from the AI assistant +- Use it to interact with actual note data when developing or debugging note-related features + ## TypeScript Configuration - **Project references**: Monorepo uses TypeScript project references (`tsconfig.json`) diff --git a/CLAUDE.md b/CLAUDE.md index a818b18929..71a82f39f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -161,6 +161,12 @@ Trilium provides powerful user scripting capabilities: - **Do not use `import.meta.url`/`fileURLToPath`** to resolve file paths — the server is bundled into CJS for production, so `import.meta.url` will not point to the source directory - **Do not use `__dirname` with relative paths** from source files — after bundling, `__dirname` points to the bundle output, not the original source tree +## MCP Server +- Trilium exposes an MCP (Model Context Protocol) server at `http://localhost:8080/mcp`, configured in `.mcp.json` +- The MCP server is **only available when the Trilium server is running** (`pnpm run server:start`) +- It provides tools for reading, searching, and modifying notes directly from the AI assistant +- Use it to interact with actual note data when developing or debugging note-related features + ## Build System Notes - Uses pnpm for monorepo management - Vite for fast development builds From 63d4b8894b4d687e279f60a01472f5e9b43074a0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 1 Apr 2026 11:44:01 +0300 Subject: [PATCH 04/82] feat(llm): gate MCP access behind option --- .../src/translations/en/translation.json | 4 +++- .../src/widgets/type_widgets/options/llm.tsx | 23 ++++++++++++++++++- apps/server/src/routes/api/options.ts | 3 ++- apps/server/src/routes/mcp.ts | 23 ++++++++++++------- apps/server/src/services/options_init.ts | 3 ++- packages/commons/src/lib/options_interface.ts | 2 ++ 6 files changed, 46 insertions(+), 12 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 0ee627e9da..a398f9036a 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2305,6 +2305,8 @@ "delete_provider_confirmation": "Are you sure you want to delete the provider \"{{name}}\"?", "api_key": "API Key", "api_key_placeholder": "Enter your API key", - "cancel": "Cancel" + "cancel": "Cancel", + "mcp_enabled": "Enable MCP server", + "mcp_enabled_description": "Expose a Model Context Protocol (MCP) endpoint so that AI coding assistants (e.g. Claude Code, GitHub Copilot) can read and modify your notes. The endpoint is only accessible from localhost." } } diff --git a/apps/client/src/widgets/type_widgets/options/llm.tsx b/apps/client/src/widgets/type_widgets/options/llm.tsx index caa867f5fd..34d22ee928 100644 --- a/apps/client/src/widgets/type_widgets/options/llm.tsx +++ b/apps/client/src/widgets/type_widgets/options/llm.tsx @@ -1,11 +1,13 @@ import { useCallback, useMemo, useState } from "preact/hooks"; import { t } from "../../../services/i18n"; import Button from "../../react/Button"; +import FormCheckbox from "../../react/FormCheckbox"; +import FormGroup from "../../react/FormGroup"; import OptionsSection from "./components/OptionsSection"; import AddProviderModal, { type LlmProviderConfig, PROVIDER_TYPES } from "./llm/AddProviderModal"; import ActionButton from "../../react/ActionButton"; import dialog from "../../../services/dialog"; -import { useTriliumOption } from "../../react/hooks"; +import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; export default function LlmSettings() { const [providersJson, setProvidersJson] = useTriliumOption("llmProviders"); @@ -56,10 +58,29 @@ export default function LlmSettings() { onHidden={() => setShowAddModal(false)} onSave={handleAddProvider} /> + +
+ + ); } +function McpSettings() { + const [mcpEnabled, setMcpEnabled] = useTriliumOptionBool("mcpEnabled"); + + return ( + + + + ); +} + interface ProviderListProps { providers: LlmProviderConfig[]; onDelete: (providerId: string, providerName: string) => Promise; diff --git a/apps/server/src/routes/api/options.ts b/apps/server/src/routes/api/options.ts index f2e44a3e2e..f0fabbd8b9 100644 --- a/apps/server/src/routes/api/options.ts +++ b/apps/server/src/routes/api/options.ts @@ -105,7 +105,8 @@ const ALLOWED_OPTIONS = new Set([ "newLayout", "mfaEnabled", "mfaMethod", - "llmProviders" + "llmProviders", + "mcpEnabled" ]); function getOptions() { diff --git a/apps/server/src/routes/mcp.ts b/apps/server/src/routes/mcp.ts index 9e5f82b73a..b9ebad7e2c 100644 --- a/apps/server/src/routes/mcp.ts +++ b/apps/server/src/routes/mcp.ts @@ -11,15 +11,22 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/ import { createMcpServer } from "../services/mcp/mcp_server.js"; import log from "../services/log.js"; +import optionService from "../services/options.js"; const LOCALHOST_ADDRESSES = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]); -function localhostOnly(req: express.Request, res: express.Response, next: express.NextFunction) { - if (LOCALHOST_ADDRESSES.has(req.socket.remoteAddress ?? "")) { - next(); - } else { - res.status(403).json({ error: "MCP is only available from localhost" }); +function mcpGuard(req: express.Request, res: express.Response, next: express.NextFunction) { + if (!optionService.getOptionBool("mcpEnabled")) { + res.status(403).json({ error: "MCP server is disabled. Enable it in Options > AI / LLM." }); + return; } + + if (!LOCALHOST_ADDRESSES.has(req.socket.remoteAddress ?? "")) { + res.status(403).json({ error: "MCP is only available from localhost" }); + return; + } + + next(); } async function handleMcpRequest(req: express.Request, res: express.Response) { @@ -45,9 +52,9 @@ async function handleMcpRequest(req: express.Request, res: express.Response) { } export function register(app: express.Application) { - app.post("/mcp", localhostOnly, handleMcpRequest); - app.get("/mcp", localhostOnly, handleMcpRequest); - app.delete("/mcp", localhostOnly, handleMcpRequest); + app.post("/mcp", mcpGuard, handleMcpRequest); + app.get("/mcp", mcpGuard, handleMcpRequest); + app.delete("/mcp", mcpGuard, handleMcpRequest); log.info("MCP server registered at /mcp (localhost only)"); } diff --git a/apps/server/src/services/options_init.ts b/apps/server/src/services/options_init.ts index 43f20e54f2..51ba85553d 100644 --- a/apps/server/src/services/options_init.ts +++ b/apps/server/src/services/options_init.ts @@ -212,7 +212,8 @@ const defaultOptions: DefaultOption[] = [ { name: "experimentalFeatures", value: "[]", isSynced: true }, // AI / LLM - { name: "llmProviders", value: "[]", isSynced: false } + { name: "llmProviders", value: "[]", isSynced: false }, + { name: "mcpEnabled", value: "false", isSynced: false } ]; /** diff --git a/packages/commons/src/lib/options_interface.ts b/packages/commons/src/lib/options_interface.ts index 334a8bd7a4..1f57ba8f47 100644 --- a/packages/commons/src/lib/options_interface.ts +++ b/packages/commons/src/lib/options_interface.ts @@ -144,6 +144,8 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions Date: Wed, 1 Apr 2026 11:46:54 +0300 Subject: [PATCH 05/82] feat(llm): improve MCP settings card --- .../src/translations/en/translation.json | 1 + .../src/widgets/type_widgets/options/llm.tsx | 50 +++++++++---------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index a398f9036a..04ca75b6cf 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2306,6 +2306,7 @@ "api_key": "API Key", "api_key_placeholder": "Enter your API key", "cancel": "Cancel", + "mcp_title": "MCP (Model Context Protocol)", "mcp_enabled": "Enable MCP server", "mcp_enabled_description": "Expose a Model Context Protocol (MCP) endpoint so that AI coding assistants (e.g. Claude Code, GitHub Copilot) can read and modify your notes. The endpoint is only accessible from localhost." } diff --git a/apps/client/src/widgets/type_widgets/options/llm.tsx b/apps/client/src/widgets/type_widgets/options/llm.tsx index 34d22ee928..83793be0ac 100644 --- a/apps/client/src/widgets/type_widgets/options/llm.tsx +++ b/apps/client/src/widgets/type_widgets/options/llm.tsx @@ -2,7 +2,6 @@ import { useCallback, useMemo, useState } from "preact/hooks"; import { t } from "../../../services/i18n"; import Button from "../../react/Button"; import FormCheckbox from "../../react/FormCheckbox"; -import FormGroup from "../../react/FormGroup"; import OptionsSection from "./components/OptionsSection"; import AddProviderModal, { type LlmProviderConfig, PROVIDER_TYPES } from "./llm/AddProviderModal"; import ActionButton from "../../react/ActionButton"; @@ -35,34 +34,34 @@ export default function LlmSettings() { }, [providers, setProviders]); return ( - -

{t("llm.settings_description")}

+ <> + +

{t("llm.settings_description")}

-