kibi-mcp 2.0.0 → 2.1.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/env.js +21 -30
- package/dist/runtime/mcp-runtime.js +21 -2
- package/dist/server/json-schema-to-zod.js +14 -0
- package/dist/server/tool-registration.js +6 -3
- package/dist/server/tools-runtime.js +22 -15
- package/dist/server/tools.js +72 -29
- package/dist/tools/check-impact.js +1 -1
- package/dist/tools/discovery-adapter.js +30 -7
- package/dist/tools/model-requirement.js +3 -3
- package/dist/tools/semantic-advisor.js +2 -2
- package/dist/tools/suggest-predicates.js +10 -9
- package/dist/workspace.js +7 -33
- package/package.json +4 -8
package/dist/env.js
CHANGED
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
import fs from "node:fs";
|
|
19
|
-
import { resolveEnvFilePath,
|
|
20
|
-
const DEFAULT_ENV_FILE = ".env";
|
|
19
|
+
import { KIBI_PROJECT_ENV_FILE, bootstrapKibiEnvironment, inspectSecretSource, parseEnvContent, resolveEnvFilePath, resolveKibiProjectEnvPath, resolveKibiWorkspaceRoot, } from "kibi-runtime";
|
|
21
20
|
function getEnvValue(key) {
|
|
22
21
|
const value = process.env[key];
|
|
23
22
|
return typeof value === "string" ? value : undefined;
|
|
@@ -26,9 +25,10 @@ function getTrimmedEnvValue(key) {
|
|
|
26
25
|
const value = getEnvValue(key)?.trim();
|
|
27
26
|
return value ? value : undefined;
|
|
28
27
|
}
|
|
28
|
+
/** Preferred project env file name (`.env.kibi` or `KIBI_ENV_FILE` override). */
|
|
29
29
|
export function getEnvFileName() {
|
|
30
30
|
// implements REQ-002
|
|
31
|
-
return getTrimmedEnvValue("KIBI_ENV_FILE") ??
|
|
31
|
+
return getTrimmedEnvValue("KIBI_ENV_FILE") ?? KIBI_PROJECT_ENV_FILE;
|
|
32
32
|
}
|
|
33
33
|
export function isMcpDebugEnabled() {
|
|
34
34
|
// implements REQ-002
|
|
@@ -51,12 +51,26 @@ export function getCoreModulePathOverride(fileName) {
|
|
|
51
51
|
const envKey = `KIBI_${fileName.replace(/\W/g, "_").toUpperCase()}_PATH`;
|
|
52
52
|
return getEnvValue(envKey);
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Harness-independent bootstrap: process → project `.env.kibi`/`KIBI_ENV_FILE`
|
|
56
|
+
* → user `~/.config/kibi/env`, plus legacy `.env` gap-fill.
|
|
57
|
+
* Prefer calling from `startServer()` only.
|
|
58
|
+
*/
|
|
54
59
|
export function loadDefaultEnvFile() {
|
|
55
60
|
// implements REQ-002
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
61
|
+
const result = bootstrapKibiEnvironment();
|
|
62
|
+
const keysLoaded = [
|
|
63
|
+
...result.keysLoadedFromUser,
|
|
64
|
+
...result.keysLoadedFromProject,
|
|
65
|
+
...result.keysLoadedFromLegacy,
|
|
66
|
+
];
|
|
67
|
+
return {
|
|
68
|
+
loaded: keysLoaded.length > 0,
|
|
69
|
+
envFilePath: result.projectEnvPath,
|
|
70
|
+
keysLoaded,
|
|
71
|
+
};
|
|
59
72
|
}
|
|
73
|
+
/** @deprecated Prefer bootstrapKibiEnvironment; kept for targeted file tests. */
|
|
60
74
|
export function loadEnvFile(options) {
|
|
61
75
|
const { envFileName, workspaceRoot } = options;
|
|
62
76
|
const envFilePath = resolveEnvFilePath(envFileName, workspaceRoot);
|
|
@@ -80,27 +94,4 @@ export function loadEnvFile(options) {
|
|
|
80
94
|
return { loaded: false, envFilePath, keysLoaded };
|
|
81
95
|
}
|
|
82
96
|
}
|
|
83
|
-
|
|
84
|
-
const lines = content.split(/\r?\n/);
|
|
85
|
-
const entries = [];
|
|
86
|
-
for (const rawLine of lines) {
|
|
87
|
-
const line = rawLine.trim();
|
|
88
|
-
if (!line || line.startsWith("#")) {
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
const eqIndex = line.indexOf("=");
|
|
92
|
-
if (eqIndex <= 0) {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const key = line.substring(0, eqIndex).trim();
|
|
96
|
-
let value = line.substring(eqIndex + 1).trim();
|
|
97
|
-
if (value.startsWith('"') && value.endsWith('"')) {
|
|
98
|
-
value = value.slice(1, -1);
|
|
99
|
-
}
|
|
100
|
-
else if (value.startsWith("'") && value.endsWith("'")) {
|
|
101
|
-
value = value.slice(1, -1);
|
|
102
|
-
}
|
|
103
|
-
entries.push({ key, value });
|
|
104
|
-
}
|
|
105
|
-
return entries;
|
|
106
|
-
}
|
|
97
|
+
export { resolveEnvFilePath, resolveKibiProjectEnvPath, resolveKibiWorkspaceRoot, inspectSecretSource, };
|
|
@@ -1,19 +1,38 @@
|
|
|
1
|
-
import { resolveBranchAttachment } from "kibi-runtime";
|
|
1
|
+
import { CapabilityRegistryCache, ensureCapabilityRegistry, resolveBranchAttachment, } from "kibi-runtime";
|
|
2
2
|
export function attachedContextWithProlog(withAttachment, prolog) {
|
|
3
3
|
return { ...withAttachment, prolog };
|
|
4
4
|
}
|
|
5
|
+
function resolvePluginsOption(plugins) {
|
|
6
|
+
if (plugins === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (typeof plugins === "function")
|
|
9
|
+
return plugins;
|
|
10
|
+
return async () => plugins;
|
|
11
|
+
}
|
|
5
12
|
// implements REQ-kibi-operation-interface-parity
|
|
6
13
|
export function createMcpRuntime(session) {
|
|
7
14
|
const sessionPrologs = new WeakMap();
|
|
15
|
+
// Per-runtime cache — not a process-global singleton.
|
|
16
|
+
const pluginCache = new CapabilityRegistryCache();
|
|
8
17
|
return {
|
|
9
18
|
open: async (spec, options = {}) => {
|
|
10
19
|
const fs = options.fs ?? session.fs;
|
|
11
20
|
const git = options.git ?? session.git;
|
|
12
21
|
const net = options.net ?? session.net;
|
|
22
|
+
const pluginsOption = options.plugins ?? session.plugins;
|
|
23
|
+
const injectedPlugins = resolvePluginsOption(pluginsOption);
|
|
24
|
+
const workspaceRoot = options.workspaceRoot ?? session.workspaceRoot;
|
|
25
|
+
const ensurePlugins = async () => {
|
|
26
|
+
if (injectedPlugins)
|
|
27
|
+
return injectedPlugins();
|
|
28
|
+
return ensureCapabilityRegistry(workspaceRoot, { cache: pluginCache });
|
|
29
|
+
};
|
|
13
30
|
const context = {
|
|
14
|
-
workspaceRoot
|
|
31
|
+
workspaceRoot,
|
|
15
32
|
signal: options.signal ?? session.signal ?? new AbortController().signal,
|
|
16
33
|
clock: options.clock ?? session.clock ?? (() => new Date()),
|
|
34
|
+
ensurePlugins,
|
|
35
|
+
...(pluginsOption !== undefined ? { plugins: pluginsOption } : {}),
|
|
17
36
|
...(fs ? { fs } : {}),
|
|
18
37
|
...(fs ? { sourceFirst: true } : {}),
|
|
19
38
|
...(git ? { git } : {}),
|
|
@@ -88,6 +88,20 @@ export function jsonSchemaToZod(schema) {
|
|
|
88
88
|
: typeof obj.type === "string"
|
|
89
89
|
? [obj.type]
|
|
90
90
|
: [];
|
|
91
|
+
// JSON Schema anyOf unions become real Zod unions so MCP output
|
|
92
|
+
// validation enforces the declared alternatives instead of silently
|
|
93
|
+
// degrading to z.any() — e.g. kb_check's synchronous payload versus its
|
|
94
|
+
// kibi.job.v1 async receipt. oneOf is intentionally left degrading: its
|
|
95
|
+
// required-guard branches carry no properties and would loosen input
|
|
96
|
+
// validation instead of tightening it.
|
|
97
|
+
const anyOfVariants = Array.isArray(obj.anyOf) ? obj.anyOf : [];
|
|
98
|
+
if (anyOfVariants.length > 0) {
|
|
99
|
+
const variants = anyOfVariants.map((variant) => jsonSchemaToZod(variant));
|
|
100
|
+
const union = z.union(variants);
|
|
101
|
+
return typeof obj.description === "string"
|
|
102
|
+
? union.describe(obj.description)
|
|
103
|
+
: union;
|
|
104
|
+
}
|
|
91
105
|
// JSON Schema nullable fields are represented as a type union in the
|
|
92
106
|
// catalog. Build a real Zod union so MCP publishes and validates the same
|
|
93
107
|
// nullability instead of silently degrading the field to z.any().
|
|
@@ -27,6 +27,9 @@ export function registerConfiguredTools(server, runtime, registerTool) {
|
|
|
27
27
|
name,
|
|
28
28
|
effects: publicSpec.effects,
|
|
29
29
|
requiresProlog: publicSpec.requiresProlog,
|
|
30
|
+
...(publicSpec.agentVisibleStructuredData === true
|
|
31
|
+
? { agentVisibleStructuredData: true }
|
|
32
|
+
: {}),
|
|
30
33
|
execute: (args, context) => execute(context, args),
|
|
31
34
|
};
|
|
32
35
|
registerTool(server, name, definition.description, definition.inputSchema, async () => undefined, runtime, spec, definition.annotations, definition.outputSchema);
|
|
@@ -81,7 +84,7 @@ export function registerConfiguredTools(server, runtime, registerTool) {
|
|
|
81
84
|
});
|
|
82
85
|
register({
|
|
83
86
|
name: "kb_semantic_advisor",
|
|
84
|
-
execute: async (
|
|
87
|
+
execute: async (context, args) => runtime.handleKbSemanticAdvisor(args, context),
|
|
85
88
|
});
|
|
86
89
|
register({
|
|
87
90
|
name: "kb_upsert",
|
|
@@ -110,11 +113,11 @@ export function registerConfiguredTools(server, runtime, registerTool) {
|
|
|
110
113
|
});
|
|
111
114
|
register({
|
|
112
115
|
name: "kb_model_requirement",
|
|
113
|
-
execute: async (context, args) => runtime.handleKbModelRequirement(prologFor(context), args),
|
|
116
|
+
execute: async (context, args) => runtime.handleKbModelRequirement(prologFor(context), args, withSessionProlog(context)),
|
|
114
117
|
});
|
|
115
118
|
register({
|
|
116
119
|
name: "kb_suggest_predicates",
|
|
117
|
-
execute: async (context, args) => runtime.handleKbSuggestPredicates(prologFor(context), args),
|
|
120
|
+
execute: async (context, args) => runtime.handleKbSuggestPredicates(prologFor(context), args, withSessionProlog(context)),
|
|
118
121
|
});
|
|
119
122
|
register({
|
|
120
123
|
name: "kb_plan_bootstrap",
|
|
@@ -49,10 +49,15 @@ function adaptProlog(prolog) {
|
|
|
49
49
|
if (existing) {
|
|
50
50
|
return existing;
|
|
51
51
|
}
|
|
52
|
+
const engine = prolog;
|
|
53
|
+
const queryEntities = engine.queryEntities;
|
|
54
|
+
const searchEntities = engine.searchEntities;
|
|
55
|
+
const storageStatus = engine.storageStatus;
|
|
56
|
+
const queryStatusJson = engine.queryStatusJson;
|
|
52
57
|
let lastResult = null;
|
|
53
58
|
const port = {
|
|
54
|
-
query: async (goal) => {
|
|
55
|
-
lastResult = await
|
|
59
|
+
query: async (goal, signal) => {
|
|
60
|
+
lastResult = await engine.query(goal, signal);
|
|
56
61
|
return lastResult;
|
|
57
62
|
},
|
|
58
63
|
nextSolution: async () => {
|
|
@@ -60,35 +65,37 @@ function adaptProlog(prolog) {
|
|
|
60
65
|
lastResult = null;
|
|
61
66
|
return result;
|
|
62
67
|
},
|
|
63
|
-
save: () =>
|
|
64
|
-
...(typeof
|
|
65
|
-
"function"
|
|
68
|
+
save: (signal) => engine.query("kb_save", signal),
|
|
69
|
+
...(typeof queryEntities === "function"
|
|
66
70
|
? {
|
|
67
|
-
|
|
71
|
+
// The captured method uses .call(engine, ...) below to preserve the
|
|
72
|
+
// EngineClient receiver (unbound queryStatusJson breaks this.command).
|
|
73
|
+
queryEntities: (input, signal) => queryEntities.call(engine, input, signal),
|
|
68
74
|
}
|
|
69
75
|
: {}),
|
|
70
|
-
...(typeof
|
|
71
|
-
"function"
|
|
76
|
+
...(typeof searchEntities === "function"
|
|
72
77
|
? {
|
|
73
|
-
searchEntities: (input) =>
|
|
78
|
+
searchEntities: (input, signal) => searchEntities.call(engine, input, signal),
|
|
74
79
|
}
|
|
75
80
|
: {}),
|
|
76
|
-
...(typeof
|
|
77
|
-
"function"
|
|
81
|
+
...(typeof storageStatus === "function"
|
|
78
82
|
? {
|
|
79
|
-
storageStatus: () =>
|
|
83
|
+
storageStatus: () => storageStatus.call(engine),
|
|
80
84
|
}
|
|
81
85
|
: {}),
|
|
82
|
-
...(typeof
|
|
83
|
-
"function"
|
|
86
|
+
...(typeof queryStatusJson === "function"
|
|
84
87
|
? {
|
|
85
|
-
queryStatusJson: () =>
|
|
88
|
+
queryStatusJson: (signal) => queryStatusJson.call(engine, signal),
|
|
86
89
|
}
|
|
87
90
|
: {}),
|
|
88
91
|
};
|
|
89
92
|
prologPorts.set(prolog, port);
|
|
90
93
|
return port;
|
|
91
94
|
}
|
|
95
|
+
/** Test seam: adapt a session engine so AbortSignal reaches EngineClient/PrologPort methods. */
|
|
96
|
+
export function _adaptPrologForTests(prolog) {
|
|
97
|
+
return adaptProlog(prolog);
|
|
98
|
+
}
|
|
92
99
|
const operationRuntime = createMcpRuntime({
|
|
93
100
|
workspaceRoot: resolveWorkspaceRoot(),
|
|
94
101
|
activeBranchName: async () => (await getSessionModule()).getActiveBranchName(),
|
package/dist/server/tools.js
CHANGED
|
@@ -28,10 +28,32 @@ export { jsonSchemaToZod } from "./json-schema-to-zod.js";
|
|
|
28
28
|
export { _resetSessionModulePromise, _setToolsServerDepsForTests };
|
|
29
29
|
const DEFAULT_TOOL_TIMEOUT_MS = 90_000;
|
|
30
30
|
const TOOL_TIMEOUT_ENV = "KIBI_MCP_TOOL_TIMEOUT_MS";
|
|
31
|
-
// Give cancellation
|
|
32
|
-
//
|
|
33
|
-
//
|
|
31
|
+
// Give cancellation a short bounded grace period, but do not hold the MCP
|
|
32
|
+
// request open for the full five-second shutdown budget when a handler never
|
|
33
|
+
// observes AbortSignal.
|
|
34
34
|
const TOOL_TIMEOUT_GRACE_MS = 100;
|
|
35
|
+
// Mutations that ignore AbortSignal may leave the journal indeterminate. Only
|
|
36
|
+
// then tear down the shared session engine — never on ordinary read timeouts,
|
|
37
|
+
// which would reject sibling tools with "Kibi engine connection closed".
|
|
38
|
+
const TOOL_TIMEOUT_WEDGE_MS = 1_000;
|
|
39
|
+
function isMutationEffect(effects) {
|
|
40
|
+
return (effects?.some((effect) => effect === "kb-write" || effect === "workspace-write") ?? false);
|
|
41
|
+
}
|
|
42
|
+
/** Embed envelope data in content text for opted-in tools (hosts that hide structuredContent).
|
|
43
|
+
* Preserves non-text content parts; only augments/replaces text parts. */
|
|
44
|
+
function withAgentVisibleText(content, data) {
|
|
45
|
+
const textParts = content.filter((part) => part.type === "text" && typeof part.text === "string");
|
|
46
|
+
const nonTextParts = content.filter((part) => part.type !== "text");
|
|
47
|
+
const summary = textParts
|
|
48
|
+
.map((part) => part.text)
|
|
49
|
+
.join(" ")
|
|
50
|
+
.replace(/\s+/g, " ")
|
|
51
|
+
.trim();
|
|
52
|
+
const payload = JSON.stringify(data);
|
|
53
|
+
// Prefer JSON-first so first-line-only host summaries still include machine data.
|
|
54
|
+
const text = summary.length > 0 ? `${payload}\n${summary}` : payload;
|
|
55
|
+
return [{ type: "text", text }, ...nonTextParts];
|
|
56
|
+
}
|
|
35
57
|
// implements REQ-008
|
|
36
58
|
function debugLog(...args) {
|
|
37
59
|
if (isMcpDebugEnabled()) {
|
|
@@ -64,27 +86,28 @@ async function withToolTimeout(toolName, operation, onTimeout) {
|
|
|
64
86
|
const timeoutMs = getToolTimeoutMs();
|
|
65
87
|
let timeout;
|
|
66
88
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
timeout
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.then(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
90
|
+
timeout = setTimeout(() => {
|
|
91
|
+
const error = createToolTimeoutError(toolName, timeoutMs);
|
|
92
|
+
// Do not report the timeout until cancellation/reset has completed
|
|
93
|
+
// and the original request has reached a terminal state. This is
|
|
94
|
+
// especially important for source-first mutations: a caller must
|
|
95
|
+
// never retry while the authoritative journal is still deciding its
|
|
96
|
+
// outcome.
|
|
97
|
+
void onTimeout(error, timeoutMs)
|
|
98
|
+
.then(async () => {
|
|
99
|
+
await Promise.race([
|
|
100
|
+
operation.then(() => undefined, () => undefined),
|
|
101
|
+
new Promise((resolve) => setTimeout(resolve, TOOL_TIMEOUT_GRACE_MS)),
|
|
102
|
+
]);
|
|
103
|
+
})
|
|
104
|
+
.finally(() => reject(error));
|
|
105
|
+
}, timeoutMs);
|
|
106
|
+
});
|
|
107
|
+
// When abort settles `operation` before this timeout promise rejects,
|
|
108
|
+
// the late reject must not become an unhandled rejection.
|
|
109
|
+
timeoutPromise.catch(() => undefined);
|
|
110
|
+
return await Promise.race([operation, timeoutPromise]);
|
|
88
111
|
}
|
|
89
112
|
finally {
|
|
90
113
|
if (timeout) {
|
|
@@ -139,15 +162,30 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
139
162
|
try {
|
|
140
163
|
// Execute handler
|
|
141
164
|
const result = await withToolTimeout(name, handlerPromise, async (error) => {
|
|
142
|
-
resetAttempted = true;
|
|
143
165
|
controller.abort(error);
|
|
166
|
+
// Read/discovery timeouts must cancel in-flight RPCs without
|
|
167
|
+
// terminating the shared session engine. A global resetProlog here
|
|
168
|
+
// rejects sibling tools with "Kibi engine connection closed".
|
|
169
|
+
if (!isMutationEffect(operationSpec.effects)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const settled = await Promise.race([
|
|
173
|
+
handlerPromise.then(() => true, () => true),
|
|
174
|
+
new Promise((resolve) => setTimeout(() => resolve(false), TOOL_TIMEOUT_WEDGE_MS)),
|
|
175
|
+
]);
|
|
176
|
+
if (settled) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
resetAttempted = true;
|
|
144
180
|
try {
|
|
145
181
|
await runtime.resetProlog(`tool timeout: ${name}`);
|
|
146
182
|
resetSucceeded = true;
|
|
147
183
|
}
|
|
148
|
-
catch (
|
|
184
|
+
catch (resetFailure) {
|
|
149
185
|
resetError =
|
|
150
|
-
|
|
186
|
+
resetFailure instanceof Error
|
|
187
|
+
? resetFailure.message
|
|
188
|
+
: String(resetFailure);
|
|
151
189
|
}
|
|
152
190
|
});
|
|
153
191
|
const data = operationData(result);
|
|
@@ -169,8 +207,13 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
169
207
|
typeof result === "object" &&
|
|
170
208
|
!Array.isArray(result) &&
|
|
171
209
|
"content" in result) {
|
|
210
|
+
const withContent = result;
|
|
211
|
+
const content = operationSpec.agentVisibleStructuredData === true
|
|
212
|
+
? withAgentVisibleText(withContent.content, data)
|
|
213
|
+
: withContent.content;
|
|
172
214
|
return {
|
|
173
215
|
...result,
|
|
216
|
+
content,
|
|
174
217
|
structuredContent: envelope,
|
|
175
218
|
};
|
|
176
219
|
}
|
|
@@ -195,7 +238,7 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
195
238
|
});
|
|
196
239
|
}
|
|
197
240
|
if (isToolTimeoutError(error) &&
|
|
198
|
-
operationSpec.effects
|
|
241
|
+
isMutationEffect(operationSpec.effects)) {
|
|
199
242
|
const recoveryActions = [
|
|
200
243
|
{
|
|
201
244
|
operation: "kb_status",
|
|
@@ -313,7 +356,7 @@ function registerJobStatusTool(server) {
|
|
|
313
356
|
content: [{ type: "text", text: JSON.stringify(snapshot) }],
|
|
314
357
|
structuredContent: snapshot,
|
|
315
358
|
};
|
|
316
|
-
}, undefined, undefined,
|
|
359
|
+
}, undefined, undefined, {
|
|
317
360
|
title: "Poll a Kibi background job",
|
|
318
361
|
readOnlyHint: true,
|
|
319
362
|
destructiveHint: false,
|
|
@@ -22,7 +22,7 @@ export function hasImpactOptions(args) {
|
|
|
22
22
|
args.includeWorkingTreeDiff ||
|
|
23
23
|
(args.sourceFiles && args.sourceFiles.length > 0));
|
|
24
24
|
}
|
|
25
|
-
export function analyzeKbCheckImpact(workspaceRoot, args) {
|
|
25
|
+
export async function analyzeKbCheckImpact(workspaceRoot, args) {
|
|
26
26
|
if (!hasImpactOptions(args)) {
|
|
27
27
|
return undefined;
|
|
28
28
|
}
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
2
2
|
export function createDiscoveryContext(prolog, baseContext) {
|
|
3
3
|
// implements REQ-kibi-operation-interface-parity
|
|
4
|
+
// Keep this adapter aligned with tools-runtime adaptProlog: indexed discovery
|
|
5
|
+
// and typed status must reach EngineClient command frames (not query-only
|
|
6
|
+
// fallbacks), and AbortSignal must forward for MCP tool cancellation.
|
|
7
|
+
const engine = prolog;
|
|
8
|
+
const queryEntities = engine.queryEntities;
|
|
9
|
+
const searchEntities = engine.searchEntities;
|
|
10
|
+
const storageStatus = engine.storageStatus;
|
|
11
|
+
const queryStatusJson = engine.queryStatusJson;
|
|
4
12
|
let lastResult = null;
|
|
5
13
|
const mode = prolog
|
|
6
14
|
.useOneShotMode;
|
|
15
|
+
const signal = baseContext?.signal ?? new AbortController().signal;
|
|
7
16
|
const port = {
|
|
8
|
-
query: async (goal) => {
|
|
9
|
-
lastResult = await
|
|
17
|
+
query: async (goal, querySignal) => {
|
|
18
|
+
lastResult = await engine.query(goal, querySignal ?? signal);
|
|
10
19
|
return lastResult;
|
|
11
20
|
},
|
|
12
21
|
oneShotMode: mode === undefined
|
|
@@ -17,18 +26,32 @@ export function createDiscoveryContext(prolog, baseContext) {
|
|
|
17
26
|
lastResult = null;
|
|
18
27
|
return result;
|
|
19
28
|
},
|
|
20
|
-
save: () =>
|
|
21
|
-
...(typeof
|
|
22
|
-
"function"
|
|
29
|
+
save: (saveSignal) => engine.query("kb_save", saveSignal ?? signal),
|
|
30
|
+
...(typeof queryEntities === "function"
|
|
23
31
|
? {
|
|
24
|
-
|
|
32
|
+
queryEntities: (input, querySignal) => queryEntities.call(engine, input, querySignal ?? signal),
|
|
33
|
+
}
|
|
34
|
+
: {}),
|
|
35
|
+
...(typeof searchEntities === "function"
|
|
36
|
+
? {
|
|
37
|
+
searchEntities: (input, querySignal) => searchEntities.call(engine, input, querySignal ?? signal),
|
|
38
|
+
}
|
|
39
|
+
: {}),
|
|
40
|
+
...(typeof storageStatus === "function"
|
|
41
|
+
? {
|
|
42
|
+
storageStatus: () => storageStatus.call(engine),
|
|
43
|
+
}
|
|
44
|
+
: {}),
|
|
45
|
+
...(typeof queryStatusJson === "function"
|
|
46
|
+
? {
|
|
47
|
+
queryStatusJson: (statusSignal) => queryStatusJson.call(engine, statusSignal ?? signal),
|
|
25
48
|
}
|
|
26
49
|
: {}),
|
|
27
50
|
};
|
|
28
51
|
return {
|
|
29
52
|
...(baseContext ?? {}),
|
|
30
53
|
workspaceRoot: baseContext?.workspaceRoot ?? resolveWorkspaceRoot(),
|
|
31
|
-
signal
|
|
54
|
+
signal,
|
|
32
55
|
clock: baseContext?.clock ?? (() => new Date()),
|
|
33
56
|
prolog: port,
|
|
34
57
|
};
|
|
@@ -2,11 +2,11 @@ import { modelRequirementSpec } from "kibi-runtime";
|
|
|
2
2
|
import { estimateNormativeSignalConfidence, extractRequirementClaim, getWorkspaceMigrationWarning, strictWriteSetToApplyPlan, writeSetPrimaryEntityId, } from "kibi-runtime";
|
|
3
3
|
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
4
4
|
export { estimateNormativeSignalConfidence, extractRequirementClaim, getWorkspaceMigrationWarning, strictWriteSetToApplyPlan, writeSetPrimaryEntityId, };
|
|
5
|
-
export async function handleKbModelRequirement(_prolog, args) {
|
|
6
|
-
const
|
|
5
|
+
export async function handleKbModelRequirement(_prolog, args, context) {
|
|
6
|
+
const resolved = context ?? {
|
|
7
7
|
workspaceRoot: resolveWorkspaceRoot(),
|
|
8
8
|
signal: new AbortController().signal,
|
|
9
9
|
clock: () => new Date(),
|
|
10
10
|
};
|
|
11
|
-
return modelRequirementSpec.execute(args,
|
|
11
|
+
return modelRequirementSpec.execute(args, resolved);
|
|
12
12
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { suggestPredicatesSpec } from "kibi-runtime";
|
|
2
2
|
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
3
3
|
import { createDiscoveryContext } from "./discovery-adapter.js";
|
|
4
|
-
export async function handleKbSuggestPredicates(prolog, args) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
export async function handleKbSuggestPredicates(prolog, args, context) {
|
|
5
|
+
const resolved = context ??
|
|
6
|
+
(prolog === null
|
|
7
|
+
? {
|
|
8
|
+
workspaceRoot: resolveWorkspaceRoot(),
|
|
9
|
+
signal: new AbortController().signal,
|
|
10
|
+
clock: () => new Date(),
|
|
11
|
+
}
|
|
12
|
+
: createDiscoveryContext(prolog));
|
|
13
|
+
return suggestPredicatesSpec.execute(args, resolved);
|
|
13
14
|
}
|
package/dist/workspace.js
CHANGED
|
@@ -13,32 +13,17 @@
|
|
|
13
13
|
GNU Affero General Public License for more details.
|
|
14
14
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
|
-
along with this program.
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
|
-
import fs from "node:fs";
|
|
19
18
|
import path from "node:path";
|
|
20
|
-
import { branchStorePath } from "kibi-runtime";
|
|
21
|
-
const WORKSPACE_ENV_KEYS = [
|
|
22
|
-
"KIBI_WORKSPACE",
|
|
23
|
-
"KIBI_PROJECT_ROOT",
|
|
24
|
-
"KIBI_ROOT",
|
|
25
|
-
];
|
|
19
|
+
import { branchStorePath, resolveKibiWorkspaceRoot } from "kibi-runtime";
|
|
26
20
|
const KB_PATH_ENV_KEYS = ["KIBI_KB_PATH", "KB_PATH"];
|
|
27
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Canonical workspace root — delegates to shared resolveKibiWorkspaceRoot.
|
|
23
|
+
*/
|
|
24
|
+
// implements REQ-002, REQ-012, REQ-kibi-env-bootstrap
|
|
28
25
|
export function resolveWorkspaceRoot(startDir = process.cwd()) {
|
|
29
|
-
|
|
30
|
-
if (envRoot) {
|
|
31
|
-
return path.resolve(envRoot);
|
|
32
|
-
}
|
|
33
|
-
const kbRoot = findUpwards(startDir, ".kb");
|
|
34
|
-
if (kbRoot) {
|
|
35
|
-
return kbRoot;
|
|
36
|
-
}
|
|
37
|
-
const gitRoot = findUpwards(startDir, ".git");
|
|
38
|
-
if (gitRoot) {
|
|
39
|
-
return gitRoot;
|
|
40
|
-
}
|
|
41
|
-
return path.resolve(startDir);
|
|
26
|
+
return resolveKibiWorkspaceRoot(startDir);
|
|
42
27
|
}
|
|
43
28
|
// implements REQ-002, REQ-012
|
|
44
29
|
export function resolveKbPath(workspaceRoot, branch) {
|
|
@@ -72,17 +57,6 @@ export function nextAncestorDirectory(current) {
|
|
|
72
57
|
const parent = path.dirname(current);
|
|
73
58
|
return parent === current ? undefined : parent;
|
|
74
59
|
}
|
|
75
|
-
function findUpwards(startDir, marker) {
|
|
76
|
-
let current = path.resolve(startDir);
|
|
77
|
-
while (current !== undefined) {
|
|
78
|
-
const candidate = path.join(current, marker);
|
|
79
|
-
if (fs.existsSync(candidate)) {
|
|
80
|
-
return current;
|
|
81
|
-
}
|
|
82
|
-
current = nextAncestorDirectory(current);
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
60
|
function isBranchPath(p) {
|
|
87
61
|
const parent = path.basename(path.dirname(p));
|
|
88
62
|
return parent === "branches";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
6
6
|
"ajv": "^8.18.0",
|
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
"fast-glob": "^3.2.12",
|
|
10
10
|
"gray-matter": "^4.0.3",
|
|
11
11
|
"js-yaml": "^4.1.0",
|
|
12
|
-
"kibi-runtime": "^2.0.
|
|
13
|
-
"kibi-core": "^0.
|
|
12
|
+
"kibi-runtime": "^2.0.1",
|
|
13
|
+
"kibi-core": "^0.13.0",
|
|
14
14
|
"mcpcat": "^0.1.12",
|
|
15
|
-
"ts-morph": "^23.0.0",
|
|
16
15
|
"zod": "^4.3.6"
|
|
17
16
|
},
|
|
18
17
|
"type": "module",
|
|
@@ -29,10 +28,7 @@
|
|
|
29
28
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
30
29
|
"prepack": "npm run build && node ../../scripts/verify-package-contract.mjs kibi-mcp"
|
|
31
30
|
},
|
|
32
|
-
"files": [
|
|
33
|
-
"dist",
|
|
34
|
-
"bin"
|
|
35
|
-
],
|
|
31
|
+
"files": ["dist", "bin"],
|
|
36
32
|
"engines": {
|
|
37
33
|
"node": ">=22"
|
|
38
34
|
},
|