zotero-bridge 1.1.0 → 1.1.1

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/src/index.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * SQLite database for collection management, tagging, PDF reading, and more.
8
8
  *
9
9
  * @author Combjellyshen
10
- * @version 1.0.0
10
+ * @version 1.1.0 (Consolidated tools)
11
11
  */
12
12
 
13
13
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -22,55 +22,24 @@ import { ZoteroDatabase } from './database.js';
22
22
  import { PDFProcessor } from './pdf.js';
23
23
  import {
24
24
  toolDefinitions,
25
- listCollectionsSchema,
26
- getCollectionSchema,
27
- createCollectionSchema,
28
- renameCollectionSchema,
29
- moveCollectionSchema,
30
- deleteCollectionSchema,
31
- getSubcollectionsSchema,
32
- listTagsSchema,
33
- addTagSchema,
34
- removeTagSchema,
35
- getItemTagsSchema,
36
- createTagSchema,
25
+ manageCollectionSchema,
26
+ manageTagsSchema,
37
27
  searchItemsSchema,
38
28
  getItemDetailsSchema,
39
- addItemToCollectionSchema,
40
- removeItemFromCollectionSchema,
41
- getCollectionItemsSchema,
42
- getItemAbstractSchema,
43
- setItemAbstractSchema,
44
- getItemNotesSchema,
45
- addItemNoteSchema,
46
- extractPDFTextSchema,
47
- getPDFSummarySchema,
48
- getItemPDFsSchema,
49
- searchPDFSchema,
50
- generateAbstractFromPDFSchema,
51
- getDatabaseInfoSchema,
52
- rawQuerySchema,
53
- // New schemas
54
- findByDOISchema,
55
- findByISBNSchema,
29
+ manageItemContentSchema,
30
+ managePDFSchema,
56
31
  findByIdentifierSchema,
57
- getItemAnnotationsSchema,
58
- getAttachmentAnnotationsSchema,
59
- getAnnotationsByTypeSchema,
60
- getAnnotationsByColorSchema,
61
- searchAnnotationsSchema,
32
+ getAnnotationsSchema,
62
33
  searchFulltextSchema,
63
- getFulltextContentSchema,
64
- searchFulltextWithContextSchema,
65
- getRelatedItemsSchema,
66
- findSimilarByTagsSchema,
67
- findSimilarByCreatorsSchema,
68
- findSimilarByCollectionSchema
34
+ findRelatedItemsSchema,
35
+ getDatabaseInfoSchema,
36
+ rawQuerySchema,
37
+ libraryMaintenanceSchema
69
38
  } from './tools.js';
70
39
 
71
40
  // Server configuration
72
41
  const SERVER_NAME = 'zotero-bridge';
73
- const SERVER_VERSION = '1.0.0';
42
+ const SERVER_VERSION = '1.1.0';
74
43
 
75
44
  // Parse command line arguments
