kibi-mcp 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,199 @@
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
+ import process from "node:process";
19
+ import { z } from "zod";
20
+ import { TOOLS } from "../tools-config.js";
21
+ import { handleKbCheck } from "../tools/check.js";
22
+ import { handleKbDelete } from "../tools/delete.js";
23
+ import { handleKbQuery } from "../tools/query.js";
24
+ import { handleKbUpsert } from "../tools/upsert.js";
25
+ import { ensureProlog, inFlightRequests, isShuttingDown } from "./session.js";
26
+ const ACTIVE_TOOLS = TOOLS;
27
+ function debugLog(...args) {
28
+ if (process.env.KIBI_MCP_DEBUG) {
29
+ console.error(...args);
30
+ }
31
+ }
32
+ export function jsonSchemaToZod(schema) {
33
+ if (!schema || typeof schema !== "object") {
34
+ return z.any();
35
+ }
36
+ const obj = schema;
37
+ if (Array.isArray(obj.enum) && obj.enum.length > 0) {
38
+ const description = typeof obj.description === "string" ? obj.description : undefined;
39
+ const literals = obj.enum.filter((value) => typeof value === "string" ||
40
+ typeof value === "number" ||
41
+ typeof value === "boolean" ||
42
+ value === null);
43
+ if (literals.length === 0) {
44
+ return description ? z.any().describe(description) : z.any();
45
+ }
46
+ const literalSchemas = literals.map((value) => z.literal(value));
47
+ if (literalSchemas.length === 1) {
48
+ const single = literalSchemas[0];
49
+ return description ? single.describe(description) : single;
50
+ }
51
+ const union = z.union(literalSchemas);
52
+ return description ? union.describe(description) : union;
53
+ }
54
+ const schemaType = typeof obj.type === "string" ? obj.type : undefined;
55
+ switch (schemaType) {
56
+ case "object": {
57
+ const properties = obj.properties && typeof obj.properties === "object"
58
+ ? obj.properties
59
+ : {};
60
+ const required = new Set(Array.isArray(obj.required)
61
+ ? obj.required.filter((k) => typeof k === "string" && k.length > 0)
62
+ : []);
63
+ const shape = {};
64
+ for (const [key, value] of Object.entries(properties)) {
65
+ const propSchema = jsonSchemaToZod(value);
66
+ shape[key] = required.has(key) ? propSchema : propSchema.optional();
67
+ }
68
+ let objectSchema = z.object(shape);
69
+ if (obj.additionalProperties !== false) {
70
+ objectSchema = objectSchema.passthrough();
71
+ }
72
+ const description = typeof obj.description === "string" ? obj.description : undefined;
73
+ return description ? objectSchema.describe(description) : objectSchema;
74
+ }
75
+ case "array": {
76
+ const itemSchema = jsonSchemaToZod(obj.items);
77
+ let arraySchema = z.array(itemSchema);
78
+ const description = typeof obj.description === "string" ? obj.description : undefined;
79
+ if (typeof obj.minItems === "number") {
80
+ arraySchema = arraySchema.min(obj.minItems);
81
+ }
82
+ if (typeof obj.maxItems === "number") {
83
+ arraySchema = arraySchema.max(obj.maxItems);
84
+ }
85
+ return description ? arraySchema.describe(description) : arraySchema;
86
+ }
87
+ case "string": {
88
+ let s = z.string();
89
+ const description = typeof obj.description === "string" ? obj.description : undefined;
90
+ if (typeof obj.minLength === "number") {
91
+ s = s.min(obj.minLength);
92
+ }
93
+ if (typeof obj.maxLength === "number") {
94
+ s = s.max(obj.maxLength);
95
+ }
96
+ return description ? s.describe(description) : s;
97
+ }
98
+ case "number": {
99
+ let n = z.number();
100
+ const description = typeof obj.description === "string" ? obj.description : undefined;
101
+ if (typeof obj.minimum === "number") {
102
+ n = n.min(obj.minimum);
103
+ }
104
+ if (typeof obj.maximum === "number") {
105
+ n = n.max(obj.maximum);
106
+ }
107
+ return description ? n.describe(description) : n;
108
+ }
109
+ case "integer": {
110
+ let n = z.number().int();
111
+ const description = typeof obj.description === "string" ? obj.description : undefined;
112
+ if (typeof obj.minimum === "number") {
113
+ n = n.min(obj.minimum);
114
+ }
115
+ if (typeof obj.maximum === "number") {
116
+ n = n.max(obj.maximum);
117
+ }
118
+ return description ? n.describe(description) : n;
119
+ }
120
+ case "boolean": {
121
+ const b = z.boolean();
122
+ const description = typeof obj.description === "string" ? obj.description : undefined;
123
+ return description ? b.describe(description) : b;
124
+ }
125
+ default: {
126
+ const anySchema = z.any();
127
+ const description = typeof obj.description === "string" ? obj.description : undefined;
128
+ return description ? anySchema.describe(description) : anySchema;
129
+ }
130
+ }
131
+ }
132
+ function addTool(server, name, description, inputSchema, handler) {
133
+ const wrappedHandler = async (args) => {
134
+ try {
135
+ // Validate that args is a valid object
136
+ if (typeof args !== "object" || args === null) {
137
+ throw new Error(`Invalid arguments for tool ${name}: expected object, got ${typeof args}`);
138
+ }
139
+ const businessArgs = args;
140
+ // Check if shutting down before processing
141
+ if (isShuttingDown) {
142
+ throw new Error(`Tool ${name} rejected: server is shutting down`);
143
+ }
144
+ // Extract or generate requestId from args
145
+ const requestIdArg = businessArgs._requestId;
146
+ const requestId = typeof requestIdArg === "string"
147
+ ? requestIdArg
148
+ : `${name}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
149
+ // Log tool call for debugging (to stderr to avoid breaking stdio protocol)
150
+ if (process.env.KIBI_MCP_DEBUG) {
151
+ console.error(`[KIBI-MCP] Tool called: ${name} (requestId: ${requestId}) with args:`, JSON.stringify(businessArgs));
152
+ }
153
+ // Track the handler promise in inFlightRequests Map
154
+ const handlerPromise = handler(businessArgs);
155
+ inFlightRequests.set(requestId, handlerPromise);
156
+ try {
157
+ // Execute handler
158
+ return await handlerPromise;
159
+ }
160
+ finally {
161
+ // Always clean up from Map when done (success or failure)
162
+ inFlightRequests.delete(requestId);
163
+ }
164
+ }
165
+ catch (error) {
166
+ const err = error instanceof Error ? error : new Error(String(error));
167
+ console.error(`[KIBI-MCP] Error in tool ${name}:`, err.message);
168
+ if (err.stack) {
169
+ debugLog(`[KIBI-MCP] Tool ${name} stack:`, err.stack);
170
+ }
171
+ throw new Error(`Tool ${name} failed: ${err.message}`, { cause: err });
172
+ }
173
+ };
174
+ server.registerTool(name, { description, inputSchema: jsonSchemaToZod(inputSchema) }, wrappedHandler);
175
+ }
176
+ export function registerAllTools(server) {
177
+ const toolDef = (name) => {
178
+ const t = ACTIVE_TOOLS.find((t) => t.name === name);
179
+ if (!t)
180
+ throw new Error(`Unknown tool: ${name}`);
181
+ return t;
182
+ };
183
+ addTool(server, "kb_query", toolDef("kb_query").description, toolDef("kb_query").inputSchema, async (args) => {
184
+ const prolog = await ensureProlog();
185
+ return handleKbQuery(prolog, args);
186
+ });
187
+ addTool(server, "kb_upsert", toolDef("kb_upsert").description, toolDef("kb_upsert").inputSchema, async (args) => {
188
+ const prolog = await ensureProlog();
189
+ return handleKbUpsert(prolog, args);
190
+ });
191
+ addTool(server, "kb_delete", toolDef("kb_delete").description, toolDef("kb_delete").inputSchema, async (args) => {
192
+ const prolog = await ensureProlog();
193
+ return handleKbDelete(prolog, args);
194
+ });
195
+ addTool(server, "kb_check", toolDef("kb_check").description, toolDef("kb_check").inputSchema, async (args) => {
196
+ const prolog = await ensureProlog();
197
+ return handleKbCheck(prolog, args);
198
+ });
199
+ }
@@ -0,0 +1,71 @@
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
+ import process from "node:process";
19
+ import { initiateGracefulShutdown } from "./session.js";
20
+ function debugLog(...args) {
21
+ if (process.env.KIBI_MCP_DEBUG) {
22
+ console.error(...args);
23
+ }
24
+ }
25
+ export function setupTransportHandlers(server, transport) {
26
+ transport.onerror = (error) => {
27
+ // Stdio transport surfaces JSON parse / schema validation failures via onerror.
28
+ // Those errors should not crash the server: emit a JSON-RPC error (id omitted)
29
+ // and continue reading subsequent messages.
30
+ if (error.name === "SyntaxError") {
31
+ debugLog("[KIBI-MCP] Parse error from stdin:", error.message);
32
+ void transport
33
+ .send({
34
+ jsonrpc: "2.0",
35
+ error: { code: -32700, message: "Parse error" },
36
+ })
37
+ .catch((sendError) => {
38
+ console.error("[KIBI-MCP] Failed to send parse error response:", sendError);
39
+ initiateGracefulShutdown(1);
40
+ });
41
+ return;
42
+ }
43
+ if (error.name === "ZodError") {
44
+ debugLog("[KIBI-MCP] Invalid JSON-RPC message:", error.message);
45
+ void transport
46
+ .send({
47
+ jsonrpc: "2.0",
48
+ error: { code: -32600, message: "Invalid Request" },
49
+ })
50
+ .catch((sendError) => {
51
+ console.error("[KIBI-MCP] Failed to send invalid request response:", sendError);
52
+ initiateGracefulShutdown(1);
53
+ });
54
+ return;
55
+ }
56
+ console.error(`[KIBI-MCP] Transport error: ${error.message}`, error);
57
+ debugLog("[KIBI-MCP] Transport error stack:", error.stack);
58
+ initiateGracefulShutdown(1);
59
+ };
60
+ transport.onclose = () => {
61
+ debugLog("[KIBI-MCP] Transport closed");
62
+ initiateGracefulShutdown(0);
63
+ };
64
+ process.on("SIGTERM", () => {
65
+ debugLog("[KIBI-MCP] Received SIGTERM, initiating graceful shutdown");
66
+ void initiateGracefulShutdown(0);
67
+ });
68
+ }
69
+ export async function connectTransport(server, transport) {
70
+ await server.connect(transport);
71
+ }