@baruchiro/paperless-mcp 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ export declare class PaperlessAPI {
6
6
  constructor(baseUrl: string, token: string);
7
7
  request<T = any>(path: string, options?: RequestInit): Promise<T>;
8
8
  bulkEditDocuments(documents: number[], method: string, parameters?: BulkEditParameters): Promise<BulkEditDocumentsResult>;
9
- postDocument(file: File, metadata?: Record<string, string | string[] | number | number[]>): Promise<string>;
9
+ postDocument(document: Buffer, filename: string, metadata?: Record<string, string | string[] | number | number[]>): Promise<string>;
10
10
  getDocuments(query?: string): Promise<DocumentsResponse>;
11
11
  getDocument(id: number): Promise<Document>;
12
12
  updateDocument(id: number, data: Partial<Document>): Promise<Document>;
@@ -79,10 +79,10 @@ class PaperlessAPI {
79
79
  });
80
80
  });
81
81
  }
82
- postDocument(file_1) {
83
- return __awaiter(this, arguments, void 0, function* (file, metadata = {}) {
82
+ postDocument(document_1, filename_1) {
83
+ return __awaiter(this, arguments, void 0, function* (document, filename, metadata = {}) {
84
84
  const formData = new form_data_1.default();
85
- formData.append("document", file);
85
+ formData.append("document", document, { filename });
86
86
  // Add optional metadata fields
87
87
  if (metadata.title)
88
88
  formData.append("title", metadata.title);
@@ -98,10 +98,10 @@ class PaperlessAPI {
98
98
  metadata.tags.forEach((tag) => formData.append("tags", tag));
99
99
  }
100
100
  if (metadata.archive_serial_number) {
101
- formData.append("archive_serial_number", metadata.archive_serial_number);
101
+ formData.append("archive_serial_number", String(metadata.archive_serial_number));
102
102
  }
103
103
  if (metadata.custom_fields) {
104
- metadata.custom_fields.forEach((field) => formData.append("custom_fields", field));
104
+ metadata.custom_fields.forEach((field) => formData.append("custom_fields", String(field)));
105
105
  }
106
106
  const response = yield axios_1.default.post(`${this.baseUrl}/api/documents/post_document/`, formData, {
107
107
  headers: Object.assign({ Authorization: `Token ${this.token}` }, formData.getHeaders()),
@@ -55,7 +55,7 @@ function registerCustomFieldTools(server, api) {
55
55
  content: [{ type: "text", text: JSON.stringify(response) }],
56
56
  };
57
57
  })));
58
- server.tool("create_custom_field", "Create a new custom field with a specified data type (string, url, date, boolean, integer, float, monetary, documentlink, or select).", {
58
+ server.tool("create_custom_field", "Create a new custom field with a specified data type (string, url, date, boolean, integer, float, monetary, documentlink, or select). For monetary fields, values must use currency code prefix format (e.g., USD10.00, GBP123.45) — NOT trailing symbol format (e.g., 10.00$).", {
59
59
  name: zod_1.z.string(),
60
60
  data_type: zod_1.z.enum([
61
61
  "string",
@@ -25,6 +25,8 @@ const zod_1 = require("zod");
25
25
  const documentEnhancer_1 = require("../api/documentEnhancer");
26
26
  const empty_1 = require("./utils/empty");
27
27
  const middlewares_1 = require("./utils/middlewares");
28
+ const monetary_1 = require("./utils/monetary");
29
+ const descriptions_1 = require("./utils/descriptions");
28
30
  function registerDocumentTools(server, api) {
29
31
  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.", {
30
32
  documents: zod_1.z.array(zod_1.z.number()),
@@ -59,7 +61,7 @@ function registerDocumentTools(server, api) {
59
61
  zod_1.z.boolean(),
60
62
  zod_1.z.array(zod_1.z.number()),
61
63
  zod_1.z.null(),
62
- ]),
64
+ ]).describe(descriptions_1.CUSTOM_FIELD_VALUE_DESCRIPTION),
63
65
  }))
64
66
  .optional()
65
67
  .transform(empty_1.arrayNotEmpty),
@@ -101,6 +103,7 @@ function registerDocumentTools(server, api) {
101
103
  throw new Error("Confirmation required for destructive operation. Set confirm: true to proceed.");
102
104
  }
103
105
  const { documents, method, add_custom_fields } = args, parameters = __rest(args, ["documents", "method", "add_custom_fields"]);
106
+ (0, monetary_1.validateCustomFields)(add_custom_fields);
104
107
  // Transform add_custom_fields into the two separate API parameters
105
108
  const apiParameters = Object.assign({}, parameters);
106
109
  if (add_custom_fields && add_custom_fields.length > 0) {
@@ -126,16 +129,19 @@ function registerDocumentTools(server, api) {
126
129
  document_type: zod_1.z.number().optional(),
127
130
  storage_path: zod_1.z.number().optional(),
128
131
  tags: zod_1.z.array(zod_1.z.number()).optional(),
129
- archive_serial_number: zod_1.z.string().optional(),
132
+ archive_serial_number: zod_1.z.number().optional(),
130
133
  custom_fields: zod_1.z.array(zod_1.z.number()).optional(),
131
134
  }, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
132
135
  if (!api)
133
136
  throw new Error("Please configure API connection first");
134
- const binaryData = Buffer.from(args.file, "base64");
135
- const blob = new Blob([binaryData]);
136
- const file = new File([blob], args.filename);
137
- const { file: _, filename: __ } = args, metadata = __rest(args, ["file", "filename"]);
138
- const response = yield api.postDocument(file, metadata);
137
+ // Validate base64 input
138
+ const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
139
+ if (!base64Regex.test(args.file)) {
140
+ throw new Error("Invalid base64-encoded file data. Please provide a valid base64 string.");
141
+ }
142
+ const { file, filename } = args, metadata = __rest(args, ["file", "filename"]);
143
+ const document = Buffer.from(file, "base64");
144
+ const response = yield api.postDocument(document, filename, metadata);
139
145
  let result;
140
146
  if (typeof response === "string" && /^\d+$/.test(response)) {
141
147
  result = { id: Number(response) };
@@ -322,7 +328,7 @@ function registerDocumentTools(server, api) {
322
328
  zod_1.z.array(zod_1.z.number()),
323
329
  zod_1.z.null(),
324
330
  ])
325
- .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])."),
331
+ .describe(descriptions_1.CUSTOM_FIELD_VALUE_DESCRIPTION),
326
332
  }))
327
333
  .optional()
328
334
  .describe("Array of custom field values to assign"),
@@ -330,6 +336,7 @@ function registerDocumentTools(server, api) {
330
336
  if (!api)
331
337
  throw new Error("Please configure API connection first");
332
338
  const { id } = args, updateData = __rest(args, ["id"]);
339
+ (0, monetary_1.validateCustomFields)(updateData.custom_fields);
333
340
  const response = yield api.updateDocument(id, updateData);
334
341
  return (0, documentEnhancer_1.convertDocsWithNames)(response, api);
335
342
  })));
