kibi-mcp 0.15.3 → 0.16.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.
@@ -37,7 +37,7 @@ let diagnosticUsageLogPath = null;
37
37
  export function initializeDiagnosticMode(enabled = DIAGNOSTIC_MODE_ENABLED) {
38
38
  diagnosticUsageLogPath = null;
39
39
  if (!enabled) {
40
- Reflect.deleteProperty(process.env, "KIBI_MCP_DIAGNOSTIC_MODE");
40
+ process.env.KIBI_MCP_DIAGNOSTIC_MODE = "0";
41
41
  return;
42
42
  }
43
43
  const workspaceRoot = resolveWorkspaceRoot();
@@ -38,6 +38,7 @@ function renderToolsDoc() {
38
38
  lines.push("");
39
39
  lines.push("Modeling note: Kibi has eight core entity types grouped into common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).");
40
40
  lines.push("Only strict domain facts (`fact_kind: subject` + `property_value`) participate in contradiction inference; use `flag` for runtime/config gates and `fact_kind: observation` or `meta` for bug/workaround notes.");
41
+ lines.push("Predicate flow: before writing ontology prose, call `kb_suggest_predicates`; apply a suggested `fact_kind: predicate` via `requires_predicate`, or record the returned `review:ontology-gap` observation when no predicate fits.");
41
42
  return lines.join("\n");
42
43
  }
43
44
  export const PROMPTS = [
@@ -135,8 +136,9 @@ export const PROMPTS = [
135
136
  "3. **Create-before-link**: Create endpoint entities with `kb_upsert` before linking them.",
136
137
  "4. **Validate intent**: If creating links, call `kb_query` for both endpoint IDs first to ensure they exist.",
137
138
  "5. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property` (automated via `kb_model_requirement`).",
138
- "5. **Mutate**: Call `kb_upsert` for create/update, or `kb_delete` for explicit removals.",
139
- "6. **Targeted checks**: Run `kb_check` after meaningful mutations; specify only the rules you need.",
139
+ "6. **Suggest predicates before prose**: For ontology-lane requirements, spell out the prose claim and call `kb_suggest_predicates` before writing `fact_kind: observation`. Apply the selected `fact_kind: predicate` applyPlan, then attach the returned `relationshipPlan` as `requires_predicate` while preserving existing req metadata; use the returned `review:ontology-gap` observation when no predicate fits.",
140
+ "7. **Mutate**: Call `kb_upsert` for create/update, or `kb_delete` for explicit removals.",
141
+ "8. **Targeted checks**: Run `kb_check` after meaningful mutations; specify only the rules you need.",
140
142
  "",
141
143
  "If a tool returns empty results, do not assume failure. Re-check filters (type, id, tags, sourceFile, limit, or offset).",
142
144
  ].join("\n"),
@@ -203,16 +205,16 @@ function registerDocResources() {
203
205
  "4. Reuse the same constrained fact ID across related requirements; vary property facts only when semantics differ",
204
206
  '5. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation',
205
207
  "",
208
+ "## Model requirements as ontology predicates",
209
+ '1. Spell out the requirement prose and call `kb_suggest_predicates` with `{ "text": "...", "requirementId": "REQ-..." }`',
210
+ "2. If candidates are returned, apply the top or user-selected `structuredContent.applyPlan` to create `fact_kind: predicate`, then attach `structuredContent.relationshipPlan` with `requires_predicate` while preserving existing req metadata",
211
+ "3. If no candidate fits, apply or review the returned `review:ontology-gap` observation instead of silently writing prose",
212
+ "",
206
213
  "Note: Kibi has eight core entity types. Create or reuse `fact` entities first, then create `req` entities and link with `constrains` and `requires_property` (create-before-link).",
207
214
  "Only strict domain facts are contradiction-safe. Use `flag` for runtime/config gates; use `fact` with `fact_kind: observation` or `meta` for bug/workaround notes.",
208
215
  "",
209
216
  "## Find missing coverage",
210
217
  '1. `kb_find_gaps` with `{ "type": "req", "missingRelationships": ["specified_by", "verified_by"] }` to find under-linked requirements',
211
- "",
212
- "## Find missing coverage",
213
- "",
214
- "## Find missing coverage",
215
- '1. `kb_find_gaps` with `{ "type": "req", "missingRelationships": ["specified_by", "verified_by"] }` to find under-linked requirements',
216
218
  '2. `kb_coverage` with `{ "by": "req", "includePassing": false }` to review evaluated coverage rows',
217
219
  '3. `kb_graph` with `{ "seedIds": ["REQ-001"], "direction": "both", "depth": 2 }` to inspect neighboring entities',
218
220
  "",
@@ -30,8 +30,11 @@ import { handleKbModelRequirement, } from "../tools/model-requirement.js";
30
30
  import { handleKbQuery } from "../tools/query.js";
31
31
  import { handleKbSearch } from "../tools/search.js";
32
32
  import { handleKbSkillsList, handleKbSkillsLoad, handleKbSkillsRead, } from "../tools/skills.js";
33
+ import { handleSparql } from "../tools/sparql.js";
33
34
  import { handleKbStatus } from "../tools/status.js";
35
+ import { handleKbSuggestPredicates, } from "../tools/suggest-predicates.js";
34
36
  import { handleKbUpsert } from "../tools/upsert.js";
37
+ import { handleKbValidateUpsert } from "../tools/validate-upsert.js";
35
38
  const DEFAULT_TOOL_TIMEOUT_MS = 90_000;
36
39
  const TOOL_TIMEOUT_ENV = "KIBI_MCP_TOOL_TIMEOUT_MS";
37
40
  const defaultToolsServerDeps = {
@@ -78,6 +81,7 @@ const DEFAULT_TOOLS_RUNTIME = {
78
81
  handleKbDelete,
79
82
  handleKbFindGaps,
80
83
  handleKbGraph,
84
+ handleSparql,
81
85
  handleKbQuery,
82
86
  handleKbSearch,
83
87
  handleKbStatus,
@@ -85,7 +89,9 @@ const DEFAULT_TOOLS_RUNTIME = {
85
89
  handleKbSkillsLoad,
86
90
  handleKbSkillsRead,
87
91
  handleKbUpsert,
92
+ handleKbValidateUpsert,
88
93
  handleKbModelRequirement,
94
+ handleKbSuggestPredicates,
89
95
  handleKbAutopilotGenerate,
90
96
  };
91
97
  // implements REQ-008
@@ -391,10 +397,17 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
391
397
  const prolog = await runtime.ensureProlog();
392
398
  return runtime.handleKbGraph(prolog, args);
393
399
  }, runtime);
400
+ addTool(server, "kb_sparql_remote", toolDef("kb_sparql_remote").description, toolDef("kb_sparql_remote").inputSchema, async (args) => {
401
+ const prolog = await runtime.ensureProlog();
402
+ return runtime.handleSparql(prolog, args);
403
+ }, runtime);
394
404
  addTool(server, "kb_upsert", toolDef("kb_upsert").description, toolDef("kb_upsert").inputSchema, async (args) => {
395
405
  const prolog = await runtime.ensureProlog();
396
406
  return runtime.handleKbUpsert(prolog, args);
397
407
  }, runtime);
408
+ addTool(server, "kb_validate_upsert", toolDef("kb_validate_upsert").description, toolDef("kb_validate_upsert").inputSchema, async (args) => {
409
+ return runtime.handleKbValidateUpsert(args);
410
+ }, runtime);
398
411
  addTool(server, "kb_delete", toolDef("kb_delete").description, toolDef("kb_delete").inputSchema, async (args) => {
399
412
  const prolog = await runtime.ensureProlog();
400
413
  return runtime.handleKbDelete(prolog, args);
@@ -407,6 +420,10 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
407
420
  const prolog = await runtime.ensureProlog();
408
421
  return runtime.handleKbModelRequirement(prolog, args);
409
422
  }, runtime);
423
+ addTool(server, "kb_suggest_predicates", toolDef("kb_suggest_predicates").description, toolDef("kb_suggest_predicates").inputSchema, async (args) => {
424
+ const prolog = await runtime.ensureProlog();
425
+ return runtime.handleKbSuggestPredicates(prolog, args);
426
+ }, runtime);
410
427
  addTool(server, "kb_autopilot_generate", toolDef("kb_autopilot_generate").description, toolDef("kb_autopilot_generate").inputSchema, async (args) => {
411
428
  const prolog = await runtime.ensureProlog();
412
429
  return runtime.handleKbAutopilotGenerate(prolog, args);
@@ -847,7 +847,7 @@ export async function classifyActivationState(workspaceRoot, prolog) {
847
847
  }
848
848
  }
849
849
  // Recursively collect markdown files under `dir`, excluding known ignore dirs.
850
- function collectMarkdownFiles(dir, workspaceRoot, vendoredRoots) {
850
+ export function collectMarkdownFiles(dir, workspaceRoot, vendoredRoots) {
851
851
  const results = [];
852
852
  if (!fs.existsSync(dir))
853
853
  return results;
@@ -6,6 +6,25 @@ import { buildGenericMarkdownCandidates, buildNormativeRequirementCandidates, bu
6
6
  import { discoverProviderEvidence, resolveActivationPolicy, } from "./autopilot-discovery.js";
7
7
  import { loadEntities } from "./entity-query.js";
8
8
  import { getWorkspaceMigrationWarning } from "./model-requirement.js";
9
+ const defaultAutopilotGenerateDeps = {
10
+ buildGenericMarkdownCandidates,
11
+ buildNormativeRequirementCandidates,
12
+ buildProviderEvidenceCandidates,
13
+ buildSymbolManifestCandidates,
14
+ buildTypedMarkdownCandidates,
15
+ collectSourceOnlyAuthoringSignals,
16
+ discoverProviderEvidence,
17
+ getWorkspaceMigrationWarning,
18
+ loadEntities,
19
+ resolveActivationPolicy,
20
+ };
21
+ let autopilotGenerateDeps = defaultAutopilotGenerateDeps;
22
+ export function _setAutopilotGenerateDepsForTests(deps) {
23
+ autopilotGenerateDeps = { ...defaultAutopilotGenerateDeps, ...deps };
24
+ }
25
+ export function _resetAutopilotGenerateDepsForTests() {
26
+ autopilotGenerateDeps = defaultAutopilotGenerateDeps;
27
+ }
9
28
  function clamp(value, min, max) {
10
29
  return Math.max(min, Math.min(max, value));
11
30
  }
@@ -377,7 +396,7 @@ _prolog, args) {
377
396
  // Gather existing entity ids to suppress duplicates
378
397
  let existingIds = new Set();
379
398
  try {
380
- const entities = await loadEntities(prolog, {});
399
+ const entities = await autopilotGenerateDeps.loadEntities(prolog, {});
381
400
  for (const e of entities) {
382
401
  const id = String(e.id ?? "");
383
402
  if (id)
@@ -389,10 +408,10 @@ _prolog, args) {
389
408
  existingIds = new Set();
390
409
  }
391
410
  const workspaceRoot = resolveWorkspaceRoot();
392
- const activation = await resolveActivationPolicy(workspaceRoot, prolog);
411
+ const activation = await autopilotGenerateDeps.resolveActivationPolicy(workspaceRoot, prolog);
393
412
  const activationState = activation.activationState;
394
- const activationDiscovery = discoverProviderEvidence(workspaceRoot, activation);
395
- const migrationWarning = await getWorkspaceMigrationWarning(workspaceRoot);
413
+ const activationDiscovery = autopilotGenerateDeps.discoverProviderEvidence(workspaceRoot, activation);
414
+ const migrationWarning = await autopilotGenerateDeps.getWorkspaceMigrationWarning(workspaceRoot);
396
415
  const declaredContext = normalizeBootstrapContext(bootstrapContext);
397
416
  const discoveredCandidatePaths = activationDiscovery.evidence.reduce((acc, item) => {
398
417
  const relativePath = item.relativePath;
@@ -414,7 +433,7 @@ _prolog, args) {
414
433
  markdownFiles: [],
415
434
  evidence: candidateDiscovery.evidence.filter((item) => item.kind !== "generic_markdown"),
416
435
  };
417
- let sourceOnlySignals = collectSourceOnlyAuthoringSignals(guidanceDiscovery, {
436
+ let sourceOnlySignals = autopilotGenerateDeps.collectSourceOnlyAuthoringSignals(guidanceDiscovery, {
418
437
  ids: existingIds,
419
438
  workspaceRoot,
420
439
  }, normalizedMinConfidence);
@@ -435,28 +454,31 @@ _prolog, args) {
435
454
  return `${entityType}::${String(title).trim().toLowerCase().replace(/\s+/g, " ")}`;
436
455
  }
437
456
  if (activation.allowCandidateGeneration) {
438
- typedMarkdownCandidates = buildTypedMarkdownCandidates(candidateDiscovery, {
439
- ids: existingIds,
440
- workspaceRoot,
441
- });
442
- manifestCandidates = buildSymbolManifestCandidates(candidateDiscovery, {
457
+ typedMarkdownCandidates =
458
+ autopilotGenerateDeps.buildTypedMarkdownCandidates(candidateDiscovery, {
459
+ ids: existingIds,
460
+ workspaceRoot,
461
+ });
462
+ manifestCandidates = autopilotGenerateDeps.buildSymbolManifestCandidates(candidateDiscovery, {
443
463
  ids: existingIds,
444
464
  workspaceRoot,
445
465
  });
446
466
  if (includeGenericMarkdown) {
447
- genericCandidates = buildGenericMarkdownCandidates(candidateDiscovery, {
467
+ genericCandidates = autopilotGenerateDeps.buildGenericMarkdownCandidates(candidateDiscovery, {
448
468
  ids: existingIds,
449
469
  workspaceRoot,
450
470
  }, normalizedMinConfidence);
451
- normativeRequirementCandidates = buildNormativeRequirementCandidates(candidateDiscovery, {
471
+ normativeRequirementCandidates =
472
+ autopilotGenerateDeps.buildNormativeRequirementCandidates(candidateDiscovery, {
473
+ ids: existingIds,
474
+ workspaceRoot,
475
+ }, normalizedMinConfidence);
476
+ }
477
+ providerEvidenceCandidates =
478
+ autopilotGenerateDeps.buildProviderEvidenceCandidates(candidateDiscovery, {
452
479
  ids: existingIds,
453
480
  workspaceRoot,
454
481
  }, normalizedMinConfidence);
455
- }
456
- providerEvidenceCandidates = buildProviderEvidenceCandidates(candidateDiscovery, {
457
- ids: existingIds,
458
- workspaceRoot,
459
- }, normalizedMinConfidence);
460
482
  allCandidates = [
461
483
  ...typedMarkdownCandidates,
462
484
  ...manifestCandidates,
@@ -308,6 +308,15 @@ export async function handleKbModelRequirement(_prolog, args) {
308
308
  });
309
309
  const applyPlan = strictWriteSetToApplyPlan(writeSet);
310
310
  const migrationWarning = await getWorkspaceMigrationWarning();
311
+ const warnings = writeSet.isStrict
312
+ ? []
313
+ : [
314
+ {
315
+ kind: "low_confidence_observation_downgrade",
316
+ message: `Claim confidence ${writeSet.confidence.toFixed(2)} is below the strict threshold 0.70, so Kibi emitted an observation fact instead of strict subject/property facts.`,
317
+ nextAction: "If this is normative, provide subjectKey, propertyKey, operator, and value explicitly, then apply the returned strict write-set sequentially.",
318
+ },
319
+ ];
311
320
  const strictSummary = writeSet.isStrict
312
321
  ? `Modeled strict requirement into ${applyPlan.length} sequential applyPlan step(s).`
313
322
  : "Modeled a non-blocking observation review artifact; deterministic claim extraction stayed below the strict threshold.";
@@ -322,6 +331,7 @@ export async function handleKbModelRequirement(_prolog, args) {
322
331
  confidence: writeSet.confidence,
323
332
  extractionMode: extracted.extractionMode,
324
333
  extractionWarnings: extracted.extractionWarnings,
334
+ warnings,
325
335
  migrationWarning,
326
336
  };
327
337
  return {
@@ -0,0 +1,96 @@
1
+ import { runJsonModuleQuery, toPrologAtom } from "./core-module.js";
2
+ // implements REQ-002, REQ-013
3
+ export async function handleSparql(prolog, args) {
4
+ validateSparqlArgs(args);
5
+ try {
6
+ const payload = await runJsonModuleQuery(prolog, "sparql_client.pl", `kibi_sparql_client:remote_sparql_select_json(${toPrologAtom(args.endpoint)}, ${toPrologAtom(args.query)}, ${toSparqlOptions(args)}, JsonString)`, "SPARQL remote query");
7
+ const rows = payload?.rows ?? [];
8
+ return {
9
+ content: [
10
+ {
11
+ type: "text",
12
+ text: `Remote SPARQL query returned ${rows.length} row${rows.length === 1 ? "" : "s"}.`,
13
+ },
14
+ ],
15
+ ...(payload !== undefined ? { structuredContent: payload } : {}),
16
+ };
17
+ }
18
+ catch (error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ throw new Error(`SPARQL remote query failed: ${message}`);
21
+ }
22
+ }
23
+ function validateSparqlArgs(args) {
24
+ if (!args.endpoint || args.endpoint.trim().length === 0) {
25
+ throw new Error("SPARQL endpoint is required");
26
+ }
27
+ if (!args.query || args.query.trim().length === 0) {
28
+ throw new Error("SPARQL query is required");
29
+ }
30
+ if (!isRemoteHttpEndpoint(args.endpoint)) {
31
+ throw new Error("SPARQL endpoint must be an http:// or https:// URL");
32
+ }
33
+ if (!isSelectQuery(args.query)) {
34
+ throw new Error("SPARQL query must be a SELECT query");
35
+ }
36
+ if (!isPublicRemoteEndpoint(args.endpoint)) {
37
+ throw new Error("SPARQL endpoint must target a public remote host");
38
+ }
39
+ if (args.timeoutMs !== undefined &&
40
+ (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) {
41
+ throw new Error("SPARQL timeoutMs must be a positive number when provided");
42
+ }
43
+ }
44
+ function isRemoteHttpEndpoint(endpoint) {
45
+ try {
46
+ const url = new URL(endpoint);
47
+ return url.protocol === "http:" || url.protocol === "https:";
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ function isSelectQuery(query) {
54
+ return /^\s*select\b/i.test(query);
55
+ }
56
+ function isPublicRemoteEndpoint(endpoint) {
57
+ const url = new URL(endpoint);
58
+ const host = normalizeHostname(url.hostname);
59
+ return !isLocalOrPrivateHost(host);
60
+ }
61
+ function normalizeHostname(hostname) {
62
+ return hostname
63
+ .toLowerCase()
64
+ .replace(/^\[/, "")
65
+ .replace(/\]$/, "")
66
+ .replace(/\.$/, "");
67
+ }
68
+ function isLocalOrPrivateHost(host) {
69
+ if (host === "localhost" ||
70
+ host.endsWith(".localhost") ||
71
+ host === "0.0.0.0" ||
72
+ host === "::1" ||
73
+ (host.includes(":") &&
74
+ (host.startsWith("fe80:") ||
75
+ host.startsWith("fc") ||
76
+ host.startsWith("fd")))) {
77
+ return true;
78
+ }
79
+ const octets = host.split(".").map((part) => Number(part));
80
+ if (octets.length !== 4 || octets.some((octet) => !Number.isInteger(octet))) {
81
+ return false;
82
+ }
83
+ const [first = -1, second = -1] = octets;
84
+ return (first === 10 ||
85
+ first === 127 ||
86
+ (first === 169 && second === 254) ||
87
+ (first === 172 && second >= 16 && second <= 31) ||
88
+ (first === 192 && second === 168));
89
+ }
90
+ function toSparqlOptions(args) {
91
+ if (args.timeoutMs === undefined) {
92
+ return "[]";
93
+ }
94
+ const timeoutSeconds = Math.max(1, Math.ceil(args.timeoutMs / 1000));
95
+ return `[timeout(${timeoutSeconds})]`;
96
+ }