opentology 0.1.0

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +609 -0
  3. package/dist/commands/context.d.ts +29 -0
  4. package/dist/commands/context.js +369 -0
  5. package/dist/commands/delete.d.ts +2 -0
  6. package/dist/commands/delete.js +46 -0
  7. package/dist/commands/diff.d.ts +2 -0
  8. package/dist/commands/diff.js +43 -0
  9. package/dist/commands/drop.d.ts +2 -0
  10. package/dist/commands/drop.js +41 -0
  11. package/dist/commands/graph.d.ts +2 -0
  12. package/dist/commands/graph.js +130 -0
  13. package/dist/commands/infer.d.ts +2 -0
  14. package/dist/commands/infer.js +47 -0
  15. package/dist/commands/init.d.ts +2 -0
  16. package/dist/commands/init.js +53 -0
  17. package/dist/commands/mcp.d.ts +2 -0
  18. package/dist/commands/mcp.js +9 -0
  19. package/dist/commands/prefix.d.ts +2 -0
  20. package/dist/commands/prefix.js +73 -0
  21. package/dist/commands/pull.d.ts +2 -0
  22. package/dist/commands/pull.js +43 -0
  23. package/dist/commands/push.d.ts +2 -0
  24. package/dist/commands/push.js +79 -0
  25. package/dist/commands/query.d.ts +2 -0
  26. package/dist/commands/query.js +119 -0
  27. package/dist/commands/shapes.d.ts +2 -0
  28. package/dist/commands/shapes.js +67 -0
  29. package/dist/commands/status.d.ts +2 -0
  30. package/dist/commands/status.js +47 -0
  31. package/dist/commands/validate.d.ts +2 -0
  32. package/dist/commands/validate.js +46 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +38 -0
  35. package/dist/lib/codebase-scanner.d.ts +41 -0
  36. package/dist/lib/codebase-scanner.js +360 -0
  37. package/dist/lib/config.d.ts +16 -0
  38. package/dist/lib/config.js +70 -0
  39. package/dist/lib/embedded-adapter.d.ts +45 -0
  40. package/dist/lib/embedded-adapter.js +202 -0
  41. package/dist/lib/http-adapter.d.ts +41 -0
  42. package/dist/lib/http-adapter.js +169 -0
  43. package/dist/lib/oxigraph.d.ts +62 -0
  44. package/dist/lib/oxigraph.js +323 -0
  45. package/dist/lib/reasoner.d.ts +19 -0
  46. package/dist/lib/reasoner.js +310 -0
  47. package/dist/lib/shacl.d.ts +22 -0
  48. package/dist/lib/shacl.js +105 -0
  49. package/dist/lib/sparql-utils.d.ts +28 -0
  50. package/dist/lib/sparql-utils.js +217 -0
  51. package/dist/lib/store-adapter.d.ts +50 -0
  52. package/dist/lib/store-adapter.js +1 -0
  53. package/dist/lib/store-factory.d.ts +9 -0
  54. package/dist/lib/store-factory.js +71 -0
  55. package/dist/lib/validator.d.ts +10 -0
  56. package/dist/lib/validator.js +40 -0
  57. package/dist/mcp/server.d.ts +3 -0
  58. package/dist/mcp/server.js +1020 -0
  59. package/dist/templates/claude-md-context.d.ts +4 -0
  60. package/dist/templates/claude-md-context.js +104 -0
  61. package/dist/templates/otx-ontology.d.ts +2 -0
  62. package/dist/templates/otx-ontology.js +31 -0
  63. package/dist/templates/session-start-hook.d.ts +1 -0
  64. package/dist/templates/session-start-hook.js +94 -0
  65. package/dist/templates/slash-commands.d.ts +5 -0
  66. package/dist/templates/slash-commands.js +108 -0
  67. package/package.json +58 -0
