kibi-mcp 2.4.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 +6 -6
- 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 +64 -11
- package/dist/server.js +2 -2
- package/dist/tools-config.js +58 -10
- 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 = [
|
|
@@ -79,7 +79,7 @@ export const PROMPTS = [
|
|
|
79
79
|
"Treat this server as a branch-aware knowledge graph interface for software traceability.",
|
|
80
80
|
"",
|
|
81
81
|
"The server exposes a curated public tool surface for KB operations:",
|
|
82
|
-
"- `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)",
|
|
83
83
|
"- `kb_query`: Exact lookup of entities by type, ID, tags, or source file",
|
|
84
84
|
"- `kb_status`: Branch, snapshot, and freshness inspection",
|
|
85
85
|
"- `kb_find_gaps`: Bulk missing/present relationship analysis",
|
|
@@ -92,7 +92,7 @@ export const PROMPTS = [
|
|
|
92
92
|
"Core modeling principles:",
|
|
93
93
|
"- Kibi has eight entity types: common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).",
|
|
94
94
|
"- Encode requirements as linked facts: `req --constrains--> fact` plus `req --requires_property--> fact`.",
|
|
95
|
-
"- High-confidence `
|
|
95
|
+
"- High-confidence `kb_model` (mode `requirement`) output is deterministic; `/kibi-bootstrap` bootstrap writes still require preview and explicit approval.",
|
|
96
96
|
"- Low-confidence claims (< 0.7) are downgraded to `observation` facts to prevent false-positive contradictions.",
|
|
97
97
|
"- Only strict domain facts participate in contradiction inference; observation and meta facts are non-blocking notes.",
|
|
98
98
|
"- v1 contradictions are limited to exact-value, boolean/enum, numeric range, and polarity conflicts.",
|
|
@@ -116,8 +116,8 @@ export const PROMPTS = [
|
|
|
116
116
|
"2. **Check schema status**: Call `kb_status` to see if a schema migration is required for the branch KB.",
|
|
117
117
|
"3. **Create-before-link**: Create endpoint entities with `kb_upsert` before linking them.",
|
|
118
118
|
"4. **Validate intent**: If creating links, call `kb_query` for both endpoint IDs first to ensure they exist.",
|
|
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 `
|
|
120
|
-
"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.",
|
|
121
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.",
|
|
122
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.",
|
|
123
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.",
|
|
@@ -191,7 +191,7 @@ function registerDocResources() {
|
|
|
191
191
|
'5. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation; supplying `rules` skips the full-KB advisory scan',
|
|
192
192
|
"",
|
|
193
193
|
"## Model requirements as ontology predicates",
|
|
194
|
-
'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-..." }`',
|
|
195
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",
|
|
196
196
|
"3. If no candidate fits, apply or review the returned `review:ontology-gap` observation instead of silently writing prose",
|
|
197
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,9 +16,11 @@
|
|
|
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";
|
|
@@ -55,7 +57,7 @@ function withAgentVisibleText(content, data) {
|
|
|
55
57
|
const text = summary.length > 0 ? `${payload}\n${summary}` : payload;
|
|
56
58
|
return [{ type: "text", text }, ...nonTextParts];
|
|
57
59
|
}
|
|
58
|
-
// implements REQ-
|
|
60
|
+
// implements REQ-002
|
|
59
61
|
function debugLog(...args) {
|
|
60
62
|
if (isMcpDebugEnabled()) {
|
|
61
63
|
console.error(...args);
|
|
@@ -142,12 +144,33 @@ function localDecision(name, args) {
|
|
|
142
144
|
const { [WORKSPACE_ROOT_ARGUMENT]: _ignored, ...rest } = args;
|
|
143
145
|
return { kind: "local", args: rest };
|
|
144
146
|
}
|
|
145
|
-
/** A result's diagnostics
|
|
147
|
+
/** A result's diagnostics carry a routing or detached-HEAD notice when one applies. */
|
|
146
148
|
function withWorkspaceNotice(envelope, notice) {
|
|
147
149
|
if (!notice)
|
|
148
150
|
return envelope;
|
|
149
151
|
return { ...envelope, diagnostics: [...envelope.diagnostics, notice] };
|
|
150
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
|
+
}
|
|
151
174
|
// implements REQ-002
|
|
152
175
|
export function addTool(server, name, description, inputSchema, handler,
|
|
153
176
|
// INTENTIONAL: DEFAULT_TOOLS_RUNTIME is typed as ToolsRuntime<PrologProcess>; the
|
|
@@ -204,7 +227,8 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
204
227
|
requiresProlog: false,
|
|
205
228
|
execute: async (input, _context) => handler(input),
|
|
206
229
|
};
|
|
207
|
-
const
|
|
230
|
+
const observed = observeDetachedReads(runtime.operationRuntime);
|
|
231
|
+
const handlerPromise = executeOperation(observed.runtime, operationSpec, businessArgs, { signal: controller.signal });
|
|
208
232
|
trackedRequests.set(requestId, handlerPromise);
|
|
209
233
|
let resetAttempted = false;
|
|
210
234
|
let resetSucceeded = false;
|
|
@@ -239,15 +263,18 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
239
263
|
}
|
|
240
264
|
});
|
|
241
265
|
const data = operationData(result);
|
|
242
|
-
const envelope = withWorkspaceNotice(toKibiResult(operationSpec, data), workspaceNotice);
|
|
266
|
+
const envelope = withWorkspaceNotice(withWorkspaceNotice(toKibiResult(operationSpec, data), workspaceNotice), observed.notice());
|
|
243
267
|
// Log usage in diagnostic mode
|
|
244
268
|
if (diagnosticModeEnabled) {
|
|
245
269
|
await appendDiagnosticSuccessUsage({
|
|
246
270
|
runtime,
|
|
247
|
-
|
|
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),
|
|
248
275
|
requestId,
|
|
249
276
|
args,
|
|
250
|
-
businessArgs,
|
|
277
|
+
businessArgs: routedBusinessArgs(name, businessArgs),
|
|
251
278
|
telemetry,
|
|
252
279
|
startedAt,
|
|
253
280
|
result: envelope,
|
|
@@ -277,10 +304,13 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
277
304
|
if (diagnosticModeEnabled) {
|
|
278
305
|
await appendDiagnosticErrorUsage({
|
|
279
306
|
runtime,
|
|
280
|
-
|
|
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),
|
|
281
311
|
requestId,
|
|
282
312
|
args,
|
|
283
|
-
businessArgs,
|
|
313
|
+
businessArgs: routedBusinessArgs(name, businessArgs),
|
|
284
314
|
telemetry,
|
|
285
315
|
startedAt,
|
|
286
316
|
error,
|
|
@@ -288,7 +318,9 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
288
318
|
});
|
|
289
319
|
}
|
|
290
320
|
if (isToolTimeoutError(error) &&
|
|
291
|
-
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)) {
|
|
292
324
|
const recoveryActions = [
|
|
293
325
|
{
|
|
294
326
|
operation: "kb_status",
|
|
@@ -329,6 +361,25 @@ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations, outputSchema) {
|
|
|
329
361
|
structuredContent: envelope,
|
|
330
362
|
};
|
|
331
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
|
+
}
|
|
332
383
|
throw error;
|
|
333
384
|
}
|
|
334
385
|
finally {
|
|
@@ -357,7 +408,9 @@ export function registerAllTools(server,
|
|
|
357
408
|
// INTENTIONAL: same generic bridge cast as addTool — see comment there.
|
|
358
409
|
runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
359
410
|
registerConfiguredTools(server, runtime, addTool);
|
|
360
|
-
|
|
411
|
+
if (enabledOptionalTools().has("kb_job_status")) {
|
|
412
|
+
registerJobStatusTool(server);
|
|
413
|
+
}
|
|
361
414
|
}
|
|
362
415
|
/**
|
|
363
416
|
* kb_job_status polls background jobs started by long-running operations
|
package/dist/server.js
CHANGED
|
@@ -31,8 +31,8 @@ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.me
|
|
|
31
31
|
const VERSION = packageJson.version ?? "0.1.0";
|
|
32
32
|
const SERVER_INSTRUCTIONS = [
|
|
33
33
|
"Kibi is a branch-local requirements knowledge base for software traceability.",
|
|
34
|
-
"Start with kb_search
|
|
35
|
-
"Model requirements as linked facts via
|
|
34
|
+
"Start with kb_search (ask it a question; its answer layer names the governing requirements, facts, ADRs and tests), then kb_query for exact lookups before any mutation.",
|
|
35
|
+
"Model requirements as linked facts via kb_model (mode requirement) or kb_upsert (create endpoints before linking them); preflight writes with kb_upsert dryRun:true.",
|
|
36
36
|
"Mutate sequentially with kb_upsert and kb_delete; validate with kb_check during iteration and run an unfiltered kb_check before completion.",
|
|
37
37
|
"Confirm branch freshness with kb_status. For first-time repository setup, use the kibi-bootstrap prompt.",
|
|
38
38
|
].join(" ");
|
package/dist/tools-config.js
CHANGED
|
@@ -18,30 +18,50 @@
|
|
|
18
18
|
import { getSpec } from "kibi-runtime";
|
|
19
19
|
import { DIAGNOSTIC_MODE_ENABLED, DIAGNOSTIC_TELEMETRY_SCHEMA, } from "./diagnostics.js";
|
|
20
20
|
import { withWorkspaceRootSchema } from "./server/workspace-router.js";
|
|
21
|
+
// implements REQ-kibi-mcp-tool-consolidation
|
|
22
|
+
/**
|
|
23
|
+
* The agent-facing tool list. Narrower operations (kb_skills_list,
|
|
24
|
+
* kb_semantic_advisor, kb_model_requirement, kb_suggest_predicates,
|
|
25
|
+
* kb_validate_upsert) stay in the operation catalog and on the CLI; MCP
|
|
26
|
+
* reaches them through kb_skills, kb_model and kb_upsert dryRun so hosts
|
|
27
|
+
* load fewer tool definitions.
|
|
28
|
+
*/
|
|
21
29
|
const MCP_TOOL_ORDER = [
|
|
22
30
|
"kb_query",
|
|
23
31
|
"kb_search",
|
|
24
32
|
"kb_status",
|
|
25
|
-
"
|
|
26
|
-
"kb_skills_load",
|
|
27
|
-
"kb_skills_read",
|
|
33
|
+
"kb_skills",
|
|
28
34
|
"kb_find_gaps",
|
|
29
35
|
"kb_coverage",
|
|
30
36
|
"kb_graph",
|
|
31
|
-
"
|
|
32
|
-
"kb_semantic_advisor",
|
|
37
|
+
"kb_model",
|
|
33
38
|
"kb_upsert",
|
|
34
|
-
"kb_validate_upsert",
|
|
35
39
|
"kb_delete",
|
|
36
40
|
"kb_check",
|
|
37
41
|
"kb_prepare_impact_review",
|
|
38
|
-
"kb_model_requirement",
|
|
39
|
-
"kb_suggest_predicates",
|
|
40
42
|
"kb_plan_bootstrap",
|
|
41
43
|
"kb_compile_intent",
|
|
42
44
|
"kb_apply_plan",
|
|
43
45
|
"kb_ingest_proof",
|
|
44
46
|
];
|
|
47
|
+
/** Tools registered only when named in KIBI_MCP_OPTIONAL_TOOLS. */
|
|
48
|
+
// implements REQ-kibi-mcp-tool-consolidation
|
|
49
|
+
export const OPTIONAL_TOOL_NAMES = [
|
|
50
|
+
"kb_sparql_remote",
|
|
51
|
+
"kb_job_status",
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
54
|
+
* Parse KIBI_MCP_OPTIONAL_TOOLS: a comma-separated list of optional tool
|
|
55
|
+
* names, or "all".
|
|
56
|
+
*/
|
|
57
|
+
// implements REQ-kibi-mcp-tool-consolidation
|
|
58
|
+
export function enabledOptionalTools(value = process.env.KIBI_MCP_OPTIONAL_TOOLS) {
|
|
59
|
+
const requested = new Set((value ?? "")
|
|
60
|
+
.split(",")
|
|
61
|
+
.map((entry) => entry.trim())
|
|
62
|
+
.filter(Boolean));
|
|
63
|
+
return new Set(OPTIONAL_TOOL_NAMES.filter((name) => requested.has("all") || requested.has(name)));
|
|
64
|
+
}
|
|
45
65
|
// implements REQ-002
|
|
46
66
|
const TOOL_ANNOTATIONS = {
|
|
47
67
|
kb_query: {
|
|
@@ -57,6 +77,20 @@ const TOOL_ANNOTATIONS = {
|
|
|
57
77
|
idempotentHint: true,
|
|
58
78
|
openWorldHint: false,
|
|
59
79
|
},
|
|
80
|
+
kb_skills: {
|
|
81
|
+
title: "Read bundled Kibi skills",
|
|
82
|
+
readOnlyHint: true,
|
|
83
|
+
destructiveHint: false,
|
|
84
|
+
idempotentHint: true,
|
|
85
|
+
openWorldHint: false,
|
|
86
|
+
},
|
|
87
|
+
kb_model: {
|
|
88
|
+
title: "Model Kibi requirement prose",
|
|
89
|
+
readOnlyHint: true,
|
|
90
|
+
destructiveHint: false,
|
|
91
|
+
idempotentHint: true,
|
|
92
|
+
openWorldHint: false,
|
|
93
|
+
},
|
|
60
94
|
kb_skills_list: {
|
|
61
95
|
title: "List bundled Kibi skills",
|
|
62
96
|
readOnlyHint: true,
|
|
@@ -151,7 +185,15 @@ const TOOL_ANNOTATIONS = {
|
|
|
151
185
|
openWorldHint: false,
|
|
152
186
|
},
|
|
153
187
|
};
|
|
154
|
-
|
|
188
|
+
function exposedToolNames(optional) {
|
|
189
|
+
return [
|
|
190
|
+
...MCP_TOOL_ORDER,
|
|
191
|
+
...(optional.has("kb_sparql_remote")
|
|
192
|
+
? ["kb_sparql_remote"]
|
|
193
|
+
: []),
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
function toolConfig(name) {
|
|
155
197
|
const spec = getSpec(name);
|
|
156
198
|
const effects = spec.declaredEffects;
|
|
157
199
|
const derived = {
|
|
@@ -173,7 +215,13 @@ const BASE_TOOLS = MCP_TOOL_ORDER.map((name) => {
|
|
|
173
215
|
...derived,
|
|
174
216
|
},
|
|
175
217
|
};
|
|
176
|
-
}
|
|
218
|
+
}
|
|
219
|
+
/** Catalog tool configs for the base list plus the given optional tools. */
|
|
220
|
+
// implements REQ-kibi-mcp-tool-consolidation
|
|
221
|
+
export function buildBaseTools(optional = enabledOptionalTools()) {
|
|
222
|
+
return exposedToolNames(optional).map(toolConfig);
|
|
223
|
+
}
|
|
224
|
+
const BASE_TOOLS = buildBaseTools();
|
|
177
225
|
/**
|
|
178
226
|
* Inject _diagnostic_telemetry schema into tool inputs when diagnostic mode is enabled.
|
|
179
227
|
* Exported for unit coverage; TOOLS still applies it only when the server starts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.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.
|
|
14
|
-
"kibi-core": "^0.
|
|
13
|
+
"kibi-runtime": "^2.3.0",
|
|
14
|
+
"kibi-core": "^0.15.0",
|
|
15
15
|
"mcpcat": "^0.1.12",
|
|
16
16
|
"zod": "^4.3.6"
|
|
17
17
|
},
|
|
@@ -29,7 +29,10 @@
|
|
|
29
29
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
30
30
|
"prepack": "npm run build && node ../../scripts/verify-package-contract.mjs kibi-mcp"
|
|
31
31
|
},
|
|
32
|
-
"files": [
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"bin"
|
|
35
|
+
],
|
|
33
36
|
"engines": {
|
|
34
37
|
"node": ">=22"
|
|
35
38
|
},
|