diff --git a/README.md b/README.md index 8144720..15f6f74 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,43 @@ get_document({ }) ``` +#### update_document +Update metadata on an existing document. Use this to correct the document date, title, correspondent, type, or tags after a document has been added to Paperless-NGX. + +> **Note on tags:** The `tags` parameter is a full replacement — it overwrites all existing tags on the document. To add or remove individual tags without affecting others, use `bulk_edit_documents` with `add_tag` or `remove_tag` instead. + +Parameters: +- id: Document ID to update +- title (optional): New title for the document +- created (optional): Document date in ISO format (YYYY-MM-DD). Corrects the date to match the actual document date, not the scan/upload date. +- correspondent (optional): ID of a correspondent, or null to clear +- document_type (optional): ID of a document type, or null to clear +- storage_path (optional): ID of a storage path, or null to use default +- tags (optional): Full list of tag IDs to assign — replaces all existing tags +- archive_serial_number (optional): Integer archive serial number, or null to clear + +```typescript +// Fix a document date and assign a correspondent +update_document({ + id: 123, + created: "2024-11-18", + correspondent: 7 +}) + +// Update title and document type +update_document({ + id: 456, + title: "Annual Pension Statement 2025", + document_type: 3 +}) + +// Replace all tags +update_document({ + id: 789, + tags: [2, 5, 11] +}) +``` + #### search_documents Full-text search across documents. diff --git a/src/api/PaperlessAPI.ts b/src/api/PaperlessAPI.ts index 5f5bd21..92b4a56 100644 --- a/src/api/PaperlessAPI.ts +++ b/src/api/PaperlessAPI.ts @@ -106,6 +106,13 @@ export class PaperlessAPI { return this.request(`/documents/${id}/`); } + async updateDocument(id: number, data: Record) { + return this.request(`/documents/${id}/`, { + method: "PATCH", + body: JSON.stringify(data), + }); + } + async searchDocuments(query, page?, pageSize?) { const params = new URLSearchParams(); params.set("query", query); diff --git a/src/tools/correspondents.ts b/src/tools/correspondents.ts index 87504f0..06ab32e 100644 --- a/src/tools/correspondents.ts +++ b/src/tools/correspondents.ts @@ -7,7 +7,8 @@ export function registerCorrespondentTools(server: McpServer, api) { "Retrieve all available correspondents (people, companies, organizations that send/receive documents). Returns names and automatic matching patterns for document assignment.", { }, async (args, extra) => { if (!api) throw new Error("Please configure API connection first"); - return api.getCorrespondents(); + const result = await api.getCorrespondents(); + return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; }); server.tool( @@ -22,7 +23,17 @@ export function registerCorrespondentTools(server: McpServer, api) { }, async (args, extra) => { if (!api) throw new Error("Please configure API connection first"); - return api.createCorrespondent(args); + const algorithmMap: Record = { + any: 1, all: 2, exact: 3, "regular expression": 4, fuzzy: 5, + }; + const payload = { + ...args, + ...(args.matching_algorithm !== undefined && { + matching_algorithm: algorithmMap[args.matching_algorithm], + }), + }; + const result = await api.createCorrespondent(payload); + return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; } ); diff --git a/src/tools/documentTypes.ts b/src/tools/documentTypes.ts index e7b4536..9f50a01 100644 --- a/src/tools/documentTypes.ts +++ b/src/tools/documentTypes.ts @@ -8,7 +8,8 @@ export function registerDocumentTypeTools(server, api) { // No parameters - returns all available document types }, async (args, extra) => { if (!api) throw new Error("Please configure API connection first"); - return api.getDocumentTypes(); + const result = await api.getDocumentTypes(); + return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; }); server.tool( @@ -23,7 +24,17 @@ export function registerDocumentTypeTools(server, api) { }, async (args, extra) => { if (!api) throw new Error("Please configure API connection first"); - return api.createDocumentType(args); + const algorithmMap: Record = { + any: 1, all: 2, exact: 3, "regular expression": 4, fuzzy: 5, + }; + const payload = { + ...args, + ...(args.matching_algorithm !== undefined && { + matching_algorithm: algorithmMap[args.matching_algorithm], + }), + }; + const result = await api.createDocumentType(payload); + return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; } ); diff --git a/src/tools/documents.ts b/src/tools/documents.ts index 1381692..ceb8508 100644 --- a/src/tools/documents.ts +++ b/src/tools/documents.ts @@ -91,7 +91,42 @@ export function registerDocumentTools(server, api) { }, async (args, extra) => { if (!api) throw new Error("Please configure API connection first"); - return api.getDocument(args.id); + const result = await api.getDocument(args.id); + const { content: _ocr, ...meta } = result; + return { content: [{ type: "text" as const, text: JSON.stringify(meta, null, 2) }] }; + } + ); + + server.tool( + "update_document", + "Update metadata for an existing document. Use this to correct or set the document date, title, correspondent, document type, tags, and other fields on a document that is already in Paperless-NGX.", + { + id: z.number().describe("Unique document ID to update. Get this from list_documents, search_documents, or get_document."), + title: z.string().optional().describe("New title for the document."), + created: z.string().optional().describe("Document date in ISO format (YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss). Use this to set the date to the actual date on the document — Paperless often defaults to the upload/scan date instead."), + correspondent: z.number().nullable().optional().describe("ID of correspondent to assign, or null to clear. Use list_correspondents to get valid IDs."), + document_type: z.number().nullable().optional().describe("ID of document type to assign, or null to clear. Use list_document_types to get valid IDs."), + storage_path: z.number().nullable().optional().describe("ID of storage path to assign, or null to use default."), + tags: z.array(z.number()).optional().describe("Full list of tag IDs to assign. Replaces all existing tags on the document. Use list_tags to get valid IDs."), + archive_serial_number: z.string().nullable().optional().describe("Archive serial number for physical document cross-reference, or null to clear. Use the same format as post_document (e.g. '2024-001' or '42')."), + }, + async (args, extra) => { + if (!api) throw new Error("Please configure API connection first"); + const { id, ...data } = args; + if (Object.keys(data).length === 0) { + throw new Error("At least one field must be provided to update."); + } + try { + const result = await api.updateDocument(id, data); + // Explicitly return MCP content format. We can't return the raw Paperless document + // object because it has a "content" key (OCR text, a string) — the MCP client + // validates responses against CallToolResultSchema where content must be ContentBlock[]. + // Stripping the OCR field and wrapping in the proper format satisfies both constraints. + const { content: _ocr, ...meta } = result; + return { content: [{ type: "text" as const, text: JSON.stringify(meta, null, 2) }] }; + } catch (err: any) { + return { content: [{ type: "text" as const, text: `Error updating document ${id}: ${err?.message ?? String(err)}` }], isError: true }; + } } ); @@ -105,7 +140,8 @@ export function registerDocumentTools(server, api) { }, async (args, extra) => { if (!api) throw new Error("Please configure API connection first"); - return api.searchDocuments(args.query, args.page, args.page_size); + const result = await api.searchDocuments(args.query, args.page, args.page_size); + return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; } ); diff --git a/src/tools/tags.ts b/src/tools/tags.ts index 5447764..a69bae9 100644 --- a/src/tools/tags.ts +++ b/src/tools/tags.ts @@ -8,7 +8,8 @@ export function registerTagTools(server, api) { // No parameters - returns all available tags }, async (args, extra) => { if (!api) throw new Error("Please configure API connection first"); - return api.getTags(); + const result = await api.getTags(); + return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; }); server.tool(