@baruchiro/paperless-mcp 2.0.1 → 2.1.0
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.
- package/README.md +3 -1
- package/build/api/types.d.ts +10 -12
- package/build/tools/documents.js +44 -14
- package/build/tools/documents.test.js +159 -0
- package/package.json +1 -1
- package/paperless-mcp.dxt +0 -0
package/README.md
CHANGED
|
@@ -252,7 +252,9 @@ Parameters:
|
|
|
252
252
|
- tag: ID for add_tag/remove_tag
|
|
253
253
|
- add_tags: Array of tag IDs for modify_tags
|
|
254
254
|
- remove_tags: Array of tag IDs for modify_tags
|
|
255
|
-
-
|
|
255
|
+
- set_permissions: Object for set_permissions with view/change users and groups (`{"view": {"users": [], "groups": []}, "change": {...}}`). Omitted actions/lists are left untouched
|
|
256
|
+
- owner: User ID (or null to remove) for set_permissions. Unless merge is true, omitting owner clears the current owner
|
|
257
|
+
- merge: Boolean for set_permissions — true adds to existing permissions and keeps the owner; false (default) replaces the listed users/groups
|
|
256
258
|
- metadata_document_id: ID for merge to specify metadata source
|
|
257
259
|
- delete_originals: Boolean for merge/split
|
|
258
260
|
- pages: String for split "[1,2-3,4,5-7]" or delete_pages "[2,3,4]"
|
package/build/api/types.d.ts
CHANGED
|
@@ -188,18 +188,16 @@ export interface BulkEditParameters {
|
|
|
188
188
|
document_type?: number;
|
|
189
189
|
storage_path?: number;
|
|
190
190
|
tag?: number;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
users: number[];
|
|
200
|
-
groups: number[];
|
|
201
|
-
};
|
|
191
|
+
set_permissions?: {
|
|
192
|
+
view?: {
|
|
193
|
+
users?: number[];
|
|
194
|
+
groups?: number[];
|
|
195
|
+
};
|
|
196
|
+
change?: {
|
|
197
|
+
users?: number[];
|
|
198
|
+
groups?: number[];
|
|
202
199
|
};
|
|
203
|
-
merge?: boolean;
|
|
204
200
|
};
|
|
201
|
+
owner?: number | null;
|
|
202
|
+
merge?: boolean;
|
|
205
203
|
}
|
package/build/tools/documents.js
CHANGED
|
@@ -164,25 +164,37 @@ function registerDocumentTools(server, api) {
|
|
|
164
164
|
.array(zod_1.z.number())
|
|
165
165
|
.optional()
|
|
166
166
|
.transform(empty_1.arrayNotEmpty),
|
|
167
|
-
|
|
167
|
+
set_permissions: zod_1.z
|
|
168
168
|
.object({
|
|
169
|
-
|
|
170
|
-
set_permissions: zod_1.z
|
|
169
|
+
view: zod_1.z
|
|
171
170
|
.object({
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
groups: zod_1.z.array(zod_1.z.number()),
|
|
175
|
-
}),
|
|
176
|
-
change: zod_1.z.object({
|
|
177
|
-
users: zod_1.z.array(zod_1.z.number()),
|
|
178
|
-
groups: zod_1.z.array(zod_1.z.number()),
|
|
179
|
-
}),
|
|
171
|
+
users: zod_1.z.array(zod_1.z.number()).optional(),
|
|
172
|
+
groups: zod_1.z.array(zod_1.z.number()).optional(),
|
|
180
173
|
})
|
|
174
|
+
.strict()
|
|
175
|
+
.optional(),
|
|
176
|
+
change: zod_1.z
|
|
177
|
+
.object({
|
|
178
|
+
users: zod_1.z.array(zod_1.z.number()).optional(),
|
|
179
|
+
groups: zod_1.z.array(zod_1.z.number()).optional(),
|
|
180
|
+
})
|
|
181
|
+
.strict()
|
|
181
182
|
.optional(),
|
|
182
|
-
merge: zod_1.z.boolean().optional(),
|
|
183
183
|
})
|
|
184
|
+
// strict: a misspelled action ("read") must not silently collapse
|
|
185
|
+
// into {} and clear permissions/ownership.
|
|
186
|
+
.strict()
|
|
187
|
+
.optional()
|
|
188
|
+
.describe("For set_permissions: view/change permissions to apply. Omitted actions (view/change) and omitted users/groups lists are left untouched; an empty list [] removes all (unless merge is true). Omit entirely for owner-only changes."),
|
|
189
|
+
owner: zod_1.z
|
|
190
|
+
.number()
|
|
191
|
+
.nullable()
|
|
192
|
+
.optional()
|
|
193
|
+
.describe("For set_permissions: new owner user ID, or null to remove the owner. Unless merge is true, omitting owner also clears the current owner."),
|
|
194
|
+
merge: zod_1.z
|
|
195
|
+
.boolean()
|
|
184
196
|
.optional()
|
|
185
|
-
.
|
|
197
|
+
.describe("For set_permissions: true adds to existing permissions and keeps the current owner; false (default) replaces the listed users/groups"),
|
|
186
198
|
metadata_document_id: zod_1.z.number().optional(),
|
|
187
199
|
delete_originals: zod_1.z.boolean().optional(),
|
|
188
200
|
pages: zod_1.z.string().optional(),
|
|
@@ -192,12 +204,23 @@ function registerDocumentTools(server, api) {
|
|
|
192
204
|
.optional()
|
|
193
205
|
.describe("Must be true when method is 'delete' to confirm destructive operation"),
|
|
194
206
|
}, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
|
|
207
|
+
var _a;
|
|
195
208
|
if (!api)
|
|
196
209
|
throw new Error("Please configure API connection first");
|
|
197
210
|
if (args.method === "delete" && !args.confirm) {
|
|
198
211
|
throw new Error("Confirmation required for destructive operation. Set confirm: true to proceed.");
|
|
199
212
|
}
|
|
213
|
+
if (args.method === "set_permissions" &&
|
|
214
|
+
!args.set_permissions &&
|
|
215
|
+
args.owner === undefined) {
|
|
216
|
+
throw new Error("Method 'set_permissions' requires set_permissions and/or owner.");
|
|
217
|
+
}
|
|
200
218
|
const { documents, method, add_custom_fields, confirm } = args, parameters = __rest(args, ["documents", "method", "add_custom_fields", "confirm"]);
|
|
219
|
+
if (method === "set_permissions") {
|
|
220
|
+
// Paperless rejects (Paperless <= 3.0.5: crashes with a 500 on) a missing
|
|
221
|
+
// set_permissions key even for owner-only changes.
|
|
222
|
+
(_a = parameters.set_permissions) !== null && _a !== void 0 ? _a : (parameters.set_permissions = {});
|
|
223
|
+
}
|
|
201
224
|
(0, monetary_1.validateCustomFields)(add_custom_fields);
|
|
202
225
|
const resolvedCustomFields = yield (0, selectFields_1.resolveSelectCustomFieldValues)(api, add_custom_fields, "stored");
|
|
203
226
|
const response = yield api.bulkEditDocuments(documents, method, method === "delete"
|
|
@@ -378,6 +401,9 @@ function registerDocumentTools(server, api) {
|
|
|
378
401
|
});
|
|
379
402
|
return {
|
|
380
403
|
content: [
|
|
404
|
+
// Legacy clients surface only content[].text and drop resource blocks
|
|
405
|
+
// entirely, so the URI is repeated here to stay reachable (issue #134).
|
|
406
|
+
{ type: "text", text: uri },
|
|
381
407
|
{
|
|
382
408
|
type: "resource",
|
|
383
409
|
resource: {
|
|
@@ -397,12 +423,16 @@ function registerDocumentTools(server, api) {
|
|
|
397
423
|
}, (0, middlewares_1.withErrorHandling)((args, extra) => __awaiter(this, void 0, void 0, function* () {
|
|
398
424
|
if (!api)
|
|
399
425
|
throw new Error("Please configure API connection first");
|
|
426
|
+
const uri = (0, resourceUri_1.buildThumbnailResourceUri)(args.id);
|
|
400
427
|
return {
|
|
401
428
|
content: [
|
|
429
|
+
// See download_document above: the URI is repeated as text for legacy
|
|
430
|
+
// clients that drop resource blocks.
|
|
431
|
+
{ type: "text", text: uri },
|
|
402
432
|
{
|
|
403
433
|
type: "resource",
|
|
404
434
|
resource: {
|
|
405
|
-
uri
|
|
435
|
+
uri,
|
|
406
436
|
// See download_document above: the binary thumbnail is fetched
|
|
407
437
|
// lazily through resources/read instead of embedded here.
|
|
408
438
|
text: "",
|
|
@@ -21,6 +21,7 @@ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
|
21
21
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
22
22
|
const documents_1 = require("./documents");
|
|
23
23
|
const documentQuery_1 = require("./utils/documentQuery");
|
|
24
|
+
const resourceUri_1 = require("./utils/resourceUri");
|
|
24
25
|
function getQueryParams(queryString) {
|
|
25
26
|
return new URLSearchParams(queryString.replace(/^\?/, ""));
|
|
26
27
|
}
|
|
@@ -480,3 +481,161 @@ const OBJECT_SELECT_FIELD = {
|
|
|
480
481
|
strict_1.default.equal(calls.updateDocument.length, 0, "no document update should be sent when the option is invalid");
|
|
481
482
|
}));
|
|
482
483
|
});
|
|
484
|
+
(0, node_test_1.describe)("document resource reference tools", () => {
|
|
485
|
+
// Expected URIs come from the builders rather than literals: their exact
|
|
486
|
+
// format is already pinned by utils/resourceUri.test.ts, and what matters
|
|
487
|
+
// here is that the handler surfaces that URI in both content blocks.
|
|
488
|
+
const cases = [
|
|
489
|
+
{
|
|
490
|
+
tool: "download_document",
|
|
491
|
+
args: { id: 4 },
|
|
492
|
+
uri: (0, resourceUri_1.buildDocumentResourceUri)(4),
|
|
493
|
+
mimeType: "application/octet-stream",
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
tool: "download_document",
|
|
497
|
+
args: { id: 4, original: true },
|
|
498
|
+
uri: (0, resourceUri_1.buildDocumentResourceUri)(4, { original: true }),
|
|
499
|
+
mimeType: "application/octet-stream",
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
tool: "get_document_thumbnail",
|
|
503
|
+
args: { id: 123 },
|
|
504
|
+
uri: (0, resourceUri_1.buildThumbnailResourceUri)(123),
|
|
505
|
+
mimeType: "image/webp",
|
|
506
|
+
},
|
|
507
|
+
];
|
|
508
|
+
for (const { tool, args, uri, mimeType } of cases) {
|
|
509
|
+
(0, node_test_1.test)(`${tool} ${JSON.stringify(args)} returns the URI as text beside the resource`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
510
|
+
const { api } = createDocumentApi([]);
|
|
511
|
+
let result;
|
|
512
|
+
yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
|
|
513
|
+
result = (yield client.callTool({
|
|
514
|
+
name: tool,
|
|
515
|
+
arguments: args,
|
|
516
|
+
}));
|
|
517
|
+
}));
|
|
518
|
+
strict_1.default.ok(result && !result.isError, `${tool} failed`);
|
|
519
|
+
const [text, embedded] = result.content;
|
|
520
|
+
// Legacy clients read only content[].text (issue #134).
|
|
521
|
+
strict_1.default.equal(text.type, "text");
|
|
522
|
+
strict_1.default.equal(text.text, uri);
|
|
523
|
+
strict_1.default.equal(embedded.type, "resource");
|
|
524
|
+
const { resource } = embedded;
|
|
525
|
+
strict_1.default.equal(resource.uri, uri, "both blocks must reference the same URI");
|
|
526
|
+
strict_1.default.equal(resource.mimeType, mimeType);
|
|
527
|
+
}));
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
(0, node_test_1.describe)("bulk_edit_documents set_permissions", () => {
|
|
531
|
+
(0, node_test_1.test)("sends set_permissions, owner and merge at the top level of parameters", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
532
|
+
const { api, calls } = createDocumentApi([]);
|
|
533
|
+
yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
|
|
534
|
+
var _a;
|
|
535
|
+
const result = (yield client.callTool({
|
|
536
|
+
name: "bulk_edit_documents",
|
|
537
|
+
arguments: {
|
|
538
|
+
documents: [4103],
|
|
539
|
+
method: "set_permissions",
|
|
540
|
+
set_permissions: {
|
|
541
|
+
view: { users: [], groups: [10] },
|
|
542
|
+
change: { users: [], groups: [10] },
|
|
543
|
+
},
|
|
544
|
+
owner: 3,
|
|
545
|
+
merge: true,
|
|
546
|
+
},
|
|
547
|
+
}));
|
|
548
|
+
strict_1.default.ok(!result.isError, (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error);
|
|
549
|
+
}));
|
|
550
|
+
strict_1.default.equal(calls.bulkEditDocuments.length, 1);
|
|
551
|
+
const [documents, method, parameters] = calls.bulkEditDocuments[0];
|
|
552
|
+
strict_1.default.deepEqual(documents, [4103]);
|
|
553
|
+
strict_1.default.equal(method, "set_permissions");
|
|
554
|
+
// Paperless reads parameters["set_permissions"] directly; nesting it under
|
|
555
|
+
// another key makes the server crash with a KeyError (HTTP 500).
|
|
556
|
+
strict_1.default.deepEqual(parameters, {
|
|
557
|
+
set_permissions: {
|
|
558
|
+
view: { users: [], groups: [10] },
|
|
559
|
+
change: { users: [], groups: [10] },
|
|
560
|
+
},
|
|
561
|
+
owner: 3,
|
|
562
|
+
merge: true,
|
|
563
|
+
});
|
|
564
|
+
}));
|
|
565
|
+
(0, node_test_1.test)("forwards partial permissions without filling in omitted actions or lists", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
566
|
+
const { api, calls } = createDocumentApi([]);
|
|
567
|
+
yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
|
|
568
|
+
var _a;
|
|
569
|
+
const result = (yield client.callTool({
|
|
570
|
+
name: "bulk_edit_documents",
|
|
571
|
+
arguments: {
|
|
572
|
+
documents: [1],
|
|
573
|
+
method: "set_permissions",
|
|
574
|
+
set_permissions: { view: { groups: [10] } },
|
|
575
|
+
},
|
|
576
|
+
}));
|
|
577
|
+
strict_1.default.ok(!result.isError, (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error);
|
|
578
|
+
}));
|
|
579
|
+
// Paperless only touches the actions/lists that are present, and clears
|
|
580
|
+
// the owner when it is omitted without merge — so nothing may be added.
|
|
581
|
+
const [, , parameters] = calls.bulkEditDocuments[0];
|
|
582
|
+
strict_1.default.deepEqual(parameters, { set_permissions: { view: { groups: [10] } } });
|
|
583
|
+
}));
|
|
584
|
+
(0, node_test_1.test)("owner-only changes send an empty set_permissions object", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
585
|
+
const { api, calls } = createDocumentApi([]);
|
|
586
|
+
yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
|
|
587
|
+
var _a;
|
|
588
|
+
for (const owner of [3, null]) {
|
|
589
|
+
const result = (yield client.callTool({
|
|
590
|
+
name: "bulk_edit_documents",
|
|
591
|
+
arguments: { documents: [1], method: "set_permissions", owner },
|
|
592
|
+
}));
|
|
593
|
+
strict_1.default.ok(!result.isError, (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error);
|
|
594
|
+
}
|
|
595
|
+
}));
|
|
596
|
+
strict_1.default.deepEqual(calls.bulkEditDocuments.map(([, , parameters]) => parameters), [
|
|
597
|
+
{ set_permissions: {}, owner: 3 },
|
|
598
|
+
{ set_permissions: {}, owner: null },
|
|
599
|
+
]);
|
|
600
|
+
}));
|
|
601
|
+
(0, node_test_1.test)("rejects method set_permissions with neither set_permissions nor owner", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
602
|
+
const { api, calls } = createDocumentApi([]);
|
|
603
|
+
yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
|
|
604
|
+
var _a, _b;
|
|
605
|
+
const result = (yield client.callTool({
|
|
606
|
+
name: "bulk_edit_documents",
|
|
607
|
+
arguments: { documents: [1], method: "set_permissions", merge: true },
|
|
608
|
+
}));
|
|
609
|
+
strict_1.default.ok(result.isError, "expected an error when both are missing");
|
|
610
|
+
strict_1.default.match((_b = (_a = parseToolText(result)) === null || _a === void 0 ? void 0 : _a.error) !== null && _b !== void 0 ? _b : "", /set_permissions and\/or owner/);
|
|
611
|
+
}));
|
|
612
|
+
strict_1.default.equal(calls.bulkEditDocuments.length, 0);
|
|
613
|
+
}));
|
|
614
|
+
(0, node_test_1.test)("rejects malformed set_permissions input before calling Paperless", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
615
|
+
const { api, calls } = createDocumentApi([]);
|
|
616
|
+
yield withDocumentClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
|
|
617
|
+
for (const set_permissions of [
|
|
618
|
+
{ view: { groups: ["ai-agents"] } },
|
|
619
|
+
{ read: { groups: [10] } },
|
|
620
|
+
{ view: [10] },
|
|
621
|
+
]) {
|
|
622
|
+
// Depending on the installed MCP SDK version, an input-schema (zod)
|
|
623
|
+
// violation either rejects with a protocol error (-32602) or resolves
|
|
624
|
+
// with a CallToolResult carrying isError: true.
|
|
625
|
+
let rejected = false;
|
|
626
|
+
let result;
|
|
627
|
+
try {
|
|
628
|
+
result = (yield client.callTool({
|
|
629
|
+
name: "bulk_edit_documents",
|
|
630
|
+
arguments: { documents: [1], method: "set_permissions", set_permissions },
|
|
631
|
+
}));
|
|
632
|
+
}
|
|
633
|
+
catch (_a) {
|
|
634
|
+
rejected = true;
|
|
635
|
+
}
|
|
636
|
+
strict_1.default.ok(rejected || (result === null || result === void 0 ? void 0 : result.isError), `expected ${JSON.stringify(set_permissions)} to be rejected`);
|
|
637
|
+
}
|
|
638
|
+
}));
|
|
639
|
+
strict_1.default.equal(calls.bulkEditDocuments.length, 0);
|
|
640
|
+
}));
|
|
641
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baruchiro/paperless-mcp",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
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": {
|
package/paperless-mcp.dxt
CHANGED
|
Binary file
|