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.

282 lines (281 loc) 13.1 kB
"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 __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.registerDocumentTools = registerDocumentTools; const zod_1 = require("zod"); const middlewares_1 = require("./utils/middlewares"); function registerDocumentTools(server, api) { server.tool("bulk_edit_documents", { documents: zod_1.z.array(zod_1.z.number()), method: zod_1.z.enum([ "set_correspondent", "set_document_type", "set_storage_path", "add_tag", "remove_tag", "modify_tags", "delete", "reprocess", "set_permissions", "merge", "split", "rotate", "delete_pages", ]), correspondent: zod_1.z.number().optional(), document_type: zod_1.z.number().optional(), storage_path: zod_1.z.number().optional(), tag: zod_1.z.number().optional(), add_tags: zod_1.z.array(zod_1.z.number()).optional(), remove_tags: zod_1.z.array(zod_1.z.number()).optional(), permissions: zod_1.z .object({ owner: zod_1.z.number().nullable().optional(), set_permissions: zod_1.z .object({ view: zod_1.z.object({ users: zod_1.z.array(zod_1.z.number()), groups: zod_1.z.array(zod_1.z.number()), }), change: zod_1.z.object({ users: zod_1.z.array(zod_1.z.number()), groups: zod_1.z.array(zod_1.z.number()), }), }) .optional(), merge: zod_1.z.boolean().optional(), }) .optional(), metadata_document_id: zod_1.z.number().optional(), delete_originals: zod_1.z.boolean().optional(), pages: zod_1.z.string().optional(), degrees: zod_1.z.number().optional(), }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () { if (!api) throw new Error("Please configure API connection first"); const { documents, method } = args, parameters = __rest(args, ["documents", "method"]); const response = yield api.bulkEditDocuments(documents, method, parameters); return { content: [ { type: "text", text: JSON.stringify({ result: response.result || response }), }, ], }; }))); server.tool("post_document", { file: zod_1.z.string(), filename: zod_1.z.string(), title: zod_1.z.string().optional(), created: zod_1.z.string().optional(), correspondent: zod_1.z.number().optional(), document_type: zod_1.z.number().optional(), storage_path: zod_1.z.number().optional(), tags: zod_1.z.array(zod_1.z.number()).optional(), archive_serial_number: zod_1.z.string().optional(), custom_fields: zod_1.z.array(zod_1.z.number()).optional(), }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () { if (!api) throw new Error("Please configure API connection first"); const binaryData = Buffer.from(args.file, "base64"); const blob = new Blob([binaryData]); const file = new File([blob], args.filename); const { file: _, filename: __ } = args, metadata = __rest(args, ["file", "filename"]); const response = yield api.postDocument(file, metadata); let result; if (typeof response === "string" && /^\d+$/.test(response)) { result = { id: Number(response) }; } else { result = { status: response }; } return { content: [ { type: "text", text: JSON.stringify(result), }, ], }; }))); server.tool("list_documents", "List and filter documents by fields such as title, correspondent, document type, tag, storage path, creation date, and more. IMPORTANT: For queries like 'the last 3 contributions' or when searching by tag, correspondent, document type, or storage path, you should FIRST use the relevant tool (e.g., 'list_tags', 'list_correspondents', 'list_document_types', 'list_storage_paths') to find the correct ID, and then use that ID as a filter here. Only use the 'search' argument for free-text search when no specific field applies. Using the correct ID filter will yield much more accurate results.", { page: zod_1.z.number().optional(), page_size: zod_1.z.number().optional(), search: zod_1.z.string().optional(), correspondent: zod_1.z.number().optional(), document_type: zod_1.z.number().optional(), tag: zod_1.z.number().optional(), storage_path: zod_1.z.number().optional(), created__gte: zod_1.z.string().optional(), created__lte: zod_1.z.string().optional(), ordering: zod_1.z.string().optional(), }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () { if (!api) throw new Error("Please configure API connection first"); const query = new URLSearchParams(); if (args.page) query.set("page", args.page.toString()); if (args.page_size) query.set("page_size", args.page_size.toString()); if (args.search) query.set("search", args.search); if (args.correspondent) query.set("correspondent__id", args.correspondent.toString()); if (args.document_type) query.set("document_type__id", args.document_type.toString()); if (args.tag) query.set("tags__id", args.tag.toString()); if (args.storage_path) query.set("storage_path__id", args.storage_path.toString()); if (args.created__gte) query.set("created__gte", args.created__gte); if (args.created__lte) query.set("created__lte", args.created__lte); if (args.ordering) query.set("ordering", args.ordering); const docsResponse = yield api.getDocuments(query.toString() ? `?${query.toString()}` : ""); return convertDocsWithNames(docsResponse, api); }))); server.tool("get_document", { id: zod_1.z.number(), }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () { if (!api) throw new Error("Please configure API connection first"); const doc = yield api.getDocument(args.id); const [correspondents, documentTypes, tags] = yield Promise.all([ api.getCorrespondents(), api.getDocumentTypes(), api.getTags(), ]); const correspondentMap = new Map((correspondents.results || []).map((c) => [c.id, c.name])); const documentTypeMap = new Map((documentTypes.results || []).map((dt) => [dt.id, dt.name])); const tagMap = new Map((tags.results || []).map((tag) => [tag.id, tag.name])); const docWithNames = Object.assign(Object.assign({}, doc), { correspondent: doc.correspondent ? { id: doc.correspondent, name: correspondentMap.get(doc.correspondent) || String(doc.correspondent), } : null, document_type: doc.document_type ? { id: doc.document_type, name: documentTypeMap.get(doc.document_type) || String(doc.document_type), } : null, tags: Array.isArray(doc.tags) ? doc.tags.map((tagId) => ({ id: tagId, name: tagMap.get(tagId) || String(tagId), })) : doc.tags }); return { content: [ { type: "text", text: JSON.stringify(docWithNames), }, ], }; }))); server.tool("search_documents", "Full text search for documents. This tool is for searching document content, title, and metadata using a full text query. For general document listing or filtering by fields, use 'list_documents' instead.", { query: zod_1.z.string(), }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () { if (!api) throw new Error("Please configure API connection first"); const docsResponse = yield api.searchDocuments(args.query); return convertDocsWithNames(docsResponse, api); }))); server.tool("download_document", { id: zod_1.z.number(), original: zod_1.z.boolean().optional(), }, (0, middlewares_1.errorMiddleware)((args, extra) => __awaiter(this, void 0, void 0, function* () { var _a, _b; if (!api) throw new Error("Please configure API connection first"); const response = yield api.downloadDocument(args.id, args.original); const filename = ((_b = (_a = (typeof response.headers.get === "function" ? response.headers.get("content-disposition") : response.headers["content-disposition"])) === null || _a === void 0 ? void 0 : _a.split("filename=")[1]) === null || _b === void 0 ? void 0 : _b.replace(/"/g, "")) || `document-${args.id}`; return { content: [ { type: "resource", resource: { uri: filename, blob: Buffer.from(response.data).toString("base64"), mimeType: "application/pdf", }, }, ], }; }))); } function convertDocsWithNames(docsResponse, api) { return __awaiter(this, void 0, void 0, function* () { var _a; if (!((_a = docsResponse.results) === null || _a === void 0 ? void 0 : _a.length)) { return { content: [ { type: "text", text: "No documents found", }, ], }; } // Fetch all related entities for name mapping const [correspondents, documentTypes, tags] = yield Promise.all([ api.getCorrespondents(), api.getDocumentTypes(), api.getTags(), ]); const correspondentMap = new Map((correspondents.results || []).map((c) => [c.id, c.name])); const documentTypeMap = new Map((documentTypes.results || []).map((dt) => [dt.id, dt.name])); const tagMap = new Map((tags.results || []).map((tag) => [tag.id, tag.name])); const docsWithNames = docsResponse.results.map((doc) => (Object.assign(Object.assign({}, doc), { correspondent: doc.correspondent ? { id: doc.correspondent, name: correspondentMap.get(doc.correspondent) || String(doc.correspondent), } : null, document_type: doc.document_type ? { id: doc.document_type, name: documentTypeMap.get(doc.document_type) || String(doc.document_type), } : null, tags: Array.isArray(doc.tags) ? doc.tags.map((tagId) => ({ id: tagId, name: tagMap.get(tagId) || String(tagId), })) : doc.tags }))); return { content: [ { type: "text", text: JSON.stringify(docsWithNames), }, ], }; }); }