zotero-bridge 1.1.0 → 1.1.2

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)
326
- // ============================================
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
- }
429
+ // Library Maintenance (Consolidated)
388
430
  // ============================================
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:
@@ -448,50 +503,63 @@ async function main() {
448
503
  console.error(`ZoteroBridge MCP Server v${SERVER_VERSION} started`);
449
504
  console.error(`Database: ${db.getPath()}`);
450
505
  }
451
- // Helper function to convert Zod type to JSON Schema
506
+ // Helper function to convert Zod type to JSON Schema (Zod v4 compatible)
452
507
  function zodToJsonSchema(zodType) {
453
- // Unwrap optional, default, and nullable types
454
- if (zodType instanceof z.ZodOptional) {
455
- return zodToJsonSchema(zodType._def.innerType);
508
+ // Get the type name from _zod property or check type directly
509
+ const typeName = zodType?._zod?.def?.type || zodType?.constructor?.name || '';
510
+ // Unwrap optional types
511
+ if (typeName === 'optional' || zodType instanceof z.ZodOptional) {
512
+ const inner = zodType._zod?.def?.innerType || zodType._def?.innerType;
513
+ if (inner)
514
+ return zodToJsonSchema(inner);
456
515
  }
457
- if (zodType instanceof z.ZodDefault) {
458
- return zodToJsonSchema(zodType._def.innerType);
516
+ // Unwrap default types
517
+ if (typeName === 'default' || zodType instanceof z.ZodDefault) {
518
+ const inner = zodType._zod?.def?.innerType || zodType._def?.innerType;
519
+ if (inner)
520
+ return zodToJsonSchema(inner);
459
521
  }
460
- if (zodType instanceof z.ZodNullable) {
461
- return zodToJsonSchema(zodType._def.innerType);
522
+ // Unwrap nullable types
523
+ if (typeName === 'nullable' || zodType instanceof z.ZodNullable) {
524
+ const inner = zodType._zod?.def?.innerType || zodType._def?.innerType;
525
+ if (inner)
526
+ return zodToJsonSchema(inner);
462
527
  }
463
528
  // Handle basic types
464
- if (zodType instanceof z.ZodString) {
529
+ if (typeName === 'string' || zodType instanceof z.ZodString) {
465
530
  return { type: 'string' };
466
531
  }
467
- if (zodType instanceof z.ZodNumber) {
532
+ if (typeName === 'number' || zodType instanceof z.ZodNumber) {
468
533
  return { type: 'number' };
469
534
  }
470
- if (zodType instanceof z.ZodBoolean) {
535
+ if (typeName === 'boolean' || zodType instanceof z.ZodBoolean) {
471
536
  return { type: 'boolean' };
472
537
  }
473
- // Handle array types - MUST include items
474
- if (zodType instanceof z.ZodArray) {
475
- const elementType = zodType._def.type;
476
- const itemSchema = zodToJsonSchema(elementType);
477
- // If items schema is empty (from ZodAny), use a more explicit schema
478
- 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
- };
538
+ // Handle enum types (Zod v4 uses 'entries' instead of 'values')
539
+ if (typeName === 'enum' || zodType instanceof z.ZodEnum) {
540
+ const enumValues = zodType._zod?.def?.entries
541
+ || zodType._def?.values
542
+ || Object.keys(zodType._zod?.def?.entries || {});
543
+ return { type: 'string', enum: Array.isArray(enumValues) ? enumValues : Object.keys(enumValues) };
544
+ }
545
+ // Handle array types
546
+ if (typeName === 'array' || zodType instanceof z.ZodArray) {
547
+ const elementType = zodType._zod?.def?.element || zodType._def?.type;
548
+ if (elementType) {
549
+ const itemSchema = zodToJsonSchema(elementType);
550
+ if (Object.keys(itemSchema).length === 0) {
551
+ return { type: 'array', items: { type: 'string' } };
552
+ }
553
+ return { type: 'array', items: itemSchema };
483
554
  }
484
- return {
485
- type: 'array',
486
- items: itemSchema
487
- };
555
+ return { type: 'array', items: { type: 'string' } };
488
556
  }
489
557
  // Handle object types
490
- if (zodType instanceof z.ZodObject) {
558
+ if (typeName === 'object' || zodType instanceof z.ZodObject) {
491
559
  return { type: 'object' };
492
560
  }
493
- // Handle ZodAny - return empty object (accepts any value)
494
- if (zodType instanceof z.ZodAny) {
561
+ // Handle ZodAny
562
+ if (typeName === 'any' || zodType instanceof z.ZodAny) {
495
563
  return {};
496
564
  }
497
565
  // Default to string