@lexq/cli 0.1.32 → 0.1.33
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/index.js +147 -31
- package/dist/mcp/register.d.ts +36 -2
- package/dist/mcp/register.js +34 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -59,7 +59,7 @@ var ApiError = class extends Error {
|
|
|
59
59
|
statusCode;
|
|
60
60
|
errorCode;
|
|
61
61
|
};
|
|
62
|
-
async function
|
|
62
|
+
async function doFetch(method, path, options) {
|
|
63
63
|
const config = loadConfig();
|
|
64
64
|
const baseUrl = options.baseUrl ?? config.baseUrl;
|
|
65
65
|
const apiKey = options.apiKey ?? config.apiKey;
|
|
@@ -69,18 +69,11 @@ async function apiRequest(method, path, options = {}) {
|
|
|
69
69
|
const url = new URL(path, baseUrl.endsWith("/") ? baseUrl : baseUrl + "/");
|
|
70
70
|
if (options.params) {
|
|
71
71
|
for (const [key, value] of Object.entries(options.params)) {
|
|
72
|
-
if (value !== void 0 && value !== "")
|
|
73
|
-
url.searchParams.set(key, value);
|
|
74
|
-
}
|
|
72
|
+
if (value !== void 0 && value !== "") url.searchParams.set(key, value);
|
|
75
73
|
}
|
|
76
74
|
}
|
|
77
|
-
const headers = {
|
|
78
|
-
|
|
79
|
-
Accept: "application/json"
|
|
80
|
-
};
|
|
81
|
-
if (options.body) {
|
|
82
|
-
headers["Content-Type"] = "application/json";
|
|
83
|
-
}
|
|
75
|
+
const headers = { "X-API-KEY": apiKey, Accept: "application/json" };
|
|
76
|
+
if (options.body) headers["Content-Type"] = "application/json";
|
|
84
77
|
if (options.dryRun) {
|
|
85
78
|
const masked = apiKey.length > 8 ? apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length - 4) : "****";
|
|
86
79
|
console.log(`${method} ${url.toString()}`);
|
|
@@ -94,9 +87,7 @@ async function apiRequest(method, path, options = {}) {
|
|
|
94
87
|
console.log("\n(Use without --dry-run to execute)");
|
|
95
88
|
process.exit(0);
|
|
96
89
|
}
|
|
97
|
-
if (options.verbose) {
|
|
98
|
-
console.error(`\u2192 ${method} ${url.toString()}`);
|
|
99
|
-
}
|
|
90
|
+
if (options.verbose) console.error(`\u2192 ${method} ${url.toString()}`);
|
|
100
91
|
const startTime = Date.now();
|
|
101
92
|
const response = await fetch(url.toString(), {
|
|
102
93
|
method,
|
|
@@ -106,14 +97,9 @@ async function apiRequest(method, path, options = {}) {
|
|
|
106
97
|
if (options.verbose) {
|
|
107
98
|
console.error(`\u2190 ${response.status} ${response.statusText} (${Date.now() - startTime}ms)`);
|
|
108
99
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
if (response.status === 204 || contentType === "") {
|
|
114
|
-
return void 0;
|
|
115
|
-
}
|
|
116
|
-
const json = await response.json();
|
|
100
|
+
return response;
|
|
101
|
+
}
|
|
102
|
+
function assertOk(response, json) {
|
|
117
103
|
if (!response.ok || json.result !== "SUCCESS") {
|
|
118
104
|
throw new ApiError(
|
|
119
105
|
response.status,
|
|
@@ -121,7 +107,23 @@ async function apiRequest(method, path, options = {}) {
|
|
|
121
107
|
json.message ?? `Request failed with status ${response.status}`
|
|
122
108
|
);
|
|
123
109
|
}
|
|
124
|
-
|
|
110
|
+
}
|
|
111
|
+
async function apiRequest(method, path, options = {}) {
|
|
112
|
+
const { data } = await apiRequestWithMeta(method, path, options);
|
|
113
|
+
return data;
|
|
114
|
+
}
|
|
115
|
+
async function apiRequestWithMeta(method, path, options = {}) {
|
|
116
|
+
const response = await doFetch(method, path, options);
|
|
117
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
118
|
+
if (contentType.includes("text/csv") || contentType.includes("application/octet-stream")) {
|
|
119
|
+
return { data: response, meta: null };
|
|
120
|
+
}
|
|
121
|
+
if (response.status === 204 || contentType === "") {
|
|
122
|
+
return { data: void 0, meta: null };
|
|
123
|
+
}
|
|
124
|
+
const json = await response.json();
|
|
125
|
+
assertOk(response, json);
|
|
126
|
+
return { data: json.data, meta: json.meta ?? null };
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
// src/lib/output.ts
|
|
@@ -163,6 +165,23 @@ function printError(error) {
|
|
|
163
165
|
console.error(JSON.stringify({ error: "UNKNOWN", message: String(error) }, null, 2));
|
|
164
166
|
}
|
|
165
167
|
}
|
|
168
|
+
function printUnregisteredFactsWarning(facts) {
|
|
169
|
+
if (facts.length === 0) return;
|
|
170
|
+
const s = facts.length === 1 ? "" : "s";
|
|
171
|
+
console.error(
|
|
172
|
+
`
|
|
173
|
+
\u26A0 ${facts.length} undefined fact${s} referenced \u2014 register to enable validation:`
|
|
174
|
+
);
|
|
175
|
+
for (const f of facts) {
|
|
176
|
+
const type = f.inferredType ?? f.candidateTypes?.[0] ?? "STRING";
|
|
177
|
+
const note = f.conflict ? ` (ambiguous: ${f.candidateTypes?.join(" | ") ?? "?"})` : "";
|
|
178
|
+
console.error(` \u2022 ${f.key} \u2192 ${type}${note}`);
|
|
179
|
+
console.error(
|
|
180
|
+
` lexq facts create --key ${f.key} --name "${f.suggestedName}" --type ${type}`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
console.error("");
|
|
184
|
+
}
|
|
166
185
|
|
|
167
186
|
// src/commands/auth.ts
|
|
168
187
|
function registerAuthCommands(program) {
|
|
@@ -935,7 +954,7 @@ ${data.length} total`);
|
|
|
935
954
|
try {
|
|
936
955
|
const globalOpts = program.opts();
|
|
937
956
|
const body = JSON.parse(opts.json);
|
|
938
|
-
const data = await
|
|
957
|
+
const { data, meta } = await apiRequestWithMeta(
|
|
939
958
|
"POST",
|
|
940
959
|
`policy-groups/${opts.groupId}/versions/${opts.versionId}/rules`,
|
|
941
960
|
{
|
|
@@ -947,6 +966,7 @@ ${data.length} total`);
|
|
|
947
966
|
}
|
|
948
967
|
);
|
|
949
968
|
printJson(data);
|
|
969
|
+
printUnregisteredFactsWarning(meta?.unregisteredFacts ?? []);
|
|
950
970
|
} catch (error) {
|
|
951
971
|
printError(error);
|
|
952
972
|
process.exit(1);
|
|
@@ -970,7 +990,7 @@ ${data.length} total`);
|
|
|
970
990
|
try {
|
|
971
991
|
const globalOpts = program.opts();
|
|
972
992
|
const body = JSON.parse(opts.json);
|
|
973
|
-
const data = await
|
|
993
|
+
const { data, meta } = await apiRequestWithMeta(
|
|
974
994
|
"PUT",
|
|
975
995
|
`policy-groups/${opts.groupId}/versions/${opts.versionId}/rules/${opts.id}`,
|
|
976
996
|
{
|
|
@@ -982,6 +1002,7 @@ ${data.length} total`);
|
|
|
982
1002
|
}
|
|
983
1003
|
);
|
|
984
1004
|
printJson(data);
|
|
1005
|
+
printUnregisteredFactsWarning(meta?.unregisteredFacts ?? []);
|
|
985
1006
|
} catch (error) {
|
|
986
1007
|
printError(error);
|
|
987
1008
|
process.exit(1);
|
|
@@ -1099,6 +1120,7 @@ function registerFactCommands(program) {
|
|
|
1099
1120
|
|
|
1100
1121
|
Commands:
|
|
1101
1122
|
list List all fact definitions
|
|
1123
|
+
unregistered List facts referenced by a version but not yet defined
|
|
1102
1124
|
create Register a new fact
|
|
1103
1125
|
update Update fact metadata
|
|
1104
1126
|
delete Remove a fact definition
|
|
@@ -1143,6 +1165,52 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
1143
1165
|
process.exit(1);
|
|
1144
1166
|
}
|
|
1145
1167
|
});
|
|
1168
|
+
facts.command("unregistered").description("List facts referenced by a version but not yet defined").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").addHelpText(
|
|
1169
|
+
"after",
|
|
1170
|
+
dedent6`
|
|
1171
|
+
|
|
1172
|
+
Facts used in a version's rules that have no definition yet (read-only).
|
|
1173
|
+
Does not block publish/deploy — register them to enable validation.
|
|
1174
|
+
|
|
1175
|
+
Example:
|
|
1176
|
+
$ lexq facts unregistered --group-id <gid> --version-id <vid> --format table
|
|
1177
|
+
`
|
|
1178
|
+
).action(async (opts) => {
|
|
1179
|
+
try {
|
|
1180
|
+
const globalOpts = program.opts();
|
|
1181
|
+
const format = globalOpts.format ?? "json";
|
|
1182
|
+
const data = await apiRequest(
|
|
1183
|
+
"GET",
|
|
1184
|
+
`policy-groups/${opts.groupId}/versions/${opts.versionId}/unregistered-facts`,
|
|
1185
|
+
{
|
|
1186
|
+
apiKey: globalOpts.apiKey,
|
|
1187
|
+
baseUrl: globalOpts.baseUrl,
|
|
1188
|
+
dryRun: globalOpts.dryRun,
|
|
1189
|
+
verbose: globalOpts.verbose
|
|
1190
|
+
}
|
|
1191
|
+
);
|
|
1192
|
+
if (format === "table") {
|
|
1193
|
+
printTable(
|
|
1194
|
+
["Key", "Type", "Suggested Name", "Conflict", "Sources"],
|
|
1195
|
+
data.map((f) => [
|
|
1196
|
+
f.key,
|
|
1197
|
+
f.inferredType ?? f.candidateTypes?.join("|") ?? "?",
|
|
1198
|
+
f.suggestedName,
|
|
1199
|
+
f.conflict ? "\u2713" : "\u2013",
|
|
1200
|
+
f.sources.map((s) => `${s.kind}:${s.field}`).join(", ")
|
|
1201
|
+
]),
|
|
1202
|
+
{ truncate: 28 }
|
|
1203
|
+
);
|
|
1204
|
+
console.log(`
|
|
1205
|
+
${data.length} unregistered`);
|
|
1206
|
+
} else {
|
|
1207
|
+
printJson(data);
|
|
1208
|
+
}
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
printError(error);
|
|
1211
|
+
process.exit(1);
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1146
1214
|
facts.command("create").description("Create a new fact definition").option("--key <key>", "Fact key (lowercase, underscores)").option("--name <n>", "Display name").option("--type <type>", "Value type: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER").option("--description <desc>", "Description").option("--required", "Mark as required", false).option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
|
|
1147
1215
|
"after",
|
|
1148
1216
|
dedent6`
|
|
@@ -1316,6 +1384,7 @@ function registerDeployCommands(program) {
|
|
|
1316
1384
|
).action(async (opts) => {
|
|
1317
1385
|
try {
|
|
1318
1386
|
const globalOpts = program.opts();
|
|
1387
|
+
await warnUnregisteredFacts(globalOpts, opts.groupId, opts.versionId);
|
|
1319
1388
|
await apiRequest(
|
|
1320
1389
|
"POST",
|
|
1321
1390
|
`policy-groups/${opts.groupId}/versions/${opts.versionId}/publish`,
|
|
@@ -1345,6 +1414,7 @@ function registerDeployCommands(program) {
|
|
|
1345
1414
|
).action(async (opts) => {
|
|
1346
1415
|
try {
|
|
1347
1416
|
const globalOpts = program.opts();
|
|
1417
|
+
await warnUnregisteredFacts(globalOpts, opts.groupId, opts.versionId);
|
|
1348
1418
|
await apiRequest("POST", `policy-groups/${opts.groupId}/deploy`, {
|
|
1349
1419
|
apiKey: globalOpts.apiKey,
|
|
1350
1420
|
baseUrl: globalOpts.baseUrl,
|
|
@@ -1588,6 +1658,18 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
1588
1658
|
}
|
|
1589
1659
|
});
|
|
1590
1660
|
}
|
|
1661
|
+
async function warnUnregisteredFacts(globalOpts, groupId, versionId) {
|
|
1662
|
+
if (globalOpts.dryRun) return;
|
|
1663
|
+
try {
|
|
1664
|
+
const facts = await apiRequest(
|
|
1665
|
+
"GET",
|
|
1666
|
+
`policy-groups/${groupId}/versions/${versionId}/unregistered-facts`,
|
|
1667
|
+
{ apiKey: globalOpts.apiKey, baseUrl: globalOpts.baseUrl, verbose: globalOpts.verbose }
|
|
1668
|
+
);
|
|
1669
|
+
printUnregisteredFactsWarning(facts);
|
|
1670
|
+
} catch {
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1591
1673
|
|
|
1592
1674
|
// src/commands/analytics.ts
|
|
1593
1675
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
@@ -2858,14 +2940,17 @@ function createCallApiFromConfig() {
|
|
|
2858
2940
|
apiKey: config.apiKey,
|
|
2859
2941
|
baseUrl: config.baseUrl
|
|
2860
2942
|
};
|
|
2861
|
-
const data = await
|
|
2943
|
+
const { data, meta } = await apiRequestWithMeta(method, path, {
|
|
2862
2944
|
...clientOpts,
|
|
2863
2945
|
body: opts?.body,
|
|
2864
2946
|
params: opts?.params
|
|
2865
2947
|
});
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2948
|
+
const content = [
|
|
2949
|
+
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
2950
|
+
];
|
|
2951
|
+
const warning = formatUnregisteredFactWarning(meta);
|
|
2952
|
+
if (warning) content.push({ type: "text", text: warning });
|
|
2953
|
+
return { content };
|
|
2869
2954
|
} catch (error) {
|
|
2870
2955
|
const message = error instanceof Error ? error.message : String(error);
|
|
2871
2956
|
return {
|
|
@@ -2881,6 +2966,22 @@ function paginationParams(page, size) {
|
|
|
2881
2966
|
if (size !== void 0) params.size = String(size);
|
|
2882
2967
|
return params;
|
|
2883
2968
|
}
|
|
2969
|
+
function formatUnregisteredFactWarning(meta) {
|
|
2970
|
+
const facts = meta?.unregisteredFacts ?? [];
|
|
2971
|
+
if (facts.length === 0) return null;
|
|
2972
|
+
const lines = facts.map((f) => {
|
|
2973
|
+
if (f.inferredType) {
|
|
2974
|
+
return ` \u2022 ${f.key} (${f.inferredType}) \u2014 register: lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: "${f.inferredType}" })`;
|
|
2975
|
+
}
|
|
2976
|
+
const choices = f.candidateTypes?.join(" | ") ?? "STRING | NUMBER | BOOLEAN";
|
|
2977
|
+
return ` \u2022 ${f.key} (ambiguous) \u2014 pick a type (${choices}), then lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: <chosen> })`;
|
|
2978
|
+
});
|
|
2979
|
+
return [
|
|
2980
|
+
`\u26A0 The saved rule references ${facts.length} fact(s) not defined in the schema. The rule was saved \u2014 this does not block \u2014 but undefined facts skip type validation and the dry-run requirements analyzer.`,
|
|
2981
|
+
...lines,
|
|
2982
|
+
`Register them now so the rule validates as intended, or call lexq_facts_unregistered to review the version's full list.`
|
|
2983
|
+
].join("\n");
|
|
2984
|
+
}
|
|
2884
2985
|
|
|
2885
2986
|
// src/mcp/tools/status.ts
|
|
2886
2987
|
function registerStatusTools(server, callApi) {
|
|
@@ -3157,6 +3258,9 @@ function registerRuleTools(server, callApi) {
|
|
|
3157
3258
|
If a required key is missing, ask the user to confirm the type, isRequired, and description
|
|
3158
3259
|
before calling lexq_facts_create — registering facts enables type validation, Console UI
|
|
3159
3260
|
autocomplete, and the dry-run requirements analyzer.
|
|
3261
|
+
|
|
3262
|
+
After saving, lexq_facts_unregistered lists any keys this version references but has not
|
|
3263
|
+
defined (non-blocking, version-wide) — use it to decide what to register.
|
|
3160
3264
|
|
|
3161
3265
|
Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
|
|
3162
3266
|
Operators: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, CONTAINS, IN, NOT_IN
|
|
@@ -3342,6 +3446,18 @@ function registerFactTools(server, callApi) {
|
|
|
3342
3446
|
},
|
|
3343
3447
|
async () => callApi("GET", "schema/action-metadata")
|
|
3344
3448
|
);
|
|
3449
|
+
server.registerTool(
|
|
3450
|
+
"lexq_facts_unregistered",
|
|
3451
|
+
{
|
|
3452
|
+
title: "List Unregistered Facts",
|
|
3453
|
+
description: "List facts referenced by a version's rules but not yet defined (read-only \u2014 does not block publish/deploy, INV-4). Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Register them with lexq_facts_create to enable type validation and the dry-run requirements analyzer.",
|
|
3454
|
+
inputSchema: {
|
|
3455
|
+
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
3456
|
+
versionId: z4.string().uuid().describe("Version ID")
|
|
3457
|
+
}
|
|
3458
|
+
},
|
|
3459
|
+
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
|
|
3460
|
+
);
|
|
3345
3461
|
}
|
|
3346
3462
|
|
|
3347
3463
|
// src/mcp/tools/deploy.ts
|
|
@@ -3351,7 +3467,7 @@ function registerDeployTools(server, callApi) {
|
|
|
3351
3467
|
"lexq_deploy_publish",
|
|
3352
3468
|
{
|
|
3353
3469
|
title: "Publish Version",
|
|
3354
|
-
description: "Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule.",
|
|
3470
|
+
description: "Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts referenced by rules do not block publishing (INV-4); call lexq_facts_unregistered first to review them.",
|
|
3355
3471
|
inputSchema: {
|
|
3356
3472
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
3357
3473
|
versionId: z5.string().uuid().describe("Version ID to publish"),
|
|
@@ -3364,7 +3480,7 @@ function registerDeployTools(server, callApi) {
|
|
|
3364
3480
|
"lexq_deploy_live",
|
|
3365
3481
|
{
|
|
3366
3482
|
title: "Deploy to Live",
|
|
3367
|
-
description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately.",
|
|
3483
|
+
description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately. Undefined facts do not block deployment (INV-4); use lexq_facts_unregistered to review what the version references but has not defined.",
|
|
3368
3484
|
inputSchema: {
|
|
3369
3485
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
3370
3486
|
versionId: z5.string().uuid().describe("Version ID to deploy"),
|
package/dist/mcp/register.d.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
|
|
3
|
+
declare const ValueType: readonly ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
|
|
4
|
+
type ValueType = (typeof ValueType)[number];
|
|
5
|
+
declare const Confidence: readonly ["EXACT", "AMBIGUOUS"];
|
|
6
|
+
type Confidence = (typeof Confidence)[number];
|
|
7
|
+
declare const SourceKind: readonly ["CONDITION", "ACTION"];
|
|
8
|
+
type SourceKind = (typeof SourceKind)[number];
|
|
9
|
+
|
|
10
|
+
interface ResponseMeta {
|
|
11
|
+
unregisteredFacts?: UnregisteredFact[];
|
|
12
|
+
}
|
|
13
|
+
interface UnregisteredFact {
|
|
14
|
+
key: string;
|
|
15
|
+
inferredType: ValueType | null;
|
|
16
|
+
confidence: Confidence;
|
|
17
|
+
conflict: boolean;
|
|
18
|
+
candidateTypes: ValueType[] | null;
|
|
19
|
+
suggestedName: string;
|
|
20
|
+
sources: UnregisteredFactSource[];
|
|
21
|
+
}
|
|
22
|
+
interface UnregisteredFactSource {
|
|
23
|
+
kind: SourceKind;
|
|
24
|
+
field: string;
|
|
25
|
+
operator?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
3
28
|
interface McpToolResult {
|
|
4
29
|
[key: string]: unknown;
|
|
5
30
|
content: Array<{
|
|
@@ -25,13 +50,22 @@ type CallApi = (method: string, path: string, opts?: {
|
|
|
25
50
|
};
|
|
26
51
|
}) => Promise<McpToolResult>;
|
|
27
52
|
declare function paginationParams(page?: number, size?: number): Record<string, string>;
|
|
53
|
+
/**
|
|
54
|
+
* Builds a salient, actionable warning from a response's meta when it reports facts that
|
|
55
|
+
* rules reference but the schema does not define. Returns null when there is nothing to warn.
|
|
56
|
+
*
|
|
57
|
+
* Shared by both CallApi implementations (CLI stdio here, lexq-mcp HTTP via @lexq/cli/mcp)
|
|
58
|
+
* so the agent-facing text is identical on every surface. The backend only populates
|
|
59
|
+
* meta.unregisteredFacts on rule create/update, so this only ever fires there.
|
|
60
|
+
*/
|
|
61
|
+
declare function formatUnregisteredFactWarning(meta: ResponseMeta | null | undefined): string | null;
|
|
28
62
|
|
|
29
63
|
/**
|
|
30
|
-
* Registers all
|
|
64
|
+
* Registers all MCP tools on the given server.
|
|
31
65
|
*
|
|
32
66
|
* @param server - McpServer instance
|
|
33
67
|
* @param callApi - API caller function (config-based for CLI, Bearer-based for HTTP)
|
|
34
68
|
*/
|
|
35
69
|
declare function registerAllTools(server: McpServer, callApi: CallApi): void;
|
|
36
70
|
|
|
37
|
-
export { type CallApi, type McpToolResult, paginationParams, registerAllTools };
|
|
71
|
+
export { type CallApi, type McpToolResult, formatUnregisteredFactWarning, paginationParams, registerAllTools };
|
package/dist/mcp/register.js
CHANGED
|
@@ -161,6 +161,22 @@ function paginationParams(page, size) {
|
|
|
161
161
|
if (size !== void 0) params.size = String(size);
|
|
162
162
|
return params;
|
|
163
163
|
}
|
|
164
|
+
function formatUnregisteredFactWarning(meta) {
|
|
165
|
+
const facts = meta?.unregisteredFacts ?? [];
|
|
166
|
+
if (facts.length === 0) return null;
|
|
167
|
+
const lines = facts.map((f) => {
|
|
168
|
+
if (f.inferredType) {
|
|
169
|
+
return ` \u2022 ${f.key} (${f.inferredType}) \u2014 register: lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: "${f.inferredType}" })`;
|
|
170
|
+
}
|
|
171
|
+
const choices = f.candidateTypes?.join(" | ") ?? "STRING | NUMBER | BOOLEAN";
|
|
172
|
+
return ` \u2022 ${f.key} (ambiguous) \u2014 pick a type (${choices}), then lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: <chosen> })`;
|
|
173
|
+
});
|
|
174
|
+
return [
|
|
175
|
+
`\u26A0 The saved rule references ${facts.length} fact(s) not defined in the schema. The rule was saved \u2014 this does not block \u2014 but undefined facts skip type validation and the dry-run requirements analyzer.`,
|
|
176
|
+
...lines,
|
|
177
|
+
`Register them now so the rule validates as intended, or call lexq_facts_unregistered to review the version's full list.`
|
|
178
|
+
].join("\n");
|
|
179
|
+
}
|
|
164
180
|
|
|
165
181
|
// src/mcp/tools/versions.ts
|
|
166
182
|
function registerVersionTools(server, callApi) {
|
|
@@ -290,6 +306,9 @@ function registerRuleTools(server, callApi) {
|
|
|
290
306
|
If a required key is missing, ask the user to confirm the type, isRequired, and description
|
|
291
307
|
before calling lexq_facts_create — registering facts enables type validation, Console UI
|
|
292
308
|
autocomplete, and the dry-run requirements analyzer.
|
|
309
|
+
|
|
310
|
+
After saving, lexq_facts_unregistered lists any keys this version references but has not
|
|
311
|
+
defined (non-blocking, version-wide) — use it to decide what to register.
|
|
293
312
|
|
|
294
313
|
Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
|
|
295
314
|
Operators: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, CONTAINS, IN, NOT_IN
|
|
@@ -475,6 +494,18 @@ function registerFactTools(server, callApi) {
|
|
|
475
494
|
},
|
|
476
495
|
async () => callApi("GET", "schema/action-metadata")
|
|
477
496
|
);
|
|
497
|
+
server.registerTool(
|
|
498
|
+
"lexq_facts_unregistered",
|
|
499
|
+
{
|
|
500
|
+
title: "List Unregistered Facts",
|
|
501
|
+
description: "List facts referenced by a version's rules but not yet defined (read-only \u2014 does not block publish/deploy, INV-4). Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Register them with lexq_facts_create to enable type validation and the dry-run requirements analyzer.",
|
|
502
|
+
inputSchema: {
|
|
503
|
+
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
504
|
+
versionId: z4.string().uuid().describe("Version ID")
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
|
|
508
|
+
);
|
|
478
509
|
}
|
|
479
510
|
|
|
480
511
|
// src/mcp/tools/deploy.ts
|
|
@@ -484,7 +515,7 @@ function registerDeployTools(server, callApi) {
|
|
|
484
515
|
"lexq_deploy_publish",
|
|
485
516
|
{
|
|
486
517
|
title: "Publish Version",
|
|
487
|
-
description: "Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule.",
|
|
518
|
+
description: "Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts referenced by rules do not block publishing (INV-4); call lexq_facts_unregistered first to review them.",
|
|
488
519
|
inputSchema: {
|
|
489
520
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
490
521
|
versionId: z5.string().uuid().describe("Version ID to publish"),
|
|
@@ -497,7 +528,7 @@ function registerDeployTools(server, callApi) {
|
|
|
497
528
|
"lexq_deploy_live",
|
|
498
529
|
{
|
|
499
530
|
title: "Deploy to Live",
|
|
500
|
-
description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately.",
|
|
531
|
+
description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately. Undefined facts do not block deployment (INV-4); use lexq_facts_unregistered to review what the version references but has not defined.",
|
|
501
532
|
inputSchema: {
|
|
502
533
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
503
534
|
versionId: z5.string().uuid().describe("Version ID to deploy"),
|
|
@@ -1175,6 +1206,7 @@ function registerAllTools(server, callApi) {
|
|
|
1175
1206
|
registerWebhookSubscriptionTools(server, callApi);
|
|
1176
1207
|
}
|
|
1177
1208
|
export {
|
|
1209
|
+
formatUnregisteredFactWarning,
|
|
1178
1210
|
paginationParams,
|
|
1179
1211
|
registerAllTools
|
|
1180
1212
|
};
|