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 +277 -114
- package/dist/tools/check.js +17 -1
- package/dist/tools/context.js +2 -12
- package/dist/tools/symbols.js +10 -3
- package/dist/tools/upsert.js +19 -1
- package/dist/tools-config.js +10 -206
- package/package.json +7 -4
- package/dist/mcpcat.js +0 -129
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
|
|
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
|
|
97
|
-
"- Use
|
|
98
|
-
"-
|
|
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. **
|
|
112
|
-
"2. **
|
|
113
|
-
"3. **
|
|
114
|
-
"4. **
|
|
115
|
-
"5. **
|
|
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
|
|
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
|
|
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
|
-
"-
|
|
148
|
-
"-
|
|
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
|
|
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
|
|
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]
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
541
|
-
|
|
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 =
|
|
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.
|
package/dist/tools/check.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/tools/context.js
CHANGED
|
@@ -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
|
|
19
|
-
const { sourceFile
|
|
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)`;
|
package/dist/tools/symbols.js
CHANGED
|
@@ -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
|
package/dist/tools/upsert.js
CHANGED
|
@@ -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:
|
|
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
|
package/dist/tools-config.js
CHANGED
|
@@ -234,215 +234,19 @@ export const TOOLS = [
|
|
|
234
234
|
properties: {
|
|
235
235
|
rules: {
|
|
236
236
|
type: "array",
|
|
237
|
-
items: {
|
|
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
|
|
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.
|
|
13
|
-
"kibi-core": "^0.1.
|
|
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": [
|
|
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
|
-
}
|