kibi-mcp 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/docs.js +5 -1
- package/dist/server/tool-registration.js +4 -0
- package/dist/server/tools.js +56 -6
- package/dist/server/workspace-router.js +583 -0
- package/dist/server.js +4 -0
- package/dist/tools/plan-bootstrap.js +1 -1
- package/dist/tools/symbols.js +2 -2
- package/dist/tools-config.js +11 -2
- package/package.json +3 -3
package/dist/server/docs.js
CHANGED
|
@@ -54,9 +54,13 @@ export const PROMPTS = [
|
|
|
54
54
|
"",
|
|
55
55
|
"Inspect `kb_status.bootstrap` and follow its typed `nextAction`. If infrastructure is missing, run `kibi init` before planning.",
|
|
56
56
|
"",
|
|
57
|
+
"## Source interview",
|
|
58
|
+
"",
|
|
59
|
+
"Before planning, list the knowledge sources you can read (your MCP connectors for issue trackers, wikis, and specs, plus in-repo docs) and ask the user which hold product intent and which are authoritative, supporting, or stale. Read the confirmed sources and pass them as `bootstrapContext.knowledgeSources` with cited `bootstrapContext.intentClaims`. Kibi never contacts those sources itself.",
|
|
60
|
+
"",
|
|
57
61
|
"## Preview and Approval",
|
|
58
62
|
"",
|
|
59
|
-
"Call `kb_plan_bootstrap` read-only.
|
|
63
|
+
"Call `kb_plan_bootstrap` read-only. After the source interview, ask only the bounded questions returned when its status is `needs_context`.",
|
|
60
64
|
"",
|
|
61
65
|
"Show the exact returned `structuredContent.plan`, its actions, and canonical hash. Get explicit user approval, then pass that plan object unchanged to `kb_apply_plan`.",
|
|
62
66
|
"",
|
|
@@ -111,6 +111,10 @@ export function registerConfiguredTools(server, runtime, registerTool) {
|
|
|
111
111
|
return startJob("kb_check", () => runtime.handleKbCheck(prologFor(context), checkArgs));
|
|
112
112
|
},
|
|
113
113
|
});
|
|
114
|
+
register({
|
|
115
|
+
name: "kb_prepare_impact_review",
|
|
116
|
+
execute: async (context, args) => getSpec("kb_prepare_impact_review").execute(args, context),
|
|
117
|
+
});
|
|
114
118
|
register({
|
|
115
119
|
name: "kb_model_requirement",
|
|
116
120
|
execute: async (context, args) => runtime.handleKbModelRequirement(prologFor(context), args, withSessionProlog(context)),
|
package/dist/server/tools.js
CHANGED
|
@@ -24,6 +24,7 @@ import { getJob, jobSnapshot } from "./jobs.js";
|
|
|
24
24
|
import { jsonSchemaToZod } from "./json-schema-to-zod.js";
|
|
25
25
|
import { registerConfiguredTools } from "./tool-registration.js";
|
|
26
26
|
import { DEFAULT_TOOLS_RUNTIME, _resetSessionModulePromise, _setToolsServerDepsForTests, } from "./tools-runtime.js";
|
|
27
|
+
import { WORKSPACE_ROOT_ARGUMENT, getWorkspaceRouter, } from "./workspace-router.js";
|
|
27
28
|
export { jsonSchemaToZod } from "./json-schema-to-zod.js";
|
|
28
29
|
export { _resetSessionModulePromise, _setToolsServerDepsForTests };
|
|
29
30
|
const DEFAULT_TOOL_TIMEOUT_MS = 90_000;
|
|
@@ -115,23 +116,72 @@ async function withToolTimeout(toolName, operation, onTimeout) {
|
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* A module that vanished after startup means the package was upgraded or
|
|
121
|
+
* reinstalled under a running server; only a restart loads the new install.
|
|
122
|
+
*/
|
|
123
|
+
function staleInstallHint(error) {
|
|
124
|
+
const code = error.code;
|
|
125
|
+
const missingModule = code === "ERR_MODULE_NOT_FOUND" ||
|
|
126
|
+
code === "MODULE_NOT_FOUND" ||
|
|
127
|
+
/Cannot find (module|package)/.test(error.message);
|
|
128
|
+
return missingModule
|
|
129
|
+
? " — this Kibi MCP server is running from files that are no longer installed (Kibi was likely upgraded or reinstalled after it started). Restart the Kibi MCP server; the project-local kibi CLI works in the meantime."
|
|
130
|
+
: "";
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The decision when no workspace router is installed (unit tests, embedded
|
|
134
|
+
* use): every call is local, and `workspaceRoot` is dropped so handlers never
|
|
135
|
+
* see a routing-only argument. kb_check keeps it: there it also names the
|
|
136
|
+
* tree to inspect for impact diagnostics.
|
|
137
|
+
*/
|
|
138
|
+
function localDecision(name, args) {
|
|
139
|
+
if (!(WORKSPACE_ROOT_ARGUMENT in args) || name === "kb_check") {
|
|
140
|
+
return { kind: "local", args };
|
|
141
|
+
}
|
|
142
|
+
const { [WORKSPACE_ROOT_ARGUMENT]: _ignored, ...rest } = args;
|
|
143
|
+
return { kind: "local", args: rest };
|
|
144
|
+
}
|
|
145
|
+
/** A result's diagnostics name the workspace mismatch when routing fell back. */
|
|
146
|
+
function withWorkspaceNotice(envelope, notice) {
|
|
147
|
+
if (!notice)
|
|
148
|
+
return envelope;
|
|
149
|
+
return { ...envelope, diagnostics: [...envelope.diagnostics, notice] };
|
|
150
|
+
}
|
|
118
151
|
// implements REQ-002
|
|
119
152
|
export function addTool(server, name, description, inputSchema, handler,
|
|
120
153
|
// INTENTIONAL: DEFAULT_TOOLS_RUNTIME is typed as ToolsRuntime<PrologProcess>; the
|
|
121
154
|
// generic TProlog parameter exists so tests can inject a mock type. The cast is safe
|
|
122
155
|
// because the runtime object satisfies the full ToolsRuntime contract at runtime.
|
|
123
156
|
runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
124
|
-
const wrappedHandler = async (
|
|
157
|
+
const wrappedHandler = async (rawArgs) => {
|
|
125
158
|
const startedAt = new Date();
|
|
126
159
|
const diagnosticModeEnabled = runtime.diagnosticModeEnabled();
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
160
|
+
let args = rawArgs;
|
|
161
|
+
let workspaceNotice;
|
|
162
|
+
let businessArgs = rawArgs;
|
|
163
|
+
let telemetry = null;
|
|
130
164
|
try {
|
|
131
165
|
// Validate that args is a valid object
|
|
132
166
|
if (typeof args !== "object" || args === null) {
|
|
133
167
|
throw new Error(`Invalid arguments for tool ${name}: expected object, got ${typeof args}`);
|
|
134
168
|
}
|
|
169
|
+
// Answer from the workspace the caller is working in. A call routed to
|
|
170
|
+
// another workspace's kibi-mcp returns that server's result as is; the
|
|
171
|
+
// child logs its own usage and the parent records nothing for it.
|
|
172
|
+
// Without a router the decision is synchronous, so a plain call reaches
|
|
173
|
+
// its handler without an extra microtask.
|
|
174
|
+
const router = getWorkspaceRouter();
|
|
175
|
+
const routing = router
|
|
176
|
+
? await router.route(name, args)
|
|
177
|
+
: localDecision(name, args);
|
|
178
|
+
if (routing.kind === "remote")
|
|
179
|
+
return routing.result;
|
|
180
|
+
args = routing.args;
|
|
181
|
+
workspaceNotice = routing.notice;
|
|
182
|
+
({ businessArgs, telemetry } = diagnosticModeEnabled
|
|
183
|
+
? runtime.extractToolCallPayload(args)
|
|
184
|
+
: { businessArgs: args, telemetry: null });
|
|
135
185
|
// Check if shutting down before processing
|
|
136
186
|
if (await runtime.isShuttingDown()) {
|
|
137
187
|
throw new Error(`Tool ${name} rejected: server is shutting down`);
|
|
@@ -189,7 +239,7 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
189
239
|
}
|
|
190
240
|
});
|
|
191
241
|
const data = operationData(result);
|
|
192
|
-
const envelope = toKibiResult(operationSpec, data);
|
|
242
|
+
const envelope = withWorkspaceNotice(toKibiResult(operationSpec, data), workspaceNotice);
|
|
193
243
|
// Log usage in diagnostic mode
|
|
194
244
|
if (diagnosticModeEnabled) {
|
|
195
245
|
await appendDiagnosticSuccessUsage({
|
|
@@ -292,7 +342,7 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
292
342
|
if (err.stack) {
|
|
293
343
|
debugLog(`[KIBI-MCP] Tool ${name} stack:`, err.stack);
|
|
294
344
|
}
|
|
295
|
-
throw new Error(`Tool ${name} failed: ${err.message}`, { cause: err });
|
|
345
|
+
throw new Error(`Tool ${name} failed: ${err.message}${staleInstallHint(err)}`, { cause: err });
|
|
296
346
|
}
|
|
297
347
|
};
|
|
298
348
|
server.registerTool(name, {
|
|
@@ -0,0 +1,583 @@
|
|
|
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
|
+
// implements REQ-mcp-workspace-routing
|
|
19
|
+
import { spawnSync } from "node:child_process";
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import { createRequire } from "node:module";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import process from "node:process";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
26
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
27
|
+
import { CallToolResultSchema, RootsListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
28
|
+
/**
|
|
29
|
+
* Workspace routing: answer every tool call from the workspace the caller is
|
|
30
|
+
* working in, not the one this server happened to start in.
|
|
31
|
+
*
|
|
32
|
+
* Hosts start an MCP server once per project and then let the agent work
|
|
33
|
+
* elsewhere, most often in a git worktree. A server attached at startup would
|
|
34
|
+
* answer from the original checkout's branch, and nothing in the result would
|
|
35
|
+
* say so. This router decides, per call, which workspace should answer:
|
|
36
|
+
*
|
|
37
|
+
* 1. an explicit `workspaceRoot` argument (every tool accepts it; host hooks
|
|
38
|
+
* fill it in from the agent's current directory where hooks exist, and any
|
|
39
|
+
* agent can pass its working directory);
|
|
40
|
+
* 2. otherwise the client's MCP roots, when the client declares them;
|
|
41
|
+
* 3. otherwise the workspace this server is attached to.
|
|
42
|
+
*
|
|
43
|
+
* Another workspace is served by a child kibi-mcp started there (the
|
|
44
|
+
* workspace's own project-local install when present, else this server's
|
|
45
|
+
* entry), connected as an MCP client, pooled, and retired when idle. Routing
|
|
46
|
+
* is limited to worktrees of the same repository, directories under the
|
|
47
|
+
* client's roots, and `KIBI_MCP_ROUTABLE_ROOTS`; anything else, and any
|
|
48
|
+
* failure to start the child, falls back to the attached workspace with a
|
|
49
|
+
* `workspace_mismatch` diagnostic in the result, so a wrong-branch answer is
|
|
50
|
+
* never silent. `KIBI_WORKSPACE` (or an alias) pins the server and disables
|
|
51
|
+
* routing; a routed child is itself pinned and never routes further.
|
|
52
|
+
*/
|
|
53
|
+
export const WORKSPACE_ROOT_ARGUMENT = "workspaceRoot";
|
|
54
|
+
export const WORKSPACE_ROOT_SCHEMA = {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Optional absolute path of the workspace this call is about when it differs from the one the server started in, for example the git worktree you are working in. The server answers from that workspace's knowledge base when it is a worktree of the same repository or lies under the client's MCP roots; otherwise it answers from its own workspace and adds a workspace_mismatch diagnostic. Host hooks fill this in automatically where available.",
|
|
57
|
+
};
|
|
58
|
+
/** Mirror of the runtime's workspace env keys; any of them pins the server. */
|
|
59
|
+
const WORKSPACE_PIN_KEYS = [
|
|
60
|
+
"KIBI_WORKSPACE",
|
|
61
|
+
"KIBI_PROJECT_ROOT",
|
|
62
|
+
"KIBI_ROOT",
|
|
63
|
+
];
|
|
64
|
+
const ROUTED_CHILD_ENV = "KIBI_MCP_ROUTED";
|
|
65
|
+
const ROUTING_SWITCH_ENV = "KIBI_MCP_ROUTING";
|
|
66
|
+
const ROUTABLE_ROOTS_ENV = "KIBI_MCP_ROUTABLE_ROOTS";
|
|
67
|
+
const KIBI_MCP_PACKAGE = "kibi-mcp";
|
|
68
|
+
const DEFAULT_MAX_CHILDREN = 4;
|
|
69
|
+
const DEFAULT_IDLE_RETIRE_MS = 10 * 60 * 1000;
|
|
70
|
+
const DEFAULT_ROOTS_TIMEOUT_MS = 2000;
|
|
71
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 30000;
|
|
72
|
+
function isRecord(value) {
|
|
73
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The Kibi workspace that owns `directory`: the nearest ancestor holding
|
|
77
|
+
* `.kb/manifest.json`, without crossing a `.git` boundary so an unrelated
|
|
78
|
+
* enclosing repository never claims the directory.
|
|
79
|
+
*/
|
|
80
|
+
export function kibiWorkspaceFor(directory) {
|
|
81
|
+
let current = path.resolve(directory);
|
|
82
|
+
for (;;) {
|
|
83
|
+
if (fs.existsSync(path.join(current, ".kb", "manifest.json"))) {
|
|
84
|
+
return current;
|
|
85
|
+
}
|
|
86
|
+
if (fs.existsSync(path.join(current, ".git")))
|
|
87
|
+
return null;
|
|
88
|
+
const parent = path.dirname(current);
|
|
89
|
+
if (parent === current)
|
|
90
|
+
return null;
|
|
91
|
+
current = parent;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** The workspace named by MCP roots: the first file root in a Kibi workspace. */
|
|
95
|
+
export function workspaceFromRoots(roots, resolveWorkspace = kibiWorkspaceFor) {
|
|
96
|
+
for (const root of roots) {
|
|
97
|
+
const workspace = resolveWorkspace(root);
|
|
98
|
+
if (workspace)
|
|
99
|
+
return workspace;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
function realpathOrSelf(target) {
|
|
104
|
+
try {
|
|
105
|
+
return fs.realpathSync.native(target);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return path.resolve(target);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function isWithin(parent, candidate) {
|
|
112
|
+
const relative = path.relative(realpathOrSelf(parent), realpathOrSelf(candidate));
|
|
113
|
+
return (relative === "" ||
|
|
114
|
+
(!relative.startsWith("..") && !path.isAbsolute(relative)));
|
|
115
|
+
}
|
|
116
|
+
/** The repository's shared git directory, so worktrees compare equal. */
|
|
117
|
+
export function gitCommonDirectory(directory) {
|
|
118
|
+
const result = spawnSync("git", ["rev-parse", "--git-common-dir"], {
|
|
119
|
+
cwd: directory,
|
|
120
|
+
encoding: "utf8",
|
|
121
|
+
timeout: 5000,
|
|
122
|
+
});
|
|
123
|
+
if (result.status !== 0)
|
|
124
|
+
return null;
|
|
125
|
+
const common = result.stdout.trim();
|
|
126
|
+
if (!common)
|
|
127
|
+
return null;
|
|
128
|
+
return realpathOrSelf(path.resolve(directory, common));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Default routing boundary: worktrees of the attached repository, anything
|
|
132
|
+
* under the client's roots, and operator-listed roots.
|
|
133
|
+
*/
|
|
134
|
+
export function defaultIsRoutable(attachedRoot, env) {
|
|
135
|
+
let attachedCommon;
|
|
136
|
+
const allowlist = (env[ROUTABLE_ROOTS_ENV] ?? "")
|
|
137
|
+
.split(path.delimiter)
|
|
138
|
+
.map((entry) => entry.trim())
|
|
139
|
+
.filter((entry) => entry.length > 0);
|
|
140
|
+
return (workspaceRoot, roots) => {
|
|
141
|
+
if (roots.some((root) => isWithin(root, workspaceRoot)))
|
|
142
|
+
return true;
|
|
143
|
+
if (allowlist.some((root) => isWithin(root, workspaceRoot)))
|
|
144
|
+
return true;
|
|
145
|
+
if (attachedCommon === undefined) {
|
|
146
|
+
attachedCommon = gitCommonDirectory(attachedRoot);
|
|
147
|
+
}
|
|
148
|
+
if (!attachedCommon)
|
|
149
|
+
return false;
|
|
150
|
+
return gitCommonDirectory(workspaceRoot) === attachedCommon;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function readDeclaredBin(packageJsonPath) {
|
|
154
|
+
try {
|
|
155
|
+
const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
156
|
+
if (manifest.name !== KIBI_MCP_PACKAGE)
|
|
157
|
+
return null;
|
|
158
|
+
const bin = typeof manifest.bin === "string"
|
|
159
|
+
? manifest.bin
|
|
160
|
+
: isRecord(manifest.bin) &&
|
|
161
|
+
typeof manifest.bin[KIBI_MCP_PACKAGE] === "string"
|
|
162
|
+
? manifest.bin[KIBI_MCP_PACKAGE]
|
|
163
|
+
: null;
|
|
164
|
+
if (!bin)
|
|
165
|
+
return null;
|
|
166
|
+
const entry = path.resolve(path.dirname(packageJsonPath), bin);
|
|
167
|
+
return fs.existsSync(entry) ? entry : null;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The kibi-mcp to run for a workspace: its own project-local install when it
|
|
175
|
+
* has one (so the child matches that branch's code), else this server's
|
|
176
|
+
* entry. Resolution goes through the package's public entry, since its
|
|
177
|
+
* exports map usually blocks `kibi-mcp/package.json`.
|
|
178
|
+
*/
|
|
179
|
+
export function defaultResolveChildCommand(workspaceRoot) {
|
|
180
|
+
const marker = path.join(workspaceRoot, "package.json");
|
|
181
|
+
if (fs.existsSync(marker)) {
|
|
182
|
+
try {
|
|
183
|
+
const entry = createRequire(marker).resolve(KIBI_MCP_PACKAGE);
|
|
184
|
+
let current = path.dirname(entry);
|
|
185
|
+
for (;;) {
|
|
186
|
+
const bin = readDeclaredBin(path.join(current, "package.json"));
|
|
187
|
+
if (bin)
|
|
188
|
+
return { command: process.execPath, args: [bin] };
|
|
189
|
+
const parent = path.dirname(current);
|
|
190
|
+
if (parent === current)
|
|
191
|
+
break;
|
|
192
|
+
current = parent;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// No project-local install; fall through to this server's own entry.
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const self = process.argv[1];
|
|
200
|
+
if (self && fs.existsSync(self)) {
|
|
201
|
+
return { command: process.execPath, args: [path.resolve(self)] };
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const entry = fileURLToPath(new URL("../../bin/kibi-mcp", import.meta.url));
|
|
205
|
+
if (fs.existsSync(entry))
|
|
206
|
+
return { command: process.execPath, args: [entry] };
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// Not resolvable from this module location.
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function isPinned(env) {
|
|
214
|
+
return WORKSPACE_PIN_KEYS.some((key) => (env[key] ?? "").trim().length > 0);
|
|
215
|
+
}
|
|
216
|
+
function withTimeout(promise, timeoutMs, what) {
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
const timer = setTimeout(() => reject(new Error(`${what} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
219
|
+
promise.then((value) => {
|
|
220
|
+
clearTimeout(timer);
|
|
221
|
+
resolve(value);
|
|
222
|
+
}, (error) => {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
reject(error);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
export class WorkspaceRouter {
|
|
229
|
+
options;
|
|
230
|
+
env;
|
|
231
|
+
attachedRoot;
|
|
232
|
+
resolveWorkspace;
|
|
233
|
+
isRoutable;
|
|
234
|
+
resolveChildCommand;
|
|
235
|
+
log;
|
|
236
|
+
now;
|
|
237
|
+
children = new Map();
|
|
238
|
+
starting = new Map();
|
|
239
|
+
/** Background jobs started in a child, so polls reach the same process. */
|
|
240
|
+
jobOwners = new Map();
|
|
241
|
+
rootsCache = null;
|
|
242
|
+
rootsHandlerInstalled = false;
|
|
243
|
+
shutDown = false;
|
|
244
|
+
constructor(options) {
|
|
245
|
+
this.options = options;
|
|
246
|
+
this.env = options.env ?? process.env;
|
|
247
|
+
this.attachedRoot = realpathOrSelf(options.workspaceRoot);
|
|
248
|
+
this.resolveWorkspace = options.resolveWorkspace ?? kibiWorkspaceFor;
|
|
249
|
+
this.isRoutable =
|
|
250
|
+
options.isRoutable ?? defaultIsRoutable(this.attachedRoot, this.env);
|
|
251
|
+
this.resolveChildCommand =
|
|
252
|
+
options.resolveChildCommand ?? defaultResolveChildCommand;
|
|
253
|
+
this.log =
|
|
254
|
+
options.log ?? ((text) => process.stderr.write(`[kibi-mcp] ${text}\n`));
|
|
255
|
+
this.now = options.now ?? Date.now;
|
|
256
|
+
}
|
|
257
|
+
/** Routing is off for pinned servers, routed children, and by switch. */
|
|
258
|
+
get enabled() {
|
|
259
|
+
if (this.shutDown)
|
|
260
|
+
return false;
|
|
261
|
+
if ((this.env[ROUTING_SWITCH_ENV] ?? "").trim() === "0")
|
|
262
|
+
return false;
|
|
263
|
+
if ((this.env[ROUTED_CHILD_ENV] ?? "").trim() === "1")
|
|
264
|
+
return false;
|
|
265
|
+
return !isPinned(this.env);
|
|
266
|
+
}
|
|
267
|
+
/** Decide where `toolName(args)` is answered; strips `workspaceRoot`. */
|
|
268
|
+
async route(toolName, args) {
|
|
269
|
+
// Callers that never mention a workspace get their argument object back
|
|
270
|
+
// untouched, so handlers see exactly what the client sent.
|
|
271
|
+
if (!(WORKSPACE_ROOT_ARGUMENT in args)) {
|
|
272
|
+
if (!this.enabled)
|
|
273
|
+
return { kind: "local", args };
|
|
274
|
+
if (toolName === "kb_job_status")
|
|
275
|
+
return this.routeJobPoll(args);
|
|
276
|
+
const roots = await this.clientRoots();
|
|
277
|
+
const desired = workspaceFromRoots(roots, this.resolveWorkspace);
|
|
278
|
+
if (!desired)
|
|
279
|
+
return { kind: "local", args };
|
|
280
|
+
return this.routeTo(desired, roots[0] ?? desired, toolName, args);
|
|
281
|
+
}
|
|
282
|
+
const { [WORKSPACE_ROOT_ARGUMENT]: requestedRaw, ...rest } = args;
|
|
283
|
+
const requested = typeof requestedRaw === "string" && requestedRaw.trim().length > 0
|
|
284
|
+
? requestedRaw.trim()
|
|
285
|
+
: undefined;
|
|
286
|
+
if (!this.enabled) {
|
|
287
|
+
if (requested === undefined)
|
|
288
|
+
return { kind: "local", args: rest };
|
|
289
|
+
if (toolName === "kb_check")
|
|
290
|
+
return { kind: "local", args };
|
|
291
|
+
const resolved = this.resolveWorkspace(requested);
|
|
292
|
+
if (resolved && realpathOrSelf(resolved) === this.attachedRoot) {
|
|
293
|
+
return { kind: "local", args: rest };
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
kind: "local",
|
|
297
|
+
args: rest,
|
|
298
|
+
notice: this.notice(requested, resolved, "pinned"),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (toolName === "kb_job_status")
|
|
302
|
+
return this.routeJobPoll(rest);
|
|
303
|
+
if (requested !== undefined) {
|
|
304
|
+
const resolved = this.resolveWorkspace(requested);
|
|
305
|
+
if (!resolved) {
|
|
306
|
+
// kb_check keeps its older meaning for a plain directory: impact
|
|
307
|
+
// diagnostics for that tree, answered by the attached knowledge base.
|
|
308
|
+
if (toolName === "kb_check")
|
|
309
|
+
return { kind: "local", args };
|
|
310
|
+
return {
|
|
311
|
+
kind: "local",
|
|
312
|
+
args: rest,
|
|
313
|
+
notice: this.notice(requested, null, "not_a_kibi_workspace"),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return this.routeTo(resolved, requested, toolName, rest);
|
|
317
|
+
}
|
|
318
|
+
// An empty workspaceRoot means "no preference": fall back to roots.
|
|
319
|
+
const roots = await this.clientRoots();
|
|
320
|
+
const desired = workspaceFromRoots(roots, this.resolveWorkspace);
|
|
321
|
+
if (!desired)
|
|
322
|
+
return { kind: "local", args: rest };
|
|
323
|
+
return this.routeTo(desired, roots[0] ?? desired, toolName, rest);
|
|
324
|
+
}
|
|
325
|
+
/** Polls for a job started in a child go to that child. */
|
|
326
|
+
async routeJobPoll(args) {
|
|
327
|
+
const owner = typeof args.jobId === "string"
|
|
328
|
+
? this.jobOwners.get(args.jobId)
|
|
329
|
+
: undefined;
|
|
330
|
+
const child = owner ? this.children.get(owner) : undefined;
|
|
331
|
+
if (child && !child.closed) {
|
|
332
|
+
return this.callChild(child, "kb_job_status", args);
|
|
333
|
+
}
|
|
334
|
+
return { kind: "local", args };
|
|
335
|
+
}
|
|
336
|
+
async shutdown() {
|
|
337
|
+
this.shutDown = true;
|
|
338
|
+
await Promise.all([...this.children.values()].map((child) => this.retire(child)));
|
|
339
|
+
}
|
|
340
|
+
notice(requested, resolved, reason, error) {
|
|
341
|
+
const why = {
|
|
342
|
+
pinned: "this server is pinned to its workspace by KIBI_WORKSPACE (or an alias) and does not route calls",
|
|
343
|
+
not_a_kibi_workspace: "no Kibi workspace (.kb/manifest.json) owns that directory",
|
|
344
|
+
not_routable: "that workspace is neither a worktree of this server's repository nor under the client's MCP roots or KIBI_MCP_ROUTABLE_ROOTS",
|
|
345
|
+
unavailable: `kibi-mcp could not be started there${error ? `: ${error}` : ""}`,
|
|
346
|
+
}[reason];
|
|
347
|
+
return {
|
|
348
|
+
code: "workspace_mismatch",
|
|
349
|
+
severity: "warning",
|
|
350
|
+
message: `Answered from ${this.attachedRoot}, not from the requested workspace ${requested}: ${why}. Run the Kibi CLI from that directory if you need its knowledge base.`,
|
|
351
|
+
detail: {
|
|
352
|
+
requested,
|
|
353
|
+
resolved,
|
|
354
|
+
answeredFrom: this.attachedRoot,
|
|
355
|
+
reason,
|
|
356
|
+
...(error ? { error } : {}),
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
async routeTo(resolved, requested, toolName, args) {
|
|
361
|
+
const target = realpathOrSelf(resolved);
|
|
362
|
+
if (target === this.attachedRoot)
|
|
363
|
+
return { kind: "local", args };
|
|
364
|
+
const roots = this.rootsCache ?? [];
|
|
365
|
+
if (!this.isRoutable(target, roots)) {
|
|
366
|
+
return {
|
|
367
|
+
kind: "local",
|
|
368
|
+
args,
|
|
369
|
+
notice: this.notice(requested, resolved, "not_routable"),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
let child;
|
|
373
|
+
try {
|
|
374
|
+
child = await this.childFor(target);
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
child = null;
|
|
378
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
379
|
+
return {
|
|
380
|
+
kind: "local",
|
|
381
|
+
args,
|
|
382
|
+
notice: this.notice(requested, resolved, "unavailable", message),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (!child) {
|
|
386
|
+
return {
|
|
387
|
+
kind: "local",
|
|
388
|
+
args,
|
|
389
|
+
notice: this.notice(requested, resolved, "unavailable", "no kibi-mcp entry resolves for that workspace"),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return this.callChild(child, toolName, args);
|
|
393
|
+
}
|
|
394
|
+
async callChild(child, toolName, args) {
|
|
395
|
+
child.inFlight += 1;
|
|
396
|
+
this.touch(child);
|
|
397
|
+
try {
|
|
398
|
+
const result = (await child.client.callTool({ name: toolName, arguments: args }, CallToolResultSchema));
|
|
399
|
+
this.rememberJob(child, result);
|
|
400
|
+
return { kind: "remote", workspaceRoot: child.root, result };
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
child.inFlight -= 1;
|
|
404
|
+
this.touch(child);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
rememberJob(child, result) {
|
|
408
|
+
const structured = result
|
|
409
|
+
.structuredContent;
|
|
410
|
+
const data = isRecord(structured) ? structured.data : undefined;
|
|
411
|
+
const jobId = isRecord(data) ? data.jobId : undefined;
|
|
412
|
+
if (typeof jobId === "string")
|
|
413
|
+
this.jobOwners.set(jobId, child.root);
|
|
414
|
+
}
|
|
415
|
+
/** The client's current roots, cached while the client reports changes. */
|
|
416
|
+
async clientRoots() {
|
|
417
|
+
const capabilities = this.options.server.server.getClientCapabilities();
|
|
418
|
+
if (!capabilities?.roots)
|
|
419
|
+
return [];
|
|
420
|
+
const listChanged = capabilities.roots.listChanged === true;
|
|
421
|
+
if (listChanged && !this.rootsHandlerInstalled) {
|
|
422
|
+
this.rootsHandlerInstalled = true;
|
|
423
|
+
this.options.server.server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
|
|
424
|
+
this.rootsCache = null;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
if (listChanged && this.rootsCache)
|
|
428
|
+
return this.rootsCache;
|
|
429
|
+
try {
|
|
430
|
+
const listed = await withTimeout(this.options.server.server.listRoots(), this.options.rootsTimeoutMs ?? DEFAULT_ROOTS_TIMEOUT_MS, "roots/list");
|
|
431
|
+
const roots = listed.roots.flatMap((root) => {
|
|
432
|
+
try {
|
|
433
|
+
return [fileURLToPath(root.uri)];
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
this.rootsCache = roots;
|
|
440
|
+
return roots;
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
this.log(`roots/list failed; answering from ${this.attachedRoot}: ${error instanceof Error ? error.message : String(error)}`);
|
|
444
|
+
return this.rootsCache ?? [];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async childFor(root) {
|
|
448
|
+
const existing = this.children.get(root);
|
|
449
|
+
if (existing && !existing.closed)
|
|
450
|
+
return existing;
|
|
451
|
+
const pending = this.starting.get(root);
|
|
452
|
+
if (pending)
|
|
453
|
+
return pending;
|
|
454
|
+
const start = this.startChild(root).finally(() => {
|
|
455
|
+
this.starting.delete(root);
|
|
456
|
+
});
|
|
457
|
+
this.starting.set(root, start);
|
|
458
|
+
return start;
|
|
459
|
+
}
|
|
460
|
+
async startChild(root) {
|
|
461
|
+
const command = this.resolveChildCommand(root);
|
|
462
|
+
if (!command)
|
|
463
|
+
return null;
|
|
464
|
+
this.evictIfFull();
|
|
465
|
+
const env = {};
|
|
466
|
+
for (const [key, value] of Object.entries(this.env)) {
|
|
467
|
+
if (typeof value === "string")
|
|
468
|
+
env[key] = value;
|
|
469
|
+
}
|
|
470
|
+
env.KIBI_WORKSPACE = root;
|
|
471
|
+
env[ROUTED_CHILD_ENV] = "1";
|
|
472
|
+
const transport = new StdioClientTransport({
|
|
473
|
+
command: command.command,
|
|
474
|
+
args: [...command.args],
|
|
475
|
+
cwd: root,
|
|
476
|
+
env,
|
|
477
|
+
stderr: "pipe",
|
|
478
|
+
});
|
|
479
|
+
const client = new Client({
|
|
480
|
+
name: "kibi-mcp-workspace-router",
|
|
481
|
+
version: "1",
|
|
482
|
+
});
|
|
483
|
+
const child = {
|
|
484
|
+
root,
|
|
485
|
+
client,
|
|
486
|
+
transport,
|
|
487
|
+
lastUsed: this.now(),
|
|
488
|
+
inFlight: 0,
|
|
489
|
+
idleTimer: null,
|
|
490
|
+
closed: false,
|
|
491
|
+
};
|
|
492
|
+
transport.onclose = () => {
|
|
493
|
+
child.closed = true;
|
|
494
|
+
if (this.children.get(root) === child)
|
|
495
|
+
this.children.delete(root);
|
|
496
|
+
if (child.idleTimer)
|
|
497
|
+
clearTimeout(child.idleTimer);
|
|
498
|
+
};
|
|
499
|
+
await withTimeout(client.connect(transport), this.options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS, `kibi-mcp for ${root} initialize`);
|
|
500
|
+
const label = `[kibi-mcp ${path.basename(root)}]`;
|
|
501
|
+
transport.stderr?.on("data", (chunk) => {
|
|
502
|
+
for (const line of chunk.toString("utf8").split("\n")) {
|
|
503
|
+
if (line.trim().length > 0)
|
|
504
|
+
process.stderr.write(`${label} ${line}\n`);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
this.children.set(root, child);
|
|
508
|
+
this.touch(child);
|
|
509
|
+
this.log(`Serving ${root} through its own kibi-mcp`);
|
|
510
|
+
return child;
|
|
511
|
+
}
|
|
512
|
+
touch(child) {
|
|
513
|
+
child.lastUsed = this.now();
|
|
514
|
+
if (child.idleTimer)
|
|
515
|
+
clearTimeout(child.idleTimer);
|
|
516
|
+
const idleMs = this.options.idleRetireMs ?? DEFAULT_IDLE_RETIRE_MS;
|
|
517
|
+
child.idleTimer = setTimeout(() => {
|
|
518
|
+
if (child.inFlight === 0)
|
|
519
|
+
void this.retire(child);
|
|
520
|
+
}, idleMs);
|
|
521
|
+
child.idleTimer.unref?.();
|
|
522
|
+
}
|
|
523
|
+
evictIfFull() {
|
|
524
|
+
const max = this.options.maxChildren ?? DEFAULT_MAX_CHILDREN;
|
|
525
|
+
while (this.children.size >= max) {
|
|
526
|
+
let oldest = null;
|
|
527
|
+
for (const child of this.children.values()) {
|
|
528
|
+
if (child.inFlight > 0)
|
|
529
|
+
continue;
|
|
530
|
+
if (!oldest || child.lastUsed < oldest.lastUsed)
|
|
531
|
+
oldest = child;
|
|
532
|
+
}
|
|
533
|
+
if (!oldest)
|
|
534
|
+
return;
|
|
535
|
+
void this.retire(oldest);
|
|
536
|
+
this.children.delete(oldest.root);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async retire(child) {
|
|
540
|
+
if (child.closed)
|
|
541
|
+
return;
|
|
542
|
+
child.closed = true;
|
|
543
|
+
if (child.idleTimer)
|
|
544
|
+
clearTimeout(child.idleTimer);
|
|
545
|
+
if (this.children.get(child.root) === child)
|
|
546
|
+
this.children.delete(child.root);
|
|
547
|
+
for (const [jobId, owner] of this.jobOwners) {
|
|
548
|
+
if (owner === child.root)
|
|
549
|
+
this.jobOwners.delete(jobId);
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
await child.client.close();
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
// The child is gone either way.
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
let activeRouter;
|
|
560
|
+
/** Install the router the shared tool handler consults; undefined disables it. */
|
|
561
|
+
export function setWorkspaceRouter(router) {
|
|
562
|
+
activeRouter = router;
|
|
563
|
+
}
|
|
564
|
+
export function getWorkspaceRouter() {
|
|
565
|
+
return activeRouter;
|
|
566
|
+
}
|
|
567
|
+
/** Add `workspaceRoot` to a tool input schema, keeping any existing entry's type. */
|
|
568
|
+
export function withWorkspaceRootSchema(tools) {
|
|
569
|
+
return tools.map((tool) => {
|
|
570
|
+
const schema = tool.inputSchema;
|
|
571
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
572
|
+
return {
|
|
573
|
+
...tool,
|
|
574
|
+
inputSchema: {
|
|
575
|
+
...schema,
|
|
576
|
+
properties: {
|
|
577
|
+
...properties,
|
|
578
|
+
[WORKSPACE_ROOT_ARGUMENT]: WORKSPACE_ROOT_SCHEMA,
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
});
|
|
583
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -24,6 +24,8 @@ import { setupDocsAndPrompts } from "./server/docs.js";
|
|
|
24
24
|
import { KIBI_ICONS } from "./server/icons.js";
|
|
25
25
|
import { registerAllTools } from "./server/tools.js";
|
|
26
26
|
import { connectTransport, setupTransportHandlers, } from "./server/transport.js";
|
|
27
|
+
import { WorkspaceRouter, setWorkspaceRouter, } from "./server/workspace-router.js";
|
|
28
|
+
import { resolveWorkspaceRoot } from "./workspace.js";
|
|
27
29
|
// Read version from package.json to prevent drift
|
|
28
30
|
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
29
31
|
const VERSION = packageJson.version ?? "0.1.0";
|
|
@@ -48,6 +50,8 @@ export async function startServer() {
|
|
|
48
50
|
version: VERSION,
|
|
49
51
|
icons: KIBI_ICONS,
|
|
50
52
|
}, { instructions: SERVER_INSTRUCTIONS });
|
|
53
|
+
// Answer each call from the caller's workspace (worktrees, parallel agents).
|
|
54
|
+
setWorkspaceRouter(new WorkspaceRouter({ server, workspaceRoot: resolveWorkspaceRoot() }));
|
|
51
55
|
// Setup documentation resources and prompts
|
|
52
56
|
setupDocsAndPrompts(server);
|
|
53
57
|
// Register all KB tools
|
|
@@ -7,7 +7,7 @@ function isOperationContext(value) {
|
|
|
7
7
|
"workspaceRoot" in value &&
|
|
8
8
|
"signal" in value);
|
|
9
9
|
}
|
|
10
|
-
// implements REQ-
|
|
10
|
+
// implements REQ-KIBI-BOOTSTRAP-PLAN, REQ-kibi-operation-interface-parity
|
|
11
11
|
// implements REQ-KIBI-BOOTSTRAP-PLAN
|
|
12
12
|
export async function handleKbPlanBootstrap(input, context) {
|
|
13
13
|
if (!isOperationContext(context))
|
package/dist/tools/symbols.js
CHANGED
|
@@ -74,7 +74,7 @@ async function handleKbSymbolsRefreshUnlocked(args, workspaceRoot) {
|
|
|
74
74
|
id: typeof entry.id === "string" ? entry.id : "",
|
|
75
75
|
title: typeof entry.title === "string" ? entry.title : "",
|
|
76
76
|
}));
|
|
77
|
-
const enriched = await enrichSymbolCoordinates(entriesForEnrichment, workspaceRoot);
|
|
77
|
+
const enriched = await enrichSymbolCoordinates(entriesForEnrichment, workspaceRoot, { allowPythonDecoratorCoordinates: true });
|
|
78
78
|
const finalized = await Promise.all(enriched.map((entry, index) => fillMissingCoordinates(original[index] ?? {}, entry, workspaceRoot)));
|
|
79
79
|
parsed.symbols = finalized;
|
|
80
80
|
let refreshed = 0;
|
|
@@ -148,7 +148,7 @@ async function refreshCoordinatesForSymbolIdUnlocked(symbolId, workspaceRoot) {
|
|
|
148
148
|
? original.title
|
|
149
149
|
: "",
|
|
150
150
|
};
|
|
151
|
-
const [enriched] = await enrichSymbolCoordinates([singleEntry], workspaceRoot);
|
|
151
|
+
const [enriched] = await enrichSymbolCoordinates([singleEntry], workspaceRoot, { allowPythonDecoratorCoordinates: true });
|
|
152
152
|
const finalized = await fillMissingCoordinates(original, enriched ?? singleEntry, workspaceRoot);
|
|
153
153
|
symbols[index] = finalized;
|
|
154
154
|
parsed.symbols = symbols;
|
package/dist/tools-config.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { getSpec } from "kibi-runtime";
|
|
19
19
|
import { DIAGNOSTIC_MODE_ENABLED, DIAGNOSTIC_TELEMETRY_SCHEMA, } from "./diagnostics.js";
|
|
20
|
+
import { withWorkspaceRootSchema } from "./server/workspace-router.js";
|
|
20
21
|
const MCP_TOOL_ORDER = [
|
|
21
22
|
"kb_query",
|
|
22
23
|
"kb_search",
|
|
@@ -33,6 +34,7 @@ const MCP_TOOL_ORDER = [
|
|
|
33
34
|
"kb_validate_upsert",
|
|
34
35
|
"kb_delete",
|
|
35
36
|
"kb_check",
|
|
37
|
+
"kb_prepare_impact_review",
|
|
36
38
|
"kb_model_requirement",
|
|
37
39
|
"kb_suggest_predicates",
|
|
38
40
|
"kb_plan_bootstrap",
|
|
@@ -114,6 +116,13 @@ const TOOL_ANNOTATIONS = {
|
|
|
114
116
|
kb_check: {
|
|
115
117
|
title: "Validate Kibi knowledge base",
|
|
116
118
|
},
|
|
119
|
+
kb_prepare_impact_review: {
|
|
120
|
+
title: "Prepare an impact review",
|
|
121
|
+
readOnlyHint: true,
|
|
122
|
+
destructiveHint: false,
|
|
123
|
+
idempotentHint: true,
|
|
124
|
+
openWorldHint: false,
|
|
125
|
+
},
|
|
117
126
|
kb_model_requirement: {
|
|
118
127
|
title: "Model a Kibi requirement",
|
|
119
128
|
},
|
|
@@ -192,6 +201,6 @@ export function withDiagnosticTelemetrySchema(tools) {
|
|
|
192
201
|
* Active tools list.
|
|
193
202
|
* In diagnostic mode, all tools include the _diagnostic_telemetry parameter.
|
|
194
203
|
*/
|
|
195
|
-
export const TOOLS = DIAGNOSTIC_MODE_ENABLED
|
|
204
|
+
export const TOOLS = withWorkspaceRootSchema(DIAGNOSTIC_MODE_ENABLED
|
|
196
205
|
? withDiagnosticTelemetrySchema(BASE_TOOLS)
|
|
197
|
-
: [...BASE_TOOLS];
|
|
206
|
+
: [...BASE_TOOLS]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"mcpName": "io.github.Looted/kibi-mcp",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"fast-glob": "^3.2.12",
|
|
11
11
|
"gray-matter": "^4.0.3",
|
|
12
12
|
"js-yaml": "^4.1.0",
|
|
13
|
-
"kibi-runtime": "^2.0
|
|
14
|
-
"kibi-core": "^0.14.
|
|
13
|
+
"kibi-runtime": "^2.2.0",
|
|
14
|
+
"kibi-core": "^0.14.1",
|
|
15
15
|
"mcpcat": "^0.1.12",
|
|
16
16
|
"zod": "^4.3.6"
|
|
17
17
|
},
|