@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.
178 lines (177 loc) • 8.47 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 __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const strict_1 = __importDefault(require("node:assert/strict"));
const node_test_1 = require("node:test");
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const notes_1 = require("./notes");
class TestTransport {
start() {
return __awaiter(this, void 0, void 0, function* () { });
}
send(message) {
return __awaiter(this, void 0, void 0, function* () {
queueMicrotask(() => { var _a, _b; return (_b = (_a = this.peer) === null || _a === void 0 ? void 0 : _a.onmessage) === null || _b === void 0 ? void 0 : _b.call(_a, message); });
});
}
close() {
return __awaiter(this, void 0, void 0, function* () {
var _a;
(_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
});
}
}
function createTransportPair() {
const clientTransport = new TestTransport();
const serverTransport = new TestTransport();
clientTransport.peer = serverTransport;
serverTransport.peer = clientTransport;
return { clientTransport, serverTransport };
}
function parseToolText(result) {
var _a;
const item = (_a = result.content) === null || _a === void 0 ? void 0 : _a[0];
if (!item || item.type !== "text") {
throw new Error("Expected text tool response");
}
return JSON.parse(item.text);
}
function createNoteApi(notes = []) {
const calls = {
getDocumentNotes: [],
createDocumentNote: [],
deleteDocumentNote: [],
};
const api = {
getDocumentNotes: (id) => __awaiter(this, void 0, void 0, function* () {
calls.getDocumentNotes.push(id);
return notes;
}),
createDocumentNote: (id, note) => __awaiter(this, void 0, void 0, function* () {
calls.createDocumentNote.push([id, note]);
return notes;
}),
deleteDocumentNote: (id, noteId) => __awaiter(this, void 0, void 0, function* () {
calls.deleteDocumentNote.push([id, noteId]);
return notes;
}),
};
return { api, calls };
}
function withNoteClient(api, run) {
return __awaiter(this, void 0, void 0, function* () {
const server = new mcp_js_1.McpServer({ name: "paperless-note-test", version: "1.0.0" });
(0, notes_1.registerNoteTools)(server, api);
const client = new index_js_1.Client({
name: "paperless-note-test-client",
version: "1.0.0",
});
const { clientTransport, serverTransport } = createTransportPair();
yield server.connect(serverTransport);
yield client.connect(clientTransport);
try {
yield run(client);
}
finally {
yield client.close();
yield server.close();
}
});
}
(0, node_test_1.describe)("document note tools", () => {
const sampleNotes = [
{
id: 5,
note: "Paid 2026-06-30",
created: "2026-06-30T10:00:00Z",
user: { id: 3, username: "nick" },
},
];
(0, node_test_1.test)("create_document_note posts the note text to the document", () => __awaiter(void 0, void 0, void 0, function* () {
const { api, calls } = createNoteApi(sampleNotes);
yield withNoteClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const result = (yield client.callTool({
name: "create_document_note",
arguments: { id: 1740, note: "Antwort an Finanzamt versendet" },
}));
strict_1.default.ok(!result.isError, (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error);
strict_1.default.deepEqual(parseToolText(result), sampleNotes);
}));
strict_1.default.deepEqual(calls.createDocumentNote, [
[1740, "Antwort an Finanzamt versendet"],
]);
}));
(0, node_test_1.test)("list_document_notes fetches notes for the document", () => __awaiter(void 0, void 0, void 0, function* () {
const { api, calls } = createNoteApi(sampleNotes);
yield withNoteClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const result = (yield client.callTool({
name: "list_document_notes",
arguments: { id: 42 },
}));
strict_1.default.ok(!result.isError, (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error);
strict_1.default.deepEqual(parseToolText(result), sampleNotes);
}));
strict_1.default.deepEqual(calls.getDocumentNotes, [42]);
}));
(0, node_test_1.test)("delete_document_note removes a note by its note ID when confirmed", () => __awaiter(void 0, void 0, void 0, function* () {
const { api, calls } = createNoteApi([]);
yield withNoteClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const result = (yield client.callTool({
name: "delete_document_note",
arguments: { id: 42, note_id: 5, confirm: true },
}));
strict_1.default.ok(!result.isError, (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error);
}));
strict_1.default.deepEqual(calls.deleteDocumentNote, [[42, 5]]);
}));
(0, node_test_1.test)("delete_document_note refuses to delete without confirmation", () => __awaiter(void 0, void 0, void 0, function* () {
const { api, calls } = createNoteApi([]);
yield withNoteClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b;
const result = (yield client.callTool({
name: "delete_document_note",
arguments: { id: 42, note_id: 5, confirm: false },
}));
strict_1.default.ok(result.isError, "expected an error when confirm is false");
strict_1.default.match((_b = (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error) !== null && _b !== void 0 ? _b : "", /confirm/i);
}));
strict_1.default.equal(calls.deleteDocumentNote.length, 0, "no delete should be sent without confirmation");
}));
(0, node_test_1.test)("create_document_note rejects an empty note", () => __awaiter(void 0, void 0, void 0, function* () {
const { api, calls } = createNoteApi(sampleNotes);
yield withNoteClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
// Depending on the installed MCP SDK version, an input-schema (zod)
// violation either rejects with a protocol error (-32602) or resolves
// with a CallToolResult carrying isError: true. Accept both so the test
// stays correct across SDK versions.
let rejected = false;
let result;
try {
result = (yield client.callTool({
name: "create_document_note",
arguments: { id: 1, note: "" },
}));
}
catch (_a) {
rejected = true;
}
strict_1.default.ok(rejected || (result === null || result === void 0 ? void 0 : result.isError), "expected an empty note to be rejected");
}));
strict_1.default.equal(calls.createDocumentNote.length, 0, "no note should be created when validation fails");
}));
});