@grainulation/silo 1.0.0 → 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/lib/serve-mcp.js CHANGED
@@ -19,28 +19,30 @@
19
19
  * claude mcp add silo -- npx @grainulation/silo serve-mcp
20
20
  */
21
21
 
22
- const fs = require('fs');
23
- const path = require('path');
24
- const readline = require('readline');
25
- const { Store } = require('./store.js');
26
- const { Search } = require('./search.js');
27
- const { ImportExport } = require('./import-export.js');
28
- const { Packs } = require('./packs.js');
22
+ const fs = require("fs");
23
+ const path = require("path");
24
+ const readline = require("readline");
25
+ const { Store } = require("./store.js");
26
+ const { Search } = require("./search.js");
27
+ const { ImportExport } = require("./import-export.js");
28
+ const { Packs } = require("./packs.js");
29
+ const { Graph } = require("./graph.js");
30
+ const { Confluence } = require("./confluence.js");
29
31
 
30
32
  // ─── Constants ──────────────────────────────────────────────────────────────
31
33
 
32
- const SERVER_NAME = 'silo';
33
- const SERVER_VERSION = '1.0.0';
34
- const PROTOCOL_VERSION = '2024-11-05';
34
+ const SERVER_NAME = "silo";
35
+ const SERVER_VERSION = "1.0.0";
36
+ const PROTOCOL_VERSION = "2024-11-05";
35
37
 
36
38
  // ─── JSON-RPC helpers ───────────────────────────────────────────────────────
37
39
 
38
40
  function jsonRpcResponse(id, result) {
39
- return JSON.stringify({ jsonrpc: '2.0', id, result });
41
+ return JSON.stringify({ jsonrpc: "2.0", id, result });
40
42
  }
41
43
 
42
44
  function jsonRpcError(id, code, message) {
43
- return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
45
+ return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
44
46
  }
45
47
 
46
48
  // ─── Initialize silo components ─────────────────────────────────────────────
@@ -49,13 +51,15 @@ const store = new Store();
49
51
  const search = new Search(store);
50
52
  const io = new ImportExport(store);
51
53
  const packs = new Packs(store);
54
+ const graph = new Graph(store);
55
+ const confluence = new Confluence();
52
56
 
53
57
  // ─── Tool implementations ───────────────────────────────────────────────────
54
58
 
