zotero-bridge 1.0.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/LICENSE +21 -0
- package/README-en.md +324 -0
- package/README.md +324 -0
- package/dist/database.d.ts +280 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +1031 -0
- package/dist/database.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +504 -0
- package/dist/index.js.map +1 -0
- package/dist/pdf.d.ts +57 -0
- package/dist/pdf.d.ts.map +1 -0
- package/dist/pdf.js +143 -0
- package/dist/pdf.js.map +1 -0
- package/dist/tools.d.ts +396 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +425 -0
- package/dist/tools.js.map +1 -0
- package/package.json +50 -0
- package/server.json +20 -0
- package/src/database.ts +1225 -0
- package/src/index.ts +630 -0
- package/src/pdf.ts +184 -0
- package/src/tools.ts +489 -0
- package/tsconfig.json +20 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ZoteroBridge - MCP Server for Zotero SQLite Database
|
|
5
|
+
*
|
|
6
|
+
* A Model Context Protocol server that provides direct access to Zotero's
|
|
7
|
+
* SQLite database for collection management, tagging, PDF reading, and more.
|
|
8
|
+
*
|
|
9
|
+
* @author Combjellyshen
|
|
10
|
+
* @version 1.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
14
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
15
|
+
import {
|
|
16
|
+
CallToolRequestSchema,
|
|
17
|
+
ListToolsRequestSchema,
|
|
18
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
|
|
21
|
+
import { ZoteroDatabase } from './database.js';
|
|
22
|
+
import { PDFProcessor } from './pdf.js';
|
|
23
|
+
import {
|
|
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,
|
|
37
|
+
searchItemsSchema,
|
|
38
|
+
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,
|
|
56
|
+
findByIdentifierSchema,
|
|
57
|
+
getItemAnnotationsSchema,
|
|
58
|
+
getAttachmentAnnotationsSchema,
|
|
59
|
+
getAnnotationsByTypeSchema,
|
|
60
|
+
getAnnotationsByColorSchema,
|
|
61
|
+
searchAnnotationsSchema,
|
|
62
|
+
searchFulltextSchema,
|
|
63
|
+
getFulltextContentSchema,
|
|
64
|
+
searchFulltextWithContextSchema,
|
|
65
|
+
getRelatedItemsSchema,
|
|
66
|
+
findSimilarByTagsSchema,
|
|
67
|
+
findSimilarByCreatorsSchema,
|
|
68
|
+
findSimilarByCollectionSchema
|
|
69
|
+
} from './tools.js';
|
|
70
|
+
|
|
71
|
+
// Server configuration
|
|
72
|
+
const SERVER_NAME = 'zotero-bridge';
|
|
73
|
+
const SERVER_VERSION = '1.0.0';
|
|
74
|
+
|
|
75
|
+
// Parse command line arguments
|
|
76
|
+
function parseArgs(): { dbPath?: string; readonly: boolean } {
|
|
77
|
+
const args = process.argv.slice(2);
|
|
78
|
+
let dbPath: string | undefined;
|
|
79
|
+
let readonly = false;
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < args.length; i++) {
|
|
82
|
+
if (args[i] === '--db' || args[i] === '-d') {
|
|
83
|
+
dbPath = args[i + 1];
|
|
84
|
+
i++;
|
|
85
|
+
} else if (args[i] === '--readonly' || args[i] === '-r') {
|
|
86
|
+
readonly = true;
|
|
87
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
88
|
+
console.log(`
|
|
89
|
+
ZoteroBridge - MCP Server for Zotero SQLite Database
|
|
90
|
+
|
|
91
|
+
Usage: zotero-bridge [options]
|
|
92
|
+
|
|
93
|
+
Options:
|
|
94
|
+
-d, --db <path> Path to zotero.sqlite database
|
|
95
|
+
-r, --readonly Open database in read-only mode
|
|
96
|
+
-h, --help Show this help message
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
zotero-bridge --db ~/Zotero/zotero.sqlite
|
|
100
|
+
zotero-bridge --readonly
|
|
101
|
+
`);
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { dbPath, readonly };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Initialize the server
|
|
110
|
+
async function main() {
|
|
111
|
+
const { dbPath, readonly } = parseArgs();
|
|
112
|
+
|
|
113
|
+
// Initialize database
|
|
114
|
+
const db = new ZoteroDatabase(dbPath, readonly);
|
|
115
|
+
await db.connect(); // Connect to database
|
|
116
|
+
const pdf = new PDFProcessor(db);
|
|
117
|
+
|
|
118
|
+
// Create MCP server
|
|
119
|
+
const server = new Server(
|
|
120
|
+
{
|
|
121
|
+
name: SERVER_NAME,
|
|
122
|
+
version: SERVER_VERSION,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
capabilities: {
|
|
126
|
+
tools: {},
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Handle tool listing
|
|
132
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
133
|
+
return {
|
|
134
|
+
tools: toolDefinitions.map(tool => ({
|
|
135
|
+
name: tool.name,
|
|
136
|
+
description: tool.description,
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: 'object' as const,
|
|
139
|
+
properties: Object.fromEntries(
|
|
140
|
+
Object.entries(tool.inputSchema.shape || {}).map(([key, value]) => {
|
|
141
|
+
const zodType = value as z.ZodTypeAny;
|
|
142
|
+
const schema = zodToJsonSchema(zodType);
|
|
143
|
+
return [key, {
|
|
144
|
+
...schema,
|
|
145
|
+
description: zodType.description || ''
|
|
146
|
+
}];
|
|
147
|
+
})
|
|
148
|
+
),
|
|
149
|
+
required: Object.entries(tool.inputSchema.shape || {})
|
|
150
|
+
.filter(([_, value]) => !(value as z.ZodTypeAny).isOptional())
|
|
151
|
+
.map(([key]) => key)
|
|
152
|
+
}
|
|
153
|
+
}))
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Handle tool calls
|
|
158
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
159
|
+
const { name, arguments: args } = request.params;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
let result: any;
|
|
163
|
+
|
|
164
|
+
switch (name) {
|
|
165
|
+
// ============================================
|
|
166
|
+
// Collection Tools
|
|
167
|
+
// ============================================
|
|
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');
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
|
|
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
|
+
// ============================================
|
|
225
|
+
// Tag Tools
|
|
226
|
+
// ============================================
|
|
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 };
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ============================================
|
|
261
|
+
// Item Tools
|
|
262
|
+
// ============================================
|
|
263
|
+
case 'search_items': {
|
|
264
|
+
const params = searchItemsSchema.parse(args);
|
|
265
|
+
result = db.searchItems(params.query, params.limit, params.libraryID);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
case 'get_item_details': {
|
|
270
|
+
const params = getItemDetailsSchema.parse(args);
|
|
271
|
+
if (params.itemID) {
|
|
272
|
+
result = db.getItemDetails(params.itemID);
|
|
273
|
+
} else if (params.itemKey) {
|
|
274
|
+
const item = db.getItemByKey(params.itemKey);
|
|
275
|
+
if (item) {
|
|
276
|
+
result = db.getItemDetails(item.itemID);
|
|
277
|
+
} else {
|
|
278
|
+
result = null;
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
throw new Error('Either itemID or itemKey is required');
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
|
|
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
|
+
// ============================================
|
|
307
|
+
// Abstract/Note Tools
|
|
308
|
+
// ============================================
|
|
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 };
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ============================================
|
|
336
|
+
// PDF Tools
|
|
337
|
+
// ============================================
|
|
338
|
+
case 'extract_pdf_text': {
|
|
339
|
+
const params = extractPDFTextSchema.parse(args);
|
|
340
|
+
result = await pdf.extractTextFromAttachment(params.attachmentItemID);
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
case 'get_pdf_summary': {
|
|
345
|
+
const params = getPDFSummarySchema.parse(args);
|
|
346
|
+
result = await pdf.getPDFSummary(params.attachmentItemID);
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
|
|
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
|
+
}));
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
|
|
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);
|
|
365
|
+
} else {
|
|
366
|
+
result = [];
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
|
|
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);
|
|
380
|
+
|
|
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
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
result = { abstract, length: abstract.length };
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ============================================
|
|
398
|
+
// Utility Tools
|
|
399
|
+
// ============================================
|
|
400
|
+
case 'get_database_info': {
|
|
401
|
+
getDatabaseInfoSchema.parse(args);
|
|
402
|
+
result = {
|
|
403
|
+
path: db.getPath(),
|
|
404
|
+
storagePath: db.getStoragePath(),
|
|
405
|
+
readonly,
|
|
406
|
+
collectionsCount: db.getCollections().length,
|
|
407
|
+
tagsCount: db.getTags().length
|
|
408
|
+
};
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
case 'raw_query': {
|
|
413
|
+
const params = rawQuerySchema.parse(args);
|
|
414
|
+
|
|
415
|
+
// Security check - only allow SELECT queries
|
|
416
|
+
if (!params.sql.trim().toUpperCase().startsWith('SELECT')) {
|
|
417
|
+
throw new Error('Only SELECT queries are allowed');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
result = db.query(params.sql, params.params);
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ============================================
|
|
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
|
|
447
|
+
// ============================================
|
|
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);
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
default:
|
|
527
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
content: [
|
|
532
|
+
{
|
|
533
|
+
type: 'text',
|
|
534
|
+
text: JSON.stringify(result, null, 2)
|
|
535
|
+
}
|
|
536
|
+
]
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
} catch (error) {
|
|
540
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
541
|
+
return {
|
|
542
|
+
content: [
|
|
543
|
+
{
|
|
544
|
+
type: 'text',
|
|
545
|
+
text: JSON.stringify({ error: errorMessage })
|
|
546
|
+
}
|
|
547
|
+
],
|
|
548
|
+
isError: true
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Start the server
|
|
554
|
+
const transport = new StdioServerTransport();
|
|
555
|
+
await server.connect(transport);
|
|
556
|
+
|
|
557
|
+
// Handle shutdown
|
|
558
|
+
process.on('SIGINT', () => {
|
|
559
|
+
db.disconnect();
|
|
560
|
+
process.exit(0);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
process.on('SIGTERM', () => {
|
|
564
|
+
db.disconnect();
|
|
565
|
+
process.exit(0);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
console.error(`ZoteroBridge MCP Server v${SERVER_VERSION} started`);
|
|
569
|
+
console.error(`Database: ${db.getPath()}`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Helper function to convert Zod type to JSON Schema
|
|
573
|
+
function zodToJsonSchema(zodType: z.ZodTypeAny): Record<string, any> {
|
|
574
|
+
// Unwrap optional, default, and nullable types
|
|
575
|
+
if (zodType instanceof z.ZodOptional) {
|
|
576
|
+
return zodToJsonSchema(zodType._def.innerType);
|
|
577
|
+
}
|
|
578
|
+
if (zodType instanceof z.ZodDefault) {
|
|
579
|
+
return zodToJsonSchema(zodType._def.innerType);
|
|
580
|
+
}
|
|
581
|
+
if (zodType instanceof z.ZodNullable) {
|
|
582
|
+
return zodToJsonSchema(zodType._def.innerType);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Handle basic types
|
|
586
|
+
if (zodType instanceof z.ZodString) {
|
|
587
|
+
return { type: 'string' };
|
|
588
|
+
}
|
|
589
|
+
if (zodType instanceof z.ZodNumber) {
|
|
590
|
+
return { type: 'number' };
|
|
591
|
+
}
|
|
592
|
+
if (zodType instanceof z.ZodBoolean) {
|
|
593
|
+
return { type: 'boolean' };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Handle array types - MUST include items
|
|
597
|
+
if (zodType instanceof z.ZodArray) {
|
|
598
|
+
const elementType = zodType._def.type;
|
|
599
|
+
const itemSchema = zodToJsonSchema(elementType);
|
|
600
|
+
// If items schema is empty (from ZodAny), use a more explicit schema
|
|
601
|
+
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
|
+
};
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
type: 'array',
|
|
609
|
+
items: itemSchema
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Handle object types
|
|
614
|
+
if (zodType instanceof z.ZodObject) {
|
|
615
|
+
return { type: 'object' };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Handle ZodAny - return empty object (accepts any value)
|
|
619
|
+
if (zodType instanceof z.ZodAny) {
|
|
620
|
+
return {};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Default to string
|
|
624
|
+
return { type: 'string' };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
main().catch((error) => {
|
|
628
|
+
console.error('Fatal error:', error);
|
|
629
|
+
process.exit(1);
|
|
630
|
+
});
|