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.
Files changed (2) hide show
  1. package/index.js +310 -90
  2. 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.0.0" },
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
- switch (name) {
286
- // ---- Project ----
287
- case "create_project":
288
- result = store.createProject(args.name);
289
- break;
290
- case "list_projects":
291
- result = store.listProjects();
292
- break;
293
- case "delete_project":
294
- result = store.deleteProject(args.name);
295
- break;
296
- case "get_project_stats":
297
- result = store.getStats(args.name);
298
- break;
299
-
300
- // ---- Category ----
301
- case "create_category":
302
- result = store.createCategory(args.project, args.name);
303
- break;
304
- case "list_categories":
305
- result = store.listCategories(args.project);
306
- break;
307
- case "rename_category":
308
- result = store.renameCategory(args.project, args.old_name, args.new_name);
309
- break;
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
- break;
322
- case "bulk_add_chunks":
323
- result = store.bulkAddChunks(args.project, args.category, args.chunks);
324
- break;
325
- case "get_chunk":
326
- result = store.getChunk(args.project, args.id);
327
- break;
328
- case "update_chunk":
329
- result = store.updateChunk(args.project, args.id, {
330
- newId: args.new_id,
331
- text: args.text,
332
- page_title: args.page_title,
333
- source: args.source,
334
- license: args.license,
335
- metadata: args.metadata,
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
- break;
338
- case "delete_chunk":
339
- result = store.deleteChunk(args.project, args.id);
340
- break;
341
- case "duplicate_chunk":
342
- result = store.duplicateChunk(args.project, args.id);
343
- break;
344
- case "move_chunk":
345
- result = store.moveChunk(args.project, args.id, args.target_category);
346
- break;
347
-
348
- // ---- Search & Export ----
349
- case "search_chunks":
350
- result = store.searchChunks(args.project, args.query);
351
- break;
352
-
353
- case "export_project": {
354
- const exported = store.exportProject(args.project);
355
- if (args.save_to_file) {
356
- const outPath = store._filePath(args.project).replace('.json', '.export.json');
357
- const { writeFileSync } = await import('fs');
358
- writeFileSync(outPath, JSON.stringify(exported, null, 2), 'utf-8');
359
- result = { exported: exported.length, savedTo: outPath };
360
- } else {
361
- result = { exported: exported.length, data: exported };
362
- }
363
- break;
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
- case "import_json": {
367
- let jsonData = args.data;
368
- if (!jsonData && args.json_path) {
369
- const { readFileSync } = await import('fs');
370
- jsonData = JSON.parse(readFileSync(args.json_path, 'utf-8'));
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
- default:
378
- throw new Error(`Unknown tool: ${name}`);
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.0.0",
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
  }