kibi-mcp 0.19.0 → 0.19.2

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.
Files changed (36) hide show
  1. package/bin/kibi-mcp +1 -1
  2. package/dist/runtime/mcp-runtime.js +43 -0
  3. package/dist/server/docs.js +4 -4
  4. package/dist/server/json-schema-to-zod.js +73 -2
  5. package/dist/server/session.js +33 -15
  6. package/dist/server/tool-registration.js +35 -62
  7. package/dist/server/tools-runtime.js +52 -2
  8. package/dist/server/tools.js +19 -5
  9. package/dist/server/transport.js +9 -5
  10. package/dist/tools/autopilot-generate.js +37 -646
  11. package/dist/tools/autopilot-test-compat.js +88 -0
  12. package/dist/tools/autopilot-test-parsers.js +120 -0
  13. package/dist/tools/check.js +13 -106
  14. package/dist/tools/coverage.js +11 -25
  15. package/dist/tools/delete.js +5 -112
  16. package/dist/tools/discovery-adapter.js +23 -0
  17. package/dist/tools/entity-query.js +1 -111
  18. package/dist/tools/find-gaps.js +11 -26
  19. package/dist/tools/graph.js +11 -24
  20. package/dist/tools/model-requirement.js +8 -347
  21. package/dist/tools/mutation-context.js +28 -0
  22. package/dist/tools/query.js +5 -50
  23. package/dist/tools/relationship-validation.js +9 -116
  24. package/dist/tools/search.js +4 -37
  25. package/dist/tools/semantic-advisor.js +2 -40
  26. package/dist/tools/skills.js +17 -57
  27. package/dist/tools/sparql.js +12 -93
  28. package/dist/tools/status.js +5 -19
  29. package/dist/tools/suggest-predicates.js +11 -2448
  30. package/dist/tools/upsert.js +21 -816
  31. package/dist/tools/validate-upsert.js +12 -48
  32. package/dist/tools-config.js +75 -802
  33. package/package.json +4 -3
  34. package/dist/semantic-advisor/analyze-prose.js +0 -1392
  35. package/dist/semantic-advisor/prose-coverage-evaluator.js +0 -66
  36. package/dist/semantic-advisor/types.js +0 -1
