tryll-dataset-builder-mcp 1.0.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/index.js +310 -90
- package/package.json +2 -1
package/index.js
CHANGED
|
@@ -4,19 +4,64 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
import { Store } from "./lib/store.js";
|
|
7
|
+
import WebSocket from "ws";
|
|
7
8
|
|
|
8
9
|
const store = new Store(process.env.DATA_DIR);
|
|
9
10
|
|
|
10
11
|
const server = new Server(
|
|
11
|
-
{ name: "tryll-dataset-builder", version: "1.
|
|
12
|
+
{ name: "tryll-dataset-builder", version: "1.1.0" },
|
|
12
13
|
{ capabilities: { tools: {} } }
|
|
13
14
|
);
|
|
14
15
|
|
|
16
|
+
// ============================================
|
|
17
|
+
// SESSION CONNECTION STATE
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
let sessionWs = null; // WebSocket to the web app
|
|
21
|
+
let sessionBase = null; // e.g. "http://localhost:3000"
|
|
22
|
+
let sessionCode = null; // e.g. "4F8K2M"
|
|
23
|
+
|
|
24
|
+
function isConnected() {
|
|
25
|
+
return sessionWs && sessionWs.readyState === WebSocket.OPEN;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function apiCall(method, path, body) {
|
|
29
|
+
const url = `${sessionBase}${path}`;
|
|
30
|
+
const opts = {
|
|
31
|
+
method,
|
|
32
|
+
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
};
|
|
34
|
+
if (body !== undefined) opts.body = JSON.stringify(body);
|
|
35
|
+
const res = await fetch(url, opts);
|
|
36
|
+
const data = await res.json();
|
|
37
|
+
if (!res.ok) throw new Error(data.error || `API ${res.status}`);
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
|
|
15
41
|
// ============================================
|
|
16
42
|
// TOOL DEFINITIONS
|
|
17
43
|
// ============================================
|
|
18
44
|
|
|
19
45
|
const TOOLS = [
|
|
46
|
+
// ---- Session ----
|
|
47
|
+
{
|
|
48
|
+
name: "connect_session",
|
|
49
|
+
description: "Connect to the Dataset Builder web app for real-time collaboration. After connecting, all operations will appear live in the browser. The user will give you a 6-character session code shown in the web app's topbar. Default server: https://trylljsoncreator.onrender.com",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
code: { type: "string", description: "6-character session code shown in the web app's topbar" },
|
|
54
|
+
url: { type: "string", description: "Web app URL. Default: https://trylljsoncreator.onrender.com. Only change if self-hosting." },
|
|
55
|
+
},
|
|
56
|
+
required: ["code"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "disconnect_session",
|
|
61
|
+
description: "Disconnect from the Dataset Builder web app. Operations will switch back to local file storage.",
|
|
62
|
+
inputSchema: { type: "object", properties: {} },
|
|
63
|
+
},
|
|
64
|
+
|
|
20
65
|
// ---- Project ----
|
|
21
66
|
{
|
|
22
67
|
name: "create_project",
|
|
@@ -272,6 +317,112 @@ const TOOLS = [
|
|
|
272
317
|
|
|
273
318
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
274
319
|
|
|
320
|
+
// ============================================
|
|
321
|
+
// REMOTE API HANDLERS (when connected to web app)
|
|
322
|
+
// ============================================
|
|
323
|
+
|
|
324
|
+
async function handleRemote(name, args) {
|
|
325
|
+
const p = (n) => encodeURIComponent(n);
|
|
326
|
+
const s = sessionCode;
|
|
327
|
+
|
|
328
|
+
switch (name) {
|
|
329
|
+
case "create_project":
|
|
330
|
+
return apiCall('POST', '/api/projects', { name: args.name, session: s });
|
|
331
|
+
case "list_projects":
|
|
332
|
+
return apiCall('GET', '/api/projects');
|
|
333
|
+
case "delete_project":
|
|
334
|
+
return apiCall('DELETE', `/api/projects/${p(args.name)}?session=${s}`);
|
|
335
|
+
case "get_project_stats":
|
|
336
|
+
return apiCall('GET', `/api/projects/${p(args.name)}/stats`);
|
|
337
|
+
case "create_category":
|
|
338
|
+
return apiCall('POST', `/api/projects/${p(args.project)}/categories`, { name: args.name, session: s });
|
|
339
|
+
case "list_categories":
|
|
340
|
+
return apiCall('GET', `/api/projects/${p(args.project)}/categories`);
|
|
341
|
+
case "rename_category":
|
|
342
|
+
return apiCall('PUT', `/api/projects/${p(args.project)}/categories/${p(args.old_name)}`, { newName: args.new_name, session: s });
|
|
343
|
+
case "delete_category":
|
|
344
|
+
return apiCall('DELETE', `/api/projects/${p(args.project)}/categories/${p(args.name)}?session=${s}`);
|
|
345
|
+
case "add_chunk":
|
|
346
|
+
return apiCall('POST', `/api/projects/${p(args.project)}/categories/${p(args.category)}/chunks`, {
|
|
347
|
+
id: args.id, text: args.text, metadata: args.metadata, session: s,
|
|
348
|
+
});
|
|
349
|
+
case "bulk_add_chunks":
|
|
350
|
+
return apiCall('POST', `/api/projects/${p(args.project)}/categories/${p(args.category)}/chunks/bulk`, {
|
|
351
|
+
chunks: args.chunks, session: s,
|
|
352
|
+
});
|
|
353
|
+
case "get_chunk": {
|
|
354
|
+
const proj = await apiCall('GET', `/api/projects/${p(args.project)}`);
|
|
355
|
+
for (const cat of proj.categories) {
|
|
356
|
+
const ch = cat.chunks.find(c => c.id === args.id);
|
|
357
|
+
if (ch) return { ...ch, category: cat.name };
|
|
358
|
+
}
|
|
359
|
+
throw new Error(`Chunk "${args.id}" not found`);
|
|
360
|
+
}
|
|
361
|
+
case "update_chunk": {
|
|
362
|
+
const proj2 = await apiCall('GET', `/api/projects/${p(args.project)}`);
|
|
363
|
+
for (const cat of proj2.categories) {
|
|
364
|
+
const ch = cat.chunks.find(c => c.id === args.id);
|
|
365
|
+
if (ch) {
|
|
366
|
+
const body = { session: s };
|
|
367
|
+
if (args.new_id !== undefined) body.id = args.new_id;
|
|
368
|
+
if (args.text !== undefined) body.text = args.text;
|
|
369
|
+
const meta = {};
|
|
370
|
+
if (args.page_title !== undefined) meta.page_title = args.page_title;
|
|
371
|
+
if (args.source !== undefined) meta.source = args.source;
|
|
372
|
+
if (args.license !== undefined) meta.license = args.license;
|
|
373
|
+
if (Object.keys(meta).length) body.metadata = meta;
|
|
374
|
+
if (args.metadata) {
|
|
375
|
+
body.customFields = Object.entries(args.metadata).map(([key, value]) => ({ key, value: String(value ?? '') }));
|
|
376
|
+
}
|
|
377
|
+
return apiCall('PUT', `/api/projects/${p(args.project)}/categories/${cat.id}/chunks/${ch._uid}`, body);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
throw new Error(`Chunk "${args.id}" not found`);
|
|
381
|
+
}
|
|
382
|
+
case "delete_chunk": {
|
|
383
|
+
const proj3 = await apiCall('GET', `/api/projects/${p(args.project)}`);
|
|
384
|
+
for (const cat of proj3.categories) {
|
|
385
|
+
const ch = cat.chunks.find(c => c.id === args.id);
|
|
386
|
+
if (ch) {
|
|
387
|
+
return apiCall('DELETE', `/api/projects/${p(args.project)}/categories/${cat.id}/chunks/${ch._uid}?session=${s}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
throw new Error(`Chunk "${args.id}" not found`);
|
|
391
|
+
}
|
|
392
|
+
case "duplicate_chunk": {
|
|
393
|
+
const proj4 = await apiCall('GET', `/api/projects/${p(args.project)}`);
|
|
394
|
+
for (const cat of proj4.categories) {
|
|
395
|
+
const ch = cat.chunks.find(c => c.id === args.id);
|
|
396
|
+
if (ch) {
|
|
397
|
+
return apiCall('POST', `/api/projects/${p(args.project)}/categories/${cat.id}/chunks/${ch._uid}/duplicate`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
throw new Error(`Chunk "${args.id}" not found`);
|
|
401
|
+
}
|
|
402
|
+
case "move_chunk":
|
|
403
|
+
return apiCall('POST', `/api/projects/${p(args.project)}/chunks/${p(args.id)}/move`, {
|
|
404
|
+
targetCategory: args.target_category, session: s,
|
|
405
|
+
});
|
|
406
|
+
case "search_chunks":
|
|
407
|
+
return apiCall('GET', `/api/projects/${p(args.project)}/search?q=${encodeURIComponent(args.query)}`);
|
|
408
|
+
case "export_project":
|
|
409
|
+
return apiCall('GET', `/api/projects/${p(args.project)}/export`);
|
|
410
|
+
case "import_json": {
|
|
411
|
+
let jsonData = args.data;
|
|
412
|
+
if (!jsonData && args.json_path) {
|
|
413
|
+
const { readFileSync } = await import('fs');
|
|
414
|
+
jsonData = JSON.parse(readFileSync(args.json_path, 'utf-8'));
|
|
415
|
+
}
|
|
416
|
+
if (!jsonData) throw new Error('Provide either "json_path" or "data" parameter');
|
|
417
|
+
return apiCall('POST', `/api/projects/${p(args.project)}/import`, {
|
|
418
|
+
data: jsonData, category: args.category, session: s,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
default:
|
|
422
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
275
426
|
// ============================================
|
|
276
427
|
// CALL TOOL
|
|
277
428
|
// ============================================
|
|
@@ -282,100 +433,169 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
282
433
|
try {
|
|
283
434
|
let result;
|
|
284
435
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
case "delete_category":
|
|
311
|
-
result = store.deleteCategory(args.project, args.name);
|
|
312
|
-
break;
|
|
313
|
-
|
|
314
|
-
// ---- Chunk ----
|
|
315
|
-
case "add_chunk":
|
|
316
|
-
result = store.addChunk(args.project, args.category, {
|
|
317
|
-
id: args.id,
|
|
318
|
-
text: args.text,
|
|
319
|
-
metadata: args.metadata,
|
|
436
|
+
// ---- Session tools ----
|
|
437
|
+
if (name === "connect_session") {
|
|
438
|
+
if (isConnected()) {
|
|
439
|
+
sessionWs.close();
|
|
440
|
+
sessionWs = null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const base = (args.url || 'https://trylljsoncreator.onrender.com').replace(/\/+$/, '');
|
|
444
|
+
const code = args.code.toUpperCase().trim();
|
|
445
|
+
|
|
446
|
+
// Test the connection with a health check
|
|
447
|
+
const health = await fetch(`${base}/health`).then(r => r.json()).catch(() => null);
|
|
448
|
+
if (!health) throw new Error(`Cannot reach ${base}. Is the Dataset Builder server running?`);
|
|
449
|
+
|
|
450
|
+
// Open WebSocket
|
|
451
|
+
const wsProto = base.startsWith('https') ? 'wss' : 'ws';
|
|
452
|
+
const wsHost = base.replace(/^https?:\/\//, '');
|
|
453
|
+
const wsUrl = `${wsProto}://${wsHost}/ws?session=${code}&type=mcp`;
|
|
454
|
+
|
|
455
|
+
await new Promise((resolve, reject) => {
|
|
456
|
+
const ws = new WebSocket(wsUrl);
|
|
457
|
+
const timer = setTimeout(() => { ws.close(); reject(new Error('Connection timed out')); }, 5000);
|
|
458
|
+
|
|
459
|
+
ws.on('open', () => {
|
|
460
|
+
clearTimeout(timer);
|
|
320
461
|
});
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
462
|
+
|
|
463
|
+
ws.on('message', (raw) => {
|
|
464
|
+
try {
|
|
465
|
+
const msg = JSON.parse(raw.toString());
|
|
466
|
+
if (msg.event === 'connected') {
|
|
467
|
+
sessionWs = ws;
|
|
468
|
+
sessionBase = base;
|
|
469
|
+
sessionCode = code;
|
|
470
|
+
resolve();
|
|
471
|
+
} else if (msg.event === 'error') {
|
|
472
|
+
clearTimeout(timer);
|
|
473
|
+
ws.close();
|
|
474
|
+
reject(new Error(msg.data?.message || 'Connection rejected'));
|
|
475
|
+
}
|
|
476
|
+
} catch {}
|
|
336
477
|
});
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
478
|
+
|
|
479
|
+
ws.on('error', (err) => {
|
|
480
|
+
clearTimeout(timer);
|
|
481
|
+
reject(new Error(`WebSocket error: ${err.message}`));
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
ws.on('close', () => {
|
|
485
|
+
if (!sessionWs) {
|
|
486
|
+
clearTimeout(timer);
|
|
487
|
+
reject(new Error('Connection closed unexpectedly'));
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Handle unexpected disconnects
|
|
493
|
+
sessionWs.on('close', () => {
|
|
494
|
+
console.error('Disconnected from web app');
|
|
495
|
+
sessionWs = null;
|
|
496
|
+
sessionBase = null;
|
|
497
|
+
sessionCode = null;
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
result = { connected: true, session: code, server: base };
|
|
501
|
+
|
|
502
|
+
} else if (name === "disconnect_session") {
|
|
503
|
+
if (sessionWs) {
|
|
504
|
+
sessionWs.close();
|
|
505
|
+
sessionWs = null;
|
|
506
|
+
sessionBase = null;
|
|
507
|
+
sessionCode = null;
|
|
364
508
|
}
|
|
509
|
+
result = { disconnected: true };
|
|
510
|
+
|
|
511
|
+
} else if (isConnected()) {
|
|
512
|
+
// ---- Remote mode: proxy through web app API ----
|
|
513
|
+
result = await handleRemote(name, args);
|
|
365
514
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
515
|
+
} else {
|
|
516
|
+
// ---- Local mode: use local store ----
|
|
517
|
+
switch (name) {
|
|
518
|
+
case "create_project":
|
|
519
|
+
result = store.createProject(args.name);
|
|
520
|
+
break;
|
|
521
|
+
case "list_projects":
|
|
522
|
+
result = store.listProjects();
|
|
523
|
+
break;
|
|
524
|
+
case "delete_project":
|
|
525
|
+
result = store.deleteProject(args.name);
|
|
526
|
+
break;
|
|
527
|
+
case "get_project_stats":
|
|
528
|
+
result = store.getStats(args.name);
|
|
529
|
+
break;
|
|
530
|
+
case "create_category":
|
|
531
|
+
result = store.createCategory(args.project, args.name);
|
|
532
|
+
break;
|
|
533
|
+
case "list_categories":
|
|
534
|
+
result = store.listCategories(args.project);
|
|
535
|
+
break;
|
|
536
|
+
case "rename_category":
|
|
537
|
+
result = store.renameCategory(args.project, args.old_name, args.new_name);
|
|
538
|
+
break;
|
|
539
|
+
case "delete_category":
|
|
540
|
+
result = store.deleteCategory(args.project, args.name);
|
|
541
|
+
break;
|
|
542
|
+
case "add_chunk":
|
|
543
|
+
result = store.addChunk(args.project, args.category, {
|
|
544
|
+
id: args.id, text: args.text, metadata: args.metadata,
|
|
545
|
+
});
|
|
546
|
+
break;
|
|
547
|
+
case "bulk_add_chunks":
|
|
548
|
+
result = store.bulkAddChunks(args.project, args.category, args.chunks);
|
|
549
|
+
break;
|
|
550
|
+
case "get_chunk":
|
|
551
|
+
result = store.getChunk(args.project, args.id);
|
|
552
|
+
break;
|
|
553
|
+
case "update_chunk":
|
|
554
|
+
result = store.updateChunk(args.project, args.id, {
|
|
555
|
+
newId: args.new_id, text: args.text, page_title: args.page_title,
|
|
556
|
+
source: args.source, license: args.license, metadata: args.metadata,
|
|
557
|
+
});
|
|
558
|
+
break;
|
|
559
|
+
case "delete_chunk":
|
|
560
|
+
result = store.deleteChunk(args.project, args.id);
|
|
561
|
+
break;
|
|
562
|
+
case "duplicate_chunk":
|
|
563
|
+
result = store.duplicateChunk(args.project, args.id);
|
|
564
|
+
break;
|
|
565
|
+
case "move_chunk":
|
|
566
|
+
result = store.moveChunk(args.project, args.id, args.target_category);
|
|
567
|
+
break;
|
|
568
|
+
case "search_chunks":
|
|
569
|
+
result = store.searchChunks(args.project, args.query);
|
|
570
|
+
break;
|
|
571
|
+
|
|
572
|
+
case "export_project": {
|
|
573
|
+
const exported = store.exportProject(args.project);
|
|
574
|
+
if (args.save_to_file) {
|
|
575
|
+
const outPath = store._filePath(args.project).replace('.json', '.export.json');
|
|
576
|
+
const { writeFileSync } = await import('fs');
|
|
577
|
+
writeFileSync(outPath, JSON.stringify(exported, null, 2), 'utf-8');
|
|
578
|
+
result = { exported: exported.length, savedTo: outPath };
|
|
579
|
+
} else {
|
|
580
|
+
result = { exported: exported.length, data: exported };
|
|
581
|
+
}
|
|
582
|
+
break;
|
|
371
583
|
}
|
|
372
|
-
if (!jsonData) throw new Error('Provide either "json_path" or "data" parameter');
|
|
373
|
-
result = store.importJSON(args.project, jsonData, args.category);
|
|
374
|
-
break;
|
|
375
|
-
}
|
|
376
584
|
|
|
377
|
-
|
|
378
|
-
|
|
585
|
+
case "import_json": {
|
|
586
|
+
let jsonData = args.data;
|
|
587
|
+
if (!jsonData && args.json_path) {
|
|
588
|
+
const { readFileSync } = await import('fs');
|
|
589
|
+
jsonData = JSON.parse(readFileSync(args.json_path, 'utf-8'));
|
|
590
|
+
}
|
|
591
|
+
if (!jsonData) throw new Error('Provide either "json_path" or "data" parameter');
|
|
592
|
+
result = store.importJSON(args.project, jsonData, args.category);
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
default:
|
|
597
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
598
|
+
}
|
|
379
599
|
}
|
|
380
600
|
|
|
381
601
|
return {
|
|
@@ -397,7 +617,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
397
617
|
async function main() {
|
|
398
618
|
const transport = new StdioServerTransport();
|
|
399
619
|
await server.connect(transport);
|
|
400
|
-
console.error("Tryll Dataset Builder MCP server running");
|
|
620
|
+
console.error("Tryll Dataset Builder MCP server running (v1.1.0)");
|
|
401
621
|
}
|
|
402
622
|
|
|
403
623
|
main().catch((err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tryll-dataset-builder-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "MCP server for building RAG knowledge base datasets. Create, manage and export structured JSON datasets via Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
34
|
+
"ws": "^8.19.0",
|
|
34
35
|
"zod": "^3.24.0"
|
|
35
36
|
}
|
|
36
37
|
}
|