Skip to content
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
7 changes: 7 additions & 0 deletions src/api/PaperlessAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ export class PaperlessAPI {
return this.request(`/documents/${id}/`);
}

async updateDocument(id: number, data: Record<string, any>) {
return this.request(`/documents/${id}/`, {
method: "PATCH",
body: JSON.stringify(data),
});
}

async searchDocuments(query, page?, pageSize?) {
const params = new URLSearchParams();
params.set("query", query);
Expand Down
15 changes: 13 additions & 2 deletions src/tools/correspondents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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<string, number> = {
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) }] };
}
);

Expand Down
15 changes: 13 additions & 2 deletions src/tools/documentTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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<string, number> = {
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) }] };
}
);

Expand Down
40 changes: 38 additions & 2 deletions src/tools/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);

Expand All @@ -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) }] };
}
);

Expand Down
3 changes: 2 additions & 1 deletion src/tools/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down