@baruchiro/paperless-mcp 0.5.1 → 2.0.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 +334 -11
- package/build/api/PaperlessAPI.d.ts +29 -2
- package/build/api/PaperlessAPI.js +97 -8
- package/build/api/types.d.ts +47 -0
- package/build/index.js +28 -5
- package/build/resources/documents.d.ts +7 -0
- package/build/resources/documents.js +119 -0
- package/build/resources/documents.test.d.ts +1 -0
- package/build/resources/documents.test.js +124 -0
- package/build/server.d.ts +14 -1
- package/build/server.js +21 -5
- package/build/server.test.d.ts +1 -0
- package/build/server.test.js +54 -0
- package/build/tools/documents.d.ts +3 -1
- package/build/tools/documents.js +170 -67
- package/build/tools/documents.test.js +404 -0
- package/build/tools/mail.d.ts +3 -0
- package/build/tools/mail.js +187 -0
- package/build/tools/mail.test.d.ts +1 -0
- package/build/tools/mail.test.js +212 -0
- package/build/tools/notes.d.ts +3 -0
- package/build/tools/notes.js +55 -0
- package/build/tools/notes.test.d.ts +1 -0
- package/build/tools/notes.test.js +177 -0
- package/build/tools/utils/descriptions.d.ts +1 -1
- package/build/tools/utils/descriptions.js +1 -1
- package/build/tools/utils/documentQuery.d.ts +71 -0
- package/build/tools/utils/documentQuery.js +270 -0
- package/build/tools/utils/resourceUri.d.ts +12 -7
- package/build/tools/utils/resourceUri.js +13 -8
- package/build/tools/utils/resourceUri.test.js +12 -12
- package/build/tools/utils/selectFields.d.ts +11 -0
- package/build/tools/utils/selectFields.js +91 -0
- package/build/tools/utils/selectFields.test.d.ts +1 -0
- package/build/tools/utils/selectFields.test.js +164 -0
- package/package.json +2 -1
- package/paperless-mcp.dxt +0 -0
package/build/index.js
CHANGED
|
@@ -20,13 +20,14 @@ const express_1 = __importDefault(require("express"));
|
|
|
20
20
|
const node_util_1 = require("node:util");
|
|
21
21
|
const server_1 = require("./server");
|
|
22
22
|
const { version } = require("../package.json");
|
|
23
|
-
const { values: { baseUrl, token, http: useHttp, port, publicUrl }, } = (0, node_util_1.parseArgs)({
|
|
23
|
+
const { values: { baseUrl, token, http: useHttp, port, publicUrl, "no-auth": noAuth }, } = (0, node_util_1.parseArgs)({
|
|
24
24
|
options: {
|
|
25
25
|
baseUrl: { type: "string" },
|
|
26
26
|
token: { type: "string" },
|
|
27
27
|
http: { type: "boolean", default: false },
|
|
28
28
|
port: { type: "string" },
|
|
29
29
|
publicUrl: { type: "string", default: "" },
|
|
30
|
+
"no-auth": { type: "boolean", default: false },
|
|
30
31
|
},
|
|
31
32
|
allowPositionals: true,
|
|
32
33
|
});
|
|
@@ -35,15 +36,21 @@ const resolvedToken = token || process.env.PAPERLESS_API_KEY;
|
|
|
35
36
|
const resolvedPublicUrl = publicUrl || process.env.PAPERLESS_PUBLIC_URL || resolvedBaseUrl;
|
|
36
37
|
const resolvedPort = port ? parseInt(port, 10) : 3000;
|
|
37
38
|
if (!resolvedBaseUrl) {
|
|
38
|
-
console.error("Usage: paperless-mcp --baseUrl <url> --token <token> [--http] [--port <port>] [--publicUrl <url>]");
|
|
39
|
+
console.error("Usage: paperless-mcp --baseUrl <url> --token <token> [--http] [--port <port>] [--publicUrl <url>] [--no-auth]");
|
|
39
40
|
console.error("Or set PAPERLESS_URL and PAPERLESS_API_KEY environment variables.");
|
|
40
41
|
process.exit(1);
|
|
41
42
|
}
|
|
42
43
|
if (!useHttp && !resolvedToken) {
|
|
43
|
-
console.error("Usage: paperless-mcp --baseUrl <url> --token <token> [--http] [--port <port>] [--publicUrl <url>]");
|
|
44
|
+
console.error("Usage: paperless-mcp --baseUrl <url> --token <token> [--http] [--port <port>] [--publicUrl <url>] [--no-auth]");
|
|
44
45
|
console.error("Or set PAPERLESS_URL and PAPERLESS_API_KEY environment variables.");
|
|
45
46
|
process.exit(1);
|
|
46
47
|
}
|
|
48
|
+
if (noAuth && !resolvedToken) {
|
|
49
|
+
console.error("--no-auth allows unauthenticated requests to use the server's Paperless token, " +
|
|
50
|
+
"but no server token is configured. Provide --token <token> or set PAPERLESS_API_KEY, " +
|
|
51
|
+
"or drop --no-auth and have clients authenticate with 'Authorization: Bearer <token>'.");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
47
54
|
function buildServer(requestToken) {
|
|
48
55
|
return (0, server_1.createMcpServer)({
|
|
49
56
|
baseUrl: resolvedBaseUrl,
|
|
@@ -55,12 +62,25 @@ function buildServer(requestToken) {
|
|
|
55
62
|
function main() {
|
|
56
63
|
return __awaiter(this, void 0, void 0, function* () {
|
|
57
64
|
if (useHttp) {
|
|
65
|
+
if (noAuth) {
|
|
66
|
+
console.log("[paperless-mcp] --no-auth is enabled: requests without an 'Authorization: Bearer' header " +
|
|
67
|
+
"will use the server's Paperless token. Only use this on a trusted/local network.");
|
|
68
|
+
}
|
|
69
|
+
else if (resolvedToken) {
|
|
70
|
+
console.log("[paperless-mcp] A server token is configured, but unauthenticated requests are rejected. " +
|
|
71
|
+
"Clients must send 'Authorization: Bearer <paperless-token>'. " +
|
|
72
|
+
"To use the server token for unauthenticated requests instead, restart with the --no-auth flag " +
|
|
73
|
+
"(trusted/local networks only).");
|
|
74
|
+
}
|
|
58
75
|
const app = (0, express_1.default)();
|
|
59
76
|
app.use(express_1.default.json());
|
|
60
77
|
// Store transports for each session
|
|
61
78
|
const sseTransports = {};
|
|
62
79
|
app.post("/mcp", (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
63
|
-
const requestToken = (0, server_1.getBearerToken)(req,
|
|
80
|
+
const requestToken = (0, server_1.getBearerToken)(req, {
|
|
81
|
+
fallbackToken: resolvedToken,
|
|
82
|
+
allowAnonymous: noAuth,
|
|
83
|
+
});
|
|
64
84
|
if (!requestToken) {
|
|
65
85
|
(0, server_1.sendUnauthorized)(res);
|
|
66
86
|
return;
|
|
@@ -112,7 +132,10 @@ function main() {
|
|
|
112
132
|
}));
|
|
113
133
|
app.get("/sse", (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
114
134
|
console.log("SSE request received");
|
|
115
|
-
const requestToken = (0, server_1.getBearerToken)(req,
|
|
135
|
+
const requestToken = (0, server_1.getBearerToken)(req, {
|
|
136
|
+
fallbackToken: resolvedToken,
|
|
137
|
+
allowAnonymous: noAuth,
|
|
138
|
+
});
|
|
116
139
|
if (!requestToken) {
|
|
117
140
|
(0, server_1.sendUnauthorized)(res);
|
|
118
141
|
return;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types";
|
|
3
|
+
import { PaperlessAPI } from "../api/PaperlessAPI";
|
|
4
|
+
type TemplateVariables = Record<string, string | string[]>;
|
|
5
|
+
export declare function registerDocumentResources(server: McpServer, api: PaperlessAPI): void;
|
|
6
|
+
export declare function readDocumentResource(api: PaperlessAPI, uri: URL, variables: TemplateVariables): Promise<ReadResourceResult>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.registerDocumentResources = registerDocumentResources;
|
|
13
|
+
exports.readDocumentResource = readDocumentResource;
|
|
14
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
15
|
+
function registerDocumentResources(server, api) {
|
|
16
|
+
server.resource("paperless-document-resource", new mcp_js_1.ResourceTemplate("paperless://documents/{id}/{resource}", {
|
|
17
|
+
list: undefined,
|
|
18
|
+
}), (uri, variables) => __awaiter(this, void 0, void 0, function* () { return readDocumentResource(api, uri, variables); }));
|
|
19
|
+
server.resource("paperless-document-original-download", new mcp_js_1.ResourceTemplate("paperless://documents/{id}/download{?original}", {
|
|
20
|
+
list: undefined,
|
|
21
|
+
}), (uri, variables) => __awaiter(this, void 0, void 0, function* () {
|
|
22
|
+
return readDocumentDownloadResource(api, uri, parseDocumentId(readVariable(variables, "id")), isTrueQueryValue(readVariable(variables, "original")));
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
function readDocumentResource(api, uri, variables) {
|
|
26
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
assertPaperlessDocumentsUri(uri);
|
|
28
|
+
const id = parseDocumentId(readVariable(variables, "id"));
|
|
29
|
+
const resource = readVariable(variables, "resource").split("?")[0];
|
|
30
|
+
switch (resource) {
|
|
31
|
+
case "download":
|
|
32
|
+
return readDocumentDownloadResource(api, uri, id, isTrueQueryValue(uri.searchParams.get("original") || undefined));
|
|
33
|
+
case "thumbnail":
|
|
34
|
+
case "thumb":
|
|
35
|
+
return readDocumentThumbnailResource(api, uri, id);
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(`Unsupported Paperless document resource: ${resource}`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function readDocumentDownloadResource(api, uri, id, original) {
|
|
42
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
const response = yield api.downloadDocument(id, original);
|
|
44
|
+
return {
|
|
45
|
+
contents: [
|
|
46
|
+
responseToResourceContents(uri.href, response, "application/octet-stream"),
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function readDocumentThumbnailResource(api, uri, id) {
|
|
52
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
const response = yield api.getThumbnail(id);
|
|
54
|
+
return {
|
|
55
|
+
contents: [responseToResourceContents(uri.href, response, "image/webp")],
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function responseToResourceContents(uri, response, fallbackMimeType) {
|
|
60
|
+
const mimeType = getHeader(response, "content-type") || fallbackMimeType;
|
|
61
|
+
const data = Buffer.from(response.data);
|
|
62
|
+
if (isTextMimeType(mimeType)) {
|
|
63
|
+
return {
|
|
64
|
+
uri,
|
|
65
|
+
mimeType,
|
|
66
|
+
text: data.toString("utf8"),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
uri,
|
|
71
|
+
mimeType,
|
|
72
|
+
blob: data.toString("base64"),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function assertPaperlessDocumentsUri(uri) {
|
|
76
|
+
if (uri.protocol !== "paperless:" || uri.hostname !== "documents") {
|
|
77
|
+
throw new Error(`Unsupported Paperless resource URI: ${uri.href}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function parseDocumentId(idValue) {
|
|
81
|
+
const id = Number(idValue);
|
|
82
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
83
|
+
throw new Error(`Invalid Paperless document id in resource URI: ${idValue}`);
|
|
84
|
+
}
|
|
85
|
+
return id;
|
|
86
|
+
}
|
|
87
|
+
function readVariable(variables, name) {
|
|
88
|
+
const value = variables[name];
|
|
89
|
+
const stringValue = Array.isArray(value) ? value[0] : value;
|
|
90
|
+
if (!stringValue) {
|
|
91
|
+
throw new Error(`Missing ${name} in Paperless resource URI`);
|
|
92
|
+
}
|
|
93
|
+
return stringValue;
|
|
94
|
+
}
|
|
95
|
+
function isTrueQueryValue(value) {
|
|
96
|
+
return value === "true" || value === "1";
|
|
97
|
+
}
|
|
98
|
+
function getHeader(response, headerName) {
|
|
99
|
+
const headers = response.headers;
|
|
100
|
+
if (typeof headers.get === "function") {
|
|
101
|
+
const value = headers.get(headerName);
|
|
102
|
+
if (typeof value === "string") {
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const value = headers[headerName] || headers[headerName.toLowerCase()];
|
|
107
|
+
if (Array.isArray(value)) {
|
|
108
|
+
return String(value[0]);
|
|
109
|
+
}
|
|
110
|
+
return typeof value === "string" ? value : undefined;
|
|
111
|
+
}
|
|
112
|
+
function isTextMimeType(mimeType) {
|
|
113
|
+
const normalizedMimeType = mimeType.split(";")[0].trim().toLowerCase();
|
|
114
|
+
return (normalizedMimeType.startsWith("text/") ||
|
|
115
|
+
normalizedMimeType === "application/json" ||
|
|
116
|
+
normalizedMimeType === "application/xml" ||
|
|
117
|
+
normalizedMimeType.endsWith("+json") ||
|
|
118
|
+
normalizedMimeType.endsWith("+xml"));
|
|
119
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
16
|
+
const node_test_1 = require("node:test");
|
|
17
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
18
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
19
|
+
const paperlessApi_1 = require("../test/mocks/paperlessApi");
|
|
20
|
+
const documents_1 = require("./documents");
|
|
21
|
+
class TestTransport {
|
|
22
|
+
start() {
|
|
23
|
+
return __awaiter(this, void 0, void 0, function* () { });
|
|
24
|
+
}
|
|
25
|
+
send(message) {
|
|
26
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
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); });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
close() {
|
|
31
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
32
|
+
var _a;
|
|
33
|
+
(_a = this.onclose) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function createTransportPair() {
|
|
38
|
+
const clientTransport = new TestTransport();
|
|
39
|
+
const serverTransport = new TestTransport();
|
|
40
|
+
clientTransport.peer = serverTransport;
|
|
41
|
+
serverTransport.peer = clientTransport;
|
|
42
|
+
return { clientTransport, serverTransport };
|
|
43
|
+
}
|
|
44
|
+
function emptyPaginationResponse(results = [], all = []) {
|
|
45
|
+
return {
|
|
46
|
+
count: results.length,
|
|
47
|
+
next: null,
|
|
48
|
+
previous: null,
|
|
49
|
+
all,
|
|
50
|
+
results,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function createBinaryResponse(body, contentType) {
|
|
54
|
+
return {
|
|
55
|
+
data: Buffer.from(body),
|
|
56
|
+
status: 200,
|
|
57
|
+
statusText: "OK",
|
|
58
|
+
headers: {
|
|
59
|
+
"content-type": contentType,
|
|
60
|
+
},
|
|
61
|
+
config: {},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function withResourceClient(api, run) {
|
|
65
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
const server = new mcp_js_1.McpServer({ name: "paperless-test", version: "1.0.0" });
|
|
67
|
+
(0, documents_1.registerDocumentResources)(server, api);
|
|
68
|
+
const client = new index_js_1.Client({ name: "paperless-test-client", version: "1.0.0" });
|
|
69
|
+
const { clientTransport, serverTransport } = createTransportPair();
|
|
70
|
+
yield server.connect(serverTransport);
|
|
71
|
+
yield client.connect(clientTransport);
|
|
72
|
+
try {
|
|
73
|
+
yield run(client);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
yield client.close();
|
|
77
|
+
yield server.close();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
(0, node_test_1.test)("resources/list does not enumerate documents at startup", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
82
|
+
// Documents are dynamic DMS data and must not be pre-registered as MCP
|
|
83
|
+
// resources: enumerating them floods `resources/list` and the LLM context
|
|
84
|
+
// window (issue #112). The list must stay empty even when documents exist;
|
|
85
|
+
// documents are reached on demand via tools + the `resources/read` template.
|
|
86
|
+
const api = {
|
|
87
|
+
getDocuments: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
return emptyPaginationResponse([
|
|
89
|
+
(0, paperlessApi_1.createDocument)({
|
|
90
|
+
id: 1,
|
|
91
|
+
title: "Invoice",
|
|
92
|
+
mime_type: "application/pdf",
|
|
93
|
+
}),
|
|
94
|
+
(0, paperlessApi_1.createDocument)({
|
|
95
|
+
id: 2,
|
|
96
|
+
title: "Receipt",
|
|
97
|
+
mime_type: "image/png",
|
|
98
|
+
}),
|
|
99
|
+
], [1, 2, 3]);
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
yield withResourceClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
|
|
103
|
+
const result = yield client.listResources();
|
|
104
|
+
strict_1.default.deepEqual(result.resources, []);
|
|
105
|
+
}));
|
|
106
|
+
}));
|
|
107
|
+
(0, node_test_1.test)("resources/read returns text contents for text responses", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
108
|
+
const api = {
|
|
109
|
+
getDocuments: () => __awaiter(void 0, void 0, void 0, function* () { return emptyPaginationResponse(); }),
|
|
110
|
+
downloadDocument: () => __awaiter(void 0, void 0, void 0, function* () { return createBinaryResponse("plain text", "text/plain; charset=utf-8"); }),
|
|
111
|
+
};
|
|
112
|
+
yield withResourceClient(api, (client) => __awaiter(void 0, void 0, void 0, function* () {
|
|
113
|
+
const result = yield client.readResource({
|
|
114
|
+
uri: "paperless://documents/4/download",
|
|
115
|
+
});
|
|
116
|
+
strict_1.default.deepEqual(result.contents, [
|
|
117
|
+
{
|
|
118
|
+
uri: "paperless://documents/4/download",
|
|
119
|
+
mimeType: "text/plain; charset=utf-8",
|
|
120
|
+
text: "plain text",
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
}));
|
|
124
|
+
}));
|
package/build/server.d.ts
CHANGED
|
@@ -7,5 +7,18 @@ export interface CreateMcpServerOptions {
|
|
|
7
7
|
publicUrl: string;
|
|
8
8
|
}
|
|
9
9
|
export declare function createMcpServer({ baseUrl, token, version, publicUrl, }: CreateMcpServerOptions): McpServer;
|
|
10
|
-
export
|
|
10
|
+
export interface ResolveTokenOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Server-configured token used as a fallback for unauthenticated requests.
|
|
13
|
+
* Only consulted when `allowAnonymous` is true.
|
|
14
|
+
*/
|
|
15
|
+
fallbackToken?: string;
|
|
16
|
+
/**
|
|
17
|
+
* When true, requests without a `Bearer` header fall back to `fallbackToken`
|
|
18
|
+
* (the legacy behaviour). When false (the default in HTTP mode), a request
|
|
19
|
+
* without a `Bearer` header is rejected and never uses the server token.
|
|
20
|
+
*/
|
|
21
|
+
allowAnonymous: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function getBearerToken(req: express.Request, options: ResolveTokenOptions): string | undefined;
|
|
11
24
|
export declare function sendUnauthorized(res: express.Response): void;
|
package/build/server.js
CHANGED
|
@@ -5,33 +5,49 @@ exports.getBearerToken = getBearerToken;
|
|
|
5
5
|
exports.sendUnauthorized = sendUnauthorized;
|
|
6
6
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
7
7
|
const PaperlessAPI_1 = require("./api/PaperlessAPI");
|
|
8
|
+
const documents_1 = require("./resources/documents");
|
|
8
9
|
const correspondents_1 = require("./tools/correspondents");
|
|
9
10
|
const customFields_1 = require("./tools/customFields");
|
|
10
|
-
const
|
|
11
|
+
const documents_2 = require("./tools/documents");
|
|
11
12
|
const documentTypes_1 = require("./tools/documentTypes");
|
|
13
|
+
const mail_1 = require("./tools/mail");
|
|
14
|
+
const notes_1 = require("./tools/notes");
|
|
12
15
|
const tags_1 = require("./tools/tags");
|
|
13
16
|
function createMcpServer({ baseUrl, token, version, publicUrl, }) {
|
|
14
17
|
const api = new PaperlessAPI_1.PaperlessAPI(baseUrl, token);
|
|
15
18
|
const server = new mcp_js_1.McpServer({ name: "paperless-ngx", version }, { instructions: buildInstructions(publicUrl) });
|
|
16
|
-
(0,
|
|
19
|
+
(0, documents_2.registerDocumentTools)(server, api);
|
|
20
|
+
(0, documents_1.registerDocumentResources)(server, api);
|
|
21
|
+
(0, notes_1.registerNoteTools)(server, api);
|
|
17
22
|
(0, tags_1.registerTagTools)(server, api);
|
|
18
23
|
(0, correspondents_1.registerCorrespondentTools)(server, api);
|
|
19
24
|
(0, documentTypes_1.registerDocumentTypeTools)(server, api);
|
|
20
25
|
(0, customFields_1.registerCustomFieldTools)(server, api);
|
|
26
|
+
(0, mail_1.registerMailTools)(server, api);
|
|
21
27
|
return server;
|
|
22
28
|
}
|
|
23
|
-
function getBearerToken(req,
|
|
29
|
+
function getBearerToken(req, options) {
|
|
24
30
|
const authHeader = req.headers["authorization"];
|
|
25
31
|
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
26
32
|
return authHeader.slice(7);
|
|
27
33
|
}
|
|
28
|
-
|
|
34
|
+
if (options.allowAnonymous) {
|
|
35
|
+
return options.fallbackToken || undefined;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
29
38
|
}
|
|
30
39
|
function sendUnauthorized(res) {
|
|
40
|
+
// Log operator-facing guidance server-side; keep the wire response minimal so
|
|
41
|
+
// we don't echo configuration hints back to unauthenticated callers.
|
|
42
|
+
console.error("[paperless-mcp] Rejected request with no 'Authorization: Bearer <paperless-ngx-api-token>' header. " +
|
|
43
|
+
"As of v2.0.0, HTTP mode requires a per-request Bearer token and no longer falls back to the " +
|
|
44
|
+
"server-configured token for unauthenticated requests. Have clients send their Paperless-NGX API " +
|
|
45
|
+
"token as a Bearer token, or restart the server with --no-auth to use the server token for " +
|
|
46
|
+
"unauthenticated requests (trusted/local networks only).");
|
|
31
47
|
res
|
|
32
48
|
.status(401)
|
|
33
49
|
.set("WWW-Authenticate", 'Bearer realm="paperless-mcp"')
|
|
34
|
-
.
|
|
50
|
+
.json({ error: "unauthorized" });
|
|
35
51
|
}
|
|
36
52
|
function buildInstructions(publicUrl) {
|
|
37
53
|
return `
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
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 server_1 = require("./server");
|
|
9
|
+
function reqWith(authorization) {
|
|
10
|
+
return {
|
|
11
|
+
headers: authorization ? { authorization } : {},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
(0, node_test_1.test)("client Bearer token takes precedence over the server token", () => {
|
|
15
|
+
const token = (0, server_1.getBearerToken)(reqWith("Bearer client-token"), {
|
|
16
|
+
fallbackToken: "server-token",
|
|
17
|
+
allowAnonymous: true,
|
|
18
|
+
});
|
|
19
|
+
strict_1.default.equal(token, "client-token");
|
|
20
|
+
});
|
|
21
|
+
(0, node_test_1.test)("client Bearer token is used even when anonymous access is disabled", () => {
|
|
22
|
+
const token = (0, server_1.getBearerToken)(reqWith("Bearer client-token"), {
|
|
23
|
+
fallbackToken: "server-token",
|
|
24
|
+
allowAnonymous: false,
|
|
25
|
+
});
|
|
26
|
+
strict_1.default.equal(token, "client-token");
|
|
27
|
+
});
|
|
28
|
+
(0, node_test_1.test)("falls back to the server token only when anonymous access is allowed", () => {
|
|
29
|
+
const token = (0, server_1.getBearerToken)(reqWith(), {
|
|
30
|
+
fallbackToken: "server-token",
|
|
31
|
+
allowAnonymous: true,
|
|
32
|
+
});
|
|
33
|
+
strict_1.default.equal(token, "server-token");
|
|
34
|
+
});
|
|
35
|
+
(0, node_test_1.test)("no header without anonymous access yields no token (request is rejected)", () => {
|
|
36
|
+
const token = (0, server_1.getBearerToken)(reqWith(), {
|
|
37
|
+
fallbackToken: "server-token",
|
|
38
|
+
allowAnonymous: false,
|
|
39
|
+
});
|
|
40
|
+
strict_1.default.equal(token, undefined);
|
|
41
|
+
});
|
|
42
|
+
(0, node_test_1.test)("non-Bearer Authorization scheme never leaks the server token by default", () => {
|
|
43
|
+
const token = (0, server_1.getBearerToken)(reqWith("Token server-token"), {
|
|
44
|
+
fallbackToken: "server-token",
|
|
45
|
+
allowAnonymous: false,
|
|
46
|
+
});
|
|
47
|
+
strict_1.default.equal(token, undefined);
|
|
48
|
+
});
|
|
49
|
+
(0, node_test_1.test)("anonymous access with no configured server token yields no token", () => {
|
|
50
|
+
const token = (0, server_1.getBearerToken)(reqWith(), {
|
|
51
|
+
allowAnonymous: true,
|
|
52
|
+
});
|
|
53
|
+
strict_1.default.equal(token, undefined);
|
|
54
|
+
});
|
|
@@ -9,6 +9,8 @@ export type BulkCustomFieldParameters = {
|
|
|
9
9
|
add_custom_fields?: Record<string, BulkCustomFieldValue>;
|
|
10
10
|
remove_custom_fields?: number[];
|
|
11
11
|
};
|
|
12
|
+
/** Validates that a file path is safe to read for document upload. */
|
|
13
|
+
export declare function validateFilePath(filePath: string): Promise<void>;
|
|
12
14
|
/**
|
|
13
15
|
* Builds Paperless-NGX bulk edit parameters from base parameters plus optional
|
|
14
16
|
* custom field updates.
|
|
@@ -32,5 +34,5 @@ export type BulkCustomFieldParameters = {
|
|
|
32
34
|
* @returns The merged API parameters with custom field updates transformed into
|
|
33
35
|
* Paperless-NGX's `add_custom_fields` record shape.
|
|
34
36
|
*/
|
|
35
|
-
export declare function buildBulkEditParameters<T extends Record<string, unknown>>(parameters: T, addCustomFields?: BulkCustomFieldUpdate[], includeCustomFieldDefaults?: boolean): T & BulkCustomFieldParameters;
|
|
37
|
+
export declare function buildBulkEditParameters<T extends Record<string, unknown>>(parameters: T, addCustomFields?: BulkCustomFieldUpdate[], includeCustomFieldDefaults?: boolean, includeTagDefaults?: boolean): T & BulkCustomFieldParameters;
|
|
36
38
|
export declare function registerDocumentTools(server: McpServer, api: PaperlessAPI): void;
|