@@ -0,0 +1 @@
1
+ export declare const CUSTOM_FIELD_VALUE_DESCRIPTION = "The value for the custom field. For monetary fields, use currency code prefix format (e.g., USD10.00, GBP123.45, EUR9.99) \u2014 NOT trailing symbol format (e.g., 10.00$). For documentlink fields, use a single document ID (e.g., 123) or an array of document IDs (e.g., [123, 456]).";
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CUSTOM_FIELD_VALUE_DESCRIPTION = void 0;
4
+ exports.CUSTOM_FIELD_VALUE_DESCRIPTION = "The value for the custom field. For monetary fields, use currency code prefix format (e.g., USD10.00, GBP123.45, EUR9.99) — NOT trailing symbol format (e.g., 10.00$). For documentlink fields, use a single document ID (e.g., 123) or an array of document IDs (e.g., [123, 456]).";
@@ -0,0 +1,5 @@
1
+ export declare function getMonetaryValidationError(value: string): string | null;
2
+ export declare function validateCustomFields(custom_fields: {
3
+ field: number;
4
+ value: unknown;
5
+ }[] | undefined): void;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMonetaryValidationError = getMonetaryValidationError;
4
+ exports.validateCustomFields = validateCustomFields;
5
+ const SYMBOL_TO_CODE = {
6
+ $: "USD",
7
+ "€": "EUR",
8
+ "£": "GBP",
9
+ "¥": "JPY",
10
+ "₹": "INR",
11
+ "₪": "ILS",
12
+ };
13
+ const TRAILING_SYMBOL_REGEX = new RegExp(`^(\\d+(?:\\.\\d+)?)[${Object.keys(SYMBOL_TO_CODE).join("")}]$`);
14
+ function getMonetaryValidationError(value) {
15
+ const trailingMatch = TRAILING_SYMBOL_REGEX.exec(value);
16
+ if (trailingMatch) {
17
+ const amount = trailingMatch[1];
18
+ const symbol = value.slice(-1);
19
+ const code = SYMBOL_TO_CODE[symbol] || "USD";
20
+ const numericAmount = parseFloat(amount);
21
+ const formattedAmount = isNaN(numericAmount) ? amount : numericAmount.toFixed(2);
22
+ return (`Invalid monetary format "${value}". ` +
23
+ `Paperless-NGX requires the currency code as a prefix, e.g. "${code}${formattedAmount}". ` +
24
+ `Use the format: {CURRENCY_CODE}{amount} (e.g., USD10.00, GBP123.45, EUR9.99).`);
25
+ }
26
+ return null;
27
+ }
28
+ function validateCustomFields(custom_fields) {
29
+ custom_fields === null || custom_fields === void 0 ? void 0 : custom_fields.filter((cf) => typeof cf.value === "string").forEach((cf) => {
30
+ const monetaryError = getMonetaryValidationError(cf.value);
31
+ if (monetaryError)
32
+ throw new Error(monetaryError);
33
+ });
34
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = require("node:test");
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const monetary_1 = require("./monetary");
9
+ (0, node_test_1.test)("returns null for non-monetary strings", () => {
10
+ strict_1.default.equal((0, monetary_1.getMonetaryValidationError)("hello"), null);
11
+ strict_1.default.equal((0, monetary_1.getMonetaryValidationError)("some text"), null);
12
+ strict_1.default.equal((0, monetary_1.getMonetaryValidationError)(""), null);
13
+ });
14
+ (0, node_test_1.test)("returns null for valid monetary prefix format", () => {
15
+ strict_1.default.equal((0, monetary_1.getMonetaryValidationError)("USD10.00"), null);
16
+ strict_1.default.equal((0, monetary_1.getMonetaryValidationError)("GBP123.45"), null);
17
+ strict_1.default.equal((0, monetary_1.getMonetaryValidationError)("EUR9.99"), null);
18
+ strict_1.default.equal((0, monetary_1.getMonetaryValidationError)("ILS50.00"), null);
19
+ });
20
+ (0, node_test_1.test)("returns error for trailing currency symbol", () => {
21
+ const err = (0, monetary_1.getMonetaryValidationError)("10.00$");
22
+ strict_1.default.ok(err);
23
+ strict_1.default.match(err, /USD10\.00/);
24
+ strict_1.default.match(err, /currency code as a prefix/);
25
+ });
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@baruchiro/paperless-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "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.",
5
5
  "main": "build/index.js",
6
6
  "bin": {
7
7
  "paperless-mcp": "build/index.js"
8
8
  },
9
9
  "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1",
10
+ "test": "node --require ts-node/register --test src/**/*.test.ts",
11
11
  "start": "ts-node src/index.ts",
12
12
  "build": "tsc",
13
13
  "dxt-pack": "dxt pack",
package/paperless-mcp.dxt CHANGED
Binary file