@orellbuehler/paperless-mcp 1.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.
@@ -0,0 +1,654 @@
1
+ import { z } from "zod";
2
+ import { readFile } from "node:fs/promises";
3
+ import { buildQS, ok, err, summarizeDocs } from "../paperless/format.js";
4
+ export function registerCoreTools(server, client) {
5
+ // --- System ---
6
+ server.tool("get_status", "Get Paperless-ngx server status", {}, async () => {
7
+ try {
8
+ return ok(await client.fetch("/api/status/"));
9
+ }
10
+ catch (e) {
11
+ return err(e);
12
+ }
13
+ });
14
+ server.tool("get_statistics", "Get document statistics (total count, inbox count, etc.)", {}, async () => {
15
+ try {
16
+ return ok(await client.fetch("/api/statistics/"));
17
+ }
18
+ catch (e) {
19
+ return err(e);
20
+ }
21
+ });
22
+ server.tool("list_tasks", "List background tasks (consumption, etc.)", {}, async () => {
23
+ try {
24
+ return ok(await client.fetch("/api/tasks/"));
25
+ }
26
+ catch (e) {
27
+ return err(e);
28
+ }
29
+ });
30
+ // --- Search ---
31
+ server.tool("search_documents", "Full-text search across all documents. Returns metadata only (no OCR content) — use get_document or get_documents for full text.", {
32
+ query: z.string().describe("Search query"),
33
+ db_only: z.boolean().optional().describe("Search database only, skip full-text index"),
34
+ }, async ({ query, db_only }) => {
35
+ try {
36
+ return ok(summarizeDocs(await client.fetch(`/api/search/${buildQS({ query, db_only })}`)));
37
+ }
38
+ catch (e) {
39
+ return err(e);
40
+ }
41
+ });
42
+ server.tool("search_autocomplete", "Get autocomplete suggestions for a search term", {
43
+ term: z.string().describe("Partial search term"),
44
+ limit: z.number().optional(),
45
+ }, async ({ term, limit }) => {
46
+ try {
47
+ return ok(await client.fetch(`/api/search/autocomplete/${buildQS({ term, limit })}`));
48
+ }
49
+ catch (e) {
50
+ return err(e);
51
+ }
52
+ });
53
+ // --- Documents ---
54
+ server.tool("list_documents", "List documents with optional filtering, searching and pagination. Returns metadata only (no OCR content) — use get_document or get_documents for full text.", {
55
+ page: z.number().optional(),
56
+ page_size: z.number().optional(),
57
+ query: z.string().optional().describe("Full-text search query"),
58
+ title__icontains: z.string().optional(),
59
+ correspondent__id: z.number().optional(),
60
+ document_type__id: z.number().optional(),
61
+ storage_path__id: z.number().optional(),
62
+ tags__id__all: z.array(z.number()).optional().describe("Must have ALL these tags"),
63
+ tags__id__in: z.array(z.number()).optional().describe("Must have at least one of these tags"),
64
+ is_in_inbox: z.boolean().optional(),
65
+ created__date__gt: z.string().optional().describe("Created after (YYYY-MM-DD)"),
66
+ created__date__lt: z.string().optional().describe("Created before (YYYY-MM-DD)"),
67
+ added__date__gt: z.string().optional().describe("Added after (YYYY-MM-DD)"),
68
+ added__date__lt: z.string().optional().describe("Added before (YYYY-MM-DD)"),
69
+ ordering: z.string().optional().describe("Field to order by, prefix with - for descending"),
70
+ }, async (params) => {
71
+ try {
72
+ return ok(summarizeDocs(await client.fetch(`/api/documents/${buildQS(params)}`)));
73
+ }
74
+ catch (e) {
75
+ return err(e);
76
+ }
77
+ });
78
+ server.tool("get_document", "Get a single document by ID, including its full OCR text content", { id: z.number().describe("Document ID") }, async ({ id }) => {
79
+ try {
80
+ return ok(await client.fetch(`/api/documents/${id}/`));
81
+ }
82
+ catch (e) {
83
+ return err(e);
84
+ }
85
+ });
86
+ server.tool("download_document", "Download a document's content as text (only works for text-based documents)", {
87
+ id: z.number().describe("Document ID"),
88
+ original: z.boolean().optional().describe("Download original instead of archived version"),
89
+ }, async ({ id, original }) => {
90
+ try {
91
+ const res = await client.download(`/api/documents/${id}/download/${buildQS({ original })}`);
92
+ if (!res.ok)
93
+ throw new Error(`${res.status} ${res.statusText}`);
94
+ const ct = res.headers.get("content-type") || "";
95
+ if (ct.includes("text") || ct.includes("json") || ct.includes("xml")) {
96
+ return ok({ content_type: ct, content: await res.text() });
97
+ }
98
+ const buf = await res.arrayBuffer();
99
+ return ok({
100
+ content_type: ct,
101
+ size: buf.byteLength,
102
+ note: "Binary file, content not shown. Use the Paperless web UI to view this document.",
103
+ });
104
+ }
105
+ catch (e) {
106
+ return err(e);
107
+ }
108
+ });
109
+ server.tool("update_document", "Update a document's metadata", {
110
+ id: z.number().describe("Document ID"),
111
+ title: z.string().optional(),
112
+ correspondent: z.number().nullable().optional().describe("Correspondent ID or null to clear"),
113
+ document_type: z.number().nullable().optional().describe("Document type ID or null to clear"),
114
+ storage_path: z.number().nullable().optional().describe("Storage path ID or null to clear"),
115
+ tags: z.array(z.number()).optional().describe("Replace all tags with these IDs"),
116
+ archive_serial_number: z.number().nullable().optional(),
117
+ created: z.string().optional().describe("Created date (YYYY-MM-DD)"),
118
+ custom_fields: z.array(z.object({ field: z.number(), value: z.unknown() })).optional(),
119
+ }, async ({ id, ...body }) => {
120
+ try {
121
+ return ok(await client.fetch(`/api/documents/${id}/`, {
122
+ method: "PATCH",
123
+ body: JSON.stringify(body),
124
+ }));
125
+ }
126
+ catch (e) {
127
+ return err(e);
128
+ }
129
+ });
130
+ server.tool("delete_document", "Delete a document", { id: z.number().describe("Document ID") }, async ({ id }) => {
131
+ try {
132
+ return ok(await client.fetch(`/api/documents/${id}/`, { method: "DELETE" }));
133
+ }
134
+ catch (e) {
135
+ return err(e);
136
+ }
137
+ });
138
+ server.tool("upload_document", "Upload a document from a local file path", {
139
+ file_path: z.string().describe("Absolute path to the file on disk"),
140
+ title: z.string().optional(),
141
+ correspondent: z.number().optional(),
142
+ document_type: z.number().optional(),
143
+ storage_path: z.number().optional(),
144
+ tags: z.array(z.number()).optional(),
145
+ archive_serial_number: z.number().optional(),
146
+ created: z.string().optional().describe("Created date (YYYY-MM-DD)"),
147
+ }, async ({ file_path, title, correspondent, document_type, storage_path, tags, archive_serial_number, created, }) => {
148
+ try {
149
+ const fileData = await readFile(file_path);
150
+ const filename = file_path.split("/").pop() || "document";
151
+ const form = new FormData();
152
+ form.append("document", new Blob([fileData]), filename);
153
+ if (title !== undefined)
154
+ form.append("title", title);
155
+ if (correspondent !== undefined)
156
+ form.append("correspondent", String(correspondent));
157
+ if (document_type !== undefined)
158
+ form.append("document_type", String(document_type));
159
+ if (storage_path !== undefined)
160
+ form.append("storage_path", String(storage_path));
161
+ if (archive_serial_number !== undefined)
162
+ form.append("archive_serial_number", String(archive_serial_number));
163
+ if (created !== undefined)
164
+ form.append("created", created);
165
+ if (tags)
166
+ tags.forEach((t) => form.append("tags", String(t)));
167
+ const res = await client.upload("/api/documents/post_document/", form);
168
+ if (!res.ok)
169
+ throw new Error(`${res.status}: ${await res.text()}`);
170
+ return ok(await res.json().catch(() => ({ status: "accepted", task: res.headers.get("location") })));
171
+ }
172
+ catch (e) {
173
+ return err(e);
174
+ }
175
+ });
176
+ server.tool("get_document_metadata", "Get metadata (original filename, checksum, dates, etc.) for a document", { id: z.number().describe("Document ID") }, async ({ id }) => {
177
+ try {
178
+ return ok(await client.fetch(`/api/documents/${id}/metadata/`));
179
+ }
180
+ catch (e) {
181
+ return err(e);
182
+ }
183
+ });
184
+ server.tool("get_document_suggestions", "Get AI-generated suggestions for correspondent, type, tags, and dates", { id: z.number().describe("Document ID") }, async ({ id }) => {
185
+ try {
186
+ return ok(await client.fetch(`/api/documents/${id}/suggestions/`));
187
+ }
188
+ catch (e) {
189
+ return err(e);
190
+ }
191
+ });
192
+ server.tool("get_document_notes", "Get notes/comments on a document", { id: z.number().describe("Document ID") }, async ({ id }) => {
193
+ try {
194
+ return ok(await client.fetch(`/api/documents/${id}/notes/`));
195
+ }
196
+ catch (e) {
197
+ return err(e);
198
+ }
199
+ });
200
+ server.tool("add_document_note", "Add a note/comment to a document", {
201
+ id: z.number().describe("Document ID"),
202
+ note: z.string().describe("Note text"),
203
+ }, async ({ id, note }) => {
204
+ try {
205
+ return ok(await client.fetch(`/api/documents/${id}/notes/`, {
206
+ method: "POST",
207
+ body: JSON.stringify({ note }),
208
+ }));
209
+ }
210
+ catch (e) {
211
+ return err(e);
212
+ }
213
+ });
214
+ server.tool("delete_document_note", "Delete a note from a document", {
215
+ id: z.number().describe("Document ID"),
216
+ note_id: z.number().describe("Note ID to delete"),
217
+ }, async ({ id, note_id }) => {
218
+ try {
219
+ return ok(await client.fetch(`/api/documents/${id}/notes/`, {
220
+ method: "DELETE",
221
+ body: JSON.stringify({ id: note_id }),
222
+ }));
223
+ }
224
+ catch (e) {
225
+ return err(e);
226
+ }
227
+ });
228
+ // --- Bulk Operations ---
229
+ server.tool("bulk_edit_documents", "Perform bulk operations on multiple documents (set tags, correspondent, type, delete, merge, etc.)", {
230
+ documents: z.array(z.number()).describe("Array of document IDs"),
231
+ method: z
232
+ .string()
233
+ .describe("Operation: set_correspondent, set_document_type, set_storage_path, add_tag, remove_tag, modify_tags, delete, redo_ocr, reprocess, set_permissions, rotate, merge, split, convert"),
234
+ parameters: z.record(z.unknown()).optional().describe("Operation-specific parameters"),
235
+ }, async ({ documents, method, parameters }) => {
236
+ try {
237
+ return ok(await client.fetch("/api/documents/bulk_edit/", {
238
+ method: "POST",
239
+ body: JSON.stringify({ documents, method, parameters: parameters || {} }),
240
+ }));
241
+ }
242
+ catch (e) {
243
+ return err(e);
244
+ }
245
+ });
246
+ server.tool("get_next_asn", "Get the next available archive serial number", {}, async () => {
247
+ try {
248
+ return ok(await client.fetch("/api/documents/next_asn/"));
249
+ }
250
+ catch (e) {
251
+ return err(e);
252
+ }
253
+ });
254
+ // --- Correspondents ---
255
+ server.tool("list_correspondents", "List all correspondents (senders/recipients)", {
256
+ page: z.number().optional(),
257
+ page_size: z.number().optional(),
258
+ name__icontains: z.string().optional(),
259
+ ordering: z.string().optional(),
260
+ }, async (params) => {
261
+ try {
262
+ return ok(await client.fetch(`/api/correspondents/${buildQS(params)}`));
263
+ }
264
+ catch (e) {
265
+ return err(e);
266
+ }
267
+ });
268
+ server.tool("get_correspondent", "Get a single correspondent by ID", { id: z.number() }, async ({ id }) => {
269
+ try {
270
+ return ok(await client.fetch(`/api/correspondents/${id}/`));
271
+ }
272
+ catch (e) {
273
+ return err(e);
274
+ }
275
+ });
276
+ server.tool("create_correspondent", "Create a new correspondent", {
277
+ name: z.string(),
278
+ match: z.string().optional().describe("Auto-matching pattern"),
279
+ matching_algorithm: z
280
+ .number()
281
+ .optional()
282
+ .describe("1=any, 2=all, 3=literal, 4=regex, 5=fuzzy, 6=auto"),
283
+ is_insensitive: z.boolean().optional(),
284
+ }, async (body) => {
285
+ try {
286
+ return ok(await client.fetch("/api/correspondents/", {
287
+ method: "POST",
288
+ body: JSON.stringify(body),
289
+ }));
290
+ }
291
+ catch (e) {
292
+ return err(e);
293
+ }
294
+ });
295
+ server.tool("update_correspondent", "Update an existing correspondent (partial update)", {
296
+ id: z.number(),
297
+ name: z.string().optional(),
298
+ match: z.string().optional().describe("Auto-matching pattern"),
299
+ matching_algorithm: z
300
+ .number()
301
+ .optional()
302
+ .describe("1=any, 2=all, 3=literal, 4=regex, 5=fuzzy, 6=auto"),
303
+ is_insensitive: z.boolean().optional(),
304
+ }, async ({ id, ...body }) => {
305
+ try {
306
+ return ok(await client.fetch(`/api/correspondents/${id}/`, {
307
+ method: "PATCH",
308
+ body: JSON.stringify(body),
309
+ }));
310
+ }
311
+ catch (e) {
312
+ return err(e);
313
+ }
314
+ });
315
+ server.tool("delete_correspondent", "Delete a correspondent", { id: z.number() }, async ({ id }) => {
316
+ try {
317
+ return ok(await client.fetch(`/api/correspondents/${id}/`, { method: "DELETE" }));
318
+ }
319
+ catch (e) {
320
+ return err(e);
321
+ }
322
+ });
323
+ // --- Document Types ---
324
+ server.tool("list_document_types", "List all document types", {
325
+ page: z.number().optional(),
326
+ page_size: z.number().optional(),
327
+ name__icontains: z.string().optional(),
328
+ ordering: z.string().optional(),
329
+ }, async (params) => {
330
+ try {
331
+ return ok(await client.fetch(`/api/document_types/${buildQS(params)}`));
332
+ }
333
+ catch (e) {
334
+ return err(e);
335
+ }
336
+ });
337
+ server.tool("get_document_type", "Get a single document type by ID", { id: z.number() }, async ({ id }) => {
338
+ try {
339
+ return ok(await client.fetch(`/api/document_types/${id}/`));
340
+ }
341
+ catch (e) {
342
+ return err(e);
343
+ }
344
+ });
345
+ server.tool("create_document_type", "Create a new document type", {
346
+ name: z.string(),
347
+ match: z.string().optional(),
348
+ matching_algorithm: z
349
+ .number()
350
+ .optional()
351
+ .describe("1=any, 2=all, 3=literal, 4=regex, 5=fuzzy, 6=auto"),
352
+ is_insensitive: z.boolean().optional(),
353
+ }, async (body) => {
354
+ try {
355
+ return ok(await client.fetch("/api/document_types/", {
356
+ method: "POST",
357
+ body: JSON.stringify(body),
358
+ }));
359
+ }
360
+ catch (e) {
361
+ return err(e);
362
+ }
363
+ });
364
+ server.tool("update_document_type", "Update an existing document type (partial update)", {
365
+ id: z.number(),
366
+ name: z.string().optional(),
367
+ match: z.string().optional(),
368
+ matching_algorithm: z
369
+ .number()
370
+ .optional()
371
+ .describe("1=any, 2=all, 3=literal, 4=regex, 5=fuzzy, 6=auto"),
372
+ is_insensitive: z.boolean().optional(),
373
+ }, async ({ id, ...body }) => {
374
+ try {
375
+ return ok(await client.fetch(`/api/document_types/${id}/`, {
376
+ method: "PATCH",
377
+ body: JSON.stringify(body),
378
+ }));
379
+ }
380
+ catch (e) {
381
+ return err(e);
382
+ }
383
+ });
384
+ server.tool("delete_document_type", "Delete a document type", { id: z.number() }, async ({ id }) => {
385
+ try {
386
+ return ok(await client.fetch(`/api/document_types/${id}/`, { method: "DELETE" }));
387
+ }
388
+ catch (e) {
389
+ return err(e);
390
+ }
391
+ });
392
+ // --- Tags ---
393
+ server.tool("list_tags", "List all tags", {
394
+ page: z.number().optional(),
395
+ page_size: z.number().optional(),
396
+ name__icontains: z.string().optional(),
397
+ is_inbox_tag: z.boolean().optional(),
398
+ ordering: z.string().optional(),
399
+ }, async (params) => {
400
+ try {
401
+ return ok(await client.fetch(`/api/tags/${buildQS(params)}`));
402
+ }
403
+ catch (e) {
404
+ return err(e);
405
+ }
406
+ });
407
+ server.tool("get_tag", "Get a single tag by ID", { id: z.number() }, async ({ id }) => {
408
+ try {
409
+ return ok(await client.fetch(`/api/tags/${id}/`));
410
+ }
411
+ catch (e) {
412
+ return err(e);
413
+ }
414
+ });
415
+ server.tool("create_tag", "Create a new tag", {
416
+ name: z.string(),
417
+ color: z.string().optional().describe("Hex color like #ff0000"),
418
+ is_inbox_tag: z.boolean().optional(),
419
+ match: z.string().optional(),
420
+ matching_algorithm: z
421
+ .number()
422
+ .optional()
423
+ .describe("1=any, 2=all, 3=literal, 4=regex, 5=fuzzy, 6=auto"),
424
+ is_insensitive: z.boolean().optional(),
425
+ }, async (body) => {
426
+ try {
427
+ return ok(await client.fetch("/api/tags/", {
428
+ method: "POST",
429
+ body: JSON.stringify(body),
430
+ }));
431
+ }
432
+ catch (e) {
433
+ return err(e);
434
+ }
435
+ });
436
+ server.tool("update_tag", "Update an existing tag (partial update)", {
437
+ id: z.number(),
438
+ name: z.string().optional(),
439
+ color: z.string().optional().describe("Hex color like #ff0000"),
440
+ is_inbox_tag: z.boolean().optional(),
441
+ match: z.string().optional(),
442
+ matching_algorithm: z
443
+ .number()
444
+ .optional()
445
+ .describe("1=any, 2=all, 3=literal, 4=regex, 5=fuzzy, 6=auto"),
446
+ is_insensitive: z.boolean().optional(),
447
+ }, async ({ id, ...body }) => {
448
+ try {
449
+ return ok(await client.fetch(`/api/tags/${id}/`, {
450
+ method: "PATCH",
451
+ body: JSON.stringify(body),
452
+ }));
453
+ }
454
+ catch (e) {
455
+ return err(e);
456
+ }
457
+ });
458
+ server.tool("delete_tag", "Delete a tag", { id: z.number() }, async ({ id }) => {
459
+ try {
460
+ return ok(await client.fetch(`/api/tags/${id}/`, { method: "DELETE" }));
461
+ }
462
+ catch (e) {
463
+ return err(e);
464
+ }
465
+ });
466
+ // --- Saved Views ---
467
+ server.tool("list_saved_views", "List all saved views", {
468
+ page: z.number().optional(),
469
+ page_size: z.number().optional(),
470
+ }, async (params) => {
471
+ try {
472
+ return ok(await client.fetch(`/api/saved_views/${buildQS(params)}`));
473
+ }
474
+ catch (e) {
475
+ return err(e);
476
+ }
477
+ });
478
+ server.tool("get_saved_view", "Get a single saved view by ID", { id: z.number() }, async ({ id }) => {
479
+ try {
480
+ return ok(await client.fetch(`/api/saved_views/${id}/`));
481
+ }
482
+ catch (e) {
483
+ return err(e);
484
+ }
485
+ });
486
+ server.tool("create_saved_view", "Create a new saved view", {
487
+ name: z.string(),
488
+ filter_rules: z
489
+ .array(z.object({ rule_type: z.number(), value: z.string() }))
490
+ .describe("Filter rules: rule_type is a Paperless filter code (e.g. 0=title contains, 3=correspondent id, 6=has tag, 19=ASN); value is the matched value as a string."),
491
+ show_on_dashboard: z.boolean(),
492
+ show_in_sidebar: z.boolean(),
493
+ sort_field: z.string().optional().describe("Field to sort by, e.g. 'created'"),
494
+ sort_reverse: z.boolean().optional(),
495
+ page_size: z.number().optional(),
496
+ }, async (body) => {
497
+ try {
498
+ return ok(await client.fetch("/api/saved_views/", {
499
+ method: "POST",
500
+ body: JSON.stringify(body),
501
+ }));
502
+ }
503
+ catch (e) {
504
+ return err(e);
505
+ }
506
+ });
507
+ server.tool("update_saved_view", "Update an existing saved view (partial update)", {
508
+ id: z.number(),
509
+ name: z.string().optional(),
510
+ filter_rules: z.array(z.object({ rule_type: z.number(), value: z.string() })).optional(),
511
+ show_on_dashboard: z.boolean().optional(),
512
+ show_in_sidebar: z.boolean().optional(),
513
+ sort_field: z.string().optional(),
514
+ sort_reverse: z.boolean().optional(),
515
+ page_size: z.number().optional(),
516
+ }, async ({ id, ...body }) => {
517
+ try {
518
+ return ok(await client.fetch(`/api/saved_views/${id}/`, {
519
+ method: "PATCH",
520
+ body: JSON.stringify(body),
521
+ }));
522
+ }
523
+ catch (e) {
524
+ return err(e);
525
+ }
526
+ });
527
+ // --- Storage Paths ---
528
+ server.tool("list_storage_paths", "List all storage paths", {
529
+ page: z.number().optional(),
530
+ page_size: z.number().optional(),
531
+ name__icontains: z.string().optional(),
532
+ }, async (params) => {
533
+ try {
534
+ return ok(await client.fetch(`/api/storage_paths/${buildQS(params)}`));
535
+ }
536
+ catch (e) {
537
+ return err(e);
538
+ }
539
+ });
540
+ server.tool("get_storage_path", "Get a single storage path by ID", { id: z.number() }, async ({ id }) => {
541
+ try {
542
+ return ok(await client.fetch(`/api/storage_paths/${id}/`));
543
+ }
544
+ catch (e) {
545
+ return err(e);
546
+ }
547
+ });
548
+ server.tool("create_storage_path", "Create a new storage path", {
549
+ name: z.string(),
550
+ path: z.string().describe("Path template, e.g. '{correspondent}/{created_year}'"),
551
+ match: z.string().optional(),
552
+ matching_algorithm: z
553
+ .number()
554
+ .optional()
555
+ .describe("1=any, 2=all, 3=literal, 4=regex, 5=fuzzy, 6=auto"),
556
+ is_insensitive: z.boolean().optional(),
557
+ }, async (body) => {
558
+ try {
559
+ return ok(await client.fetch("/api/storage_paths/", {
560
+ method: "POST",
561
+ body: JSON.stringify(body),
562
+ }));
563
+ }
564
+ catch (e) {
565
+ return err(e);
566
+ }
567
+ });
568
+ server.tool("update_storage_path", "Update an existing storage path (partial update)", {
569
+ id: z.number(),
570
+ name: z.string().optional(),
571
+ path: z.string().optional(),
572
+ match: z.string().optional(),
573
+ matching_algorithm: z
574
+ .number()
575
+ .optional()
576
+ .describe("1=any, 2=all, 3=literal, 4=regex, 5=fuzzy, 6=auto"),
577
+ is_insensitive: z.boolean().optional(),
578
+ }, async ({ id, ...body }) => {
579
+ try {
580
+ return ok(await client.fetch(`/api/storage_paths/${id}/`, {
581
+ method: "PATCH",
582
+ body: JSON.stringify(body),
583
+ }));
584
+ }
585
+ catch (e) {
586
+ return err(e);
587
+ }
588
+ });
589
+ // --- Custom Fields ---
590
+ server.tool("list_custom_fields", "List all custom fields", {
591
+ page: z.number().optional(),
592
+ page_size: z.number().optional(),
593
+ }, async (params) => {
594
+ try {
595
+ return ok(await client.fetch(`/api/custom_fields/${buildQS(params)}`));
596
+ }
597
+ catch (e) {
598
+ return err(e);
599
+ }
600
+ });
601
+ server.tool("get_custom_field", "Get a single custom field by ID", { id: z.number() }, async ({ id }) => {
602
+ try {
603
+ return ok(await client.fetch(`/api/custom_fields/${id}/`));
604
+ }
605
+ catch (e) {
606
+ return err(e);
607
+ }
608
+ });
609
+ server.tool("create_custom_field", "Create a new custom field", {
610
+ name: z.string(),
611
+ data_type: z
612
+ .enum([
613
+ "string",
614
+ "url",
615
+ "date",
616
+ "boolean",
617
+ "integer",
618
+ "float",
619
+ "monetary",
620
+ "documentlink",
621
+ "select",
622
+ ])
623
+ .describe("Field data type"),
624
+ extra_data: z
625
+ .record(z.unknown())
626
+ .optional()
627
+ .describe("For 'select': { select_options: [{ label }] }. For 'monetary': { default_currency }"),
628
+ }, async (body) => {
629
+ try {
630
+ return ok(await client.fetch("/api/custom_fields/", {
631
+ method: "POST",
632
+ body: JSON.stringify(body),
633
+ }));
634
+ }
635
+ catch (e) {
636
+ return err(e);
637
+ }
638
+ });
639
+ server.tool("update_custom_field", "Update an existing custom field (partial update)", {
640
+ id: z.number(),
641
+ name: z.string().optional(),
642
+ extra_data: z.record(z.unknown()).optional(),
643
+ }, async ({ id, ...body }) => {
644
+ try {
645
+ return ok(await client.fetch(`/api/custom_fields/${id}/`, {
646
+ method: "PATCH",
647
+ body: JSON.stringify(body),
648
+ }));
649
+ }
650
+ catch (e) {
651
+ return err(e);
652
+ }
653
+ });
654
+ }