kibi-mcp 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tools/check.js +118 -1
- package/dist/tools/delete.js +17 -27
- package/dist/tools/query.js +65 -22
- package/dist/tools/upsert.js +10 -3
- package/package.json +2 -2
package/dist/tools/check.js
CHANGED
|
@@ -42,8 +42,29 @@
|
|
|
42
42
|
fi
|
|
43
43
|
done
|
|
44
44
|
*/
|
|
45
|
+
import { existsSync } from "node:fs";
|
|
46
|
+
import { createRequire } from "node:module";
|
|
45
47
|
import * as path from "node:path";
|
|
46
48
|
import { parsePairList } from "./prolog-list.js";
|
|
49
|
+
const require = createRequire(import.meta.url);
|
|
50
|
+
function resolveChecksPlPath() {
|
|
51
|
+
const overrideChecksPath = process.env.KIBI_CHECKS_PL_PATH;
|
|
52
|
+
if (overrideChecksPath && existsSync(overrideChecksPath)) {
|
|
53
|
+
return overrideChecksPath;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const installedChecksPl = require.resolve("kibi-core/src/checks.pl");
|
|
57
|
+
if (existsSync(installedChecksPl)) {
|
|
58
|
+
return installedChecksPl;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
const localChecksPl = path.join(process.cwd(), "packages/core/src/checks.pl");
|
|
63
|
+
if (existsSync(localChecksPl)) {
|
|
64
|
+
return localChecksPl;
|
|
65
|
+
}
|
|
66
|
+
throw new Error("Unable to resolve checks.pl path");
|
|
67
|
+
}
|
|
47
68
|
function formatDiagnosticsForMcp(diagnostics) {
|
|
48
69
|
return diagnostics.map((d) => ({
|
|
49
70
|
category: d.category,
|
|
@@ -61,7 +82,7 @@ export async function handleKbCheck(prolog, args) {
|
|
|
61
82
|
const { rules } = args;
|
|
62
83
|
try {
|
|
63
84
|
const violations = [];
|
|
64
|
-
|
|
85
|
+
let allEntityIds = null;
|
|
65
86
|
// Run all validation rules (or specific rules if provided)
|
|
66
87
|
const allRules = [
|
|
67
88
|
"must-priority-coverage",
|
|
@@ -71,6 +92,33 @@ export async function handleKbCheck(prolog, args) {
|
|
|
71
92
|
"symbol-coverage",
|
|
72
93
|
];
|
|
73
94
|
const rulesToRun = rules && rules.length > 0 ? rules : allRules;
|
|
95
|
+
const rulesAllowlist = new Set(rulesToRun);
|
|
96
|
+
const aggregatedViolations = await runAggregatedChecks(prolog, rulesAllowlist);
|
|
97
|
+
if (aggregatedViolations) {
|
|
98
|
+
const diagnostics = aggregatedViolations.map((v) => ({
|
|
99
|
+
category: "SYNC_ERROR",
|
|
100
|
+
severity: "error",
|
|
101
|
+
message: v.description,
|
|
102
|
+
file: v.source,
|
|
103
|
+
suggestion: v.suggestion,
|
|
104
|
+
}));
|
|
105
|
+
const summary = aggregatedViolations.length === 0
|
|
106
|
+
? "No violations found"
|
|
107
|
+
: `${aggregatedViolations.length} violations found`;
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: summary,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
structuredContent: {
|
|
116
|
+
violations: aggregatedViolations,
|
|
117
|
+
count: aggregatedViolations.length,
|
|
118
|
+
diagnostics: formatDiagnosticsForMcp(diagnostics),
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
74
122
|
if (rulesToRun.includes("must-priority-coverage")) {
|
|
75
123
|
violations.push(...(await checkMustPriorityCoverage(prolog)));
|
|
76
124
|
}
|
|
@@ -81,6 +129,9 @@ export async function handleKbCheck(prolog, args) {
|
|
|
81
129
|
violations.push(...(await checkNoCycles(prolog)));
|
|
82
130
|
}
|
|
83
131
|
if (rulesToRun.includes("required-fields")) {
|
|
132
|
+
if (!allEntityIds) {
|
|
133
|
+
allEntityIds = await getAllEntityIds(prolog);
|
|
134
|
+
}
|
|
84
135
|
violations.push(...(await checkRequiredFields(prolog, allEntityIds)));
|
|
85
136
|
}
|
|
86
137
|
if (rulesToRun.includes("symbol-coverage")) {
|
|
@@ -115,6 +166,72 @@ export async function handleKbCheck(prolog, args) {
|
|
|
115
166
|
throw new Error(`Check execution failed: ${message}`);
|
|
116
167
|
}
|
|
117
168
|
}
|
|
169
|
+
async function runAggregatedChecks(prolog, rulesAllowlist) {
|
|
170
|
+
const checksPlPath = resolveChecksPlPath();
|
|
171
|
+
const normalizedChecksPlPath = checksPlPath.replace(/\\/g, "/");
|
|
172
|
+
const checksPlPathEscaped = normalizedChecksPlPath.replace(/'/g, "''");
|
|
173
|
+
const violations = [];
|
|
174
|
+
const ruleToPredicate = {
|
|
175
|
+
"must-priority-coverage": "check_must_priority_coverage",
|
|
176
|
+
"no-dangling-refs": "check_no_dangling_refs",
|
|
177
|
+
"no-cycles": "check_no_cycles",
|
|
178
|
+
"required-fields": "check_required_fields",
|
|
179
|
+
"symbol-coverage": "check_symbol_coverage",
|
|
180
|
+
};
|
|
181
|
+
for (const rule of rulesAllowlist) {
|
|
182
|
+
const predicate = ruleToPredicate[rule];
|
|
183
|
+
if (!predicate) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const query = `(use_module('${checksPlPathEscaped}'), call(checks:${predicate}(Violations)), findall(_{rule:Rule,entityId:EntityId,description:Description,suggestion:Suggestion,source:Source}, member(violation(Rule, EntityId, Description, Suggestion, Source), Violations), Rows), call(checks:atom_json_dict(JsonString, Rows, [])))`;
|
|
187
|
+
const result = await prolog.query(query);
|
|
188
|
+
if (!result.success || !result.bindings.JsonString) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
let parsedRows;
|
|
192
|
+
try {
|
|
193
|
+
parsedRows = JSON.parse(result.bindings.JsonString);
|
|
194
|
+
if (typeof parsedRows === "string") {
|
|
195
|
+
parsedRows = JSON.parse(parsedRows);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
if (!Array.isArray(parsedRows)) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
for (const row of parsedRows) {
|
|
205
|
+
if (!row || typeof row !== "object") {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const raw = row;
|
|
209
|
+
const rule = typeof raw.rule === "string" ? raw.rule : "";
|
|
210
|
+
if (!rulesAllowlist.has(rule)) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const entityId = typeof raw.entityId === "string"
|
|
214
|
+
? raw.entityId
|
|
215
|
+
: typeof raw.entity_id === "string"
|
|
216
|
+
? raw.entity_id
|
|
217
|
+
: "";
|
|
218
|
+
const description = typeof raw.description === "string" ? raw.description : "";
|
|
219
|
+
const suggestion = typeof raw.suggestion === "string" ? raw.suggestion : undefined;
|
|
220
|
+
const source = typeof raw.source === "string" ? raw.source : undefined;
|
|
221
|
+
if (!rule || !entityId || !description) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
violations.push({
|
|
225
|
+
rule,
|
|
226
|
+
entityId,
|
|
227
|
+
description,
|
|
228
|
+
suggestion,
|
|
229
|
+
source,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return violations;
|
|
234
|
+
}
|
|
118
235
|
async function checkMustPriorityCoverage(prolog) {
|
|
119
236
|
const violations = [];
|
|
120
237
|
const gapsResult = await prolog.query("setof([Req,Reason], coverage_gap(Req, Reason), Rows)");
|
package/dist/tools/delete.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
function escapeAtom(value) {
|
|
19
|
-
return value.replace(/'/g, "
|
|
19
|
+
return value.replace(/'/g, "''");
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
22
|
* Handle kb.delete tool calls
|
|
@@ -32,8 +32,9 @@ export async function handleKbDelete(prolog, args) {
|
|
|
32
32
|
const errors = [];
|
|
33
33
|
try {
|
|
34
34
|
for (const id of ids) {
|
|
35
|
+
const safeId = escapeAtom(id);
|
|
35
36
|
// Check if entity exists
|
|
36
|
-
const checkGoal = `once(kb_entity('${
|
|
37
|
+
const checkGoal = `once(kb_entity('${safeId}', _, _))`;
|
|
37
38
|
const checkResult = await prolog.query(checkGoal);
|
|
38
39
|
if (!checkResult.success) {
|
|
39
40
|
errors.push(`Entity ${id} does not exist`);
|
|
@@ -41,36 +42,24 @@ export async function handleKbDelete(prolog, args) {
|
|
|
41
42
|
continue;
|
|
42
43
|
}
|
|
43
44
|
// Check for dependents (entities that reference this one)
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
for (const relType of relTypes) {
|
|
57
|
-
const dependentsGoal = `findall(From, kb_relationship(${relType}, From, '${id}'), Dependents)`;
|
|
58
|
-
const dependentsResult = await prolog.query(dependentsGoal);
|
|
59
|
-
if (dependentsResult.success && dependentsResult.bindings.Dependents) {
|
|
60
|
-
const dependentsStr = dependentsResult.bindings.Dependents;
|
|
61
|
-
if (dependentsStr !== "[]") {
|
|
62
|
-
errors.push(`Cannot delete entity ${id}: has dependents (other entities reference it via ${relType})`);
|
|
63
|
-
skipped++;
|
|
64
|
-
hasDependents = true;
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
45
|
+
const dependentsGoal = `findall([RelType,From], (member(RelType, [depends_on, verified_by, validates, specified_by, relates_to, guards, publishes, consumes]), kb_relationship(RelType, From, '${safeId}')), Dependents)`;
|
|
46
|
+
const dependentsResult = await prolog.query(dependentsGoal);
|
|
47
|
+
if (!dependentsResult.success) {
|
|
48
|
+
errors.push(`Failed to inspect dependents for entity ${id}: ${dependentsResult.error ?? "Query failed"}`);
|
|
49
|
+
skipped++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const hasDependents = dependentsResult.bindings.Dependents !== undefined &&
|
|
53
|
+
dependentsResult.bindings.Dependents !== "[]";
|
|
54
|
+
if (hasDependents) {
|
|
55
|
+
errors.push(`Cannot delete entity ${id}: has dependents (other entities reference it)`);
|
|
56
|
+
skipped++;
|
|
68
57
|
}
|
|
69
58
|
if (hasDependents) {
|
|
70
59
|
continue;
|
|
71
60
|
}
|
|
72
61
|
// No dependents, safe to delete
|
|
73
|
-
const deleteGoal = `kb_retract_entity('${
|
|
62
|
+
const deleteGoal = `kb_retract_entity('${safeId}')`;
|
|
74
63
|
const deleteResult = await prolog.query(deleteGoal);
|
|
75
64
|
if (!deleteResult.success) {
|
|
76
65
|
errors.push(`Failed to delete entity ${id}: ${deleteResult.error || "Unknown error"}`);
|
|
@@ -82,6 +71,7 @@ export async function handleKbDelete(prolog, args) {
|
|
|
82
71
|
}
|
|
83
72
|
// Save KB to disk
|
|
84
73
|
await prolog.query("kb_save");
|
|
74
|
+
prolog.invalidateCache();
|
|
85
75
|
return {
|
|
86
76
|
content: [
|
|
87
77
|
{
|
package/dist/tools/query.js
CHANGED
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
|
+
/**
|
|
19
|
+
* Escape a string for embedding inside a single-quoted Prolog atom.
|
|
20
|
+
* Doubles single-quote characters per ISO Prolog standard.
|
|
21
|
+
*/
|
|
22
|
+
function escapeAtomContent(value) {
|
|
23
|
+
return value.replace(/'/g, "''");
|
|
24
|
+
}
|
|
18
25
|
export const VALID_ENTITY_TYPES = [
|
|
19
26
|
"req",
|
|
20
27
|
"scenario",
|
|
@@ -42,58 +49,63 @@ export async function handleKbQuery(prolog, args) {
|
|
|
42
49
|
// Build Prolog query
|
|
43
50
|
let goal;
|
|
44
51
|
if (sourceFile) {
|
|
45
|
-
const safeSource = sourceFile
|
|
52
|
+
const safeSource = escapeAtomContent(sourceFile);
|
|
46
53
|
if (type) {
|
|
47
|
-
|
|
54
|
+
const safeType = escapeAtomContent(type);
|
|
55
|
+
goal = `findall([Id,'${safeType}',Props], (kb_entities_by_source('${safeSource}', SourceIds), member(Id, SourceIds), kb_entity(Id, '${safeType}', Props)), Results)`;
|
|
48
56
|
}
|
|
49
57
|
else {
|
|
50
58
|
goal = `findall([Id,Type,Props], (kb_entities_by_source('${safeSource}', SourceIds), member(Id, SourceIds), kb_entity(Id, Type, Props)), Results)`;
|
|
51
59
|
}
|
|
52
60
|
}
|
|
53
61
|
else if (id && type) {
|
|
54
|
-
|
|
62
|
+
const safeId = escapeAtomContent(id);
|
|
63
|
+
const safeType = escapeAtomContent(type);
|
|
64
|
+
goal = `findall(['${safeId}','${safeType}',Props], kb_entity('${safeId}', '${safeType}', Props), Results)`;
|
|
55
65
|
}
|
|
56
66
|
else if (id) {
|
|
57
|
-
|
|
67
|
+
const safeId = escapeAtomContent(id);
|
|
68
|
+
goal = `findall(['${safeId}',Type,Props], kb_entity('${safeId}', Type, Props), Results)`;
|
|
58
69
|
}
|
|
59
70
|
else if (tags && tags.length > 0) {
|
|
60
|
-
|
|
71
|
+
// TODO: Reintroduce server-side (Prolog) tag filtering once normalization
|
|
72
|
+
// issues with tag list formats are resolved, to avoid fetching all entities
|
|
73
|
+
// before filtering in JS for large knowledge bases.
|
|
61
74
|
if (type) {
|
|
62
|
-
|
|
75
|
+
const safeType = escapeAtomContent(type);
|
|
76
|
+
goal = `findall([Id,'${safeType}',Props], kb_entity(Id, '${safeType}', Props), Results)`;
|
|
63
77
|
}
|
|
64
78
|
else {
|
|
65
|
-
goal =
|
|
79
|
+
goal = "findall([Id,Type,Props], kb_entity(Id, Type, Props), Results)";
|
|
66
80
|
}
|
|
67
81
|
}
|
|
68
82
|
else if (type) {
|
|
69
|
-
|
|
83
|
+
const safeType = escapeAtomContent(type);
|
|
84
|
+
goal = `findall([Id,'${safeType}',Props], kb_entity(Id, '${safeType}', Props), Results)`;
|
|
70
85
|
}
|
|
71
86
|
else {
|
|
72
87
|
goal = "findall([Id,Type,Props], kb_entity(Id, Type, Props), Results)";
|
|
73
88
|
}
|
|
74
89
|
const queryResult = await prolog.query(goal);
|
|
75
90
|
if (queryResult.success) {
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const entity =
|
|
80
|
-
results
|
|
91
|
+
if (queryResult.bindings.Results) {
|
|
92
|
+
const entitiesData = parseListOfLists(queryResult.bindings.Results);
|
|
93
|
+
for (const data of entitiesData) {
|
|
94
|
+
const entity = parseEntityFromList(data);
|
|
95
|
+
results.push(entity);
|
|
81
96
|
}
|
|
82
97
|
}
|
|
83
|
-
else {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const entitiesData = parseListOfLists(queryResult.bindings.Results);
|
|
87
|
-
for (const data of entitiesData) {
|
|
88
|
-
const entity = parseEntityFromList(data);
|
|
89
|
-
results.push(entity);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
98
|
+
else if (queryResult.bindings.Result) {
|
|
99
|
+
const entity = parseEntityFromBinding(queryResult.bindings.Result);
|
|
100
|
+
results = [entity];
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
else {
|
|
95
104
|
throw new Error(queryResult.error || "Query failed with unknown error");
|
|
96
105
|
}
|
|
106
|
+
if (tags && tags.length > 0) {
|
|
107
|
+
results = dedupeEntities(results.filter((entity) => hasAnyTag(entity, tags)));
|
|
108
|
+
}
|
|
97
109
|
// Apply pagination
|
|
98
110
|
const paginated = results.slice(offset, offset + limit);
|
|
99
111
|
// Build human-readable text with entity IDs and titles
|
|
@@ -131,6 +143,37 @@ export async function handleKbQuery(prolog, args) {
|
|
|
131
143
|
throw new Error(`Query execution failed: ${message}`);
|
|
132
144
|
}
|
|
133
145
|
}
|
|
146
|
+
function hasAnyTag(entity, requestedTags) {
|
|
147
|
+
const expected = new Set(requestedTags.map(normalizeTagValue));
|
|
148
|
+
const rawTags = entity.tags;
|
|
149
|
+
if (!Array.isArray(rawTags) || rawTags.length === 0) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
for (const tag of rawTags) {
|
|
153
|
+
if (expected.has(normalizeTagValue(tag))) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
function normalizeTagValue(tag) {
|
|
160
|
+
return String(tag).trim();
|
|
161
|
+
}
|
|
162
|
+
function dedupeEntities(entities) {
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
const deduped = [];
|
|
165
|
+
for (const entity of entities) {
|
|
166
|
+
const id = String(entity.id ?? "");
|
|
167
|
+
const type = String(entity.type ?? "");
|
|
168
|
+
const key = `${type}::${id}`;
|
|
169
|
+
if (seen.has(key)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
seen.add(key);
|
|
173
|
+
deduped.push(entity);
|
|
174
|
+
}
|
|
175
|
+
return deduped;
|
|
176
|
+
}
|
|
134
177
|
/**
|
|
135
178
|
* Parse a Prolog list of lists into a JavaScript array.
|
|
136
179
|
* Input: "[[a,b,c],[d,e,f]]"
|
package/dist/tools/upsert.js
CHANGED
|
@@ -20,7 +20,13 @@ import entitySchema from "kibi-cli/schemas/entity";
|
|
|
20
20
|
import relationshipSchema from "kibi-cli/schemas/relationship";
|
|
21
21
|
import { refreshCoordinatesForSymbolId } from "./symbols.js";
|
|
22
22
|
function escapeAtom(value) {
|
|
23
|
-
return value.replace(/'/g, "
|
|
23
|
+
return value.replace(/'/g, "''");
|
|
24
|
+
}
|
|
25
|
+
function toPrologAtom(value) {
|
|
26
|
+
const simplePrologAtom = /^[a-z][a-zA-Z0-9_]*$/;
|
|
27
|
+
return simplePrologAtom.test(value)
|
|
28
|
+
? value
|
|
29
|
+
: `'${value.replace(/'/g, "''")}'`;
|
|
24
30
|
}
|
|
25
31
|
const ajv = new Ajv({ strict: false });
|
|
26
32
|
const validateEntity = ajv.compile(entitySchema);
|
|
@@ -118,6 +124,7 @@ export async function handleKbUpsert(prolog, args) {
|
|
|
118
124
|
}
|
|
119
125
|
// Save KB to disk
|
|
120
126
|
await prolog.query("kb_save");
|
|
127
|
+
prolog.invalidateCache();
|
|
121
128
|
let contradictionPairsDetected;
|
|
122
129
|
if (type === "req") {
|
|
123
130
|
contradictionPairsDetected = await detectContradictionPairs(prolog, id);
|
|
@@ -189,13 +196,13 @@ function buildPropertyList(entity) {
|
|
|
189
196
|
continue;
|
|
190
197
|
let prologValue;
|
|
191
198
|
if (key === "id" && typeof value === "string") {
|
|
192
|
-
prologValue = `'${value}'`;
|
|
199
|
+
prologValue = `'${value.replace(/'/g, "''")}'`;
|
|
193
200
|
}
|
|
194
201
|
else if (Array.isArray(value)) {
|
|
195
202
|
prologValue = JSON.stringify(value);
|
|
196
203
|
}
|
|
197
204
|
else if (ATOM_FIELDS.includes(key) && typeof value === "string") {
|
|
198
|
-
prologValue = value;
|
|
205
|
+
prologValue = toPrologAtom(value);
|
|
199
206
|
}
|
|
200
207
|
else if (STRING_FIELDS.includes(key) && typeof value === "string") {
|
|
201
208
|
prologValue = `"${escapeQuotes(value)}"`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
6
6
|
"ajv": "^8.18.0",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"gray-matter": "^4.0.3",
|
|
11
11
|
"js-yaml": "^4.1.0",
|
|
12
12
|
"kibi-cli": "^0.2.3",
|
|
13
|
-
"kibi-core": "^0.1.
|
|
13
|
+
"kibi-core": "^0.1.8",
|
|
14
14
|
"mcpcat": "^0.1.12",
|
|
15
15
|
"ts-morph": "^23.0.0",
|
|
16
16
|
"zod": "^4.3.6"
|