kibi-mcp 3.1.2 → 3.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.
|
@@ -64,6 +64,7 @@ export function createMcpRuntime(session) {
|
|
|
64
64
|
...(fs ? { sourceFirst: true } : {}),
|
|
65
65
|
...(git ? { git } : {}),
|
|
66
66
|
...(net ? { net } : {}),
|
|
67
|
+
...(options.onProgress ? { onProgress: options.onProgress } : {}),
|
|
67
68
|
};
|
|
68
69
|
const resolved = resolveMcpAttachment(context.workspaceRoot, spec);
|
|
69
70
|
const attachment = resolved instanceof Promise ? await resolved : resolved;
|
|
@@ -13,23 +13,92 @@ export function singleLiteralOrAny(literalSchemas, description) {
|
|
|
13
13
|
}
|
|
14
14
|
return description ? single.describe(description) : single;
|
|
15
15
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
16
|
+
function matchesJsonType(value, type) {
|
|
17
|
+
switch (type) {
|
|
18
|
+
case "object":
|
|
19
|
+
return (value !== null && typeof value === "object" && !Array.isArray(value));
|
|
20
|
+
case "array":
|
|
21
|
+
return Array.isArray(value);
|
|
22
|
+
case "string":
|
|
23
|
+
return typeof value === "string";
|
|
24
|
+
case "number":
|
|
25
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
26
|
+
case "integer":
|
|
27
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
28
|
+
case "boolean":
|
|
29
|
+
return typeof value === "boolean";
|
|
30
|
+
case "null":
|
|
31
|
+
return value === null;
|
|
32
|
+
default:
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function sameJsonValue(left, right) {
|
|
37
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
38
|
+
}
|
|
39
|
+
// Evaluates a JSON Schema `if` condition the way a JSON Schema validator
|
|
40
|
+
// would for the keywords Kibi's operation schemas use in conditionals:
|
|
41
|
+
// boolean schemas, type, const, enum, required, properties (applied only to
|
|
42
|
+
// present keys), not, anyOf, allOf and oneOf. Keywords outside this set do not
|
|
43
|
+
// constrain the condition. The CLI validates the same schemas with ajv, so
|
|
44
|
+
// this must stay faithful: a mismatch makes MCP reject payloads the CLI
|
|
45
|
+
// accepts (or the reverse).
|
|
46
|
+
// implements REQ-002, REQ-mcp-conditional-input-validation
|
|
47
|
+
export function matchesJsonSchemaCondition(value, schema) {
|
|
48
|
+
if (schema === true || schema === undefined)
|
|
49
|
+
return true;
|
|
50
|
+
if (schema === false)
|
|
18
51
|
return false;
|
|
52
|
+
if (schema === null || typeof schema !== "object")
|
|
53
|
+
return true;
|
|
19
54
|
const condition = schema;
|
|
20
|
-
const
|
|
21
|
-
? condition.
|
|
22
|
-
:
|
|
23
|
-
|
|
24
|
-
|
|
55
|
+
const types = Array.isArray(condition.type)
|
|
56
|
+
? condition.type.filter((entry) => typeof entry === "string")
|
|
57
|
+
: typeof condition.type === "string"
|
|
58
|
+
? [condition.type]
|
|
59
|
+
: [];
|
|
60
|
+
if (types.length > 0 && !types.some((type) => matchesJsonType(value, type))) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (Object.hasOwn(condition, "const") &&
|
|
64
|
+
!sameJsonValue(value, condition.const)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(condition.enum) &&
|
|
68
|
+
!condition.enum.some((entry) => sameJsonValue(value, entry))) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const isObject = value !== null && typeof value === "object" && !Array.isArray(value);
|
|
72
|
+
if (isObject) {
|
|
73
|
+
const record = value;
|
|
74
|
+
const required = Array.isArray(condition.required)
|
|
75
|
+
? condition.required.filter((key) => typeof key === "string" && key.length > 0)
|
|
76
|
+
: [];
|
|
77
|
+
if (!required.every((key) => Object.hasOwn(record, key)))
|
|
78
|
+
return false;
|
|
79
|
+
if (condition.properties && typeof condition.properties === "object") {
|
|
80
|
+
for (const [key, propertySchema] of Object.entries(condition.properties)) {
|
|
81
|
+
if (Object.hasOwn(record, key) &&
|
|
82
|
+
!matchesJsonSchemaCondition(record[key], propertySchema)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if ("not" in condition && matchesJsonSchemaCondition(value, condition.not)) {
|
|
25
89
|
return false;
|
|
26
90
|
}
|
|
27
91
|
if (Array.isArray(condition.anyOf) &&
|
|
28
|
-
!condition.anyOf.some((entry) =>
|
|
92
|
+
!condition.anyOf.some((entry) => matchesJsonSchemaCondition(value, entry))) {
|
|
29
93
|
return false;
|
|
30
94
|
}
|
|
31
95
|
if (Array.isArray(condition.allOf) &&
|
|
32
|
-
!condition.allOf.every((entry) =>
|
|
96
|
+
!condition.allOf.every((entry) => matchesJsonSchemaCondition(value, entry))) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(condition.oneOf) &&
|
|
100
|
+
condition.oneOf.filter((entry) => matchesJsonSchemaCondition(value, entry))
|
|
101
|
+
.length !== 1) {
|
|
33
102
|
return false;
|
|
34
103
|
}
|
|
35
104
|
return true;
|
|
@@ -40,7 +109,7 @@ function conditionalRequiredKeys(value, condition) {
|
|
|
40
109
|
const rule = condition;
|
|
41
110
|
if (!("if" in rule) || !("then" in rule))
|
|
42
111
|
return [];
|
|
43
|
-
if (!
|
|
112
|
+
if (!matchesJsonSchemaCondition(value, rule.if))
|
|
44
113
|
return [];
|
|
45
114
|
if (rule.then === null || typeof rule.then !== "object")
|
|
46
115
|
return [];
|
|
@@ -150,9 +150,33 @@ export function registerConfiguredTools(server, runtime, registerTool) {
|
|
|
150
150
|
});
|
|
151
151
|
register({
|
|
152
152
|
name: "kb_apply_plan",
|
|
153
|
-
execute: async (context, args) =>
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
execute: async (context, args) => {
|
|
154
|
+
const { async: asyncMode, ...applyArgs } = args;
|
|
155
|
+
const apply = (applyContext) => runtime.handleKbApplyPlan
|
|
156
|
+
? runtime.handleKbApplyPlan(applyArgs, applyContext)
|
|
157
|
+
: executeApplyPlan(applyArgs, applyContext);
|
|
158
|
+
// Without a registered kb_job_status nothing could poll the receipt,
|
|
159
|
+
// so async falls back to a synchronous apply (which still reports
|
|
160
|
+
// progress when the request carries a progressToken).
|
|
161
|
+
if (asyncMode !== true || !enabledOptionalTools().has("kb_job_status")) {
|
|
162
|
+
return apply(context);
|
|
163
|
+
}
|
|
164
|
+
// A large bootstrap plan can outlast any client request timeout.
|
|
165
|
+
// Detach it into a job and return the kibi.job.v1 receipt; the agent
|
|
166
|
+
// polls kb_job_status. The request is over once the receipt returns,
|
|
167
|
+
// so the job reports no progress and refreshes the branch stamp itself.
|
|
168
|
+
const { onProgress: _requestProgress, ...jobContext } = context;
|
|
169
|
+
return startJob("kb_apply_plan", async () => {
|
|
170
|
+
const result = await apply(jobContext);
|
|
171
|
+
await runtime.operationRuntime.afterSuccess({
|
|
172
|
+
name: "kb_apply_plan",
|
|
173
|
+
effects: getSpec("kb_apply_plan").effects,
|
|
174
|
+
requiresProlog: true,
|
|
175
|
+
execute: async () => undefined,
|
|
176
|
+
}, jobContext);
|
|
177
|
+
return result;
|
|
178
|
+
});
|
|
179
|
+
},
|
|
156
180
|
});
|
|
157
181
|
register({
|
|
158
182
|
name: "kb_ingest_proof",
|
package/dist/server/tools.js
CHANGED
|
@@ -85,12 +85,14 @@ function isToolTimeoutError(value) {
|
|
|
85
85
|
value.code === "KIBI_TOOL_TIMEOUT");
|
|
86
86
|
}
|
|
87
87
|
// implements REQ-002
|
|
88
|
-
async function withToolTimeout(toolName, operation, onTimeout) {
|
|
88
|
+
async function withToolTimeout(toolName, operation, onTimeout, activity) {
|
|
89
89
|
const timeoutMs = getToolTimeoutMs();
|
|
90
90
|
let timeout;
|
|
91
|
+
let fired = false;
|
|
91
92
|
try {
|
|
92
93
|
const timeoutPromise = new Promise((_, reject) => {
|
|
93
|
-
|
|
94
|
+
const fire = () => {
|
|
95
|
+
fired = true;
|
|
94
96
|
const error = createToolTimeoutError(toolName, timeoutMs);
|
|
95
97
|
// Do not report the timeout until cancellation/reset has completed
|
|
96
98
|
// and the original request has reached a terminal state. This is
|
|
@@ -105,7 +107,17 @@ async function withToolTimeout(toolName, operation, onTimeout) {
|
|
|
105
107
|
]);
|
|
106
108
|
})
|
|
107
109
|
.finally(() => reject(error));
|
|
108
|
-
}
|
|
110
|
+
};
|
|
111
|
+
timeout = setTimeout(fire, timeoutMs);
|
|
112
|
+
if (activity) {
|
|
113
|
+
activity.touch = () => {
|
|
114
|
+
if (fired)
|
|
115
|
+
return;
|
|
116
|
+
if (timeout)
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
timeout = setTimeout(fire, timeoutMs);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
109
121
|
});
|
|
110
122
|
// When abort settles `operation` before this timeout promise rejects,
|
|
111
123
|
// the late reject must not become an unhandled rejection.
|
|
@@ -118,6 +130,35 @@ async function withToolTimeout(toolName, operation, onTimeout) {
|
|
|
118
130
|
}
|
|
119
131
|
}
|
|
120
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Forward operation progress as MCP notifications/progress when the request
|
|
135
|
+
* carries a progressToken, and push the server-side tool timeout back on each
|
|
136
|
+
* report. Progress values must increase, so repeats are nudged forward.
|
|
137
|
+
*/
|
|
138
|
+
// implements REQ-bootstrap-apply-long-running
|
|
139
|
+
export function progressReporter(extra, activity) {
|
|
140
|
+
const token = extra?._meta?.progressToken;
|
|
141
|
+
const send = extra?.sendNotification;
|
|
142
|
+
if (token === undefined || typeof send !== "function")
|
|
143
|
+
return undefined;
|
|
144
|
+
let last = -1;
|
|
145
|
+
return ({ progress, total, message }) => {
|
|
146
|
+
activity.touch();
|
|
147
|
+
const value = progress > last ? progress : last + 0.001;
|
|
148
|
+
last = value;
|
|
149
|
+
void send({
|
|
150
|
+
method: "notifications/progress",
|
|
151
|
+
params: {
|
|
152
|
+
progressToken: token,
|
|
153
|
+
progress: value,
|
|
154
|
+
...(total === undefined ? {} : { total }),
|
|
155
|
+
...(message === undefined ? {} : { message }),
|
|
156
|
+
},
|
|
157
|
+
}).catch((error) => {
|
|
158
|
+
debugLog("[KIBI-MCP] progress notification failed:", error instanceof Error ? error.message : String(error));
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
}
|
|
121
162
|
/**
|
|
122
163
|
* A module that vanished after startup means the package was upgraded or
|
|
123
164
|
* reinstalled under a running server; only a restart loads the new install.
|
|
@@ -177,7 +218,7 @@ export function addTool(server, name, description, inputSchema, handler,
|
|
|
177
218
|
// generic TProlog parameter exists so tests can inject a mock type. The cast is safe
|
|
178
219
|
// because the runtime object satisfies the full ToolsRuntime contract at runtime.
|
|
179
220
|
runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
180
|
-
const wrappedHandler = async (rawArgs) => {
|
|
221
|
+
const wrappedHandler = async (rawArgs, extra) => {
|
|
181
222
|
const startedAt = new Date();
|
|
182
223
|
const diagnosticModeEnabled = runtime.diagnosticModeEnabled();
|
|
183
224
|
let args = rawArgs;
|
|
@@ -228,7 +269,12 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
228
269
|
execute: async (input, _context) => handler(input),
|
|
229
270
|
};
|
|
230
271
|
const observed = observeDetachedReads(runtime.operationRuntime);
|
|
231
|
-
const
|
|
272
|
+
const activity = { touch: () => undefined };
|
|
273
|
+
const onProgress = progressReporter(extra, activity);
|
|
274
|
+
const handlerPromise = executeOperation(observed.runtime, operationSpec, businessArgs, {
|
|
275
|
+
signal: controller.signal,
|
|
276
|
+
...(onProgress ? { onProgress } : {}),
|
|
277
|
+
});
|
|
232
278
|
trackedRequests.set(requestId, handlerPromise);
|
|
233
279
|
let resetAttempted = false;
|
|
234
280
|
let resetSucceeded = false;
|
|
@@ -261,7 +307,7 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
261
307
|
? resetFailure.message
|
|
262
308
|
: String(resetFailure);
|
|
263
309
|
}
|
|
264
|
-
});
|
|
310
|
+
}, activity);
|
|
265
311
|
const data = operationData(result);
|
|
266
312
|
const envelope = withWorkspaceNotice(withWorkspaceNotice(toKibiResult(operationSpec, data), workspaceNotice), observed.notice());
|
|
267
313
|
// Log usage in diagnostic mode
|
|
@@ -414,12 +460,12 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
|
414
460
|
}
|
|
415
461
|
/**
|
|
416
462
|
* kb_job_status polls background jobs started by long-running operations
|
|
417
|
-
* (
|
|
463
|
+
* (`kb_check` and `kb_apply_plan` with `async: true`). This is an MCP-server-native
|
|
418
464
|
* companion to the job receipts, not part of the canonical CLI operation
|
|
419
465
|
* catalog: jobs live in the server process and have no CLI counterpart.
|
|
420
466
|
*/
|
|
421
467
|
function registerJobStatusTool(server) {
|
|
422
|
-
addTool(server, "kb_job_status", "Poll a background job started with async:true (
|
|
468
|
+
addTool(server, "kb_job_status", "Poll a background job started with async:true (kb_check or kb_apply_plan async jobs). Returns the kibi.job.v1 state: running, succeeded (with the full result), or failed (with the error).", {
|
|
423
469
|
type: "object",
|
|
424
470
|
properties: {
|
|
425
471
|
jobId: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"mcpName": "io.github.Looted/kibi-mcp",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
@@ -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.
|
|
14
|
-
"kibi-core": "^0.
|
|
13
|
+
"kibi-runtime": "^2.7.0",
|
|
14
|
+
"kibi-core": "^0.17.0",
|
|
15
15
|
"zod": "^4.3.6"
|
|
16
16
|
},
|
|
17
17
|
"type": "module",
|