@lexq/cli 2.1.1 → 3.0.1
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/AGENTS.md +2 -2
- package/CONTEXT.md +1 -1
- package/README.md +7 -4
- package/dist/index.js +254 -304
- package/dist/mcp/register.d.ts +7 -1
- package/dist/mcp/register.js +288 -315
- package/package.json +3 -1
- package/skills/lexq-execution/SKILL.md +8 -20
- package/skills/lexq-recipes/SKILL.md +9 -13
- package/skills/lexq-shared/SKILL.md +6 -3
package/dist/index.js
CHANGED
|
@@ -184,6 +184,14 @@ async function apiRequestWithMeta(method, path, options = {}) {
|
|
|
184
184
|
|
|
185
185
|
// src/lib/output.ts
|
|
186
186
|
import Table from "cli-table3";
|
|
187
|
+
import { InvalidArgumentError } from "commander";
|
|
188
|
+
var OUTPUT_FORMATS = ["json", "table"];
|
|
189
|
+
function parseOutputFormat(value) {
|
|
190
|
+
if (OUTPUT_FORMATS.includes(value)) return value;
|
|
191
|
+
throw new InvalidArgumentError(
|
|
192
|
+
`Use ${OUTPUT_FORMATS.join(" or ")}. The format of an exported file is chosen with --as.`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
187
195
|
function printJson(data) {
|
|
188
196
|
console.log(stringifyJson(data, 2));
|
|
189
197
|
}
|
|
@@ -394,6 +402,7 @@ var PURGE_AFTER_DAYS = 30;
|
|
|
394
402
|
var DESCRIPTION_MAX_LENGTH = 255;
|
|
395
403
|
var COMMIT_MESSAGE_MAX_LENGTH = 500;
|
|
396
404
|
var SCHEDULE_TICK_MAX_MILLIS = 6e4;
|
|
405
|
+
var MAX_DRY_RUN_EVALUATIONS_PER_MINUTE = 60;
|
|
397
406
|
|
|
398
407
|
// src/commands/groups.ts
|
|
399
408
|
import "commander";
|
|
@@ -425,7 +434,6 @@ var SimulationDatasetSource = ["EXECUTION_LOGS", "S3_BUCKET", "REQUEST_BODY"];
|
|
|
425
434
|
var SimulationMetricType = ["COUNT", "SUM", "AVG"];
|
|
426
435
|
var ExportFormat = ["CSV", "JSON"];
|
|
427
436
|
var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
|
|
428
|
-
var FailureAction = ["IGNORE", "RESOLVE"];
|
|
429
437
|
var TaskType = ["PLATFORM_WEBHOOK", "SCHEDULED_DEPLOYMENT"];
|
|
430
438
|
var PlatformEventType = [
|
|
431
439
|
"VERSION_PUBLISHED",
|
|
@@ -1347,6 +1355,7 @@ function escapeUnsafe(json) {
|
|
|
1347
1355
|
|
|
1348
1356
|
// src/lib/export.ts
|
|
1349
1357
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
1358
|
+
import { extname } from "path";
|
|
1350
1359
|
var EXPORT_FORMATS = ExportFormat.map((name) => name.toLowerCase()).sort();
|
|
1351
1360
|
function parseFormat(value) {
|
|
1352
1361
|
if (typeof value === "string" && EXPORT_FORMATS.includes(value)) {
|
|
@@ -1356,6 +1365,25 @@ function parseFormat(value) {
|
|
|
1356
1365
|
`Unsupported export format: ${String(value)}. Use ${EXPORT_FORMATS.join(" or ")}.`
|
|
1357
1366
|
);
|
|
1358
1367
|
}
|
|
1368
|
+
function fileFormat(command) {
|
|
1369
|
+
const opts = command.opts();
|
|
1370
|
+
const format = parseFormat(opts.as);
|
|
1371
|
+
const extension = typeof opts.output === "string" ? extname(opts.output).slice(1).toLowerCase() : "";
|
|
1372
|
+
let root = command;
|
|
1373
|
+
while (root.parent) root = root.parent;
|
|
1374
|
+
if (EXPORT_FORMATS.includes(extension)) {
|
|
1375
|
+
if (extension !== format) {
|
|
1376
|
+
console.error(
|
|
1377
|
+
`Note: ${opts.output} will hold ${format}. Pass --as ${extension} to write ${extension}.`
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
} else if (root.getOptionValueSource("format") === "cli" && command.getOptionValueSource("as") !== "cli") {
|
|
1381
|
+
console.error(
|
|
1382
|
+
`Note: --format sets how results are printed, not the file. This file is ${format}; choose ${EXPORT_FORMATS.join(" or ")} with --as.`
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
return format;
|
|
1386
|
+
}
|
|
1359
1387
|
async function runExport(path, format, options) {
|
|
1360
1388
|
const { output, params, ...clientOptions } = options;
|
|
1361
1389
|
const body = await apiRequest("GET", path, {
|
|
@@ -1622,10 +1650,10 @@ ${warning}
|
|
|
1622
1650
|
$ lexq facts export --as json --output facts.json
|
|
1623
1651
|
$ lexq facts export --keyword user --as json
|
|
1624
1652
|
`
|
|
1625
|
-
).action(async (opts) => {
|
|
1653
|
+
).action(async (opts, command) => {
|
|
1626
1654
|
try {
|
|
1627
1655
|
const globalOpts = program.opts();
|
|
1628
|
-
await runExport("schema/facts/export",
|
|
1656
|
+
await runExport("schema/facts/export", fileFormat(command), {
|
|
1629
1657
|
apiKey: globalOpts.apiKey,
|
|
1630
1658
|
baseUrl: globalOpts.baseUrl,
|
|
1631
1659
|
dryRun: globalOpts.dryRun,
|
|
@@ -2209,13 +2237,13 @@ import "commander";
|
|
|
2209
2237
|
import dedent8 from "dedent";
|
|
2210
2238
|
|
|
2211
2239
|
// src/lib/dataset-upload.ts
|
|
2212
|
-
import { extname } from "path";
|
|
2240
|
+
import { extname as extname2 } from "path";
|
|
2213
2241
|
var DATASET_CONTENT_TYPES = {
|
|
2214
2242
|
".csv": "text/csv",
|
|
2215
2243
|
".json": "application/json"
|
|
2216
2244
|
};
|
|
2217
2245
|
function datasetContentType(filename) {
|
|
2218
|
-
return DATASET_CONTENT_TYPES[
|
|
2246
|
+
return DATASET_CONTENT_TYPES[extname2(filename).toLowerCase()];
|
|
2219
2247
|
}
|
|
2220
2248
|
function uploadDataset(bytes, filename, options) {
|
|
2221
2249
|
const form = new FormData();
|
|
@@ -2227,6 +2255,9 @@ function uploadDataset(bytes, filename, options) {
|
|
|
2227
2255
|
});
|
|
2228
2256
|
}
|
|
2229
2257
|
|
|
2258
|
+
// src/lib/dry-run-limit.ts
|
|
2259
|
+
var DRY_RUN_LIMIT_NOTE = `Not billed and not counted toward the plan's usage. Dry runs, dry run compares, and single-decision replays share a limit of ${MAX_DRY_RUN_EVALUATIONS_PER_MINUTE} evaluations per minute for each organization; a compare counts as two. Past the limit, a call fails with 429 (C-007) until the next minute starts.`;
|
|
2260
|
+
|
|
2230
2261
|
// src/commands/analytics.ts
|
|
2231
2262
|
function registerAnalyticsCommands(program) {
|
|
2232
2263
|
const analytics = program.command("analytics").description("Dry run, simulation, and requirements").addHelpText(
|
|
@@ -2256,6 +2287,8 @@ function registerAnalyticsCommands(program) {
|
|
|
2256
2287
|
$ lexq analytics dry-run --version-id <vid> --file test-input.json
|
|
2257
2288
|
|
|
2258
2289
|
The request body must include a "facts" object. Use --debug for execution traces
|
|
2290
|
+
|
|
2291
|
+
${DRY_RUN_LIMIT_NOTE}
|
|
2259
2292
|
`
|
|
2260
2293
|
).action(async (opts) => {
|
|
2261
2294
|
try {
|
|
@@ -2294,6 +2327,8 @@ function registerAnalyticsCommands(program) {
|
|
|
2294
2327
|
}'
|
|
2295
2328
|
|
|
2296
2329
|
Shows side-by-side which rules matched and what actions fired for each version.
|
|
2330
|
+
|
|
2331
|
+
${DRY_RUN_LIMIT_NOTE}
|
|
2297
2332
|
`
|
|
2298
2333
|
).action(async (opts) => {
|
|
2299
2334
|
try {
|
|
@@ -2605,20 +2640,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2605
2640
|
$ lexq analytics simulation export --id <simId> --as csv --output results.csv
|
|
2606
2641
|
$ lexq analytics simulation export --id <simId> --as json
|
|
2607
2642
|
`
|
|
2608
|
-
).action(async (opts) => {
|
|
2643
|
+
).action(async (opts, command) => {
|
|
2609
2644
|
try {
|
|
2610
2645
|
const globalOpts = program.opts();
|
|
2611
|
-
await runExport(
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
verbose: globalOpts.verbose,
|
|
2619
|
-
output: opts.output
|
|
2620
|
-
}
|
|
2621
|
-
);
|
|
2646
|
+
await runExport(`analytics/simulations/${opts.id}/export`, fileFormat(command), {
|
|
2647
|
+
apiKey: globalOpts.apiKey,
|
|
2648
|
+
baseUrl: globalOpts.baseUrl,
|
|
2649
|
+
dryRun: globalOpts.dryRun,
|
|
2650
|
+
verbose: globalOpts.verbose,
|
|
2651
|
+
output: opts.output
|
|
2652
|
+
});
|
|
2622
2653
|
} catch (error) {
|
|
2623
2654
|
printError(error);
|
|
2624
2655
|
process.exit(1);
|
|
@@ -2671,7 +2702,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2671
2702
|
$ lexq analytics dataset template --group-id <gid> --version-id <vid> --output template.csv
|
|
2672
2703
|
$ lexq analytics dataset template --group-id <gid> --version-id <vid> --as json
|
|
2673
2704
|
`
|
|
2674
|
-
).action(async (opts) => {
|
|
2705
|
+
).action(async (opts, command) => {
|
|
2675
2706
|
try {
|
|
2676
2707
|
const globalOpts = program.opts();
|
|
2677
2708
|
const config = loadConfig();
|
|
@@ -2680,7 +2711,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2680
2711
|
if (!apiKey) {
|
|
2681
2712
|
throw new Error('Not authenticated. Run "lexq auth login" first.');
|
|
2682
2713
|
}
|
|
2683
|
-
const fmt =
|
|
2714
|
+
const fmt = fileFormat(command);
|
|
2684
2715
|
const url = new URL(
|
|
2685
2716
|
`analytics/groups/${opts.groupId}/versions/${opts.versionId}/dataset-template`,
|
|
2686
2717
|
baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"
|
|
@@ -3012,8 +3043,10 @@ function registerReplayCommands(program) {
|
|
|
3012
3043
|
"after",
|
|
3013
3044
|
dedent11`
|
|
3014
3045
|
|
|
3015
|
-
|
|
3016
|
-
|
|
3046
|
+
Returns decisionChanged, effect changes, fired rules on both sides,
|
|
3047
|
+
and a determinism verdict.
|
|
3048
|
+
|
|
3049
|
+
${DRY_RUN_LIMIT_NOTE}
|
|
3017
3050
|
|
|
3018
3051
|
Example:
|
|
3019
3052
|
$ lexq replay decision --trace-id <tid> --version-id <vid>
|
|
@@ -3145,10 +3178,10 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3145
3178
|
$ lexq replay export --id <jobId> --as csv --output blast-radius.csv
|
|
3146
3179
|
$ lexq replay export --id <jobId> --as json
|
|
3147
3180
|
`
|
|
3148
|
-
).action(async (opts) => {
|
|
3181
|
+
).action(async (opts, command) => {
|
|
3149
3182
|
try {
|
|
3150
3183
|
const globalOpts = program.opts();
|
|
3151
|
-
await runExport(`replay/jobs/${opts.id}/export`,
|
|
3184
|
+
await runExport(`replay/jobs/${opts.id}/export`, fileFormat(command), {
|
|
3152
3185
|
apiKey: globalOpts.apiKey,
|
|
3153
3186
|
baseUrl: globalOpts.baseUrl,
|
|
3154
3187
|
dryRun: globalOpts.dryRun,
|
|
@@ -3254,6 +3287,10 @@ function withNotes(notes) {
|
|
|
3254
3287
|
return Object.entries(notes).map(([value, note]) => note ? `${value} (${note})` : value);
|
|
3255
3288
|
}
|
|
3256
3289
|
|
|
3290
|
+
// src/lib/console.ts
|
|
3291
|
+
var CONSOLE_URL = "https://console.lexq.io";
|
|
3292
|
+
var FAILURE_LOG_CONSOLE_NOTE = `Logs can be read here but not resolved or ignored. An organization admin does that in the LexQ console: Management \u2192 Failure Logs (${CONSOLE_URL}/management/failure-logs).`;
|
|
3293
|
+
|
|
3257
3294
|
// src/commands/logs.ts
|
|
3258
3295
|
var STATUS_NOTES = {
|
|
3259
3296
|
PENDING: "needs attention",
|
|
@@ -3270,8 +3307,8 @@ function registerLogCommands(program) {
|
|
|
3270
3307
|
Commands:
|
|
3271
3308
|
list List failure logs with filters
|
|
3272
3309
|
get Get failure log detail (includes the original payload)
|
|
3273
|
-
|
|
3274
|
-
|
|
3310
|
+
|
|
3311
|
+
${FAILURE_LOG_CONSOLE_NOTE}
|
|
3275
3312
|
|
|
3276
3313
|
Statuses: ${withNotes(STATUS_NOTES).join(", ")}
|
|
3277
3314
|
`
|
|
@@ -3332,7 +3369,8 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3332
3369
|
dedent13`
|
|
3333
3370
|
|
|
3334
3371
|
Includes the full payload that was used for the failed operation.
|
|
3335
|
-
Use this to inspect what went wrong
|
|
3372
|
+
Use this to inspect what went wrong. Resolving or ignoring a log happens in the
|
|
3373
|
+
console; see lexq logs --help.
|
|
3336
3374
|
`
|
|
3337
3375
|
).action(async (opts) => {
|
|
3338
3376
|
try {
|
|
@@ -3349,63 +3387,6 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3349
3387
|
process.exit(1);
|
|
3350
3388
|
}
|
|
3351
3389
|
});
|
|
3352
|
-
logs.command("action").description(`Process a failure log action (${FailureAction.join(", ")})`).requiredOption("--id <logId>", "Log ID").requiredOption("--action <action>", `Action: ${FailureAction.join(" or ")}`).addHelpText(
|
|
3353
|
-
"after",
|
|
3354
|
-
dedent13`
|
|
3355
|
-
|
|
3356
|
-
Actions:
|
|
3357
|
-
IGNORE Mark as intentionally skipped (won't appear in PENDING)
|
|
3358
|
-
RESOLVE Mark as manually resolved (e.g., fixed via external system)
|
|
3359
|
-
|
|
3360
|
-
Example:
|
|
3361
|
-
$ lexq logs action --id <logId> --action RESOLVE
|
|
3362
|
-
`
|
|
3363
|
-
).action(async (opts) => {
|
|
3364
|
-
try {
|
|
3365
|
-
const globalOpts = program.opts();
|
|
3366
|
-
const data = await apiRequest(
|
|
3367
|
-
"POST",
|
|
3368
|
-
`failure-logs/${opts.id}/actions`,
|
|
3369
|
-
{
|
|
3370
|
-
apiKey: globalOpts.apiKey,
|
|
3371
|
-
baseUrl: globalOpts.baseUrl,
|
|
3372
|
-
dryRun: globalOpts.dryRun,
|
|
3373
|
-
verbose: globalOpts.verbose,
|
|
3374
|
-
params: { action: opts.action }
|
|
3375
|
-
}
|
|
3376
|
-
);
|
|
3377
|
-
printJson(data);
|
|
3378
|
-
} catch (error) {
|
|
3379
|
-
printError(error);
|
|
3380
|
-
process.exit(1);
|
|
3381
|
-
}
|
|
3382
|
-
});
|
|
3383
|
-
logs.command("bulk-action").description("Bulk process failure logs").requiredOption("--ids <logIds>", "Comma-separated log IDs").requiredOption("--action <action>", `Action: ${FailureAction.join(" or ")}`).addHelpText(
|
|
3384
|
-
"after",
|
|
3385
|
-
dedent13`
|
|
3386
|
-
|
|
3387
|
-
Processes each log individually. Failures are skipped with a warning.
|
|
3388
|
-
|
|
3389
|
-
Example:
|
|
3390
|
-
$ lexq logs bulk-action --ids "id1,id2,id3" --action RESOLVE
|
|
3391
|
-
`
|
|
3392
|
-
).action(async (opts) => {
|
|
3393
|
-
try {
|
|
3394
|
-
const globalOpts = program.opts();
|
|
3395
|
-
const logIds = opts.ids.split(",").map((id) => id.trim());
|
|
3396
|
-
const data = await apiRequest("POST", "failure-logs/bulk-actions", {
|
|
3397
|
-
apiKey: globalOpts.apiKey,
|
|
3398
|
-
baseUrl: globalOpts.baseUrl,
|
|
3399
|
-
dryRun: globalOpts.dryRun,
|
|
3400
|
-
verbose: globalOpts.verbose,
|
|
3401
|
-
body: { logIds, action: opts.action }
|
|
3402
|
-
});
|
|
3403
|
-
printJson(data);
|
|
3404
|
-
} catch (error) {
|
|
3405
|
-
printError(error);
|
|
3406
|
-
process.exit(1);
|
|
3407
|
-
}
|
|
3408
|
-
});
|
|
3409
3390
|
}
|
|
3410
3391
|
|
|
3411
3392
|
// src/commands/webhook-subscriptions.ts
|
|
@@ -3627,6 +3608,21 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
|
3627
3608
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
3628
3609
|
|
|
3629
3610
|
// src/mcp/tools/_shared.ts
|
|
3611
|
+
var READ_ONLY = {
|
|
3612
|
+
readOnlyHint: true,
|
|
3613
|
+
destructiveHint: false,
|
|
3614
|
+
openWorldHint: false
|
|
3615
|
+
};
|
|
3616
|
+
var ADDITIVE = {
|
|
3617
|
+
readOnlyHint: false,
|
|
3618
|
+
destructiveHint: false,
|
|
3619
|
+
openWorldHint: false
|
|
3620
|
+
};
|
|
3621
|
+
var DESTRUCTIVE = {
|
|
3622
|
+
readOnlyHint: false,
|
|
3623
|
+
destructiveHint: true,
|
|
3624
|
+
openWorldHint: false
|
|
3625
|
+
};
|
|
3630
3626
|
function localApiOptions() {
|
|
3631
3627
|
const stored = loadConfig();
|
|
3632
3628
|
return {
|
|
@@ -3649,9 +3645,8 @@ function createCallApiFromConfig() {
|
|
|
3649
3645
|
form,
|
|
3650
3646
|
params: opts?.params
|
|
3651
3647
|
});
|
|
3652
|
-
const
|
|
3653
|
-
|
|
3654
|
-
];
|
|
3648
|
+
const text = typeof data === "string" ? data : stringifyJson(data, 2) ?? "";
|
|
3649
|
+
const content = [{ type: "text", text }];
|
|
3655
3650
|
const warning = formatUnregisteredFactWarning(meta);
|
|
3656
3651
|
if (warning) content.push({ type: "text", text: warning });
|
|
3657
3652
|
const domainWarning = formatValueDomainMismatchWarning(meta);
|
|
@@ -3672,6 +3667,20 @@ function paginationParams(page, size) {
|
|
|
3672
3667
|
if (size !== void 0) params.size = String(size);
|
|
3673
3668
|
return params;
|
|
3674
3669
|
}
|
|
3670
|
+
var MAX_EXPORT_CHARS = 1e5;
|
|
3671
|
+
var formatCount = (count) => count.toLocaleString("en-US");
|
|
3672
|
+
var EXPORT_LIMIT_NOTE = `An export longer than ${formatCount(MAX_EXPORT_CHARS)} characters is refused.`;
|
|
3673
|
+
function limitExport(result, command, narrower) {
|
|
3674
|
+
if (result.isError) return result;
|
|
3675
|
+
const length = result.content.reduce((sum, block) => sum + block.text.length, 0);
|
|
3676
|
+
if (length <= MAX_EXPORT_CHARS) return result;
|
|
3677
|
+
const text = [
|
|
3678
|
+
`The export is ${formatCount(length)} characters long, and this tool returns at most ${formatCount(MAX_EXPORT_CHARS)}.`,
|
|
3679
|
+
narrower,
|
|
3680
|
+
`To get the whole file, download it from the LexQ console (${CONSOLE_URL}) or with \`${command}\`.`
|
|
3681
|
+
].filter(Boolean).join(" ");
|
|
3682
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
3683
|
+
}
|
|
3675
3684
|
function formatUnregisteredFactWarning(meta) {
|
|
3676
3685
|
const facts = meta?.unregisteredFacts ?? [];
|
|
3677
3686
|
if (facts.length === 0) return null;
|
|
@@ -3683,9 +3692,9 @@ function formatUnregisteredFactWarning(meta) {
|
|
|
3683
3692
|
return ` \u2022 ${f.key} (ambiguous) \u2014 pick a type (${choices}), then lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: <chosen> })`;
|
|
3684
3693
|
});
|
|
3685
3694
|
return [
|
|
3686
|
-
`\u26A0 The saved rule references ${facts.length} fact(s) not defined in the schema. The rule was saved
|
|
3695
|
+
`\u26A0 The saved rule references ${facts.length} fact(s) not defined in the schema. The rule was saved, but the engine does not check request values for these keys against a type or value domain, and lexq_requirements lists them with type UNKNOWN.`,
|
|
3687
3696
|
...lines,
|
|
3688
|
-
`Register them
|
|
3697
|
+
`Register them before the version is published to have those values checked, or call lexq_facts_unregistered to review the version's full list.`
|
|
3689
3698
|
].join("\n");
|
|
3690
3699
|
}
|
|
3691
3700
|
|
|
@@ -3711,9 +3720,9 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3711
3720
|
"lexq_dry_run",
|
|
3712
3721
|
{
|
|
3713
3722
|
title: "Dry Run",
|
|
3714
|
-
annotations:
|
|
3723
|
+
annotations: READ_ONLY,
|
|
3715
3724
|
description: dedent15`
|
|
3716
|
-
Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
|
|
3725
|
+
Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects. ${DRY_RUN_LIMIT_NOTE}
|
|
3717
3726
|
|
|
3718
3727
|
Returns:
|
|
3719
3728
|
inputFacts — normalized input facts
|
|
@@ -3722,8 +3731,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3722
3731
|
executionTraces — per-rule match status
|
|
3723
3732
|
decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
|
|
3724
3733
|
|
|
3725
|
-
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
|
|
3726
|
-
Always dry-run before publishing to validate rule behavior.`,
|
|
3734
|
+
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }`,
|
|
3727
3735
|
inputSchema: z.object({
|
|
3728
3736
|
versionId: z.string().uuid().describe("Policy version ID to test against"),
|
|
3729
3737
|
facts: z.string().describe('JSON string of facts object, e.g. {"paymentAmount":100000}'),
|
|
@@ -3741,9 +3749,9 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3741
3749
|
"lexq_dry_run_compare",
|
|
3742
3750
|
{
|
|
3743
3751
|
title: "Dry Run Compare",
|
|
3744
|
-
annotations:
|
|
3752
|
+
annotations: READ_ONLY,
|
|
3745
3753
|
description: dedent15`
|
|
3746
|
-
Compare dry run results between two versions using the same input facts. Useful for validating changes.
|
|
3754
|
+
Compare dry run results between two versions using the same input facts. Useful for validating changes. ${DRY_RUN_LIMIT_NOTE}
|
|
3747
3755
|
|
|
3748
3756
|
Returns:
|
|
3749
3757
|
resultA / resultB — full DryRunResponse for each version
|
|
@@ -3766,7 +3774,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3766
3774
|
"lexq_requirements",
|
|
3767
3775
|
{
|
|
3768
3776
|
title: "Analyze Requirements",
|
|
3769
|
-
annotations:
|
|
3777
|
+
annotations: READ_ONLY,
|
|
3770
3778
|
description: "Analyze which input facts a version requires. Returns required keys, types, and an example request body.",
|
|
3771
3779
|
inputSchema: z.object({
|
|
3772
3780
|
groupId: z.string().uuid().describe("Policy group ID"),
|
|
@@ -3779,10 +3787,13 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3779
3787
|
"lexq_simulation_start",
|
|
3780
3788
|
{
|
|
3781
3789
|
title: "Start Simulation",
|
|
3782
|
-
annotations:
|
|
3790
|
+
annotations: DESTRUCTIVE,
|
|
3783
3791
|
description: dedent15`
|
|
3784
3792
|
Start an Impact Simulation against historical, uploaded, or inline data.
|
|
3785
3793
|
|
|
3794
|
+
Starting checks the plan's simulation usage limit. The records a simulation processes
|
|
3795
|
+
count toward that usage when it completes or is canceled.
|
|
3796
|
+
|
|
3786
3797
|
dataset.type and dataset.source are BOTH required, and must be paired:
|
|
3787
3798
|
HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
|
|
3788
3799
|
in the organization time zone, both inclusive)
|
|
@@ -3823,7 +3834,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3823
3834
|
"lexq_simulation_status",
|
|
3824
3835
|
{
|
|
3825
3836
|
title: "Simulation Status",
|
|
3826
|
-
annotations:
|
|
3837
|
+
annotations: READ_ONLY,
|
|
3827
3838
|
description: "Get simulation status and results. Poll until status is COMPLETED or FAILED.",
|
|
3828
3839
|
inputSchema: z.object({
|
|
3829
3840
|
simulationId: z.string().uuid().describe("Simulation ID")
|
|
@@ -3835,7 +3846,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3835
3846
|
"lexq_simulation_list",
|
|
3836
3847
|
{
|
|
3837
3848
|
title: "List Simulations",
|
|
3838
|
-
annotations:
|
|
3849
|
+
annotations: READ_ONLY,
|
|
3839
3850
|
description: "List simulation history with optional filters.",
|
|
3840
3851
|
inputSchema: z.object({
|
|
3841
3852
|
page: z.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -3857,7 +3868,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3857
3868
|
"lexq_simulation_cancel",
|
|
3858
3869
|
{
|
|
3859
3870
|
title: "Cancel Simulation",
|
|
3860
|
-
annotations:
|
|
3871
|
+
annotations: DESTRUCTIVE,
|
|
3861
3872
|
description: "Cancel a running or pending simulation.",
|
|
3862
3873
|
inputSchema: z.object({
|
|
3863
3874
|
simulationId: z.string().uuid().describe("Simulation ID")
|
|
@@ -3869,22 +3880,25 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3869
3880
|
"lexq_simulation_export",
|
|
3870
3881
|
{
|
|
3871
3882
|
title: "Export Simulation",
|
|
3872
|
-
annotations:
|
|
3873
|
-
description:
|
|
3883
|
+
annotations: READ_ONLY,
|
|
3884
|
+
description: `Export simulation results as JSON or CSV. Returns the raw data. ${EXPORT_LIMIT_NOTE}`,
|
|
3874
3885
|
inputSchema: z.object({
|
|
3875
3886
|
simulationId: z.string().uuid().describe("Simulation ID"),
|
|
3876
3887
|
format: z.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
3877
3888
|
})
|
|
3878
3889
|
},
|
|
3879
|
-
async ({ simulationId, format }) =>
|
|
3880
|
-
|
|
3881
|
-
|
|
3890
|
+
async ({ simulationId, format }) => limitExport(
|
|
3891
|
+
await callApi("GET", `analytics/simulations/${simulationId}/export`, {
|
|
3892
|
+
params: { format }
|
|
3893
|
+
}),
|
|
3894
|
+
`lexq analytics simulation export --id ${simulationId} --as ${format} --output <file>`
|
|
3895
|
+
)
|
|
3882
3896
|
);
|
|
3883
3897
|
server.registerTool(
|
|
3884
3898
|
"lexq_dataset_upload",
|
|
3885
3899
|
{
|
|
3886
3900
|
title: "Upload Dataset",
|
|
3887
|
-
annotations:
|
|
3901
|
+
annotations: ADDITIVE,
|
|
3888
3902
|
description: dedent15`
|
|
3889
3903
|
Upload inline CSV or JSON content as a simulation dataset.
|
|
3890
3904
|
The content is uploaded to S3 and a path is returned in the "path" field.
|
|
@@ -3927,7 +3941,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3927
3941
|
"lexq_dataset_template",
|
|
3928
3942
|
{
|
|
3929
3943
|
title: "Download Dataset Template",
|
|
3930
|
-
annotations:
|
|
3944
|
+
annotations: READ_ONLY,
|
|
3931
3945
|
description: "Generate a sample CSV or JSON template based on the required facts of a version. Use this to understand the expected data format before uploading a dataset.",
|
|
3932
3946
|
inputSchema: z.object({
|
|
3933
3947
|
groupId: z.string().uuid().describe("Policy group ID"),
|
|
@@ -3984,7 +3998,7 @@ function registerLocalTools(server) {
|
|
|
3984
3998
|
"lexq_dataset_upload_file",
|
|
3985
3999
|
{
|
|
3986
4000
|
title: "Upload Dataset File",
|
|
3987
|
-
annotations:
|
|
4001
|
+
annotations: ADDITIVE,
|
|
3988
4002
|
description: dedent16`
|
|
3989
4003
|
Upload a CSV or JSON file from this computer as a simulation dataset, up to ${MAX_DATASET_MB} MB.
|
|
3990
4004
|
The server reads the file from disk, so its content never passes through the conversation.
|
|
@@ -4043,7 +4057,7 @@ function registerStatusTools(server, callApi) {
|
|
|
4043
4057
|
"lexq_whoami",
|
|
4044
4058
|
{
|
|
4045
4059
|
title: "Who Am I",
|
|
4046
|
-
annotations:
|
|
4060
|
+
annotations: READ_ONLY,
|
|
4047
4061
|
description: 'Show current authentication info (tenant ID, user ID, role) and tenantTimezone, the organization time zone that every yyyy-MM-dd date filter uses. Work out "today" or "yesterday" in that zone, not in UTC.',
|
|
4048
4062
|
inputSchema: z3.object({})
|
|
4049
4063
|
},
|
|
@@ -4058,7 +4072,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4058
4072
|
"lexq_groups_list",
|
|
4059
4073
|
{
|
|
4060
4074
|
title: "List Policy Groups",
|
|
4061
|
-
annotations:
|
|
4075
|
+
annotations: READ_ONLY,
|
|
4062
4076
|
description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
|
|
4063
4077
|
inputSchema: z4.object({})
|
|
4064
4078
|
},
|
|
@@ -4068,7 +4082,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4068
4082
|
"lexq_groups_get",
|
|
4069
4083
|
{
|
|
4070
4084
|
title: "Get Policy Group",
|
|
4071
|
-
annotations:
|
|
4085
|
+
annotations: READ_ONLY,
|
|
4072
4086
|
description: "Get a single policy group by ID. Its currentVersionId and currentDeploymentId are what lexq_deploy_rollback expects, and previousVersionName is the version a rollback restores.",
|
|
4073
4087
|
inputSchema: z4.object({
|
|
4074
4088
|
groupId: z4.string().uuid().describe("Policy group ID")
|
|
@@ -4080,7 +4094,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4080
4094
|
"lexq_groups_create",
|
|
4081
4095
|
{
|
|
4082
4096
|
title: "Create Policy Group",
|
|
4083
|
-
annotations:
|
|
4097
|
+
annotations: ADDITIVE,
|
|
4084
4098
|
description: `Create a new policy group. Requires name. Priority is auto-assigned (appended last, tenant-wide); use lexq_groups_reorder to change order. Optionally set conflict resolution, activation group, and description. Policy groups that share an activationGroup form a cluster and must share the same activationMode / activationStrategy / executionLimit; executionLimit is how many of those groups run, not how many rules. A tenant can have at most ${MAX_GROUPS_PER_TENANT} groups that are not ARCHIVED; archive groups you no longer use to free a slot.`,
|
|
4085
4099
|
inputSchema: z4.object({
|
|
4086
4100
|
name: z4.string().describe("Group name (unique among non-ARCHIVED)"),
|
|
@@ -4111,7 +4125,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4111
4125
|
"lexq_groups_update",
|
|
4112
4126
|
{
|
|
4113
4127
|
title: "Update Policy Group",
|
|
4114
|
-
annotations:
|
|
4128
|
+
annotations: DESTRUCTIVE,
|
|
4115
4129
|
description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
|
|
4116
4130
|
inputSchema: z4.object({
|
|
4117
4131
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
@@ -4136,7 +4150,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4136
4150
|
"lexq_groups_delete",
|
|
4137
4151
|
{
|
|
4138
4152
|
title: "Delete Policy Group",
|
|
4139
|
-
annotations:
|
|
4153
|
+
annotations: DESTRUCTIVE,
|
|
4140
4154
|
description: `Delete (archive) a policy group. A deployed group cannot be deleted (P-020); undeploy it first with lexq_deploy_undeploy, which also ends a running A/B test. This cannot be undone. The group no longer appears in lexq_groups_list and its name can be reused right away. Calls that change it (settings, versions, rules, starting an A/B test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted after ${PURGE_AFTER_DAYS} days.`,
|
|
4141
4155
|
inputSchema: z4.object({
|
|
4142
4156
|
groupId: z4.string().uuid().describe("Policy group ID")
|
|
@@ -4148,7 +4162,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4148
4162
|
"lexq_groups_reorder",
|
|
4149
4163
|
{
|
|
4150
4164
|
title: "Reorder Policy Groups",
|
|
4151
|
-
annotations:
|
|
4165
|
+
annotations: DESTRUCTIVE,
|
|
4152
4166
|
description: "Reorder policy groups by priority. Priority is tenant-wide and flat (1...N continuous); array index 0 = priority 1 (highest precedence). activationGroup is not affected \u2014 this only changes priority.",
|
|
4153
4167
|
inputSchema: z4.object({
|
|
4154
4168
|
groupIds: z4.array(z4.string().uuid()).describe("Group IDs in desired priority order (index 0 = priority 1)")
|
|
@@ -4166,8 +4180,8 @@ function registerGroupTools(server, callApi) {
|
|
|
4166
4180
|
"lexq_ab_test_start",
|
|
4167
4181
|
{
|
|
4168
4182
|
title: "Start A/B Test",
|
|
4169
|
-
annotations:
|
|
4170
|
-
description: "Start an A/B test on a policy group. Requires a challenger version ID and traffic rate. The challenger must be a published version of the same group; a version from another group is rejected with P-016. The split is computed from context.trafficKey on each execution request; requests that omit it never reach the challenger and the test stays at 0%.",
|
|
4183
|
+
annotations: DESTRUCTIVE,
|
|
4184
|
+
description: "Start an A/B test on a policy group. Requires a challenger version ID and traffic rate. The challenger must be a published version of the same group; a version from another group is rejected with P-016. The split is computed from context.trafficKey on each execution request; requests that omit it never reach the challenger and the test stays at 0%. Starting cancels the group's pending scheduled deployment. Starting while a test is already running replaces its challenger and traffic rate.",
|
|
4171
4185
|
inputSchema: z4.object({
|
|
4172
4186
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
4173
4187
|
testVersionId: z4.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
|
|
@@ -4182,7 +4196,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4182
4196
|
"lexq_ab_test_stop",
|
|
4183
4197
|
{
|
|
4184
4198
|
title: "Stop A/B Test",
|
|
4185
|
-
annotations:
|
|
4199
|
+
annotations: DESTRUCTIVE,
|
|
4186
4200
|
description: "Stop a running A/B test. All traffic is restored to the control (current) version.",
|
|
4187
4201
|
inputSchema: z4.object({
|
|
4188
4202
|
groupId: z4.string().uuid().describe("Policy group ID")
|
|
@@ -4194,7 +4208,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4194
4208
|
"lexq_ab_test_adjust",
|
|
4195
4209
|
{
|
|
4196
4210
|
title: "Adjust A/B Test",
|
|
4197
|
-
annotations:
|
|
4211
|
+
annotations: DESTRUCTIVE,
|
|
4198
4212
|
description: "Adjust traffic rate of a running A/B test.",
|
|
4199
4213
|
inputSchema: z4.object({
|
|
4200
4214
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
@@ -4214,7 +4228,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4214
4228
|
"lexq_versions_list",
|
|
4215
4229
|
{
|
|
4216
4230
|
title: "List Policy Versions",
|
|
4217
|
-
annotations:
|
|
4231
|
+
annotations: READ_ONLY,
|
|
4218
4232
|
description: "List all versions of a policy group.",
|
|
4219
4233
|
inputSchema: z5.object({
|
|
4220
4234
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4230,7 +4244,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4230
4244
|
"lexq_versions_get",
|
|
4231
4245
|
{
|
|
4232
4246
|
title: "Get Policy Version",
|
|
4233
|
-
annotations:
|
|
4247
|
+
annotations: READ_ONLY,
|
|
4234
4248
|
description: "Get a single version by ID, including its rules and fact requirements.",
|
|
4235
4249
|
inputSchema: z5.object({
|
|
4236
4250
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4243,7 +4257,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4243
4257
|
"lexq_versions_create",
|
|
4244
4258
|
{
|
|
4245
4259
|
title: "Create Policy Version",
|
|
4246
|
-
annotations:
|
|
4260
|
+
annotations: ADDITIVE,
|
|
4247
4261
|
description: `Create a new DRAFT version in a policy group. Optionally provide a commit message and effective date range. A group can hold at most ${MAX_DRAFT_VERSIONS_PER_GROUP} DRAFT versions at a time; published versions do not count. Publish or delete drafts to make room.`,
|
|
4248
4262
|
inputSchema: z5.object({
|
|
4249
4263
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4258,7 +4272,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4258
4272
|
"lexq_versions_update",
|
|
4259
4273
|
{
|
|
4260
4274
|
title: "Update Policy Version",
|
|
4261
|
-
annotations:
|
|
4275
|
+
annotations: DESTRUCTIVE,
|
|
4262
4276
|
description: "Update a DRAFT version. Only DRAFT versions can be modified. Omitted fields keep their current values. To remove an end of the effective window, set clearEffectiveFrom or clearEffectiveTo to true instead of sending a value.",
|
|
4263
4277
|
inputSchema: z5.object({
|
|
4264
4278
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4266,7 +4280,9 @@ function registerVersionTools(server, callApi) {
|
|
|
4266
4280
|
commitMessage: z5.string().optional().describe("New commit message"),
|
|
4267
4281
|
effectiveFrom: z5.string().optional().describe(`Effective start, ${INSTANT} Omit it to keep the current start.`),
|
|
4268
4282
|
effectiveTo: z5.string().optional().describe(`Effective end, ${INSTANT} Omit it to keep the current end.`),
|
|
4269
|
-
clearEffectiveFrom: z5.boolean().optional().describe(
|
|
4283
|
+
clearEffectiveFrom: z5.boolean().optional().describe(
|
|
4284
|
+
"Set to true to remove the effective start. Do not send effectiveFrom with it."
|
|
4285
|
+
),
|
|
4270
4286
|
clearEffectiveTo: z5.boolean().optional().describe("Set to true to remove the effective end. Do not send effectiveTo with it.")
|
|
4271
4287
|
})
|
|
4272
4288
|
},
|
|
@@ -4276,7 +4292,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4276
4292
|
"lexq_versions_delete",
|
|
4277
4293
|
{
|
|
4278
4294
|
title: "Delete Policy Version",
|
|
4279
|
-
annotations:
|
|
4295
|
+
annotations: DESTRUCTIVE,
|
|
4280
4296
|
description: "Delete a DRAFT version. Only DRAFT versions can be deleted.",
|
|
4281
4297
|
inputSchema: z5.object({
|
|
4282
4298
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4289,7 +4305,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4289
4305
|
"lexq_versions_clone",
|
|
4290
4306
|
{
|
|
4291
4307
|
title: "Clone Policy Version",
|
|
4292
|
-
annotations:
|
|
4308
|
+
annotations: ADDITIVE,
|
|
4293
4309
|
description: `Clone an existing version to create a new DRAFT. Useful when the source version is already published. The new DRAFT counts toward the limit of ${MAX_DRAFT_VERSIONS_PER_GROUP} DRAFT versions per group, and a source with more than ${MAX_RULES_PER_VERSION} rules cannot be cloned.`,
|
|
4294
4310
|
inputSchema: z5.object({
|
|
4295
4311
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4313,7 +4329,7 @@ function registerRuleTools(server, callApi) {
|
|
|
4313
4329
|
"lexq_rules_list",
|
|
4314
4330
|
{
|
|
4315
4331
|
title: "List Rules",
|
|
4316
|
-
annotations:
|
|
4332
|
+
annotations: READ_ONLY,
|
|
4317
4333
|
description: "List all rules in a version (priority ASC). Returns summary with conditionSummary and actionSummary.",
|
|
4318
4334
|
inputSchema: z6.object({
|
|
4319
4335
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4326,7 +4342,7 @@ function registerRuleTools(server, callApi) {
|
|
|
4326
4342
|
"lexq_rules_get",
|
|
4327
4343
|
{
|
|
4328
4344
|
title: "Get Rule Detail",
|
|
4329
|
-
annotations:
|
|
4345
|
+
annotations: READ_ONLY,
|
|
4330
4346
|
description: "Get full rule detail including condition tree and action definitions.",
|
|
4331
4347
|
inputSchema: z6.object({
|
|
4332
4348
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4340,87 +4356,39 @@ function registerRuleTools(server, callApi) {
|
|
|
4340
4356
|
"lexq_rules_create",
|
|
4341
4357
|
{
|
|
4342
4358
|
title: "Create Rule",
|
|
4343
|
-
annotations:
|
|
4359
|
+
annotations: ADDITIVE,
|
|
4344
4360
|
description: dedent17`
|
|
4345
|
-
Create a rule in a DRAFT version.
|
|
4346
|
-
|
|
4347
|
-
Before creating rules with new fact keys, call lexq_facts_list to check existing facts.
|
|
4348
|
-
If a required key is missing, ask the user to confirm the type, isRequired, and description
|
|
4349
|
-
before calling lexq_facts_create — registering facts enables type validation, Console UI
|
|
4350
|
-
autocomplete, and the dry-run requirements analyzer.
|
|
4361
|
+
Create a rule in a DRAFT version. It goes last in priority order; lexq_rules_reorder changes the order.
|
|
4351
4362
|
|
|
4352
|
-
An action's targetVar
|
|
4353
|
-
publishing a rule whose targetVar is not registered fails with ACT-032, so register it
|
|
4354
|
-
with lexq_facts_create first. Keys a rule only reads (condition fields, refVar) may stay
|
|
4355
|
-
unregistered.
|
|
4356
|
-
|
|
4357
|
-
After saving, lexq_facts_unregistered lists any keys this version references but has not
|
|
4358
|
-
defined (non-blocking, version-wide) — use it to decide what to register.
|
|
4363
|
+
An action's targetVar must be a registered fact (ACT-032 otherwise). Keys a rule only reads (condition fields, refVar) may stay unregistered.
|
|
4359
4364
|
|
|
4360
4365
|
Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: ${oneOf(LogicalOperator)}, children: [...] }
|
|
4361
4366
|
Value types: ${ValueType.join(", ")}
|
|
4362
|
-
Limits:
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
-
|
|
4368
|
-
-
|
|
4369
|
-
|
|
4370
|
-
- LIST_* fact: HAS_ANY, HAS_ALL, HAS_NONE (only these)
|
|
4371
|
-
|
|
4372
|
-
HAS_* query list-typed facts. Value is always an array whose element type matches the fact:
|
|
4373
|
-
- HAS_ANY: fact has at least one of the given values
|
|
4374
|
-
- HAS_ALL: fact has all of the given values
|
|
4375
|
-
- HAS_NONE: fact has none of the given values
|
|
4367
|
+
Limits: ${MAX_CONDITIONS_PER_RULE} SINGLE conditions per rule (GROUP nodes do not count), ${MAX_CONDITION_DEPTH} levels of nesting below the root, ${MAX_RULES_PER_VERSION} rules per version.
|
|
4368
|
+
|
|
4369
|
+
Operators that work on each fact type:
|
|
4370
|
+
- STRING: EQUALS, NOT_EQUALS, CONTAINS (substring), IN, NOT_IN
|
|
4371
|
+
- NUMBER: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, IN, NOT_IN
|
|
4372
|
+
- BOOLEAN: EQUALS, NOT_EQUALS
|
|
4373
|
+
- LIST_STRING, LIST_NUMBER: HAS_ANY, HAS_ALL, HAS_NONE
|
|
4374
|
+
IN and NOT_IN test whether the fact's value is in the array. HAS_ANY, HAS_ALL and HAS_NONE match when the fact's list holds at least one, all, or none of the array's values. The array holds values of the fact's type, or for a list fact, of its element type.
|
|
4376
4375
|
Example: { "type": "SINGLE", "field": "userTags", "operator": "HAS_ANY", "value": ["VIP","GOLD"], "valueType": "LIST_STRING" }
|
|
4377
4376
|
|
|
4378
|
-
Do NOT use CONTAINS on a list fact — CONTAINS is substring match on STRING facts only.
|
|
4379
|
-
IN is the mirror of HAS_*: IN takes a scalar fact with a list value; HAS_* takes lists on both sides.
|
|
4380
|
-
|
|
4381
4377
|
Actions: [{ type, parameters }]
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
PERCENTAGE,
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
operator: "ADD", method: "PERCENTAGE", operand: 5 }.
|
|
4397
|
-
operator × method matrix:
|
|
4398
|
-
ASSIGN targetVar = operand | targetVar = refVar × operand/100
|
|
4399
|
-
ADD targetVar += operand | targetVar += refVar × operand/100
|
|
4400
|
-
SUB targetVar -= operand | targetVar -= refVar × operand/100
|
|
4401
|
-
MUL targetVar *= operand | targetVar *= (operand/100 + 1)
|
|
4402
|
-
DIV targetVar /= operand | invalid
|
|
4403
|
-
Constraints: DIV + PERCENTAGE is invalid (use MUL with the inverse). DIV + AMOUNT
|
|
4404
|
-
requires operand !== 0.
|
|
4405
|
-
- SET_FACT: { targetVar: string, value: string|number|boolean } targetVar must be a
|
|
4406
|
-
registered fact (ACT-032 otherwise). The request does not have to send it: SET_FACT is
|
|
4407
|
-
the only action whose targetVar may be absent from the input facts. MUTATE_FACT requires
|
|
4408
|
-
its target to be present in the input facts.
|
|
4409
|
-
- BLOCK: { reason?: string } Records a rejection decision. It does NOT halt rule execution —
|
|
4410
|
-
subsequent actions and subsequent winning rules still run. Enforcement is the caller's
|
|
4411
|
-
responsibility; the decision surfaces as isBlocked (true) and blockReason (reason, or
|
|
4412
|
-
"Policy Violation" when omitted).
|
|
4413
|
-
Only BLOCK writes those two keys: saving or publishing a rule whose SET_FACT or
|
|
4414
|
-
MUTATE_FACT targets either one fails with ACT-033.
|
|
4415
|
-
|
|
4416
|
-
Evaluation order: every rule's condition is checked against the input facts before any
|
|
4417
|
-
action runs, so a condition never sees a value an action writes in the same request. Then
|
|
4418
|
-
the actions of the winning rules run in priority order. Each action works on the values
|
|
4419
|
-
the actions before it left, in its own rule or an earlier one. To test a value an action
|
|
4420
|
-
writes, send that value as an input fact in a second request. If a rule should run
|
|
4421
|
-
whenever another one does, put its actions in that rule instead.
|
|
4422
|
-
|
|
4423
|
-
RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: ${oneOf(RoundingPolicy)} } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
|
|
4378
|
+
- MUTATE_FACT: { targetVar, operator: ${oneOf(MutateOperator)}, method: ${oneOf(CalculationMethod)}, operand: number, refVar?, rounding? }
|
|
4379
|
+
Reads and writes targetVar. The request must send it (P-015 otherwise), and it must hold a number when the action runs. operand may be negative, and a percent may be over 100.
|
|
4380
|
+
AMOUNT PERCENTAGE (base = refVar, or targetVar when omitted)
|
|
4381
|
+
ASSIGN targetVar = operand targetVar = base × operand/100
|
|
4382
|
+
ADD targetVar += operand targetVar += base × operand/100
|
|
4383
|
+
SUB targetVar -= operand targetVar -= base × operand/100
|
|
4384
|
+
MUL targetVar *= operand targetVar *= (operand/100 + 1)
|
|
4385
|
+
DIV targetVar /= operand invalid
|
|
4386
|
+
refVar is allowed only with PERCENTAGE and ASSIGN, ADD or SUB. DIV with AMOUNT needs a nonzero operand.
|
|
4387
|
+
rounding: { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: ${oneOf(RoundingPolicy)} }. mode defaults to HALF_UP. Without rounding, the result keeps full precision.
|
|
4388
|
+
- SET_FACT: { targetVar, value: string|number|boolean }. The request does not have to send targetVar.
|
|
4389
|
+
- BLOCK: { reason? } sets isBlocked to true and blockReason to the reason, or "Policy Violation" without one. It does not stop later actions or rules; the caller enforces the block. Only BLOCK writes these two keys: a SET_FACT or MUTATE_FACT targeting either is rejected (ACT-033).
|
|
4390
|
+
|
|
4391
|
+
Every condition is checked against the input facts before any action runs, so a condition never sees a value an action writes in the same request. The winning rules' actions then run in priority order, each on the values the actions before it left.
|
|
4424
4392
|
`,
|
|
4425
4393
|
inputSchema: z6.object({
|
|
4426
4394
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4439,7 +4407,7 @@ function registerRuleTools(server, callApi) {
|
|
|
4439
4407
|
"lexq_rules_update",
|
|
4440
4408
|
{
|
|
4441
4409
|
title: "Update Rule",
|
|
4442
|
-
annotations:
|
|
4410
|
+
annotations: DESTRUCTIVE,
|
|
4443
4411
|
description: `Update an existing rule in a DRAFT version. Only provided fields are changed. A new condition tree must stay within ${MAX_CONDITIONS_PER_RULE} conditions (SINGLE nodes) and ${MAX_CONDITION_DEPTH} levels of nesting below the root. Saving re-checks every action in the rule, including ones you did not change, and fails with ACT-032 if any action's targetVar is not a registered fact.`,
|
|
4444
4412
|
inputSchema: z6.object({
|
|
4445
4413
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4461,28 +4429,21 @@ function registerRuleTools(server, callApi) {
|
|
|
4461
4429
|
"lexq_rules_delete",
|
|
4462
4430
|
{
|
|
4463
4431
|
title: "Delete Rule",
|
|
4464
|
-
annotations:
|
|
4432
|
+
annotations: DESTRUCTIVE,
|
|
4465
4433
|
description: "Delete a rule from a DRAFT version.",
|
|
4466
4434
|
inputSchema: z6.object({
|
|
4467
4435
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4468
4436
|
versionId: z6.string().uuid().describe("Version ID"),
|
|
4469
|
-
ruleId: z6.string().uuid().describe("Rule ID")
|
|
4470
|
-
force: z6.boolean().default(false).describe("Skip confirmation (for the last rule in a version)")
|
|
4437
|
+
ruleId: z6.string().uuid().describe("Rule ID")
|
|
4471
4438
|
})
|
|
4472
4439
|
},
|
|
4473
|
-
async ({ groupId, versionId, ruleId
|
|
4474
|
-
const params = {};
|
|
4475
|
-
if (force) params.force = "true";
|
|
4476
|
-
return callApi("DELETE", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`, {
|
|
4477
|
-
params
|
|
4478
|
-
});
|
|
4479
|
-
}
|
|
4440
|
+
async ({ groupId, versionId, ruleId }) => callApi("DELETE", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`)
|
|
4480
4441
|
);
|
|
4481
4442
|
server.registerTool(
|
|
4482
4443
|
"lexq_rules_reorder",
|
|
4483
4444
|
{
|
|
4484
4445
|
title: "Reorder Rules",
|
|
4485
|
-
annotations:
|
|
4446
|
+
annotations: DESTRUCTIVE,
|
|
4486
4447
|
description: "Reorder rules by specifying rule IDs in desired order. Priorities are assigned 1...N (1-based, continuous); array index 0 = priority 1 (highest precedence).",
|
|
4487
4448
|
inputSchema: z6.object({
|
|
4488
4449
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4504,7 +4465,7 @@ function registerRuleTools(server, callApi) {
|
|
|
4504
4465
|
"lexq_rules_toggle",
|
|
4505
4466
|
{
|
|
4506
4467
|
title: "Toggle Rule",
|
|
4507
|
-
annotations:
|
|
4468
|
+
annotations: DESTRUCTIVE,
|
|
4508
4469
|
description: "Enable or disable a rule without deleting it.",
|
|
4509
4470
|
inputSchema: z6.object({
|
|
4510
4471
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4527,13 +4488,14 @@ var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
|
4527
4488
|
|
|
4528
4489
|
// src/mcp/tools/facts.ts
|
|
4529
4490
|
var VALUE_TYPES = ValueType;
|
|
4491
|
+
var IS_REQUIRED_NOTE = "isRequired is stored on the definition only; the engine does not read it.";
|
|
4530
4492
|
function registerFactTools(server, callApi) {
|
|
4531
4493
|
server.registerTool(
|
|
4532
4494
|
"lexq_facts_list",
|
|
4533
4495
|
{
|
|
4534
4496
|
title: "List Fact Definitions",
|
|
4535
|
-
annotations:
|
|
4536
|
-
description:
|
|
4497
|
+
annotations: READ_ONLY,
|
|
4498
|
+
description: `List all fact definitions (input variable schema): key, name, type, description, PII and system flags, and value domain. ${IS_REQUIRED_NOTE}`,
|
|
4537
4499
|
inputSchema: z7.object({
|
|
4538
4500
|
page: z7.number().int().min(0).default(0).describe("Page number"),
|
|
4539
4501
|
size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
@@ -4550,14 +4512,14 @@ function registerFactTools(server, callApi) {
|
|
|
4550
4512
|
"lexq_facts_create",
|
|
4551
4513
|
{
|
|
4552
4514
|
title: "Create Fact Definition",
|
|
4553
|
-
annotations:
|
|
4515
|
+
annotations: ADDITIVE,
|
|
4554
4516
|
description: `Register a new input variable. Key starts with a letter, then letters, numbers, and underscores (e.g. paymentAmount). Casing is not enforced. Types: ${VALUE_TYPES.join(", ")}.`,
|
|
4555
4517
|
inputSchema: z7.object({
|
|
4556
4518
|
key: z7.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
|
|
4557
4519
|
name: z7.string().describe("Display name"),
|
|
4558
4520
|
type: z7.enum(VALUE_TYPES).describe("Value type"),
|
|
4559
4521
|
description: z7.string().optional().describe("Description"),
|
|
4560
|
-
isRequired: z7.boolean().default(false).describe(
|
|
4522
|
+
isRequired: z7.boolean().default(false).describe(IS_REQUIRED_NOTE),
|
|
4561
4523
|
isPii: z7.boolean().default(false).describe(
|
|
4562
4524
|
"Mark as PII \u2014 masked on every read surface, revealable only in the console (audited)"
|
|
4563
4525
|
),
|
|
@@ -4578,7 +4540,7 @@ function registerFactTools(server, callApi) {
|
|
|
4578
4540
|
"lexq_facts_update",
|
|
4579
4541
|
{
|
|
4580
4542
|
title: "Update Fact Definition",
|
|
4581
|
-
annotations:
|
|
4543
|
+
annotations: DESTRUCTIVE,
|
|
4582
4544
|
description: "Update a fact definition. The key is immutable. The type can change only while no rule references the fact; if any does, the call fails with FD-007 and reports the count. Rules in deleted groups do not count. Only the fields you send are changed. System facts (isBlocked, blockReason) accept changes to name, description, PII, and the value domain; isRequired is ignored, and their type cannot change (FD-003).",
|
|
4583
4545
|
inputSchema: z7.object({
|
|
4584
4546
|
factId: z7.string().uuid().describe("Fact definition ID"),
|
|
@@ -4587,7 +4549,7 @@ function registerFactTools(server, callApi) {
|
|
|
4587
4549
|
type: z7.enum(VALUE_TYPES).optional().describe(
|
|
4588
4550
|
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
|
|
4589
4551
|
),
|
|
4590
|
-
isRequired: z7.boolean().optional().describe(
|
|
4552
|
+
isRequired: z7.boolean().optional().describe(IS_REQUIRED_NOTE),
|
|
4591
4553
|
isPii: z7.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
|
|
4592
4554
|
valueDomain: z7.object({
|
|
4593
4555
|
allowedValues: z7.array(z7.union([z7.string(), z7.number()])).optional().describe(
|
|
@@ -4606,7 +4568,7 @@ function registerFactTools(server, callApi) {
|
|
|
4606
4568
|
"lexq_facts_delete",
|
|
4607
4569
|
{
|
|
4608
4570
|
title: "Delete Fact Definition",
|
|
4609
|
-
annotations:
|
|
4571
|
+
annotations: DESTRUCTIVE,
|
|
4610
4572
|
description: "Delete a fact definition. System facts cannot be deleted. Neither can a fact that any rule references: that call fails with FD-006 and reports the count. Rules in deleted groups do not count. Remove the references first.",
|
|
4611
4573
|
inputSchema: z7.object({
|
|
4612
4574
|
factId: z7.string().uuid().describe("Fact definition ID")
|
|
@@ -4618,7 +4580,7 @@ function registerFactTools(server, callApi) {
|
|
|
4618
4580
|
"lexq_facts_action_metadata",
|
|
4619
4581
|
{
|
|
4620
4582
|
title: "Get Action Runtime Fact Metadata",
|
|
4621
|
-
annotations:
|
|
4583
|
+
annotations: READ_ONLY,
|
|
4622
4584
|
description: "Retrieve runtime fact requirements per Action type. For each Action, shows which input facts must be present in the execution payload \u2014 e.g. MUTATE_FACT always requires its targetVar fact, plus refVar when one is specified. The factRequired flag describes the FACT, not the parameter: refVar is an optional parameter, but if you specify it the named fact must exist. A required fact absent at runtime throws \u2014 the engine never defaults to 0. These facts must be sent in the request (P-015 otherwise), even when an earlier action in the same rule writes them. SET_FACT requires no input fact: it is the only action whose targetVar the request does not have to send, and that targetVar must be a registered fact (ACT-032). Static data, safe to cache in-session.",
|
|
4623
4585
|
inputSchema: z7.object({})
|
|
4624
4586
|
},
|
|
@@ -4628,8 +4590,8 @@ function registerFactTools(server, callApi) {
|
|
|
4628
4590
|
"lexq_facts_unregistered",
|
|
4629
4591
|
{
|
|
4630
4592
|
title: "List Unregistered Facts",
|
|
4631
|
-
annotations:
|
|
4632
|
-
description: "List facts referenced by a version's rules but not yet defined (read-only). Keys that rules only read do not block publish/deploy; a rule whose action targetVar is not a registered fact is rejected at save and publish with ACT-032. Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action).
|
|
4593
|
+
annotations: READ_ONLY,
|
|
4594
|
+
description: "List facts referenced by a version's rules but not yet defined (read-only). Keys that rules only read do not block publish/deploy; a rule whose action targetVar is not a registered fact is rejected at save and publish with ACT-032. Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Registering one with lexq_facts_create before the version is published makes the engine check request values for it against its type (P-060) and any value domain (P-052).",
|
|
4633
4595
|
inputSchema: z7.object({
|
|
4634
4596
|
groupId: z7.string().uuid().describe("Policy group ID"),
|
|
4635
4597
|
versionId: z7.string().uuid().describe("Version ID")
|
|
@@ -4641,16 +4603,20 @@ function registerFactTools(server, callApi) {
|
|
|
4641
4603
|
"lexq_facts_export",
|
|
4642
4604
|
{
|
|
4643
4605
|
title: "Export Fact Catalog",
|
|
4644
|
-
annotations:
|
|
4645
|
-
description:
|
|
4606
|
+
annotations: READ_ONLY,
|
|
4607
|
+
description: `Export the fact catalog. The two formats carry different things: CSV is the catalog as it stands, system facts included, for reading in a spreadsheet, without value domains; JSON lists each definition with only the fields a new definition takes (key, name, type, description, isRequired, isPii, valueDomain), without IDs. Returns the file contents as text. ${EXPORT_LIMIT_NOTE}`,
|
|
4646
4608
|
inputSchema: z7.object({
|
|
4647
4609
|
format: z7.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
|
|
4648
4610
|
keyword: z7.string().optional().describe("Filter by key or name")
|
|
4649
4611
|
})
|
|
4650
4612
|
},
|
|
4651
|
-
async ({ format, keyword }) =>
|
|
4652
|
-
|
|
4653
|
-
|
|
4613
|
+
async ({ format, keyword }) => limitExport(
|
|
4614
|
+
await callApi("GET", "schema/facts/export", {
|
|
4615
|
+
params: keyword ? { format, keyword } : { format }
|
|
4616
|
+
}),
|
|
4617
|
+
`lexq facts export --as ${format} --output <file>`,
|
|
4618
|
+
"Pass keyword to export fewer facts."
|
|
4619
|
+
)
|
|
4654
4620
|
);
|
|
4655
4621
|
}
|
|
4656
4622
|
|
|
@@ -4662,7 +4628,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4662
4628
|
"lexq_deploy_publish",
|
|
4663
4629
|
{
|
|
4664
4630
|
title: "Publish Version",
|
|
4665
|
-
annotations:
|
|
4631
|
+
annotations: DESTRUCTIVE,
|
|
4666
4632
|
description: `Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts that rules only read do not block publishing; call lexq_facts_unregistered first to review them. Publishing fails if an action's targetVar is not a registered fact (ACT-032), if the version's compiled rules exceed ${MAX_SNAPSHOT_BYTES / 1024} KB, or if its effective end has already passed (P-058); move the end into the future with lexq_versions_update first.`,
|
|
4667
4633
|
inputSchema: z8.object({
|
|
4668
4634
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
@@ -4676,7 +4642,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4676
4642
|
"lexq_deploy_live",
|
|
4677
4643
|
{
|
|
4678
4644
|
title: "Deploy to Live",
|
|
4679
|
-
annotations:
|
|
4645
|
+
annotations: DESTRUCTIVE,
|
|
4680
4646
|
description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately. Versions whose effective start date has not arrived are rejected (P-037) \u2014 use lexq_deploy_schedule for those. Versions whose effective end has passed are rejected (P-058). Undefined facts do not block deployment; use lexq_facts_unregistered to review what the version references but has not defined.",
|
|
4681
4647
|
inputSchema: z8.object({
|
|
4682
4648
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
@@ -4692,7 +4658,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4692
4658
|
"lexq_deploy_rollback",
|
|
4693
4659
|
{
|
|
4694
4660
|
title: "Rollback Deployment",
|
|
4695
|
-
annotations:
|
|
4661
|
+
annotations: DESTRUCTIVE,
|
|
4696
4662
|
description: dedent18`
|
|
4697
4663
|
Roll the group back to the version that was live before the current one. Call lexq_groups_get first: currentVersionName is live now, previousVersionName is what a rollback restores, and a rollback is rejected while canRollback is false. A rollback needs a previous version that is still ACTIVE and whose effective end has not passed (P-058).
|
|
4698
4664
|
|
|
@@ -4713,7 +4679,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4713
4679
|
"lexq_deploy_undeploy",
|
|
4714
4680
|
{
|
|
4715
4681
|
title: "Undeploy",
|
|
4716
|
-
annotations:
|
|
4682
|
+
annotations: DESTRUCTIVE,
|
|
4717
4683
|
description: "Remove the live version from traffic. The version stays ACTIVE but no longer serves requests.",
|
|
4718
4684
|
inputSchema: z8.object({
|
|
4719
4685
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
@@ -4728,7 +4694,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4728
4694
|
"lexq_deploy_schedule",
|
|
4729
4695
|
{
|
|
4730
4696
|
title: "Schedule Deployment",
|
|
4731
|
-
annotations:
|
|
4697
|
+
annotations: DESTRUCTIVE,
|
|
4732
4698
|
description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). The time is the version's effectiveFrom, set on the DRAFT before publishing; this call takes no time of its own. One pending schedule per group; manual deploy/rollback/undeploy, starting an A/B test, or archiving the group cancels it. The snapshot hash is sealed at scheduling and re-verified at execution (fail-closed).",
|
|
4733
4699
|
inputSchema: z8.object({
|
|
4734
4700
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
@@ -4744,7 +4710,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4744
4710
|
"lexq_deploy_unschedule",
|
|
4745
4711
|
{
|
|
4746
4712
|
title: "Cancel Scheduled Deployment",
|
|
4747
|
-
annotations:
|
|
4713
|
+
annotations: DESTRUCTIVE,
|
|
4748
4714
|
description: "Cancel the pending scheduled deployment for a group. The version itself is not affected. Fails with P-039 if no pending schedule exists.",
|
|
4749
4715
|
inputSchema: z8.object({
|
|
4750
4716
|
groupId: z8.string().uuid().describe("Policy group ID")
|
|
@@ -4756,7 +4722,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4756
4722
|
"lexq_deploy_schedules",
|
|
4757
4723
|
{
|
|
4758
4724
|
title: "List Scheduled Deployments",
|
|
4759
|
-
annotations:
|
|
4725
|
+
annotations: READ_ONLY,
|
|
4760
4726
|
description: `List scheduled deployments across all groups (all statuses: ${DeploymentScheduleStatus.join(", ")}), newest first.`,
|
|
4761
4727
|
inputSchema: z8.object({
|
|
4762
4728
|
page: z8.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -4769,7 +4735,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4769
4735
|
"lexq_deploy_history",
|
|
4770
4736
|
{
|
|
4771
4737
|
title: "Deployment History",
|
|
4772
|
-
annotations:
|
|
4738
|
+
annotations: READ_ONLY,
|
|
4773
4739
|
description: "List deployment history across all groups.",
|
|
4774
4740
|
inputSchema: z8.object({
|
|
4775
4741
|
page: z8.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -4793,7 +4759,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4793
4759
|
"lexq_deploy_detail",
|
|
4794
4760
|
{
|
|
4795
4761
|
title: "Deployment Detail",
|
|
4796
|
-
annotations:
|
|
4762
|
+
annotations: READ_ONLY,
|
|
4797
4763
|
description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
|
|
4798
4764
|
inputSchema: z8.object({
|
|
4799
4765
|
deploymentId: z8.string().uuid().describe("Deployment ID")
|
|
@@ -4805,7 +4771,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4805
4771
|
"lexq_deploy_overview",
|
|
4806
4772
|
{
|
|
4807
4773
|
title: "Deployment Overview",
|
|
4808
|
-
annotations:
|
|
4774
|
+
annotations: READ_ONLY,
|
|
4809
4775
|
description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer. Each group also carries currentDeploymentId, previousVersionName (the version a rollback restores) and canRollback, as in lexq_groups_get.",
|
|
4810
4776
|
inputSchema: z8.object({})
|
|
4811
4777
|
},
|
|
@@ -4815,7 +4781,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4815
4781
|
"lexq_deploy_deployable",
|
|
4816
4782
|
{
|
|
4817
4783
|
title: "List Deployable Versions",
|
|
4818
|
-
annotations:
|
|
4784
|
+
annotations: READ_ONLY,
|
|
4819
4785
|
description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
|
|
4820
4786
|
inputSchema: z8.object({
|
|
4821
4787
|
groupId: z8.string().uuid().describe("Policy group ID")
|
|
@@ -4827,7 +4793,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4827
4793
|
"lexq_deploy_diff",
|
|
4828
4794
|
{
|
|
4829
4795
|
title: "Deployment Diff",
|
|
4830
|
-
annotations:
|
|
4796
|
+
annotations: READ_ONLY,
|
|
4831
4797
|
description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
|
|
4832
4798
|
inputSchema: z8.object({
|
|
4833
4799
|
baseVersionId: z8.string().uuid().describe("Base version ID (typically the current live)"),
|
|
@@ -4856,7 +4822,7 @@ function registerProfileTools(server, callApi) {
|
|
|
4856
4822
|
"lexq_profile_overview",
|
|
4857
4823
|
{
|
|
4858
4824
|
title: "Group Latency Profile",
|
|
4859
|
-
annotations:
|
|
4825
|
+
annotations: READ_ONLY,
|
|
4860
4826
|
description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cache state (HIT = compiled ruleset cache hit, MISS = deep-load + compile), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD + ` Every percentile is accompanied by its sample count n; a percentile is withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 needs n \u2265 ${PERCENTILE_MIN_N.p50}, p95 n \u2265 ${PERCENTILE_MIN_N.p95}, p99 n \u2265 ${PERCENTILE_MIN_N.p99} \u2014 display gate, separate from the n \u2265 ${MIN_SAMPLES} judgment gate). Baselines report INSUFFICIENT_COHORT when fewer than ${MIN_COHORT_SIZE} rules qualify. Rule detail comes from a deterministic ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls; TOTAL is recorded for every call. Defaults: last ${PROFILE_DEFAULT_WINDOW_HOURS}h, live version, cacheState HIT.`,
|
|
4861
4827
|
inputSchema: z9.object({
|
|
4862
4828
|
groupId: z9.string().uuid().describe("Policy group ID"),
|
|
@@ -4876,7 +4842,7 @@ function registerProfileTools(server, callApi) {
|
|
|
4876
4842
|
"lexq_profile_rule",
|
|
4877
4843
|
{
|
|
4878
4844
|
title: "Rule Latency Detail",
|
|
4879
|
-
annotations:
|
|
4845
|
+
annotations: READ_ONLY,
|
|
4880
4846
|
description: `Single-rule latency detail: merged phase \xD7 cacheState distributions plus a per-window time series (${LATENCY_WINDOW_MILLIS / 1e3}s windows). Missing windows are genuine gaps \u2014 never interpolated. Series points carry each window's own values; percentiles in merged distributions are withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 n \u2265 ${PERCENTILE_MIN_N.p50}, p95 n \u2265 ${PERCENTILE_MIN_N.p95}, p99 n \u2265 ${PERCENTILE_MIN_N.p99}). ` + RELATIVE_THRESHOLD,
|
|
4881
4847
|
inputSchema: z9.object({
|
|
4882
4848
|
groupId: z9.string().uuid().describe("Policy group ID"),
|
|
@@ -4901,8 +4867,8 @@ function registerReplayTools(server, callApi) {
|
|
|
4901
4867
|
"lexq_replay_decision",
|
|
4902
4868
|
{
|
|
4903
4869
|
title: "Replay a Decision",
|
|
4904
|
-
annotations:
|
|
4905
|
-
description:
|
|
4870
|
+
annotations: READ_ONLY,
|
|
4871
|
+
description: `Re-evaluate a past execution (traceId) against a candidate version and return the decision diff (decisionChanged, effect changes, fired rules) plus a determinism verdict. Synchronous. ${DRY_RUN_LIMIT_NOTE} A replay sends no webhook, notification, or event: rule actions produce no outward effects.`,
|
|
4906
4872
|
inputSchema: z10.object({
|
|
4907
4873
|
traceId: z10.string().describe("Trace ID of the past execution to replay"),
|
|
4908
4874
|
candidateVersionId: z10.string().uuid().describe("Version to re-evaluate against")
|
|
@@ -4914,7 +4880,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4914
4880
|
"lexq_replay_start",
|
|
4915
4881
|
{
|
|
4916
4882
|
title: "Start Window Replay (Blast Radius)",
|
|
4917
|
-
annotations:
|
|
4883
|
+
annotations: DESTRUCTIVE,
|
|
4918
4884
|
description: "Submit an async job that replays a date window of past executions against a candidate version and measures the blast radius (how many decisions change). Billed per replayed record (REPLAY metric); VIEWER role cannot submit. Poll with lexq_replay_status.",
|
|
4919
4885
|
inputSchema: z10.object({
|
|
4920
4886
|
candidateVersionId: z10.string().uuid().describe("Version to re-evaluate against"),
|
|
@@ -4931,7 +4897,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4931
4897
|
"lexq_replay_status",
|
|
4932
4898
|
{
|
|
4933
4899
|
title: "Get Replay Job Status",
|
|
4934
|
-
annotations:
|
|
4900
|
+
annotations: READ_ONLY,
|
|
4935
4901
|
description: "Poll a window replay job. RUNNING shows progress 0\u2013100; COMPLETED fills summary and changedSamples; FAILED carries errorMessage. capped=true means the window exceeded the sample cap and only part was replayed.",
|
|
4936
4902
|
inputSchema: z10.object({
|
|
4937
4903
|
jobId: z10.string().describe("Replay job ID from lexq_replay_start")
|
|
@@ -4943,7 +4909,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4943
4909
|
"lexq_replay_list",
|
|
4944
4910
|
{
|
|
4945
4911
|
title: "List Replay Jobs",
|
|
4946
|
-
annotations:
|
|
4912
|
+
annotations: READ_ONLY,
|
|
4947
4913
|
description: "List window replay job history (reverse-chronological). Lightweight items \u2014 use lexq_replay_status for summary and changed samples.",
|
|
4948
4914
|
inputSchema: z10.object({
|
|
4949
4915
|
page: z10.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -4956,7 +4922,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4956
4922
|
"lexq_replay_cancel",
|
|
4957
4923
|
{
|
|
4958
4924
|
title: "Cancel Replay Job",
|
|
4959
|
-
annotations:
|
|
4925
|
+
annotations: DESTRUCTIVE,
|
|
4960
4926
|
description: "Cooperatively cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
|
|
4961
4927
|
inputSchema: z10.object({
|
|
4962
4928
|
jobId: z10.string().describe("Replay job ID")
|
|
@@ -4968,14 +4934,17 @@ function registerReplayTools(server, callApi) {
|
|
|
4968
4934
|
"lexq_replay_export",
|
|
4969
4935
|
{
|
|
4970
4936
|
title: "Export Replay Result",
|
|
4971
|
-
annotations:
|
|
4972
|
-
description:
|
|
4937
|
+
annotations: READ_ONLY,
|
|
4938
|
+
description: `Export a COMPLETED window replay job. A running job is rejected \u2014 a partial result reads as the whole thing on the receiving end. The two formats carry different things: CSV holds the effect blast radius, whose columns are fixed; JSON holds the changed samples and action parameters, which nest and whose keys differ per tenant. JSON is not a superset of CSV. Returns the file contents as text. ${EXPORT_LIMIT_NOTE}`,
|
|
4973
4939
|
inputSchema: z10.object({
|
|
4974
4940
|
jobId: z10.string().describe("Replay job ID"),
|
|
4975
4941
|
format: z10.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
4976
4942
|
})
|
|
4977
4943
|
},
|
|
4978
|
-
async ({ jobId, format }) =>
|
|
4944
|
+
async ({ jobId, format }) => limitExport(
|
|
4945
|
+
await callApi("GET", `replay/jobs/${jobId}/export`, { params: { format } }),
|
|
4946
|
+
`lexq replay export --id ${jobId} --as ${format} --output <file>`
|
|
4947
|
+
)
|
|
4979
4948
|
);
|
|
4980
4949
|
}
|
|
4981
4950
|
|
|
@@ -4986,7 +4955,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
4986
4955
|
"lexq_history_list",
|
|
4987
4956
|
{
|
|
4988
4957
|
title: "List Execution History",
|
|
4989
|
-
annotations:
|
|
4958
|
+
annotations: READ_ONLY,
|
|
4990
4959
|
description: "List policy execution history. Shows trace ID, group, version, status, match result, and latency.",
|
|
4991
4960
|
inputSchema: z11.object({
|
|
4992
4961
|
page: z11.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -5014,7 +4983,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
5014
4983
|
"lexq_history_get",
|
|
5015
4984
|
{
|
|
5016
4985
|
title: "Get Execution Detail",
|
|
5017
|
-
annotations:
|
|
4986
|
+
annotations: READ_ONLY,
|
|
5018
4987
|
description: "Get full execution detail including inputFacts, mutatedFacts, generatedVariables, executionTraces, and decisionTraces.",
|
|
5019
4988
|
inputSchema: z11.object({
|
|
5020
4989
|
traceId: z11.string().describe("Trace ID from execution history")
|
|
@@ -5026,7 +4995,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
5026
4995
|
"lexq_history_stats",
|
|
5027
4996
|
{
|
|
5028
4997
|
title: "Execution Statistics",
|
|
5029
|
-
annotations:
|
|
4998
|
+
annotations: READ_ONLY,
|
|
5030
4999
|
description: `Get execution KPIs: total executions, success/failure counts, success rate, and average latency. Without dates the window is the last ${STATS_DEFAULT_WINDOW_DAYS} days through today, in the organization time zone. At most ${STATS_MAX_WINDOW_DAYS} days per call; a longer range is rejected with AN-039. startDate alone runs through today.`,
|
|
5031
5000
|
inputSchema: z11.object({
|
|
5032
5001
|
groupId: z11.string().uuid().optional().describe("Filter by policy group"),
|
|
@@ -5051,7 +5020,7 @@ function registerProvenanceTools(server, callApi) {
|
|
|
5051
5020
|
"lexq_provenance_get",
|
|
5052
5021
|
{
|
|
5053
5022
|
title: "Get Decision Provenance",
|
|
5054
|
-
annotations:
|
|
5023
|
+
annotations: READ_ONLY,
|
|
5055
5024
|
description: "Get the lineage of a single decision: what was decided, deterministic why per rule, input facts (PII facts are masked as \u2022\u2022\u2022\u2022\u2022\u2022 with maskedKeys listing them \u2014 values are revealable only in the console, audited), the authored/published/deployed responsibility chain, and the rule snapshot fingerprint.",
|
|
5056
5025
|
inputSchema: z12.object({
|
|
5057
5026
|
traceId: z12.string().describe("Trace ID of the execution")
|
|
@@ -5063,7 +5032,7 @@ function registerProvenanceTools(server, callApi) {
|
|
|
5063
5032
|
"lexq_pii_reveals_list",
|
|
5064
5033
|
{
|
|
5065
5034
|
title: "List PII Reveal Audits",
|
|
5066
|
-
annotations:
|
|
5035
|
+
annotations: READ_ONLY,
|
|
5067
5036
|
description: "List the PII reveal audit ledger \u2014 who revealed which fact of which trace, and when. Metadata only; revealed values are never stored or returned. Use for monthly access-log inspection and SIEM collection.",
|
|
5068
5037
|
inputSchema: z12.object({
|
|
5069
5038
|
page: z12.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -5089,17 +5058,13 @@ function registerProvenanceTools(server, callApi) {
|
|
|
5089
5058
|
|
|
5090
5059
|
// src/mcp/tools/logs.ts
|
|
5091
5060
|
import { z as z13 } from "zod";
|
|
5092
|
-
var ACTION_NOTES = {
|
|
5093
|
-
RESOLVE: "mark as manually fixed",
|
|
5094
|
-
IGNORE: "skip intentionally"
|
|
5095
|
-
};
|
|
5096
5061
|
function registerLogTools(server, callApi) {
|
|
5097
5062
|
server.registerTool(
|
|
5098
5063
|
"lexq_logs_list",
|
|
5099
5064
|
{
|
|
5100
5065
|
title: "List Failure Logs",
|
|
5101
|
-
annotations:
|
|
5102
|
-
description: `List system failure logs from background tasks (platform event webhooks, scheduled deployments). Without dates the window is ${FAILURE_LOG_LOOKBACK_DAYS} days back through today, in the organization time zone; pass startDate to look further back
|
|
5066
|
+
annotations: READ_ONLY,
|
|
5067
|
+
description: `List system failure logs from background tasks (platform event webhooks, scheduled deployments). Without dates the window is ${FAILURE_LOG_LOOKBACK_DAYS} days back through today, in the organization time zone; pass startDate to look further back. ${FAILURE_LOG_CONSOLE_NOTE}`,
|
|
5103
5068
|
inputSchema: z13.object({
|
|
5104
5069
|
page: z13.number().int().min(0).default(0).describe("Page number"),
|
|
5105
5070
|
size: z13.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
@@ -5124,44 +5089,14 @@ function registerLogTools(server, callApi) {
|
|
|
5124
5089
|
"lexq_logs_get",
|
|
5125
5090
|
{
|
|
5126
5091
|
title: "Get Failure Log",
|
|
5127
|
-
annotations:
|
|
5128
|
-
description:
|
|
5092
|
+
annotations: READ_ONLY,
|
|
5093
|
+
description: `Get failure log detail by ID. ${FAILURE_LOG_CONSOLE_NOTE}`,
|
|
5129
5094
|
inputSchema: z13.object({
|
|
5130
5095
|
logId: z13.string().uuid().describe("Failure log ID")
|
|
5131
5096
|
})
|
|
5132
5097
|
},
|
|
5133
5098
|
async ({ logId }) => callApi("GET", `failure-logs/${logId}`)
|
|
5134
5099
|
);
|
|
5135
|
-
server.registerTool(
|
|
5136
|
-
"lexq_logs_action",
|
|
5137
|
-
{
|
|
5138
|
-
title: "Process Failure Log",
|
|
5139
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
5140
|
-
description: `Process a single failure log: ${withNotes(ACTION_NOTES).join(" or ")}.`,
|
|
5141
|
-
inputSchema: z13.object({
|
|
5142
|
-
logId: z13.string().uuid().describe("Failure log ID"),
|
|
5143
|
-
action: z13.enum(FailureAction).describe("Action to take")
|
|
5144
|
-
})
|
|
5145
|
-
},
|
|
5146
|
-
async ({ logId, action }) => callApi("POST", `failure-logs/${logId}/actions`, {
|
|
5147
|
-
params: { action }
|
|
5148
|
-
})
|
|
5149
|
-
);
|
|
5150
|
-
server.registerTool(
|
|
5151
|
-
"lexq_logs_bulk_action",
|
|
5152
|
-
{
|
|
5153
|
-
title: "Bulk Process Failure Logs",
|
|
5154
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
5155
|
-
description: "Process multiple failure logs at once. Provide an array of log IDs and the action.",
|
|
5156
|
-
inputSchema: z13.object({
|
|
5157
|
-
logIds: z13.array(z13.string().uuid()).describe("Array of failure log IDs"),
|
|
5158
|
-
action: z13.enum(FailureAction).describe("Action to apply to all logs")
|
|
5159
|
-
})
|
|
5160
|
-
},
|
|
5161
|
-
async ({ logIds, action }) => callApi("POST", "failure-logs/bulk-actions", {
|
|
5162
|
-
body: { logIds, action }
|
|
5163
|
-
})
|
|
5164
|
-
);
|
|
5165
5100
|
}
|
|
5166
5101
|
|
|
5167
5102
|
// src/mcp/tools/webhook-subscriptions.ts
|
|
@@ -5175,7 +5110,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5175
5110
|
"lexq_webhook_subscriptions_list",
|
|
5176
5111
|
{
|
|
5177
5112
|
title: "List Webhook Subscriptions",
|
|
5178
|
-
annotations:
|
|
5113
|
+
annotations: READ_ONLY,
|
|
5179
5114
|
description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications: publish, deploy, rollback, and undeploy, plus a deployment being scheduled or a schedule being canceled.",
|
|
5180
5115
|
inputSchema: z14.object({
|
|
5181
5116
|
page: z14.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -5191,7 +5126,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5191
5126
|
"lexq_webhook_subscriptions_get",
|
|
5192
5127
|
{
|
|
5193
5128
|
title: "Get Webhook Subscription",
|
|
5194
|
-
annotations:
|
|
5129
|
+
annotations: READ_ONLY,
|
|
5195
5130
|
description: "Get webhook subscription detail by ID.",
|
|
5196
5131
|
inputSchema: z14.object({
|
|
5197
5132
|
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
@@ -5203,7 +5138,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5203
5138
|
"lexq_webhook_subscriptions_save",
|
|
5204
5139
|
{
|
|
5205
5140
|
title: "Save Webhook Subscription",
|
|
5206
|
-
annotations: {
|
|
5141
|
+
annotations: { ...DESTRUCTIVE, openWorldHint: true },
|
|
5207
5142
|
description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: ${withNotes(FORMAT_NOTES2).join(", ")}.`,
|
|
5208
5143
|
inputSchema: z14.object({
|
|
5209
5144
|
id: z14.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
|
|
@@ -5221,7 +5156,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5221
5156
|
"lexq_webhook_subscriptions_delete",
|
|
5222
5157
|
{
|
|
5223
5158
|
title: "Delete Webhook Subscription",
|
|
5224
|
-
annotations:
|
|
5159
|
+
annotations: DESTRUCTIVE,
|
|
5225
5160
|
description: "Delete a webhook subscription by ID.",
|
|
5226
5161
|
inputSchema: z14.object({
|
|
5227
5162
|
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
@@ -5233,7 +5168,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5233
5168
|
"lexq_webhook_subscriptions_test",
|
|
5234
5169
|
{
|
|
5235
5170
|
title: "Test Webhook Subscription",
|
|
5236
|
-
annotations: {
|
|
5171
|
+
annotations: { ...DESTRUCTIVE, openWorldHint: true },
|
|
5237
5172
|
description: "Send a test event to verify webhook connectivity. Returns the HTTP status code and success/failure message.",
|
|
5238
5173
|
inputSchema: z14.object({
|
|
5239
5174
|
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
@@ -5250,7 +5185,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5250
5185
|
"lexq_domain_templates_list",
|
|
5251
5186
|
{
|
|
5252
5187
|
title: "List Domain Templates",
|
|
5253
|
-
annotations:
|
|
5188
|
+
annotations: READ_ONLY,
|
|
5254
5189
|
description: "List all domain templates. A domain template is a starter pack of fact definitions and sample rules for a common decision, such as checkout pricing (ECOMMERCE) or transfer approval (FINTECH). Each entry reports its key, whether it can be applied (isAvailable), and how many facts and sample rules it provisions. Call this before preview or apply to see which templates exist.",
|
|
5255
5190
|
inputSchema: z15.object({})
|
|
5256
5191
|
},
|
|
@@ -5260,7 +5195,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5260
5195
|
"lexq_domain_templates_preview",
|
|
5261
5196
|
{
|
|
5262
5197
|
title: "Preview Domain Template",
|
|
5263
|
-
annotations:
|
|
5198
|
+
annotations: READ_ONLY,
|
|
5264
5199
|
description: "Preview exactly what a domain template will provision before applying it: the fact definitions it registers (an input with a value domain carries its allowed values or range), the sample rules it creates with a description of each, and an apply plan. This is a read-only dry run; nothing is created. factConflicts lists keys this tenant already has with a different type, or with different allowed values or a different range on an input fact. While it is not empty, apply fails with P-061 and creates nothing: delete those facts or change them to match the template, then apply. plan is null for a template that cannot be applied yet.",
|
|
5265
5200
|
inputSchema: z15.object({
|
|
5266
5201
|
template: z15.string().describe(
|
|
@@ -5274,7 +5209,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5274
5209
|
"lexq_domain_templates_apply",
|
|
5275
5210
|
{
|
|
5276
5211
|
title: "Apply Domain Template",
|
|
5277
|
-
annotations:
|
|
5212
|
+
annotations: ADDITIVE,
|
|
5278
5213
|
description: "Apply a domain template to the current tenant. Creates the template's fact definitions and a new policy group pre-populated with its sample rules as a DRAFT version. Apply never overwrites existing schema: a key that already exists with the same definition is kept as it is. Run lexq_domain_templates_preview first. If its factConflicts is not empty, apply fails with P-061 and creates nothing until those facts are deleted or changed to match the template. A template whose isAvailable is false cannot be applied.",
|
|
5279
5214
|
inputSchema: z15.object({
|
|
5280
5215
|
template: z15.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
|
|
@@ -5309,6 +5244,13 @@ function registerAllTools(server, callApi) {
|
|
|
5309
5244
|
registerWebhookSubscriptionTools(server, callApi);
|
|
5310
5245
|
}
|
|
5311
5246
|
var SERVER_INFO = { name: "lexq", ...server_info_default };
|
|
5247
|
+
var SERVER_INSTRUCTIONS = [
|
|
5248
|
+
"LexQ evaluates business rules: a policy group holds versions, a version holds rules, and rules read input facts.",
|
|
5249
|
+
"- Date filters (yyyy-MM-dd) are whole days in the organization's time zone, not UTC. Call lexq_whoami for tenantTimezone before working out the current date or a date range.",
|
|
5250
|
+
"- Before creating rules, call lexq_facts_list to see which facts exist and their types. When a rule needs a fact that is not defined, confirm its type and description with the user before calling lexq_facts_create.",
|
|
5251
|
+
"- Before lexq_deploy_publish, run lexq_dry_run on the version with representative facts. A published version cannot go back to draft.",
|
|
5252
|
+
`- For anything these tools cannot do, point the user to the LexQ console at ${CONSOLE_URL}.`
|
|
5253
|
+
].join("\n");
|
|
5312
5254
|
|
|
5313
5255
|
// src/mcp/server.ts
|
|
5314
5256
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -5325,7 +5267,10 @@ function startMcpServer() {
|
|
|
5325
5267
|
const callApi = createCallApiFromConfig();
|
|
5326
5268
|
serveStdio(
|
|
5327
5269
|
() => {
|
|
5328
|
-
const server = new McpServer(
|
|
5270
|
+
const server = new McpServer(
|
|
5271
|
+
{ ...SERVER_INFO, version },
|
|
5272
|
+
{ instructions: SERVER_INSTRUCTIONS }
|
|
5273
|
+
);
|
|
5329
5274
|
registerAllTools(server, callApi);
|
|
5330
5275
|
registerLocalTools(server);
|
|
5331
5276
|
return server;
|
|
@@ -5521,7 +5466,12 @@ function createCli() {
|
|
|
5521
5466
|
const program = new Command();
|
|
5522
5467
|
program.name("lexq").description(
|
|
5523
5468
|
"Command-line interface for the LexQ Decision Operations Platform. Manage policies, run Impact Simulation, and deploy from your terminal."
|
|
5524
|
-
).version(getVersion2(), "-V, --version").option(
|
|
5469
|
+
).version(getVersion2(), "-V, --version").option(
|
|
5470
|
+
"--format <format>",
|
|
5471
|
+
`How results are printed: ${OUTPUT_FORMATS.join(" or ")}`,
|
|
5472
|
+
parseOutputFormat,
|
|
5473
|
+
"json"
|
|
5474
|
+
).option("--api-key <key>", "Override stored API key").option(
|
|
5525
5475
|
"--base-url <url>",
|
|
5526
5476
|
"Override API base URL \u2014 must include the API prefix, e.g. http://localhost:8080/api/v1/partners"
|
|
5527
5477
|
).option("--dry-run", "Preview the HTTP request without executing").option("--verbose", "Show request/response details").option("--no-color", "Disable colored output");
|