@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
JavaScript
;
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),
},
],
};
});
}