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.

318 lines (317 loc) 16 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 documentEnhancer_1 = require("../api/documentEnhancer"); const empty_1 = require("./utils/empty"); const middlewares_1 = require("./utils/middlewares"); function registerDocumentTools(server, api) { server.tool("bulk_edit_documents", "Perform bulk operations on multiple documents. Note: 'remove_tag' removes a tag from specific documents (tag remains in system), while 'delete_tag' permanently deletes a tag from the entire system. ⚠️ WARNING: 'delete' method permanently deletes documents and requires confirmation.", { 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", "modify_custom_fields", "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().transform(empty_1.arrayNotEmpty), remove_tags: zod_1.z.array(zod_1.z.number()).optional().transform(empty_1.arrayNotEmpty), add_custom_fields: zod_1.z .array(zod_1.z.object({ field: zod_1.z.number(), value: zod_1.z.union([ zod_1.z.string(), zod_1.z.number(), zod_1.z.boolean(), zod_1.z.array(zod_1.z.number()), zod_1.z.null(), ]), })) .optional() .transform(empty_1.arrayNotEmpty), remove_custom_fields: zod_1.z .array(zod_1.z.number()) .optional() .transform(empty_1.arrayNotEmpty), 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() .transform(empty_1.objectNotEmpty), 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(), confirm: zod_1.z .boolean() .optional() .describe("Must be true when method is 'delete' to confirm destructive operation"), }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () { if (!api) throw new Error("Please configure API connection first"); if (args.method === "delete" && !args.confirm) { throw new Error("Confirmation required for destructive operation. Set confirm: true to proceed."); } const { documents, method, add_custom_fields } = args, parameters = __rest(args, ["documents", "method", "add_custom_fields"]); // Transform add_custom_fields into the two separate API parameters const apiParameters = Object.assign({}, parameters); if (add_custom_fields && add_custom_fields.length > 0) { apiParameters.assign_custom_fields = add_custom_fields.map((cf) => cf.field); apiParameters.assign_custom_fields_values = add_custom_fields; } const response = yield api.bulkEditDocuments(documents, method, apiParameters); return { content: [ { type: "text", text: JSON.stringify({ result: response.result || response }), }, ], }; }))); server.tool("post_document", "Upload a new document to Paperless-NGX with optional metadata like title, correspondent, document type, tags, and custom fields.", { 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.withErrorHandling)((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. Note: Document content is excluded from results by default. Use 'get_document_content' to retrieve content when needed.", { 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__date__gte: zod_1.z.string().optional(), created__date__lte: zod_1.z.string().optional(), ordering: zod_1.z.string().optional(), }, (0, middlewares_1.withErrorHandling)((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__date__gte) query.set("created__date__gte", args.created__date__gte); if (args.created__date__lte) query.set("created__date__lte", args.created__date__lte); if (args.ordering) query.set("ordering", args.ordering); const docsResponse = yield api.getDocuments(query.toString() ? `?${query.toString()}` : ""); return (0, documentEnhancer_1.convertDocsWithNames)(docsResponse, api); }))); server.tool("get_document", "Get a specific document by ID with full details including correspondent, document type, tags, and custom fields. Note: Document content is excluded from results by default. Use 'get_document_content' to retrieve content when needed.", { id: zod_1.z.number(), }, (0, middlewares_1.withErrorHandling)((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); return (0, documentEnhancer_1.convertDocsWithNames)(doc, api); }))); server.tool("get_document_content", "Get the text content of a specific document by ID. Use this when you need to read or analyze the actual document text.", { id: zod_1.z.number(), }, (0, middlewares_1.withErrorHandling)((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); return { content: [ { type: "text", text: JSON.stringify({ id: doc.id, title: doc.title, content: doc.content, }), }, ], }; }))); 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. Note: Document content is excluded from results by default. Use 'get_document_content' to retrieve content when needed.", { query: zod_1.z.string(), }, (0, middlewares_1.withErrorHandling)((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 (0, documentEnhancer_1.convertDocsWithNames)(docsResponse, api); }))); server.tool("download_document", "Download a document file by ID. Returns the document as a base64-encoded resource.", { id: zod_1.z.number(), original: zod_1.z.boolean().optional(), }, (0, middlewares_1.withErrorHandling)((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", }, }, ], }; }))); server.tool("update_document", "Update a specific document with new values. This tool allows you to modify any document field including title, correspondent, document type, storage path, tags, custom fields, and more. Only the fields you specify will be updated.", { id: zod_1.z.number().describe("The ID of the document to update"), title: zod_1.z .string() .max(128) .optional() .describe("The new title for the document (max 128 characters)"), correspondent: zod_1.z .number() .nullable() .optional() .describe("The ID of the correspondent to assign"), document_type: zod_1.z .number() .nullable() .optional() .describe("The ID of the document type to assign"), storage_path: zod_1.z .number() .nullable() .optional() .describe("The ID of the storage path to assign"), tags: zod_1.z .array(zod_1.z.number()) .optional() .describe("Array of tag IDs to assign to the document"), content: zod_1.z .string() .optional() .describe("The raw text content of the document (used for searching)"), created: zod_1.z .string() .optional() .describe("The creation date in YYYY-MM-DD format"), archive_serial_number: zod_1.z .number() .optional() .describe("The archive serial number (0-4294967295)"), owner: zod_1.z .number() .nullable() .optional() .describe("The ID of the user who owns the document"), custom_fields: zod_1.z .array(zod_1.z.object({ field: zod_1.z.number().describe("The custom field ID"), value: zod_1.z .union([ zod_1.z.string(), zod_1.z.number(), zod_1.z.boolean(), zod_1.z.array(zod_1.z.number()), zod_1.z.null(), ]) .describe("The value for the custom field. For documentlink fields, use a single document ID (e.g., 123) or an array of document IDs (e.g., [123, 456])."), })) .optional() .describe("Array of custom field values to assign"), }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () { if (!api) throw new Error("Please configure API connection first"); const { id } = args, updateData = __rest(args, ["id"]); const response = yield api.updateDocument(id, updateData); return (0, documentEnhancer_1.convertDocsWithNames)(response, api); }))); }