@hasna/knowledge 0.2.21 → 0.2.23

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/mcp.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env bun
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { z } from 'zod';
5
5
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
6
6
  import pkg from '../package.json' with { type: 'json' };
7
+ import { migrateKnowledgeDb, openKnowledgeDb } from './knowledge-db.ts';
7
8
  import { defaultStorePath, loadStore, saveStore, makeId, withLock } from './store.ts';
8
9
  import { parseSourceRef } from './source-ref.ts';
9
10
  import { createKnowledgeService } from './service.ts';
@@ -61,16 +62,651 @@ function activeItems(items, includeArchived) {
61
62
  return includeArchived ? items : items.filter((item) => !item.archived);
62
63
  }
63
64
 
65
+ function limitNumber(value, fallback = 20, max = 100) {
66
+ if (!Number.isFinite(value) || value <= 0) return fallback;
67
+ return Math.min(Math.floor(value), max);
68
+ }
69
+
70
+ function parseJsonObject(value) {
71
+ if (!value) return {};
72
+ try {
73
+ const parsed = JSON.parse(value);
74
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
75
+ } catch {
76
+ return {};
77
+ }
78
+ }
79
+
80
+ function jsonResource(uri, data) {
81
+ return {
82
+ contents: [{
83
+ uri: uri.toString(),
84
+ mimeType: 'application/json',
85
+ text: JSON.stringify(data, null, 2),
86
+ }],
87
+ };
88
+ }
89
+
64
90
  function registerTool(server, name, title, description, inputSchema, handler) {
65
91
  server.registerTool(name, { title, description, inputSchema }, handler);
66
92
  }
67
93
 
