kibi-mcp 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.
package/dist/server.js ADDED
@@ -0,0 +1,673 @@
1
+ /*
2
+ Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
+ Copyright (C) 2026 Piotr Franczyk
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+ /*
19
+ How to apply this header to source files (examples)
20
+
21
+ 1) Prepend header to a single file (POSIX shells):
22
+
23
+ cat LICENSE_HEADER.txt "$FILE" > "$FILE".with-header && mv "$FILE".with-header "$FILE"
24
+
25
+ 2) Apply to multiple files (example: the project's main entry files):
26
+
27
+ for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp packages/cli/src/*.ts packages/mcp/src/*.ts; do
28
+ if [ -f "$f" ]; then
29
+ cp "$f" "$f".bak
30
+ (cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
31
+ fi
32
+ done
33
+
34
+ 3) Avoid duplicating the header: run a quick guard to only add if missing
35
+
36
+ for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp; do
37
+ if [ -f "$f" ]; then
38
+ if ! head -n 5 "$f" | grep -q "Copyright (C) 2026 Piotr Franczyk"; then
39
+ cp "$f" "$f".bak
40
+ (cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
41
+ fi
42
+ fi
43
+ done
44
+ */
45
+ import process from "node:process";
46
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
47
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
48
+ import { PrologProcess } from "kibi-cli/prolog";
49
+ import { z } from "zod";
50
+ import { loadDefaultEnvFile } from "./env.js";
51
+ import { attachMcpcat } from "./mcpcat.js";
52
+ import { TOOLS } from "./tools-config.js";
53
+ import { handleKbBranchEnsure, handleKbBranchGc, } from "./tools/branch.js";
54
+ import { handleKbCheck } from "./tools/check.js";
55
+ import { handleKbContext } from "./tools/context.js";
56
+ import { handleKbCoverageReport, } from "./tools/coverage-report.js";
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
+ import { handleKbQuery } from "./tools/query.js";
63
+ import { handleKbUpsert } from "./tools/upsert.js";
64
+ import { handleKbSymbolsRefresh, } from "./tools/symbols.js";
65
+ import { handleSuggestSharedFacts, } from "./tools/suggest-shared-facts.js";
66
+ import { resolveKbPath, resolveWorkspaceRoot } from "./workspace.js";
67
+ function renderToolsDoc() {
68
+ const lines = [
69
+ "# kibi-mcp Tools",
70
+ "",
71
+ "Use this reference to choose the correct tool before calling it.",
72
+ "",
73
+ "| Tool | Summary | Required Parameters |",
74
+ "| --- | --- | --- |",
75
+ ];
76
+ for (const tool of TOOLS) {
77
+ const required = Array.isArray(tool.inputSchema?.required)
78
+ ? tool.inputSchema.required.join(", ")
79
+ : "none";
80
+ lines.push(`| ${tool.name} | ${tool.description} | ${required} |`);
81
+ }
82
+ return lines.join("\n");
83
+ }
84
+ const PROMPTS = [
85
+ {
86
+ name: "kibi_overview",
87
+ description: "High-level model for using kibi-mcp safely and effectively.",
88
+ text: [
89
+ "# kibi-mcp Overview",
90
+ "",
91
+ "Treat this server as a branch-aware knowledge graph interface for software traceability.",
92
+ "",
93
+ "- Encode requirements as linked facts: `req --constrains--> fact` plus `req --requires_property--> fact`.",
94
+ "- 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.",
98
+ "- Prefer explicit IDs and enum values to avoid invalid parameters.",
99
+ "- Assume every write can affect downstream traceability queries.",
100
+ ].join("\n"),
101
+ },
102
+ {
103
+ name: "kibi_workflow",
104
+ description: "Step-by-step call order for discovery, mutation, and verification.",
105
+ text: [
106
+ "# kibi-mcp Workflow",
107
+ "",
108
+ "Follow this sequence for reliable operation:",
109
+ "",
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.",
117
+ "",
118
+ "If a tool returns empty results, do not assume failure. Re-check filters (type, id, tags, sourceFile, or relationship type).",
119
+ ].join("\n"),
120
+ },
121
+ {
122
+ name: "kibi_constraints",
123
+ description: "Operational limits, validation rules, and mutation gotchas.",
124
+ text: [
125
+ "# kibi-mcp Constraints",
126
+ "",
127
+ "Apply these rules before calling write operations:",
128
+ "",
129
+ "- `kb_upsert` validates entity and relationship payloads against JSON Schema.",
130
+ "- `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
+ "- 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.",
135
+ ].join("\n"),
136
+ },
137
+ ];
138
+ function registerDocResources() {
139
+ const overview = [
140
+ "# kibi-mcp Server Overview",
141
+ "",
142
+ "kibi-mcp is a stdio MCP server for querying and mutating the Kibi knowledge base.",
143
+ "",
144
+ "Scope:",
145
+ "- Entity CRUD-like operations for KB records",
146
+ "- Relationship inspection",
147
+ "- Validation and branch KB maintenance",
148
+ "- Deterministic inference for traceability and impact analysis",
149
+ "",
150
+ "Use this server when you need branch-local, machine-readable project memory.",
151
+ ].join("\n");
152
+ const errors = [
153
+ "# kibi-mcp Error Guide",
154
+ "",
155
+ "Common failure modes and recoveries:",
156
+ "",
157
+ "- `-32602 INVALID_PARAMS`: Tool arguments are missing/invalid. Recover by checking enum values and required fields.",
158
+ "- `-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.",
160
+ "- `VALIDATION_ERROR` message: `kb_upsert` payload failed schema checks. Recover by fixing required fields and enum values.",
161
+ "- Delete blocked by dependents: `kb_delete` detected incoming references. Recover by removing/rewiring relationships first.",
162
+ "- Empty results: filters may be too strict. Recover by loosening type/id/tags/source filters and retrying.",
163
+ ].join("\n");
164
+ const examples = [
165
+ "# kibi-mcp Examples",
166
+ "",
167
+ "## Model requirements as reusable facts",
168
+ "1. `kb_query` to find existing fact IDs before creating new ones",
169
+ "2. `kb_upsert` for the req entity and include `relationships` with `constrains` and `requires_property`",
170
+ "3. Reuse the same constrained fact ID across related requirements; vary property facts only when semantics differ",
171
+ '4. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }`',
172
+ "",
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
+ "## Add a requirement and link it to a test",
179
+ "1. `kb_query` for existing IDs to avoid collisions",
180
+ "2. `kb_upsert` with entity payload and `relationships` containing `verified_by`",
181
+ '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
+ ].join("\n");
188
+ return [
189
+ {
190
+ uri: "kibi://docs/overview",
191
+ name: "kibi docs overview",
192
+ description: "Full server description, purpose, and scope.",
193
+ mimeType: "text/markdown",
194
+ text: overview,
195
+ },
196
+ {
197
+ uri: "kibi://docs/tools",
198
+ name: "kibi docs tools",
199
+ description: "Available tools with summaries and required parameters.",
200
+ mimeType: "text/markdown",
201
+ text: renderToolsDoc(),
202
+ },
203
+ {
204
+ uri: "kibi://docs/errors",
205
+ name: "kibi docs errors",
206
+ description: "Common error modes and suggested recovery actions.",
207
+ mimeType: "text/markdown",
208
+ text: errors,
209
+ },
210
+ {
211
+ uri: "kibi://docs/examples",
212
+ name: "kibi docs examples",
213
+ description: "Concrete tool call sequences for common tasks.",
214
+ mimeType: "text/markdown",
215
+ text: examples,
216
+ },
217
+ ];
218
+ }
219
+ const DOC_RESOURCES = registerDocResources();
220
+ function getHelpText(topic) {
221
+ const normalized = (topic ?? "overview").trim().toLowerCase();
222
+ if (normalized === "tools") {
223
+ return renderToolsDoc();
224
+ }
225
+ if (normalized === "workflow") {
226
+ return PROMPTS.find((p) => p.name === "kibi_workflow")?.text ?? "";
227
+ }
228
+ if (normalized === "constraints") {
229
+ return PROMPTS.find((p) => p.name === "kibi_constraints")?.text ?? "";
230
+ }
231
+ if (normalized === "examples") {
232
+ return (DOC_RESOURCES.find((r) => r.uri === "kibi://docs/examples")?.text ?? "");
233
+ }
234
+ if (normalized === "errors") {
235
+ return (DOC_RESOURCES.find((r) => r.uri === "kibi://docs/errors")?.text ?? "");
236
+ }
237
+ if (normalized === "branching") {
238
+ return [
239
+ "# Branch Selection",
240
+ "",
241
+ "Kibi is branch-aware. By default, the MCP server detects the current git branch and attaches to the corresponding KB in `.kb/branches/<branch>`.",
242
+ "",
243
+ "## Forcing a Branch",
244
+ "You can override the detected branch by setting the `KIBI_BRANCH` environment variable before starting the server.",
245
+ "",
246
+ "Example:",
247
+ "```bash",
248
+ "KIBI_BRANCH=feature/auth bun run packages/mcp/src/server.ts",
249
+ "```",
250
+ "",
251
+ "## How it works",
252
+ "1. If `KIBI_BRANCH` is set, it uses that value.",
253
+ "2. If not set, it runs `git branch --show-current`.",
254
+ "3. If git detection fails, it falls back to `develop`.",
255
+ "4. The server logs the selection process to stderr on startup.",
256
+ ].join("\n");
257
+ }
258
+ return (DOC_RESOURCES.find((r) => r.uri === "kibi://docs/overview")?.text ?? "");
259
+ }
260
+ let prologProcess = null;
261
+ let isInitialized = false;
262
+ let activeBranchName = "develop";
263
+ // Shutdown tracking state
264
+ let isShuttingDown = false;
265
+ let shutdownTimeout = null;
266
+ const inFlightRequests = new Map();
267
+ function debugLog(...args) {
268
+ if (process.env.KIBI_MCP_DEBUG) {
269
+ console.error(...args);
270
+ }
271
+ }
272
+ async function initiateGracefulShutdown(exitCode = 0) {
273
+ if (isShuttingDown) {
274
+ return;
275
+ }
276
+ isShuttingDown = true;
277
+ debugLog(`[KIBI-MCP] Initiating graceful shutdown (exit code: ${exitCode})`);
278
+ // Wait for in-flight requests
279
+ if (inFlightRequests.size > 0) {
280
+ debugLog(`[KIBI-MCP] Waiting for ${inFlightRequests.size} in-flight requests to complete...`);
281
+ const timeoutPromise = new Promise((_, reject) => {
282
+ shutdownTimeout = setTimeout(() => {
283
+ reject(new Error("Shutdown timeout"));
284
+ }, 10000); // 10 second timeout
285
+ });
286
+ try {
287
+ await Promise.race([
288
+ Promise.allSettled(Array.from(inFlightRequests.values())),
289
+ timeoutPromise,
290
+ ]);
291
+ debugLog("[KIBI-MCP] All in-flight requests completed");
292
+ }
293
+ catch (_error) {
294
+ console.error("[KIBI-MCP] Shutdown timeout reached, forcing exit");
295
+ }
296
+ finally {
297
+ if (shutdownTimeout) {
298
+ clearTimeout(shutdownTimeout);
299
+ shutdownTimeout = null;
300
+ }
301
+ }
302
+ }
303
+ // Cleanup Prolog process
304
+ if (prologProcess?.isRunning()) {
305
+ debugLog("[KIBI-MCP] Terminating Prolog process...");
306
+ try {
307
+ await prologProcess.terminate();
308
+ debugLog("[KIBI-MCP] Prolog process terminated");
309
+ }
310
+ catch (error) {
311
+ console.error("[KIBI-MCP] Error terminating Prolog:", error);
312
+ }
313
+ }
314
+ // Exit
315
+ process.exit(exitCode);
316
+ }
317
+ async function ensureProlog() {
318
+ if (isInitialized && prologProcess?.isRunning()) {
319
+ return prologProcess;
320
+ }
321
+ debugLog("[KIBI-MCP] Initializing Prolog process...");
322
+ prologProcess = new PrologProcess({ timeout: 30000 });
323
+ await prologProcess.start();
324
+ const workspaceRoot = resolveWorkspaceRoot();
325
+ let branch = process.env.KIBI_BRANCH || "develop";
326
+ let gitBranch;
327
+ if (!process.env.KIBI_BRANCH) {
328
+ 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;
338
+ }
339
+ }
340
+ catch {
341
+ // fall back to develop
342
+ }
343
+ }
344
+ debugLog("[KIBI-MCP] Branch selection:");
345
+ 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);
351
+ const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
352
+ if (!attachResult.success) {
353
+ throw new Error(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
354
+ }
355
+ isInitialized = true;
356
+ debugLog(`[KIBI-MCP] Prolog process started (PID: ${prologProcess.getPid()})`);
357
+ debugLog(`[KIBI-MCP] KB attached: ${kbPath}`);
358
+ return prologProcess;
359
+ }
360
+ function jsonSchemaToZod(schema) {
361
+ if (!schema || typeof schema !== "object") {
362
+ return z.any();
363
+ }
364
+ const obj = schema;
365
+ if (Array.isArray(obj.enum) && obj.enum.length > 0) {
366
+ const description = typeof obj.description === "string" ? obj.description : undefined;
367
+ const literals = obj.enum.filter((value) => typeof value === "string" ||
368
+ typeof value === "number" ||
369
+ typeof value === "boolean" ||
370
+ value === null);
371
+ if (literals.length === 0) {
372
+ return description ? z.any().describe(description) : z.any();
373
+ }
374
+ const literalSchemas = literals.map((value) => z.literal(value));
375
+ if (literalSchemas.length === 1) {
376
+ const single = literalSchemas[0];
377
+ return description ? single.describe(description) : single;
378
+ }
379
+ const union = z.union(literalSchemas);
380
+ return description ? union.describe(description) : union;
381
+ }
382
+ const schemaType = typeof obj.type === "string" ? obj.type : undefined;
383
+ switch (schemaType) {
384
+ case "object": {
385
+ const properties = obj.properties && typeof obj.properties === "object"
386
+ ? obj.properties
387
+ : {};
388
+ const required = new Set(Array.isArray(obj.required)
389
+ ? obj.required.filter((k) => typeof k === "string" && k.length > 0)
390
+ : []);
391
+ const shape = {};
392
+ for (const [key, value] of Object.entries(properties)) {
393
+ const propSchema = jsonSchemaToZod(value);
394
+ shape[key] = required.has(key) ? propSchema : propSchema.optional();
395
+ }
396
+ let objectSchema = z.object(shape);
397
+ if (obj.additionalProperties !== false) {
398
+ objectSchema = objectSchema.passthrough();
399
+ }
400
+ const description = typeof obj.description === "string" ? obj.description : undefined;
401
+ return description ? objectSchema.describe(description) : objectSchema;
402
+ }
403
+ case "array": {
404
+ const itemSchema = jsonSchemaToZod(obj.items);
405
+ let arraySchema = z.array(itemSchema);
406
+ const description = typeof obj.description === "string" ? obj.description : undefined;
407
+ if (typeof obj.minItems === "number") {
408
+ arraySchema = arraySchema.min(obj.minItems);
409
+ }
410
+ if (typeof obj.maxItems === "number") {
411
+ arraySchema = arraySchema.max(obj.maxItems);
412
+ }
413
+ return description ? arraySchema.describe(description) : arraySchema;
414
+ }
415
+ case "string": {
416
+ let s = z.string();
417
+ const description = typeof obj.description === "string" ? obj.description : undefined;
418
+ if (typeof obj.minLength === "number") {
419
+ s = s.min(obj.minLength);
420
+ }
421
+ if (typeof obj.maxLength === "number") {
422
+ s = s.max(obj.maxLength);
423
+ }
424
+ return description ? s.describe(description) : s;
425
+ }
426
+ case "number": {
427
+ let n = z.number();
428
+ const description = typeof obj.description === "string" ? obj.description : undefined;
429
+ if (typeof obj.minimum === "number") {
430
+ n = n.min(obj.minimum);
431
+ }
432
+ if (typeof obj.maximum === "number") {
433
+ n = n.max(obj.maximum);
434
+ }
435
+ return description ? n.describe(description) : n;
436
+ }
437
+ case "integer": {
438
+ let n = z.number().int();
439
+ const description = typeof obj.description === "string" ? obj.description : undefined;
440
+ if (typeof obj.minimum === "number") {
441
+ n = n.min(obj.minimum);
442
+ }
443
+ if (typeof obj.maximum === "number") {
444
+ n = n.max(obj.maximum);
445
+ }
446
+ return description ? n.describe(description) : n;
447
+ }
448
+ case "boolean": {
449
+ const b = z.boolean();
450
+ const description = typeof obj.description === "string" ? obj.description : undefined;
451
+ return description ? b.describe(description) : b;
452
+ }
453
+ default: {
454
+ const anySchema = z.any();
455
+ const description = typeof obj.description === "string" ? obj.description : undefined;
456
+ return description ? anySchema.describe(description) : anySchema;
457
+ }
458
+ }
459
+ }
460
+ function addTool(server, name, description, inputSchema, handler) {
461
+ const wrappedHandler = async (args) => {
462
+ try {
463
+ // Validate that args is a valid object
464
+ if (typeof args !== "object" || args === null) {
465
+ throw new Error(`Invalid arguments for tool ${name}: expected object, got ${typeof args}`);
466
+ }
467
+ // Check if shutting down before processing
468
+ if (isShuttingDown) {
469
+ throw new Error(`Tool ${name} rejected: server is shutting down`);
470
+ }
471
+ // Extract or generate requestId from args
472
+ const requestIdArg = args._requestId;
473
+ const requestId = typeof requestIdArg === "string"
474
+ ? requestIdArg
475
+ : `${name}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
476
+ // Log tool call for debugging (to stderr to avoid breaking stdio protocol)
477
+ if (process.env.KIBI_MCP_DEBUG) {
478
+ console.error(`[KIBI-MCP] Tool called: ${name} (requestId: ${requestId}) with args:`, JSON.stringify(args));
479
+ }
480
+ // Track the handler promise in inFlightRequests Map
481
+ const handlerPromise = handler(args);
482
+ inFlightRequests.set(requestId, handlerPromise);
483
+ try {
484
+ // Execute handler
485
+ const result = await handlerPromise;
486
+ return result;
487
+ }
488
+ finally {
489
+ // Always clean up from Map when done (success or failure)
490
+ inFlightRequests.delete(requestId);
491
+ }
492
+ }
493
+ catch (error) {
494
+ const err = error instanceof Error ? error : new Error(String(error));
495
+ console.error(`[KIBI-MCP] Error in tool ${name}:`, err.message);
496
+ if (err.stack) {
497
+ debugLog(`[KIBI-MCP] Tool ${name} stack:`, err.stack);
498
+ }
499
+ throw new Error(`Tool ${name} failed: ${err.message}`, { cause: err });
500
+ }
501
+ };
502
+ server.registerTool(name, { description, inputSchema: jsonSchemaToZod(inputSchema) }, wrappedHandler);
503
+ }
504
+ export async function startServer() {
505
+ loadDefaultEnvFile();
506
+ const server = new McpServer({ name: "kibi-mcp", version: "0.1.0" });
507
+ attachMcpcat(server);
508
+ for (const prompt of PROMPTS) {
509
+ server.prompt(prompt.name, prompt.description, async () => ({
510
+ messages: [
511
+ {
512
+ role: "user",
513
+ content: { type: "text", text: prompt.text },
514
+ },
515
+ ],
516
+ }));
517
+ }
518
+ for (const resource of DOC_RESOURCES) {
519
+ server.resource(resource.name, resource.uri, { description: resource.description, mimeType: resource.mimeType }, async () => ({
520
+ contents: [
521
+ {
522
+ uri: resource.uri,
523
+ mimeType: resource.mimeType,
524
+ text: resource.text,
525
+ },
526
+ ],
527
+ }));
528
+ }
529
+ const toolDef = (name) => {
530
+ const t = TOOLS.find((t) => t.name === name);
531
+ if (!t)
532
+ throw new Error(`Unknown tool: ${name}`);
533
+ return t;
534
+ };
535
+ addTool(server, "kb_query", toolDef("kb_query").description, toolDef("kb_query").inputSchema, async (args) => {
536
+ const prolog = await ensureProlog();
537
+ return handleKbQuery(prolog, args);
538
+ });
539
+ addTool(server, "kb_upsert", toolDef("kb_upsert").description, toolDef("kb_upsert").inputSchema, async (args) => {
540
+ const prolog = await ensureProlog();
541
+ return handleKbUpsert(prolog, args);
542
+ });
543
+ addTool(server, "kb_delete", toolDef("kb_delete").description, toolDef("kb_delete").inputSchema, async (args) => {
544
+ const prolog = await ensureProlog();
545
+ return handleKbDelete(prolog, args);
546
+ });
547
+ addTool(server, "kb_check", toolDef("kb_check").description, toolDef("kb_check").inputSchema, async (args) => {
548
+ const prolog = await ensureProlog();
549
+ return handleKbCheck(prolog, args);
550
+ });
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
+ const transport = new StdioServerTransport();
595
+ transport.onerror = (error) => {
596
+ // Stdio transport surfaces JSON parse / schema validation failures via onerror.
597
+ // Those errors should not crash the server: emit a JSON-RPC error (id omitted)
598
+ // and continue reading subsequent messages.
599
+ if (error.name === "SyntaxError") {
600
+ debugLog("[KIBI-MCP] Parse error from stdin:", error.message);
601
+ void transport
602
+ .send({
603
+ jsonrpc: "2.0",
604
+ error: { code: -32700, message: "Parse error" },
605
+ })
606
+ .catch((sendError) => {
607
+ console.error("[KIBI-MCP] Failed to send parse error response:", sendError);
608
+ initiateGracefulShutdown(1);
609
+ });
610
+ return;
611
+ }
612
+ if (error.name === "ZodError") {
613
+ debugLog("[KIBI-MCP] Invalid JSON-RPC message:", error.message);
614
+ void transport
615
+ .send({
616
+ jsonrpc: "2.0",
617
+ error: { code: -32600, message: "Invalid Request" },
618
+ })
619
+ .catch((sendError) => {
620
+ console.error("[KIBI-MCP] Failed to send invalid request response:", sendError);
621
+ initiateGracefulShutdown(1);
622
+ });
623
+ return;
624
+ }
625
+ console.error(`[KIBI-MCP] Transport error: ${error.message}`, error);
626
+ debugLog("[KIBI-MCP] Transport error stack:", error.stack);
627
+ initiateGracefulShutdown(1);
628
+ };
629
+ transport.onclose = () => {
630
+ debugLog("[KIBI-MCP] Transport closed");
631
+ initiateGracefulShutdown(0);
632
+ };
633
+ await server.connect(transport);
634
+ process.stdout.on("error", (error) => {
635
+ const message = error instanceof Error ? error.message : String(error);
636
+ console.error("[KIBI-MCP] stdout error:", message);
637
+ debugLog("[KIBI-MCP] stdout error detail:", error);
638
+ initiateGracefulShutdown(1);
639
+ });
640
+ process.stderr.on("error", (error) => {
641
+ const message = error instanceof Error ? error.message : String(error);
642
+ try {
643
+ console.error("[KIBI-MCP] stderr error:", message);
644
+ }
645
+ catch { }
646
+ initiateGracefulShutdown(1);
647
+ });
648
+ process.on("SIGTERM", () => {
649
+ debugLog("[KIBI-MCP] Received SIGTERM");
650
+ initiateGracefulShutdown(0);
651
+ });
652
+ process.on("SIGINT", () => {
653
+ debugLog("[KIBI-MCP] Received SIGINT");
654
+ initiateGracefulShutdown(0);
655
+ });
656
+ // Handle stdin EOF/close for clean shutdown when client disconnects
657
+ // Use debugLog so these are only noisy when KIBI_MCP_DEBUG is set.
658
+ try {
659
+ process.stdin.on("end", () => {
660
+ debugLog("[KIBI-MCP] stdin ended");
661
+ // fire-and-forget; initiateGracefulShutdown is idempotent
662
+ void initiateGracefulShutdown(0);
663
+ });
664
+ process.stdin.on("close", () => {
665
+ debugLog("[KIBI-MCP] stdin closed");
666
+ void initiateGracefulShutdown(0);
667
+ });
668
+ }
669
+ catch (e) {
670
+ // Defensive: do not let stdin handler setup throw during startup
671
+ debugLog("[KIBI-MCP] Failed to attach stdin handlers:", e);
672
+ }
673
+ }