kibi-mcp 0.2.3 → 0.3.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
@@ -14,857 +14,24 @@
14
14
 
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
- */
18
- import fs from "node:fs";
19
- import { createRequire } from "node:module";
20
- import path from "node:path";
21
- /*
22
- How to apply this header to source files (examples)
23
-
24
- 1) Prepend header to a single file (POSIX shells):
25
-
26
- cat LICENSE_HEADER.txt "$FILE" > "$FILE".with-header && mv "$FILE".with-header "$FILE"
27
-
28
- 2) Apply to multiple files (example: the project's main entry files):
29
-
30
- for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp packages/cli/src/*.ts packages/mcp/src/*.ts; do
31
- if [ -f "$f" ]; then
32
- cp "$f" "$f".bak
33
- (cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
34
- fi
35
- done
36
-
37
- 3) Avoid duplicating the header: run a quick guard to only add if missing
38
-
39
- for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp; do
40
- if [ -f "$f" ]; then
41
- if ! head -n 5 "$f" | grep -q "Copyright (C) 2026 Piotr Franczyk"; then
42
- cp "$f" "$f".bak
43
- (cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
44
- fi
45
- fi
46
- done
47
- */
48
- import process from "node:process";
17
+ */
49
18
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
50
19
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
51
- import { PrologProcess } from "kibi-cli/prolog";
52
- import { copyCleanSnapshot, getBranchDiagnostic, isValidBranchName, resolveActiveBranch, } from "kibi-cli/public/branch-resolver";
53
- import { z } from "zod";
54
20
  import { loadDefaultEnvFile } from "./env.js";