94
+ function registerJsonResource(server, name, uri, title, description, read) {
95
+ server.registerResource(name, uri, {
96
+ title,
97
+ description,
98
+ mimeType: 'application/json',
99
+ }, async (resourceUri) => jsonResource(resourceUri, await read(resourceUri)));
100
+ }
101
+
102
+ function registerJsonTemplate(server, name, template, title, description, list, read) {
103
+ server.registerResource(name, new ResourceTemplate(template, { list }), {
104
+ title,
105
+ description,
106
+ mimeType: 'application/json',
107
+ }, async (resourceUri, variables) => jsonResource(resourceUri, await read(resourceUri, variables)));
108
+ }
109
+
110
+ function projectService() {
111
+ return createKnowledgeService({ scope: 'project' });
112
+ }
113
+
114
+ function openProjectDb(service = projectService()) {
115
+ const workspace = service.ensureWorkspace();
116
+ migrateKnowledgeDb(workspace.knowledgeDbPath);
117
+ return openKnowledgeDb(workspace.knowledgeDbPath);
118
+ }
119
+
120
+ function itemResources(storePath = createKnowledgeService({ scope: 'project' }).jsonStorePath()) {
121
+ return readStoreLocked(storePath, (db) => activeItems(db.items, false).slice(0, 100).map((item) => ({
122
+ uri: `knowledge://project/items/${encodeURIComponent(item.id)}`,
123
+ name: item.title,
124
+ description: `Knowledge item ${item.id}`,
125
+ mimeType: 'application/json',
126
+ })));
127
+ }
128
+
129
+ function listRows(db, sql, params = []) {
130
+ return db.query(sql).all(...params);
131
+ }
132
+
133
+ function rowWithJson(row, fields = ['metadata_json', 'acl_json']) {
134
+ if (!row) return null;
135
+ const next = { ...row };
136
+ for (const field of fields) {
137
+ if (field in next) {
138
+ const name = field.endsWith('_json') ? field.slice(0, -5) : field;
139
+ next[name] = parseJsonObject(next[field]);
140
+ delete next[field];
141
+ }
142
+ }
143
+ return next;
144
+ }
145
+
146
+ function dbStatsSnapshot(service = projectService()) {
147
+ const stats = service.dbStats();
148
+ const db = openProjectDb(service);
149
+ try {
150
+ return {
151
+ ok: true,
152
+ scope: 'project',
153
+ path: service.workspace.knowledgeDbPath,
154
+ stats,
155
+ schema_versions: listRows(db, 'SELECT version, applied_at FROM schema_versions ORDER BY version ASC'),
156
+ };
157
+ } finally {
158
+ db.close();
159
+ }
160
+ }
161
+
162
+ function storageSnapshot(service = projectService()) {
163
+ const validation = service.validateStorage();
164
+ return {
165
+ ok: validation.ok,
166
+ scope: 'project',
167
+ paths: service.paths(),
168
+ storage: service.storageContract(),
169
+ validation,
170
+ };
171
+ }
172
+
173
+ function configSnapshot(service = projectService()) {
174
+ return {
175
+ ok: true,
176
+ scope: 'project',
177
+ package: {
178
+ name: pkg.name,
179
+ version: pkg.version,
180
+ },
181
+ paths: service.paths(),
182
+ storage: service.storageContract(),
183
+ provider_status: service.providerStatus(),
184
+ model_registry: service.modelRegistry(),
185
+ };
186
+ }
187
+
188
+ function sourceRows(limit = 50, service = projectService()) {
189
+ const db = openProjectDb(service);
190
+ try {
191
+ return listRows(db, `
192
+ SELECT
193
+ s.id,
194
+ s.uri,
195
+ s.kind,
196
+ s.title,
197
+ s.metadata_json,
198
+ s.acl_json,
199
+ s.created_at,
200
+ s.updated_at,
201
+ COUNT(DISTINCT sr.id) AS revisions,
202
+ COUNT(DISTINCT c.id) AS chunks
203
+ FROM sources s
204
+ LEFT JOIN source_revisions sr ON sr.source_id = s.id
205
+ LEFT JOIN chunks c ON c.source_revision_id = sr.id
206
+ GROUP BY s.id
207
+ ORDER BY s.updated_at DESC
208
+ LIMIT ?
209
+ `, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row));
210
+ } finally {
211
+ db.close();
212
+ }
213
+ }
214
+
215
+ function sourceSnapshot(id, { limit = 10, service = projectService() } = {}) {
216
+ const db = openProjectDb(service);
217
+ try {
218
+ const source = rowWithJson(db.query(`
219
+ SELECT id, uri, kind, title, metadata_json, acl_json, created_at, updated_at
220
+ FROM sources
221
+ WHERE id = ? OR uri = ?
222
+ `).get(id, id));
223
+ if (!source) return null;
224
+ const revisions = listRows(db, `
225
+ SELECT id, revision, hash, extracted_text_uri, metadata_json, created_at
226
+ FROM source_revisions
227
+ WHERE source_id = ?
228
+ ORDER BY created_at DESC
229
+ LIMIT ?
230
+ `, [source.id, limitNumber(limit, 10, 100)]).map((row) => rowWithJson(row, ['metadata_json']));
231
+ const chunks = listRows(db, `
232
+ SELECT c.id, c.kind, c.ordinal, c.text, c.token_count, c.start_offset, c.end_offset, c.metadata_json, c.created_at,
233
+ sr.revision, sr.hash
234
+ FROM chunks c
235
+ JOIN source_revisions sr ON sr.id = c.source_revision_id
236
+ WHERE sr.source_id = ?
237
+ ORDER BY sr.created_at DESC, c.ordinal ASC
238
+ LIMIT ?
239
+ `, [source.id, limitNumber(limit, 10, 50)]).map((row) => rowWithJson(row, ['metadata_json']));
240
+ return { source, revisions, chunks };
241
+ } finally {
242
+ db.close();
243
+ }
244
+ }
245
+
246
+ function openFilesSnapshot(service = projectService()) {
247
+ const db = openProjectDb(service);
248
+ try {
249
+ const rows = listRows(db, `
250
+ SELECT
251
+ s.id,
252
+ s.uri,
253
+ s.title,
254
+ sr.revision,
255
+ sr.hash,
256
+ c.metadata_json,
257
+ COUNT(c.id) AS chunks
258
+ FROM sources s
259
+ JOIN source_revisions sr ON sr.source_id = s.id
260
+ LEFT JOIN chunks c ON c.source_revision_id = sr.id
261
+ WHERE s.uri LIKE 'open-files://%'
262
+ GROUP BY s.id, sr.id
263
+ ORDER BY s.updated_at DESC
264
+ LIMIT 100
265
+ `);
266
+ return {
267
+ ok: true,
268
+ scope: 'project',
269
+ source_ownership: 'open-files',
270
+ raw_source_bytes_exposed: false,
271
+ refs: rows.map((row) => {
272
+ const metadata = parseJsonObject(row.metadata_json);
273
+ return {
274
+ id: row.id,
275
+ uri: row.uri,
276
+ source_ref: typeof metadata.source_ref === 'string' ? metadata.source_ref : row.uri,
277
+ title: row.title,
278
+ revision: row.revision,
279
+ hash: row.hash,
280
+ chunks: row.chunks,
281
+ };
282
+ }),
283
+ };
284
+ } finally {
285
+ db.close();
286
+ }
287
+ }
288
+
289
+ function wikiRows(limit = 50, service = projectService()) {
290
+ const db = openProjectDb(service);
291
+ try {
292
+ return listRows(db, `
293
+ SELECT id, path, title, artifact_uri, content_hash, status, metadata_json, created_at, updated_at
294
+ FROM wiki_pages
295
+ ORDER BY updated_at DESC
296
+ LIMIT ?
297
+ `, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ['metadata_json']));
298
+ } finally {
299
+ db.close();
300
+ }
301
+ }
302
+
303
+ async function wikiSnapshot(id, { includeContent = true, service = projectService() } = {}) {
304
+ const db = openProjectDb(service);
305
+ try {
306
+ const page = rowWithJson(db.query(`
307
+ SELECT id, path, title, artifact_uri, content_hash, status, metadata_json, created_at, updated_at
308
+ FROM wiki_pages
309
+ WHERE id = ? OR path = ?
310
+ `).get(id, id), ['metadata_json']);
311
+ if (!page) return null;
312
+ const citations = listRows(db, `
313
+ SELECT id, chunk_id, source_uri, quote, start_offset, end_offset, metadata_json, created_at
314
+ FROM citations
315
+ WHERE wiki_page_id = ?
316
+ ORDER BY created_at ASC
317
+ LIMIT 100
318
+ `, [page.id]).map((row) => rowWithJson(row, ['metadata_json']));
319
+ let content = null;
320
+ if (includeContent) {
321
+ const artifactKey = page.metadata?.artifact_key ?? page.path;
322
+ if (typeof artifactKey === 'string') {
323
+ try {
324
+ content = await service.artifactStore().getText(artifactKey);
325
+ } catch {
326
+ content = null;
327
+ }
328
+ }
329
+ }
330
+ return { page, citations, content };
331
+ } finally {
332
+ db.close();
333
+ }
334
+ }
335
+
336
+ function indexRows(limit = 50, service = projectService()) {
337
+ const db = openProjectDb(service);
338
+ try {
339
+ return listRows(db, `
340
+ SELECT id, kind, name, artifact_uri, shard_key, metadata_json, created_at, updated_at
341
+ FROM knowledge_indexes
342
+ ORDER BY updated_at DESC
343
+ LIMIT ?
344
+ `, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ['metadata_json']));
345
+ } finally {
346
+ db.close();
347
+ }
348
+ }
349
+
350
+ function indexSnapshot(id, service = projectService()) {
351
+ const db = openProjectDb(service);
352
+ try {
353
+ const index = rowWithJson(db.query(`
354
+ SELECT id, kind, name, artifact_uri, shard_key, metadata_json, created_at, updated_at
355
+ FROM knowledge_indexes
356
+ WHERE id = ? OR name = ? OR shard_key = ?
357
+ `).get(id, id, id), ['metadata_json']);
358
+ if (!index) return null;
359
+ const vector_counts = listRows(db, `
360
+ SELECT provider, model, dimensions, status, COUNT(*) AS entries
361
+ FROM vector_index_entries
362
+ GROUP BY provider, model, dimensions, status
363
+ ORDER BY entries DESC
364
+ LIMIT 50
365
+ `);
366
+ return { index, vector_counts };
367
+ } finally {
368
+ db.close();
369
+ }
370
+ }
371
+
372
+ function runRows(limit = 50, service = projectService()) {
373
+ const db = openProjectDb(service);
374
+ try {
375
+ return listRows(db, `
376
+ SELECT id, type, prompt, status, provider, model, cost_tokens, cost_usd, metadata_json, created_at, updated_at
377
+ FROM runs
378
+ ORDER BY updated_at DESC
379
+ LIMIT ?
380
+ `, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ['metadata_json']));
381
+ } finally {
382
+ db.close();
383
+ }
384
+ }
385
+
386
+ function runSnapshot(id, { limit = 50, service = projectService() } = {}) {
387
+ const db = openProjectDb(service);
388
+ try {
389
+ const run = rowWithJson(db.query(`
390
+ SELECT id, type, prompt, status, provider, model, cost_tokens, cost_usd, metadata_json, created_at, updated_at
391
+ FROM runs
392
+ WHERE id = ?
393
+ `).get(id), ['metadata_json']);
394
+ if (!run) return null;
395
+ const events = listRows(db, `
396
+ SELECT id, level, event, metadata_json, created_at
397
+ FROM run_events
398
+ WHERE run_id = ?
399
+ ORDER BY created_at ASC
400
+ LIMIT ?
401
+ `, [id, limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ['metadata_json']));
402
+ const usage = listRows(db, `
403
+ SELECT id, provider, model, input_tokens, output_tokens, cost_usd, metadata_json, created_at
404
+ FROM provider_usage
405
+ WHERE run_id = ?
406
+ ORDER BY created_at ASC
407
+ LIMIT 100
408
+ `, [id]).map((row) => rowWithJson(row, ['metadata_json']));
409
+ return { run, events, usage };
410
+ } finally {
411
+ db.close();
412
+ }
413
+ }
414
+
415
+ function decisionsSnapshot(limit = 50, service = projectService()) {
416
+ const db = openProjectDb(service);
417
+ try {
418
+ return {
419
+ ok: true,
420
+ scope: 'project',
421
+ approval_gates: listRows(db, `
422
+ SELECT id, action, target_uri, status, reason, approved_by, metadata_json, created_at, updated_at
423
+ FROM approval_gates
424
+ ORDER BY updated_at DESC
425
+ LIMIT ?
426
+ `, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ['metadata_json'])),
427
+ audit_events: listRows(db, `
428
+ SELECT id, event_type, action, target_uri, decision, metadata_json, created_at
429
+ FROM audit_events
430
+ ORDER BY created_at DESC
431
+ LIMIT ?
432
+ `, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ['metadata_json'])),
433
+ };
434
+ } finally {
435
+ db.close();
436
+ }
437
+ }
438
+
439
+ function decisionSnapshot(id, service = projectService()) {
440
+ const db = openProjectDb(service);
441
+ try {
442
+ const approval = rowWithJson(db.query(`
443
+ SELECT id, action, target_uri, status, reason, approved_by, metadata_json, created_at, updated_at
444
+ FROM approval_gates
445
+ WHERE id = ? OR target_uri = ?
446
+ `).get(id, id), ['metadata_json']);
447
+ if (approval) return { kind: 'approval_gate', decision: approval };
448
+ const audit = rowWithJson(db.query(`
449
+ SELECT id, event_type, action, target_uri, decision, metadata_json, created_at
450
+ FROM audit_events
451
+ WHERE id = ? OR target_uri = ?
452
+ `).get(id, id), ['metadata_json']);
453
+ return audit ? { kind: 'audit_event', decision: audit } : null;
454
+ } finally {
455
+ db.close();
456
+ }
457
+ }
458
+
459
+ async function getKnowledgeRecord(kind, id, options = {}) {
460
+ const normalized = kind ?? 'auto';
461
+ const service = createKnowledgeService({ scope: options.scope });
462
+ const attempts = normalized === 'auto'
463
+ ? ['item', 'source', 'wiki_page', 'run', 'index', 'decision']
464
+ : [normalized];
465
+
466
+ for (const entry of attempts) {
467
+ if (entry === 'item') {
468
+ const storePath = resolveStorePath(options.store_path, options.scope);
469
+ const item = readStoreLocked(storePath, (db) => findItem(db, id));
470
+ if (item) return { kind: 'item', item, store_path: storePath };
471
+ }
472
+ if (entry === 'source') {
473
+ const source = sourceSnapshot(id, { limit: options.limit, service });
474
+ if (source) return { kind: 'source', ...source };
475
+ }
476
+ if (entry === 'wiki_page') {
477
+ const page = await wikiSnapshot(id, { includeContent: options.include_content !== false, service });
478
+ if (page) return { kind: 'wiki_page', ...page };
479
+ }
480
+ if (entry === 'run') {
481
+ const run = runSnapshot(id, { limit: options.limit, service });
482
+ if (run) return { kind: 'run', ...run };
483
+ }
484
+ if (entry === 'index') {
485
+ const index = indexSnapshot(id, service);
486
+ if (index) return { kind: 'index', ...index };
487
+ }
488
+ if (entry === 'decision') {
489
+ const decision = decisionSnapshot(id, service);
490
+ if (decision) return { kind: 'decision', ...decision };
491
+ }
492
+ }
493
+
494
+ return null;
495
+ }
496
+
497
+ function registerKnowledgeResources(server) {
498
+ registerJsonResource(
499
+ server,
500
+ 'knowledge-project-config',
501
+ 'knowledge://project/config',
502
+ 'Project knowledge config',
503
+ 'Resolved project workspace config, provider registry, and storage contract',
504
+ async () => configSnapshot(),
505
+ );
506
+ registerJsonResource(
507
+ server,
508
+ 'knowledge-project-storage',
509
+ 'knowledge://project/storage',
510
+ 'Project knowledge storage',
511
+ 'Artifact storage contract and validation for project knowledge',
512
+ async () => storageSnapshot(),
513
+ );
514
+ registerJsonResource(
515
+ server,
516
+ 'knowledge-project-schema',
517
+ 'knowledge://project/schema',
518
+ 'Project knowledge schema',
519
+ 'SQLite schema version and table counts for project knowledge',
520
+ async () => dbStatsSnapshot(),
521
+ );
522
+ registerJsonResource(
523
+ server,
524
+ 'knowledge-project-sources',
525
+ 'knowledge://project/sources',
526
+ 'Project knowledge sources',
527
+ 'Indexed source refs and revision/chunk counts without raw source bytes',
528
+ async () => ({ ok: true, scope: 'project', sources: sourceRows() }),
529
+ );
530
+ registerJsonResource(
531
+ server,
532
+ 'knowledge-project-open-files',
533
+ 'knowledge://project/open-files',
534
+ 'Project open-files refs',
535
+ 'Open-files source refs known to the project knowledge catalog',
536
+ async () => openFilesSnapshot(),
537
+ );
538
+ registerJsonResource(
539
+ server,
540
+ 'knowledge-project-wiki-pages',
541
+ 'knowledge://project/wiki/pages',
542
+ 'Project wiki pages',
543
+ 'Generated wiki pages and citation artifact metadata',
544
+ async () => ({ ok: true, scope: 'project', pages: wikiRows() }),
545
+ );
546
+ registerJsonResource(
547
+ server,
548
+ 'knowledge-project-indexes',
549
+ 'knowledge://project/indexes',
550
+ 'Project knowledge indexes',
551
+ 'Sharded knowledge indexes and vector-index status',
552
+ async () => ({
553
+ ok: true,
554
+ scope: 'project',
555
+ indexes: indexRows(),
556
+ embeddings: projectService().embeddingStatus(),
557
+ }),
558
+ );
559
+ registerJsonResource(
560
+ server,
561
+ 'knowledge-project-runs',
562
+ 'knowledge://project/runs',
563
+ 'Project knowledge runs',
564
+ 'Recent prompt, ingestion, web search, and reindex run ledger entries',
565
+ async () => ({ ok: true, scope: 'project', runs: runRows() }),
566
+ );
567
+ registerJsonResource(
568
+ server,
569
+ 'knowledge-project-decisions',
570
+ 'knowledge://project/decisions',
571
+ 'Project knowledge decisions',
572
+ 'Approval gates and audit decisions for generated knowledge operations',
573
+ async () => decisionsSnapshot(),
574
+ );
575
+
576
+ registerJsonTemplate(
577
+ server,
578
+ 'knowledge-project-items',
579
+ 'knowledge://project/items/{id}',
580
+ 'Project knowledge item',
581
+ 'Read a compatibility JSON-store item by id',
582
+ async () => ({ resources: itemResources() }),
583
+ async (_uri, variables) => {
584
+ const id = decodeURIComponent(String(variables.id));
585
+ const record = await getKnowledgeRecord('item', id, { scope: 'project' });
586
+ return record ? { ok: true, ...record } : { ok: false, error: `Item not found: ${id}` };
587
+ },
588
+ );
589
+ registerJsonTemplate(
590
+ server,
591
+ 'knowledge-project-source',
592
+ 'knowledge://project/sources/{id}',
593
+ 'Project source',
594
+ 'Read indexed source metadata, revisions, and derived chunks',
595
+ async () => ({
596
+ resources: sourceRows().map((source) => ({
597
+ uri: `knowledge://project/sources/${encodeURIComponent(source.id)}`,
598
+ name: source.title ?? source.uri,
599
+ description: `${source.kind} source with ${source.chunks} chunk(s)`,
600
+ mimeType: 'application/json',
601
+ })),
602
+ }),
603
+ async (_uri, variables) => {
604
+ const id = decodeURIComponent(String(variables.id));
605
+ const record = sourceSnapshot(id);
606
+ return record ? { ok: true, kind: 'source', ...record } : { ok: false, error: `Source not found: ${id}` };
607
+ },
608
+ );
609
+ registerJsonTemplate(
610
+ server,
611
+ 'knowledge-project-wiki-page',
612
+ 'knowledge://project/wiki/pages/{id}',
613
+ 'Project wiki page',
614
+ 'Read generated wiki page metadata, citations, and artifact text',
615
+ async () => ({
616
+ resources: wikiRows().map((page) => ({
617
+ uri: `knowledge://project/wiki/pages/${encodeURIComponent(page.id)}`,
618
+ name: page.title,
619
+ description: page.path,
620
+ mimeType: 'application/json',
621
+ })),
622
+ }),
623
+ async (_uri, variables) => {
624
+ const id = decodeURIComponent(String(variables.id));
625
+ const record = await wikiSnapshot(id);
626
+ return record ? { ok: true, kind: 'wiki_page', ...record } : { ok: false, error: `Wiki page not found: ${id}` };
627
+ },
628
+ );
629
+ registerJsonTemplate(
630
+ server,
631
+ 'knowledge-project-index',
632
+ 'knowledge://project/indexes/{id}',
633
+ 'Project knowledge index',
634
+ 'Read a knowledge index row and vector-count snapshot',
635
+ async () => ({
636
+ resources: indexRows().map((index) => ({
637
+ uri: `knowledge://project/indexes/${encodeURIComponent(index.id)}`,
638
+ name: index.name,
639
+ description: `${index.kind} index${index.shard_key ? ` shard ${index.shard_key}` : ''}`,
640
+ mimeType: 'application/json',
641
+ })),
642
+ }),
643
+ async (_uri, variables) => {
644
+ const id = decodeURIComponent(String(variables.id));
645
+ const record = indexSnapshot(id);
646
+ return record ? { ok: true, kind: 'index', ...record } : { ok: false, error: `Index not found: ${id}` };
647
+ },
648
+ );
649
+ registerJsonTemplate(
650
+ server,
651
+ 'knowledge-project-run',
652
+ 'knowledge://project/runs/{id}',
653
+ 'Project run',
654
+ 'Read a knowledge run ledger entry with events and usage',
655
+ async () => ({
656
+ resources: runRows().map((run) => ({
657
+ uri: `knowledge://project/runs/${encodeURIComponent(run.id)}`,
658
+ name: `${run.type}: ${run.status}`,
659
+ description: run.prompt ?? run.id,
660
+ mimeType: 'application/json',
661
+ })),
662
+ }),
663
+ async (_uri, variables) => {
664
+ const id = decodeURIComponent(String(variables.id));
665
+ const record = runSnapshot(id);
666
+ return record ? { ok: true, kind: 'run', ...record } : { ok: false, error: `Run not found: ${id}` };
667
+ },
668
+ );
669
+ registerJsonTemplate(
670
+ server,
671
+ 'knowledge-project-decision',
672
+ 'knowledge://project/decisions/{id}',
673
+ 'Project decision',
674
+ 'Read an approval gate or audit decision',
675
+ async () => {
676
+ const decisions = decisionsSnapshot();
677
+ return {
678
+ resources: [
679
+ ...decisions.approval_gates.map((entry) => ({
680
+ uri: `knowledge://project/decisions/${encodeURIComponent(entry.id)}`,
681
+ name: `${entry.action}: ${entry.status}`,
682
+ description: entry.target_uri ?? entry.id,
683
+ mimeType: 'application/json',
684
+ })),
685
+ ...decisions.audit_events.map((entry) => ({
686
+ uri: `knowledge://project/decisions/${encodeURIComponent(entry.id)}`,
687
+ name: `${entry.action}: ${entry.decision}`,
688
+ description: entry.target_uri ?? entry.id,
689
+ mimeType: 'application/json',
690
+ })),
691
+ ],
692
+ };
693
+ },
694
+ async (_uri, variables) => {
695
+ const id = decodeURIComponent(String(variables.id));
696
+ const record = decisionSnapshot(id);
697
+ return record ? { ok: true, ...record } : { ok: false, error: `Decision not found: ${id}` };
698
+ },
699
+ );
700
+ }
701
+
68
702
  export function buildServer() {
69
703
  const server = new McpServer({
70
704
  name: 'open-knowledge',
71
705
  version: pkg.version,
72
706
  });
73
707
 
708
+ registerKnowledgeResources(server);
709
+
74
710
  registerTool(server, 'ok_paths', 'Knowledge workspace paths', 'Show resolved workspace and store paths', {
75
711
  scope: scopeField,
76
712
  }, async ({ scope }) => {
@@ -266,6 +902,158 @@ export function buildServer() {
266
902
  }
267
903
  });
268
904
 
905
+ registerTool(server, 'knowledge_get', 'Get knowledge record', 'Read a knowledge item, indexed source, wiki page, run, index, or decision by id without raw source-byte access', {
906
+ scope: scopeField,
907
+ kind: z.enum(['auto', 'item', 'source', 'wiki_page', 'run', 'index', 'decision']).optional().describe('Record kind; auto tries all supported kinds'),
908
+ id: z.string().describe('Record id, short id, source URI, wiki path, index shard/name, or decision target URI'),
909
+ include_content: z.boolean().optional().describe('Include generated wiki artifact text when reading wiki pages'),
910
+ limit: z.number().optional().describe('Maximum related chunks/events to return'),
911
+ store_path: storePathField,
912
+ }, async ({ scope, kind, id, include_content, limit, store_path }) => {
913
+ try {
914
+ const record = await getKnowledgeRecord(kind ?? 'auto', id, {
915
+ scope,
916
+ include_content,
917
+ limit,
918
+ store_path,
919
+ });
920
+ return record ? jsonText({ ok: true, ...record }) : errorText(`Knowledge record not found: ${id}`);
921
+ } catch (error) {
922
+ return errorText(error instanceof Error ? error.message : String(error));
923
+ }
924
+ });
925
+
926
+ registerTool(server, 'knowledge_ingest', 'Ingest knowledge source', 'Ingest an open-files/S3/file/web source ref or open-files manifest into the derived knowledge catalog', {
927
+ scope: scopeField,
928
+ source_ref: z.string().optional().describe('Source reference URI to ingest, e.g. open-files://file/<id>/revision/<rev>'),
929
+ manifest: z.string().optional().describe('Manifest file path or s3:// URI to ingest'),
930
+ purpose: z.string().optional().describe('Read-only purpose label, default knowledge_answer'),
931
+ }, async ({ scope, source_ref, manifest, purpose }) => {
932
+ if (!source_ref && !manifest) return errorText('Missing input. Provide source_ref or manifest.');
933
+ if (source_ref && manifest) return errorText('Use either source_ref or manifest, not both.');
934
+ const service = createKnowledgeService({ scope });
935
+ try {
936
+ const result = source_ref
937
+ ? await service.ingestSource(source_ref, purpose)
938
+ : await service.ingestManifest(manifest);
939
+ return jsonText({ ok: true, mode: source_ref ? 'source' : 'manifest', ...result });
940
+ } catch (error) {
941
+ return errorText(error instanceof Error ? error.message : String(error));
942
+ }
943
+ });
944
+
945
+ registerTool(server, 'knowledge_build', 'Build knowledge answer', 'Run the knowledge prompt flow and optionally file the cited answer into generated wiki artifacts after approval', {
946
+ scope: scopeField,
947
+ prompt: z.string().describe('Prompt to answer and build durable knowledge from'),
948
+ limit: z.number().optional().describe('Maximum context results'),
949
+ semantic: z.boolean().optional().describe('Include vector semantic results'),
950
+ generate: z.boolean().optional().describe('Call AI SDK text generation; omitted returns a local citation draft'),
951
+ approve_write: z.boolean().optional().describe('Approve durable wiki filing for this call'),
952
+ file_answer: z.boolean().optional().describe('Attempt wiki answer filing; writes only with approve_write=true'),
953
+ model: z.string().optional().describe('Model alias/ref, default configured provider default'),
954
+ dimensions: z.number().optional().describe('Embedding dimensions for deterministic fake mode'),
955
+ fake: z.boolean().optional().describe('Use deterministic fake embeddings/generation for local tests'),
956
+ }, async ({ scope, prompt, limit, semantic, generate, approve_write, file_answer, model, dimensions, fake }) => {
957
+ const service = createKnowledgeService({ scope });
958
+ try {
959
+ const result = await service.runPrompt({ prompt, limit, semantic, generate, approveWrite: approve_write, modelRef: model, dimensions, fake });
960
+ let wiki_file = null;
961
+ if (file_answer === true || approve_write === true) {
962
+ wiki_file = await service.fileAnswer({
963
+ prompt,
964
+ answer: result.answer,
965
+ approveWrite: approve_write,
966
+ limit,
967
+ semantic,
968
+ modelRef: model,
969
+ dimensions,
970
+ fake,
971
+ });
972
+ }
973
+ return jsonText({ ok: true, ...result, wiki_file });
974
+ } catch (error) {
975
+ return errorText(error instanceof Error ? error.message : String(error));
976
+ }
977
+ });
978
+
979
+ registerTool(server, 'knowledge_web_search', 'Knowledge web search', 'Run safety-gated provider-native web search and optionally file snippets as web source refs', {
980
+ scope: scopeField,
981
+ query: z.string().describe('Web search query'),
982
+ limit: z.number().optional().describe('Maximum sources'),
983
+ provider: z.enum(['openai', 'anthropic', 'deepseek']).optional().describe('Provider override'),
984
+ model: z.string().optional().describe('Model alias/ref'),
985
+ domains: z.array(z.string()).optional().describe('Allowed domains'),
986
+ fake: z.boolean().optional().describe('Use deterministic fake web results'),
987
+ file_results: z.boolean().optional().describe('File web snippets as web source refs'),
988
+ }, async ({ scope, query, limit, provider, model, domains, fake, file_results }) => {
989
+ const service = createKnowledgeService({ scope });
990
+ try {
991
+ return jsonText({ ok: true, ...await service.webSearch({ query, limit, provider, modelRef: model, domains, fake, fileResults: file_results }) });
992
+ } catch (error) {
993
+ return errorText(error instanceof Error ? error.message : String(error));
994
+ }
995
+ });
996
+
997
+ registerTool(server, 'knowledge_lint', 'Lint knowledge wiki', 'Check generated wiki pages for missing citations, stale citations, duplicates, or source issues', {
998
+ scope: scopeField,
999
+ }, async ({ scope }) => {
1000
+ const service = createKnowledgeService({ scope });
1001
+ try {
1002
+ return jsonText({ ok: true, ...service.lintWiki() });
1003
+ } catch (error) {
1004
+ return errorText(error instanceof Error ? error.message : String(error));
1005
+ }
1006
+ });
1007
+
1008
+ registerTool(server, 'knowledge_run_status', 'Knowledge run status', 'List recent runs or inspect one run ledger with events and provider usage', {
1009
+ scope: scopeField,
1010
+ run_id: z.string().optional().describe('Run id to inspect; omitted lists recent runs'),
1011
+ limit: z.number().optional().describe('Maximum runs or events to return'),
1012
+ }, async ({ scope, run_id, limit }) => {
1013
+ const service = createKnowledgeService({ scope });
1014
+ try {
1015
+ if (run_id) {
1016
+ const run = runSnapshot(run_id, { limit, service });
1017
+ return run ? jsonText({ ok: true, kind: 'run', ...run }) : errorText(`Run not found: ${run_id}`);
1018
+ }
1019
+ return jsonText({ ok: true, runs: runRows(limit, service) });
1020
+ } catch (error) {
1021
+ return errorText(error instanceof Error ? error.message : String(error));
1022
+ }
1023
+ });
1024
+
1025
+ registerTool(server, 'knowledge_storage', 'Knowledge storage contract', 'Inspect local/S3 artifact storage, source ownership, and hosted/SaaS boundary metadata', {
1026
+ scope: scopeField,
1027
+ }, async ({ scope }) => {
1028
+ const service = createKnowledgeService({ scope });
1029
+ try {
1030
+ const validation = service.validateStorage();
1031
+ return jsonText({
1032
+ ok: validation.ok,
1033
+ ...service.storageContract(),
1034
+ validation,
1035
+ remote_contract: service.remoteContract(),
1036
+ });
1037
+ } catch (error) {
1038
+ return errorText(error instanceof Error ? error.message : String(error));
1039
+ }
1040
+ });
1041
+
1042
+ registerTool(server, 'knowledge_resolve_source', 'Resolve knowledge source', 'Resolve indexed source chunks through the read-only open-files/source boundary with citation evidence', {
1043
+ source_ref: z.string().describe('Source reference URI, preferably open-files://...'),
1044
+ purpose: z.string().optional().describe('Read-only purpose label, default knowledge_answer'),
1045
+ limit: z.number().optional().describe('Maximum chunks to return, default 10'),
1046
+ scope: scopeField,
1047
+ }, async ({ source_ref, purpose, limit, scope }) => {
1048
+ const service = createKnowledgeService({ scope });
1049
+ try {
1050
+ const result = await service.resolveSource(source_ref, { purpose, limit });
1051
+ return jsonText({ ok: true, ...result });
1052
+ } catch (error) {
1053
+ return errorText(error instanceof Error ? error.message : String(error));
1054
+ }
1055
+ });
1056
+
269
1057
  registerTool(server, 'ok_web_search', 'Provider web search', 'Run safety-gated provider-native web search and return citations/sources', {
270
1058
  scope: scopeField,
271
1059
  query: z.string().describe('Web search query'),