kibi-mcp 0.2.1 → 0.2.3
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 +28 -21
- 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,53 +49,53 @@ 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
|
-
const tagList = `[${tags.map((t) => `'${t}'`).join(",")}]`;
|
|
71
|
+
const tagList = `[${tags.map((t) => `'${escapeAtomContent(t)}'`).join(",")}]`;
|
|
61
72
|
if (type) {
|
|
62
|
-
|
|
73
|
+
const safeType = escapeAtomContent(type);
|
|
74
|
+
goal = `findall([Id,'${safeType}',Props], (kb_entity(Id, '${safeType}', Props), memberchk(tags=Tags, Props), member(Tag, Tags), member(Tag, ${tagList})), Results)`;
|
|
63
75
|
}
|
|
64
76
|
else {
|
|
65
77
|
goal = `findall([Id,Type,Props], (kb_entity(Id, Type, Props), memberchk(tags=Tags, Props), member(Tag, Tags), member(Tag, ${tagList})), Results)`;
|
|
66
78
|
}
|
|
67
79
|
}
|
|
68
80
|
else if (type) {
|
|
69
|
-
|
|
81
|
+
const safeType = escapeAtomContent(type);
|
|
82
|
+
goal = `findall([Id,'${safeType}',Props], kb_entity(Id, '${safeType}', Props), Results)`;
|
|
70
83
|
}
|
|
71
84
|
else {
|
|
72
85
|
goal = "findall([Id,Type,Props], kb_entity(Id, Type, Props), Results)";
|
|
73
86
|
}
|
|
74
87
|
const queryResult = await prolog.query(goal);
|
|
75
88
|
if (queryResult.success) {
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const entity =
|
|
80
|
-
results
|
|
89
|
+
if (queryResult.bindings.Results) {
|
|
90
|
+
const entitiesData = parseListOfLists(queryResult.bindings.Results);
|
|
91
|
+
for (const data of entitiesData) {
|
|
92
|
+
const entity = parseEntityFromList(data);
|
|
93
|
+
results.push(entity);
|
|
81
94
|
}
|
|
82
95
|
}
|
|
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
|
-
}
|
|
96
|
+
else if (queryResult.bindings.Result) {
|
|
97
|
+
const entity = parseEntityFromBinding(queryResult.bindings.Result);
|
|
98
|
+
results = [entity];
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
101
|
else {
|
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.3",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
6
6
|
"ajv": "^8.18.0",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"fast-glob": "^3.2.12",
|
|
10
10
|
"gray-matter": "^4.0.3",
|
|
11
11
|
"js-yaml": "^4.1.0",
|
|
12
|
-
"kibi-cli": "^0.2.
|
|
12
|
+
"kibi-cli": "^0.2.3",
|
|
13
13
|
"kibi-core": "^0.1.7",
|
|
14
14
|
"mcpcat": "^0.1.12",
|
|
15
15
|
"ts-morph": "^23.0.0",
|