76
45
  function parseArgs(): { dbPath?: string; readonly: boolean } {
@@ -112,7 +81,7 @@ async function main() {
112
81
 
113
82
  // Initialize database
114
83
  const db = new ZoteroDatabase(dbPath, readonly);
115
- await db.connect(); // Connect to database
84
+ await db.connect();
116
85
  const pdf = new PDFProcessor(db);
117
86
 
118
87
  // Create MCP server
@@ -163,97 +132,93 @@ async function main() {
163
132
 
164
133
  switch (name) {
165
134
  // ============================================
166
- // Collection Tools
135
+ // Collection Management (Consolidated)
167
136
  // ============================================
168
- case 'list_collections': {
169
- const params = listCollectionsSchema.parse(args);
170
- result = db.getCollections(params.libraryID);
171
- break;
172
- }
173
-
174
- case 'get_collection': {
175
- const params = getCollectionSchema.parse(args);
176
- if (params.collectionID) {
177
- result = db.getCollectionById(params.collectionID);
178
- } else if (params.name) {
179
- result = db.getCollectionByName(params.name, params.libraryID);
180
- } else {
181
- throw new Error('Either collectionID or name is required');
137
+ case 'manage_collection': {
138
+ const params = manageCollectionSchema.parse(args);
139
+
140
+ switch (params.action) {
141
+ case 'list':
142
+ result = db.getCollections(params.libraryID);
143
+ break;
144
+ case 'get':
145
+ if (params.collectionID) {
146
+ result = db.getCollectionById(params.collectionID);
147
+ } else if (params.name) {
148
+ result = db.getCollectionByName(params.name, params.libraryID);
149
+ } else {
150
+ throw new Error('Either collectionID or name is required for get action');
151
+ }
152
+ break;
153
+ case 'create':
154
+ if (!params.name) throw new Error('Name is required for create action');
155
+ const collectionID = db.createCollection(params.name, params.parentCollectionID || null, params.libraryID);
156
+ result = { success: true, collectionID };
157
+ break;
158
+ case 'rename':
159
+ if (!params.collectionID || !params.newName) throw new Error('collectionID and newName are required');
160
+ result = { success: db.renameCollection(params.collectionID, params.newName) };
161
+ break;
162
+ case 'move':
163
+ if (!params.collectionID) throw new Error('collectionID is required');
164
+ result = { success: db.moveCollection(params.collectionID, params.parentCollectionID ?? null) };
165
+ break;
166
+ case 'delete':
167
+ if (!params.collectionID) throw new Error('collectionID is required');
168
+ result = { success: db.deleteCollection(params.collectionID) };
169
+ break;
170
+ case 'get_subcollections':
171
+ if (!params.collectionID) throw new Error('collectionID is required');
172
+ result = db.getSubcollections(params.collectionID);
173
+ break;
174
+ case 'add_item':
175
+ if (!params.itemID || !params.collectionID) throw new Error('itemID and collectionID are required');
176
+ result = { success: db.addItemToCollection(params.itemID, params.collectionID) };
177
+ break;
178
+ case 'remove_item':
179
+ if (!params.itemID || !params.collectionID) throw new Error('itemID and collectionID are required');
180
+ result = { success: db.removeItemFromCollection(params.itemID, params.collectionID) };
181
+ break;
182
+ case 'get_items':
183
+ if (!params.collectionID) throw new Error('collectionID is required');
184
+ result = db.getCollectionItems(params.collectionID);
185
+ break;
186
+ default:
187
+ throw new Error(`Unknown action: ${params.action}`);
182
188
  }
183
189
  break;
184
190
  }
185
191
 
186
- case 'create_collection': {
187
- const params = createCollectionSchema.parse(args);
188
- const collectionID = db.createCollection(
189
- params.name,
190
- params.parentCollectionID || null,
191
- params.libraryID
192
- );
193
- result = { success: true, collectionID };
194
- break;
195
- }
196
-
197
- case 'rename_collection': {
198
- const params = renameCollectionSchema.parse(args);
199
- const success = db.renameCollection(params.collectionID, params.newName);
200
- result = { success };
201
- break;
202
- }
203
-
204
- case 'move_collection': {
205
- const params = moveCollectionSchema.parse(args);
206
- const success = db.moveCollection(params.collectionID, params.newParentID);
207
- result = { success };
208
- break;
209
- }
210
-
211
- case 'delete_collection': {
212
- const params = deleteCollectionSchema.parse(args);
213
- const success = db.deleteCollection(params.collectionID);
214
- result = { success };
215
- break;
216
- }
217
-
218
- case 'get_subcollections': {
219
- const params = getSubcollectionsSchema.parse(args);
220
- result = db.getSubcollections(params.parentCollectionID);
221
- break;
222
- }
223
-
224
192
  // ============================================
225
- // Tag Tools
193
+ // Tag Management (Consolidated)
226
194
  // ============================================
227
- case 'list_tags': {
228
- listTagsSchema.parse(args);
229
- result = db.getTags();
230
- break;
231
- }
232
-
233
- case 'add_tag': {
234
- const params = addTagSchema.parse(args);
235
- const success = db.addTagToItem(params.itemID, params.tagName, params.type);
236
- result = { success };
237
- break;
238
- }
239
-
240
- case 'remove_tag': {
241
- const params = removeTagSchema.parse(args);
242
- const success = db.removeTagFromItem(params.itemID, params.tagName);
243
- result = { success };
244
- break;
245
- }
246
-
247
- case 'get_item_tags': {
248
- const params = getItemTagsSchema.parse(args);
249
- result = db.getItemTags(params.itemID);
250
- break;
251
- }
252
-
253
- case 'create_tag': {
254
- const params = createTagSchema.parse(args);
255
- const tagID = db.createTag(params.name, params.type);
256
- result = { success: true, tagID };
195
+ case 'manage_tags': {
196
+ const params = manageTagsSchema.parse(args);
197
+
198
+ switch (params.action) {
199
+ case 'list':
200
+ result = db.getTags();
201
+ break;
202
+ case 'get_item_tags':
203
+ if (!params.itemID) throw new Error('itemID is required');
204
+ result = db.getItemTags(params.itemID);
205
+ break;
206
+ case 'add':
207
+ if (!params.itemID || !params.tagName) throw new Error('itemID and tagName are required');
208
+ result = { success: db.addTagToItem(params.itemID, params.tagName, params.type) };
209
+ break;
210
+ case 'remove':
211
+ if (!params.itemID || !params.tagName) throw new Error('itemID and tagName are required');
212
+ result = { success: db.removeTagFromItem(params.itemID, params.tagName) };
213
+ break;
214
+ case 'create':
215
+ if (!params.tagName) throw new Error('tagName is required');
216
+ const tagID = db.createTag(params.tagName, params.type);
217
+ result = { success: true, tagID };
218
+ break;
219
+ default:
220
+ throw new Error(`Unknown action: ${params.action}`);
221
+ }
257
222
  break;
258
223
  }
259
224
 
@@ -272,125 +237,190 @@ async function main() {
272
237
  result = db.getItemDetails(params.itemID);
273
238
  } else if (params.itemKey) {
274
239
  const item = db.getItemByKey(params.itemKey);
275
- if (item) {
276
- result = db.getItemDetails(item.itemID);
277
- } else {
278
- result = null;
279
- }
240
+ result = item ? db.getItemDetails(item.itemID) : null;
280
241
  } else {
281
242
  throw new Error('Either itemID or itemKey is required');
282
243
  }
283
244
  break;
284
245
  }
285
246
 
286
- case 'add_item_to_collection': {
287
- const params = addItemToCollectionSchema.parse(args);
288
- const success = db.addItemToCollection(params.itemID, params.collectionID);
289
- result = { success };
290
- break;
291
- }
292
-
293
- case 'remove_item_from_collection': {
294
- const params = removeItemFromCollectionSchema.parse(args);
295
- const success = db.removeItemFromCollection(params.itemID, params.collectionID);
296
- result = { success };
297
- break;
298
- }
299
-
300
- case 'get_collection_items': {
301
- const params = getCollectionItemsSchema.parse(args);
302
- result = db.getCollectionItems(params.collectionID);
303
- break;
304
- }
305
-
306
247
  // ============================================
307
- // Abstract/Note Tools
248
+ // Abstract/Note Management (Consolidated)
308
249
  // ============================================
309
- case 'get_item_abstract': {
310
- const params = getItemAbstractSchema.parse(args);
311
- result = { abstract: db.getItemAbstract(params.itemID) };
312
- break;
313
- }
314
-
315
- case 'set_item_abstract': {
316
- const params = setItemAbstractSchema.parse(args);
317
- const success = db.setItemAbstract(params.itemID, params.abstract);
318
- result = { success };
319
- break;
320
- }
321
-
322
- case 'get_item_notes': {
323
- const params = getItemNotesSchema.parse(args);
324
- result = db.getItemNotes(params.itemID);
325
- break;
326
- }
327
-
328
- case 'add_item_note': {
329
- const params = addItemNoteSchema.parse(args);
330
- const noteID = db.addItemNote(params.itemID, params.content, params.title);
331
- result = { success: true, noteID };
250
+ case 'manage_item_content': {
251
+ const params = manageItemContentSchema.parse(args);
252
+
253
+ switch (params.action) {
254
+ case 'get_abstract':
255
+ result = { abstract: db.getItemAbstract(params.itemID) };
256
+ break;
257
+ case 'set_abstract':
258
+ if (!params.abstract) throw new Error('abstract is required');
259
+ result = { success: db.setItemAbstract(params.itemID, params.abstract) };
260
+ break;
261
+ case 'get_notes':
262
+ result = db.getItemNotes(params.itemID);
263
+ break;
264
+ case 'add_note':
265
+ if (!params.noteContent) throw new Error('noteContent is required');
266
+ const noteID = db.addItemNote(params.itemID, params.noteContent, params.noteTitle);
267
+ result = { success: true, noteID };
268
+ break;
269
+ default:
270
+ throw new Error(`Unknown action: ${params.action}`);
271
+ }
332
272
  break;
333
273
  }
334
274
 
335
275
  // ============================================
336
- // PDF Tools
276
+ // PDF Management (Consolidated)
337
277
  // ============================================
338
- case 'extract_pdf_text': {
339
- const params = extractPDFTextSchema.parse(args);
340
- result = await pdf.extractTextFromAttachment(params.attachmentItemID);
278
+ case 'manage_pdf': {
279
+ const params = managePDFSchema.parse(args);
280
+
281
+ switch (params.action) {
282
+ case 'extract_text':
283
+ if (!params.attachmentItemID) throw new Error('attachmentItemID is required');
284
+ result = await pdf.extractTextFromAttachment(params.attachmentItemID);
285
+ break;
286
+ case 'get_summary':
287
+ if (!params.attachmentItemID) throw new Error('attachmentItemID is required');
288
+ result = await pdf.getPDFSummary(params.attachmentItemID);
289
+ break;
290
+ case 'list':
291
+ if (!params.parentItemID) throw new Error('parentItemID is required');
292
+ const attachments = db.getPDFAttachments(params.parentItemID);
293
+ result = attachments.map(att => ({
294
+ ...att,
295
+ fullPath: db.getAttachmentPath(att.itemID)
296
+ }));
297
+ break;
298
+ case 'search':
299
+ if (!params.attachmentItemID || !params.query) throw new Error('attachmentItemID and query are required');
300
+ const content = await pdf.extractTextFromAttachment(params.attachmentItemID);
301
+ result = content ? pdf.searchInPDF(content, params.query, params.caseSensitive) : [];
302
+ break;
303
+ case 'generate_abstract':
304
+ if (!params.attachmentItemID) throw new Error('attachmentItemID is required');
305
+ const pdfContent = await pdf.extractTextFromAttachment(params.attachmentItemID);
306
+ if (!pdfContent) throw new Error('Could not extract PDF content');
307
+ const abstract = pdf.generateSimpleSummary(pdfContent, params.maxLength);
308
+ if (params.saveToItem) {
309
+ const attDetails = db.query(
310
+ 'SELECT parentItemID FROM itemAttachments WHERE itemID = ?',
311
+ [params.attachmentItemID]
312
+ )[0] as { parentItemID: number } | undefined;
313
+ if (attDetails?.parentItemID) {
314
+ db.setItemAbstract(attDetails.parentItemID, abstract);
315
+ }
316
+ }
317
+ result = { abstract, length: abstract.length };
318
+ break;
319
+ default:
320
+ throw new Error(`Unknown action: ${params.action}`);
321
+ }
341
322
  break;
342
323
  }
343
324
 
344
- case 'get_pdf_summary': {
345
- const params = getPDFSummarySchema.parse(args);
346
- result = await pdf.getPDFSummary(params.attachmentItemID);
325
+ // ============================================
326
+ // Identifier Lookup (Consolidated)
327
+ // ============================================
328
+ case 'find_by_identifier': {
329
+ const params = findByIdentifierSchema.parse(args);
330
+ const identifier = params.identifier.trim();
331
+ let type = params.type;
332
+
333
+ // Auto-detect type
334
+ if (type === 'auto') {
335
+ if (/^10\.\d+\//.test(identifier) || /doi\.org/i.test(identifier)) {
336
+ type = 'doi';
337
+ } else if (/^(97[89])?\d{9}[\dXx]$/.test(identifier.replace(/[-\s]/g, ''))) {
338
+ type = 'isbn';
339
+ } else if (/^\d+$/.test(identifier) || /pubmed|pmid/i.test(identifier)) {
340
+ type = 'pmid';
341
+ } else if (/arxiv/i.test(identifier) || /^\d{4}\.\d{4,5}/.test(identifier)) {
342
+ type = 'arxiv';
343
+ } else if (/^https?:\/\//.test(identifier)) {
344
+ type = 'url';
345
+ } else {
346
+ type = 'doi'; // Default
347
+ }
348
+ }
349
+
350
+ result = db.findItemByIdentifier(identifier, type);
347
351
  break;
348
352
  }
349
353
 
350
- case 'get_item_pdfs': {
351
- const params = getItemPDFsSchema.parse(args);
352
- const attachments = db.getPDFAttachments(params.parentItemID);
353
- result = attachments.map(att => ({
354
- ...att,
355
- fullPath: db.getAttachmentPath(att.itemID)
356
- }));
354
+ // ============================================
355
+ // Annotations (Consolidated)
356
+ // ============================================
357
+ case 'get_annotations': {
358
+ const params = getAnnotationsSchema.parse(args);
359
+
360
+ if (params.searchQuery) {
361
+ result = db.searchAnnotations(params.searchQuery, params.itemID);
362
+ } else if (params.types && params.itemID) {
363
+ result = db.getAnnotationsByType(params.itemID, params.types);
364
+ } else if (params.colors && params.itemID) {
365
+ result = db.getAnnotationsByColor(params.itemID, params.colors);
366
+ } else if (params.attachmentID) {
367
+ result = db.getAttachmentAnnotations(params.attachmentID);
368
+ } else if (params.itemID) {
369
+ result = db.getItemAnnotations(params.itemID);
370
+ } else {
371
+ throw new Error('At least itemID, attachmentID, or searchQuery is required');
372
+ }
357
373
  break;
358
374
  }
359
375
 
360
- case 'search_pdf': {
361
- const params = searchPDFSchema.parse(args);
362
- const content = await pdf.extractTextFromAttachment(params.attachmentItemID);
363
- if (content) {
364
- result = pdf.searchInPDF(content, params.query, params.caseSensitive);
376
+ // ============================================
377
+ // Fulltext Search (Consolidated)
378
+ // ============================================
379
+ case 'search_fulltext': {
380
+ const params = searchFulltextSchema.parse(args);
381
+
382
+ if (params.attachmentID && !params.query) {
383
+ // Get fulltext content
384
+ result = { content: db.getFulltextContent(params.attachmentID) };
385
+ } else if (params.query) {
386
+ // Search with context
387
+ result = db.searchFulltextWithContext(params.query, params.contextLength, params.libraryID);
365
388
  } else {
366
- result = [];
389
+ throw new Error('Either query or attachmentID is required');
367
390
  }
368
391
  break;
369
392
  }
370
393
 
371
- case 'generate_abstract_from_pdf': {
372
- const params = generateAbstractFromPDFSchema.parse(args);
373
- const content = await pdf.extractTextFromAttachment(params.attachmentItemID);
374
-
375
- if (!content) {
376
- throw new Error('Could not extract PDF content');
377
- }
378
-
379
- const abstract = pdf.generateSimpleSummary(content, params.maxLength);
394
+ // ============================================
395
+ // Related Items (Consolidated)
396
+ // ============================================
397
+ case 'find_related_items': {
398
+ const params = findRelatedItemsSchema.parse(args);
380
399
 
381
- if (params.saveToItem) {
382
- // Get parent item ID from attachment
383
- const attDetails = db.query(
384
- 'SELECT parentItemID FROM itemAttachments WHERE itemID = ?',
385
- [params.attachmentItemID]
386
- )[0] as { parentItemID: number } | undefined;
387
-
388
- if (attDetails?.parentItemID) {
389
- db.setItemAbstract(attDetails.parentItemID, abstract);
390
- }
400
+ switch (params.method) {
401
+ case 'manual':
402
+ result = db.getRelatedItems(params.itemID);
403
+ break;
404
+ case 'tags':
405
+ result = db.findSimilarByTags(params.itemID, params.minSharedTags);
406
+ break;
407
+ case 'creators':
408
+ result = db.findSimilarByCreators(params.itemID);
409
+ break;
410
+ case 'collection':
411
+ result = db.findSimilarByCollection(params.itemID);
412
+ break;
413
+ case 'all':
414
+ result = {
415
+ manual: db.getRelatedItems(params.itemID),
416
+ byTags: db.findSimilarByTags(params.itemID, params.minSharedTags),
417
+ byCreators: db.findSimilarByCreators(params.itemID),
418
+ byCollection: db.findSimilarByCollection(params.itemID)
419
+ };
420
+ break;
421
+ default:
422
+ throw new Error(`Unknown method: ${params.method}`);
391
423
  }
392
-
393
- result = { abstract, length: abstract.length };
394
424
  break;
395
425
  }
396
426
 
@@ -411,115 +441,49 @@ async function main() {
411
441
 
412
442
  case 'raw_query': {
413
443
  const params = rawQuerySchema.parse(args);
414
-
415
- // Security check - only allow SELECT queries
416
444
  if (!params.sql.trim().toUpperCase().startsWith('SELECT')) {
417
445
  throw new Error('Only SELECT queries are allowed');
418
446
  }
419
-
420
447
  result = db.query(params.sql, params.params);
421
448
  break;
422
449
  }
423
450
 
424
451
  // ============================================
425
- // Identifier Tools (DOI/ISBN)
426
- // ============================================
427
- case 'find_by_doi': {
428
- const params = findByDOISchema.parse(args);
429
- result = db.findItemByDOI(params.doi);
430
- break;
431
- }
432
-
433
- case 'find_by_isbn': {
434
- const params = findByISBNSchema.parse(args);
435
- result = db.findItemByISBN(params.isbn);
436
- break;
437
- }
438
-
439
- case 'find_by_identifier': {
440
- const params = findByIdentifierSchema.parse(args);
441
- result = db.findItemByIdentifier(params.identifier, params.type);
442
- break;
443
- }
444
-
445
- // ============================================
446
- // Annotation Tools
452
+ // Library Maintenance (Consolidated)
447
453
  // ============================================
448
- case 'get_item_annotations': {
449
- const params = getItemAnnotationsSchema.parse(args);
450
- result = db.getItemAnnotations(params.itemID);
451
- break;
452
- }
453
-
454
- case 'get_attachment_annotations': {
455
- const params = getAttachmentAnnotationsSchema.parse(args);
456
- result = db.getAttachmentAnnotations(params.attachmentID);
457
- break;
458
- }
459
-
460
- case 'get_annotations_by_type': {
461
- const params = getAnnotationsByTypeSchema.parse(args);
462
- result = db.getAnnotationsByType(params.itemID, params.types);
463
- break;
464
- }
465
-
466
- case 'get_annotations_by_color': {
467
- const params = getAnnotationsByColorSchema.parse(args);
468
- result = db.getAnnotationsByColor(params.itemID, params.colors);
469
- break;
470
- }
471
-
472
- case 'search_annotations': {
473
- const params = searchAnnotationsSchema.parse(args);
474
- result = db.searchAnnotations(params.query, params.itemID);
475
- break;
476
- }
477
-
478
- // ============================================
479
- // Fulltext Search Tools
480
- // ============================================
481
- case 'search_fulltext': {
482
- const params = searchFulltextSchema.parse(args);
483
- result = db.searchFulltext(params.query, params.libraryID);
484
- break;
485
- }
486
-
487
- case 'get_fulltext_content': {
488
- const params = getFulltextContentSchema.parse(args);
489
- result = { content: db.getFulltextContent(params.attachmentID) };
490
- break;
491
- }
492
-
493
- case 'search_fulltext_with_context': {
494
- const params = searchFulltextWithContextSchema.parse(args);
495
- result = db.searchFulltextWithContext(params.query, params.contextLength, params.libraryID);
496
- break;
497
- }
498
-
499
- // ============================================
500
- // Related/Similar Items Tools
501
- // ============================================
502
- case 'get_related_items': {
503
- const params = getRelatedItemsSchema.parse(args);
504
- result = db.getRelatedItems(params.itemID);
505
- break;
506
- }
507
-
508
- case 'find_similar_by_tags': {
509
- const params = findSimilarByTagsSchema.parse(args);
510
- result = db.findSimilarByTags(params.itemID, params.minSharedTags);
511
- break;
512
- }
513
-
514
- case 'find_similar_by_creators': {
515
- const params = findSimilarByCreatorsSchema.parse(args);
516
- result = db.findSimilarByCreators(params.itemID);
517
- break;
518
- }
519
-
520
- case 'find_similar_by_collection': {
521
- const params = findSimilarByCollectionSchema.parse(args);
522
- result = db.findSimilarByCollection(params.itemID);
454
+ case 'library_maintenance': {
455
+ const params = libraryMaintenanceSchema.parse(args);
456
+
457
+ switch (params.action) {
458
+ case 'find_duplicates':
459
+ result = db.findDuplicates(params.duplicateField, params.libraryID);
460
+ break;
461
+ case 'validate_attachments':
462
+ result = db.validateAttachments(params.itemID, params.checkAll);
463
+ break;
464
+ case 'get_valid_attachment':
465
+ if (!params.parentItemID) throw new Error('parentItemID is required');
466
+ result = db.getValidAttachment(params.parentItemID, params.contentType);
467
+ break;
468
+ case 'find_with_valid_pdf':
469
+ result = db.findItemsWithValidPDF({
470
+ title: params.title,
471
+ doi: params.doi,
472
+ requireValidPDF: params.requireValidPDF
473
+ });
474
+ break;
475
+ case 'cleanup_orphans':
476
+ result = db.deleteOrphanAttachments(params.dryRun);
477
+ break;
478
+ case 'merge_items':
479
+ if (!params.targetItemID || !params.sourceItemIDs) {
480
+ throw new Error('targetItemID and sourceItemIDs are required');
481
+ }
482
+ result = db.mergeItems(params.targetItemID, params.sourceItemIDs);
483
+ break;
484
+ default:
485
+ throw new Error(`Unknown action: ${params.action}`);
486
+ }
523
487
  break;
524
488
  }
525
489
 
@@ -593,21 +557,19 @@ function zodToJsonSchema(zodType: z.ZodTypeAny): Record<string, any> {
593
557
  return { type: 'boolean' };
594
558
  }
595
559
 
596
- // Handle array types - MUST include items
560
+ // Handle enum types
561
+ if (zodType instanceof z.ZodEnum) {
562
+ return { type: 'string', enum: zodType._def.values };
563
+ }
564
+
565
+ // Handle array types
597
566
  if (zodType instanceof z.ZodArray) {
598
567
  const elementType = zodType._def.type;
599
568
  const itemSchema = zodToJsonSchema(elementType);
600
- // If items schema is empty (from ZodAny), use a more explicit schema
601
569
  if (Object.keys(itemSchema).length === 0) {
602
- return {
603
- type: 'array',
604
- items: { type: 'string' } // Default to string for any type to ensure valid JSON Schema
605
- };
570
+ return { type: 'array', items: { type: 'string' } };
606
571
  }
607
- return {
608
- type: 'array',
609
- items: itemSchema
610
- };
572
+ return { type: 'array', items: itemSchema };
611
573
  }
612
574
 
613
575
  // Handle object types
@@ -615,7 +577,7 @@ function zodToJsonSchema(zodType: z.ZodTypeAny): Record<string, any> {
615
577
  return { type: 'object' };
616
578
  }
617
579
 
618
- // Handle ZodAny - return empty object (accepts any value)
580
+ // Handle ZodAny
619
581
  if (zodType instanceof z.ZodAny) {
620
582
  return {};
621
583
  }