package/bin/kibi-mcp CHANGED
@@ -98,7 +98,7 @@ if (args.includes("-h") || args.includes("--help")) {
98
98
 
99
99
  if (args.includes("--diagnostic-mode")) {
100
100
  globalThis.__KIBI_MCP_DIAGNOSTIC_MODE = true;
101
- process.env.KIBI_WORKSPACE = process.cwd();
101
+ process.env.KIBI_WORKSPACE ??= process.cwd();
102
102
  process.env.KIBI_MCP_DIAGNOSTIC_MODE = "1";
103
103
  }
104
104
 
@@ -0,0 +1,43 @@
1
+ // implements REQ-kibi-operation-interface-parity
2
+ export function createMcpRuntime(session) {
3
+ const sessionPrologs = new WeakMap();
4
+ return {
5
+ open: async (spec, options = {}) => {
6
+ const fs = options.fs ?? session.fs;
7
+ const git = options.git ?? session.git;
8
+ const net = options.net ?? session.net;
9
+ const context = {
10
+ workspaceRoot: options.workspaceRoot ?? session.workspaceRoot,
11
+ signal: options.signal ?? session.signal ?? new AbortController().signal,
12
+ clock: options.clock ?? session.clock ?? (() => new Date()),
13
+ ...(fs ? { fs } : {}),
14
+ ...(git ? { git } : {}),
15
+ ...(net ? { net } : {}),
16
+ };
17
+ if (!spec.requiresProlog) {
18
+ return context;
19
+ }
20
+ if (options.prolog) {
21
+ return { ...context, prolog: options.prolog };
22
+ }
23
+ const sessionProlog = await session.ensureProlog();
24
+ const operationContext = {
25
+ ...context,
26
+ prolog: session.adaptProlog(sessionProlog),
27
+ };
28
+ sessionPrologs.set(operationContext, sessionProlog);
29
+ return operationContext;
30
+ },
31
+ afterSuccess: async (spec) => {
32
+ if (spec.effects.includes("kb-write") &&
33
+ (await session.attachedBranchKbPath()) !== null) {
34
+ await session.refreshAttachedBranchStamp();
35
+ }
36
+ },
37
+ close: async (context, outcome) => {
38
+ await session.requestCleanup?.(context, outcome);
39
+ sessionPrologs.delete(context);
40
+ },
41
+ sessionProlog: (context) => sessionPrologs.get(context),
42
+ };
43
+ }
@@ -16,9 +16,6 @@
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
  */
18
18
  import { TOOLS } from "../tools-config.js";
19
- // INTENTIONAL: TOOLS is imported as a Zod-inferred schema type; ToolConfig is the runtime
20
- // interface with looser Record<string, unknown> inputSchema. The cast is safe because the
21
- // tool definitions are statically authored and validated at startup.
22
19
  const ACTIVE_TOOLS = TOOLS;
23
20
  function renderToolsDoc() {
24
21
  const lines = [
@@ -87,7 +84,10 @@ export const PROMPTS = [
87
84
  "## Rules",
88
85
  "- Never apply bootstrap writes without user-facing preview and explicit approval.",
89
86
  "- `kb_autopilot_generate` is strictly read-only; synthesis is the backend, not the actor.",
90
- "- Guidance must stay MCP-only; do not suggest `kibi` CLI commands.",
87
+ "- Prefer MCP tools; CLI routes are available when MCP is unavailable.",
88
+ "- If Kibi MCP tools are visible, use MCP. If MCP availability is unknown and a trusted local Kibi CLI is available, use its dedicated JSON routes with `--input <file|->`.",
89
+ "- If neither interface is available, Kibi operation is blocked; tell the operator to enable MCP or the trusted project-local CLI. Do not infer MCP availability from config file existence.",
90
+ "- Do not read or edit `.kb/` files directly. Query before mutate. Run `kb_upsert` sequentially. Run `kb_check` before completion.",
91
91
  ].join("\n"),
92
92
  },
93
93
  {
@@ -1,4 +1,40 @@
1
1
  import { z } from "zod";
2
+ function hasRequiredProperties(value, schema) {
3
+ if (schema === null || typeof schema !== "object")
4
+ return false;
5
+ const condition = schema;
6
+ const required = Array.isArray(condition.required)
7
+ ? condition.required.filter((key) => typeof key === "string" && key.length > 0)
8
+ : [];
9
+ if (required.length > 0 &&
10
+ !required.every((key) => Object.hasOwn(value, key))) {
11
+ return false;
12
+ }
13
+ if (Array.isArray(condition.anyOf) &&
14
+ !condition.anyOf.some((entry) => hasRequiredProperties(value, entry))) {
15
+ return false;
16
+ }
17
+ if (Array.isArray(condition.allOf) &&
18
+ !condition.allOf.every((entry) => hasRequiredProperties(value, entry))) {
19
+ return false;
20
+ }
21
+ return true;
22
+ }
23
+ function conditionalRequiredKeys(value, condition) {
24
+ if (condition === null || typeof condition !== "object")
25
+ return [];
26
+ const rule = condition;
27
+ if (!("if" in rule) || !("then" in rule))
28
+ return [];
29
+ if (!hasRequiredProperties(value, rule.if))
30
+ return [];
31
+ if (rule.then === null || typeof rule.then !== "object")
32
+ return [];
33
+ const required = rule.then.required;
34
+ return Array.isArray(required)
35
+ ? required.filter((key) => typeof key === "string" && key.length > 0)
36
+ : [];
37
+ }
2
38
  // implements REQ-002
3
39
  export function jsonSchemaToZod(schema) {
4
40
  if (!schema || typeof schema !== "object") {
@@ -43,7 +79,29 @@ export function jsonSchemaToZod(schema) {
43
79
  ? z.object(shape)
44
80
  : z.looseObject(shape);
45
81
  const description = typeof obj.description === "string" ? obj.description : undefined;
46
- return description ? objectSchema.describe(description) : objectSchema;
82
+ let result = description
83
+ ? objectSchema.describe(description)
84
+ : objectSchema;
85
+ if (Array.isArray(obj.allOf)) {
86
+ result = result
87
+ .superRefine((value, context) => {
88
+ if (value === null || typeof value !== "object")
89
+ return;
90
+ for (const condition of obj.allOf) {
91
+ for (const key of conditionalRequiredKeys(value, condition)) {
92
+ if (!Object.hasOwn(value, key)) {
93
+ context.addIssue({
94
+ code: "custom",
95
+ path: [key],
96
+ message: `Required by conditional JSON Schema rule: ${key}`,
97
+ });
98
+ }
99
+ }
100
+ }
101
+ })
102
+ .meta({ allOf: obj.allOf });
103
+ }
104
+ return result;
47
105
  }
48
106
  case "array": {
49
107
  const itemSchema = jsonSchemaToZod(obj.items);
@@ -55,7 +113,17 @@ export function jsonSchemaToZod(schema) {
55
113
  if (typeof obj.maxItems === "number") {
56
114
  arraySchema = arraySchema.max(obj.maxItems);
57
115
  }
58
- return description ? arraySchema.describe(description) : arraySchema;
116
+ let result = description
117
+ ? arraySchema.describe(description)
118
+ : arraySchema;
119
+ if (obj.uniqueItems === true) {
120
+ result = result
121
+ .refine((values) => Array.isArray(values) &&
122
+ new Set(values.map((value) => JSON.stringify(value))).size ===
123
+ values.length, { message: "Array items must be unique" })
124
+ .meta({ uniqueItems: true });
125
+ }
126
+ return result;
59
127
  }
60
128
  case "string": {
61
129
  let s = z.string();
@@ -66,6 +134,9 @@ export function jsonSchemaToZod(schema) {
66
134
  if (typeof obj.maxLength === "number") {
67
135
  s = s.max(obj.maxLength);
68
136
  }
137
+ if (typeof obj.pattern === "string") {
138
+ s = s.regex(new RegExp(obj.pattern));
139
+ }
69
140
  return description ? s.describe(description) : s;
70
141
  }
71
142
  case "number": {
@@ -42,6 +42,8 @@ let ensurePrologTail = Promise.resolve();
42
42
  let prologResetGeneration = 0;
43
43
  export let attachedBranchKbPath = null;
44
44
  let attachedBranchStamp = null;
45
+ // implements REQ-mcp-kb-freshness
46
+ // covered_by TEST-mcp-kb-freshness
45
47
  export function updateAttachedBranchStamp(stamp) {
46
48
  attachedBranchStamp = stamp;
47
49
  }
@@ -85,7 +87,7 @@ export function ensureBranchKbExists(workspaceRoot, branch) {
85
87
  }
86
88
  const branchPath = sessionDeps.resolveKbPath(workspaceRoot, branch);
87
89
  if (sessionDeps.fs.existsSync(branchPath)) {
88
- return;
90
+ return false;
89
91
  }
90
92
  // Try to copy from the previously active branch if available
91
93
  const previousBranch = activeBranchName;
@@ -96,11 +98,12 @@ export function ensureBranchKbExists(workspaceRoot, branch) {
96
98
  // Copy from previous branch for continuity
97
99
  sessionDeps.copyCleanSnapshot(previousBranchPath, branchPath);
98
100
  debugLog(`[KIBI-MCP] Created branch KB for '${branch}' from '${previousBranch}'`);
99
- return;
101
+ return false;
100
102
  }
101
103
  // No previous branch available - create empty KB
102
104
  sessionDeps.fs.mkdirSync(branchPath, { recursive: true });
103
105
  debugLog(`[KIBI-MCP] Created empty branch KB for '${branch}'`);
106
+ return true;
104
107
  }
105
108
  export async function initiateGracefulShutdown(exitCode = 0) {
106
109
  if (isShuttingDown) {
@@ -108,6 +111,18 @@ export async function initiateGracefulShutdown(exitCode = 0) {
108
111
  }
109
112
  isShuttingDown = true;
110
113
  debugLog(`[KIBI-MCP] Initiating graceful shutdown (exit code: ${exitCode})`);
114
+ const currentProlog = prologProcess;
115
+ prologProcess = null;
116
+ if (currentProlog?.isRunning()) {
117
+ debugLog("[KIBI-MCP] Cancelling active Prolog work...");
118
+ try {
119
+ await currentProlog.terminate();
120
+ debugLog("[KIBI-MCP] Prolog process terminated");
121
+ }
122
+ catch (error) {
123
+ console.error("[KIBI-MCP] Error terminating Prolog:", error);
124
+ }
125
+ }
111
126
  // Wait for in-flight requests
112
127
  if (inFlightRequests.size > 0) {
113
128
  debugLog(`[KIBI-MCP] Waiting for ${inFlightRequests.size} in-flight requests to complete...`);
@@ -133,17 +148,6 @@ export async function initiateGracefulShutdown(exitCode = 0) {
133
148
  }
134
149
  }
135
150
  }
136
- // Cleanup Prolog process
137
- if (prologProcess?.isRunning()) {
138
- debugLog("[KIBI-MCP] Terminating Prolog process...");
139
- try {
140
- await prologProcess.terminate();
141
- debugLog("[KIBI-MCP] Prolog process terminated");
142
- }
143
- catch (error) {
144
- console.error("[KIBI-MCP] Error terminating Prolog:", error);
145
- }
146
- }
147
151
  // Exit
148
152
  process.exit(exitCode);
149
153
  }
@@ -271,13 +275,20 @@ async function ensurePrologUnsafe() {
271
275
  // Continue anyway - we'll try to attach to the new KB
272
276
  }
273
277
  // Ensure new branch KB exists
274
- ensureBranchKbExists(workspaceRoot, targetBranch);
278
+ const createdEmptyBranch = ensureBranchKbExists(workspaceRoot, targetBranch);
275
279
  // Attach to new branch KB
276
280
  const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
277
281
  await assertGeneration();
278
282
  if (!attachResult.success) {
279
283
  throw new Error(`Failed to attach to new branch KB: ${attachResult.error || "Unknown error"}`);
280
284
  }
285
+ if (createdEmptyBranch) {
286
+ const initialSaveResult = await prologProcess.query("kb_save");
287
+ await assertGeneration();
288
+ if (!initialSaveResult.success) {
289
+ throw new Error(`Failed to initialize empty branch KB: ${initialSaveResult.error || "Unknown error"}`);
290
+ }
291
+ }
281
292
  activeBranchName = targetBranch;
282
293
  attachedBranchKbPath = kbPath;
283
294
  attachedBranchStamp = await readBranchKbStamp(kbPath);
@@ -327,13 +338,20 @@ async function ensurePrologUnsafe() {
327
338
  debugLog(`[KIBI-MCP] KIBI_BRANCH env: ${envBranch || "not set"}`);
328
339
  debugLog(`[KIBI-MCP] Resolved branch: ${targetBranch}`);
329
340
  activeBranchName = targetBranch;
330
- ensureBranchKbExists(workspaceRoot, targetBranch);
341
+ const createdEmptyBranch = ensureBranchKbExists(workspaceRoot, targetBranch);
331
342
  const kbPath = sessionDeps.resolveKbPath(workspaceRoot, targetBranch);
332
343
  const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
333
344
  await assertGeneration();
334
345
  if (!attachResult.success) {
335
346
  throw new Error(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
336
347
  }
348
+ if (createdEmptyBranch) {
349
+ const initialSaveResult = await prologProcess.query("kb_save");
350
+ await assertGeneration();
351
+ if (!initialSaveResult.success) {
352
+ throw new Error(`Failed to initialize empty branch KB: ${initialSaveResult.error || "Unknown error"}`);
353
+ }
354
+ }
337
355
  attachedBranchKbPath = kbPath;
338
356
  attachedBranchStamp = await readBranchKbStamp(kbPath);
339
357
  isInitialized = true;
@@ -1,3 +1,4 @@
1
+ import { getSpec } from "kibi-cli/operations";
1
2
  // implements REQ-002, REQ-013
2
3
  export function registerConfiguredTools(server, runtime, registerTool) {
3
4
  const toolDef = (name) => {
@@ -6,9 +7,23 @@ export function registerConfiguredTools(server, runtime, registerTool) {
6
7
  throw new Error(`Unknown tool: ${name}`);
7
8
  return t;
8
9
  };
9
- const register = ({ name, handler }) => {
10
+ const prologFor = (context) => {
11
+ const prolog = runtime.operationRuntime.sessionProlog(context);
12
+ if (!prolog) {
13
+ throw new Error("Operation requires a session Prolog process");
14
+ }
15
+ return prolog;
16
+ };
17
+ const register = ({ name, execute }) => {
10
18
  const definition = toolDef(name);
11
- registerTool(server, name, definition.description, definition.inputSchema, handler, runtime);
19
+ const publicSpec = getSpec(name);
20
+ const spec = {
21
+ name,
22
+ effects: publicSpec.effects,
23
+ requiresProlog: publicSpec.requiresProlog,
24
+ execute: (args, context) => execute(context, args),
25
+ };
26
+ registerTool(server, name, definition.description, definition.inputSchema, async () => undefined, runtime, spec, definition.annotations);
12
27
  };
13
28
  // INTENTIONAL ARGUMENT CASTS: The `args as (unknown as)? XyzArgs` casts below
14
29
  // bridge the generic ToolHandler (which receives Record<string, unknown>) to the
@@ -16,116 +31,74 @@ export function registerConfiguredTools(server, runtime, registerTool) {
16
31
  // (via jsonSchemaToZod) before the handler is invoked, so the casts are safe at runtime.
17
32
  register({
18
33
  name: "kb_query",
19
- handler: async (args) => {
20
- const prolog = await runtime.ensureProlog();
21
- return runtime.handleKbQuery(prolog, args);
22
- },
34
+ execute: async (context, args) => runtime.handleKbQuery(prologFor(context), args),
23
35
  });
24
36
  register({
25
37
  name: "kb_search",
26
- handler: async (args) => {
27
- const prolog = await runtime.ensureProlog();
28
- return runtime.handleKbSearch(prolog, args);
29
- },
38
+ execute: async (context, args) => runtime.handleKbSearch(prologFor(context), args),
30
39
  });
31
40
  register({
32
41
  name: "kb_status",
33
- handler: async (args) => {
34
- const prolog = await runtime.ensureProlog();
35
- return runtime.handleKbStatus(prolog, args);
36
- },
42
+ execute: async (context, args) => runtime.handleKbStatus(prologFor(context), args),
37
43
  });
38
44
  register({
39
45
  name: "kb_skills_list",
40
- handler: async (args) => runtime.handleKbSkillsList(args),
46
+ execute: async (_context, args) => runtime.handleKbSkillsList(args),
41
47
  });
42
48
  register({
43
49
  name: "kb_skills_load",
44
- handler: async (args) => runtime.handleKbSkillsLoad(args),
50
+ execute: async (_context, args) => runtime.handleKbSkillsLoad(args),
45
51
  });
46
52
  register({
47
53
  name: "kb_skills_read",
48
- handler: async (args) => runtime.handleKbSkillsRead(args),
54
+ execute: async (_context, args) => runtime.handleKbSkillsRead(args),
49
55
  });
50
56
  register({
51
57
  name: "kb_find_gaps",
52
- handler: async (args) => {
53
- const prolog = await runtime.ensureProlog();
54
- return runtime.handleKbFindGaps(prolog, args);
55
- },
58
+ execute: async (context, args) => runtime.handleKbFindGaps(prologFor(context), args),
56
59
  });
57
60
  register({
58
61
  name: "kb_coverage",
59
- handler: async (args) => {
60
- const prolog = await runtime.ensureProlog();
61
- return runtime.handleKbCoverage(prolog, args);
62
- },
62
+ execute: async (context, args) => runtime.handleKbCoverage(prologFor(context), args),
63
63
  });
64
64
  register({
65
65
  name: "kb_graph",
66
- handler: async (args) => {
67
- const prolog = await runtime.ensureProlog();
68
- return runtime.handleKbGraph(prolog, args);
69
- },
66
+ execute: async (context, args) => runtime.handleKbGraph(prologFor(context), args),
70
67
  });
71
68
  register({
72
69
  name: "kb_sparql_remote",
73
- handler: async (args) => {
74
- const prolog = await runtime.ensureProlog();
75
- return runtime.handleSparql(prolog, args);
76
- },
70
+ execute: async (context, args) => runtime.handleSparql(args, context),
77
71
  });
78
72
  register({
79
73
  name: "kb_semantic_advisor",
80
- handler: async (args) => runtime.handleKbSemanticAdvisor(args),
74
+ execute: async (_context, args) => runtime.handleKbSemanticAdvisor(args),
81
75
  });
82
76
  register({
83
77
  name: "kb_upsert",
84
- handler: async (args) => {
85
- const prolog = await runtime.ensureProlog();
86
- return runtime.handleKbUpsert(prolog, args);
87
- },
78
+ execute: async (context, args) => runtime.handleKbUpsert(prologFor(context), args),
88
79
  });
89
80
  register({
90
81
  name: "kb_validate_upsert",
91
- handler: async (args) => {
92
- const prolog = await runtime.ensureProlog();
93
- return runtime.handleKbValidateUpsert(prolog, args);
94
- },
82
+ execute: async (context, args) => runtime.handleKbValidateUpsert(prologFor(context), args),
95
83
  });
96
84
  register({
97
85
  name: "kb_delete",
98
- handler: async (args) => {
99
- const prolog = await runtime.ensureProlog();
100
- return runtime.handleKbDelete(prolog, args);
101
- },
86
+ execute: async (context, args) => runtime.handleKbDelete(prologFor(context), args),
102
87
  });
103
88
  register({
104
89
  name: "kb_check",
105
- handler: async (args) => {
106
- const prolog = await runtime.ensureProlog();
107
- return runtime.handleKbCheck(prolog, args);
108
- },
90
+ execute: async (context, args) => runtime.handleKbCheck(prologFor(context), args),
109
91
  });
110
92
  register({
111
93
  name: "kb_model_requirement",
112
- handler: async (args) => {
113
- const prolog = await runtime.ensureProlog();
114
- return runtime.handleKbModelRequirement(prolog, args);
115
- },
94
+ execute: async (context, args) => runtime.handleKbModelRequirement(prologFor(context), args),
116
95
  });
117
96
  register({
118
97
  name: "kb_suggest_predicates",
119
- handler: async (args) => {
120
- const prolog = await runtime.ensureProlog();
121
- return runtime.handleKbSuggestPredicates(prolog, args);
122
- },
98
+ execute: async (context, args) => runtime.handleKbSuggestPredicates(prologFor(context), args),
123
99
  });
124
100
  register({
125
101
  name: "kb_autopilot_generate",
126
- handler: async (args) => {
127
- const prolog = await runtime.ensureProlog();
128
- return runtime.handleKbAutopilotGenerate(prolog, args);
129
- },
102
+ execute: async (context, args) => runtime.handleKbAutopilotGenerate(args, context),
130
103
  });
131
104
  }
@@ -1,4 +1,7 @@
1
+ import { getSpec } from "kibi-cli/operations";
1
2
  import { DIAGNOSTIC_MODE_ENABLED, appendUsageLogLine, classifyDiagnosticError, deriveDiagnosticFields, extractToolCallPayload, } from "../diagnostics.js";
3
+ import { isMcpDebugEnabled } from "../env.js";
4
+ import { createMcpRuntime } from "../runtime/mcp-runtime.js";
2
5
  import { TOOLS } from "../tools-config.js";
3
6
  import { handleKbAutopilotGenerate } from "../tools/autopilot-generate.js";
4
7
  import { handleKbCheck } from "../tools/check.js";
@@ -11,15 +14,17 @@ import { handleKbQuery } from "../tools/query.js";
11
14
  import { handleKbSearch } from "../tools/search.js";
12
15
  import { handleKbSemanticAdvisor } from "../tools/semantic-advisor.js";
13
16
  import { handleKbSkillsList, handleKbSkillsLoad, handleKbSkillsRead, } from "../tools/skills.js";
14
- import { handleSparql } from "../tools/sparql.js";
15
17
  import { handleKbStatus } from "../tools/status.js";
16
18
  import { handleKbSuggestPredicates } from "../tools/suggest-predicates.js";
17
19
  import { handleKbUpsert } from "../tools/upsert.js";
18
20
  import { handleKbValidateUpsert } from "../tools/validate-upsert.js";
21
+ import { resolveWorkspaceRoot } from "../workspace.js";
22
+ import { readBranchKbStamp } from "./kb-freshness.js";
19
23
  const defaultToolsServerDeps = {
20
24
  getSessionModule: () => import("./session.js"),
21
25
  };
22
26
  let sessionModulePromise = null;
27
+ const prologPorts = new WeakMap();
23
28
  // implements REQ-008
24
29
  export function _setToolsServerDepsForTests(deps, resetPromise = false) {
25
30
  defaultToolsServerDeps.getSessionModule =
@@ -39,6 +44,50 @@ async function getSessionModule() {
39
44
  sessionModulePromise ??= defaultToolsServerDeps.getSessionModule();
40
45
  return sessionModulePromise;
41
46
  }
47
+ function adaptProlog(prolog) {
48
+ const existing = prologPorts.get(prolog);
49
+ if (existing) {
50
+ return existing;
51
+ }
52
+ let lastResult = null;
53
+ const port = {
54
+ query: async (goal) => {
55
+ lastResult = await prolog.query(goal);
56
+ return lastResult;
57
+ },
58
+ nextSolution: async () => {
59
+ const result = lastResult;
60
+ lastResult = null;
61
+ return result;
62
+ },
63
+ save: () => prolog.query("kb_save"),
64
+ };
65
+ prologPorts.set(prolog, port);
66
+ return port;
67
+ }
68
+ const operationRuntime = createMcpRuntime({
69
+ workspaceRoot: resolveWorkspaceRoot(),
70
+ activeBranchName: async () => (await getSessionModule()).activeBranchName,
71
+ attachedBranchKbPath: async () => (await getSessionModule()).attachedBranchKbPath,
72
+ ensureProlog: async () => (await getSessionModule()).ensureProlog(),
73
+ adaptProlog,
74
+ net: { fetch: (input, init) => globalThis.fetch(input, init) },
75
+ refreshAttachedBranchStamp: async () => {
76
+ const session = await getSessionModule();
77
+ const kbPath = session.attachedBranchKbPath;
78
+ if (kbPath) {
79
+ try {
80
+ session.updateAttachedBranchStamp(await readBranchKbStamp(kbPath));
81
+ }
82
+ catch (error) {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ if (isMcpDebugEnabled()) {
85
+ console.warn("[KIBI-MCP] Attached branch stamp refresh failed:", message);
86
+ }
87
+ }
88
+ }
89
+ },
90
+ });
42
91
  export const DEFAULT_TOOLS_RUNTIME = {
43
92
  diagnosticModeEnabled: () => DIAGNOSTIC_MODE_ENABLED,
44
93
  appendUsageLogLine,
@@ -55,12 +104,13 @@ export const DEFAULT_TOOLS_RUNTIME = {
55
104
  inFlightRequests: async () => (await getSessionModule()).inFlightRequests,
56
105
  isShuttingDown: async () => (await getSessionModule()).isShuttingDown,
57
106
  prologProcess: async () => (await getSessionModule()).prologProcess,
107
+ operationRuntime,
58
108
  handleKbCheck,
59
109
  handleKbCoverage,
60
110
  handleKbDelete,
61
111
  handleKbFindGaps,
62
112
  handleKbGraph,
63
- handleSparql,
113
+ handleSparql: (args, context) => getSpec("kb_sparql_remote").execute(args, context),
64
114
  handleKbQuery,
65
115
  handleKbSearch,
66
116
  handleKbStatus,
@@ -16,6 +16,7 @@
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-cli/operations/runtime-types";
19
20
  import { isMcpDebugEnabled } from "../env.js";
20
21
  import { appendDiagnosticErrorUsage, appendDiagnosticSuccessUsage, } from "./diagnostic-usage.js";
21
22
  import { jsonSchemaToZod } from "./json-schema-to-zod.js";
@@ -73,7 +74,7 @@ export function addTool(server, name, description, inputSchema, handler,
73
74
  // INTENTIONAL: DEFAULT_TOOLS_RUNTIME is typed as ToolsRuntime<PrologProcess>; the
74
75
  // generic TProlog parameter exists so tests can inject a mock type. The cast is safe
75
76
  // because the runtime object satisfies the full ToolsRuntime contract at runtime.
76
- runtime = DEFAULT_TOOLS_RUNTIME) {
77
+ runtime = DEFAULT_TOOLS_RUNTIME, spec, annotations) {
77
78
  const wrappedHandler = async (args) => {
78
79
  const startedAt = new Date();
79
80
  const diagnosticModeEnabled = runtime.diagnosticModeEnabled();
@@ -100,21 +101,30 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
100
101
  }
101
102
  // Track the handler promise in inFlightRequests Map
102
103
  const trackedRequests = await runtime.inFlightRequests();
103
- const handlerPromise = handler(businessArgs);
104
+ const controller = new AbortController();
105
+ const operationSpec = spec ?? {
106
+ name,
107
+ effects: ["local-read"],
108
+ requiresProlog: false,
109
+ execute: async (input, _context) => handler(input),
110
+ };
111
+ const handlerPromise = executeOperation(runtime.operationRuntime, operationSpec, businessArgs, { signal: controller.signal });
104
112
  trackedRequests.set(requestId, handlerPromise);
105
113
  let resetAttempted = false;
106
114
  let resetSucceeded = false;
107
115
  let resetError = null;
108
116
  try {
109
117
  // Execute handler
110
- const result = await withToolTimeout(name, handlerPromise, async () => {
118
+ const result = await withToolTimeout(name, handlerPromise, async (error) => {
111
119
  resetAttempted = true;
120
+ controller.abort(error);
112
121
  try {
113
122
  await runtime.resetProlog(`tool timeout: ${name}`);
114
123
  resetSucceeded = true;
115
124
  }
116
125
  catch (error) {
117
- resetError = error instanceof Error ? error.message : String(error);
126
+ resetError =
127
+ error instanceof Error ? error.message : String(error);
118
128
  }
119
129
  });
120
130
  // Log usage in diagnostic mode
@@ -163,7 +173,11 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
163
173
  throw new Error(`Tool ${name} failed: ${err.message}`, { cause: err });
164
174
  }
165
175
  };
166
- server.registerTool(name, { description, inputSchema: jsonSchemaToZod(inputSchema) }, wrappedHandler);
176
+ server.registerTool(name, {
177
+ description,
178
+ inputSchema: jsonSchemaToZod(inputSchema),
179
+ ...(annotations ? { annotations } : {}),
180
+ }, wrappedHandler);
167
181
  }
168
182
  // implements REQ-002, REQ-013
169
183
  export function registerAllTools(server,
@@ -23,7 +23,7 @@ function debugLog(...args) {
23
23
  console.error(...args);
24
24
  }
25
25
  }
26
- export function setupTransportHandlers(server, transport) {
26
+ export function setupTransportHandlers(_server, transport) {
27
27
  transport.onerror = (error) => {
28
28
  // Stdio transport surfaces JSON parse / schema validation failures via onerror.
29
29
  // Those errors should not crash the server: emit a JSON-RPC error (id omitted)
@@ -62,11 +62,15 @@ export function setupTransportHandlers(server, transport) {
62
62
  debugLog("[KIBI-MCP] Transport closed");
63
63
  initiateGracefulShutdown(0);
64
64
  };
65
- process.on("SIGTERM", () => {
66
- debugLog("[KIBI-MCP] Received SIGTERM, initiating graceful shutdown");
67
- void initiateGracefulShutdown(0);
68
- });
65
+ for (const signal of ["SIGINT", "SIGTERM"]) {
66
+ process.on(signal, () => {
67
+ debugLog(`[KIBI-MCP] Received ${signal}, initiating graceful shutdown`);
68
+ void initiateGracefulShutdown(0);
69
+ });
70
+ }
69
71
  }
72
+ // implements REQ-002
73
+ // covered_by TEST-mcp-kb-freshness
70
74
  export async function connectTransport(server, transport) {
71
75
  await server.connect(transport);
72
76
  }