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/dist/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * SQLite database for collection management, tagging, PDF reading, and more.
7
7
  *
8
8
  * @author Combjellyshen
9
- * @version 1.0.0
9
+ * @version 1.1.0 (Consolidated tools)
10
10
  */
11
11
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
12
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -14,12 +14,10 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
14
14
  import { z } from 'zod';
15
15
  import { ZoteroDatabase } from './database.js';
16
16
  import { PDFProcessor } from './pdf.js';
17
- import { toolDefinitions, listCollectionsSchema, getCollectionSchema, createCollectionSchema, renameCollectionSchema, moveCollectionSchema, deleteCollectionSchema, getSubcollectionsSchema, listTagsSchema, addTagSchema, removeTagSchema, getItemTagsSchema, createTagSchema, searchItemsSchema, getItemDetailsSchema, addItemToCollectionSchema, removeItemFromCollectionSchema, getCollectionItemsSchema, getItemAbstractSchema, setItemAbstractSchema, getItemNotesSchema, addItemNoteSchema, extractPDFTextSchema, getPDFSummarySchema, getItemPDFsSchema, searchPDFSchema, generateAbstractFromPDFSchema, getDatabaseInfoSchema, rawQuerySchema,
18
- // New schemas
19
- findByDOISchema, findByISBNSchema, findByIdentifierSchema, getItemAnnotationsSchema, getAttachmentAnnotationsSchema, getAnnotationsByTypeSchema, getAnnotationsByColorSchema, searchAnnotationsSchema, searchFulltextSchema, getFulltextContentSchema, searchFulltextWithContextSchema, getRelatedItemsSchema, findSimilarByTagsSchema, findSimilarByCreatorsSchema, findSimilarByCollectionSchema } from './tools.js';
17
+ import { toolDefinitions, manageCollectionSchema, manageTagsSchema, searchItemsSchema, getItemDetailsSchema, manageItemContentSchema, managePDFSchema, findByIdentifierSchema, getAnnotationsSchema, searchFulltextSchema, findRelatedItemsSchema, getDatabaseInfoSchema, rawQuerySchema, libraryMaintenanceSchema } from './tools.js';
20
18
  // Server configuration
21
19
  const SERVER_NAME = 'zotero-bridge';
22
- const SERVER_VERSION = '1.0.0';
20
+ const SERVER_VERSION = '1.1.0';
23
21
  // Parse command line arguments