@@ -0,0 +1,1020 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
4
+ import { loadConfig, saveConfig, configExists, resolveGraphUri } from '../lib/config.js';
5
+ import { createReadyAdapter } from '../lib/store-factory.js';
6
+ import { hasGraphScope, autoScopeQuery, getInferenceGraphUri } from '../lib/sparql-utils.js';
7
+ import { validateTurtle } from '../lib/validator.js';
8
+ import { discoverShapes, validateWithShacl, hasShapes } from '../lib/shacl.js';
9
+ import { materializeInferences, clearInferences } from '../lib/reasoner.js';
10
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { OTX_BOOTSTRAP_TURTLE } from '../templates/otx-ontology.js';
13
+ import { generateContextSection, updateClaudeMd } from '../templates/claude-md-context.js';
14
+ import { generateHookScript } from '../templates/session-start-hook.js';
15
+ import { generateSlashCommands } from '../templates/slash-commands.js';
16
+ import { scanCodebase } from '../lib/codebase-scanner.js';
17
+ export const MAX_TRIPLES_PER_PUSH = 100;
18
+ export function assertTripleLimit(tripleCount) {
19
+ if (tripleCount > MAX_TRIPLES_PER_PUSH) {
20
+ throw new Error(`Too many triples (${tripleCount}). Maximum is ${MAX_TRIPLES_PER_PUSH} per push. Split your data into smaller batches.`);
21
+ }
22
+ }
23
+ function resolveConfig(params) {
24
+ try {
25
+ const config = loadConfig();
26
+ let graphUri = params.graphUri || config.graphUri;
27
+ if (params.graph) {
28
+ graphUri = resolveGraphUri(config, params.graph);
29
+ }
30
+ return { config, graphUri };
31
+ }
32
+ catch {
33
+ throw new Error('No config found. Run opentology init first.');
34
+ }
35
+ }
36
+ async function handleInit(args) {
37
+ const projectId = args.projectId;
38
+ const mode = args.mode || 'http';
39
+ const endpoint = mode === 'http' ? (args.endpoint || 'http://localhost:7878') : undefined;
40
+ if (configExists()) {
41
+ throw new Error('Project already initialized. .opentology.json exists in the current directory.');
42
+ }
43
+ const graphUri = `https://opentology.dev/${projectId}`;
44
+ const prefixes = {
45
+ rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
46
+ rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
47
+ owl: 'http://www.w3.org/2002/07/owl#',
48
+ xsd: 'http://www.w3.org/2001/XMLSchema#',
49
+ schema: 'http://schema.org/',
50
+ };
51
+ const config = { projectId, mode, graphUri, prefixes, ...(endpoint ? { endpoint } : {}) };
52
+ saveConfig(config);
53
+ return { projectId, mode, endpoint, graphUri };
54
+ }
55
+ async function handleValidate(args) {
56
+ const content = args.content;
57
+ const shacl = args.shacl;
58
+ const result = await validateTurtle(content);
59
+ if (shacl && result.valid) {
60
+ const shapePaths = discoverShapes();
61
+ if (shapePaths.length > 0) {
62
+ const report = await validateWithShacl(content, shapePaths);
63
+ return { ...result, shacl: report };
64
+ }
65
+ return { ...result, shacl: { conforms: true, violations: [], note: 'no shapes found' } };
66
+ }
67
+ return result;
68
+ }
69
+ async function handlePush(args) {
70
+ const content = args.content;
71
+ const replace = args.replace;
72
+ const shacl = args.shacl;
73
+ const validation = await validateTurtle(content);
74
+ if (!validation.valid) {
75
+ throw new Error(`Invalid Turtle: ${validation.error}`);
76
+ }
77
+ assertTripleLimit(validation.tripleCount);
78
+ // Auto-validate against SHACL when shapes exist (unless explicitly false)
79
+ if (shacl !== false && hasShapes()) {
80
+ const shapePaths = discoverShapes();
81
+ const report = await validateWithShacl(content, shapePaths);
82
+ if (!report.conforms) {
83
+ const violationMessages = report.violations
84
+ .map((v) => `SHACL Violation: ${v.focusNode} — ${v.message} (path: ${v.path})`)
85
+ .join('\n');
86
+ throw new Error(`SHACL validation failed:\n${violationMessages}`);
87
+ }
88
+ }
89
+ const { config, graphUri } = resolveConfig({
90
+ graphUri: args.graphUri,
91
+ graph: args.graph,
92
+ });
93
+ const adapter = await createReadyAdapter(config);
94
+ if (replace) {
95
+ await adapter.dropGraph(graphUri);
96
+ }
97
+ await adapter.insertTurtle(graphUri, content);
98
+ const infer = args.infer;
99
+ let inference;
100
+ if (infer !== false) {
101
+ inference = await materializeInferences(adapter, graphUri);
102
+ }
103
+ return {
104
+ success: true,
105
+ tripleCount: validation.tripleCount,
106
+ replaced: !!replace,
107
+ ...(inference ? { inferredCount: inference.inferredCount, inferenceRules: inference.rules } : {}),
108
+ };
109
+ }
110
+ function injectPrefixes(sparql, prefixes) {
111
+ const lines = Object.entries(prefixes)
112
+ .filter(([prefix]) => {
113
+ const re = new RegExp(`PREFIX\\s+${prefix}\\s*:`, 'i');
114
+ return !re.test(sparql);
115
+ })
116
+ .map(([prefix, uri]) => `PREFIX ${prefix}: <${uri}>`);
117
+ if (lines.length === 0)
118
+ return sparql;
119
+ return lines.join('\n') + '\n' + sparql;
120
+ }
121
+ async function handleQuery(args) {
122
+ const sparql = args.sparql;
123
+ const raw = args.raw;
124
+ const { config, graphUri } = resolveConfig({
125
+ graphUri: args.graphUri,
126
+ graph: args.graph,
127
+ });
128
+ let query = sparql;
129
+ // Inject project-level PREFIX declarations from config
130
+ if (config.prefixes) {
131
+ query = injectPrefixes(query, config.prefixes);
132
+ }
133
+ if (!raw && !hasGraphScope(sparql)) {
134
+ const scoped = autoScopeQuery(sparql, graphUri);
135
+ if (scoped) {
136
+ query = scoped;
137
+ }
138
+ }
139
+ const adapter = await createReadyAdapter(config);
140
+ return await adapter.sparqlQuery(query);
141
+ }
142
+ async function handleStatus(args) {
143
+ const { config, graphUri } = resolveConfig({
144
+ graphUri: args.graphUri,
145
+ graph: args.graph,
146
+ });
147
+ const adapter = await createReadyAdapter(config);
148
+ const inferenceGraphUri = getInferenceGraphUri(graphUri);
149
+ const assertedCount = await adapter.getGraphTripleCount(graphUri);
150
+ const inferredCount = await adapter.getGraphTripleCount(inferenceGraphUri);
151
+ return {
152
+ projectId: config.projectId,
153
+ mode: config.mode,
154
+ endpoint: config.endpoint,
155
+ graphUri,
156
+ tripleCount: assertedCount + inferredCount,
157
+ assertedCount,
158
+ inferredCount,
159
+ };
160
+ }
161
+ async function handlePull(args) {
162
+ const { config, graphUri } = resolveConfig({
163
+ graphUri: args.graphUri,
164
+ graph: args.graph,
165
+ });
166
+ const adapter = await createReadyAdapter(config);
167
+ const turtle = await adapter.exportGraph(graphUri);
168
+ return turtle;
169
+ }
170
+ async function handleSchema(args) {
171
+ const { config, graphUri } = resolveConfig({
172
+ graphUri: args.graphUri,
173
+ graph: args.graph,
174
+ });
175
+ const adapter = await createReadyAdapter(config);
176
+ const classUri = args.class;
177
+ if (classUri) {
178
+ return await adapter.getClassDetails(graphUri, classUri);
179
+ }
180
+ else {
181
+ return await adapter.getSchemaOverview(graphUri);
182
+ }
183
+ }
184
+ async function handleDrop(args) {
185
+ const confirm = args.confirm;
186
+ if (!confirm) {
187
+ throw new Error('Drop requires confirm: true to prevent accidental deletion.');
188
+ }
189
+ const { config, graphUri } = resolveConfig({
190
+ graphUri: args.graphUri,
191
+ graph: args.graph,
192
+ });
193
+ const adapter = await createReadyAdapter(config);
194
+ await adapter.dropGraph(graphUri);
195
+ return { success: true, graphUri };
196
+ }
197
+ async function handleDelete(args) {
198
+ const content = args.content;
199
+ const where = args.where;
200
+ if (!content && !where) {
201
+ throw new Error('Provide either content (Turtle) or where (SPARQL pattern)');
202
+ }
203
+ const { config, graphUri } = resolveConfig({
204
+ graphUri: args.graphUri,
205
+ graph: args.graph,
206
+ });
207
+ const adapter = await createReadyAdapter(config);
208
+ await adapter.deleteTriples(graphUri, { turtle: content, where });
209
+ return { success: true };
210
+ }
211
+ async function handleDiff(args) {
212
+ const content = args.content;
213
+ if (!content) {
214
+ throw new Error('content (Turtle) is required');
215
+ }
216
+ const { config, graphUri } = resolveConfig({
217
+ graphUri: args.graphUri,
218
+ graph: args.graph,
219
+ });
220
+ const adapter = await createReadyAdapter(config);
221
+ return await adapter.diffGraph(graphUri, content);
222
+ }
223
+ async function handleGraphList(_args) {
224
+ const config = loadConfig();
225
+ const adapter = await createReadyAdapter(config);
226
+ const results = await adapter.sparqlQuery(`SELECT DISTINCT ?g (COUNT(*) AS ?count) WHERE { GRAPH ?g { ?s ?p ?o } } GROUP BY ?g`);
227
+ const baseUri = config.graphUri;
228
+ const remoteGraphs = new Map();
229
+ for (const binding of results.results.bindings) {
230
+ const g = binding['g']?.value;
231
+ const count = binding['count']?.value;
232
+ if (g && g.startsWith(baseUri)) {
233
+ remoteGraphs.set(g, count ? parseInt(count, 10) : 0);
234
+ }
235
+ }
236
+ const configGraphs = config.graphs ?? {};
237
+ const uriToName = new Map();
238
+ uriToName.set(baseUri, '(default)');
239
+ for (const [name, uri] of Object.entries(configGraphs)) {
240
+ uriToName.set(uri, name);
241
+ }
242
+ const allUris = new Set([
243
+ baseUri,
244
+ ...remoteGraphs.keys(),
245
+ ...Object.values(configGraphs),
246
+ ]);
247
+ const graphList = [...allUris].map((uri) => ({
248
+ name: uriToName.get(uri) ?? '?',
249
+ uri,
250
+ triples: remoteGraphs.get(uri) ?? null,
251
+ }));
252
+ return { graphs: graphList };
253
+ }
254
+ async function handleGraphCreate(args) {
255
+ const name = args.name;
256
+ if (!name) {
257
+ throw new Error('name is required');
258
+ }
259
+ const config = loadConfig();
260
+ const graphs = config.graphs ?? {};
261
+ if (graphs[name]) {
262
+ throw new Error(`Graph '${name}' already exists: ${graphs[name]}`);
263
+ }
264
+ const uri = `${config.graphUri}/${name}`;
265
+ graphs[name] = uri;
266
+ config.graphs = graphs;
267
+ saveConfig(config);
268
+ return { success: true, name, uri };
269
+ }
270
+ async function handleGraphDrop(args) {
271
+ const name = args.name;
272
+ const confirm = args.confirm;
273
+ if (!name) {
274
+ throw new Error('name is required');
275
+ }
276
+ if (!confirm) {
277
+ throw new Error('Graph drop requires confirm: true to prevent accidental deletion.');
278
+ }
279
+ const config = loadConfig();
280
+ const graphUri = resolveGraphUri(config, name);
281
+ const adapter = await createReadyAdapter(config);
282
+ await adapter.dropGraph(graphUri);
283
+ const graphs = config.graphs ?? {};
284
+ delete graphs[name];
285
+ config.graphs = Object.keys(graphs).length > 0 ? graphs : undefined;
286
+ saveConfig(config);
287
+ return { success: true, name, graphUri };
288
+ }
289
+ async function handleInfer(args) {
290
+ const clear = args.clear;
291
+ const { config, graphUri } = resolveConfig({
292
+ graphUri: args.graphUri,
293
+ graph: args.graph,
294
+ });
295
+ const adapter = await createReadyAdapter(config);
296
+ if (clear) {
297
+ await clearInferences(adapter, graphUri);
298
+ return { success: true, cleared: true };
299
+ }
300
+ const result = await materializeInferences(adapter, graphUri);
301
+ return result;
302
+ }
303
+ async function handleContextScan(args) {
304
+ const maxBytes = args.maxSnapshotBytes ?? 15360;
305
+ const snapshot = await scanCodebase(process.cwd(), maxBytes);
306
+ return {
307
+ codebaseSnapshot: snapshot,
308
+ _hint: snapshot.dependencyGraph && snapshot.dependencyGraph.modules.length > 0
309
+ ? 'Analyze codebaseSnapshot and push Knowledge triples via opentology_push. Module dependency triples (otx:Module + otx:dependsOn) are available in dependencyGraph — push them to the context graph as-is.'
310
+ : 'Analyze codebaseSnapshot and push Knowledge triples via opentology_push. No dependency graph was auto-extracted (non-JS/TS project or parsing issue). Inspect key source files manually and push otx:Module + otx:dependsOn triples for the important modules you identify.',
311
+ };
312
+ }
313
+ async function handleContextInit(args) {
314
+ const force = args.force;
315
+ const config = loadConfig();
316
+ const graphs = config.graphs ?? {};
317
+ const contextUri = `${config.graphUri}/context`;
318
+ const sessionsUri = `${config.graphUri}/sessions`;
319
+ const actions = [];
320
+ // Create graphs
321
+ if (!graphs['context']) {
322
+ graphs['context'] = contextUri;
323
+ actions.push(`Created graph 'context' -> ${contextUri}`);
324
+ }
325
+ if (!graphs['sessions']) {
326
+ graphs['sessions'] = sessionsUri;
327
+ actions.push(`Created graph 'sessions' -> ${sessionsUri}`);
328
+ }
329
+ config.graphs = graphs;
330
+ // Bootstrap ontology
331
+ const ontologyDir = join(process.cwd(), '.opentology');
332
+ const ontologyPath = join(ontologyDir, 'ontology.ttl');
333
+ if (!existsSync(ontologyPath) || force) {
334
+ mkdirSync(ontologyDir, { recursive: true });
335
+ writeFileSync(ontologyPath, OTX_BOOTSTRAP_TURTLE, 'utf-8');
336
+ if (!config.files)
337
+ config.files = {};
338
+ if (!config.files[contextUri])
339
+ config.files[contextUri] = [];
340
+ const relPath = '.opentology/ontology.ttl';
341
+ if (!config.files[contextUri].includes(relPath)) {
342
+ config.files[contextUri].push(relPath);
343
+ }
344
+ actions.push('Bootstrapped otx ontology (6 classes, 12 properties)');
345
+ }
346
+ // Generate hook script
347
+ const hookDir = join(process.cwd(), '.opentology', 'hooks');
348
+ const hookPath = join(hookDir, 'session-start.mjs');
349
+ if (!existsSync(hookPath) || force) {
350
+ mkdirSync(hookDir, { recursive: true });
351
+ writeFileSync(hookPath, generateHookScript(), 'utf-8');
352
+ actions.push('Generated hook: .opentology/hooks/session-start.mjs');
353
+ }
354
+ // Update CLAUDE.md
355
+ const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
356
+ const section = generateContextSection(config.projectId, config.graphUri);
357
+ if (!existsSync(claudeMdPath) || force) {
358
+ updateClaudeMd(claudeMdPath, section);
359
+ actions.push('Updated CLAUDE.md with context instructions');
360
+ }
361
+ else {
362
+ updateClaudeMd(claudeMdPath, section);
363
+ actions.push('Updated CLAUDE.md context section');
364
+ }
365
+ // Generate slash commands
366
+ const commandsDir = join(process.cwd(), '.claude', 'commands');
367
+ const slashCommands = generateSlashCommands();
368
+ mkdirSync(commandsDir, { recursive: true });
369
+ let slashCreated = 0;
370
+ for (const cmd of slashCommands) {
371
+ const cmdPath = join(commandsDir, cmd.filename);
372
+ if (!existsSync(cmdPath) || force) {
373
+ writeFileSync(cmdPath, cmd.content, 'utf-8');
374
+ slashCreated++;
375
+ }
376
+ }
377
+ if (slashCreated > 0) {
378
+ actions.push(`Generated ${slashCreated} slash commands in .claude/commands/`);
379
+ }
380
+ saveConfig(config);
381
+ // Auto-push Module triples from dependency graph
382
+ let moduleStats = null;
383
+ try {
384
+ const snapshot = await scanCodebase(process.cwd());
385
+ if (snapshot.dependencyGraph && snapshot.dependencyGraph.modules.length > 0) {
386
+ const dg = snapshot.dependencyGraph;
387
+ const adapter = await createReadyAdapter(config);
388
+ const sparqlTriples = [];
389
+ for (const mod of dg.modules) {
390
+ sparqlTriples.push(`<urn:module:${mod}> a <https://opentology.dev/vocab#Module> .`);
391
+ }
392
+ for (const edge of dg.edges) {
393
+ sparqlTriples.push(`<urn:module:${edge.from}> <https://opentology.dev/vocab#dependsOn> <urn:module:${edge.to}> .`);
394
+ }
395
+ await adapter.sparqlUpdate(`INSERT DATA { GRAPH <${contextUri}> {\n${sparqlTriples.join('\n')}\n} }`);
396
+ moduleStats = { modules: dg.modules.length, edges: dg.edges.length };
397
+ actions.push(`Pushed ${dg.modules.length} Module triples with ${dg.edges.length} dependsOn edges`);
398
+ }
399
+ }
400
+ catch {
401
+ // Non-fatal: dependency graph push is best-effort
402
+ }
403
+ const dependencyHint = moduleStats
404
+ ? `Dependency graph pushed: ${moduleStats.modules} modules, ${moduleStats.edges} edges. Query with: SELECT ?affected WHERE { ?affected otx:dependsOn+ <urn:module:...> }`
405
+ : 'No dependency graph auto-extracted (non-JS/TS project or no local imports found). Inspect key source files and manually push otx:Module + otx:dependsOn triples for important modules.';
406
+ return {
407
+ success: true,
408
+ projectId: config.projectId,
409
+ contextGraph: contextUri,
410
+ sessionsGraph: sessionsUri,
411
+ actions,
412
+ moduleStats,
413
+ dependencyHint,
414
+ hookSnippet: {
415
+ hooks: {
416
+ SessionStart: [{
417
+ type: 'command',
418
+ command: 'node .opentology/hooks/session-start.mjs',
419
+ }],
420
+ },
421
+ },
422
+ };
423
+ }
424
+ async function handleContextLoad() {
425
+ const config = loadConfig();
426
+ const graphs = config.graphs ?? {};
427
+ if (!graphs['context'] || !graphs['sessions']) {
428
+ throw new Error('Context not initialized. Use opentology_context_init first.');
429
+ }
430
+ const contextUri = graphs['context'];
431
+ const sessionsUri = graphs['sessions'];
432
+ const adapter = await createReadyAdapter(config);
433
+ const output = {
434
+ projectId: config.projectId,
435
+ graphUri: config.graphUri,
436
+ sessions: [],
437
+ openIssues: [],
438
+ recentDecisions: [],
439
+ meta: {
440
+ contextTripleCount: 0,
441
+ sessionsTripleCount: 0,
442
+ loadedAt: new Date().toISOString(),
443
+ },
444
+ warnings: [],
445
+ };
446
+ // Query 1: Recent sessions
447
+ try {
448
+ const r = await adapter.sparqlQuery(`
449
+ PREFIX otx: <https://opentology.dev/vocab#>
450
+ SELECT ?session ?title ?date ?nextTodo WHERE {
451
+ GRAPH <${sessionsUri}> {
452
+ ?session a otx:Session ; otx:title ?title ; otx:date ?date .
453
+ OPTIONAL { ?session otx:nextTodo ?nextTodo }
454
+ }
455
+ } ORDER BY DESC(?date) LIMIT 3
456
+ `);
457
+ output.sessions = r.results.bindings.map((b) => ({
458
+ uri: b['session']?.value ?? '',
459
+ title: b['title']?.value ?? '',
460
+ date: b['date']?.value ?? '',
461
+ ...(b['nextTodo']?.value ? { nextTodo: b['nextTodo'].value } : {}),
462
+ }));
463
+ }
464
+ catch (err) {
465
+ output.warnings.push(`Sessions query failed: ${err.message}`);
466
+ }
467
+ // Query 2: Open issues
468
+ try {
469
+ const r = await adapter.sparqlQuery(`
470
+ PREFIX otx: <https://opentology.dev/vocab#>
471
+ SELECT ?issue ?title ?date WHERE {
472
+ GRAPH <${contextUri}> {
473
+ ?issue a otx:Issue ; otx:title ?title ; otx:date ?date ; otx:status "open" .
474
+ }
475
+ } ORDER BY DESC(?date) LIMIT 10
476
+ `);
477
+ output.openIssues = r.results.bindings.map((b) => ({
478
+ uri: b['issue']?.value ?? '',
479
+ title: b['title']?.value ?? '',
480
+ date: b['date']?.value ?? '',
481
+ }));
482
+ }
483
+ catch (err) {
484
+ output.warnings.push(`Issues query failed: ${err.message}`);
485
+ }
486
+ // Query 3: Recent decisions
487
+ try {
488
+ const r = await adapter.sparqlQuery(`
489
+ PREFIX otx: <https://opentology.dev/vocab#>
490
+ SELECT ?decision ?title ?date ?reason WHERE {
491
+ GRAPH <${contextUri}> {
492
+ ?decision a otx:Decision ; otx:title ?title ; otx:date ?date .
493
+ OPTIONAL { ?decision otx:reason ?reason }
494
+ }
495
+ } ORDER BY DESC(?date) LIMIT 3
496
+ `);
497
+ output.recentDecisions = r.results.bindings.map((b) => ({
498
+ uri: b['decision']?.value ?? '',
499
+ title: b['title']?.value ?? '',
500
+ date: b['date']?.value ?? '',
501
+ ...(b['reason']?.value ? { reason: b['reason'].value } : {}),
502
+ }));
503
+ }
504
+ catch (err) {
505
+ output.warnings.push(`Decisions query failed: ${err.message}`);
506
+ }
507
+ try {
508
+ output.meta.contextTripleCount = await adapter.getGraphTripleCount(contextUri);
509
+ }
510
+ catch { /* */ }
511
+ try {
512
+ output.meta.sessionsTripleCount = await adapter.getGraphTripleCount(sessionsUri);
513
+ }
514
+ catch { /* */ }
515
+ if (output.warnings.length === 0)
516
+ delete output.warnings;
517
+ return output;
518
+ }
519
+ async function handleContextStatus() {
520
+ const config = loadConfig();
521
+ const graphs = config.graphs ?? {};
522
+ const hasContext = !!graphs['context'];
523
+ const hasSessions = !!graphs['sessions'];
524
+ const initialized = hasContext && hasSessions;
525
+ const result = { initialized };
526
+ if (initialized) {
527
+ const adapter = await createReadyAdapter(config);
528
+ result.graphs = {
529
+ context: { uri: graphs['context'], triples: await adapter.getGraphTripleCount(graphs['context']).catch(() => 0) },
530
+ sessions: { uri: graphs['sessions'], triples: await adapter.getGraphTripleCount(graphs['sessions']).catch(() => 0) },
531
+ };
532
+ }
533
+ result.hook = existsSync(join(process.cwd(), '.opentology', 'hooks', 'session-start.mjs'));
534
+ const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
535
+ if (!existsSync(claudeMdPath)) {
536
+ result.claudeMd = 'missing';
537
+ }
538
+ else {
539
+ const { readFileSync } = await import('node:fs');
540
+ result.claudeMd = readFileSync(claudeMdPath, 'utf-8').includes('OPENTOLOGY:CONTEXT:BEGIN') ? 'markers_present' : 'markers_missing';
541
+ }
542
+ return result;
543
+ }
544
+ export async function startMcpServer() {
545
+ const server = new Server({ name: 'opentology', version: '0.1.0' }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
546
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
547
+ resources: [
548
+ {
549
+ uri: 'opentology://schema',
550
+ name: 'Ontology Schema Overview',
551
+ description: 'Prefix mappings, class list, and property list for the current project graph. Lightweight summary for SPARQL query context.',
552
+ mimeType: 'application/json',
553
+ },
554
+ ],
555
+ }));
556
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
557
+ const { uri } = request.params;
558
+ if (uri === 'opentology://schema') {
559
+ try {
560
+ const { config, graphUri } = resolveConfig({});
561
+ const adapter = await createReadyAdapter(config);
562
+ const overview = await adapter.getSchemaOverview(graphUri);
563
+ return {
564
+ contents: [
565
+ {
566
+ uri: 'opentology://schema',
567
+ mimeType: 'application/json',
568
+ text: JSON.stringify(overview, null, 2),
569
+ },
570
+ ],
571
+ };
572
+ }
573
+ catch (error) {
574
+ return {
575
+ contents: [
576
+ {
577
+ uri: 'opentology://schema',
578
+ mimeType: 'text/plain',
579
+ text: `Error loading schema: ${error.message}`,
580
+ },
581
+ ],
582
+ };
583
+ }
584
+ }
585
+ throw new Error(`Unknown resource: ${uri}`);
586
+ });
587
+ // --- MCP Prompts (slash commands) ---
588
+ const promptDefinitions = generateSlashCommands().map((cmd) => ({
589
+ name: cmd.filename.replace(/\.md$/, ''),
590
+ description: cmd.content.split('\n')[0],
591
+ content: cmd.content,
592
+ }));
593
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
594
+ prompts: promptDefinitions.map(({ name, description }) => ({
595
+ name,
596
+ description,
597
+ })),
598
+ }));
599
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
600
+ const { name } = request.params;
601
+ const prompt = promptDefinitions.find((p) => p.name === name);
602
+ if (!prompt) {
603
+ throw new Error(`Unknown prompt: ${name}`);
604
+ }
605
+ return {
606
+ messages: [
607
+ {
608
+ role: 'user',
609
+ content: { type: 'text', text: prompt.content },
610
+ },
611
+ ],
612
+ };
613
+ });
614
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
615
+ tools: [
616
+ {
617
+ name: 'opentology_init',
618
+ description: 'Initialize a new OpenTology project. Creates .opentology.json config with the project ID, SPARQL endpoint, and named graph URI. Must be run before other tools if no config exists.',
619
+ inputSchema: {
620
+ type: 'object',
621
+ properties: {
622
+ projectId: {
623
+ type: 'string',
624
+ description: 'Unique identifier for the project (used in the graph URI)',
625
+ },
626
+ mode: {
627
+ type: 'string',
628
+ enum: ['http', 'embedded'],
629
+ description: 'Store mode: http (needs SPARQL server) or embedded (no server needed). Default: http',
630
+ },
631
+ endpoint: {
632
+ type: 'string',
633
+ description: 'SPARQL endpoint URL (default: http://localhost:7878, only used in http mode)',
634
+ },
635
+ },
636
+ required: ['projectId'],
637
+ },
638
+ },
639
+ {
640
+ name: 'opentology_validate',
641
+ description: 'Validate Turtle (RDF) content for syntax correctness. Returns triple count and prefixes if valid, or an error message if invalid. Use before pushing to catch errors early. When shacl is true, also validates against SHACL shapes in shapes/ directory.',
642
+ inputSchema: {
643
+ type: 'object',
644
+ properties: {
645
+ content: {
646
+ type: 'string',
647
+ description: 'Turtle (RDF) content to validate',
648
+ },
649
+ shacl: {
650
+ type: 'boolean',
651
+ description: 'If true, also validate against SHACL shapes in shapes/ directory',
652
+ },
653
+ },
654
+ required: ['content'],
655
+ },
656
+ },
657
+ {
658
+ name: 'opentology_push',
659
+ description: 'Validate and insert Turtle (RDF) triples into the project graph. Validates syntax first, then pushes to the SPARQL endpoint. Auto-validates against SHACL shapes when shapes/ directory exists. Returns success status and triple count. IMPORTANT: Maximum 100 triples per call. For larger datasets, split into multiple pushes of 20-50 triples each.',
660
+ inputSchema: {
661
+ type: 'object',
662
+ properties: {
663
+ content: {
664
+ type: 'string',
665
+ description: 'Turtle (RDF) content to insert',
666
+ },
667
+ replace: {
668
+ type: 'boolean',
669
+ description: 'If true, drop the entire graph before inserting (replace mode)',
670
+ },
671
+ shacl: {
672
+ type: 'boolean',
673
+ description: 'Set to false to skip SHACL validation. When shapes exist and this is not explicitly false, SHACL validation runs automatically.',
674
+ },
675
+ infer: {
676
+ type: 'boolean',
677
+ description: 'Set to false to skip RDFS inference after push. Defaults to true.',
678
+ },
679
+ graph: {
680
+ type: 'string',
681
+ description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
682
+ },
683
+ graphUri: {
684
+ type: 'string',
685
+ description: 'Named graph URI (uses config default if omitted)',
686
+ },
687
+ },
688
+ required: ['content'],
689
+ },
690
+ },
691
+ {
692
+ name: 'opentology_query',
693
+ description: 'Execute a SPARQL query against the project graph. Automatically scopes unscoped queries to the project named graph unless raw mode is enabled.',
694
+ inputSchema: {
695
+ type: 'object',
696
+ properties: {
697
+ sparql: {
698
+ type: 'string',
699
+ description: 'SPARQL query string',
700
+ },
701
+ graph: {
702
+ type: 'string',
703
+ description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
704
+ },
705
+ graphUri: {
706
+ type: 'string',
707
+ description: 'Named graph URI (uses config default if omitted)',
708
+ },
709
+ raw: {
710
+ type: 'boolean',
711
+ description: 'If true, skip automatic graph scoping and send the query as-is',
712
+ },
713
+ },
714
+ required: ['sparql'],
715
+ },
716
+ },
717
+ {
718
+ name: 'opentology_status',
719
+ description: 'Get the current status of the OpenTology project, including project ID, endpoint, graph URI, and the number of triples stored in the graph.',
720
+ inputSchema: {
721
+ type: 'object',
722
+ properties: {
723
+ graph: {
724
+ type: 'string',
725
+ description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
726
+ },
727
+ graphUri: {
728
+ type: 'string',
729
+ description: 'Named graph URI (uses config default if omitted)',
730
+ },
731
+ },
732
+ },
733
+ },
734
+ {
735
+ name: 'opentology_pull',
736
+ description: 'Export the entire project graph as Turtle (RDF). Returns all triples from the named graph serialized in Turtle format.',
737
+ inputSchema: {
738
+ type: 'object',
739
+ properties: {
740
+ graph: {
741
+ type: 'string',
742
+ description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
743
+ },
744
+ graphUri: {
745
+ type: 'string',
746
+ description: 'Named graph URI (uses config default if omitted)',
747
+ },
748
+ },
749
+ },
750
+ },
751
+ {
752
+ name: 'opentology_schema',
753
+ description: 'Inspect the ontology schema. Without parameters, returns all classes and properties (same as opentology://schema resource). With a class parameter, returns detailed info: instance count, properties used by that class, and sample triples.',
754
+ inputSchema: {
755
+ type: 'object',
756
+ properties: {
757
+ class: {
758
+ type: 'string',
759
+ description: 'URI of a specific class to inspect (e.g., "http://schema.org/Person"). If omitted, returns the full schema overview.',
760
+ },
761
+ graph: {
762
+ type: 'string',
763
+ description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
764
+ },
765
+ graphUri: {
766
+ type: 'string',
767
+ description: 'Named graph URI (uses config default if omitted)',
768
+ },
769
+ },
770
+ },
771
+ },
772
+ {
773
+ name: 'opentology_drop',
774
+ description: 'Drop (delete) the entire project graph. Requires confirm: true to prevent accidental deletion.',
775
+ inputSchema: {
776
+ type: 'object',
777
+ properties: {
778
+ confirm: {
779
+ type: 'boolean',
780
+ description: 'Must be true to confirm graph deletion',
781
+ },
782
+ graph: {
783
+ type: 'string',
784
+ description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
785
+ },
786
+ graphUri: {
787
+ type: 'string',
788
+ description: 'Named graph URI (uses config default if omitted)',
789
+ },
790
+ },
791
+ required: ['confirm'],
792
+ },
793
+ },
794
+ {
795
+ name: 'opentology_delete',
796
+ description: 'Delete specific triples. Provide Turtle content to remove those exact triples, or a SPARQL WHERE pattern for pattern-based deletion.',
797
+ inputSchema: {
798
+ type: 'object',
799
+ properties: {
800
+ content: {
801
+ type: 'string',
802
+ description: 'Turtle (RDF) content specifying triples to delete',
803
+ },
804
+ where: {
805
+ type: 'string',
806
+ description: 'SPARQL WHERE pattern for pattern-based deletion (e.g., "?s a <http://schema.org/Person>")',
807
+ },
808
+ graph: {
809
+ type: 'string',
810
+ description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
811
+ },
812
+ graphUri: {
813
+ type: 'string',
814
+ description: 'Named graph URI (uses config default if omitted)',
815
+ },
816
+ },
817
+ },
818
+ },
819
+ {
820
+ name: 'opentology_diff',
821
+ description: 'Compare local Turtle content against the remote graph. Returns added triples (in local but not remote), removed triples (in remote but not local), and count of unchanged triples.',
822
+ inputSchema: {
823
+ type: 'object',
824
+ properties: {
825
+ content: {
826
+ type: 'string',
827
+ description: 'Turtle (RDF) content to compare against the remote graph',
828
+ },
829
+ graph: {
830
+ type: 'string',
831
+ description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
832
+ },
833
+ graphUri: {
834
+ type: 'string',
835
+ description: 'Named graph URI (uses config default if omitted)',
836
+ },
837
+ },
838
+ required: ['content'],
839
+ },
840
+ },
841
+ {
842
+ name: 'opentology_graph_list',
843
+ description: 'List all named graphs for the project. Shows graph name, URI, and triple count.',
844
+ inputSchema: {
845
+ type: 'object',
846
+ properties: {},
847
+ },
848
+ },
849
+ {
850
+ name: 'opentology_graph_create',
851
+ description: 'Create a new named graph. Generates a URI based on the project graph URI and registers it in the config.',
852
+ inputSchema: {
853
+ type: 'object',
854
+ properties: {
855
+ name: {
856
+ type: 'string',
857
+ description: 'Logical name for the new graph',
858
+ },
859
+ },
860
+ required: ['name'],
861
+ },
862
+ },
863
+ {
864
+ name: 'opentology_graph_drop',
865
+ description: 'Drop a named graph and remove it from config. Requires confirm: true to prevent accidental deletion.',
866
+ inputSchema: {
867
+ type: 'object',
868
+ properties: {
869
+ name: {
870
+ type: 'string',
871
+ description: 'Logical name of the graph to drop',
872
+ },
873
+ confirm: {
874
+ type: 'boolean',
875
+ description: 'Must be true to confirm graph deletion',
876
+ },
877
+ },
878
+ required: ['name', 'confirm'],
879
+ },
880
+ },
881
+ {
882
+ name: 'opentology_infer',
883
+ description: 'Run RDFS inference on the project graph, materializing inferred triples into the main graph (so queries work naturally). A bookkeeping copy is kept in the inference graph for status reporting and clear support. With clear: true, removes inferred triples from both graphs.',
884
+ inputSchema: {
885
+ type: 'object',
886
+ properties: {
887
+ clear: {
888
+ type: 'boolean',
889
+ description: 'If true, clear the inference graph instead of materializing',
890
+ },
891
+ graph: {
892
+ type: 'string',
893
+ description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
894
+ },
895
+ graphUri: {
896
+ type: 'string',
897
+ description: 'Named graph URI (uses config default if omitted)',
898
+ },
899
+ },
900
+ },
901
+ },
902
+ {
903
+ name: 'opentology_context_init',
904
+ description: 'Initialize project context graph for session-based knowledge management. Creates context/sessions named graphs, bootstraps otx ontology vocabulary, generates a Claude Code SessionStart hook script, and updates CLAUDE.md. Idempotent — safe to call multiple times. Use force: true to regenerate hook and CLAUDE.md.',
905
+ inputSchema: {
906
+ type: 'object',
907
+ properties: {
908
+ force: {
909
+ type: 'boolean',
910
+ description: 'Regenerate hook script and CLAUDE.md even if they already exist',
911
+ },
912
+ },
913
+ },
914
+ },
915
+ {
916
+ name: 'opentology_context_load',
917
+ description: 'Load project context: recent sessions (last 3), open issues (up to 10), and recent decisions (last 3) from the context graph. Returns structured JSON. Call this at the start of a session to understand project state. Requires context to be initialized first (opentology_context_init).',
918
+ inputSchema: {
919
+ type: 'object',
920
+ properties: {},
921
+ },
922
+ },
923
+ {
924
+ name: 'opentology_context_status',
925
+ description: 'Check whether project context is initialized. Shows graph triple counts, hook script presence, and CLAUDE.md marker status.',
926
+ inputSchema: {
927
+ type: 'object',
928
+ properties: {},
929
+ },
930
+ },
931
+ {
932
+ name: 'opentology_context_scan',
933
+ description: 'Scan the current project codebase and return a structured snapshot (package.json, directory tree, entry points, detected frameworks, dependency graph). Includes module dependency edges extracted from import statements. Use the snapshot to create Knowledge and Module triples via opentology_push. Can be called independently of context_init.',
934
+ inputSchema: {
935
+ type: 'object',
936
+ properties: {
937
+ maxSnapshotBytes: {
938
+ type: 'number',
939
+ description: 'Maximum snapshot payload size in bytes (default: 15360). Truncates and warns if exceeded.',
940
+ },
941
+ },
942
+ },
943
+ },
944
+ ],
945
+ }));
946
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
947
+ const { name, arguments: args } = request.params;
948
+ try {
949
+ let result;
950
+ switch (name) {
951
+ case 'opentology_init':
952
+ result = await handleInit(args);
953
+ break;
954
+ case 'opentology_validate':
955
+ result = await handleValidate(args);
956
+ break;
957
+ case 'opentology_push':
958
+ result = await handlePush(args);
959
+ break;
960
+ case 'opentology_query':
961
+ result = await handleQuery(args);
962
+ break;
963
+ case 'opentology_status':
964
+ result = await handleStatus(args);
965
+ break;
966
+ case 'opentology_pull':
967
+ result = await handlePull(args);
968
+ break;
969
+ case 'opentology_schema':
970
+ result = await handleSchema(args);
971
+ break;
972
+ case 'opentology_drop':
973
+ result = await handleDrop(args);
974
+ break;
975
+ case 'opentology_delete':
976
+ result = await handleDelete(args);
977
+ break;
978
+ case 'opentology_diff':
979
+ result = await handleDiff(args);
980
+ break;
981
+ case 'opentology_graph_list':
982
+ result = await handleGraphList(args);
983
+ break;
984
+ case 'opentology_graph_create':
985
+ result = await handleGraphCreate(args);
986
+ break;
987
+ case 'opentology_graph_drop':
988
+ result = await handleGraphDrop(args);
989
+ break;
990
+ case 'opentology_infer':
991
+ result = await handleInfer(args);
992
+ break;
993
+ case 'opentology_context_scan':
994
+ result = await handleContextScan(args);
995
+ break;
996
+ case 'opentology_context_init':
997
+ result = await handleContextInit(args);
998
+ break;
999
+ case 'opentology_context_load':
1000
+ result = await handleContextLoad();
1001
+ break;
1002
+ case 'opentology_context_status':
1003
+ result = await handleContextStatus();
1004
+ break;
1005
+ default:
1006
+ throw new Error(`Unknown tool: ${name}`);
1007
+ }
1008
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
1009
+ return { content: [{ type: 'text', text }] };
1010
+ }
1011
+ catch (error) {
1012
+ return {
1013
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
1014
+ isError: true,
1015
+ };
1016
+ }
1017
+ });
1018
+ const transport = new StdioServerTransport();
1019
+ await server.connect(transport);
1020
+ }