@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
JavaScript
#!/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));