24
22
  function parseArgs() {
25
23
  const args = process.argv.slice(2);
@@ -58,7 +56,7 @@ async function main() {
58
56
  const { dbPath, readonly } = parseArgs();
59
57
  // Initialize database
60
58
  const db = new ZoteroDatabase(dbPath, readonly);
61
- await db.connect(); // Connect to database
59
+ await db.connect();
62
60
  const pdf = new PDFProcessor(db);
63
61
  // Create MCP server
64
62
  const server = new Server({
@@ -99,84 +97,104 @@ async function main() {
99
97
  let result;
100
98
  switch (name) {
101
99
  // ============================================
102
- // Collection Tools
100
+ // Collection Management (Consolidated)
103
101
  // ============================================
104
- case 'list_collections': {
105
- const params = listCollectionsSchema.parse(args);
106
- result = db.getCollections(params.libraryID);
107
- break;
108
- }
109
- case 'get_collection': {
110
- const params = getCollectionSchema.parse(args);
111
- if (params.collectionID) {
112
- result = db.getCollectionById(params.collectionID);
113
- }
114
- else if (params.name) {
115
- result = db.getCollectionByName(params.name, params.libraryID);
116
- }
117
- else {
118
- throw new Error('Either collectionID or name is required');
102
+ case 'manage_collection': {
103
+ const params = manageCollectionSchema.parse(args);
104
+ switch (params.action) {
105
+ case 'list':
106
+ result = db.getCollections(params.libraryID);
107
+ break;
108
+ case 'get':
109
+ if (params.collectionID) {
110
+ result = db.getCollectionById(params.collectionID);
111
+ }
112
+ else if (params.name) {
113
+ result = db.getCollectionByName(params.name, params.libraryID);
114
+ }
115
+ else {
116
+ throw new Error('Either collectionID or name is required for get action');
117
+ }
118
+ break;
119
+ case 'create':
120
+ if (!params.name)
121
+ throw new Error('Name is required for create action');
122
+ const collectionID = db.createCollection(params.name, params.parentCollectionID || null, params.libraryID);
123
+ result = { success: true, collectionID };
124
+ break;
125
+ case 'rename':
126
+ if (!params.collectionID || !params.newName)
127
+ throw new Error('collectionID and newName are required');
128
+ result = { success: db.renameCollection(params.collectionID, params.newName) };
129
+ break;
130
+ case 'move':
131
+ if (!params.collectionID)
132
+ throw new Error('collectionID is required');
133
+ result = { success: db.moveCollection(params.collectionID, params.parentCollectionID ?? null) };
134
+ break;
135
+ case 'delete':
136
+ if (!params.collectionID)
137
+ throw new Error('collectionID is required');
138
+ result = { success: db.deleteCollection(params.collectionID) };
139
+ break;
140
+ case 'get_subcollections':
141
+ if (!params.collectionID)
142
+ throw new Error('collectionID is required');
143
+ result = db.getSubcollections(params.collectionID);
144
+ break;
145
+ case 'add_item':
146
+ if (!params.itemID || !params.collectionID)
147
+ throw new Error('itemID and collectionID are required');
148
+ result = { success: db.addItemToCollection(params.itemID, params.collectionID) };
149
+ break;
150
+ case 'remove_item':
151
+ if (!params.itemID || !params.collectionID)
152
+ throw new Error('itemID and collectionID are required');
153
+ result = { success: db.removeItemFromCollection(params.itemID, params.collectionID) };
154
+ break;
155
+ case 'get_items':
156
+ if (!params.collectionID)
157
+ throw new Error('collectionID is required');
158
+ result = db.getCollectionItems(params.collectionID);
159
+ break;
160
+ default:
161
+ throw new Error(`Unknown action: ${params.action}`);
119
162
  }
120
163
  break;
121
164
  }
122
- case 'create_collection': {
123
- const params = createCollectionSchema.parse(args);
124
- const collectionID = db.createCollection(params.name, params.parentCollectionID || null, params.libraryID);
125
- result = { success: true, collectionID };
126
- break;
127
- }
128
- case 'rename_collection': {
129
- const params = renameCollectionSchema.parse(args);
130
- const success = db.renameCollection(params.collectionID, params.newName);
131
- result = { success };
132
- break;
133
- }
134
- case 'move_collection': {
135
- const params = moveCollectionSchema.parse(args);
136
- const success = db.moveCollection(params.collectionID, params.newParentID);
137
- result = { success };
138
- break;
139
- }
140
- case 'delete_collection': {
141
- const params = deleteCollectionSchema.parse(args);
142
- const success = db.deleteCollection(params.collectionID);
143
- result = { success };
144
- break;
145
- }
146
- case 'get_subcollections': {
147
- const params = getSubcollectionsSchema.parse(args);
148
- result = db.getSubcollections(params.parentCollectionID);
149
- break;
150
- }
151
165
  // ============================================
152
- // Tag Tools
166
+ // Tag Management (Consolidated)
153
167
  // ============================================
154
- case 'list_tags': {
155
- listTagsSchema.parse(args);
156
- result = db.getTags();
157
- break;
158
- }
159
- case 'add_tag': {
160
- const params = addTagSchema.parse(args);
161
- const success = db.addTagToItem(params.itemID, params.tagName, params.type);
162
- result = { success };
163
- break;
164
- }
165
- case 'remove_tag': {
166
- const params = removeTagSchema.parse(args);
167
- const success = db.removeTagFromItem(params.itemID, params.tagName);
168
- result = { success };
169
- break;
170
- }
171
- case 'get_item_tags': {
172
- const params = getItemTagsSchema.parse(args);
173
- result = db.getItemTags(params.itemID);
174
- break;
175
- }
176
- case 'create_tag': {
177
- const params = createTagSchema.parse(args);
178
- const tagID = db.createTag(params.name, params.type);
179
- result = { success: true, tagID };
168
+ case 'manage_tags': {
169
+ const params = manageTagsSchema.parse(args);
170
+ switch (params.action) {
171
+ case 'list':
172
+ result = db.getTags();
173
+ break;
174
+ case 'get_item_tags':
175
+ if (!params.itemID)
176
+ throw new Error('itemID is required');
177
+ result = db.getItemTags(params.itemID);
178
+ break;
179
+ case 'add':
180
+ if (!params.itemID || !params.tagName)
181
+ throw new Error('itemID and tagName are required');
182
+ result = { success: db.addTagToItem(params.itemID, params.tagName, params.type) };
183
+ break;
184
+ case 'remove':
185
+ if (!params.itemID || !params.tagName)
186
+ throw new Error('itemID and tagName are required');
187
+ result = { success: db.removeTagFromItem(params.itemID, params.tagName) };
188
+ break;
189
+ case 'create':
190
+ if (!params.tagName)
191
+ throw new Error('tagName is required');
192
+ const tagID = db.createTag(params.tagName, params.type);
193
+ result = { success: true, tagID };
194
+ break;
195
+ default:
196
+ throw new Error(`Unknown action: ${params.action}`);
197
+ }
180
198
  break;
181
199
  }
182
200
  // ============================================
@@ -194,108 +212,195 @@ async function main() {
194
212
  }
195
213
  else if (params.itemKey) {
196
214
  const item = db.getItemByKey(params.itemKey);
197
- if (item) {
198
- result = db.getItemDetails(item.itemID);
199
- }
200
- else {
201
- result = null;
202
- }
215
+ result = item ? db.getItemDetails(item.itemID) : null;
203
216
  }
204
217
  else {
205
218
  throw new Error('Either itemID or itemKey is required');
206
219
  }
207
220
  break;
208
221
  }
209
- case 'add_item_to_collection': {
210
- const params = addItemToCollectionSchema.parse(args);
211
- const success = db.addItemToCollection(params.itemID, params.collectionID);
212
- result = { success };
213
- break;
214
- }
215
- case 'remove_item_from_collection': {
216
- const params = removeItemFromCollectionSchema.parse(args);
217
- const success = db.removeItemFromCollection(params.itemID, params.collectionID);
218
- result = { success };
219
- break;
220
- }
221
- case 'get_collection_items': {
222
- const params = getCollectionItemsSchema.parse(args);
223
- result = db.getCollectionItems(params.collectionID);
224
- break;
225
- }
226
222
  // ============================================
227
- // Abstract/Note Tools
223
+ // Abstract/Note Management (Consolidated)
228
224
  // ============================================
229
- case 'get_item_abstract': {
230
- const params = getItemAbstractSchema.parse(args);
231
- result = { abstract: db.getItemAbstract(params.itemID) };
232
- break;
233
- }
234
- case 'set_item_abstract': {
235
- const params = setItemAbstractSchema.parse(args);
236
- const success = db.setItemAbstract(params.itemID, params.abstract);
237
- result = { success };
238
- break;
239
- }
240
- case 'get_item_notes': {
241
- const params = getItemNotesSchema.parse(args);
242
- result = db.getItemNotes(params.itemID);
243
- break;
244
- }
245
- case 'add_item_note': {
246
- const params = addItemNoteSchema.parse(args);
247
- const noteID = db.addItemNote(params.itemID, params.content, params.title);
248
- result = { success: true, noteID };
225
+ case 'manage_item_content': {
226
+ const params = manageItemContentSchema.parse(args);
227
+ switch (params.action) {
228
+ case 'get_abstract':
229
+ result = { abstract: db.getItemAbstract(params.itemID) };
230
+ break;
231
+ case 'set_abstract':
232
+ if (!params.abstract)
233
+ throw new Error('abstract is required');
234
+ result = { success: db.setItemAbstract(params.itemID, params.abstract) };
235
+ break;
236
+ case 'get_notes':
237
+ result = db.getItemNotes(params.itemID);
238
+ break;
239
+ case 'add_note':
240
+ if (!params.noteContent)
241
+ throw new Error('noteContent is required');
242
+ const noteID = db.addItemNote(params.itemID, params.noteContent, params.noteTitle);
243
+ result = { success: true, noteID };
244
+ break;
245
+ default:
246
+ throw new Error(`Unknown action: ${params.action}`);
247
+ }
249
248
  break;
250
249
  }
251
250
  // ============================================
252
- // PDF Tools
251
+ // PDF Management (Consolidated)
253
252
  // ============================================
254
- case 'extract_pdf_text': {
255
- const params = extractPDFTextSchema.parse(args);
256
- result = await pdf.extractTextFromAttachment(params.attachmentItemID);
253
+ case 'manage_pdf': {
254
+ const params = managePDFSchema.parse(args);
255
+ switch (params.action) {
256
+ case 'extract_text':
257
+ if (!params.attachmentItemID)
258
+ throw new Error('attachmentItemID is required');
259
+ result = await pdf.extractTextFromAttachment(params.attachmentItemID);
260
+ break;
261
+ case 'get_summary':
262
+ if (!params.attachmentItemID)
263
+ throw new Error('attachmentItemID is required');
264
+ result = await pdf.getPDFSummary(params.attachmentItemID);
265
+ break;
266
+ case 'list':
267
+ if (!params.parentItemID)
268
+ throw new Error('parentItemID is required');
269
+ const attachments = db.getPDFAttachments(params.parentItemID);
270
+ result = attachments.map(att => ({
271
+ ...att,
272
+ fullPath: db.getAttachmentPath(att.itemID)
273
+ }));
274
+ break;
275
+ case 'search':
276
+ if (!params.attachmentItemID || !params.query)
277
+ throw new Error('attachmentItemID and query are required');
278
+ const content = await pdf.extractTextFromAttachment(params.attachmentItemID);
279
+ result = content ? pdf.searchInPDF(content, params.query, params.caseSensitive) : [];
280
+ break;
281
+ case 'generate_abstract':
282
+ if (!params.attachmentItemID)
283
+ throw new Error('attachmentItemID is required');
284
+ const pdfContent = await pdf.extractTextFromAttachment(params.attachmentItemID);
285
+ if (!pdfContent)
286
+ throw new Error('Could not extract PDF content');
287
+ const abstract = pdf.generateSimpleSummary(pdfContent, params.maxLength);
288
+ if (params.saveToItem) {
289
+ const attDetails = db.query('SELECT parentItemID FROM itemAttachments WHERE itemID = ?', [params.attachmentItemID])[0];
290
+ if (attDetails?.parentItemID) {
291
+ db.setItemAbstract(attDetails.parentItemID, abstract);
292
+ }
293
+ }
294
+ result = { abstract, length: abstract.length };
295
+ break;
296
+ default:
297
+ throw new Error(`Unknown action: ${params.action}`);
298
+ }
257
299
  break;
258
300
  }
259
- case 'get_pdf_summary': {
260
- const params = getPDFSummarySchema.parse(args);
261
- result = await pdf.getPDFSummary(params.attachmentItemID);
301
+ // ============================================
302
+ // Identifier Lookup (Consolidated)
303
+ // ============================================
304
+ case 'find_by_identifier': {
305
+ const params = findByIdentifierSchema.parse(args);
306
+ const identifier = params.identifier.trim();
307
+ let type = params.type;
308
+ // Auto-detect type
309
+ if (type === 'auto') {
310
+ if (/^10\.\d+\//.test(identifier) || /doi\.org/i.test(identifier)) {
311
+ type = 'doi';
312
+ }
313
+ else if (/^(97[89])?\d{9}[\dXx]$/.test(identifier.replace(/[-\s]/g, ''))) {
314
+ type = 'isbn';
315
+ }
316
+ else if (/^\d+$/.test(identifier) || /pubmed|pmid/i.test(identifier)) {
317
+ type = 'pmid';
318
+ }
319
+ else if (/arxiv/i.test(identifier) || /^\d{4}\.\d{4,5}/.test(identifier)) {
320
+ type = 'arxiv';
321
+ }
322
+ else if (/^https?:\/\//.test(identifier)) {
323
+ type = 'url';
324
+ }
325
+ else {
326
+ type = 'doi'; // Default
327
+ }
328
+ }
329
+ result = db.findItemByIdentifier(identifier, type);
262
330
  break;
263
331
  }
264
- case 'get_item_pdfs': {
265
- const params = getItemPDFsSchema.parse(args);
266
- const attachments = db.getPDFAttachments(params.parentItemID);
267
- result = attachments.map(att => ({
268
- ...att,
269
- fullPath: db.getAttachmentPath(att.itemID)
270
- }));
332
+ // ============================================
333
+ // Annotations (Consolidated)
334
+ // ============================================
335
+ case 'get_annotations': {
336
+ const params = getAnnotationsSchema.parse(args);
337
+ if (params.searchQuery) {
338
+ result = db.searchAnnotations(params.searchQuery, params.itemID);
339
+ }
340
+ else if (params.types && params.itemID) {
341
+ result = db.getAnnotationsByType(params.itemID, params.types);
342
+ }
343
+ else if (params.colors && params.itemID) {
344
+ result = db.getAnnotationsByColor(params.itemID, params.colors);
345
+ }
346
+ else if (params.attachmentID) {
347
+ result = db.getAttachmentAnnotations(params.attachmentID);
348
+ }
349
+ else if (params.itemID) {
350
+ result = db.getItemAnnotations(params.itemID);
351
+ }
352
+ else {
353
+ throw new Error('At least itemID, attachmentID, or searchQuery is required');
354
+ }
271
355
  break;
272
356
  }
273
- case 'search_pdf': {
274
- const params = searchPDFSchema.parse(args);
275
- const content = await pdf.extractTextFromAttachment(params.attachmentItemID);
276
- if (content) {
277
- result = pdf.searchInPDF(content, params.query, params.caseSensitive);
357
+ // ============================================
358
+ // Fulltext Search (Consolidated)
359
+ // ============================================
360
+ case 'search_fulltext': {
361
+ const params = searchFulltextSchema.parse(args);
362
+ if (params.attachmentID && !params.query) {
363
+ // Get fulltext content
364
+ result = { content: db.getFulltextContent(params.attachmentID) };
365
+ }
366
+ else if (params.query) {
367
+ // Search with context
368
+ result = db.searchFulltextWithContext(params.query, params.contextLength, params.libraryID);
278
369
  }
279
370
  else {
280
- result = [];
371
+ throw new Error('Either query or attachmentID is required');
281
372
  }
282
373
  break;
283
374
  }
284
- case 'generate_abstract_from_pdf': {
285
- const params = generateAbstractFromPDFSchema.parse(args);
286
- const content = await pdf.extractTextFromAttachment(params.attachmentItemID);
287
- if (!content) {
288
- throw new Error('Could not extract PDF content');
289
- }
290
- const abstract = pdf.generateSimpleSummary(content, params.maxLength);
291
- if (params.saveToItem) {
292
- // Get parent item ID from attachment
293
- const attDetails = db.query('SELECT parentItemID FROM itemAttachments WHERE itemID = ?', [params.attachmentItemID])[0];
294
- if (attDetails?.parentItemID) {
295
- db.setItemAbstract(attDetails.parentItemID, abstract);
296
- }
375
+ // ============================================
376
+ // Related Items (Consolidated)
377
+ // ============================================
378
+ case 'find_related_items': {
379
+ const params = findRelatedItemsSchema.parse(args);
380
+ switch (params.method) {
381
+ case 'manual':
382
+ result = db.getRelatedItems(params.itemID);
383
+ break;
384
+ case 'tags':
385
+ result = db.findSimilarByTags(params.itemID, params.minSharedTags);
386
+ break;
387
+ case 'creators':
388
+ result = db.findSimilarByCreators(params.itemID);
389
+ break;
390
+ case 'collection':
391
+ result = db.findSimilarByCollection(params.itemID);
392
+ break;
393
+ case 'all':
394
+ result = {
395
+ manual: db.getRelatedItems(params.itemID),
396
+ byTags: db.findSimilarByTags(params.itemID, params.minSharedTags),
397
+ byCreators: db.findSimilarByCreators(params.itemID),
398
+ byCollection: db.findSimilarByCollection(params.itemID)
399
+ };
400
+ break;
401
+ default:
402
+ throw new Error(`Unknown method: ${params.method}`);
297
403
  }
298
- result = { abstract, length: abstract.length };
299
404
  break;
300
405
  }
301
406
  // ============================================
@@ -314,7 +419,6 @@ async function main() {
314
419
  }
315
420
  case 'raw_query': {
316
421
  const params = rawQuerySchema.parse(args);
317
- // Security check - only allow SELECT queries
318
422
  if (!params.sql.trim().toUpperCase().startsWith('SELECT')) {
319
423
  throw new Error('Only SELECT queries are allowed');
320
424
  }
@@ -322,90 +426,41 @@ async function main() {
322
426
  break;
323
427
  }
324
428
  // ============================================
325
- // Identifier Tools (DOI/ISBN)
429
+ // Library Maintenance (Consolidated)
326
430
  // ============================================
327
- case 'find_by_doi': {
328
- const params = findByDOISchema.parse(args);
329
- result = db.findItemByDOI(params.doi);
330
- break;
331
- }
332
- case 'find_by_isbn': {
333
- const params = findByISBNSchema.parse(args);
334
- result = db.findItemByISBN(params.isbn);
335
- break;
336
- }
337
- case 'find_by_identifier': {
338
- const params = findByIdentifierSchema.parse(args);
339
- result = db.findItemByIdentifier(params.identifier, params.type);
340
- break;
341
- }
342
- // ============================================
343
- // Annotation Tools
344
- // ============================================
345
- case 'get_item_annotations': {
346
- const params = getItemAnnotationsSchema.parse(args);
347
- result = db.getItemAnnotations(params.itemID);
348
- break;
349
- }
350
- case 'get_attachment_annotations': {
351
- const params = getAttachmentAnnotationsSchema.parse(args);
352
- result = db.getAttachmentAnnotations(params.attachmentID);
353
- break;
354
- }
355
- case 'get_annotations_by_type': {
356
- const params = getAnnotationsByTypeSchema.parse(args);
357
- result = db.getAnnotationsByType(params.itemID, params.types);
358
- break;
359
- }
360
- case 'get_annotations_by_color': {
361
- const params = getAnnotationsByColorSchema.parse(args);
362
- result = db.getAnnotationsByColor(params.itemID, params.colors);
363
- break;
364
- }
365
- case 'search_annotations': {
366
- const params = searchAnnotationsSchema.parse(args);
367
- result = db.searchAnnotations(params.query, params.itemID);
368
- break;
369
- }
370
- // ============================================
371
- // Fulltext Search Tools
372
- // ============================================
373
- case 'search_fulltext': {
374
- const params = searchFulltextSchema.parse(args);
375
- result = db.searchFulltext(params.query, params.libraryID);
376
- break;
377
- }
378
- case 'get_fulltext_content': {
379
- const params = getFulltextContentSchema.parse(args);
380
- result = { content: db.getFulltextContent(params.attachmentID) };
381
- break;
382
- }
383
- case 'search_fulltext_with_context': {
384
- const params = searchFulltextWithContextSchema.parse(args);
385
- result = db.searchFulltextWithContext(params.query, params.contextLength, params.libraryID);
386
- break;
387
- }
388
- // ============================================
389
- // Related/Similar Items Tools
390
- // ============================================
391
- case 'get_related_items': {
392
- const params = getRelatedItemsSchema.parse(args);
393
- result = db.getRelatedItems(params.itemID);
394
- break;
395
- }
396
- case 'find_similar_by_tags': {
397
- const params = findSimilarByTagsSchema.parse(args);
398
- result = db.findSimilarByTags(params.itemID, params.minSharedTags);
399
- break;
400
- }
401
- case 'find_similar_by_creators': {
402
- const params = findSimilarByCreatorsSchema.parse(args);
403
- result = db.findSimilarByCreators(params.itemID);
404
- break;
405
- }
406
- case 'find_similar_by_collection': {
407
- const params = findSimilarByCollectionSchema.parse(args);
408
- result = db.findSimilarByCollection(params.itemID);
431
+ case 'library_maintenance': {
432
+ const params = libraryMaintenanceSchema.parse(args);
433
+ switch (params.action) {
434
+ case 'find_duplicates':
435
+ result = db.findDuplicates(params.duplicateField, params.libraryID);
436
+ break;
437
+ case 'validate_attachments':
438
+ result = db.validateAttachments(params.itemID, params.checkAll);
439
+ break;
440
+ case 'get_valid_attachment':
441
+ if (!params.parentItemID)
442
+ throw new Error('parentItemID is required');
443
+ result = db.getValidAttachment(params.parentItemID, params.contentType);
444
+ break;
445
+ case 'find_with_valid_pdf':
446
+ result = db.findItemsWithValidPDF({
447
+ title: params.title,
448
+ doi: params.doi,
449
+ requireValidPDF: params.requireValidPDF
450
+ });
451
+ break;
452
+ case 'cleanup_orphans':
453
+ result = db.deleteOrphanAttachments(params.dryRun);
454
+ break;
455
+ case 'merge_items':
456
+ if (!params.targetItemID || !params.sourceItemIDs) {
457
+ throw new Error('targetItemID and sourceItemIDs are required');
458
+ }
459
+ result = db.mergeItems(params.targetItemID, params.sourceItemIDs);
460
+ break;
461
+ default:
462
+ throw new Error(`Unknown action: ${params.action}`);
463
+ }
409
464
  break;
410
465
  }
411
466
  default:
@@ -470,27 +525,24 @@ function zodToJsonSchema(zodType) {
470
525
  if (zodType instanceof z.ZodBoolean) {
471
526
  return { type: 'boolean' };
472
527
  }
473
- // Handle array types - MUST include items
528
+ // Handle enum types
529
+ if (zodType instanceof z.ZodEnum) {
530
+ return { type: 'string', enum: zodType._def.values };
531
+ }
532
+ // Handle array types
474
533
  if (zodType instanceof z.ZodArray) {
475
534
  const elementType = zodType._def.type;
476
535
  const itemSchema = zodToJsonSchema(elementType);
477
- // If items schema is empty (from ZodAny), use a more explicit schema
478
536
  if (Object.keys(itemSchema).length === 0) {
479
- return {
480
- type: 'array',
481
- items: { type: 'string' } // Default to string for any type to ensure valid JSON Schema
482
- };
537
+ return { type: 'array', items: { type: 'string' } };
483
538
  }
484
- return {
485
- type: 'array',
486
- items: itemSchema
487
- };
539
+ return { type: 'array', items: itemSchema };
488
540
  }
489
541
  // Handle object types
490
542
  if (zodType instanceof z.ZodObject) {
491
543
  return { type: 'object' };
492
544
  }
493
- // Handle ZodAny - return empty object (accepts any value)
545
+ // Handle ZodAny
494
546
  if (zodType instanceof z.ZodAny) {
495
547
  return {};
496
548
  }