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/tools.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
- * ZoteroBridge - MCP Tool Definitions
2
+ * ZoteroBridge - MCP Tool Definitions (Consolidated)
3
3
  *
4
4
  * Defines all available MCP tools for Zotero operations
5
+ * Tools are consolidated to reduce total count while maintaining functionality
5
6
  *
6
7
  * @author Combjellyshen
7
8
  */
@@ -9,69 +10,32 @@
9
10
  import { z } from 'zod';
10
11
 
11
12
  // ============================================
12
- // Collection/Directory Tools
13
+ // Collection Tools (Consolidated)
13
14
  // ============================================
14
15
 
15
- export const listCollectionsSchema = z.object({
16
- libraryID: z.number().optional().default(1).describe('Library ID (default: 1 for personal library)')
17
- });
18
-
19
- export const getCollectionSchema = z.object({
20
- collectionID: z.number().optional().describe('Collection ID'),
21
- name: z.string().optional().describe('Collection name'),
22
- libraryID: z.number().optional().default(1).describe('Library ID')
23
- });
24
-
25
- export const createCollectionSchema = z.object({
26
- name: z.string().describe('Name of the new collection'),
27
- parentCollectionID: z.number().optional().describe('Parent collection ID (null for root)'),
16
+ export const manageCollectionSchema = z.object({
17
+ action: z.enum(['list', 'get', 'create', 'rename', 'move', 'delete', 'get_subcollections', 'add_item', 'remove_item', 'get_items'])
18
+ .describe('Action to perform: list, get, create, rename, move, delete, get_subcollections, add_item, remove_item, get_items'),
19
+ collectionID: z.number().optional().describe('Collection ID (for get/rename/move/delete/get_subcollections/add_item/remove_item/get_items)'),
20
+ name: z.string().optional().describe('Collection name (for get by name or create)'),
21
+ newName: z.string().optional().describe('New name (for rename)'),
22
+ parentCollectionID: z.number().nullable().optional().describe('Parent collection ID (for create/move, null for root)'),
23
+ itemID: z.number().optional().describe('Item ID (for add_item/remove_item)'),
28
24
  libraryID: z.number().optional().default(1).describe('Library ID')
29
25
  });
30
26
 
31
- export const renameCollectionSchema = z.object({
32
- collectionID: z.number().describe('Collection ID to rename'),
33
- newName: z.string().describe('New name for the collection')
34
- });
35
-
36
- export const moveCollectionSchema = z.object({
37
- collectionID: z.number().describe('Collection ID to move'),
38
- newParentID: z.number().nullable().describe('New parent collection ID (null for root)')
39
- });
40
-
41
- export const deleteCollectionSchema = z.object({
42
- collectionID: z.number().describe('Collection ID to delete')
43
- });
44
-
45
- export const getSubcollectionsSchema = z.object({
46
- parentCollectionID: z.number().describe('Parent collection ID')
47
- });
48
-
49
27
  // ============================================
50
- // Tag Tools
28
+ // Tag Tools (Consolidated)
51
29
  // ============================================
52
30
 
53
- export const listTagsSchema = z.object({});
54
-
55
- export const addTagSchema = z.object({
56
- itemID: z.number().describe('Item ID to add tag to'),
57
- tagName: z.string().describe('Tag name to add'),
31
+ export const manageTagsSchema = z.object({
32
+ action: z.enum(['list', 'get_item_tags', 'add', 'remove', 'create'])
33
+ .describe('Action: list (all tags), get_item_tags, add (to item), remove (from item), create'),
34
+ itemID: z.number().optional().describe('Item ID (for get_item_tags/add/remove)'),
35
+ tagName: z.string().optional().describe('Tag name (for add/remove/create)'),
58
36
  type: z.number().optional().default(0).describe('Tag type (0=user, 1=automatic)')
59
37
  });
60
38
 
61
- export const removeTagSchema = z.object({
62
- itemID: z.number().describe('Item ID to remove tag from'),
63
- tagName: z.string().describe('Tag name to remove')
64
- });
65
-
66
- export const getItemTagsSchema = z.object({
67
- itemID: z.number().describe('Item ID to get tags for')
68
- });
69
-
70
- export const createTagSchema = z.object({
71
- name: z.string().describe('Tag name to create'),
72
- type: z.number().optional().default(0).describe('Tag type')
73
- });
74
-
75
39
  // ============================================
76
40
  // Item Tools
77
41
  // ============================================
@@ -87,153 +51,76 @@ export const getItemDetailsSchema = z.object({
87
51
  itemKey: z.string().optional().describe('Item key')
88
52
  });
89
53
 
90
- export const addItemToCollectionSchema = z.object({
91
- itemID: z.number().describe('Item ID to add'),
92
- collectionID: z.number().describe('Collection ID to add item to')
93
- });
94
-
95
- export const removeItemFromCollectionSchema = z.object({
96
- itemID: z.number().describe('Item ID to remove'),
97
- collectionID: z.number().describe('Collection ID to remove item from')
98
- });
99
-
100
- export const getCollectionItemsSchema = z.object({
101
- collectionID: z.number().describe('Collection ID to get items from')
102
- });
103
-
104
54
  // ============================================
105
- // Abstract/Note Tools
55
+ // Abstract/Note Tools (Consolidated)
106
56
  // ============================================
107
57
 
108
- export const getItemAbstractSchema = z.object({
109
- itemID: z.number().describe('Item ID to get abstract for')
110
- });
111
-
112
- export const setItemAbstractSchema = z.object({
113
- itemID: z.number().describe('Item ID to set abstract for'),
114
- abstract: z.string().describe('Abstract text')
115
- });
116
-
117
- export const getItemNotesSchema = z.object({
118
- itemID: z.number().describe('Item ID to get notes for')
119
- });
120
-
121
- export const addItemNoteSchema = z.object({
122
- itemID: z.number().describe('Parent item ID'),
123
- content: z.string().describe('Note content (can include HTML)'),
124
- title: z.string().optional().default('').describe('Note title')
58
+ export const manageItemContentSchema = z.object({
59
+ action: z.enum(['get_abstract', 'set_abstract', 'get_notes', 'add_note'])
60
+ .describe('Action: get_abstract, set_abstract, get_notes, add_note'),
61
+ itemID: z.number().describe('Item ID'),
62
+ abstract: z.string().optional().describe('Abstract text (for set_abstract)'),
63
+ noteContent: z.string().optional().describe('Note content in HTML (for add_note)'),
64
+ noteTitle: z.string().optional().default('').describe('Note title (for add_note)')
125
65
  });
126
66
 
127
67
  // ============================================
128
- // PDF Tools
68
+ // PDF Tools (Consolidated)
129
69
  // ============================================
130
70
 
131
- export const extractPDFTextSchema = z.object({
132
- attachmentItemID: z.number().describe('Attachment item ID of the PDF')
133
- });
134
-
135
- export const getPDFSummarySchema = z.object({
136
- attachmentItemID: z.number().describe('Attachment item ID of the PDF')
137
- });
138
-
139
- export const getItemPDFsSchema = z.object({
140
- parentItemID: z.number().describe('Parent item ID to get PDFs from')
141
- });
142
-
143
- export const searchPDFSchema = z.object({
144
- attachmentItemID: z.number().describe('Attachment item ID of the PDF'),
145
- query: z.string().describe('Search query'),
146
- caseSensitive: z.boolean().optional().default(false).describe('Case sensitive search')
147
- });
148
-
149
- export const generateAbstractFromPDFSchema = z.object({
150
- attachmentItemID: z.number().describe('Attachment item ID of the PDF'),
151
- maxLength: z.number().optional().default(1000).describe('Maximum length of generated abstract'),
152
- saveToItem: z.boolean().optional().default(false).describe('Save generated abstract to parent item')
71
+ export const managePDFSchema = z.object({
72
+ action: z.enum(['extract_text', 'get_summary', 'list', 'search', 'generate_abstract'])
73
+ .describe('Action: extract_text, get_summary, list (PDFs for item), search, generate_abstract'),
74
+ attachmentItemID: z.number().optional().describe('Attachment item ID (for extract_text/get_summary/search/generate_abstract)'),
75
+ parentItemID: z.number().optional().describe('Parent item ID (for list)'),
76
+ query: z.string().optional().describe('Search query (for search)'),
77
+ caseSensitive: z.boolean().optional().default(false).describe('Case sensitive search'),
78
+ maxLength: z.number().optional().default(1000).describe('Max abstract length (for generate_abstract)'),
79
+ saveToItem: z.boolean().optional().default(false).describe('Save abstract to parent item')
153
80
  });
154
81
 
155
82
  // ============================================
156
- // Identifier Tools (DOI/ISBN)
83
+ // Identifier Tools (Consolidated - single tool)
157
84
  // ============================================
158
85
 
159
- export const findByDOISchema = z.object({
160
- doi: z.string().describe('DOI to search for (with or without doi.org prefix)')
161
- });
162
-
163
- export const findByISBNSchema = z.object({
164
- isbn: z.string().describe('ISBN to search for (with or without hyphens)')
165
- });
166
-
167
86
  export const findByIdentifierSchema = z.object({
168
- identifier: z.string().describe('Identifier value (DOI, ISBN, PMID, etc.)'),
169
- type: z.string().optional().describe('Identifier type: doi, isbn, pmid, arxiv, url')
87
+ identifier: z.string().describe('Identifier value (DOI, ISBN, PMID, arXiv ID, or URL)'),
88
+ type: z.enum(['doi', 'isbn', 'pmid', 'arxiv', 'url', 'auto']).optional().default('auto')
89
+ .describe('Identifier type. Use "auto" to detect automatically')
170
90
  });
171
91
 
172
92
  // ============================================
173
- // Annotation Tools
93
+ // Annotation Tools (Consolidated)
174
94
  // ============================================
175
95
 
176
- export const getItemAnnotationsSchema = z.object({
177
- itemID: z.number().describe('Parent item ID to get annotations from')
178
- });
179
-
180
- export const getAttachmentAnnotationsSchema = z.object({
181
- attachmentID: z.number().describe('Attachment item ID to get annotations from')
182
- });
183
-
184
- export const getAnnotationsByTypeSchema = z.object({
185
- itemID: z.number().describe('Parent item ID'),
186
- types: z.array(z.string()).describe('Annotation types: highlight, note, image, ink, underline')
187
- });
188
-
189
- export const getAnnotationsByColorSchema = z.object({
190
- itemID: z.number().describe('Parent item ID'),
191
- colors: z.array(z.string()).describe('Annotation colors (hex codes like #ffff00)')
192
- });
193
-
194
- export const searchAnnotationsSchema = z.object({
195
- query: z.string().describe('Search query for annotation text/comments'),
196
- itemID: z.number().optional().describe('Limit search to specific item')
96
+ export const getAnnotationsSchema = z.object({
97
+ itemID: z.number().optional().describe('Parent item ID to get annotations from'),
98
+ attachmentID: z.number().optional().describe('Specific attachment ID'),
99
+ types: z.array(z.string()).optional().describe('Filter by types: highlight, note, image, ink, underline'),
100
+ colors: z.array(z.string()).optional().describe('Filter by colors (hex codes like #ffff00)'),
101
+ searchQuery: z.string().optional().describe('Search in annotation text/comments')
197
102
  });
198
103
 
199
104
  // ============================================
200
- // Fulltext Search Tools
105
+ // Fulltext Search Tools (Consolidated)
201
106
  // ============================================
202
107
 
203
108
  export const searchFulltextSchema = z.object({
204
- query: z.string().describe('Search query for fulltext content'),
205
- libraryID: z.number().optional().default(1).describe('Library ID')
206
- });
207
-
208
- export const getFulltextContentSchema = z.object({
209
- attachmentID: z.number().describe('Attachment item ID to get fulltext from')
210
- });
211
-
212
- export const searchFulltextWithContextSchema = z.object({
213
- query: z.string().describe('Search query'),
109
+ query: z.string().optional().describe('Search query for fulltext content'),
110
+ attachmentID: z.number().optional().describe('Get fulltext for specific attachment (if no query)'),
214
111
  contextLength: z.number().optional().default(100).describe('Characters of context around matches'),
215
112
  libraryID: z.number().optional().default(1).describe('Library ID')
216
113
  });
217
114
 
218
115
  // ============================================
219
- // Similar Items Tools
116
+ // Similar/Related Items Tools (Consolidated)
220
117
  // ============================================
221
118
 
222
- export const getRelatedItemsSchema = z.object({
223
- itemID: z.number().describe('Item ID to get related items for')
224
- });
225
-
226
- export const findSimilarByTagsSchema = z.object({
227
- itemID: z.number().describe('Item ID to find similar items for'),
228
- minSharedTags: z.number().optional().default(2).describe('Minimum number of shared tags')
229
- });
230
-
231
- export const findSimilarByCreatorsSchema = z.object({
232
- itemID: z.number().describe('Item ID to find similar items for')
233
- });
234
-
235
- export const findSimilarByCollectionSchema = z.object({
236
- itemID: z.number().describe('Item ID to find similar items for')
119
+ export const findRelatedItemsSchema = z.object({
120
+ itemID: z.number().describe('Item ID to find related items for'),
121
+ method: z.enum(['manual', 'tags', 'creators', 'collection', 'all']).optional().default('all')
122
+ .describe('Method: manual (linked), tags, creators, collection, or all'),
123
+ minSharedTags: z.number().optional().default(2).describe('Min shared tags (for tags method)')
237
124
  });
238
125
 
239
126
  // ============================================
@@ -248,75 +135,51 @@ export const rawQuerySchema = z.object({
248
135
  });
249
136
 
250
137
  // ============================================
251
- // Tool Definitions
138
+ // Library Maintenance Tools (Consolidated)
139
+ // ============================================
140
+
141
+ export const libraryMaintenanceSchema = z.object({
142
+ action: z.enum(['find_duplicates', 'validate_attachments', 'get_valid_attachment', 'find_with_valid_pdf', 'cleanup_orphans', 'merge_items'])
143
+ .describe('Action: find_duplicates, validate_attachments, get_valid_attachment, find_with_valid_pdf, cleanup_orphans, merge_items'),
144
+ // For find_duplicates
145
+ duplicateField: z.enum(['title', 'doi', 'isbn']).optional().default('title').describe('Field to check for duplicates'),
146
+ // For validate_attachments
147
+ itemID: z.number().optional().describe('Item ID to check attachments for'),
148
+ checkAll: z.boolean().optional().default(false).describe('Check all attachments in library'),
149
+ // For get_valid_attachment / find_with_valid_pdf
150
+ parentItemID: z.number().optional().describe('Parent item ID'),
151
+ contentType: z.string().optional().default('application/pdf').describe('Content type filter'),
152
+ title: z.string().optional().describe('Search by title'),
153
+ doi: z.string().optional().describe('Search by DOI'),
154
+ requireValidPDF: z.boolean().optional().default(true).describe('Only return items with valid PDFs'),
155
+ // For cleanup_orphans
156
+ dryRun: z.boolean().optional().default(true).describe('Only report, do not delete'),
157
+ // For merge_items
158
+ targetItemID: z.number().optional().describe('Target item to keep'),
159
+ sourceItemIDs: z.array(z.number()).optional().describe('Source items to merge from'),
160
+ libraryID: z.number().optional().default(1).describe('Library ID')
161
+ });
162
+
163
+ // ============================================
164
+ // Tool Definitions (Consolidated: 42 → 12 tools)
252
165
  // ============================================
253
166
 
254
167
  export const toolDefinitions = [
255
- // Collection Tools
256
- {
257
- name: 'list_collections',
258
- description: 'List all collections (folders) in the Zotero library',
259
- inputSchema: listCollectionsSchema
260
- },
261
- {
262
- name: 'get_collection',
263
- description: 'Get a collection by ID or name',
264
- inputSchema: getCollectionSchema
265
- },
266
- {
267
- name: 'create_collection',
268
- description: 'Create a new collection (folder)',
269
- inputSchema: createCollectionSchema
270
- },
271
- {
272
- name: 'rename_collection',
273
- description: 'Rename an existing collection',
274
- inputSchema: renameCollectionSchema
275
- },
168
+ // Collection Management (7 → 1)
276
169
  {
277
- name: 'move_collection',
278
- description: 'Move a collection to a new parent (or root)',
279
- inputSchema: moveCollectionSchema
280
- },
281
- {
282
- name: 'delete_collection',
283
- description: 'Delete a collection',
284
- inputSchema: deleteCollectionSchema
285
- },
286
- {
287
- name: 'get_subcollections',
288
- description: 'Get all subcollections of a collection',
289
- inputSchema: getSubcollectionsSchema
170
+ name: 'manage_collection',
171
+ description: 'Manage Zotero collections: list, get, create, rename, move, delete, get_subcollections, add/remove items, get collection items',
172
+ inputSchema: manageCollectionSchema
290
173
  },
291
174
 
292
- // Tag Tools
293
- {
294
- name: 'list_tags',
295
- description: 'List all tags in the library',
296
- inputSchema: listTagsSchema
297
- },
298
- {
299
- name: 'add_tag',
300
- description: 'Add a tag to an item',
301
- inputSchema: addTagSchema
302
- },
175
+ // Tag Management (5 → 1)
303
176
  {
304
- name: 'remove_tag',
305
- description: 'Remove a tag from an item',
306
- inputSchema: removeTagSchema
307
- },
308
- {
309
- name: 'get_item_tags',
310
- description: 'Get all tags for an item',
311
- inputSchema: getItemTagsSchema
312
- },
313
- {
314
- name: 'create_tag',
315
- description: 'Create a new tag',
316
- inputSchema: createTagSchema
177
+ name: 'manage_tags',
178
+ description: 'Manage tags: list all, get item tags, add/remove tags from items, create new tags',
179
+ inputSchema: manageTagsSchema
317
180
  },
318
181
 
319
- // Item Tools
182
+ // Item Search & Details (kept separate as core functionality)
320
183
  {
321
184
  name: 'search_items',
322
185
  description: 'Search items by title',
@@ -324,166 +187,68 @@ export const toolDefinitions = [
324
187
  },
325
188
  {
326
189
  name: 'get_item_details',
327
- description: 'Get detailed information about an item',
190
+ description: 'Get detailed information about an item by ID or key',
328
191
  inputSchema: getItemDetailsSchema
329
192
  },
330
- {
331
- name: 'add_item_to_collection',
332
- description: 'Add an item to a collection',
333
- inputSchema: addItemToCollectionSchema
334
- },
335
- {
336
- name: 'remove_item_from_collection',
337
- description: 'Remove an item from a collection',
338
- inputSchema: removeItemFromCollectionSchema
339
- },
340
- {
341
- name: 'get_collection_items',
342
- description: 'Get all items in a collection',
343
- inputSchema: getCollectionItemsSchema
344
- },
345
193
 
346
- // Abstract/Note Tools
347
- {
348
- name: 'get_item_abstract',
349
- description: 'Get the abstract of an item',
350
- inputSchema: getItemAbstractSchema
351
- },
352
- {
353
- name: 'set_item_abstract',
354
- description: 'Set or update the abstract of an item',
355
- inputSchema: setItemAbstractSchema
356
- },
357
- {
358
- name: 'get_item_notes',
359
- description: 'Get all notes attached to an item',
360
- inputSchema: getItemNotesSchema
361
- },
194
+ // Abstract & Notes (4 → 1)
362
195
  {
363
- name: 'add_item_note',
364
- description: 'Add a note to an item',
365
- inputSchema: addItemNoteSchema
196
+ name: 'manage_item_content',
197
+ description: 'Manage item content: get/set abstract, get/add notes',
198
+ inputSchema: manageItemContentSchema
366
199
  },
367
200
 
368
- // PDF Tools
201
+ // PDF Operations (5 → 1)
369
202
  {
370
- name: 'extract_pdf_text',
371
- description: 'Extract full text from a PDF attachment',
372
- inputSchema: extractPDFTextSchema
373
- },
374
- {
375
- name: 'get_pdf_summary',
376
- description: 'Get summary information about a PDF',
377
- inputSchema: getPDFSummarySchema
378
- },
379
- {
380
- name: 'get_item_pdfs',
381
- description: 'Get all PDF attachments for an item',
382
- inputSchema: getItemPDFsSchema
383
- },
384
- {
385
- name: 'search_pdf',
386
- description: 'Search within a PDF for specific text',
387
- inputSchema: searchPDFSchema
388
- },
389
- {
390
- name: 'generate_abstract_from_pdf',
391
- description: 'Extract and generate an abstract from PDF content',
392
- inputSchema: generateAbstractFromPDFSchema
203
+ name: 'manage_pdf',
204
+ description: 'PDF operations: extract text, get summary, list PDFs, search within PDF, generate abstract',
205
+ inputSchema: managePDFSchema
393
206
  },
394
207
 
395
- // Identifier Tools
396
- {
397
- name: 'find_by_doi',
398
- description: 'Find an item by its DOI',
399
- inputSchema: findByDOISchema
400
- },
401
- {
402
- name: 'find_by_isbn',
403
- description: 'Find an item by its ISBN',
404
- inputSchema: findByISBNSchema
405
- },
208
+ // Identifier Lookup (3 → 1)
406
209
  {
407
210
  name: 'find_by_identifier',
408
- description: 'Find an item by any identifier (DOI, ISBN, PMID, arXiv, URL)',
211
+ description: 'Find item by identifier (DOI, ISBN, PMID, arXiv, URL) with auto-detection',
409
212
  inputSchema: findByIdentifierSchema
410
213
  },
411
214
 
412
- // Annotation Tools
413
- {
414
- name: 'get_item_annotations',
415
- description: 'Get all annotations (highlights, notes, etc.) from an item\'s PDF attachments',
416
- inputSchema: getItemAnnotationsSchema
417
- },
418
- {
419
- name: 'get_attachment_annotations',
420
- description: 'Get annotations from a specific PDF attachment',
421
- inputSchema: getAttachmentAnnotationsSchema
422
- },
215
+ // Annotations (5 → 1)
423
216
  {
424
- name: 'get_annotations_by_type',
425
- description: 'Get annotations filtered by type (highlight, note, image, ink, underline)',
426
- inputSchema: getAnnotationsByTypeSchema
427
- },
428
- {
429
- name: 'get_annotations_by_color',
430
- description: 'Get annotations filtered by color',
431
- inputSchema: getAnnotationsByColorSchema
432
- },
433
- {
434
- name: 'search_annotations',
435
- description: 'Search annotations by text content',
436
- inputSchema: searchAnnotationsSchema
217
+ name: 'get_annotations',
218
+ description: 'Get annotations from PDFs with optional filters by type, color, or search query',
219
+ inputSchema: getAnnotationsSchema
437
220
  },
438
221
 
439
- // Fulltext Search Tools
222
+ // Fulltext Search (3 → 1)
440
223
  {
441
224
  name: 'search_fulltext',
442
- description: 'Search in Zotero\'s fulltext index (searches indexed PDF content)',
225
+ description: 'Search Zotero fulltext index or get fulltext content of an attachment',
443
226
  inputSchema: searchFulltextSchema
444
227
  },
445
- {
446
- name: 'get_fulltext_content',
447
- description: 'Get the indexed fulltext content of an attachment',
448
- inputSchema: getFulltextContentSchema
449
- },
450
- {
451
- name: 'search_fulltext_with_context',
452
- description: 'Search fulltext and return matching context snippets',
453
- inputSchema: searchFulltextWithContextSchema
454
- },
455
228
 
456
- // Similar Items Tools
457
- {
458
- name: 'get_related_items',
459
- description: 'Get manually linked related items',
460
- inputSchema: getRelatedItemsSchema
461
- },
462
- {
463
- name: 'find_similar_by_tags',
464
- description: 'Find items with similar tags',
465
- inputSchema: findSimilarByTagsSchema
466
- },
467
- {
468
- name: 'find_similar_by_creators',
469
- description: 'Find items by the same authors/creators',
470
- inputSchema: findSimilarByCreatorsSchema
471
- },
229
+ // Related Items (4 → 1)
472
230
  {
473
- name: 'find_similar_by_collection',
474
- description: 'Find items in the same collections',
475
- inputSchema: findSimilarByCollectionSchema
231
+ name: 'find_related_items',
232
+ description: 'Find related items by manual links, shared tags, creators, or collection',
233
+ inputSchema: findRelatedItemsSchema
476
234
  },
477
235
 
478
- // Utility Tools
236
+ // Utilities
479
237
  {
480
238
  name: 'get_database_info',
481
- description: 'Get information about the Zotero database',
239
+ description: 'Get Zotero database information (path, storage, counts)',
482
240
  inputSchema: getDatabaseInfoSchema
483
241
  },
484
242
  {
485
243
  name: 'raw_query',
486
- description: 'Execute a raw SQL SELECT query (read-only)',
244
+ description: 'Execute raw SQL SELECT query (read-only)',
487
245
  inputSchema: rawQuerySchema
246
+ },
247
+
248
+ // Library Maintenance (6 → 1)
249
+ {
250
+ name: 'library_maintenance',
251
+ description: 'Library maintenance: find duplicates, validate attachments, get valid attachment, find items with valid PDF, cleanup orphans, merge items',
252
+ inputSchema: libraryMaintenanceSchema
488
253
  }
489
254
  ];