kibi-mcp 0.1.6 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -15,7 +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
+ import fs from "node:fs";
18
19
  import { createRequire } from "node:module";
20
+ import path from "node:path";
19
21
  /*
20
22
  How to apply this header to source files (examples)
21
23
 
@@ -47,24 +49,67 @@ import process from "node:process";
47
49
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
48
50
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
49
51
  import { PrologProcess } from "kibi-cli/prolog";
52
+ import { copyCleanSnapshot, getBranchDiagnostic, isValidBranchName, resolveActiveBranch, } from "kibi-cli/public/branch-resolver";
50
53
  import { z } from "zod";
51
54
  import { loadDefaultEnvFile } from "./env.js";
52
- import { attachMcpcat } from "./mcpcat.js";
53
55
  import { TOOLS } from "./tools-config.js";
54
- import { handleKbBranchEnsure, handleKbBranchGc, } from "./tools/branch.js";
55
56
  import { handleKbCheck } from "./tools/check.js";
56
- import { handleKbContext } from "./tools/context.js";
57
- import { handleKbCoverageReport, } from "./tools/coverage-report.js";
58
57
  import { handleKbDelete } from "./tools/delete.js";
59
- import { handleKbDerive } from "./tools/derive.js";
60
- import { handleKbImpact } from "./tools/impact.js";
61
- import { handleKbListEntityTypes, handleKbListRelationshipTypes, } from "./tools/list-types.js";
62
- import { handleKbQueryRelationships, } from "./tools/query-relationships.js";
63
58
  import { handleKbQuery } from "./tools/query.js";
64
- import { handleSuggestSharedFacts, } from "./tools/suggest-shared-facts.js";
65
- import { handleKbSymbolsRefresh, } from "./tools/symbols.js";
66
59
  import { handleKbUpsert } from "./tools/upsert.js";
67
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;
68
113
  function renderToolsDoc() {
69
114
  const lines = [
70
115
  "# kibi-mcp Tools",
@@ -74,7 +119,7 @@ function renderToolsDoc() {
74
119
  "| Tool | Summary | Required Parameters |",
75
120
  "| --- | --- | --- |",
76
121
  ];
77
- for (const tool of TOOLS) {
122
+ for (const tool of ACTIVE_TOOLS) {
78
123
  const required = Array.isArray(tool.inputSchema?.required)
79
124
  ? tool.inputSchema.required.join(", ")
80
125
  : "none";
@@ -93,9 +138,9 @@ const PROMPTS = [
93
138
  "",
94
139
  "- Encode requirements as linked facts: `req --constrains--> fact` plus `req --requires_property--> fact`.",
95
140
  "- Reuse canonical fact IDs across requirements; shared constrained facts make contradictions detectable.",
96
- "- Use read tools first (`kb_query`, `kb_query_relationships`, `kbcontext`) to establish context.",
97
- "- Use mutation tools (`kb_upsert`, `kb_delete`, branch tools) only after you can justify the change.",
98
- "- Use inference tools (`kb_derive`, `kb_impact`, `kb_coverage_report`) for deterministic analysis.",
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.",
99
144
  "- Prefer explicit IDs and enum values to avoid invalid parameters.",
100
145
  "- Assume every write can affect downstream traceability queries.",
101
146
  ].join("\n"),
@@ -108,15 +153,13 @@ const PROMPTS = [
108
153
  "",
109
154
  "Follow this sequence for reliable operation:",
110
155
  "",
111
- "1. **Discover**: Call `kb_list_entity_types`/`kb_list_relationship_types` if you are unsure about allowed values.",
112
- "2. **Inspect**: Call `kb_query` or `kbcontext` to confirm current state before any mutation.",
113
- "3. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property`.",
114
- "4. **Validate intent**: If creating links, call `kb_query` for both endpoint IDs first.",
115
- "5. **Mutate**: Call `kb_upsert` for create/update, or `kb_delete` for explicit removals.",
116
- "6. **Verify integrity**: Call `kb_check` after mutations.",
117
- "7. **Assess impact**: Call `kb_impact`, `kb_derive`, or `kb_coverage_report` as needed.",
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.",
118
161
  "",
119
- "If a tool returns empty results, do not assume failure. Re-check filters (type, id, tags, sourceFile, or relationship type).",
162
+ "If a tool returns empty results, do not assume failure. Re-check filters (type, id, tags, sourceFile, limit, or offset).",
120
163
  ].join("\n"),
121
164
  },
122
165
  {
@@ -129,10 +172,8 @@ const PROMPTS = [
129
172
  "",
130
173
  "- `kb_upsert` validates entity and relationship payloads against JSON Schema.",
131
174
  "- `kb_delete` blocks deletion when dependents still reference the entity.",
132
- "- `kb_branch_gc` may permanently remove stale branch KB directories when `dry_run` is `false`.",
133
175
  "- Relationship and rule names are strict enums; unknown values fail validation.",
134
- "- Branch names are sanitized; path traversal patterns are rejected.",
135
- "- `kb_symbols_refresh` can rewrite the symbols manifest unless `dryRun` is enabled.",
176
+ "- Branch KB setup is automatic at server startup; lifecycle maintenance stays outside the public MCP tool surface.",
136
177
  ].join("\n"),
137
178
  },
138
179
  ];
@@ -144,9 +185,8 @@ function registerDocResources() {
144
185
  "",
145
186
  "Scope:",
146
187
  "- Entity CRUD-like operations for KB records",
147
- "- Relationship inspection",
148
- "- Validation and branch KB maintenance",
149
- "- Deterministic inference for traceability and impact analysis",
188
+ "- Validation of KB integrity after changes",
189
+ "- Automatic branch-local attachment for the active workspace",
150
190
  "",
151
191
  "Use this server when you need branch-local, machine-readable project memory.",
152
192
  ].join("\n");
@@ -157,7 +197,7 @@ function registerDocResources() {
157
197
  "",
158
198
  "- `-32602 INVALID_PARAMS`: Tool arguments are missing/invalid. Recover by checking enum values and required fields.",
159
199
  "- `-32601 METHOD_NOT_FOUND`: Unknown MCP method. Recover by using supported methods (`tools/*`, `prompts/*`, `resources/*`).",
160
- "- `-32000 PROLOG_QUERY_FAILED`: Prolog query failed. Recover by validating IDs, rule names, and relationship types.",
200
+ "- `-32000 PROLOG_QUERY_FAILED`: Prolog query failed. Recover by validating IDs, rule names, and branch KB availability.",
161
201
  "- `VALIDATION_ERROR` message: `kb_upsert` payload failed schema checks. Recover by fixing required fields and enum values.",
162
202
  "- Delete blocked by dependents: `kb_delete` detected incoming references. Recover by removing/rewiring relationships first.",
163
203
  "- Empty results: filters may be too strict. Recover by loosening type/id/tags/source filters and retrying.",
@@ -171,20 +211,10 @@ function registerDocResources() {
171
211
  "3. Reuse the same constrained fact ID across related requirements; vary property facts only when semantics differ",
172
212
  '4. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }`',
173
213
  "",
174
- "## Discover requirement coverage gaps",
175
- '1. `kb_query` with `{ "type": "req", "limit": 20 }`',
176
- '2. `kb_coverage_report` with `{ "type": "req" }`',
177
- '3. `kb_derive` with `{ "rule": "coverage_gap" }`',
178
- "",
179
214
  "## Add a requirement and link it to a test",
180
215
  "1. `kb_query` for existing IDs to avoid collisions",
181
216
  "2. `kb_upsert` with entity payload and `relationships` containing `verified_by`",
182
217
  '3. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }`',
183
- "",
184
- "## Safe cleanup of stale branch KBs",
185
- '1. `kb_branch_gc` with `{ "dry_run": true }`',
186
- "2. Review `structuredContent.stale`",
187
- '3. `kb_branch_gc` with `{ "dry_run": false }` only when deletion is intended',
188
218
  ].join("\n");
189
219
  return [
190
220
  {
@@ -261,10 +291,86 @@ function getHelpText(topic) {
261
291
  let prologProcess = null;
262
292
  let isInitialized = false;
263
293
  let activeBranchName = "develop";
294
+ let ensurePrologTail = Promise.resolve();
264
295
  // Shutdown tracking state
265
296
  let isShuttingDown = false;
266
297
  let shutdownTimeout = null;
267
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
+ }
268
374
  function debugLog(...args) {
269
375
  if (process.env.KIBI_MCP_DEBUG) {
270
376
  console.error(...args);
@@ -315,10 +421,56 @@ async function initiateGracefulShutdown(exitCode = 0) {
315
421
  // Exit
316
422
  process.exit(exitCode);
317
423
  }
318
- async function ensureProlog() {
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
319
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}`);
320
471
  return prologProcess;
321
472
  }
473
+ // First initialization
322
474
  debugLog("[KIBI-MCP] Initializing Prolog process...");
323
475
  prologProcess = new PrologProcess({ timeout: 120000 });
324
476
  await prologProcess.start();
@@ -355,33 +507,12 @@ async function ensureProlog() {
355
507
  debugLog("[KIBI-MCP] Failed to create require() for debug lookup:", err.message);
356
508
  }
357
509
  }
358
- const workspaceRoot = resolveWorkspaceRoot();
359
- let branch = process.env.KIBI_BRANCH || "develop";
360
- let gitBranch;
361
- if (!process.env.KIBI_BRANCH) {
362
- try {
363
- const { execSync } = await import("node:child_process");
364
- const detected = execSync("git branch --show-current", {
365
- cwd: workspaceRoot,
366
- encoding: "utf8",
367
- timeout: 3000,
368
- }).trim();
369
- if (detected) {
370
- gitBranch = detected === "master" ? "develop" : detected;
371
- branch = gitBranch;
372
- }
373
- }
374
- catch {
375
- // fall back to develop
376
- }
377
- }
378
510
  debugLog("[KIBI-MCP] Branch selection:");
379
511
  debugLog(`[KIBI-MCP] KIBI_BRANCH env: ${process.env.KIBI_BRANCH || "not set"}`);
380
- debugLog(`[KIBI-MCP] Git branch: ${gitBranch || "n/a"}`);
381
- debugLog(`[KIBI-MCP] Attached to: ${branch}`);
382
- debugLog("[KIBI-MCP] To change branch: set KIBI_BRANCH=<branch> and restart");
383
- activeBranchName = branch;
384
- const kbPath = resolveKbPath(workspaceRoot, branch);
512
+ debugLog(`[KIBI-MCP] Resolved branch: ${targetBranch}`);
513
+ activeBranchName = targetBranch;
514
+ ensureBranchKbExists(workspaceRoot, targetBranch);
515
+ const kbPath = resolveKbPath(workspaceRoot, targetBranch);
385
516
  const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
386
517
  if (!attachResult.success) {
387
518
  throw new Error(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
@@ -391,6 +522,20 @@ async function ensureProlog() {
391
522
  debugLog(`[KIBI-MCP] KB attached: ${kbPath}`);
392
523
  return prologProcess;
393
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
+ }
394
539
  function jsonSchemaToZod(schema) {
395
540
  if (!schema || typeof schema !== "object") {
396
541
  return z.any();
@@ -493,32 +638,89 @@ function jsonSchemaToZod(schema) {
493
638
  }
494
639
  function addTool(server, name, description, inputSchema, handler) {
495
640
  const wrappedHandler = async (args) => {
641
+ let telemetry = null;
642
+ let businessArgs = {};
643
+ const startedAt = new Date();
496
644
  try {
497
645
  // Validate that args is a valid object
498
646
  if (typeof args !== "object" || args === null) {
499
647
  throw new Error(`Invalid arguments for tool ${name}: expected object, got ${typeof args}`);
500
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
+ }
501
657
  // Check if shutting down before processing
502
658
  if (isShuttingDown) {
503
659
  throw new Error(`Tool ${name} rejected: server is shutting down`);
504
660
  }
505
661
  // Extract or generate requestId from args
506
- const requestIdArg = args._requestId;
662
+ const requestIdArg = businessArgs._requestId;
507
663
  const requestId = typeof requestIdArg === "string"
508
664
  ? requestIdArg
509
665
  : `${name}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
510
666
  // Log tool call for debugging (to stderr to avoid breaking stdio protocol)
511
667
  if (process.env.KIBI_MCP_DEBUG) {
512
- console.error(`[KIBI-MCP] Tool called: ${name} (requestId: ${requestId}) with args:`, JSON.stringify(args));
668
+ console.error(`[KIBI-MCP] Tool called: ${name} (requestId: ${requestId}) with args:`, JSON.stringify(businessArgs));
513
669
  }
514
670
  // Track the handler promise in inFlightRequests Map
515
- const handlerPromise = handler(args);
671
+ const handlerPromise = handler(businessArgs);
516
672
  inFlightRequests.set(requestId, handlerPromise);
517
673
  try {
518
674
  // Execute handler
519
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
+ });
520
703
  return result;
521
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
+ }
522
724
  finally {
523
725
  // Always clean up from Map when done (success or failure)
524
726
  inFlightRequests.delete(requestId);
@@ -537,8 +739,12 @@ function addTool(server, name, description, inputSchema, handler) {
537
739
  }
538
740
  export async function startServer() {
539
741
  loadDefaultEnvFile();
540
- const server = new McpServer({ name: "kibi-mcp", version: "0.1.0" });
541
- attachMcpcat(server);
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
+ }
747
+ const server = new McpServer({ name: "kibi-mcp", version: "0.2.1" });
542
748
  for (const prompt of PROMPTS) {
543
749
  server.prompt(prompt.name, prompt.description, async () => ({
544
750
  messages: [
@@ -561,7 +767,7 @@ export async function startServer() {
561
767
  }));
562
768
  }
563
769
  const toolDef = (name) => {
564
- const t = TOOLS.find((t) => t.name === name);
770
+ const t = ACTIVE_TOOLS.find((t) => t.name === name);
565
771
  if (!t)
566
772
  throw new Error(`Unknown tool: ${name}`);
567
773
  return t;
@@ -582,49 +788,6 @@ export async function startServer() {
582
788
  const prolog = await ensureProlog();
583
789
  return handleKbCheck(prolog, args);
584
790
  });
585
- addTool(server, "kb_branch_ensure", toolDef("kb_branch_ensure").description, toolDef("kb_branch_ensure").inputSchema, async (args) => {
586
- const prolog = await ensureProlog();
587
- return handleKbBranchEnsure(prolog, args);
588
- });
589
- addTool(server, "kb_branch_gc", toolDef("kb_branch_gc").description, toolDef("kb_branch_gc").inputSchema, async (args) => {
590
- const prolog = await ensureProlog();
591
- return handleKbBranchGc(prolog, args);
592
- });
593
- addTool(server, "kb_query_relationships", toolDef("kb_query_relationships").description, toolDef("kb_query_relationships").inputSchema, async (args) => {
594
- const prolog = await ensureProlog();
595
- return handleKbQueryRelationships(prolog, args);
596
- });
597
- addTool(server, "kb_derive", toolDef("kb_derive").description, toolDef("kb_derive").inputSchema, async (args) => {
598
- const prolog = await ensureProlog();
599
- return handleKbDerive(prolog, args);
600
- });
601
- addTool(server, "kb_impact", toolDef("kb_impact").description, toolDef("kb_impact").inputSchema, async (args) => {
602
- const prolog = await ensureProlog();
603
- return handleKbImpact(prolog, args);
604
- });
605
- addTool(server, "kb_coverage_report", toolDef("kb_coverage_report").description, toolDef("kb_coverage_report").inputSchema, async (args) => {
606
- const prolog = await ensureProlog();
607
- return handleKbCoverageReport(prolog, args);
608
- });
609
- addTool(server, "kb_symbols_refresh", toolDef("kb_symbols_refresh").description, toolDef("kb_symbols_refresh").inputSchema, async (args) => handleKbSymbolsRefresh(args));
610
- addTool(server, "kb_list_entity_types", toolDef("kb_list_entity_types").description, toolDef("kb_list_entity_types").inputSchema, handleKbListEntityTypes);
611
- addTool(server, "kb_list_relationship_types", toolDef("kb_list_relationship_types").description, toolDef("kb_list_relationship_types").inputSchema, handleKbListRelationshipTypes);
612
- addTool(server, "kbcontext", toolDef("kbcontext").description, toolDef("kbcontext").inputSchema, async (args) => {
613
- const prolog = await ensureProlog();
614
- return handleKbContext(prolog, args, activeBranchName);
615
- });
616
- addTool(server, "get_help", toolDef("get_help").description, toolDef("get_help").inputSchema, async (args) => {
617
- const topic = typeof args?.topic === "string" ? args.topic : undefined;
618
- const text = getHelpText(topic);
619
- return {
620
- content: [{ type: "text", text }],
621
- structuredContent: { topic: topic ?? "overview" },
622
- };
623
- });
624
- addTool(server, "analyze_shared_facts", toolDef("analyze_shared_facts").description, toolDef("analyze_shared_facts").inputSchema, async (args) => {
625
- const prolog = await ensureProlog();
626
- return handleSuggestSharedFacts(prolog, args);
627
- });
628
791
  const transport = new StdioServerTransport();
629
792
  transport.onerror = (error) => {
630
793
  // 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)`;
@@ -44,9 +44,9 @@
44
44
  */