55
- import { TOOLS } from "./tools-config.js";
56
- import { handleKbCheck } from "./tools/check.js";
57
- import { handleKbDelete } from "./tools/delete.js";
58
- import { handleKbQuery } from "./tools/query.js";
59
- import { handleKbUpsert } from "./tools/upsert.js";
60
- import { resolveKbPath, resolveWorkspaceRoot } from "./workspace.js";
61
- const DIAGNOSTIC_MODE_FLAG = "--diagnostic-mode";
62
- const DIAGNOSTIC_MODE_ENABLED = process.argv.includes(DIAGNOSTIC_MODE_FLAG);
63
- const DIAGNOSTIC_TELEMETRY_SCHEMA = {
64
- type: "object",
65
- description: "REQUIRED when diagnostic mode is on. Provide self-reflection metadata about this tool call.",
66
- properties: {
67
- is_autonomous: {
68
- type: "boolean",
69
- description: "Set to TRUE if you are calling this tool on your own initiative to retrieve context. Set to FALSE if the user explicitly commanded you to use the knowledge base.",
70
- },
71
- reasoning: {
72
- type: "string",
73
- description: "A brief, 1-2 sentence internal thought explaining exactly why you are calling this tool right now and what information you expect to get.",
74
- },
75
- confidence_score: {
76
- type: "number",
77
- description: "A score from 0.0 to 1.0 representing your confidence that the exact parameters, IDs, or tags you provided will yield a successful result.",
78
- },
79
- attempt_number: {
80
- type: "integer",
81
- description: "If you are retrying this exact task because a previous tool call failed or returned empty results, increment this number (start at 1).",
82
- },
83
- missing_context: {
84
- type: "string",
85
- description: "If you had to split your task into multiple steps because this tool lacks a specific filtering or querying capability, describe what parameter is missing. Otherwise, leave empty.",
86
- },
87
- },
88
- };
89
- function withDiagnosticTelemetrySchema(tools) {
90
- return tools.map((tool) => {
91
- const schema = tool.inputSchema && typeof tool.inputSchema === "object"
92
- ? tool.inputSchema
93
- : {};
94
- const properties = schema.properties && typeof schema.properties === "object"
95
- ? schema.properties
96
- : {};
97
- return {
98
- ...tool,
99
- inputSchema: {
100
- ...schema,
101
- properties: {
102
- ...properties,
103
- _diagnostic_telemetry: DIAGNOSTIC_TELEMETRY_SCHEMA,
104
- },
105
- },
106
- };
107
- });
108
- }
109
- const BASE_TOOLS = TOOLS;
110
- const ACTIVE_TOOLS = DIAGNOSTIC_MODE_ENABLED
111
- ? withDiagnosticTelemetrySchema(BASE_TOOLS)
112
- : BASE_TOOLS;
113
- function renderToolsDoc() {
114
- const lines = [
115
- "# kibi-mcp Tools",
116
- "",
117
- "Use this reference to choose the correct tool before calling it.",
118
- "",
119
- "| Tool | Summary | Required Parameters |",
120
- "| --- | --- | --- |",
121
- ];
122
- for (const tool of ACTIVE_TOOLS) {
123
- const required = Array.isArray(tool.inputSchema?.required)
124
- ? tool.inputSchema.required.join(", ")
125
- : "none";
126
- lines.push(`| ${tool.name} | ${tool.description} | ${required} |`);
127
- }
128
- return lines.join("\n");
129
- }
130
- const PROMPTS = [
131
- {
132
- name: "kibi_overview",
133
- description: "High-level model for using kibi-mcp safely and effectively.",
134
- text: [
135
- "# kibi-mcp Overview",
136
- "",
137
- "Treat this server as a branch-aware knowledge graph interface for software traceability.",
138
- "",
139
- "- Encode requirements as linked facts: `req --constrains--> fact` plus `req --requires_property--> fact`.",
140
- "- Reuse canonical fact IDs across requirements; shared constrained facts make contradictions detectable.",
141
- "- Use `kb_query` first to confirm current state before any mutation.",
142
- "- Use `kb_upsert` and `kb_delete` only for intentional, traceable KB changes.",
143
- "- Run `kb_check` after meaningful mutations to catch integrity issues early.",
144
- "- Prefer explicit IDs and enum values to avoid invalid parameters.",
145
- "- Assume every write can affect downstream traceability queries.",
146
- ].join("\n"),
147
- },
148
- {
149
- name: "kibi_workflow",
150
- description: "Step-by-step call order for discovery, mutation, and verification.",
151
- text: [
152
- "# kibi-mcp Workflow",
153
- "",
154
- "Follow this sequence for reliable operation:",
155
- "",
156
- "1. **Inspect**: Call `kb_query` to confirm current state before any mutation.",
157
- "2. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property`.",
158
- "3. **Validate intent**: If creating links, call `kb_query` for both endpoint IDs first.",
159
- "4. **Mutate**: Call `kb_upsert` for create/update, or `kb_delete` for explicit removals.",
160
- "5. **Verify integrity**: Call `kb_check` after mutations.",
161
- "",
162
- "If a tool returns empty results, do not assume failure. Re-check filters (type, id, tags, sourceFile, limit, or offset).",
163
- ].join("\n"),
164
- },
165
- {
166
- name: "kibi_constraints",
167
- description: "Operational limits, validation rules, and mutation gotchas.",
168
- text: [
169
- "# kibi-mcp Constraints",
170
- "",
171
- "Apply these rules before calling write operations:",
172
- "",
173
- "- `kb_upsert` validates entity and relationship payloads against JSON Schema.",
174
- "- `kb_delete` blocks deletion when dependents still reference the entity.",
175
- "- Relationship and rule names are strict enums; unknown values fail validation.",
176
- "- Branch KB setup is automatic at server startup; lifecycle maintenance stays outside the public MCP tool surface.",
177
- ].join("\n"),
178
- },
179
- ];
180
- function registerDocResources() {
181
- const overview = [
182
- "# kibi-mcp Server Overview",
183
- "",
184
- "kibi-mcp is a stdio MCP server for querying and mutating the Kibi knowledge base.",
185
- "",
186
- "Scope:",
187
- "- Entity CRUD-like operations for KB records",
188
- "- Validation of KB integrity after changes",
189
- "- Automatic branch-local attachment for the active workspace",
190
- "",
191
- "Use this server when you need branch-local, machine-readable project memory.",
192
- ].join("\n");
193
- const errors = [
194
- "# kibi-mcp Error Guide",
195
- "",
196
- "Common failure modes and recoveries:",
197
- "",
198
- "- `-32602 INVALID_PARAMS`: Tool arguments are missing/invalid. Recover by checking enum values and required fields.",
199
- "- `-32601 METHOD_NOT_FOUND`: Unknown MCP method. Recover by using supported methods (`tools/*`, `prompts/*`, `resources/*`).",
200
- "- `-32000 PROLOG_QUERY_FAILED`: Prolog query failed. Recover by validating IDs, rule names, and branch KB availability.",
201
- "- `VALIDATION_ERROR` message: `kb_upsert` payload failed schema checks. Recover by fixing required fields and enum values.",
202
- "- Delete blocked by dependents: `kb_delete` detected incoming references. Recover by removing/rewiring relationships first.",
203
- "- Empty results: filters may be too strict. Recover by loosening type/id/tags/source filters and retrying.",
204
- ].join("\n");
205
- const examples = [
206
- "# kibi-mcp Examples",
207
- "",
208
- "## Model requirements as reusable facts",
209
- "1. `kb_query` to find existing fact IDs before creating new ones",
210
- "2. `kb_upsert` for the req entity and include `relationships` with `constrains` and `requires_property`",
211
- "3. Reuse the same constrained fact ID across related requirements; vary property facts only when semantics differ",
212
- '4. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }`',
213
- "",
214
- "## Add a requirement and link it to a test",
215
- "1. `kb_query` for existing IDs to avoid collisions",
216
- "2. `kb_upsert` with entity payload and `relationships` containing `verified_by`",
217
- '3. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }`',
218
- ].join("\n");
219
- return [
220
- {
221
- uri: "kibi://docs/overview",
222
- name: "kibi docs overview",
223
- description: "Full server description, purpose, and scope.",
224
- mimeType: "text/markdown",
225
- text: overview,
226
- },
227
- {
228
- uri: "kibi://docs/tools",
229
- name: "kibi docs tools",
230
- description: "Available tools with summaries and required parameters.",
231
- mimeType: "text/markdown",
232
- text: renderToolsDoc(),
233
- },
234
- {
235
- uri: "kibi://docs/errors",
236
- name: "kibi docs errors",
237
- description: "Common error modes and suggested recovery actions.",
238
- mimeType: "text/markdown",
239
- text: errors,
240
- },
241
- {
242
- uri: "kibi://docs/examples",
243
- name: "kibi docs examples",
244
- description: "Concrete tool call sequences for common tasks.",
245
- mimeType: "text/markdown",
246
- text: examples,
247
- },
248
- ];
249
- }
250
- const DOC_RESOURCES = registerDocResources();
251
- function getHelpText(topic) {
252
- const normalized = (topic ?? "overview").trim().toLowerCase();
253
- if (normalized === "tools") {
254
- return renderToolsDoc();
255
- }
256
- if (normalized === "workflow") {
257
- return PROMPTS.find((p) => p.name === "kibi_workflow")?.text ?? "";
258
- }
259
- if (normalized === "constraints") {
260
- return PROMPTS.find((p) => p.name === "kibi_constraints")?.text ?? "";
261
- }
262
- if (normalized === "examples") {
263
- return (DOC_RESOURCES.find((r) => r.uri === "kibi://docs/examples")?.text ?? "");
264
- }
265
- if (normalized === "errors") {
266
- return (DOC_RESOURCES.find((r) => r.uri === "kibi://docs/errors")?.text ?? "");
267
- }
268
- if (normalized === "branching") {
269
- return [
270
- "# Branch Selection",
271
- "",
272
- "Kibi is branch-aware. By default, the MCP server detects the current git branch and attaches to the corresponding KB in `.kb/branches/<branch>`.",
273
- "",
274
- "## Forcing a Branch",
275
- "You can override the detected branch by setting the `KIBI_BRANCH` environment variable before starting the server.",
276
- "",
277
- "Example:",
278
- "```bash",
279
- "KIBI_BRANCH=feature/auth bun run packages/mcp/src/server.ts",
280
- "```",
281
- "",
282
- "## How it works",
283
- "1. If `KIBI_BRANCH` is set, it uses that value.",
284
- "2. If not set, it runs `git branch --show-current`.",
285
- "3. If git detection fails, it falls back to `develop`.",
286
- "4. The server logs the selection process to stderr on startup.",
287
- ].join("\n");
288
- }
289
- return (DOC_RESOURCES.find((r) => r.uri === "kibi://docs/overview")?.text ?? "");
290
- }
291
- let prologProcess = null;
292
- let isInitialized = false;
293
- let activeBranchName = "develop";
294
- let ensurePrologTail = Promise.resolve();
295
- // Shutdown tracking state
296
- let isShuttingDown = false;
297
- let shutdownTimeout = null;
298
- const inFlightRequests = new Map();
299
- let diagnosticUsageLogPath = null;
300
- function extractToolCallPayload(args) {
301
- const { _diagnostic_telemetry, ...businessArgs } = args;
302
- const telemetry = _diagnostic_telemetry && typeof _diagnostic_telemetry === "object"
303
- ? _diagnostic_telemetry
304
- : null;
305
- return { businessArgs, telemetry };
306
- }
307
- function appendUsageLogLine(entry) {
308
- if (!DIAGNOSTIC_MODE_ENABLED || !diagnosticUsageLogPath) {
309
- return;
310
- }
311
- const logDir = path.dirname(diagnosticUsageLogPath);
312
- fs.mkdirSync(logDir, { recursive: true });
313
- fs.appendFileSync(diagnosticUsageLogPath, `${JSON.stringify(entry)}\n`, {
314
- encoding: "utf8",
315
- });
316
- }
317
- function extractContradictionSignal(tool, args, result) {
318
- if (tool !== "kb_upsert") {
319
- return undefined;
320
- }
321
- const id = typeof args.id === "string" ? args.id : undefined;
322
- if (!id) {
323
- return undefined;
324
- }
325
- const structured = result && typeof result === "object"
326
- ? result
327
- .structuredContent
328
- : undefined;
329
- if (!structured || typeof structured !== "object") {
330
- return undefined;
331
- }
332
- const rawCount = structured.contradiction_pairs_detected;
333
- const count = typeof rawCount === "number" ? rawCount : Number(rawCount);
334
- if (!Number.isFinite(count) || count < 0) {
335
- return undefined;
336
- }
337
- return {
338
- attempted_entity_id: id,
339
- contradiction_pairs_detected: count,
340
- };
341
- }
342
- async function probeContradictionsForReq(reqId) {
343
- if (!prologProcess?.isRunning()) {
344
- return { count: null, error: "prolog_process_not_running" };
345
- }
346
- const escaped = reqId.replace(/'/g, "\\'");
347
- const goal = `aggregate_all(count, (contradicting_reqs(A, B, _), (A = '${escaped}' ; B = '${escaped}' ; A = 'file:///${escaped}' ; B = 'file:///${escaped}')), Count)`;
348
- const result = await prologProcess.query(goal);
349
- if (!result.success) {
350
- return { count: null, error: result.error ?? "probe_query_failed" };
351
- }
352
- const count = Number(result.bindings.Count);
353
- if (!Number.isFinite(count) || count < 0) {
354
- return { count: null, error: "invalid_probe_count" };
355
- }
356
- return { count };
357
- }
358
- function ensureBranchKbExists(workspaceRoot, branch) {
359
- if (!isValidBranchName(branch)) {
360
- throw new Error(`Invalid branch name: ${branch}`);
361
- }
362
- const branchPath = resolveKbPath(workspaceRoot, branch);
363
- if (fs.existsSync(branchPath)) {
364
- return;
365
- }
366
- const templateBranch = ["develop", "main"].find((candidate) => candidate !== branch &&
367
- fs.existsSync(resolveKbPath(workspaceRoot, candidate)));
368
- if (!templateBranch) {
369
- throw new Error(`No template branch KB found for '${branch}'. Expected '.kb/branches/develop' or '.kb/branches/main'.`);
370
- }
371
- // Use clean snapshot copy that excludes volatile artifacts
372
- copyCleanSnapshot(resolveKbPath(workspaceRoot, templateBranch), branchPath);
373
- }
374
- function debugLog(...args) {
375
- if (process.env.KIBI_MCP_DEBUG) {
376
- console.error(...args);
377
- }
378
- }
379
- async function initiateGracefulShutdown(exitCode = 0) {
380
- if (isShuttingDown) {
381
- return;
382
- }
383
- isShuttingDown = true;
384
- debugLog(`[KIBI-MCP] Initiating graceful shutdown (exit code: ${exitCode})`);
385
- // Wait for in-flight requests
386
- if (inFlightRequests.size > 0) {
387
- debugLog(`[KIBI-MCP] Waiting for ${inFlightRequests.size} in-flight requests to complete...`);
388
- const timeoutPromise = new Promise((_, reject) => {
389
- shutdownTimeout = setTimeout(() => {
390
- reject(new Error("Shutdown timeout"));
391
- }, 10000); // 10 second timeout
392
- });
393
- try {
394
- await Promise.race([
395
- Promise.allSettled(Array.from(inFlightRequests.values())),
396
- timeoutPromise,
397
- ]);
398
- debugLog("[KIBI-MCP] All in-flight requests completed");
399
- }
400
- catch (_error) {
401
- console.error("[KIBI-MCP] Shutdown timeout reached, forcing exit");
402
- }
403
- finally {
404
- if (shutdownTimeout) {
405
- clearTimeout(shutdownTimeout);
406
- shutdownTimeout = null;
407
- }
408
- }
409
- }
410
- // Cleanup Prolog process
411
- if (prologProcess?.isRunning()) {
412
- debugLog("[KIBI-MCP] Terminating Prolog process...");
413
- try {
414
- await prologProcess.terminate();
415
- debugLog("[KIBI-MCP] Prolog process terminated");
416
- }
417
- catch (error) {
418
- console.error("[KIBI-MCP] Error terminating Prolog:", error);
419
- }
420
- }
421
- // Exit
422
- process.exit(exitCode);
423
- }
424
- async function ensurePrologUnsafe() {
425
- const workspaceRoot = resolveWorkspaceRoot();
426
- // Determine target branch: respect KIBI_BRANCH override or resolve from git
427
- const envBranch = process.env.KIBI_BRANCH?.trim();
428
- let targetBranch;
429
- if (envBranch) {
430
- // KIBI_BRANCH override is set - use it without re-resolving from git
431
- if (!isValidBranchName(envBranch)) {
432
- throw new Error(`Invalid branch name from KIBI_BRANCH: '${envBranch}'`);
433
- }
434
- targetBranch = envBranch;
435
- }
436
- else {
437
- // No override - resolve active branch from git (may change between requests)
438
- const branchResult = resolveActiveBranch(workspaceRoot);
439
- if ("error" in branchResult) {
440
- const diagnostic = getBranchDiagnostic(undefined, branchResult.error);
441
- console.error(`[KIBI-MCP] ${diagnostic}`);
442
- throw new Error(`Failed to resolve active branch: ${branchResult.error}`);
443
- }
444
- targetBranch = branchResult.branch;
445
- }
446
- // Check if we need to switch branches
447
- if (isInitialized && prologProcess?.isRunning()) {
448
- if (targetBranch === activeBranchName) {
449
- // Still on the same branch - return existing connection
450
- return prologProcess;
451
- }
452
- // Branch changed - need to detach and re-attach
453
- debugLog(`[KIBI-MCP] Branch changed: ${activeBranchName} -> ${targetBranch}`);
454
- // Detach from old KB
455
- const detachResult = await prologProcess.query("kb_detach");
456
- if (!detachResult.success) {
457
- debugLog(`[KIBI-MCP] Warning: failed to detach from old KB: ${detachResult.error || "Unknown error"}`);
458
- // Continue anyway - we'll try to attach to the new KB
459
- }
460
- // Ensure new branch KB exists
461
- ensureBranchKbExists(workspaceRoot, targetBranch);
462
- const newKbPath = resolveKbPath(workspaceRoot, targetBranch);
463
- // Attach to new branch KB
464
- const attachResult = await prologProcess.query(`kb_attach('${newKbPath}')`);
465
- if (!attachResult.success) {
466
- throw new Error(`Failed to attach to new branch KB: ${attachResult.error || "Unknown error"}`);
467
- }
468
- activeBranchName = targetBranch;
469
- debugLog(`[KIBI-MCP] Re-attached to branch: ${targetBranch}`);
470
- debugLog(`[KIBI-MCP] KB path: ${newKbPath}`);
471
- return prologProcess;
472
- }
473
- // First initialization
474
- debugLog("[KIBI-MCP] Initializing Prolog process...");
475
- prologProcess = new PrologProcess({ timeout: 120000 });
476
- await prologProcess.start();
477
- // Startup debug: resolve which kibi-cli is being used and its version (best-effort).
478
- // Gate all output under KIBI_MCP_DEBUG and write only to stderr via debugLog.
479
- if (process.env.KIBI_MCP_DEBUG) {
480
- try {
481
- const req = createRequire(import.meta.url);
482
- try {
483
- const resolved = req.resolve("kibi-cli/prolog");
484
- debugLog(`[KIBI-MCP] require.resolve('kibi-cli/prolog') -> ${resolved}`);
485
- }
486
- catch (resolveErr) {
487
- debugLog("[KIBI-MCP] require.resolve('kibi-cli/prolog') failed:", resolveErr.message);
488
- }
489
- // Try to read package.json for kibi-cli to get version. This may fail if
490
- // the package uses exports blocking package.json access — log explicit failure.
491
- try {
492
- // prefer direct package.json require; createRequire makes this ESM-friendly
493
- // eslint-disable-next-line @typescript-eslint/no-var-requires
494
- const pkg = req("kibi-cli/package.json");
495
- if (pkg && typeof pkg.version === "string") {
496
- debugLog(`[KIBI-MCP] kibi-cli version: ${pkg.version}`);
497
- }
498
- else {
499
- debugLog("[KIBI-MCP] kibi-cli package.json read but no version field");
500
- }
501
- }
502
- catch (pkgErr) {
503
- debugLog("[KIBI-MCP] Failed to read kibi-cli package.json (exports may restrict access):", pkgErr.message);
504
- }
505
- }
506
- catch (err) {
507
- debugLog("[KIBI-MCP] Failed to create require() for debug lookup:", err.message);
508
- }
509
- }
510
- debugLog("[KIBI-MCP] Branch selection:");
511
- debugLog(`[KIBI-MCP] KIBI_BRANCH env: ${process.env.KIBI_BRANCH || "not set"}`);
512
- debugLog(`[KIBI-MCP] Resolved branch: ${targetBranch}`);
513
- activeBranchName = targetBranch;
514
- ensureBranchKbExists(workspaceRoot, targetBranch);
515
- const kbPath = resolveKbPath(workspaceRoot, targetBranch);
516
- const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
517
- if (!attachResult.success) {
518
- throw new Error(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
519
- }
520
- isInitialized = true;
521
- debugLog(`[KIBI-MCP] Prolog process started (PID: ${prologProcess.getPid()})`);
522
- debugLog(`[KIBI-MCP] KB attached: ${kbPath}`);
523
- return prologProcess;
524
- }
525
- async function ensureProlog() {
526
- const previous = ensurePrologTail;
527
- let release;
528
- ensurePrologTail = new Promise((resolve) => {
529
- release = resolve;
530
- });
531
- await previous;
532
- try {
533
- return await ensurePrologUnsafe();
534
- }
535
- finally {
536
- release();
537
- }
538
- }
539
- function jsonSchemaToZod(schema) {
540
- if (!schema || typeof schema !== "object") {
541
- return z.any();
542
- }
543
- const obj = schema;
544
- if (Array.isArray(obj.enum) && obj.enum.length > 0) {
545
- const description = typeof obj.description === "string" ? obj.description : undefined;
546
- const literals = obj.enum.filter((value) => typeof value === "string" ||
547
- typeof value === "number" ||
548
- typeof value === "boolean" ||
549
- value === null);
550
- if (literals.length === 0) {
551
- return description ? z.any().describe(description) : z.any();
552
- }
553
- const literalSchemas = literals.map((value) => z.literal(value));
554
- if (literalSchemas.length === 1) {
555
- const single = literalSchemas[0];
556
- return description ? single.describe(description) : single;
557
- }
558
- const union = z.union(literalSchemas);
559
- return description ? union.describe(description) : union;
560
- }
561
- const schemaType = typeof obj.type === "string" ? obj.type : undefined;
562
- switch (schemaType) {
563
- case "object": {
564
- const properties = obj.properties && typeof obj.properties === "object"
565
- ? obj.properties
566
- : {};
567
- const required = new Set(Array.isArray(obj.required)
568
- ? obj.required.filter((k) => typeof k === "string" && k.length > 0)
569
- : []);
570
- const shape = {};
571
- for (const [key, value] of Object.entries(properties)) {
572
- const propSchema = jsonSchemaToZod(value);
573
- shape[key] = required.has(key) ? propSchema : propSchema.optional();
574
- }
575
- let objectSchema = z.object(shape);
576
- if (obj.additionalProperties !== false) {
577
- objectSchema = objectSchema.passthrough();
578
- }
579
- const description = typeof obj.description === "string" ? obj.description : undefined;
580
- return description ? objectSchema.describe(description) : objectSchema;
581
- }
582
- case "array": {
583
- const itemSchema = jsonSchemaToZod(obj.items);
584
- let arraySchema = z.array(itemSchema);
585
- const description = typeof obj.description === "string" ? obj.description : undefined;
586
- if (typeof obj.minItems === "number") {
587
- arraySchema = arraySchema.min(obj.minItems);
588
- }
589
- if (typeof obj.maxItems === "number") {
590
- arraySchema = arraySchema.max(obj.maxItems);
591
- }
592
- return description ? arraySchema.describe(description) : arraySchema;
593
- }
594
- case "string": {
595
- let s = z.string();
596
- const description = typeof obj.description === "string" ? obj.description : undefined;
597
- if (typeof obj.minLength === "number") {
598
- s = s.min(obj.minLength);
599
- }
600
- if (typeof obj.maxLength === "number") {
601
- s = s.max(obj.maxLength);
602
- }
603
- return description ? s.describe(description) : s;
604
- }
605
- case "number": {
606
- let n = z.number();
607
- const description = typeof obj.description === "string" ? obj.description : undefined;
608
- if (typeof obj.minimum === "number") {
609
- n = n.min(obj.minimum);
610
- }
611
- if (typeof obj.maximum === "number") {
612
- n = n.max(obj.maximum);
613
- }
614
- return description ? n.describe(description) : n;
615
- }
616
- case "integer": {
617
- let n = z.number().int();
618
- const description = typeof obj.description === "string" ? obj.description : undefined;
619
- if (typeof obj.minimum === "number") {
620
- n = n.min(obj.minimum);
621
- }
622
- if (typeof obj.maximum === "number") {
623
- n = n.max(obj.maximum);
624
- }
625
- return description ? n.describe(description) : n;
626
- }
627
- case "boolean": {
628
- const b = z.boolean();
629
- const description = typeof obj.description === "string" ? obj.description : undefined;
630
- return description ? b.describe(description) : b;
631
- }
632
- default: {
633
- const anySchema = z.any();
634
- const description = typeof obj.description === "string" ? obj.description : undefined;
635
- return description ? anySchema.describe(description) : anySchema;
636
- }
637
- }
638
- }
639
- function addTool(server, name, description, inputSchema, handler) {
640
- const wrappedHandler = async (args) => {
641
- let telemetry = null;
642
- let businessArgs = {};
643
- const startedAt = new Date();
644
- try {
645
- // Validate that args is a valid object
646
- if (typeof args !== "object" || args === null) {
647
- throw new Error(`Invalid arguments for tool ${name}: expected object, got ${typeof args}`);
648
- }
649
- if (DIAGNOSTIC_MODE_ENABLED) {
650
- const payload = extractToolCallPayload(args);
651
- telemetry = payload.telemetry;
652
- businessArgs = payload.businessArgs;
653
- }
654
- else {
655
- businessArgs = args;
656
- }
657
- // Check if shutting down before processing
658
- if (isShuttingDown) {
659
- throw new Error(`Tool ${name} rejected: server is shutting down`);
660
- }
661
- // Extract or generate requestId from args
662
- const requestIdArg = businessArgs._requestId;
663
- const requestId = typeof requestIdArg === "string"
664
- ? requestIdArg
665
- : `${name}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
666
- // Log tool call for debugging (to stderr to avoid breaking stdio protocol)
667
- if (process.env.KIBI_MCP_DEBUG) {
668
- console.error(`[KIBI-MCP] Tool called: ${name} (requestId: ${requestId}) with args:`, JSON.stringify(businessArgs));
669
- }
670
- // Track the handler promise in inFlightRequests Map
671
- const handlerPromise = handler(businessArgs);
672
- inFlightRequests.set(requestId, handlerPromise);
673
- try {
674
- // Execute handler
675
- const result = await handlerPromise;
676
- const finishedAt = new Date();
677
- const contradictionSignal = extractContradictionSignal(name, businessArgs, result);
678
- let contradictionSignalFinal = contradictionSignal;
679
- if (name === "kb_upsert" && typeof businessArgs.id === "string") {
680
- const probe = businessArgs.type === "req"
681
- ? await probeContradictionsForReq(businessArgs.id)
682
- : { count: null, error: "non_req_entity" };
683
- contradictionSignalFinal = {
684
- attempted_entity_id: businessArgs.id,
685
- contradiction_pairs_detected: probe.count !== null ? probe.count : -1,
686
- probe_error: probe.error,
687
- };
688
- }
689
- appendUsageLogLine({
690
- timestamp: finishedAt.toISOString(),
691
- request_id: requestId,
692
- tool: name,
693
- telemetry,
694
- business_args: businessArgs,
695
- status: "success",
696
- started_at: startedAt.toISOString(),
697
- finished_at: finishedAt.toISOString(),
698
- duration_ms: finishedAt.getTime() - startedAt.getTime(),
699
- prolog_pid: prologProcess?.getPid() ?? null,
700
- active_branch: activeBranchName,
701
- contradiction_signal: contradictionSignalFinal,
702
- });
703
- return result;
704
- }
705
- catch (error) {
706
- const finishedAt = new Date();
707
- const err = error instanceof Error ? error : new Error(String(error));
708
- appendUsageLogLine({
709
- timestamp: finishedAt.toISOString(),
710
- request_id: requestId,
711
- tool: name,
712
- telemetry,
713
- business_args: businessArgs,
714
- status: "error",
715
- started_at: startedAt.toISOString(),
716
- finished_at: finishedAt.toISOString(),
717
- duration_ms: finishedAt.getTime() - startedAt.getTime(),
718
- prolog_pid: prologProcess?.getPid() ?? null,
719
- active_branch: activeBranchName,
720
- error_message: err.message,
721
- });
722
- throw error;
723
- }
724
- finally {
725
- // Always clean up from Map when done (success or failure)
726
- inFlightRequests.delete(requestId);
727
- }
728
- }
729
- catch (error) {
730
- const err = error instanceof Error ? error : new Error(String(error));
731
- console.error(`[KIBI-MCP] Error in tool ${name}:`, err.message);
732
- if (err.stack) {
733
- debugLog(`[KIBI-MCP] Tool ${name} stack:`, err.stack);
734
- }
735
- throw new Error(`Tool ${name} failed: ${err.message}`, { cause: err });
736
- }
737
- };
738
- server.registerTool(name, { description, inputSchema: jsonSchemaToZod(inputSchema) }, wrappedHandler);
739
- }
21
+ import { setupDocsAndPrompts } from "./server/docs.js";
22
+ import { registerAllTools } from "./server/tools.js";
23
+ import { connectTransport, setupTransportHandlers, } from "./server/transport.js";
740
24
  export async function startServer() {
25
+ // Load environment configuration
741
26
  loadDefaultEnvFile();
742
- if (DIAGNOSTIC_MODE_ENABLED) {
743
- const workspaceRoot = resolveWorkspaceRoot();
744
- diagnosticUsageLogPath = path.join(workspaceRoot, ".kb", "usage.log");
745
- process.env.KIBI_MCP_DIAGNOSTIC_MODE = "1";
746
- }
27
+ // Create MCP server
747
28
  const server = new McpServer({ name: "kibi-mcp", version: "0.2.1" });
748
- for (const prompt of PROMPTS) {
749
- server.prompt(prompt.name, prompt.description, async () => ({
750
- messages: [
751
- {
752
- role: "user",
753
- content: { type: "text", text: prompt.text },
754
- },
755
- ],
756
- }));
757
- }
758
- for (const resource of DOC_RESOURCES) {
759
- server.resource(resource.name, resource.uri, { description: resource.description, mimeType: resource.mimeType }, async () => ({
760
- contents: [
761
- {
762
- uri: resource.uri,
763
- mimeType: resource.mimeType,
764
- text: resource.text,
765
- },
766
- ],
767
- }));
768
- }
769
- const toolDef = (name) => {
770
- const t = ACTIVE_TOOLS.find((t) => t.name === name);
771
- if (!t)
772
- throw new Error(`Unknown tool: ${name}`);
773
- return t;
774
- };
775
- addTool(server, "kb_query", toolDef("kb_query").description, toolDef("kb_query").inputSchema, async (args) => {
776
- const prolog = await ensureProlog();
777
- return handleKbQuery(prolog, args);
778
- });
779
- addTool(server, "kb_upsert", toolDef("kb_upsert").description, toolDef("kb_upsert").inputSchema, async (args) => {
780
- const prolog = await ensureProlog();
781
- return handleKbUpsert(prolog, args);
782
- });
783
- addTool(server, "kb_delete", toolDef("kb_delete").description, toolDef("kb_delete").inputSchema, async (args) => {
784
- const prolog = await ensureProlog();
785
- return handleKbDelete(prolog, args);
786
- });
787
- addTool(server, "kb_check", toolDef("kb_check").description, toolDef("kb_check").inputSchema, async (args) => {
788
- const prolog = await ensureProlog();
789
- return handleKbCheck(prolog, args);
790
- });
29
+ // Setup documentation resources and prompts
30
+ setupDocsAndPrompts(server);
31
+ // Register all KB tools
32
+ registerAllTools(server);
33
+ // Setup transport and connect
791
34
  const transport = new StdioServerTransport();
792
- transport.onerror = (error) => {
793
- // Stdio transport surfaces JSON parse / schema validation failures via onerror.
794
- // Those errors should not crash the server: emit a JSON-RPC error (id omitted)
795
- // and continue reading subsequent messages.
796
- if (error.name === "SyntaxError") {
797
- debugLog("[KIBI-MCP] Parse error from stdin:", error.message);
798
- void transport
799
- .send({
800
- jsonrpc: "2.0",
801
- error: { code: -32700, message: "Parse error" },
802
- })
803
- .catch((sendError) => {
804
- console.error("[KIBI-MCP] Failed to send parse error response:", sendError);
805
- initiateGracefulShutdown(1);
806
- });
807
- return;
808
- }
809
- if (error.name === "ZodError") {
810
- debugLog("[KIBI-MCP] Invalid JSON-RPC message:", error.message);
811
- void transport
812
- .send({
813
- jsonrpc: "2.0",
814
- error: { code: -32600, message: "Invalid Request" },
815
- })
816
- .catch((sendError) => {
817
- console.error("[KIBI-MCP] Failed to send invalid request response:", sendError);
818
- initiateGracefulShutdown(1);
819
- });
820
- return;
821
- }
822
- console.error(`[KIBI-MCP] Transport error: ${error.message}`, error);
823
- debugLog("[KIBI-MCP] Transport error stack:", error.stack);
824
- initiateGracefulShutdown(1);
825
- };
826
- transport.onclose = () => {
827
- debugLog("[KIBI-MCP] Transport closed");
828
- initiateGracefulShutdown(0);
829
- };
830
- await server.connect(transport);
831
- process.stdout.on("error", (error) => {
832
- const message = error instanceof Error ? error.message : String(error);
833
- console.error("[KIBI-MCP] stdout error:", message);
834
- debugLog("[KIBI-MCP] stdout error detail:", error);
835
- initiateGracefulShutdown(1);
836
- });
837
- process.stderr.on("error", (error) => {
838
- const message = error instanceof Error ? error.message : String(error);
839
- try {
840
- console.error("[KIBI-MCP] stderr error:", message);
841
- }
842
- catch { }
843
- initiateGracefulShutdown(1);
844
- });
845
- process.on("SIGTERM", () => {
846
- debugLog("[KIBI-MCP] Received SIGTERM");
847
- initiateGracefulShutdown(0);
848
- });
849
- process.on("SIGINT", () => {
850
- debugLog("[KIBI-MCP] Received SIGINT");
851
- initiateGracefulShutdown(0);
852
- });
853
- // Handle stdin EOF/close for clean shutdown when client disconnects
854
- // Use debugLog so these are only noisy when KIBI_MCP_DEBUG is set.
855
- try {
856
- process.stdin.on("end", () => {
857
- debugLog("[KIBI-MCP] stdin ended");
858
- // fire-and-forget; initiateGracefulShutdown is idempotent
859
- void initiateGracefulShutdown(0);
860
- });
861
- process.stdin.on("close", () => {
862
- debugLog("[KIBI-MCP] stdin closed");
863
- void initiateGracefulShutdown(0);
864
- });
865
- }
866
- catch (e) {
867
- // Defensive: do not let stdin handler setup throw during startup
868
- debugLog("[KIBI-MCP] Failed to attach stdin handlers:", e);
869
- }
35
+ setupTransportHandlers(server, transport);
36
+ await connectTransport(server, transport);
870
37
  }