UNPKG

@baruchiro/paperless-mcp

Version:

Model Context Protocol (MCP) server for interacting with Paperless-NGX document management system. Enables AI assistants to manage documents, tags, correspondents, and document types through the Paperless-NGX API.

172 lines (165 loc) 8.3 kB
#!/usr/bin/env node "use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js"); const express_1 = __importDefault(require("express")); const node_util_1 = require("node:util"); const PaperlessAPI_1 = require("./api/PaperlessAPI"); const correspondents_1 = require("./tools/correspondents"); const customFields_1 = require("./tools/customFields"); const documents_1 = require("./tools/documents"); const documentTypes_1 = require("./tools/documentTypes"); const tags_1 = require("./tools/tags"); const { values: { baseUrl, token, http: useHttp, port, publicUrl }, } = (0, node_util_1.parseArgs)({ options: { baseUrl: { type: "string" }, token: { type: "string" }, http: { type: "boolean", default: false }, port: { type: "string" }, publicUrl: { type: "string", default: "" }, }, allowPositionals: true, }); const resolvedBaseUrl = baseUrl || process.env.PAPERLESS_URL; const resolvedToken = token || process.env.PAPERLESS_API_KEY; const resolvedPublicUrl = publicUrl || process.env.PAPERLESS_PUBLIC_URL || resolvedBaseUrl; const resolvedPort = port ? parseInt(port, 10) : 3000; if (!resolvedBaseUrl || !resolvedToken) { console.error("Usage: paperless-mcp --baseUrl <url> --token <token> [--http] [--port <port>] [--publicUrl <url>]"); console.error("Or set PAPERLESS_URL and PAPERLESS_API_KEY environment variables."); process.exit(1); } function main() { return __awaiter(this, void 0, void 0, function* () { // Initialize API client and server once const api = new PaperlessAPI_1.PaperlessAPI(resolvedBaseUrl, resolvedToken); const server = new mcp_js_1.McpServer({ name: "paperless-ngx", version: "1.0.0" }, { instructions: ` Paperless-NGX MCP Server Instructions ⚠️ CRITICAL: Always differentiate between operations on specific documents vs operations on the entire system: - REMOVE operations (e.g., remove_tag in bulk_edit_documents): Affect only the specified documents, items remain in the system - DELETE operations (e.g., delete_tag, delete_correspondent): Permanently delete items from the entire system, affecting ALL documents that use them When a user asks to "remove" something, prefer operations that affect specific documents. Only use DELETE operations when explicitly asked to delete from the system. To view documents in your Paperless-NGX web interface, construct URLs using this pattern: ${resolvedPublicUrl}/documents/{document_id}/ Example: If your base URL is "http://localhost:8000", the web interface URL would be "http://localhost:8000/documents/123/" for document ID 123. The document tools return JSON data with document IDs that you can use to construct these URLs. `, }); (0, documents_1.registerDocumentTools)(server, api); (0, tags_1.registerTagTools)(server, api); (0, correspondents_1.registerCorrespondentTools)(server, api); (0, documentTypes_1.registerDocumentTypeTools)(server, api); (0, customFields_1.registerCustomFieldTools)(server, api); if (useHttp) { const app = (0, express_1.default)(); app.use(express_1.default.json()); // Store transports for each session const sseTransports = {}; app.post("/mcp", (req, res) => __awaiter(this, void 0, void 0, function* () { try { const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); res.on("close", () => { transport.close(); }); yield server.connect(transport); yield transport.handleRequest(req, res, req.body); } catch (error) { console.error("Error handling MCP request:", error); if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error", }, id: null, }); } } })); app.get("/mcp", (req, res) => __awaiter(this, void 0, void 0, function* () { res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed.", }, id: null, })); })); app.delete("/mcp", (req, res) => __awaiter(this, void 0, void 0, function* () { res.writeHead(405).end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed.", }, id: null, })); })); app.get("/sse", (req, res) => __awaiter(this, void 0, void 0, function* () { console.log("SSE request received"); try { const transport = new sse_js_1.SSEServerTransport("/messages", res); sseTransports[transport.sessionId] = transport; res.on("close", () => { delete sseTransports[transport.sessionId]; transport.close(); }); yield server.connect(transport); } catch (error) { console.error("Error handling SSE request:", error); if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error", }, id: null, }); } } })); app.post("/messages", (req, res) => __awaiter(this, void 0, void 0, function* () { const sessionId = req.query.sessionId; const transport = sseTransports[sessionId]; if (transport) { yield transport.handlePostMessage(req, res, req.body); } else { res.status(400).send("No transport found for sessionId"); } })); app.listen(resolvedPort, () => { console.log(`MCP Stateless Streamable HTTP Server listening on port ${resolvedPort}`); }); // await new Promise((resolve) => setTimeout(resolve, 1000000)); } else { const transport = new stdio_js_1.StdioServerTransport(); yield server.connect(transport); } }); } main().catch((e) => console.error(e.message));