45
45
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
46
46
  import path from "node:path";
47
- import { resolveWorkspaceRoot } from "../workspace.js";
48
- import { enrichSymbolCoordinates, } from "kibi-cli/extractors/symbols-coordinator";
49
47
  import { dump as dumpYAML, load as parseYAML } from "js-yaml";
48
+ import { enrichSymbolCoordinates, } from "kibi-cli/extractors/symbols-coordinator";
49
+ import { resolveWorkspaceRoot } from "../workspace.js";
50
50
  const COMMENT_BLOCK = `# symbols.yaml
51
51
  # AUTHORED fields (edit freely):
52
52
  # id, title, sourceFile, links, status, tags, owner, priority
@@ -177,11 +177,18 @@ export async function refreshCoordinatesForSymbolId(symbolId, workspaceRoot = re
177
177
  }
178
178
  return { refreshed, found: true };
179
179
  }
180
- function resolveManifestPath(workspaceRoot) {
180
+ export function resolveManifestPath(workspaceRoot) {
181
181
  const configPath = path.join(workspaceRoot, ".kb", "config.json");
182
182
  if (existsSync(configPath)) {
183
183
  try {
184
184
  const config = JSON.parse(readFileSync(configPath, "utf8"));
185
+ // Prefer paths.symbols (new standard) over symbolsManifest (legacy)
186
+ if (config.paths?.symbols) {
187
+ return path.isAbsolute(config.paths.symbols)
188
+ ? config.paths.symbols
189
+ : path.resolve(workspaceRoot, config.paths.symbols);
190
+ }
191
+ // Backward compatibility: check legacy symbolsManifest field
185
192
  if (config.symbolsManifest) {
186
193
  return path.isAbsolute(config.symbolsManifest)
187
194
  ? config.symbolsManifest
@@ -118,6 +118,10 @@ export async function handleKbUpsert(prolog, args) {
118
118
  }
119
119
  // Save KB to disk
120
120
  await prolog.query("kb_save");
121
+ let contradictionPairsDetected;
122
+ if (type === "req") {
123
+ contradictionPairsDetected = await detectContradictionPairs(prolog, id);
124
+ }
121
125
  if (type === "symbol") {
122
126
  try {
123
127
  await refreshCoordinatesForSymbolId(id);
@@ -133,13 +137,16 @@ export async function handleKbUpsert(prolog, args) {
133
137
  content: [
134
138
  {
135
139
  type: "text",
136
- text: `Upserted ${id} (${created > 0 ? "created" : "updated"}) with ${relationshipsCreated} relationship(s).`,
140
+ text: contradictionPairsDetected && contradictionPairsDetected > 0
141
+ ? `Upserted ${id} (${created > 0 ? "created" : "updated"}) with ${relationshipsCreated} relationship(s). Contradiction probe detected ${contradictionPairsDetected} potential conflict pair(s).`
142
+ : `Upserted ${id} (${created > 0 ? "created" : "updated"}) with ${relationshipsCreated} relationship(s).`,
137
143
  },
138
144
  ],
139
145
  structuredContent: {
140
146
  created,
141
147
  updated,
142
148
  relationships_created: relationshipsCreated,
149
+ contradiction_pairs_detected: contradictionPairsDetected,
143
150
  },
144
151
  };
145
152
  }
@@ -148,6 +155,17 @@ export async function handleKbUpsert(prolog, args) {
148
155
  throw new Error(`Upsert execution failed: ${message}`);
149
156
  }
150
157
  }
158
+ async function detectContradictionPairs(prolog, reqId) {
159
+ const escaped = escapeAtom(reqId);
160
+ const goal = `aggregate_all(count, (contradicting_reqs(A, B, _), (A = '${escaped}' ; B = '${escaped}' ; A = 'file:///${escaped}' ; B = 'file:///${escaped}')), Count)`;
161
+ const result = await prolog.query(goal);
162
+ if (!result.success) {
163
+ return 0;
164
+ }
165
+ const raw = result.bindings.Count;
166
+ const count = Number(raw);
167
+ return Number.isFinite(count) ? count : 0;
168
+ }
151
169
  /**
152
170
  * Build Prolog property list from entity object
153
171
  * Returns simple Key=Value format without typed literals
@@ -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.6",
3
+ "version": "0.2.1",
4
4
  "dependencies": {
5
5
  "@modelcontextprotocol/sdk": "^1.26.0",
6
6
  "ajv": "^8.18.0",
@@ -9,8 +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-cli": "^0.1.7",
13
- "kibi-core": "^0.1.6",
12
+ "kibi-cli": "^0.2.2",
13
+ "kibi-core": "^0.1.7",
14
14
  "mcpcat": "^0.1.12",
15
15
  "ts-morph": "^23.0.0",
16
16
  "zod": "^4.3.6"
@@ -27,7 +27,10 @@
27
27
  "build": "tsc -p tsconfig.json",
28
28
  "prepack": "npm run build"
29
29
  },
30
- "files": ["dist", "bin"],
30
+ "files": [
31
+ "dist",
32
+ "bin"
33
+ ],
31
34
  "engines": {
32
35
  "node": ">=18",
33
36
  "bun": ">=1.0"
package/dist/mcpcat.js DELETED
@@ -1,129 +0,0 @@
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 { createHash } from "node:crypto";
46
- import fs from "node:fs";
47
- import os from "node:os";
48
- import path from "node:path";
49
- import * as mcpcat from "mcpcat";
50
- import { resolveWorkspaceRoot } from "./workspace.js";
51
- const projectId = (process.env.MCPCAT_PROJECT_ID ?? "").trim();
52
- const trackedIdentity = resolveTrackedIdentity();
53
- /**
54
- * Attach mcpcat analytics tracking to the MCP server.
55
- *
56
- * NOTE ON SESSIONS: With stdio transport, many MCP clients (including OpenCode)
57
- * spawn a new process for each tool call. This means each tool call gets a new
58
- * MCP session ID, resulting in single-tool-call "sessions" in mcpcat.
59
- *
60
- * This is expected behavior for stdio transport - each process IS a different
61
- * session. User identity (via the identify() function) still provides useful
62
- * aggregation across all tool calls from the same user/machine.
63
- *
64
- * For true session aggregation, clients would need to either:
65
- * 1. Use HTTP transport with persistent connections
66
- * 2. Maintain long-lived stdio connections across multiple tool calls
67
- * 3. Implement custom session headers
68
- */
69
- export function attachMcpcat(server) {
70
- if (!projectId) {
71
- return;
72
- }
73
- try {
74
- mcpcat.track(server, projectId, {
75
- identify: async () => trackedIdentity,
76
- enableReportMissing: false, // Don't add get_more_tools tool - it's internal
77
- enableTracing: true,
78
- enableToolCallContext: false, // Don't inject context parameter into tools
79
- });
80
- if (process.env.KIBI_MCP_DEBUG) {
81
- console.error(`[KIBI-MCP] MCPcat tracking enabled for project ${projectId}`);
82
- }
83
- }
84
- catch (error) {
85
- const details = error instanceof Error ? error.message : String(error);
86
- console.error(`[KIBI-MCP] MCPcat tracking attach failed: ${details}`);
87
- }
88
- }
89
- function resolveTrackedIdentity() {
90
- const explicitUserId = readEnv("MCPCAT_USER_ID");
91
- if (explicitUserId) {
92
- return {
93
- userId: explicitUserId,
94
- userName: readEnv("MCPCAT_USER_NAME") ?? "local-operator",
95
- userData: { identitySource: "env" },
96
- };
97
- }
98
- const repoRoot = findRepoRoot(resolveWorkspaceRoot());
99
- const repoName = path.basename(repoRoot);
100
- const username = readEnv("USER") ?? readEnv("USERNAME") ?? "unknown-user";
101
- const host = os.hostname() || "unknown-host";
102
- const stableId = createHash("sha256")
103
- .update(`${host}:${username}:${repoRoot}`)
104
- .digest("hex")
105
- .slice(0, 24);
106
- return {
107
- userId: `anon_${stableId}`,
108
- userName: `local-${repoName}`,
109
- userData: { identitySource: "host-user-repo-hash", repo: repoName },
110
- };
111
- }
112
- function readEnv(name) {
113
- const value = process.env[name]?.trim();
114
- return value ? value : null;
115
- }
116
- function findRepoRoot(startDir) {
117
- let current = path.resolve(startDir);
118
- while (true) {
119
- const gitMarker = path.join(current, ".git");
120
- if (fs.existsSync(gitMarker)) {
121
- return current;
122
- }
123
- const parent = path.dirname(current);
124
- if (parent === current) {
125
- return path.resolve(startDir);
126
- }
127
- current = parent;
128
- }
129
- }