kibi-mcp 2.3.0 → 3.0.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/bin/kibi-mcp +22 -1
- package/dist/diagnostics.js +43 -2
- package/dist/runtime/mcp-runtime.js +30 -2
- package/dist/server/docs.js +11 -7
- package/dist/server/session.js +14 -12
- package/dist/server/tool-registration.js +41 -26
- package/dist/server/tools-runtime.js +1 -1
- package/dist/server/tools.js +118 -15
- package/dist/server/workspace-router.js +583 -0
- package/dist/server.js +6 -2
- package/dist/tools/plan-bootstrap.js +1 -1
- package/dist/tools/symbols.js +2 -2
- package/dist/tools-config.js +61 -12
- package/package.json +7 -4
package/bin/kibi-mcp
CHANGED
|
@@ -99,7 +99,8 @@ if (args.includes("-h") || args.includes("--help")) {
|
|
|
99
99
|
|
|
100
100
|
if (args.includes("--diagnostic-mode")) {
|
|
101
101
|
globalThis.__KIBI_MCP_DIAGNOSTIC_MODE = true;
|
|
102
|
-
process.env.KIBI_WORKSPACE ??=
|
|
102
|
+
process.env.KIBI_WORKSPACE ??=
|
|
103
|
+
process.env.KIBI_MCP_ATTACH_ROOT?.trim() || process.cwd();
|
|
103
104
|
process.env.KIBI_MCP_DIAGNOSTIC_MODE = "1";
|
|
104
105
|
}
|
|
105
106
|
|
|
@@ -108,6 +109,26 @@ if (args.includes("-h") || args.includes("--help")) {
|
|
|
108
109
|
process.stderr.write(json);
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
// implements REQ-mcp-workspace-routing
|
|
113
|
+
// Host launchers name the workspace with KIBI_MCP_ATTACH_ROOT rather than
|
|
114
|
+
// KIBI_WORKSPACE: a pin disables per-call workspace routing. Attach after
|
|
115
|
+
// runtime resolution (which reads the launch directory) and only when no
|
|
116
|
+
// operator pin is set.
|
|
117
|
+
const attachRoot = process.env.KIBI_MCP_ATTACH_ROOT?.trim();
|
|
118
|
+
const pinned = ["KIBI_WORKSPACE", "KIBI_PROJECT_ROOT", "KIBI_ROOT"].some(
|
|
119
|
+
(key) => (process.env[key] ?? "").trim().length > 0,
|
|
120
|
+
);
|
|
121
|
+
if (attachRoot && !pinned) {
|
|
122
|
+
try {
|
|
123
|
+
process.chdir(attachRoot);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
process.stderr.write(
|
|
126
|
+
`[KIBI-MCP] Cannot attach to ${attachRoot}: ${error?.message ?? String(error)}\n`,
|
|
127
|
+
);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
111
132
|
if (result.stale) {
|
|
112
133
|
if (result.projectLocal && process.env.KIBI_MCP_PROJECT_LOCAL_REENTRY !== "1") {
|
|
113
134
|
process.env.KIBI_MCP_PROJECT_LOCAL_REENTRY = "1";
|
package/dist/diagnostics.js
CHANGED
|
@@ -179,8 +179,11 @@ function canonicalDiagnosticValue(value) {
|
|
|
179
179
|
.map(([key, child]) => [key, canonicalDiagnosticValue(child)]));
|
|
180
180
|
}
|
|
181
181
|
function mutationFingerprint(args) {
|
|
182
|
+
// A kb_upsert dry run must fingerprint like the write it previews, so
|
|
183
|
+
// validation-before-upsert pairs the two.
|
|
184
|
+
const { dryRun: _dryRun, ...payload } = args;
|
|
182
185
|
return createHash("sha256")
|
|
183
|
-
.update(JSON.stringify(canonicalDiagnosticValue(
|
|
186
|
+
.update(JSON.stringify(canonicalDiagnosticValue(payload)))
|
|
184
187
|
.digest("hex");
|
|
185
188
|
}
|
|
186
189
|
function appendMutationFields(fields, args) {
|
|
@@ -311,7 +314,45 @@ function appendContradictionCheckFields(fields, contradictionCheck) {
|
|
|
311
314
|
fields.semantic_property_key = propertyKey;
|
|
312
315
|
}
|
|
313
316
|
}
|
|
314
|
-
|
|
317
|
+
const MODEL_MODE_OPERATIONS = {
|
|
318
|
+
analyze: "kb_semantic_advisor",
|
|
319
|
+
requirement: "kb_model_requirement",
|
|
320
|
+
predicates: "kb_suggest_predicates",
|
|
321
|
+
};
|
|
322
|
+
const SKILL_ACTION_OPERATIONS = {
|
|
323
|
+
list: "kb_skills_list",
|
|
324
|
+
load: "kb_skills_load",
|
|
325
|
+
read: "kb_skills_read",
|
|
326
|
+
};
|
|
327
|
+
/** Composite tools report diagnostics under the operation they routed to. */
|
|
328
|
+
// implements REQ-kibi-mcp-tool-consolidation
|
|
329
|
+
export function routedOperationName(toolName, args) {
|
|
330
|
+
if (toolName === "kb_model" && typeof args.mode === "string")
|
|
331
|
+
return MODEL_MODE_OPERATIONS[args.mode] ?? toolName;
|
|
332
|
+
if (toolName === "kb_skills" && typeof args.action === "string")
|
|
333
|
+
return SKILL_ACTION_OPERATIONS[args.action] ?? toolName;
|
|
334
|
+
if (toolName === "kb_upsert" && args.dryRun === true)
|
|
335
|
+
return "kb_validate_upsert";
|
|
336
|
+
return toolName;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* The business arguments the routed operation received: the composite's
|
|
340
|
+
* selector is dropped, so a usage entry matches the narrow CLI route's entry.
|
|
341
|
+
*/
|
|
342
|
+
// implements REQ-kibi-mcp-tool-consolidation
|
|
343
|
+
export function routedBusinessArgs(toolName, args) {
|
|
344
|
+
if (routedOperationName(toolName, args) === toolName)
|
|
345
|
+
return args;
|
|
346
|
+
const selector = toolName === "kb_model"
|
|
347
|
+
? "mode"
|
|
348
|
+
: toolName === "kb_skills"
|
|
349
|
+
? "action"
|
|
350
|
+
: "dryRun";
|
|
351
|
+
const { [selector]: _selector, ...rest } = args;
|
|
352
|
+
return rest;
|
|
353
|
+
}
|
|
354
|
+
export function deriveDiagnosticFields(invokedToolName, args, telemetry, result) {
|
|
355
|
+
const toolName = routedOperationName(invokedToolName, args);
|
|
315
356
|
const fields = {
|
|
316
357
|
telemetry_status: telemetry ? "provided" : "missing",
|
|
317
358
|
};
|
|
@@ -1,4 +1,31 @@
|
|
|
1
|
-
import { CapabilityRegistryCache, ensureCapabilityRegistry, resolveBranchAttachment, } from "kibi-runtime";
|
|
1
|
+
import { CapabilityRegistryCache, declaresWriteEffect, detachedHeadWriteRefusal, ensureCapabilityRegistry, resolveBranchAttachment, resolveOperationAttachment, } from "kibi-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Branch attachment for one MCP operation. A detached HEAD keeps the exact
|
|
4
|
+
* contract for writes (refused with the way out) while KB reads attach the
|
|
5
|
+
* read-only snapshot compiled from the checkout, which the shared session
|
|
6
|
+
* engine then serves.
|
|
7
|
+
*/
|
|
8
|
+
// implements REQ-branch-store-recovery-v4
|
|
9
|
+
function resolveMcpAttachment(workspaceRoot, spec) {
|
|
10
|
+
// Synchronous unless HEAD is detached: an attached checkout opens its
|
|
11
|
+
// operation context without extra event-loop turns.
|
|
12
|
+
const attachment = resolveBranchAttachment(workspaceRoot);
|
|
13
|
+
if (!("error" in attachment) || attachment.code !== "DETACHED_HEAD") {
|
|
14
|
+
return attachment;
|
|
15
|
+
}
|
|
16
|
+
if (declaresWriteEffect(spec.effects)) {
|
|
17
|
+
throw new Error(detachedHeadWriteRefusal(spec.name, workspaceRoot));
|
|
18
|
+
}
|
|
19
|
+
if (!spec.effects.includes("kb-read"))
|
|
20
|
+
return attachment;
|
|
21
|
+
return resolveOperationAttachment(workspaceRoot, spec).catch((error) => {
|
|
22
|
+
// Status diagnoses a snapshot that failed to compile instead of failing.
|
|
23
|
+
if (spec.requiresProlog)
|
|
24
|
+
throw error;
|
|
25
|
+
console.warn(`[KIBI-MCP] ${error instanceof Error ? error.message : String(error)}`);
|
|
26
|
+
return attachment;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
2
29
|
export function attachedContextWithProlog(withAttachment, prolog) {
|
|
3
30
|
return { ...withAttachment, prolog };
|
|
4
31
|
}
|
|
@@ -38,7 +65,8 @@ export function createMcpRuntime(session) {
|
|
|
38
65
|
...(git ? { git } : {}),
|
|
39
66
|
...(net ? { net } : {}),
|
|
40
67
|
};
|
|
41
|
-
const
|
|
68
|
+
const resolved = resolveMcpAttachment(context.workspaceRoot, spec);
|
|
69
|
+
const attachment = resolved instanceof Promise ? await resolved : resolved;
|
|
42
70
|
if (!("error" in attachment) && attachment.migrationRequired) {
|
|
43
71
|
console.warn(`[KIBI-MCP] Legacy branch attachment: Git '${attachment.gitBranch}' is reading KB '${attachment.kbBranch}'. Run 'kibi branch migrate --from ${attachment.kbBranch} --to ${attachment.gitBranch} --apply'; writes are blocked until then.`);
|
|
44
72
|
}
|
package/dist/server/docs.js
CHANGED
|
@@ -36,7 +36,7 @@ function renderToolsDoc() {
|
|
|
36
36
|
lines.push("");
|
|
37
37
|
lines.push("Modeling note: Kibi has eight core entity types grouped into common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).");
|
|
38
38
|
lines.push("Only strict domain facts (`fact_kind: subject` + `property_value`) participate in contradiction inference; use `flag` for runtime/config gates and `fact_kind: observation` or `meta` for bug/workaround notes.");
|
|
39
|
-
lines.push("Predicate flow: before writing ontology prose, call `
|
|
39
|
+
lines.push("Predicate flow: before writing ontology prose, call `kb_model` (mode `predicates`); apply a suggested `fact_kind: predicate` via `requires_predicate`, or record the returned `review:ontology-gap` observation when no predicate fits.");
|
|
40
40
|
return lines.join("\n");
|
|
41
41
|
}
|
|
42
42
|
export const PROMPTS = [
|
|
@@ -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
|
"",
|
|
@@ -75,7 +79,7 @@ export const PROMPTS = [
|
|
|
75
79
|
"Treat this server as a branch-aware knowledge graph interface for software traceability.",
|
|
76
80
|
"",
|
|
77
81
|
"The server exposes a curated public tool surface for KB operations:",
|
|
78
|
-
"- `kb_search`:
|
|
82
|
+
"- `kb_search`: Ask a question or search; returns ranked matches plus an answer layer (governing requirements, linked facts, ADRs, scenarios and tests)",
|
|
79
83
|
"- `kb_query`: Exact lookup of entities by type, ID, tags, or source file",
|
|
80
84
|
"- `kb_status`: Branch, snapshot, and freshness inspection",
|
|
81
85
|
"- `kb_find_gaps`: Bulk missing/present relationship analysis",
|
|
@@ -88,7 +92,7 @@ export const PROMPTS = [
|
|
|
88
92
|
"Core modeling principles:",
|
|
89
93
|
"- Kibi has eight entity types: common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).",
|
|
90
94
|
"- Encode requirements as linked facts: `req --constrains--> fact` plus `req --requires_property--> fact`.",
|
|
91
|
-
"- High-confidence `
|
|
95
|
+
"- High-confidence `kb_model` (mode `requirement`) output is deterministic; `/kibi-bootstrap` bootstrap writes still require preview and explicit approval.",
|
|
92
96
|
"- Low-confidence claims (< 0.7) are downgraded to `observation` facts to prevent false-positive contradictions.",
|
|
93
97
|
"- Only strict domain facts participate in contradiction inference; observation and meta facts are non-blocking notes.",
|
|
94
98
|
"- v1 contradictions are limited to exact-value, boolean/enum, numeric range, and polarity conflicts.",
|
|
@@ -112,8 +116,8 @@ export const PROMPTS = [
|
|
|
112
116
|
"2. **Check schema status**: Call `kb_status` to see if a schema migration is required for the branch KB.",
|
|
113
117
|
"3. **Create-before-link**: Create endpoint entities with `kb_upsert` before linking them.",
|
|
114
118
|
"4. **Validate intent**: If creating links, call `kb_query` for both endpoint IDs first to ensure they exist.",
|
|
115
|
-
"5. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property` (automated via `
|
|
116
|
-
"6. **Suggest predicates before prose**: For ontology-lane requirements, spell out the prose claim and call `
|
|
119
|
+
"5. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property` (automated via `kb_model` (mode `requirement`)).",
|
|
120
|
+
"6. **Suggest predicates before prose**: For ontology-lane requirements, spell out the prose claim and call `kb_model` (mode `predicates`) before writing `fact_kind: observation`. Apply the selected `fact_kind: predicate` applyPlan, then attach the returned `relationshipPlan` as `requires_predicate` while preserving existing req metadata; use the returned `review:ontology-gap` observation when no predicate fits.",
|
|
117
121
|
"7. **Plan repairs**: Requirement `kb_coverage` returns `repairPlan`; require `repairPlan.scope.complete`, apply only a batch whose state is `ready`, and treat every batch as non-auto-applicable guidance.",
|
|
118
122
|
"8. **Mutate sequentially**: Query current endpoints, validate each payload, then call `kb_upsert` one at a time or `kb_delete` for explicit removals. Re-run `kb_coverage` after each batch rather than continuing from a stale plan.",
|
|
119
123
|
"9. **Targeted checks**: Run `kb_check` after meaningful mutations; specify only the rules you need so scoped validation stays fast and skips the full-KB advisory scan.",
|
|
@@ -187,7 +191,7 @@ function registerDocResources() {
|
|
|
187
191
|
'5. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation; supplying `rules` skips the full-KB advisory scan',
|
|
188
192
|
"",
|
|
189
193
|
"## Model requirements as ontology predicates",
|
|
190
|
-
'1. Spell out the requirement prose and call `
|
|
194
|
+
'1. Spell out the requirement prose and call `kb_model` with `{ "mode": "predicates", "text": "...", "requirementId": "REQ-..." }`',
|
|
191
195
|
"2. If candidates are returned, apply the top or user-selected `structuredContent.applyPlan` to create `fact_kind: predicate`, then attach `structuredContent.relationshipPlan` with `requires_predicate` while preserving existing req metadata",
|
|
192
196
|
"3. If no candidate fits, apply or review the returned `review:ontology-gap` observation instead of silently writing prose",
|
|
193
197
|
"",
|
package/dist/server/session.js
CHANGED
|
@@ -20,7 +20,7 @@ import { createRequire } from "node:module";
|
|
|
20
20
|
import process from "node:process";
|
|
21
21
|
import { EngineClient } from "kibi-runtime";
|
|
22
22
|
import { PrologProcess } from "kibi-runtime";
|
|
23
|
-
import { copyCleanSnapshot, ensureBranchStoreManifest, getBranchDiagnostic, isValidBranchName, resolveActiveBranch,
|
|
23
|
+
import { copyCleanSnapshot, ensureBranchStoreManifest, getBranchDiagnostic, isValidBranchName, resolveActiveBranch, resolveReadBranchAttachment, } from "kibi-runtime";
|
|
24
24
|
import { getBranchOverride, isMcpDebugEnabled } from "../env.js";
|
|
25
25
|
import { resolveKbPath, resolveWorkspaceRoot } from "../workspace.js";
|
|
26
26
|
import { KbRefreshError, describeBranchKbStamp, readBranchKbStamp, sameBranchKbStamp, } from "./kb-freshness.js";
|
|
@@ -32,7 +32,9 @@ const defaultSessionDeps = {
|
|
|
32
32
|
getBranchDiagnostic,
|
|
33
33
|
isValidBranchName,
|
|
34
34
|
resolveActiveBranch,
|
|
35
|
-
|
|
35
|
+
// The session engine serves reads; on a detached HEAD it attaches the
|
|
36
|
+
// checkout's read-only snapshot, and writes are refused before reaching it.
|
|
37
|
+
resolveBranchAttachment: resolveReadBranchAttachment,
|
|
36
38
|
resolveKbPath,
|
|
37
39
|
resolveWorkspaceRoot,
|
|
38
40
|
};
|
|
@@ -48,7 +50,7 @@ const state = {
|
|
|
48
50
|
isShuttingDown: false,
|
|
49
51
|
shutdownTimeout: null,
|
|
50
52
|
};
|
|
51
|
-
// implements REQ-
|
|
53
|
+
// implements REQ-core-prolog-process-management
|
|
52
54
|
export function getPrologProcess() {
|
|
53
55
|
return state.prologProcess;
|
|
54
56
|
}
|
|
@@ -56,11 +58,11 @@ export function getPrologProcess() {
|
|
|
56
58
|
export function getActiveBranchName() {
|
|
57
59
|
return state.activeBranchName;
|
|
58
60
|
}
|
|
59
|
-
// implements REQ-
|
|
61
|
+
// implements REQ-mcp-kb-freshness
|
|
60
62
|
export function getAttachedBranchKbPath() {
|
|
61
63
|
return state.attachedBranchKbPath;
|
|
62
64
|
}
|
|
63
|
-
// implements REQ-
|
|
65
|
+
// implements REQ-core-prolog-process-management
|
|
64
66
|
export function getIsShuttingDown() {
|
|
65
67
|
return state.isShuttingDown;
|
|
66
68
|
}
|
|
@@ -70,7 +72,7 @@ export const inFlightRequests = new Map();
|
|
|
70
72
|
export function updateAttachedBranchStamp(stamp) {
|
|
71
73
|
state.attachedBranchStamp = stamp;
|
|
72
74
|
}
|
|
73
|
-
// implements REQ-
|
|
75
|
+
// implements REQ-core-prolog-process-management
|
|
74
76
|
export function resetSessionStateForTests() {
|
|
75
77
|
state.prologProcess = null;
|
|
76
78
|
state.isInitialized = false;
|
|
@@ -87,15 +89,15 @@ export function resetSessionStateForTests() {
|
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
export function _setSessionDepsForTests(
|
|
90
|
-
// implements REQ-
|
|
92
|
+
// implements REQ-core-prolog-process-management
|
|
91
93
|
overrides) {
|
|
92
94
|
sessionDeps = { ...sessionDeps, ...overrides };
|
|
93
95
|
}
|
|
94
96
|
export function _resetSessionDepsForTests() {
|
|
95
|
-
// implements REQ-
|
|
97
|
+
// implements REQ-core-prolog-process-management
|
|
96
98
|
sessionDeps = { ...defaultSessionDeps };
|
|
97
99
|
}
|
|
98
|
-
// implements REQ-
|
|
100
|
+
// implements REQ-core-prolog-process-management
|
|
99
101
|
export function _setPrologProcessForTests(process) {
|
|
100
102
|
state.prologProcess = process;
|
|
101
103
|
}
|
|
@@ -105,7 +107,7 @@ function debugLog(...args) {
|
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
export function ensureBranchKbExists(workspaceRoot, branch) {
|
|
108
|
-
// implements REQ-
|
|
110
|
+
// implements REQ-branch-store-recovery-v4
|
|
109
111
|
if (!sessionDeps.isValidBranchName(branch)) {
|
|
110
112
|
throw new Error(`Invalid branch name: ${branch}`);
|
|
111
113
|
}
|
|
@@ -175,7 +177,7 @@ export async function initiateGracefulShutdown(exitCode = 0) {
|
|
|
175
177
|
// Exit
|
|
176
178
|
process.exit(exitCode);
|
|
177
179
|
}
|
|
178
|
-
// implements REQ-
|
|
180
|
+
// implements REQ-core-prolog-process-management
|
|
179
181
|
export async function resetProlog(reason) {
|
|
180
182
|
debugLog(`[KIBI-MCP] Resetting Prolog worker: ${reason}`);
|
|
181
183
|
state.prologResetGeneration += 1;
|
|
@@ -222,7 +224,7 @@ async function refreshAttachedBranchKbWithRetry(prolog, kbPath, currentStamp, as
|
|
|
222
224
|
function usesBranchKbPath(kbPath) {
|
|
223
225
|
return (kbPath.includes("/.kb/branches/") || kbPath.includes("\\.kb\\branches\\"));
|
|
224
226
|
}
|
|
225
|
-
// implements REQ-
|
|
227
|
+
// implements REQ-core-prolog-process-management
|
|
226
228
|
async function ensurePrologUnsafe() {
|
|
227
229
|
const generationAtStart = state.prologResetGeneration;
|
|
228
230
|
const workspaceRoot = sessionDeps.resolveWorkspaceRoot();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getSpec, statusSpec } from "kibi-runtime";
|
|
2
|
-
import { executeApplyPlan, executeCompileIntent, executeDelete, executeIngestProof, executeUpsert,
|
|
2
|
+
import { MODEL_ROUTES, SKILL_ROUTES, dispatchComposite, executeApplyPlan, executeCompileIntent, executeDelete, executeIngestProof, executeUpsert, } from "kibi-runtime";
|
|
3
|
+
import { OPTIONAL_TOOL_NAMES, enabledOptionalTools } from "../tools-config.js";
|
|
3
4
|
import { startJob } from "./jobs.js";
|
|
4
5
|
// implements REQ-002, REQ-013
|
|
5
6
|
export function registerConfiguredTools(server, runtime, registerTool) {
|
|
@@ -21,6 +22,12 @@ export function registerConfiguredTools(server, runtime, registerTool) {
|
|
|
21
22
|
prolog: prologFor(context),
|
|
22
23
|
});
|
|
23
24
|
const register = ({ name, execute }) => {
|
|
25
|
+
// Optional tools (kb_sparql_remote) are registered only when enabled;
|
|
26
|
+
// every other configured tool must have a definition.
|
|
27
|
+
if (OPTIONAL_TOOL_NAMES.includes(name) &&
|
|
28
|
+
!runtime.tools.some((tool) => tool.name === name)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
24
31
|
const definition = toolDef(name);
|
|
25
32
|
const publicSpec = getSpec(name);
|
|
26
33
|
const spec = {
|
|
@@ -55,16 +62,21 @@ export function registerConfiguredTools(server, runtime, registerTool) {
|
|
|
55
62
|
statusSpec.execute(args, context),
|
|
56
63
|
});
|
|
57
64
|
register({
|
|
58
|
-
name: "
|
|
59
|
-
execute: async (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
name: "kb_skills",
|
|
66
|
+
execute: async (context, args) => dispatchComposite("action", {
|
|
67
|
+
list: {
|
|
68
|
+
...SKILL_ROUTES.list,
|
|
69
|
+
execute: async (input) => runtime.handleKbSkillsList(input),
|
|
70
|
+
},
|
|
71
|
+
load: {
|
|
72
|
+
...SKILL_ROUTES.load,
|
|
73
|
+
execute: async (input) => runtime.handleKbSkillsLoad(input),
|
|
74
|
+
},
|
|
75
|
+
read: {
|
|
76
|
+
...SKILL_ROUTES.read,
|
|
77
|
+
execute: async (input) => runtime.handleKbSkillsRead(input),
|
|
78
|
+
},
|
|
79
|
+
}, args, context),
|
|
68
80
|
});
|
|
69
81
|
register({
|
|
70
82
|
name: "kb_find_gaps",
|
|
@@ -83,17 +95,26 @@ export function registerConfiguredTools(server, runtime, registerTool) {
|
|
|
83
95
|
execute: async (context, args) => runtime.handleSparql(args, context),
|
|
84
96
|
});
|
|
85
97
|
register({
|
|
86
|
-
name: "
|
|
87
|
-
execute: async (context, args) =>
|
|
98
|
+
name: "kb_model",
|
|
99
|
+
execute: async (context, args) => dispatchComposite("mode", {
|
|
100
|
+
analyze: {
|
|
101
|
+
...MODEL_ROUTES.analyze,
|
|
102
|
+
execute: async (input) => runtime.handleKbSemanticAdvisor(input, context),
|
|
103
|
+
},
|
|
104
|
+
requirement: {
|
|
105
|
+
...MODEL_ROUTES.requirement,
|
|
106
|
+
execute: async (input) => runtime.handleKbModelRequirement(prologFor(context), input, withSessionProlog(context)),
|
|
107
|
+
},
|
|
108
|
+
predicates: {
|
|
109
|
+
...MODEL_ROUTES.predicates,
|
|
110
|
+
execute: async (input) => runtime.handleKbSuggestPredicates(prologFor(context), input, withSessionProlog(context)),
|
|
111
|
+
},
|
|
112
|
+
}, args, context),
|
|
88
113
|
});
|
|
89
114
|
register({
|
|
90
115
|
name: "kb_upsert",
|
|
91
116
|
execute: async (context, args) => executeUpsert(args, withSessionProlog(context)),
|
|
92
117
|
});
|
|
93
|
-
register({
|
|
94
|
-
name: "kb_validate_upsert",
|
|
95
|
-
execute: async (context, args) => validateUpsertSpec.execute(args, withSessionProlog(context)),
|
|
96
|
-
});
|
|
97
118
|
register({
|
|
98
119
|
name: "kb_delete",
|
|
99
120
|
execute: async (context, args) => executeDelete(args, withSessionProlog(context)),
|
|
@@ -102,7 +123,9 @@ export function registerConfiguredTools(server, runtime, registerTool) {
|
|
|
102
123
|
name: "kb_check",
|
|
103
124
|
execute: async (context, args) => {
|
|
104
125
|
const { async: asyncMode, ...checkArgs } = args;
|
|
105
|
-
|
|
126
|
+
// Without a registered kb_job_status nothing could poll the receipt,
|
|
127
|
+
// so async falls back to a synchronous check.
|
|
128
|
+
if (asyncMode !== true || !enabledOptionalTools().has("kb_job_status")) {
|
|
106
129
|
return runtime.handleKbCheck(prologFor(context), args);
|
|
107
130
|
}
|
|
108
131
|
// Long-KB full checks can exceed the tool timeout. Detach into a job
|
|
@@ -115,14 +138,6 @@ export function registerConfiguredTools(server, runtime, registerTool) {
|
|
|
115
138
|
name: "kb_prepare_impact_review",
|
|
116
139
|
execute: async (context, args) => getSpec("kb_prepare_impact_review").execute(args, context),
|
|
117
140
|
});
|
|
118
|
-
register({
|
|
119
|
-
name: "kb_model_requirement",
|
|
120
|
-
execute: async (context, args) => runtime.handleKbModelRequirement(prologFor(context), args, withSessionProlog(context)),
|
|
121
|
-
});
|
|
122
|
-
register({
|
|
123
|
-
name: "kb_suggest_predicates",
|
|
124
|
-
execute: async (context, args) => runtime.handleKbSuggestPredicates(prologFor(context), args, withSessionProlog(context)),
|
|
125
|
-
});
|
|
126
141
|
register({
|
|
127
142
|
name: "kb_plan_bootstrap",
|
|
128
143
|
execute: async (context, args) => runtime.handleKbPlanBootstrap(args, context),
|
|
@@ -25,7 +25,7 @@ const defaultToolsServerDeps = {
|
|
|
25
25
|
};
|
|
26
26
|
let sessionModulePromise = null;
|
|
27
27
|
const prologPorts = new WeakMap();
|
|
28
|
-
// implements REQ-
|
|
28
|
+
// implements REQ-002
|
|
29
29
|
export function _setToolsServerDepsForTests(deps, resetPromise = false) {
|
|
30
30
|
defaultToolsServerDeps.getSessionModule =
|
|
31
31
|
deps.getSessionModule ?? defaultToolsServerDeps.getSessionModule;
|
package/dist/server/tools.js
CHANGED
|
@@ -16,14 +16,17 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
import process from "node:process";
|
|
19
|
-
import { executeOperation } from "kibi-runtime";
|
|
19
|
+
import { QUERY_LIMIT_EXCEEDED_CODE, detachedReadOnlyDiagnostic, executeOperation, queryLimitExceededOf, } from "kibi-runtime";
|
|
20
20
|
import { operationData, toKibiResult } from "kibi-runtime";
|
|
21
|
+
import { routedBusinessArgs, routedOperationName } from "../diagnostics.js";
|
|
21
22
|
import { isMcpDebugEnabled } from "../env.js";
|
|
23
|
+
import { enabledOptionalTools } from "../tools-config.js";
|
|
22
24
|
import { appendDiagnosticErrorUsage, appendDiagnosticSuccessUsage, } from "./diagnostic-usage.js";
|
|
23
25
|
import { getJob, jobSnapshot } from "./jobs.js";
|
|
24
26
|
import { jsonSchemaToZod } from "./json-schema-to-zod.js";
|
|
25
27
|
import { registerConfiguredTools } from "./tool-registration.js";
|
|
26
28
|
import { DEFAULT_TOOLS_RUNTIME, _resetSessionModulePromise, _setToolsServerDepsForTests, } from "./tools-runtime.js";
|
|
29
|
+
import { WORKSPACE_ROOT_ARGUMENT, getWorkspaceRouter, } from "./workspace-router.js";
|
|
27
30
|
export { jsonSchemaToZod } from "./json-schema-to-zod.js";
|
|
28
31
|
export { _resetSessionModulePromise, _setToolsServerDepsForTests };
|
|
29
32
|
const DEFAULT_TOOL_TIMEOUT_MS = 90_000;
|
|
@@ -54,7 +57,7 @@ function withAgentVisibleText(content, data) {
|
|
|
54
57
|
const text = summary.length > 0 ? `${payload}\n${summary}` : payload;
|
|
55
58
|
return [{ type: "text", text }, ...nonTextParts];
|
|
56
59
|
}
|
|
57
|
-
// implements REQ-
|
|
60
|
+
// implements REQ-002
|
|
58
61
|
function debugLog(...args) {
|
|
59
62
|
if (isMcpDebugEnabled()) {
|
|
60
63
|
console.error(...args);
|
|
@@ -115,23 +118,93 @@ async function withToolTimeout(toolName, operation, onTimeout) {
|
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* A module that vanished after startup means the package was upgraded or
|
|
123
|
+
* reinstalled under a running server; only a restart loads the new install.
|
|
124
|
+
*/
|
|
125
|
+
function staleInstallHint(error) {
|
|
126
|
+
const code = error.code;
|
|
127
|
+
const missingModule = code === "ERR_MODULE_NOT_FOUND" ||
|
|
128
|
+
code === "MODULE_NOT_FOUND" ||
|
|
129
|
+
/Cannot find (module|package)/.test(error.message);
|
|
130
|
+
return missingModule
|
|
131
|
+
? " — 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."
|
|
132
|
+
: "";
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The decision when no workspace router is installed (unit tests, embedded
|
|
136
|
+
* use): every call is local, and `workspaceRoot` is dropped so handlers never
|
|
137
|
+
* see a routing-only argument. kb_check keeps it: there it also names the
|
|
138
|
+
* tree to inspect for impact diagnostics.
|
|
139
|
+
*/
|
|
140
|
+
function localDecision(name, args) {
|
|
141
|
+
if (!(WORKSPACE_ROOT_ARGUMENT in args) || name === "kb_check") {
|
|
142
|
+
return { kind: "local", args };
|
|
143
|
+
}
|
|
144
|
+
const { [WORKSPACE_ROOT_ARGUMENT]: _ignored, ...rest } = args;
|
|
145
|
+
return { kind: "local", args: rest };
|
|
146
|
+
}
|
|
147
|
+
/** A result's diagnostics carry a routing or detached-HEAD notice when one applies. */
|
|
148
|
+
function withWorkspaceNotice(envelope, notice) {
|
|
149
|
+
if (!notice)
|
|
150
|
+
return envelope;
|
|
151
|
+
return { ...envelope, diagnostics: [...envelope.diagnostics, notice] };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Observe the context an operation opens so its envelope can say when a
|
|
155
|
+
* detached HEAD answered from the read-only snapshot, and which store.
|
|
156
|
+
*/
|
|
157
|
+
// implements REQ-branch-store-recovery-v4
|
|
158
|
+
function observeDetachedReads(base) {
|
|
159
|
+
let notice;
|
|
160
|
+
return {
|
|
161
|
+
runtime: {
|
|
162
|
+
// Read the attachment at close (success or error) so opening the
|
|
163
|
+
// context costs no extra event-loop turn.
|
|
164
|
+
open: (spec, options) => base.open(spec, options),
|
|
165
|
+
afterSuccess: (spec, context) => base.afterSuccess(spec, context),
|
|
166
|
+
close: (context, outcome) => {
|
|
167
|
+
notice = detachedReadOnlyDiagnostic(context.branchAttachment);
|
|
168
|
+
return base.close(context, outcome);
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
notice: () => notice,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
118
174
|
// implements REQ-002
|
|
119
175
|
export function addTool(server, name, description, inputSchema, handler,
|
|
120
176
|
// INTENTIONAL: DEFAULT_TOOLS_RUNTIME is typed as ToolsRuntime<PrologProcess>; the
|
|
121
177
|
// generic TProlog parameter exists so tests can inject a mock type. The cast is safe
|
|
122
178
|
// because the runtime object satisfies the full ToolsRuntime contract at runtime.
|
|
123
179
|
runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
124
|
-
const wrappedHandler = async (
|
|
180
|
+
const wrappedHandler = async (rawArgs) => {
|
|
125
181
|
const startedAt = new Date();
|
|
126
182
|
const diagnosticModeEnabled = runtime.diagnosticModeEnabled();
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
183
|
+
let args = rawArgs;
|
|
184
|
+
let workspaceNotice;
|
|
185
|
+
let businessArgs = rawArgs;
|
|
186
|
+
let telemetry = null;
|
|
130
187
|
try {
|
|
131
188
|
// Validate that args is a valid object
|
|
132
189
|
if (typeof args !== "object" || args === null) {
|
|
133
190
|
throw new Error(`Invalid arguments for tool ${name}: expected object, got ${typeof args}`);
|
|
134
191
|
}
|
|
192
|
+
// Answer from the workspace the caller is working in. A call routed to
|
|
193
|
+
// another workspace's kibi-mcp returns that server's result as is; the
|
|
194
|
+
// child logs its own usage and the parent records nothing for it.
|
|
195
|
+
// Without a router the decision is synchronous, so a plain call reaches
|
|
196
|
+
// its handler without an extra microtask.
|
|
197
|
+
const router = getWorkspaceRouter();
|
|
198
|
+
const routing = router
|
|
199
|
+
? await router.route(name, args)
|
|
200
|
+
: localDecision(name, args);
|
|
201
|
+
if (routing.kind === "remote")
|
|
202
|
+
return routing.result;
|
|
203
|
+
args = routing.args;
|
|
204
|
+
workspaceNotice = routing.notice;
|
|
205
|
+
({ businessArgs, telemetry } = diagnosticModeEnabled
|
|
206
|
+
? runtime.extractToolCallPayload(args)
|
|
207
|
+
: { businessArgs: args, telemetry: null });
|
|
135
208
|
// Check if shutting down before processing
|
|
136
209
|
if (await runtime.isShuttingDown()) {
|
|
137
210
|
throw new Error(`Tool ${name} rejected: server is shutting down`);
|
|
@@ -154,7 +227,8 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
154
227
|
requiresProlog: false,
|
|
155
228
|
execute: async (input, _context) => handler(input),
|
|
156
229
|
};
|
|
157
|
-
const
|
|
230
|
+
const observed = observeDetachedReads(runtime.operationRuntime);
|
|
231
|
+
const handlerPromise = executeOperation(observed.runtime, operationSpec, businessArgs, { signal: controller.signal });
|
|
158
232
|
trackedRequests.set(requestId, handlerPromise);
|
|
159
233
|
let resetAttempted = false;
|
|
160
234
|
let resetSucceeded = false;
|
|
@@ -189,15 +263,18 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
189
263
|
}
|
|
190
264
|
});
|
|
191
265
|
const data = operationData(result);
|
|
192
|
-
const envelope = toKibiResult(operationSpec, data);
|
|
266
|
+
const envelope = withWorkspaceNotice(withWorkspaceNotice(toKibiResult(operationSpec, data), workspaceNotice), observed.notice());
|
|
193
267
|
// Log usage in diagnostic mode
|
|
194
268
|
if (diagnosticModeEnabled) {
|
|
195
269
|
await appendDiagnosticSuccessUsage({
|
|
196
270
|
runtime,
|
|
197
|
-
|
|
271
|
+
// Usage evidence records the routed operation (kb_model mode
|
|
272
|
+
// predicates logs as kb_suggest_predicates) so telemetry
|
|
273
|
+
// acceptance keeps reading one operation per entry.
|
|
274
|
+
toolName: routedOperationName(name, businessArgs),
|
|
198
275
|
requestId,
|
|
199
276
|
args,
|
|
200
|
-
businessArgs,
|
|
277
|
+
businessArgs: routedBusinessArgs(name, businessArgs),
|
|
201
278
|
telemetry,
|
|
202
279
|
startedAt,
|
|
203
280
|
result: envelope,
|
|
@@ -227,10 +304,13 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
227
304
|
if (diagnosticModeEnabled) {
|
|
228
305
|
await appendDiagnosticErrorUsage({
|
|
229
306
|
runtime,
|
|
230
|
-
|
|
307
|
+
// Usage evidence records the routed operation (kb_model mode
|
|
308
|
+
// predicates logs as kb_suggest_predicates) so telemetry
|
|
309
|
+
// acceptance keeps reading one operation per entry.
|
|
310
|
+
toolName: routedOperationName(name, businessArgs),
|
|
231
311
|
requestId,
|
|
232
312
|
args,
|
|
233
|
-
businessArgs,
|
|
313
|
+
businessArgs: routedBusinessArgs(name, businessArgs),
|
|
234
314
|
telemetry,
|
|
235
315
|
startedAt,
|
|
236
316
|
error,
|
|
@@ -238,7 +318,9 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
238
318
|
});
|
|
239
319
|
}
|
|
240
320
|
if (isToolTimeoutError(error) &&
|
|
241
|
-
isMutationEffect(operationSpec.effects)
|
|
321
|
+
isMutationEffect(operationSpec.effects) &&
|
|
322
|
+
// A kb_upsert dry run writes nothing, so its outcome is known.
|
|
323
|
+
!(name === "kb_upsert" && businessArgs.dryRun === true)) {
|
|
242
324
|
const recoveryActions = [
|
|
243
325
|
{
|
|
244
326
|
operation: "kb_status",
|
|
@@ -279,6 +361,25 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
279
361
|
structuredContent: envelope,
|
|
280
362
|
};
|
|
281
363
|
}
|
|
364
|
+
const limitExceeded = queryLimitExceededOf(error);
|
|
365
|
+
if (error instanceof Error && limitExceeded !== null) {
|
|
366
|
+
// A read stopped at its engine limit has no answer: say which
|
|
367
|
+
// limit stopped it rather than failing like a broken tool.
|
|
368
|
+
const envelope = withWorkspaceNotice(withWorkspaceNotice(toKibiResult(operationSpec, null, {
|
|
369
|
+
status: "error",
|
|
370
|
+
error: {
|
|
371
|
+
code: QUERY_LIMIT_EXCEEDED_CODE,
|
|
372
|
+
message: error.message,
|
|
373
|
+
retryable: false,
|
|
374
|
+
details: { limitExceeded },
|
|
375
|
+
},
|
|
376
|
+
}), workspaceNotice), observed.notice());
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: "text", text: JSON.stringify(envelope) }],
|
|
379
|
+
structuredContent: envelope,
|
|
380
|
+
isError: true,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
282
383
|
throw error;
|
|
283
384
|
}
|
|
284
385
|
finally {
|
|
@@ -292,7 +393,7 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
292
393
|
if (err.stack) {
|
|
293
394
|
debugLog(`[KIBI-MCP] Tool ${name} stack:`, err.stack);
|
|
294
395
|
}
|
|
295
|
-
throw new Error(`Tool ${name} failed: ${err.message}`, { cause: err });
|
|
396
|
+
throw new Error(`Tool ${name} failed: ${err.message}${staleInstallHint(err)}`, { cause: err });
|
|
296
397
|
}
|
|
297
398
|
};
|
|
298
399
|
server.registerTool(name, {
|
|
@@ -307,7 +408,9 @@ export function registerAllTools(server,
|
|
|
307
408
|
// INTENTIONAL: same generic bridge cast as addTool — see comment there.
|
|
308
409
|
runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
309
410
|
registerConfiguredTools(server, runtime, addTool);
|
|
310
|
-
|
|
411
|
+
if (enabledOptionalTools().has("kb_job_status")) {
|
|
412
|
+
registerJobStatusTool(server);
|
|
413
|
+
}
|
|
311
414
|
}
|
|
312
415
|
/**
|
|
313
416
|
* kb_job_status polls background jobs started by long-running operations
|