55
59
  function toolSearch(args) {
56
60
  const { query, type, evidence, limit } = args;
57
61
  if (!query) {
58
- return { status: 'error', message: 'Required field: query' };
62
+ return { status: "error", message: "Required field: query" };
59
63
  }
60
64
 
61
65
  const results = search.query(query, {
@@ -65,14 +69,16 @@ function toolSearch(args) {
65
69
  });
66
70
 
67
71
  return {
68
- status: 'ok',
72
+ status: "ok",
69
73
  count: results.length,
70
- claims: results.map(r => ({
74
+ claims: results.map((r) => ({
71
75
  id: r.claim.id,
72
76
  type: r.claim.type,
73
77
  topic: r.claim.topic,
74
78
  evidence: r.claim.evidence,
75
- content: (r.claim.content || r.claim.text || '').slice(0, 200) + ((r.claim.content || r.claim.text || '').length > 200 ? '...' : ''),
79
+ content:
80
+ (r.claim.content || r.claim.text || "").slice(0, 200) +
81
+ ((r.claim.content || r.claim.text || "").length > 200 ? "..." : ""),
76
82
  score: r.score,
77
83
  collection: r.collection,
78
84
  })),
@@ -82,9 +88,9 @@ function toolSearch(args) {
82
88
  function toolList() {
83
89
  const collections = store.list();
84
90
  return {
85
- status: 'ok',
91
+ status: "ok",
86
92
  count: collections.length,
87
- collections: collections.map(c => ({
93
+ collections: collections.map((c) => ({
88
94
  id: c.id,
89
95
  name: c.name,
90
96
  claimCount: c.claimCount,
@@ -96,134 +102,376 @@ function toolList() {
96
102
  function toolPull(dir, args) {
97
103
  const { pack, into } = args;
98
104
  if (!pack) {
99
- return { status: 'error', message: 'Required field: pack (pack name or collection ID)' };
105
+ return {
106
+ status: "error",
107
+ message: "Required field: pack (pack name or collection ID)",
108
+ };
100
109
  }
101
110
 
102
- const targetFile = into || path.join(dir, 'claims.json');
111
+ const targetFile = into || path.join(dir, "claims.json");
103
112
  if (!fs.existsSync(targetFile)) {
104
- return { status: 'error', message: `Target file not found: ${targetFile}. Run wheat init first.` };
113
+ return {
114
+ status: "error",
115
+ message: `Target file not found: ${targetFile}. Run wheat init first.`,
116
+ };
105
117
  }
106
118
 
107
119
  try {
108
120
  const result = io.pull(pack, targetFile, {});
109
121
  return {
110
- status: 'ok',
122
+ status: "ok",
111
123
  message: `Pulled ${result.imported} claims from "${pack}" into ${path.basename(targetFile)}.`,
112
124
  imported: result.imported,
113
125
  skipped: result.skipped || 0,
114
126
  };
115
127
  } catch (err) {
116
- return { status: 'error', message: err.message };
128
+ return { status: "error", message: err.message };
117
129
  }
118
130
  }
119
131
 
120
132
  function toolStore(dir, args) {
121
133
  const { name, from } = args;
122
134
  if (!name) {
123
- return { status: 'error', message: 'Required field: name' };
135
+ return { status: "error", message: "Required field: name" };
124
136
  }
125
137
 
126
- const sourceFile = from || path.join(dir, 'claims.json');
138
+ const sourceFile = from || path.join(dir, "claims.json");
127
139
  if (!fs.existsSync(sourceFile)) {
128
- return { status: 'error', message: `Source file not found: ${sourceFile}` };
140
+ return { status: "error", message: `Source file not found: ${sourceFile}` };
129
141
  }
130
142
 
131
143
  try {
132
- const data = JSON.parse(fs.readFileSync(sourceFile, 'utf8'));
144
+ const data = JSON.parse(fs.readFileSync(sourceFile, "utf8"));
133
145
  const claims = data.claims || data;
134
146
  const meta = data.meta || {};
135
147
  const result = store.storeClaims(name, claims, meta);
136
148
  return {
137
- status: 'ok',
149
+ status: "ok",
138
150
  message: `Stored ${result.claimCount} claims as "${name}".`,
139
151
  id: result.id,
140
152
  claimCount: result.claimCount,
141
153
  hash: result.hash,
142
154
  };
143
155
  } catch (err) {
144
- return { status: 'error', message: err.message };
156
+ return { status: "error", message: err.message };
145
157
  }
146
158
  }
147
159
 
148
160
  function toolPacks() {
149
161
  const available = packs.list();
150
162
  return {
151
- status: 'ok',
163
+ status: "ok",
152
164
  count: available.length,
153
- packs: available.map(p => ({
165
+ packs: available.map((p) => ({
154
166
  name: p.name,
155
- description: p.description || '',
167
+ description: p.description || "",
156
168
  claimCount: p.claims ? p.claims.length : p.claimCount || 0,
157
- source: p.source || 'unknown',
169
+ source: p.source || "unknown",
158
170
  })),
159
171
  };
160
172
  }
161
173
 
174
+ function toolGraph(args) {
175
+ const { action, claimId, topic, tag, minSize } = args;
176
+ switch (action) {
177
+ case "build": {
178
+ const stats = graph.build();
179
+ return { status: "ok", message: "Knowledge graph built.", ...stats };
180
+ }
181
+ case "related": {
182
+ if (!claimId)
183
+ return { status: "error", message: "Required field: claimId" };
184
+ const results = graph.related(claimId, { limit: 20 });
185
+ return {
186
+ status: "ok",
187
+ count: results.length,
188
+ related: results.map((r) => ({
189
+ id: r.claim.id,
190
+ type: r.claim.type,
191
+ topic: r.claim.topic,
192
+ content: (r.claim.content || "").slice(0, 200),
193
+ source: r.source,
194
+ relation: r.relation,
195
+ weight: r.weight,
196
+ })),
197
+ };
198
+ }
199
+ case "topic": {
200
+ if (!topic) return { status: "error", message: "Required field: topic" };
201
+ const results = graph.byTopic(topic);
202
+ return {
203
+ status: "ok",
204
+ count: results.length,
205
+ claims: results.map((r) => ({
206
+ id: r.claim.id,
207
+ type: r.claim.type,
208
+ content: (r.claim.content || "").slice(0, 200),
209
+ source: r.source,
210
+ })),
211
+ };
212
+ }
213
+ case "clusters": {
214
+ const clusters = graph.clusters(minSize || 3);
215
+ return {
216
+ status: "ok",
217
+ count: clusters.length,
218
+ clusters: clusters.slice(0, 20).map((c) => ({
219
+ topic: c.topic,
220
+ claimCount: c.claimCount,
221
+ edgeCount: c.edgeCount,
222
+ })),
223
+ };
224
+ }
225
+ case "stats": {
226
+ return { status: "ok", ...graph.stats() };
227
+ }
228
+ default:
229
+ return {
230
+ status: "error",
231
+ message: "Unknown action. Use: build, related, topic, clusters, stats",
232
+ };
233
+ }
234
+ }
235
+
236
+ function toolConfluence(dir, args) {
237
+ const { action } = args;
238
+ if (!confluence.isConfigured()) {
239
+ return {
240
+ status: "error",
241
+ message:
242
+ "Confluence not configured. Set CONFLUENCE_BASE_URL, CONFLUENCE_TOKEN, and CONFLUENCE_EMAIL environment variables.",
243
+ };
244
+ }
245
+
246
+ switch (action) {
247
+ case "publish": {
248
+ const { title, from, spaceKey, parentId, pageId } = args;
249
+ if (!title) return { status: "error", message: "Required field: title" };
250
+ const sourceFile = from || path.join(dir, "claims.json");
251
+ if (!fs.existsSync(sourceFile))
252
+ return {
253
+ status: "error",
254
+ message: `Source file not found: ${sourceFile}`,
255
+ };
256
+ const raw = JSON.parse(fs.readFileSync(sourceFile, "utf8"));
257
+ const claims = Array.isArray(raw) ? raw : raw.claims || [];
258
+ return confluence
259
+ .publish(title, claims, { spaceKey, parentId, pageId })
260
+ .then((result) => ({
261
+ status: "ok",
262
+ message: `Published to Confluence: ${result.title}`,
263
+ ...result,
264
+ }))
265
+ .catch((err) => ({ status: "error", message: err.message }));
266
+ }
267
+ case "pull": {
268
+ const { pageId: pid, title: searchTitle, into, spaceKey } = args;
269
+ const target = pid || searchTitle;
270
+ if (!target)
271
+ return { status: "error", message: "Required: pageId or title" };
272
+ return confluence
273
+ .pull(target, { spaceKey })
274
+ .then((result) => {
275
+ if (into) {
276
+ const targetFile = path.resolve(dir, into);
277
+ let existing = [];
278
+ if (fs.existsSync(targetFile)) {
279
+ const raw = JSON.parse(fs.readFileSync(targetFile, "utf-8"));
280
+ existing = Array.isArray(raw) ? raw : raw.claims || [];
281
+ }
282
+ const merged = [...existing, ...result.claims];
283
+ fs.writeFileSync(
284
+ targetFile,
285
+ JSON.stringify(merged, null, 2) + "\n",
286
+ );
287
+ }
288
+ return {
289
+ status: "ok",
290
+ title: result.title,
291
+ claimCount: result.claims.length,
292
+ };
293
+ })
294
+ .catch((err) => ({ status: "error", message: err.message }));
295
+ }
296
+ case "list": {
297
+ const { spaceKey } = args;
298
+ return confluence
299
+ .listPages({ spaceKey })
300
+ .then((result) => ({ status: "ok", ...result }))
301
+ .catch((err) => ({ status: "error", message: err.message }));
302
+ }
303
+ default:
304
+ return {
305
+ status: "error",
306
+ message: "Unknown action. Use: publish, pull, list",
307
+ };
308
+ }
309
+ }
310
+
162
311
  // ─── Tool & Resource definitions ────────────────────────────────────────────
163
312
 
164
313
  const TOOLS = [
165
314
  {
166
- name: 'silo/search',
167
- description: 'Full-text search across all stored claims and knowledge packs. Returns ranked results with relevance scores.',
315
+ name: "silo/search",
316
+ description:
317
+ "Full-text search across all stored claims and knowledge packs. Returns ranked results with relevance scores.",
168
318
  inputSchema: {
169
- type: 'object',
319
+ type: "object",
170
320
  properties: {
171
- query: { type: 'string', description: 'Search query (space-separated terms, OR logic)' },
172
- type: { type: 'string', enum: ['constraint', 'factual', 'estimate', 'risk', 'recommendation', 'feedback'], description: 'Filter by claim type' },
173
- evidence: { type: 'string', enum: ['stated', 'web', 'documented', 'tested', 'production'], description: 'Filter by evidence tier' },
174
- limit: { type: 'number', description: 'Max results (default: 20)' },
321
+ query: {
322
+ type: "string",
323
+ description: "Search query (space-separated terms, OR logic)",
324
+ },
325
+ type: {
326
+ type: "string",
327
+ enum: [
328
+ "constraint",
329
+ "factual",
330
+ "estimate",
331
+ "risk",
332
+ "recommendation",
333
+ "feedback",
334
+ ],
335
+ description: "Filter by claim type",
336
+ },
337
+ evidence: {
338
+ type: "string",
339
+ enum: ["stated", "web", "documented", "tested", "production"],
340
+ description: "Filter by evidence tier",
341
+ },
342
+ limit: { type: "number", description: "Max results (default: 20)" },
175
343
  },
176
- required: ['query'],
344
+ required: ["query"],
177
345
  },
178
346
  },
179
347
  {
180
- name: 'silo/list',
181
- description: 'List all stored collections in the silo (completed sprints, imported knowledge).',
182
- inputSchema: { type: 'object', properties: {} },
348
+ name: "silo/list",
349
+ description:
350
+ "List all stored collections in the silo (completed sprints, imported knowledge).",
351
+ inputSchema: { type: "object", properties: {} },
183
352
  },
184
353
  {
185
- name: 'silo/pull',
186
- description: 'Pull claims from a knowledge pack or stored collection into the current sprint claims.json. Deduplicates and re-prefixes IDs to avoid collisions.',
354
+ name: "silo/pull",
355
+ description:
356
+ "Pull claims from a knowledge pack or stored collection into the current sprint claims.json. Deduplicates and re-prefixes IDs to avoid collisions.",
187
357
  inputSchema: {
188
- type: 'object',
358
+ type: "object",
189
359
  properties: {
190
- pack: { type: 'string', description: 'Pack name (e.g., "compliance", "security") or stored collection ID' },
191
- into: { type: 'string', description: 'Target claims.json path (default: ./claims.json)' },
360
+ pack: {
361
+ type: "string",
362
+ description:
363
+ 'Pack name (e.g., "compliance", "security") or stored collection ID',
364
+ },
365
+ into: {
366
+ type: "string",
367
+ description: "Target claims.json path (default: ./claims.json)",
368
+ },
192
369
  },
193
- required: ['pack'],
370
+ required: ["pack"],
194
371
  },
195
372
  },
196
373
  {
197
- name: 'silo/store',
198
- description: 'Store the current sprint claims into the silo for future reuse across other sprints.',
374
+ name: "silo/store",
375
+ description:
376
+ "Store the current sprint claims into the silo for future reuse across other sprints.",
199
377
  inputSchema: {
200
- type: 'object',
378
+ type: "object",
201
379
  properties: {
202
- name: { type: 'string', description: 'Collection name (e.g., "q4-migration-findings")' },
203
- from: { type: 'string', description: 'Source claims.json path (default: ./claims.json)' },
380
+ name: {
381
+ type: "string",
382
+ description: 'Collection name (e.g., "q4-migration-findings")',
383
+ },
384
+ from: {
385
+ type: "string",
386
+ description: "Source claims.json path (default: ./claims.json)",
387
+ },
204
388
  },
205
- required: ['name'],
389
+ required: ["name"],
206
390
  },
207
391
  },
208
392
  {
209
- name: 'silo/packs',
210
- description: 'List available knowledge packs (built-in: compliance, security, architecture, migration, etc.).',
211
- inputSchema: { type: 'object', properties: {} },
393
+ name: "silo/packs",
394
+ description:
395
+ "List available knowledge packs (built-in: compliance, security, architecture, migration, vendor-eval, adr, hackathon categories, etc.).",
396
+ inputSchema: { type: "object", properties: {} },
397
+ },
398
+ {
399
+ name: "silo/graph",
400
+ description:
401
+ "Cross-sprint knowledge graph — find related claims, explore topics, and discover clusters across all stored collections and packs.",
402
+ inputSchema: {
403
+ type: "object",
404
+ properties: {
405
+ action: {
406
+ type: "string",
407
+ enum: ["build", "related", "topic", "clusters", "stats"],
408
+ description: "Graph operation to perform",
409
+ },
410
+ claimId: {
411
+ type: "string",
412
+ description: 'Claim ID for "related" action',
413
+ },
414
+ topic: {
415
+ type: "string",
416
+ description: 'Topic string for "topic" action',
417
+ },
418
+ tag: { type: "string", description: "Tag for filtering" },
419
+ minSize: {
420
+ type: "number",
421
+ description:
422
+ 'Minimum cluster size for "clusters" action (default: 3)',
423
+ },
424
+ },
425
+ required: ["action"],
426
+ },
427
+ },
428
+ {
429
+ name: "silo/confluence",
430
+ description:
431
+ "Confluence backend adapter — publish claims to Confluence pages or pull claims from existing pages. Requires CONFLUENCE_BASE_URL, CONFLUENCE_TOKEN, CONFLUENCE_EMAIL env vars.",
432
+ inputSchema: {
433
+ type: "object",
434
+ properties: {
435
+ action: {
436
+ type: "string",
437
+ enum: ["publish", "pull", "list"],
438
+ description: "Confluence operation",
439
+ },
440
+ title: {
441
+ type: "string",
442
+ description: "Page title (publish/pull by title)",
443
+ },
444
+ from: {
445
+ type: "string",
446
+ description:
447
+ "Source claims.json path (publish, default: ./claims.json)",
448
+ },
449
+ into: { type: "string", description: "Target claims.json path (pull)" },
450
+ pageId: {
451
+ type: "string",
452
+ description: "Confluence page ID (pull/publish update)",
453
+ },
454
+ spaceKey: { type: "string", description: "Confluence space key" },
455
+ parentId: { type: "string", description: "Parent page ID (publish)" },
456
+ },
457
+ required: ["action"],
458
+ },
212
459
  },
213
460
  ];
214
461
 
215
462
  const RESOURCES = [
216
463
  {
217
- uri: 'silo://index',
218
- name: 'Silo Index',
219
- description: 'All stored collections — IDs, names, claim counts, timestamps.',
220
- mimeType: 'application/json',
464
+ uri: "silo://index",
465
+ name: "Silo Index",
466
+ description:
467
+ "All stored collections — IDs, names, claim counts, timestamps.",
468
+ mimeType: "application/json",
221
469
  },
222
470
  {
223
- uri: 'silo://packs',
224
- name: 'Knowledge Packs',
225
- description: 'Available built-in and local knowledge packs.',
226
- mimeType: 'application/json',
471
+ uri: "silo://packs",
472
+ name: "Knowledge Packs",
473
+ description: "Available built-in and local knowledge packs.",
474
+ mimeType: "application/json",
227
475
  },
228
476
  ];
229
477
 
@@ -231,66 +479,99 @@ const RESOURCES = [
231
479
 
232
480
  function handleRequest(dir, method, params, id) {
233
481
  switch (method) {
234
- case 'initialize':
482
+ case "initialize":
235
483
  return jsonRpcResponse(id, {
236
484
  protocolVersion: PROTOCOL_VERSION,
237
485
  capabilities: { tools: {}, resources: {} },
238
486
  serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
239
487
  });
240
488
 
241
- case 'notifications/initialized':
489
+ case "notifications/initialized":
242
490
  return null;
243
491
 
244
- case 'tools/list':
492
+ case "tools/list":
245
493
  return jsonRpcResponse(id, { tools: TOOLS });
246
494
 
247
- case 'tools/call': {
495
+ case "tools/call": {
248
496
  const toolName = params.name;
249
497
  const toolArgs = params.arguments || {};
250
498
  let result;
251
499
 
252
500
  switch (toolName) {
253
- case 'silo/search': result = toolSearch(toolArgs); break;
254
- case 'silo/list': result = toolList(); break;
255
- case 'silo/pull': result = toolPull(dir, toolArgs); break;
256
- case 'silo/store': result = toolStore(dir, toolArgs); break;
257
- case 'silo/packs': result = toolPacks(); break;
501
+ case "silo/search":
502
+ result = toolSearch(toolArgs);
503
+ break;
504
+ case "silo/list":
505
+ result = toolList();
506
+ break;
507
+ case "silo/pull":
508
+ result = toolPull(dir, toolArgs);
509
+ break;
510
+ case "silo/store":
511
+ result = toolStore(dir, toolArgs);
512
+ break;
513
+ case "silo/packs":
514
+ result = toolPacks();
515
+ break;
516
+ case "silo/graph":
517
+ result = toolGraph(toolArgs);
518
+ break;
519
+ case "silo/confluence":
520
+ result = toolConfluence(dir, toolArgs);
521
+ break;
258
522
  default:
259
523
  return jsonRpcError(id, -32601, `Unknown tool: ${toolName}`);
260
524
  }
261
525
 
526
+ // Handle async tool results (e.g., confluence)
527
+ if (result && typeof result.then === "function") {
528
+ return result.then((r) =>
529
+ jsonRpcResponse(id, {
530
+ content: [{ type: "text", text: JSON.stringify(r, null, 2) }],
531
+ isError: r.status === "error",
532
+ }),
533
+ );
534
+ }
535
+
262
536
  return jsonRpcResponse(id, {
263
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
264
- isError: result.status === 'error',
537
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
538
+ isError: result.status === "error",
265
539
  });
266
540
  }
267
541
 
268
- case 'resources/list':
542
+ case "resources/list":
269
543
  return jsonRpcResponse(id, { resources: RESOURCES });
270
544
 
271
- case 'resources/read': {
545
+ case "resources/read": {
272
546
  const uri = params.uri;
273
547
  let text;
274
548
 
275
549
  switch (uri) {
276
- case 'silo://index':
550
+ case "silo://index":
277
551
  text = JSON.stringify(store.list(), null, 2);
278
552
  break;
279
- case 'silo://packs':
280
- text = JSON.stringify(packs.list().map(p => ({
281
- name: p.name, description: p.description, claimCount: p.claims ? p.claims.length : 0, source: p.source,
282
- })), null, 2);
553
+ case "silo://packs":
554
+ text = JSON.stringify(
555
+ packs.list().map((p) => ({
556
+ name: p.name,
557
+ description: p.description,
558
+ claimCount: p.claims ? p.claims.length : 0,
559
+ source: p.source,
560
+ })),
561
+ null,
562
+ 2,
563
+ );
283
564
  break;
284
565
  default:
285
566
  return jsonRpcError(id, -32602, `Unknown resource: ${uri}`);
286
567
  }
287
568
 
288
569
  return jsonRpcResponse(id, {
289
- contents: [{ uri, mimeType: 'application/json', text }],
570
+ contents: [{ uri, mimeType: "application/json", text }],
290
571
  });
291
572
  }
292
573
 
293
- case 'ping':
574
+ case "ping":
294
575
  return jsonRpcResponse(id, {});
295
576
 
296
577
  default:
@@ -302,28 +583,41 @@ function handleRequest(dir, method, params, id) {
302
583
  // ─── Stdio transport ────────────────────────────────────────────────────────
303
584
 
304
585
  function startServer(dir) {
305
- const rl = readline.createInterface({ input: process.stdin, terminal: false });
586
+ const rl = readline.createInterface({
587
+ input: process.stdin,
588
+ terminal: false,
589
+ });
306
590
 
307
591
  if (process.stdout._handle && process.stdout._handle.setBlocking) {
308
592
  process.stdout._handle.setBlocking(true);
309
593
  }
310
594
 
311
- rl.on('line', (line) => {
595
+ rl.on("line", (line) => {
312
596
  if (!line.trim()) return;
313
597
  let msg;
314
- try { msg = JSON.parse(line); } catch {
315
- process.stdout.write(jsonRpcError(null, -32700, 'Parse error') + '\n');
598
+ try {
599
+ msg = JSON.parse(line);
600
+ } catch {
601
+ process.stdout.write(jsonRpcError(null, -32700, "Parse error") + "\n");
316
602
  return;
317
603
  }
318
604
  const response = handleRequest(dir, msg.method, msg.params || {}, msg.id);
319
- if (response !== null) process.stdout.write(response + '\n');
605
+ if (response && typeof response.then === "function") {
606
+ response.then((r) => {
607
+ if (r !== null) process.stdout.write(r + "\n");
608
+ });
609
+ } else if (response !== null) {
610
+ process.stdout.write(response + "\n");
611
+ }
320
612
  });
321
613
 
322
- rl.on('close', () => process.exit(0));
614
+ rl.on("close", () => process.exit(0));
323
615
 
324
616
  process.stderr.write(`silo MCP server v${SERVER_VERSION} ready on stdio\n`);
325
617
  process.stderr.write(` Silo root: ${store.root}\n`);
326
- process.stderr.write(` Tools: ${TOOLS.length} | Resources: ${RESOURCES.length}\n`);
618
+ process.stderr.write(
619
+ ` Tools: ${TOOLS.length} | Resources: ${RESOURCES.length}\n`,
620
+ );
327
621
  }
328
622
 
329
623
  // ─── Entry point ────────────────────────────────────────────────────────────
@@ -332,6 +626,8 @@ if (require.main === module) {
332
626
  startServer(process.cwd());
333
627
  }
334
628
 
335
- async function run(dir) { startServer(dir); }
629
+ async function run(dir) {
630
+ startServer(dir);
631
+ }
336
632
 
337
633
  module.exports = { startServer, handleRequest, TOOLS, RESOURCES, run };