kibi-mcp 0.1.5 → 0.2.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.
package/dist/server.js CHANGED
@@ -15,6 +15,8 @@
15
15
  You should have received a copy of the GNU Affero General Public License
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
  */
18
+ import fs from "node:fs";
19
+ import { createRequire } from "node:module";
18
20
  /*
19
21
  How to apply this header to source files (examples)
20
22
 
@@ -46,23 +48,15 @@ import process from "node:process";
46
48
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
47
49
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
48
50
  import { PrologProcess } from "kibi-cli/prolog";
51
+ import { copyCleanSnapshot, getBranchDiagnostic, isValidBranchName, resolveActiveBranch, } from "kibi-cli/public/branch-resolver";
49
52
  import { z } from "zod";
50
53
  import { loadDefaultEnvFile } from "./env.js";
51
54
  import { attachMcpcat } from "./mcpcat.js";
52
55
  import { TOOLS } from "./tools-config.js";
53
- import { handleKbBranchEnsure, handleKbBranchGc, } from "./tools/branch.js";
54
56
  import { handleKbCheck } from "./tools/check.js";
55
- import { handleKbContext } from "./tools/context.js";
56
- import { handleKbCoverageReport, } from "./tools/coverage-report.js";
57
57
  import { handleKbDelete } from "./tools/delete.js";
58
- import { handleKbDerive } from "./tools/derive.js";
59
- import { handleKbImpact } from "./tools/impact.js";
60
- import { handleKbListEntityTypes, handleKbListRelationshipTypes, } from "./tools/list-types.js";
61
- import { handleKbQueryRelationships, } from "./tools/query-relationships.js";
62
58
  import { handleKbQuery } from "./tools/query.js";
63
59
  import { handleKbUpsert } from "./tools/upsert.js";
64
- import { handleKbSymbolsRefresh, } from "./tools/symbols.js";
65
- import { handleSuggestSharedFacts, } from "./tools/suggest-shared-facts.js";
66
60
  import { resolveKbPath, resolveWorkspaceRoot } from "./workspace.js";
67
61
  function renderToolsDoc() {
68
62
  const lines = [
@@ -92,9 +86,9 @@ const PROMPTS = [
92
86
  "",
93
87
  "- Encode requirements as linked facts: `req --constrains--> fact` plus `req --requires_property--> fact`.",
94
88
  "- Reuse canonical fact IDs across requirements; shared constrained facts make contradictions detectable.",
95
- "- Use read tools first (`kb_query`, `kb_query_relationships`, `kbcontext`) to establish context.",
96
- "- Use mutation tools (`kb_upsert`, `kb_delete`, branch tools) only after you can justify the change.",
97
- "- Use inference tools (`kb_derive`, `kb_impact`, `kb_coverage_report`) for deterministic analysis.",
89
+ "- Use `kb_query` first to confirm current state before any mutation.",
90
+ "- Use `kb_upsert` and `kb_delete` only for intentional, traceable KB changes.",
91
+ "- Run `kb_check` after meaningful mutations to catch integrity issues early.",
98
92
  "- Prefer explicit IDs and enum values to avoid invalid parameters.",
99
93
  "- Assume every write can affect downstream traceability queries.",
100
94
  ].join("\n"),
@@ -107,15 +101,13 @@ const PROMPTS = [
107
101
  "",
108
102
  "Follow this sequence for reliable operation:",
109
103
  "",
110
- "1. **Discover**: Call `kb_list_entity_types`/`kb_list_relationship_types` if you are unsure about allowed values.",
111
- "2. **Inspect**: Call `kb_query` or `kbcontext` to confirm current state before any mutation.",
112
- "3. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property`.",
113
- "4. **Validate intent**: If creating links, call `kb_query` for both endpoint IDs first.",
114
- "5. **Mutate**: Call `kb_upsert` for create/update, or `kb_delete` for explicit removals.",
115
- "6. **Verify integrity**: Call `kb_check` after mutations.",
116
- "7. **Assess impact**: Call `kb_impact`, `kb_derive`, or `kb_coverage_report` as needed.",
104
+ "1. **Inspect**: Call `kb_query` to confirm current state before any mutation.",
105
+ "2. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property`.",
106
+ "3. **Validate intent**: If creating links, call `kb_query` for both endpoint IDs first.",
107
+ "4. **Mutate**: Call `kb_upsert` for create/update, or `kb_delete` for explicit removals.",
108
+ "5. **Verify integrity**: Call `kb_check` after mutations.",
117
109
  "",
118
- "If a tool returns empty results, do not assume failure. Re-check filters (type, id, tags, sourceFile, or relationship type).",
110
+ "If a tool returns empty results, do not assume failure. Re-check filters (type, id, tags, sourceFile, limit, or offset).",
119
111
  ].join("\n"),
120
112
  },
121
113
  {
@@ -128,10 +120,8 @@ const PROMPTS = [
128
120
  "",
129
121
  "- `kb_upsert` validates entity and relationship payloads against JSON Schema.",
130
122
  "- `kb_delete` blocks deletion when dependents still reference the entity.",
131
- "- `kb_branch_gc` may permanently remove stale branch KB directories when `dry_run` is `false`.",
132
123
  "- Relationship and rule names are strict enums; unknown values fail validation.",
133
- "- Branch names are sanitized; path traversal patterns are rejected.",
134
- "- `kb_symbols_refresh` can rewrite the symbols manifest unless `dryRun` is enabled.",
124
+ "- Branch KB setup is automatic at server startup; lifecycle maintenance stays outside the public MCP tool surface.",
135
125
  ].join("\n"),
136
126
  },
137
127
  ];
@@ -143,9 +133,8 @@ function registerDocResources() {
143
133
  "",
144
134
  "Scope:",
145
135
  "- Entity CRUD-like operations for KB records",
146
- "- Relationship inspection",
147
- "- Validation and branch KB maintenance",
148
- "- Deterministic inference for traceability and impact analysis",
136
+ "- Validation of KB integrity after changes",
137
+ "- Automatic branch-local attachment for the active workspace",
149
138
  "",
150
139
  "Use this server when you need branch-local, machine-readable project memory.",
151
140
  ].join("\n");
@@ -156,7 +145,7 @@ function registerDocResources() {
156
145
  "",
157
146
  "- `-32602 INVALID_PARAMS`: Tool arguments are missing/invalid. Recover by checking enum values and required fields.",
158
147
  "- `-32601 METHOD_NOT_FOUND`: Unknown MCP method. Recover by using supported methods (`tools/*`, `prompts/*`, `resources/*`).",
159
- "- `-32000 PROLOG_QUERY_FAILED`: Prolog query failed. Recover by validating IDs, rule names, and relationship types.",
148
+ "- `-32000 PROLOG_QUERY_FAILED`: Prolog query failed. Recover by validating IDs, rule names, and branch KB availability.",
160
149
  "- `VALIDATION_ERROR` message: `kb_upsert` payload failed schema checks. Recover by fixing required fields and enum values.",
161
150
  "- Delete blocked by dependents: `kb_delete` detected incoming references. Recover by removing/rewiring relationships first.",
162
151
  "- Empty results: filters may be too strict. Recover by loosening type/id/tags/source filters and retrying.",
@@ -170,20 +159,10 @@ function registerDocResources() {
170
159
  "3. Reuse the same constrained fact ID across related requirements; vary property facts only when semantics differ",
171
160
  '4. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }`',
172
161
  "",
173
- "## Discover requirement coverage gaps",
174
- '1. `kb_query` with `{ "type": "req", "limit": 20 }`',
175
- '2. `kb_coverage_report` with `{ "type": "req" }`',
176
- '3. `kb_derive` with `{ "rule": "coverage_gap" }`',
177
- "",
178
162
  "## Add a requirement and link it to a test",
179
163
  "1. `kb_query` for existing IDs to avoid collisions",
180
164
  "2. `kb_upsert` with entity payload and `relationships` containing `verified_by`",
181
165
  '3. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }`',
182
- "",
183
- "## Safe cleanup of stale branch KBs",
184
- '1. `kb_branch_gc` with `{ "dry_run": true }`',
185
- "2. Review `structuredContent.stale`",
186
- '3. `kb_branch_gc` with `{ "dry_run": false }` only when deletion is intended',
187
166
  ].join("\n");
188
167
  return [
189
168
  {
@@ -264,6 +243,22 @@ let activeBranchName = "develop";
264
243
  let isShuttingDown = false;
265
244
  let shutdownTimeout = null;
266
245
  const inFlightRequests = new Map();
246
+ function ensureBranchKbExists(workspaceRoot, branch) {
247
+ if (!isValidBranchName(branch)) {
248
+ throw new Error(`Invalid branch name: ${branch}`);
249
+ }
250
+ const branchPath = resolveKbPath(workspaceRoot, branch);
251
+ if (fs.existsSync(branchPath)) {
252
+ return;
253
+ }
254
+ const templateBranch = ["develop", "main"].find((candidate) => candidate !== branch &&
255
+ fs.existsSync(resolveKbPath(workspaceRoot, candidate)));
256
+ if (!templateBranch) {
257
+ throw new Error(`No template branch KB found for '${branch}'. Expected '.kb/branches/develop' or '.kb/branches/main'.`);
258
+ }
259
+ // Use clean snapshot copy that excludes volatile artifacts
260
+ copyCleanSnapshot(resolveKbPath(workspaceRoot, templateBranch), branchPath);
261
+ }
267
262
  function debugLog(...args) {
268
263
  if (process.env.KIBI_MCP_DEBUG) {
269
264
  console.error(...args);
@@ -315,39 +310,97 @@ async function initiateGracefulShutdown(exitCode = 0) {
315
310
  process.exit(exitCode);
316
311
  }
317
312
  async function ensureProlog() {
313
+ const workspaceRoot = resolveWorkspaceRoot();
314
+ // Determine target branch: respect KIBI_BRANCH override or resolve from git
315
+ const envBranch = process.env.KIBI_BRANCH?.trim();
316
+ let targetBranch;
317
+ if (envBranch) {
318
+ // KIBI_BRANCH override is set - use it without re-resolving from git
319
+ if (!isValidBranchName(envBranch)) {
320
+ throw new Error(`Invalid branch name from KIBI_BRANCH: '${envBranch}'`);
321
+ }
322
+ targetBranch = envBranch;
323
+ }
324
+ else {
325
+ // No override - resolve active branch from git (may change between requests)
326
+ const branchResult = resolveActiveBranch(workspaceRoot);
327
+ if ("error" in branchResult) {
328
+ const diagnostic = getBranchDiagnostic(undefined, branchResult.error);
329
+ console.error(`[KIBI-MCP] ${diagnostic}`);
330
+ throw new Error(`Failed to resolve active branch: ${branchResult.error}`);
331
+ }
332
+ targetBranch = branchResult.branch;
333
+ }
334
+ // Check if we need to switch branches
318
335
  if (isInitialized && prologProcess?.isRunning()) {
336
+ if (targetBranch === activeBranchName) {
337
+ // Still on the same branch - return existing connection
338
+ return prologProcess;
339
+ }
340
+ // Branch changed - need to detach and re-attach
341
+ debugLog(`[KIBI-MCP] Branch changed: ${activeBranchName} -> ${targetBranch}`);
342
+ // Detach from old KB
343
+ const detachResult = await prologProcess.query("kb_detach");
344
+ if (!detachResult.success) {
345
+ debugLog(`[KIBI-MCP] Warning: failed to detach from old KB: ${detachResult.error || "Unknown error"}`);
346
+ // Continue anyway - we'll try to attach to the new KB
347
+ }
348
+ // Ensure new branch KB exists
349
+ ensureBranchKbExists(workspaceRoot, targetBranch);
350
+ const newKbPath = resolveKbPath(workspaceRoot, targetBranch);
351
+ // Attach to new branch KB
352
+ const attachResult = await prologProcess.query(`kb_attach('${newKbPath}')`);
353
+ if (!attachResult.success) {
354
+ throw new Error(`Failed to attach to new branch KB: ${attachResult.error || "Unknown error"}`);
355
+ }
356
+ activeBranchName = targetBranch;
357
+ debugLog(`[KIBI-MCP] Re-attached to branch: ${targetBranch}`);
358
+ debugLog(`[KIBI-MCP] KB path: ${newKbPath}`);
319
359
  return prologProcess;
320
360
  }
361
+ // First initialization
321
362
  debugLog("[KIBI-MCP] Initializing Prolog process...");
322
- prologProcess = new PrologProcess({ timeout: 30000 });
363
+ prologProcess = new PrologProcess({ timeout: 120000 });
323
364
  await prologProcess.start();
324
- const workspaceRoot = resolveWorkspaceRoot();
325
- let branch = process.env.KIBI_BRANCH || "develop";
326
- let gitBranch;
327
- if (!process.env.KIBI_BRANCH) {
365
+ // Startup debug: resolve which kibi-cli is being used and its version (best-effort).
366
+ // Gate all output under KIBI_MCP_DEBUG and write only to stderr via debugLog.
367
+ if (process.env.KIBI_MCP_DEBUG) {
328
368
  try {
329
- const { execSync } = await import("node:child_process");
330
- const detected = execSync("git branch --show-current", {
331
- cwd: workspaceRoot,
332
- encoding: "utf8",
333
- timeout: 3000,
334
- }).trim();
335
- if (detected) {
336
- gitBranch = detected === "master" ? "develop" : detected;
337
- branch = gitBranch;
369
+ const req = createRequire(import.meta.url);
370
+ try {
371
+ const resolved = req.resolve("kibi-cli/prolog");
372
+ debugLog(`[KIBI-MCP] require.resolve('kibi-cli/prolog') -> ${resolved}`);
373
+ }
374
+ catch (resolveErr) {
375
+ debugLog("[KIBI-MCP] require.resolve('kibi-cli/prolog') failed:", resolveErr.message);
376
+ }
377
+ // Try to read package.json for kibi-cli to get version. This may fail if
378
+ // the package uses exports blocking package.json access — log explicit failure.
379
+ try {
380
+ // prefer direct package.json require; createRequire makes this ESM-friendly
381
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
382
+ const pkg = req("kibi-cli/package.json");
383
+ if (pkg && typeof pkg.version === "string") {
384
+ debugLog(`[KIBI-MCP] kibi-cli version: ${pkg.version}`);
385
+ }
386
+ else {
387
+ debugLog("[KIBI-MCP] kibi-cli package.json read but no version field");
388
+ }
389
+ }
390
+ catch (pkgErr) {
391
+ debugLog("[KIBI-MCP] Failed to read kibi-cli package.json (exports may restrict access):", pkgErr.message);
338
392
  }
339
393
  }
340
- catch {
341
- // fall back to develop
394
+ catch (err) {
395
+ debugLog("[KIBI-MCP] Failed to create require() for debug lookup:", err.message);
342
396
  }
343
397
  }
344
398
  debugLog("[KIBI-MCP] Branch selection:");
345
399
  debugLog(`[KIBI-MCP] KIBI_BRANCH env: ${process.env.KIBI_BRANCH || "not set"}`);
346
- debugLog(`[KIBI-MCP] Git branch: ${gitBranch || "n/a"}`);
347
- debugLog(`[KIBI-MCP] Attached to: ${branch}`);
348
- debugLog("[KIBI-MCP] To change branch: set KIBI_BRANCH=<branch> and restart");
349
- activeBranchName = branch;
350
- const kbPath = resolveKbPath(workspaceRoot, branch);
400
+ debugLog(`[KIBI-MCP] Resolved branch: ${targetBranch}`);
401
+ activeBranchName = targetBranch;
402
+ ensureBranchKbExists(workspaceRoot, targetBranch);
403
+ const kbPath = resolveKbPath(workspaceRoot, targetBranch);
351
404
  const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
352
405
  if (!attachResult.success) {
353
406
  throw new Error(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
@@ -503,7 +556,7 @@ function addTool(server, name, description, inputSchema, handler) {
503
556
  }
504
557
  export async function startServer() {
505
558
  loadDefaultEnvFile();
506
- const server = new McpServer({ name: "kibi-mcp", version: "0.1.0" });
559
+ const server = new McpServer({ name: "kibi-mcp", version: "0.2.0" });
507
560
  attachMcpcat(server);
508
561
  for (const prompt of PROMPTS) {
509
562
  server.prompt(prompt.name, prompt.description, async () => ({
@@ -548,49 +601,6 @@ export async function startServer() {
548
601
  const prolog = await ensureProlog();
549
602
  return handleKbCheck(prolog, args);
550
603
  });
551
- addTool(server, "kb_branch_ensure", toolDef("kb_branch_ensure").description, toolDef("kb_branch_ensure").inputSchema, async (args) => {
552
- const prolog = await ensureProlog();
553
- return handleKbBranchEnsure(prolog, args);
554
- });
555
- addTool(server, "kb_branch_gc", toolDef("kb_branch_gc").description, toolDef("kb_branch_gc").inputSchema, async (args) => {
556
- const prolog = await ensureProlog();
557
- return handleKbBranchGc(prolog, args);
558
- });
559
- addTool(server, "kb_query_relationships", toolDef("kb_query_relationships").description, toolDef("kb_query_relationships").inputSchema, async (args) => {
560
- const prolog = await ensureProlog();
561
- return handleKbQueryRelationships(prolog, args);
562
- });
563
- addTool(server, "kb_derive", toolDef("kb_derive").description, toolDef("kb_derive").inputSchema, async (args) => {
564
- const prolog = await ensureProlog();
565
- return handleKbDerive(prolog, args);
566
- });
567
- addTool(server, "kb_impact", toolDef("kb_impact").description, toolDef("kb_impact").inputSchema, async (args) => {
568
- const prolog = await ensureProlog();
569
- return handleKbImpact(prolog, args);
570
- });
571
- addTool(server, "kb_coverage_report", toolDef("kb_coverage_report").description, toolDef("kb_coverage_report").inputSchema, async (args) => {
572
- const prolog = await ensureProlog();
573
- return handleKbCoverageReport(prolog, args);
574
- });
575
- addTool(server, "kb_symbols_refresh", toolDef("kb_symbols_refresh").description, toolDef("kb_symbols_refresh").inputSchema, async (args) => handleKbSymbolsRefresh(args));
576
- addTool(server, "kb_list_entity_types", toolDef("kb_list_entity_types").description, toolDef("kb_list_entity_types").inputSchema, handleKbListEntityTypes);
577
- addTool(server, "kb_list_relationship_types", toolDef("kb_list_relationship_types").description, toolDef("kb_list_relationship_types").inputSchema, handleKbListRelationshipTypes);
578
- addTool(server, "kbcontext", toolDef("kbcontext").description, toolDef("kbcontext").inputSchema, async (args) => {
579
- const prolog = await ensureProlog();
580
- return handleKbContext(prolog, args, activeBranchName);
581
- });
582
- addTool(server, "get_help", toolDef("get_help").description, toolDef("get_help").inputSchema, async (args) => {
583
- const topic = typeof args?.topic === "string" ? args.topic : undefined;
584
- const text = getHelpText(topic);
585
- return {
586
- content: [{ type: "text", text }],
587
- structuredContent: { topic: topic ?? "overview" },
588
- };
589
- });
590
- addTool(server, "analyze_shared_facts", toolDef("analyze_shared_facts").description, toolDef("analyze_shared_facts").inputSchema, async (args) => {
591
- const prolog = await ensureProlog();
592
- return handleSuggestSharedFacts(prolog, args);
593
- });
594
604
  const transport = new StdioServerTransport();
595
605
  transport.onerror = (error) => {
596
606
  // Stdio transport surfaces JSON parse / schema validation failures via onerror.
@@ -44,6 +44,15 @@
44
44
  */
