kibi-mcp 0.2.0 → 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 +196 -9
- package/dist/tools/symbols.js +10 -3
- package/dist/tools/upsert.js +19 -1
- package/package.json +7 -4
- package/dist/mcpcat.js +0 -129
package/dist/server.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import fs from "node:fs";
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
|
+
import path from "node:path";
|
|
20
21
|
/*
|
|
21
22
|
How to apply this header to source files (examples)
|
|
22
23
|
|
|
@@ -51,13 +52,64 @@ import { PrologProcess } from "kibi-cli/prolog";
|
|
|
51
52
|
import { copyCleanSnapshot, getBranchDiagnostic, isValidBranchName, resolveActiveBranch, } from "kibi-cli/public/branch-resolver";
|
|
52
53
|
import { z } from "zod";
|
|
53
54
|
import { loadDefaultEnvFile } from "./env.js";
|
|
54
|
-
import { attachMcpcat } from "./mcpcat.js";
|
|
55
55
|
import { TOOLS } from "./tools-config.js";
|
|
56
56
|
import { handleKbCheck } from "./tools/check.js";
|
|
57
57
|
import { handleKbDelete } from "./tools/delete.js";
|
|
58
58
|
import { handleKbQuery } from "./tools/query.js";
|
|
59
59
|
import { handleKbUpsert } from "./tools/upsert.js";
|
|
60
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;
|
|
61
113
|
function renderToolsDoc() {
|
|
62
114
|
const lines = [
|
|
63
115
|
"# kibi-mcp Tools",
|
|
@@ -67,7 +119,7 @@ function renderToolsDoc() {
|
|
|
67
119
|
"| Tool | Summary | Required Parameters |",
|
|
68
120
|
"| --- | --- | --- |",
|
|
69
121
|
];
|
|
70
|
-
for (const tool of
|
|
122
|
+
for (const tool of ACTIVE_TOOLS) {
|
|
71
123
|
const required = Array.isArray(tool.inputSchema?.required)
|
|
72
124
|
? tool.inputSchema.required.join(", ")
|
|
73
125
|
: "none";
|
|
@@ -239,10 +291,70 @@ function getHelpText(topic) {
|
|
|
239
291
|
let prologProcess = null;
|
|
240
292
|
let isInitialized = false;
|
|
241
293
|
let activeBranchName = "develop";
|
|
294
|
+
let ensurePrologTail = Promise.resolve();
|
|
242
295
|
// Shutdown tracking state
|
|
243
296
|
let isShuttingDown = false;
|
|
244
297
|
let shutdownTimeout = null;
|
|
245
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
|
+
}
|
|
246
358
|
function ensureBranchKbExists(workspaceRoot, branch) {
|
|
247
359
|
if (!isValidBranchName(branch)) {
|
|
248
360
|
throw new Error(`Invalid branch name: ${branch}`);
|
|
@@ -309,7 +421,7 @@ async function initiateGracefulShutdown(exitCode = 0) {
|
|
|
309
421
|
// Exit
|
|
310
422
|
process.exit(exitCode);
|
|
311
423
|
}
|
|
312
|
-
async function
|
|
424
|
+
async function ensurePrologUnsafe() {
|
|
313
425
|
const workspaceRoot = resolveWorkspaceRoot();
|
|
314
426
|
// Determine target branch: respect KIBI_BRANCH override or resolve from git
|
|
315
427
|
const envBranch = process.env.KIBI_BRANCH?.trim();
|
|
@@ -410,6 +522,20 @@ async function ensureProlog() {
|
|
|
410
522
|
debugLog(`[KIBI-MCP] KB attached: ${kbPath}`);
|
|
411
523
|
return prologProcess;
|
|
412
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
|
+
}
|
|
413
539
|
function jsonSchemaToZod(schema) {
|
|
414
540
|
if (!schema || typeof schema !== "object") {
|
|
415
541
|
return z.any();
|
|
@@ -512,32 +638,89 @@ function jsonSchemaToZod(schema) {
|
|
|
512
638
|
}
|
|
513
639
|
function addTool(server, name, description, inputSchema, handler) {
|
|
514
640
|
const wrappedHandler = async (args) => {
|
|
641
|
+
let telemetry = null;
|
|
642
|
+
let businessArgs = {};
|
|
643
|
+
const startedAt = new Date();
|
|
515
644
|
try {
|
|
516
645
|
// Validate that args is a valid object
|
|
517
646
|
if (typeof args !== "object" || args === null) {
|
|
518
647
|
throw new Error(`Invalid arguments for tool ${name}: expected object, got ${typeof args}`);
|
|
519
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
|
+
}
|
|
520
657
|
// Check if shutting down before processing
|
|
521
658
|
if (isShuttingDown) {
|
|
522
659
|
throw new Error(`Tool ${name} rejected: server is shutting down`);
|
|
523
660
|
}
|
|
524
661
|
// Extract or generate requestId from args
|
|
525
|
-
const requestIdArg =
|
|
662
|
+
const requestIdArg = businessArgs._requestId;
|
|
526
663
|
const requestId = typeof requestIdArg === "string"
|
|
527
664
|
? requestIdArg
|
|
528
665
|
: `${name}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
529
666
|
// Log tool call for debugging (to stderr to avoid breaking stdio protocol)
|
|
530
667
|
if (process.env.KIBI_MCP_DEBUG) {
|
|
531
|
-
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));
|
|
532
669
|
}
|
|
533
670
|
// Track the handler promise in inFlightRequests Map
|
|
534
|
-
const handlerPromise = handler(
|
|
671
|
+
const handlerPromise = handler(businessArgs);
|
|
535
672
|
inFlightRequests.set(requestId, handlerPromise);
|
|
536
673
|
try {
|
|
537
674
|
// Execute handler
|
|
538
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
|
+
});
|
|
539
703
|
return result;
|
|
540
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
|
+
}
|
|
541
724
|
finally {
|
|
542
725
|
// Always clean up from Map when done (success or failure)
|
|
543
726
|
inFlightRequests.delete(requestId);
|
|
@@ -556,8 +739,12 @@ function addTool(server, name, description, inputSchema, handler) {
|
|
|
556
739
|
}
|
|
557
740
|
export async function startServer() {
|
|
558
741
|
loadDefaultEnvFile();
|
|
559
|
-
|
|
560
|
-
|
|
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" });
|
|
561
748
|
for (const prompt of PROMPTS) {
|
|
562
749
|
server.prompt(prompt.name, prompt.description, async () => ({
|
|
563
750
|
messages: [
|
|
@@ -580,7 +767,7 @@ export async function startServer() {
|
|
|
580
767
|
}));
|
|
581
768
|
}
|
|
582
769
|
const toolDef = (name) => {
|
|
583
|
-
const t =
|
|
770
|
+
const t = ACTIVE_TOOLS.find((t) => t.name === name);
|
|
584
771
|
if (!t)
|
|
585
772
|
throw new Error(`Unknown tool: ${name}`);
|
|
586
773
|
return t;
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "0.2.
|
|
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.2.
|
|
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
|
-
}
|