45
45
  import * as path from "node:path";
46
46
  import { parsePairList } from "./prolog-list.js";
47
+ function formatDiagnosticsForMcp(diagnostics) {
48
+ return diagnostics.map((d) => ({
49
+ category: d.category,
50
+ severity: d.severity,
51
+ message: d.message,
52
+ file: d.file,
53
+ suggestion: d.suggestion,
54
+ }));
55
+ }
47
56
  /**
48
57
  * Handle kb_check tool calls - run validation rules on the KB
49
58
  * Reuses validation logic from CLI check command
@@ -77,7 +86,13 @@ export async function handleKbCheck(prolog, args) {
77
86
  if (rulesToRun.includes("symbol-coverage")) {
78
87
  violations.push(...(await checkSymbolCoverage(prolog)));
79
88
  }
80
- // Return MCP structured response
89
+ const diagnostics = violations.map((v) => ({
90
+ category: "SYNC_ERROR",
91
+ severity: "error",
92
+ message: v.description,
93
+ file: v.source,
94
+ suggestion: v.suggestion,
95
+ }));
81
96
  const summary = violations.length === 0
82
97
  ? "No violations found"
83
98
  : `${violations.length} violations found`;
@@ -91,6 +106,7 @@ export async function handleKbCheck(prolog, args) {
91
106
  structuredContent: {
92
107
  violations,
93
108
  count: violations.length,
109
+ diagnostics: formatDiagnosticsForMcp(diagnostics),
94
110
  },
95
111
  };
96
112
  }
@@ -15,18 +15,8 @@
15
15
  You should have received a copy of the GNU Affero General Public License
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
  */
18
- export async function handleKbContext(prolog, args, activeBranch) {
19
- const { sourceFile, branch } = args;
20
- if (branch && activeBranch && branch !== activeBranch) {
21
- return {
22
- content: [
23
- {
24
- type: "text",
25
- text: `Error: branch parameter is not supported server-side; set KIBI_BRANCH at startup or restart server on the desired branch. (Requested: ${branch}, Active: ${activeBranch})`,
26
- },
27
- ],
28
- };
29
- }
18
+ export async function handleKbContext(prolog, args) {
19
+ const { sourceFile } = args;
30
20
  try {
31
21
  const safeSource = sourceFile.replace(/'/g, "\\'");
32
22
  const entityGoal = `findall([Id,Type,Props], (kb_entities_by_source('${safeSource}', SourceIds), member(Id, SourceIds), kb_entity(Id, Type, Props)), Results)`;
@@ -15,6 +15,9 @@
15
15
  You should have received a copy of the GNU Affero General Public License
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
  */
18
+ function escapeAtom(value) {
19
+ return value.replace(/'/g, "\\'");
20
+ }
18
21
  /**
19
22
  * Handle kb.delete tool calls
20
23
  * Prevents deletion of entities with dependents (referential integrity)
@@ -30,7 +33,7 @@ export async function handleKbDelete(prolog, args) {
30
33
  try {
31
34
  for (const id of ids) {
32
35
  // Check if entity exists
33
- const checkGoal = `kb_entity('${id}', _, _)`;
36
+ const checkGoal = `once(kb_entity('${escapeAtom(id)}', _, _))`;
34
37
  const checkResult = await prolog.query(checkGoal);
35
38
  if (!checkResult.success) {
36
39
  errors.push(`Entity ${id} does not exist`);
@@ -15,10 +15,13 @@
15
15
  You should have received a copy of the GNU Affero General Public License
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
  */
18
+ import Ajv from "ajv";
18
19
  import entitySchema from "kibi-cli/schemas/entity";
19
20
  import relationshipSchema from "kibi-cli/schemas/relationship";
20
- import Ajv from "ajv";
21
21
  import { refreshCoordinatesForSymbolId } from "./symbols.js";
22
+ function escapeAtom(value) {
23
+ return value.replace(/'/g, "\\'");
24
+ }
22
25
  const ajv = new Ajv({ strict: false });
23
26
  const validateEntity = ajv.compile(entitySchema);
24
27
  const validateRelationship = ajv.compile(relationshipSchema);
@@ -80,16 +83,14 @@ export async function handleKbUpsert(prolog, args) {
80
83
  const id = entity.id;
81
84
  const type = entity.type;
82
85
  // Check if entity exists
83
- const checkGoal = `kb_entity('${id}', _, _)`;
86
+ const checkGoal = `once(kb_entity('${escapeAtom(id)}', _, _))`;
84
87
  const checkResult = await prolog.query(checkGoal);
85
88
  const isUpdate = checkResult.success;
86
89
  // Build property list for Prolog
87
90
  const props = buildPropertyList(entity);
88
91
  // Assert entity (upsert)
89
92
  if (isUpdate) {
90
- // Delete old version, then insert new
91
- const retractGoal = `kb_retract_entity('${id}')`;
92
- await prolog.query(retractGoal);
93
+ // Update counter only. kb_assert_entity implements upsert semantics in Prolog.
93
94
  updated++;
94
95
  }
95
96
  else {
@@ -108,7 +109,7 @@ export async function handleKbUpsert(prolog, args) {
108
109
  const to = rel.to;
109
110
  // Build metadata
110
111
  const metadata = buildRelationshipMetadata(rel);
111
- const relGoal = `kb_assert_relationship(${relType}, '${from}', '${to}', ${metadata})`;
112
+ const relGoal = `kb_assert_relationship(${relType}, '${escapeAtom(from)}', '${escapeAtom(to)}', ${metadata})`;
112
113
  const relResult = await prolog.query(relGoal);
113
114
  if (!relResult.success) {
114
115
  throw new Error(`Failed to assert relationship ${relType} from ${from} to ${to}: ${relResult.error || "Unknown error"}`);
@@ -234,215 +234,19 @@ export const TOOLS = [
234
234
  properties: {
235
235
  rules: {
236
236
  type: "array",
237
- items: { type: "string" },
237
+ items: {
238
+ type: "string",
239
+ enum: [
240
+ "must-priority-coverage",
241
+ "no-dangling-refs",
242
+ "no-cycles",
243
+ "required-fields",
244
+ "symbol-coverage",
245
+ ],
246
+ },
238
247
  description: "Optional rule subset. Allowed: must-priority-coverage, no-dangling-refs, no-cycles, required-fields, symbol-coverage. If omitted, server runs all.",
239
248
  },
240
249
  },
241
250
  },
242
251
  },
243
- {
244
- name: "kb_branch_ensure",
245
- description: "Ensure a branch KB exists, creating it from develop when missing. Use when targeting non-develop branches. Do not use to switch git branches. Side effects: creates .kb/branches/<branch>.",
246
- inputSchema: {
247
- type: "object",
248
- required: ["branch"],
249
- properties: {
250
- branch: {
251
- type: "string",
252
- description: "Required git branch name. Example: 'feature/auth-hardening'. Path traversal patterns are rejected.",
253
- },
254
- },
255
- },
256
- },
257
- {
258
- name: "kb_branch_gc",
259
- description: "Find or delete stale branch KB directories not present in git. Use for repository hygiene. Do not use if you need historical branch KBs. Side effects: can delete branch KB folders when dry_run is false.",
260
- inputSchema: {
261
- type: "object",
262
- properties: {
263
- dry_run: {
264
- type: "boolean",
265
- default: true,
266
- description: "Optional safety flag. true = report only; false = delete stale branch KBs. Default: true.",
267
- },
268
- },
269
- },
270
- },
271
- {
272
- name: "kb_query_relationships",
273
- description: "Read relationship edges with optional from/to/type filters. Use for traceability traversal. Do not use to create links. No mutation side effects.",
274
- inputSchema: {
275
- type: "object",
276
- properties: {
277
- from: {
278
- type: "string",
279
- description: "Optional source entity ID filter. Example: 'REQ-001'.",
280
- },
281
- to: {
282
- type: "string",
283
- description: "Optional target entity ID filter. Example: 'TEST-010'.",
284
- },
285
- type: {
286
- type: "string",
287
- enum: [
288
- "depends_on",
289
- "specified_by",
290
- "verified_by",
291
- "validates",
292
- "implements",
293
- "covered_by",
294
- "constrained_by",
295
- "constrains",
296
- "requires_property",
297
- "guards",
298
- "publishes",
299
- "consumes",
300
- "supersedes",
301
- "relates_to",
302
- ],
303
- description: "Optional relationship type filter. Allowed enum values only. Example: 'implements'.",
304
- },
305
- },
306
- },
307
- },
308
- {
309
- name: "kb_derive",
310
- description: "Run deterministic inference predicates and return rows. Use for impact, coverage, and consistency analysis. Do not use for entity CRUD. No mutation side effects.",
311
- inputSchema: {
312
- type: "object",
313
- required: ["rule"],
314
- properties: {
315
- rule: {
316
- type: "string",
317
- enum: [
318
- "transitively_implements",
319
- "transitively_depends",
320
- "impacted_by_change",
321
- "affected_symbols",
322
- "coverage_gap",
323
- "untested_symbols",
324
- "stale",
325
- "orphaned",
326
- "conflicting",
327
- "deprecated_still_used",
328
- "current_adr",
329
- "adr_chain",
330
- "superseded_by",
331
- "domain_contradictions",
332
- ],
333
- description: "Required inference rule name. Allowed values are the enum options. Example: 'coverage_gap'.",
334
- },
335
- params: {
336
- type: "object",
337
- description: "Optional rule-specific parameters. Example: { changed: 'REQ-001' } for impacted_by_change.",
338
- },
339
- },
340
- },
341
- },
342
- {
343
- name: "kb_impact",
344
- description: "Return entities impacted by a changed entity ID. Use for quick change blast radius checks. Do not use for general querying. No mutation side effects.",
345
- inputSchema: {
346
- type: "object",
347
- required: ["entity"],
348
- properties: {
349
- entity: {
350
- type: "string",
351
- description: "Required changed entity ID. Example: 'REQ-001'.",
352
- },
353
- },
354
- },
355
- },
356
- {
357
- name: "kb_coverage_report",
358
- description: "Compute aggregate traceability coverage for requirements and/or symbols. Use for health snapshots. Do not use for raw entity dumps. No mutation side effects.",
359
- inputSchema: {
360
- type: "object",
361
- properties: {
362
- type: {
363
- type: "string",
364
- enum: ["req", "symbol"],
365
- description: "Optional focus scope: 'req' or 'symbol'. Omit to include both.",
366
- },
367
- },
368
- },
369
- },
370
- {
371
- name: "kb_symbols_refresh",
372
- description: "Refresh generated symbol coordinates in the symbols manifest. Use after refactors that move symbols. Do not use for semantic edits. Side effects: may rewrite symbols.yaml unless dryRun is true.",
373
- inputSchema: {
374
- type: "object",
375
- properties: {
376
- dryRun: {
377
- type: "boolean",
378
- default: false,
379
- description: "Optional preview mode. true = report only, false = apply file updates. Default: false.",
380
- },
381
- },
382
- },
383
- },
384
- {
385
- name: "kb_list_entity_types",
386
- description: "List supported entity type names. Use when building valid tool arguments. Do not use for entity data retrieval. No mutation side effects.",
387
- inputSchema: { type: "object", properties: {} },
388
- },
389
- {
390
- name: "kb_list_relationship_types",
391
- description: "List supported relationship type names. Use before asserting or filtering relationships. Do not use for graph traversal. No mutation side effects.",
392
- inputSchema: { type: "object", properties: {} },
393
- },
394
- {
395
- name: "kbcontext",
396
- description: "Return KB entities linked to a source file plus first-hop relationships. Use for file-centric traceability. Do not use for cross-repo search. No mutation side effects.",
397
- inputSchema: {
398
- type: "object",
399
- required: ["sourceFile"],
400
- properties: {
401
- sourceFile: {
402
- type: "string",
403
- description: "Required source path substring. Example: 'src/auth/login.ts'.",
404
- },
405
- branch: {
406
- type: "string",
407
- description: "Optional branch hint for clients. Must match the server's active branch or will return an error.",
408
- },
409
- },
410
- },
411
- },
412
- {
413
- name: "get_help",
414
- description: "Returns documentation for this MCP server. Call this first if you are unsure how to proceed or which tool to use. Available topics: overview, tools, workflow, constraints, examples, errors.",
415
- inputSchema: {
416
- type: "object",
417
- properties: {
418
- topic: {
419
- type: "string",
420
- enum: [
421
- "overview",
422
- "tools",
423
- "workflow",
424
- "constraints",
425
- "examples",
426
- "errors",
427
- "branching",
428
- ],
429
- description: "Optional documentation section. Omit to return overview. Example: 'workflow'.",
430
- },
431
- },
432
- },
433
- },
434
- {
435
- name: "analyze_shared_facts",
436
- description: "Analyze requirements and suggest shared domain facts for extraction. LLMs call this to identify missed semantic opportunities before upserting. Lightweight heuristic: finds overlapping capitalized terms and repeated phrases across requirements.",
437
- inputSchema: {
438
- type: "object",
439
- properties: {
440
- min_frequency: {
441
- type: "number",
442
- default: 2,
443
- description: "Minimum frequency threshold for shared concepts. Default: 2. Example: 3 to only show concepts mentioned in 3+ requirements.",
444
- },
445
- },
446
- },
447
- },
448
252
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "dependencies": {
5
5
  "@modelcontextprotocol/sdk": "^1.26.0",
6
6
  "ajv": "^8.18.0",
@@ -9,7 +9,8 @@
9
9
  "fast-glob": "^3.2.12",
10
10
  "gray-matter": "^4.0.3",
11
11
  "js-yaml": "^4.1.0",
12
- "kibi-core": "^0.1.5",
12
+ "kibi-cli": "^0.2.0",
13
+ "kibi-core": "^0.1.6",
13
14
  "mcpcat": "^0.1.12",
14
15
  "ts-morph": "^23.0.0",
15
16
  "zod": "^4.3.6"
@@ -18,13 +19,13 @@
18
19
  "description": "Model Context Protocol server for Kibi knowledge base",
19
20
  "main": "./dist/server.js",
20
21
  "bin": {
21
- "kibi-mcp": "./bin/kibi-mcp"
22
+ "kibi-mcp": "bin/kibi-mcp"
22
23
  },
23
24
  "scripts": {
24
25
  "test": "bun test",
25
26
  "dev": "bun run bin/kibi-mcp",
26
27
  "build": "tsc -p tsconfig.json",
27
- "prepublishOnly": "npm run build"
28
+ "prepack": "npm run build"
28
29
  },
29
30
  "files": ["dist", "bin"],
30
31
  "engines": {
@@ -32,10 +33,8 @@
32
33
  "bun": ">=1.0"
33
34
  },
34
35
  "exports": {
35
- ".": "./dist/server.js",
36
- "./src/*": "./src/*.ts"
36
+ ".": "./dist/server.js"
37
37
  },
38
-
39
38
  "devDependencies": {
40
39
  "@types/bun": "latest",
41
40
  "@types/node": "latest"