@lexq/cli 2.1.1 → 3.0.0
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 +1 -1
- package/CONTEXT.md +1 -1
- package/README.md +4 -3
- package/dist/index.js +196 -289
- package/dist/mcp/register.d.ts +7 -1
- package/dist/mcp/register.js +278 -312
- package/package.json +2 -1
- package/skills/lexq-execution/SKILL.md +8 -20
- package/skills/lexq-recipes/SKILL.md +9 -13
- package/skills/lexq-shared/SKILL.md +1 -1
package/dist/index.js
CHANGED
|
@@ -425,7 +425,6 @@ var SimulationDatasetSource = ["EXECUTION_LOGS", "S3_BUCKET", "REQUEST_BODY"];
|
|
|
425
425
|
var SimulationMetricType = ["COUNT", "SUM", "AVG"];
|
|
426
426
|
var ExportFormat = ["CSV", "JSON"];
|
|
427
427
|
var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
|
|
428
|
-
var FailureAction = ["IGNORE", "RESOLVE"];
|
|
429
428
|
var TaskType = ["PLATFORM_WEBHOOK", "SCHEDULED_DEPLOYMENT"];
|
|
430
429
|
var PlatformEventType = [
|
|
431
430
|
"VERSION_PUBLISHED",
|
|
@@ -2608,17 +2607,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2608
2607
|
).action(async (opts) => {
|
|
2609
2608
|
try {
|
|
2610
2609
|
const globalOpts = program.opts();
|
|
2611
|
-
await runExport(
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
verbose: globalOpts.verbose,
|
|
2619
|
-
output: opts.output
|
|
2620
|
-
}
|
|
2621
|
-
);
|
|
2610
|
+
await runExport(`analytics/simulations/${opts.id}/export`, parseFormat(opts.as), {
|
|
2611
|
+
apiKey: globalOpts.apiKey,
|
|
2612
|
+
baseUrl: globalOpts.baseUrl,
|
|
2613
|
+
dryRun: globalOpts.dryRun,
|
|
2614
|
+
verbose: globalOpts.verbose,
|
|
2615
|
+
output: opts.output
|
|
2616
|
+
});
|
|
2622
2617
|
} catch (error) {
|
|
2623
2618
|
printError(error);
|
|
2624
2619
|
process.exit(1);
|
|
@@ -3254,6 +3249,10 @@ function withNotes(notes) {
|
|
|
3254
3249
|
return Object.entries(notes).map(([value, note]) => note ? `${value} (${note})` : value);
|
|
3255
3250
|
}
|
|
3256
3251
|
|
|
3252
|
+
// src/lib/console.ts
|
|
3253
|
+
var CONSOLE_URL = "https://console.lexq.io";
|
|
3254
|
+
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).`;
|
|
3255
|
+
|
|
3257
3256
|
// src/commands/logs.ts
|
|
3258
3257
|
var STATUS_NOTES = {
|
|
3259
3258
|
PENDING: "needs attention",
|
|
@@ -3270,8 +3269,8 @@ function registerLogCommands(program) {
|
|
|
3270
3269
|
Commands:
|
|
3271
3270
|
list List failure logs with filters
|
|
3272
3271
|
get Get failure log detail (includes the original payload)
|
|
3273
|
-
|
|
3274
|
-
|
|
3272
|
+
|
|
3273
|
+
${FAILURE_LOG_CONSOLE_NOTE}
|
|
3275
3274
|
|
|
3276
3275
|
Statuses: ${withNotes(STATUS_NOTES).join(", ")}
|
|
3277
3276
|
`
|
|
@@ -3332,7 +3331,8 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3332
3331
|
dedent13`
|
|
3333
3332
|
|
|
3334
3333
|
Includes the full payload that was used for the failed operation.
|
|
3335
|
-
Use this to inspect what went wrong
|
|
3334
|
+
Use this to inspect what went wrong. Resolving or ignoring a log happens in the
|
|
3335
|
+
console; see lexq logs --help.
|
|
3336
3336
|
`
|
|
3337
3337
|
).action(async (opts) => {
|
|
3338
3338
|
try {
|
|
@@ -3349,63 +3349,6 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3349
3349
|
process.exit(1);
|
|
3350
3350
|
}
|
|
3351
3351
|
});
|
|
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
3352
|
}
|
|
3410
3353
|
|
|
3411
3354
|
// src/commands/webhook-subscriptions.ts
|
|
@@ -3627,6 +3570,21 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
|
3627
3570
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
3628
3571
|
|
|
3629
3572
|
// src/mcp/tools/_shared.ts
|
|
3573
|
+
var READ_ONLY = {
|
|
3574
|
+
readOnlyHint: true,
|
|
3575
|
+
destructiveHint: false,
|
|
3576
|
+
openWorldHint: false
|
|
3577
|
+
};
|
|
3578
|
+
var ADDITIVE = {
|
|
3579
|
+
readOnlyHint: false,
|
|
3580
|
+
destructiveHint: false,
|
|
3581
|
+
openWorldHint: false
|
|
3582
|
+
};
|
|
3583
|
+
var DESTRUCTIVE = {
|
|
3584
|
+
readOnlyHint: false,
|
|
3585
|
+
destructiveHint: true,
|
|
3586
|
+
openWorldHint: false
|
|
3587
|
+
};
|
|
3630
3588
|
function localApiOptions() {
|
|
3631
3589
|
const stored = loadConfig();
|
|
3632
3590
|
return {
|
|
@@ -3649,9 +3607,8 @@ function createCallApiFromConfig() {
|
|
|
3649
3607
|
form,
|
|
3650
3608
|
params: opts?.params
|
|
3651
3609
|
});
|
|
3652
|
-
const
|
|
3653
|
-
|
|
3654
|
-
];
|
|
3610
|
+
const text = typeof data === "string" ? data : stringifyJson(data, 2) ?? "";
|
|
3611
|
+
const content = [{ type: "text", text }];
|
|
3655
3612
|
const warning = formatUnregisteredFactWarning(meta);
|
|
3656
3613
|
if (warning) content.push({ type: "text", text: warning });
|
|
3657
3614
|
const domainWarning = formatValueDomainMismatchWarning(meta);
|
|
@@ -3672,6 +3629,20 @@ function paginationParams(page, size) {
|
|
|
3672
3629
|
if (size !== void 0) params.size = String(size);
|
|
3673
3630
|
return params;
|
|
3674
3631
|
}
|
|
3632
|
+
var MAX_EXPORT_CHARS = 1e5;
|
|
3633
|
+
var formatCount = (count) => count.toLocaleString("en-US");
|
|
3634
|
+
var EXPORT_LIMIT_NOTE = `An export longer than ${formatCount(MAX_EXPORT_CHARS)} characters is refused.`;
|
|
3635
|
+
function limitExport(result, command, narrower) {
|
|
3636
|
+
if (result.isError) return result;
|
|
3637
|
+
const length = result.content.reduce((sum, block) => sum + block.text.length, 0);
|
|
3638
|
+
if (length <= MAX_EXPORT_CHARS) return result;
|
|
3639
|
+
const text = [
|
|
3640
|
+
`The export is ${formatCount(length)} characters long, and this tool returns at most ${formatCount(MAX_EXPORT_CHARS)}.`,
|
|
3641
|
+
narrower,
|
|
3642
|
+
`To get the whole file, download it from the LexQ console (${CONSOLE_URL}) or with \`${command}\`.`
|
|
3643
|
+
].filter(Boolean).join(" ");
|
|
3644
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
3645
|
+
}
|
|
3675
3646
|
function formatUnregisteredFactWarning(meta) {
|
|
3676
3647
|
const facts = meta?.unregisteredFacts ?? [];
|
|
3677
3648
|
if (facts.length === 0) return null;
|
|
@@ -3683,9 +3654,9 @@ function formatUnregisteredFactWarning(meta) {
|
|
|
3683
3654
|
return ` \u2022 ${f.key} (ambiguous) \u2014 pick a type (${choices}), then lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: <chosen> })`;
|
|
3684
3655
|
});
|
|
3685
3656
|
return [
|
|
3686
|
-
`\u26A0 The saved rule references ${facts.length} fact(s) not defined in the schema. The rule was saved
|
|
3657
|
+
`\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
3658
|
...lines,
|
|
3688
|
-
`Register them
|
|
3659
|
+
`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
3660
|
].join("\n");
|
|
3690
3661
|
}
|
|
3691
3662
|
|
|
@@ -3711,7 +3682,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3711
3682
|
"lexq_dry_run",
|
|
3712
3683
|
{
|
|
3713
3684
|
title: "Dry Run",
|
|
3714
|
-
annotations:
|
|
3685
|
+
annotations: READ_ONLY,
|
|
3715
3686
|
description: dedent15`
|
|
3716
3687
|
Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
|
|
3717
3688
|
|
|
@@ -3722,8 +3693,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3722
3693
|
executionTraces — per-rule match status
|
|
3723
3694
|
decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
|
|
3724
3695
|
|
|
3725
|
-
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
|
|
3726
|
-
Always dry-run before publishing to validate rule behavior.`,
|
|
3696
|
+
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }`,
|
|
3727
3697
|
inputSchema: z.object({
|
|
3728
3698
|
versionId: z.string().uuid().describe("Policy version ID to test against"),
|
|
3729
3699
|
facts: z.string().describe('JSON string of facts object, e.g. {"paymentAmount":100000}'),
|
|
@@ -3741,7 +3711,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3741
3711
|
"lexq_dry_run_compare",
|
|
3742
3712
|
{
|
|
3743
3713
|
title: "Dry Run Compare",
|
|
3744
|
-
annotations:
|
|
3714
|
+
annotations: READ_ONLY,
|
|
3745
3715
|
description: dedent15`
|
|
3746
3716
|
Compare dry run results between two versions using the same input facts. Useful for validating changes.
|
|
3747
3717
|
|
|
@@ -3766,7 +3736,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3766
3736
|
"lexq_requirements",
|
|
3767
3737
|
{
|
|
3768
3738
|
title: "Analyze Requirements",
|
|
3769
|
-
annotations:
|
|
3739
|
+
annotations: READ_ONLY,
|
|
3770
3740
|
description: "Analyze which input facts a version requires. Returns required keys, types, and an example request body.",
|
|
3771
3741
|
inputSchema: z.object({
|
|
3772
3742
|
groupId: z.string().uuid().describe("Policy group ID"),
|
|
@@ -3779,10 +3749,13 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3779
3749
|
"lexq_simulation_start",
|
|
3780
3750
|
{
|
|
3781
3751
|
title: "Start Simulation",
|
|
3782
|
-
annotations:
|
|
3752
|
+
annotations: DESTRUCTIVE,
|
|
3783
3753
|
description: dedent15`
|
|
3784
3754
|
Start an Impact Simulation against historical, uploaded, or inline data.
|
|
3785
3755
|
|
|
3756
|
+
Starting checks the plan's simulation usage limit. The records a simulation processes
|
|
3757
|
+
count toward that usage when it completes or is canceled.
|
|
3758
|
+
|
|
3786
3759
|
dataset.type and dataset.source are BOTH required, and must be paired:
|
|
3787
3760
|
HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
|
|
3788
3761
|
in the organization time zone, both inclusive)
|
|
@@ -3823,7 +3796,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3823
3796
|
"lexq_simulation_status",
|
|
3824
3797
|
{
|
|
3825
3798
|
title: "Simulation Status",
|
|
3826
|
-
annotations:
|
|
3799
|
+
annotations: READ_ONLY,
|
|
3827
3800
|
description: "Get simulation status and results. Poll until status is COMPLETED or FAILED.",
|
|
3828
3801
|
inputSchema: z.object({
|
|
3829
3802
|
simulationId: z.string().uuid().describe("Simulation ID")
|
|
@@ -3835,7 +3808,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3835
3808
|
"lexq_simulation_list",
|
|
3836
3809
|
{
|
|
3837
3810
|
title: "List Simulations",
|
|
3838
|
-
annotations:
|
|
3811
|
+
annotations: READ_ONLY,
|
|
3839
3812
|
description: "List simulation history with optional filters.",
|
|
3840
3813
|
inputSchema: z.object({
|
|
3841
3814
|
page: z.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -3857,7 +3830,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3857
3830
|
"lexq_simulation_cancel",
|
|
3858
3831
|
{
|
|
3859
3832
|
title: "Cancel Simulation",
|
|
3860
|
-
annotations:
|
|
3833
|
+
annotations: DESTRUCTIVE,
|
|
3861
3834
|
description: "Cancel a running or pending simulation.",
|
|
3862
3835
|
inputSchema: z.object({
|
|
3863
3836
|
simulationId: z.string().uuid().describe("Simulation ID")
|
|
@@ -3869,22 +3842,25 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3869
3842
|
"lexq_simulation_export",
|
|
3870
3843
|
{
|
|
3871
3844
|
title: "Export Simulation",
|
|
3872
|
-
annotations:
|
|
3873
|
-
description:
|
|
3845
|
+
annotations: READ_ONLY,
|
|
3846
|
+
description: `Export simulation results as JSON or CSV. Returns the raw data. ${EXPORT_LIMIT_NOTE}`,
|
|
3874
3847
|
inputSchema: z.object({
|
|
3875
3848
|
simulationId: z.string().uuid().describe("Simulation ID"),
|
|
3876
3849
|
format: z.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
3877
3850
|
})
|
|
3878
3851
|
},
|
|
3879
|
-
async ({ simulationId, format }) =>
|
|
3880
|
-
|
|
3881
|
-
|
|
3852
|
+
async ({ simulationId, format }) => limitExport(
|
|
3853
|
+
await callApi("GET", `analytics/simulations/${simulationId}/export`, {
|
|
3854
|
+
params: { format }
|
|
3855
|
+
}),
|
|
3856
|
+
`lexq analytics simulation export --id ${simulationId} --as ${format} --output <file>`
|
|
3857
|
+
)
|
|
3882
3858
|
);
|
|
3883
3859
|
server.registerTool(
|
|
3884
3860
|
"lexq_dataset_upload",
|
|
3885
3861
|
{
|
|
3886
3862
|
title: "Upload Dataset",
|
|
3887
|
-
annotations:
|
|
3863
|
+
annotations: ADDITIVE,
|
|
3888
3864
|
description: dedent15`
|
|
3889
3865
|
Upload inline CSV or JSON content as a simulation dataset.
|
|
3890
3866
|
The content is uploaded to S3 and a path is returned in the "path" field.
|
|
@@ -3927,7 +3903,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3927
3903
|
"lexq_dataset_template",
|
|
3928
3904
|
{
|
|
3929
3905
|
title: "Download Dataset Template",
|
|
3930
|
-
annotations:
|
|
3906
|
+
annotations: READ_ONLY,
|
|
3931
3907
|
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
3908
|
inputSchema: z.object({
|
|
3933
3909
|
groupId: z.string().uuid().describe("Policy group ID"),
|
|
@@ -3984,7 +3960,7 @@ function registerLocalTools(server) {
|
|
|
3984
3960
|
"lexq_dataset_upload_file",
|
|
3985
3961
|
{
|
|
3986
3962
|
title: "Upload Dataset File",
|
|
3987
|
-
annotations:
|
|
3963
|
+
annotations: ADDITIVE,
|
|
3988
3964
|
description: dedent16`
|
|
3989
3965
|
Upload a CSV or JSON file from this computer as a simulation dataset, up to ${MAX_DATASET_MB} MB.
|
|
3990
3966
|
The server reads the file from disk, so its content never passes through the conversation.
|
|
@@ -4043,7 +4019,7 @@ function registerStatusTools(server, callApi) {
|
|
|
4043
4019
|
"lexq_whoami",
|
|
4044
4020
|
{
|
|
4045
4021
|
title: "Who Am I",
|
|
4046
|
-
annotations:
|
|
4022
|
+
annotations: READ_ONLY,
|
|
4047
4023
|
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
4024
|
inputSchema: z3.object({})
|
|
4049
4025
|
},
|
|
@@ -4058,7 +4034,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4058
4034
|
"lexq_groups_list",
|
|
4059
4035
|
{
|
|
4060
4036
|
title: "List Policy Groups",
|
|
4061
|
-
annotations:
|
|
4037
|
+
annotations: READ_ONLY,
|
|
4062
4038
|
description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
|
|
4063
4039
|
inputSchema: z4.object({})
|
|
4064
4040
|
},
|
|
@@ -4068,7 +4044,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4068
4044
|
"lexq_groups_get",
|
|
4069
4045
|
{
|
|
4070
4046
|
title: "Get Policy Group",
|
|
4071
|
-
annotations:
|
|
4047
|
+
annotations: READ_ONLY,
|
|
4072
4048
|
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
4049
|
inputSchema: z4.object({
|
|
4074
4050
|
groupId: z4.string().uuid().describe("Policy group ID")
|
|
@@ -4080,7 +4056,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4080
4056
|
"lexq_groups_create",
|
|
4081
4057
|
{
|
|
4082
4058
|
title: "Create Policy Group",
|
|
4083
|
-
annotations:
|
|
4059
|
+
annotations: ADDITIVE,
|
|
4084
4060
|
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
4061
|
inputSchema: z4.object({
|
|
4086
4062
|
name: z4.string().describe("Group name (unique among non-ARCHIVED)"),
|
|
@@ -4111,7 +4087,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4111
4087
|
"lexq_groups_update",
|
|
4112
4088
|
{
|
|
4113
4089
|
title: "Update Policy Group",
|
|
4114
|
-
annotations:
|
|
4090
|
+
annotations: DESTRUCTIVE,
|
|
4115
4091
|
description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
|
|
4116
4092
|
inputSchema: z4.object({
|
|
4117
4093
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
@@ -4136,7 +4112,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4136
4112
|
"lexq_groups_delete",
|
|
4137
4113
|
{
|
|
4138
4114
|
title: "Delete Policy Group",
|
|
4139
|
-
annotations:
|
|
4115
|
+
annotations: DESTRUCTIVE,
|
|
4140
4116
|
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
4117
|
inputSchema: z4.object({
|
|
4142
4118
|
groupId: z4.string().uuid().describe("Policy group ID")
|
|
@@ -4148,7 +4124,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4148
4124
|
"lexq_groups_reorder",
|
|
4149
4125
|
{
|
|
4150
4126
|
title: "Reorder Policy Groups",
|
|
4151
|
-
annotations:
|
|
4127
|
+
annotations: DESTRUCTIVE,
|
|
4152
4128
|
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
4129
|
inputSchema: z4.object({
|
|
4154
4130
|
groupIds: z4.array(z4.string().uuid()).describe("Group IDs in desired priority order (index 0 = priority 1)")
|
|
@@ -4166,8 +4142,8 @@ function registerGroupTools(server, callApi) {
|
|
|
4166
4142
|
"lexq_ab_test_start",
|
|
4167
4143
|
{
|
|
4168
4144
|
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%.",
|
|
4145
|
+
annotations: DESTRUCTIVE,
|
|
4146
|
+
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
4147
|
inputSchema: z4.object({
|
|
4172
4148
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
4173
4149
|
testVersionId: z4.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
|
|
@@ -4182,7 +4158,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4182
4158
|
"lexq_ab_test_stop",
|
|
4183
4159
|
{
|
|
4184
4160
|
title: "Stop A/B Test",
|
|
4185
|
-
annotations:
|
|
4161
|
+
annotations: DESTRUCTIVE,
|
|
4186
4162
|
description: "Stop a running A/B test. All traffic is restored to the control (current) version.",
|
|
4187
4163
|
inputSchema: z4.object({
|
|
4188
4164
|
groupId: z4.string().uuid().describe("Policy group ID")
|
|
@@ -4194,7 +4170,7 @@ function registerGroupTools(server, callApi) {
|
|
|
4194
4170
|
"lexq_ab_test_adjust",
|
|
4195
4171
|
{
|
|
4196
4172
|
title: "Adjust A/B Test",
|
|
4197
|
-
annotations:
|
|
4173
|
+
annotations: DESTRUCTIVE,
|
|
4198
4174
|
description: "Adjust traffic rate of a running A/B test.",
|
|
4199
4175
|
inputSchema: z4.object({
|
|
4200
4176
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
@@ -4214,7 +4190,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4214
4190
|
"lexq_versions_list",
|
|
4215
4191
|
{
|
|
4216
4192
|
title: "List Policy Versions",
|
|
4217
|
-
annotations:
|
|
4193
|
+
annotations: READ_ONLY,
|
|
4218
4194
|
description: "List all versions of a policy group.",
|
|
4219
4195
|
inputSchema: z5.object({
|
|
4220
4196
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4230,7 +4206,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4230
4206
|
"lexq_versions_get",
|
|
4231
4207
|
{
|
|
4232
4208
|
title: "Get Policy Version",
|
|
4233
|
-
annotations:
|
|
4209
|
+
annotations: READ_ONLY,
|
|
4234
4210
|
description: "Get a single version by ID, including its rules and fact requirements.",
|
|
4235
4211
|
inputSchema: z5.object({
|
|
4236
4212
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4243,7 +4219,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4243
4219
|
"lexq_versions_create",
|
|
4244
4220
|
{
|
|
4245
4221
|
title: "Create Policy Version",
|
|
4246
|
-
annotations:
|
|
4222
|
+
annotations: ADDITIVE,
|
|
4247
4223
|
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
4224
|
inputSchema: z5.object({
|
|
4249
4225
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4258,7 +4234,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4258
4234
|
"lexq_versions_update",
|
|
4259
4235
|
{
|
|
4260
4236
|
title: "Update Policy Version",
|
|
4261
|
-
annotations:
|
|
4237
|
+
annotations: DESTRUCTIVE,
|
|
4262
4238
|
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
4239
|
inputSchema: z5.object({
|
|
4264
4240
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4266,7 +4242,9 @@ function registerVersionTools(server, callApi) {
|
|
|
4266
4242
|
commitMessage: z5.string().optional().describe("New commit message"),
|
|
4267
4243
|
effectiveFrom: z5.string().optional().describe(`Effective start, ${INSTANT} Omit it to keep the current start.`),
|
|
4268
4244
|
effectiveTo: z5.string().optional().describe(`Effective end, ${INSTANT} Omit it to keep the current end.`),
|
|
4269
|
-
clearEffectiveFrom: z5.boolean().optional().describe(
|
|
4245
|
+
clearEffectiveFrom: z5.boolean().optional().describe(
|
|
4246
|
+
"Set to true to remove the effective start. Do not send effectiveFrom with it."
|
|
4247
|
+
),
|
|
4270
4248
|
clearEffectiveTo: z5.boolean().optional().describe("Set to true to remove the effective end. Do not send effectiveTo with it.")
|
|
4271
4249
|
})
|
|
4272
4250
|
},
|
|
@@ -4276,7 +4254,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4276
4254
|
"lexq_versions_delete",
|
|
4277
4255
|
{
|
|
4278
4256
|
title: "Delete Policy Version",
|
|
4279
|
-
annotations:
|
|
4257
|
+
annotations: DESTRUCTIVE,
|
|
4280
4258
|
description: "Delete a DRAFT version. Only DRAFT versions can be deleted.",
|
|
4281
4259
|
inputSchema: z5.object({
|
|
4282
4260
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4289,7 +4267,7 @@ function registerVersionTools(server, callApi) {
|
|
|
4289
4267
|
"lexq_versions_clone",
|
|
4290
4268
|
{
|
|
4291
4269
|
title: "Clone Policy Version",
|
|
4292
|
-
annotations:
|
|
4270
|
+
annotations: ADDITIVE,
|
|
4293
4271
|
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
4272
|
inputSchema: z5.object({
|
|
4295
4273
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
@@ -4313,7 +4291,7 @@ function registerRuleTools(server, callApi) {
|
|
|
4313
4291
|
"lexq_rules_list",
|
|
4314
4292
|
{
|
|
4315
4293
|
title: "List Rules",
|
|
4316
|
-
annotations:
|
|
4294
|
+
annotations: READ_ONLY,
|
|
4317
4295
|
description: "List all rules in a version (priority ASC). Returns summary with conditionSummary and actionSummary.",
|
|
4318
4296
|
inputSchema: z6.object({
|
|
4319
4297
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4326,7 +4304,7 @@ function registerRuleTools(server, callApi) {
|
|
|
4326
4304
|
"lexq_rules_get",
|
|
4327
4305
|
{
|
|
4328
4306
|
title: "Get Rule Detail",
|
|
4329
|
-
annotations:
|
|
4307
|
+
annotations: READ_ONLY,
|
|
4330
4308
|
description: "Get full rule detail including condition tree and action definitions.",
|
|
4331
4309
|
inputSchema: z6.object({
|
|
4332
4310
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4340,87 +4318,39 @@ function registerRuleTools(server, callApi) {
|
|
|
4340
4318
|
"lexq_rules_create",
|
|
4341
4319
|
{
|
|
4342
4320
|
title: "Create Rule",
|
|
4343
|
-
annotations:
|
|
4321
|
+
annotations: ADDITIVE,
|
|
4344
4322
|
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.
|
|
4323
|
+
Create a rule in a DRAFT version. It goes last in priority order; lexq_rules_reorder changes the order.
|
|
4351
4324
|
|
|
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.
|
|
4325
|
+
An action's targetVar must be a registered fact (ACT-032 otherwise). Keys a rule only reads (condition fields, refVar) may stay unregistered.
|
|
4359
4326
|
|
|
4360
4327
|
Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: ${oneOf(LogicalOperator)}, children: [...] }
|
|
4361
4328
|
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
|
|
4329
|
+
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.
|
|
4330
|
+
|
|
4331
|
+
Operators that work on each fact type:
|
|
4332
|
+
- STRING: EQUALS, NOT_EQUALS, CONTAINS (substring), IN, NOT_IN
|
|
4333
|
+
- NUMBER: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, IN, NOT_IN
|
|
4334
|
+
- BOOLEAN: EQUALS, NOT_EQUALS
|
|
4335
|
+
- LIST_STRING, LIST_NUMBER: HAS_ANY, HAS_ALL, HAS_NONE
|
|
4336
|
+
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
4337
|
Example: { "type": "SINGLE", "field": "userTags", "operator": "HAS_ANY", "value": ["VIP","GOLD"], "valueType": "LIST_STRING" }
|
|
4377
4338
|
|
|
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
4339
|
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).
|
|
4340
|
+
- MUTATE_FACT: { targetVar, operator: ${oneOf(MutateOperator)}, method: ${oneOf(CalculationMethod)}, operand: number, refVar?, rounding? }
|
|
4341
|
+
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.
|
|
4342
|
+
AMOUNT PERCENTAGE (base = refVar, or targetVar when omitted)
|
|
4343
|
+
ASSIGN targetVar = operand targetVar = base × operand/100
|
|
4344
|
+
ADD targetVar += operand targetVar += base × operand/100
|
|
4345
|
+
SUB targetVar -= operand targetVar -= base × operand/100
|
|
4346
|
+
MUL targetVar *= operand targetVar *= (operand/100 + 1)
|
|
4347
|
+
DIV targetVar /= operand invalid
|
|
4348
|
+
refVar is allowed only with PERCENTAGE and ASSIGN, ADD or SUB. DIV with AMOUNT needs a nonzero operand.
|
|
4349
|
+
rounding: { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: ${oneOf(RoundingPolicy)} }. mode defaults to HALF_UP. Without rounding, the result keeps full precision.
|
|
4350
|
+
- SET_FACT: { targetVar, value: string|number|boolean }. The request does not have to send targetVar.
|
|
4351
|
+
- 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).
|
|
4352
|
+
|
|
4353
|
+
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
4354
|
`,
|
|
4425
4355
|
inputSchema: z6.object({
|
|
4426
4356
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4439,7 +4369,7 @@ function registerRuleTools(server, callApi) {
|
|
|
4439
4369
|
"lexq_rules_update",
|
|
4440
4370
|
{
|
|
4441
4371
|
title: "Update Rule",
|
|
4442
|
-
annotations:
|
|
4372
|
+
annotations: DESTRUCTIVE,
|
|
4443
4373
|
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
4374
|
inputSchema: z6.object({
|
|
4445
4375
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4461,28 +4391,21 @@ function registerRuleTools(server, callApi) {
|
|
|
4461
4391
|
"lexq_rules_delete",
|
|
4462
4392
|
{
|
|
4463
4393
|
title: "Delete Rule",
|
|
4464
|
-
annotations:
|
|
4394
|
+
annotations: DESTRUCTIVE,
|
|
4465
4395
|
description: "Delete a rule from a DRAFT version.",
|
|
4466
4396
|
inputSchema: z6.object({
|
|
4467
4397
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4468
4398
|
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)")
|
|
4399
|
+
ruleId: z6.string().uuid().describe("Rule ID")
|
|
4471
4400
|
})
|
|
4472
4401
|
},
|
|
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
|
-
}
|
|
4402
|
+
async ({ groupId, versionId, ruleId }) => callApi("DELETE", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`)
|
|
4480
4403
|
);
|
|
4481
4404
|
server.registerTool(
|
|
4482
4405
|
"lexq_rules_reorder",
|
|
4483
4406
|
{
|
|
4484
4407
|
title: "Reorder Rules",
|
|
4485
|
-
annotations:
|
|
4408
|
+
annotations: DESTRUCTIVE,
|
|
4486
4409
|
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
4410
|
inputSchema: z6.object({
|
|
4488
4411
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4504,7 +4427,7 @@ function registerRuleTools(server, callApi) {
|
|
|
4504
4427
|
"lexq_rules_toggle",
|
|
4505
4428
|
{
|
|
4506
4429
|
title: "Toggle Rule",
|
|
4507
|
-
annotations:
|
|
4430
|
+
annotations: DESTRUCTIVE,
|
|
4508
4431
|
description: "Enable or disable a rule without deleting it.",
|
|
4509
4432
|
inputSchema: z6.object({
|
|
4510
4433
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -4527,13 +4450,14 @@ var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
|
4527
4450
|
|
|
4528
4451
|
// src/mcp/tools/facts.ts
|
|
4529
4452
|
var VALUE_TYPES = ValueType;
|
|
4453
|
+
var IS_REQUIRED_NOTE = "isRequired is stored on the definition only; the engine does not read it.";
|
|
4530
4454
|
function registerFactTools(server, callApi) {
|
|
4531
4455
|
server.registerTool(
|
|
4532
4456
|
"lexq_facts_list",
|
|
4533
4457
|
{
|
|
4534
4458
|
title: "List Fact Definitions",
|
|
4535
|
-
annotations:
|
|
4536
|
-
description:
|
|
4459
|
+
annotations: READ_ONLY,
|
|
4460
|
+
description: `List all fact definitions (input variable schema): key, name, type, description, PII and system flags, and value domain. ${IS_REQUIRED_NOTE}`,
|
|
4537
4461
|
inputSchema: z7.object({
|
|
4538
4462
|
page: z7.number().int().min(0).default(0).describe("Page number"),
|
|
4539
4463
|
size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
@@ -4550,14 +4474,14 @@ function registerFactTools(server, callApi) {
|
|
|
4550
4474
|
"lexq_facts_create",
|
|
4551
4475
|
{
|
|
4552
4476
|
title: "Create Fact Definition",
|
|
4553
|
-
annotations:
|
|
4477
|
+
annotations: ADDITIVE,
|
|
4554
4478
|
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
4479
|
inputSchema: z7.object({
|
|
4556
4480
|
key: z7.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
|
|
4557
4481
|
name: z7.string().describe("Display name"),
|
|
4558
4482
|
type: z7.enum(VALUE_TYPES).describe("Value type"),
|
|
4559
4483
|
description: z7.string().optional().describe("Description"),
|
|
4560
|
-
isRequired: z7.boolean().default(false).describe(
|
|
4484
|
+
isRequired: z7.boolean().default(false).describe(IS_REQUIRED_NOTE),
|
|
4561
4485
|
isPii: z7.boolean().default(false).describe(
|
|
4562
4486
|
"Mark as PII \u2014 masked on every read surface, revealable only in the console (audited)"
|
|
4563
4487
|
),
|
|
@@ -4578,7 +4502,7 @@ function registerFactTools(server, callApi) {
|
|
|
4578
4502
|
"lexq_facts_update",
|
|
4579
4503
|
{
|
|
4580
4504
|
title: "Update Fact Definition",
|
|
4581
|
-
annotations:
|
|
4505
|
+
annotations: DESTRUCTIVE,
|
|
4582
4506
|
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
4507
|
inputSchema: z7.object({
|
|
4584
4508
|
factId: z7.string().uuid().describe("Fact definition ID"),
|
|
@@ -4587,7 +4511,7 @@ function registerFactTools(server, callApi) {
|
|
|
4587
4511
|
type: z7.enum(VALUE_TYPES).optional().describe(
|
|
4588
4512
|
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
|
|
4589
4513
|
),
|
|
4590
|
-
isRequired: z7.boolean().optional().describe(
|
|
4514
|
+
isRequired: z7.boolean().optional().describe(IS_REQUIRED_NOTE),
|
|
4591
4515
|
isPii: z7.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
|
|
4592
4516
|
valueDomain: z7.object({
|
|
4593
4517
|
allowedValues: z7.array(z7.union([z7.string(), z7.number()])).optional().describe(
|
|
@@ -4606,7 +4530,7 @@ function registerFactTools(server, callApi) {
|
|
|
4606
4530
|
"lexq_facts_delete",
|
|
4607
4531
|
{
|
|
4608
4532
|
title: "Delete Fact Definition",
|
|
4609
|
-
annotations:
|
|
4533
|
+
annotations: DESTRUCTIVE,
|
|
4610
4534
|
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
4535
|
inputSchema: z7.object({
|
|
4612
4536
|
factId: z7.string().uuid().describe("Fact definition ID")
|
|
@@ -4618,7 +4542,7 @@ function registerFactTools(server, callApi) {
|
|
|
4618
4542
|
"lexq_facts_action_metadata",
|
|
4619
4543
|
{
|
|
4620
4544
|
title: "Get Action Runtime Fact Metadata",
|
|
4621
|
-
annotations:
|
|
4545
|
+
annotations: READ_ONLY,
|
|
4622
4546
|
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
4547
|
inputSchema: z7.object({})
|
|
4624
4548
|
},
|
|
@@ -4628,8 +4552,8 @@ function registerFactTools(server, callApi) {
|
|
|
4628
4552
|
"lexq_facts_unregistered",
|
|
4629
4553
|
{
|
|
4630
4554
|
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).
|
|
4555
|
+
annotations: READ_ONLY,
|
|
4556
|
+
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
4557
|
inputSchema: z7.object({
|
|
4634
4558
|
groupId: z7.string().uuid().describe("Policy group ID"),
|
|
4635
4559
|
versionId: z7.string().uuid().describe("Version ID")
|
|
@@ -4641,16 +4565,20 @@ function registerFactTools(server, callApi) {
|
|
|
4641
4565
|
"lexq_facts_export",
|
|
4642
4566
|
{
|
|
4643
4567
|
title: "Export Fact Catalog",
|
|
4644
|
-
annotations:
|
|
4645
|
-
description:
|
|
4568
|
+
annotations: READ_ONLY,
|
|
4569
|
+
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
4570
|
inputSchema: z7.object({
|
|
4647
4571
|
format: z7.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
|
|
4648
4572
|
keyword: z7.string().optional().describe("Filter by key or name")
|
|
4649
4573
|
})
|
|
4650
4574
|
},
|
|
4651
|
-
async ({ format, keyword }) =>
|
|
4652
|
-
|
|
4653
|
-
|
|
4575
|
+
async ({ format, keyword }) => limitExport(
|
|
4576
|
+
await callApi("GET", "schema/facts/export", {
|
|
4577
|
+
params: keyword ? { format, keyword } : { format }
|
|
4578
|
+
}),
|
|
4579
|
+
`lexq facts export --as ${format} --output <file>`,
|
|
4580
|
+
"Pass keyword to export fewer facts."
|
|
4581
|
+
)
|
|
4654
4582
|
);
|
|
4655
4583
|
}
|
|
4656
4584
|
|
|
@@ -4662,7 +4590,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4662
4590
|
"lexq_deploy_publish",
|
|
4663
4591
|
{
|
|
4664
4592
|
title: "Publish Version",
|
|
4665
|
-
annotations:
|
|
4593
|
+
annotations: DESTRUCTIVE,
|
|
4666
4594
|
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
4595
|
inputSchema: z8.object({
|
|
4668
4596
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
@@ -4676,7 +4604,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4676
4604
|
"lexq_deploy_live",
|
|
4677
4605
|
{
|
|
4678
4606
|
title: "Deploy to Live",
|
|
4679
|
-
annotations:
|
|
4607
|
+
annotations: DESTRUCTIVE,
|
|
4680
4608
|
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
4609
|
inputSchema: z8.object({
|
|
4682
4610
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
@@ -4692,7 +4620,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4692
4620
|
"lexq_deploy_rollback",
|
|
4693
4621
|
{
|
|
4694
4622
|
title: "Rollback Deployment",
|
|
4695
|
-
annotations:
|
|
4623
|
+
annotations: DESTRUCTIVE,
|
|
4696
4624
|
description: dedent18`
|
|
4697
4625
|
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
4626
|
|
|
@@ -4713,7 +4641,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4713
4641
|
"lexq_deploy_undeploy",
|
|
4714
4642
|
{
|
|
4715
4643
|
title: "Undeploy",
|
|
4716
|
-
annotations:
|
|
4644
|
+
annotations: DESTRUCTIVE,
|
|
4717
4645
|
description: "Remove the live version from traffic. The version stays ACTIVE but no longer serves requests.",
|
|
4718
4646
|
inputSchema: z8.object({
|
|
4719
4647
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
@@ -4728,7 +4656,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4728
4656
|
"lexq_deploy_schedule",
|
|
4729
4657
|
{
|
|
4730
4658
|
title: "Schedule Deployment",
|
|
4731
|
-
annotations:
|
|
4659
|
+
annotations: DESTRUCTIVE,
|
|
4732
4660
|
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
4661
|
inputSchema: z8.object({
|
|
4734
4662
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
@@ -4744,7 +4672,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4744
4672
|
"lexq_deploy_unschedule",
|
|
4745
4673
|
{
|
|
4746
4674
|
title: "Cancel Scheduled Deployment",
|
|
4747
|
-
annotations:
|
|
4675
|
+
annotations: DESTRUCTIVE,
|
|
4748
4676
|
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
4677
|
inputSchema: z8.object({
|
|
4750
4678
|
groupId: z8.string().uuid().describe("Policy group ID")
|
|
@@ -4756,7 +4684,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4756
4684
|
"lexq_deploy_schedules",
|
|
4757
4685
|
{
|
|
4758
4686
|
title: "List Scheduled Deployments",
|
|
4759
|
-
annotations:
|
|
4687
|
+
annotations: READ_ONLY,
|
|
4760
4688
|
description: `List scheduled deployments across all groups (all statuses: ${DeploymentScheduleStatus.join(", ")}), newest first.`,
|
|
4761
4689
|
inputSchema: z8.object({
|
|
4762
4690
|
page: z8.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -4769,7 +4697,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4769
4697
|
"lexq_deploy_history",
|
|
4770
4698
|
{
|
|
4771
4699
|
title: "Deployment History",
|
|
4772
|
-
annotations:
|
|
4700
|
+
annotations: READ_ONLY,
|
|
4773
4701
|
description: "List deployment history across all groups.",
|
|
4774
4702
|
inputSchema: z8.object({
|
|
4775
4703
|
page: z8.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -4793,7 +4721,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4793
4721
|
"lexq_deploy_detail",
|
|
4794
4722
|
{
|
|
4795
4723
|
title: "Deployment Detail",
|
|
4796
|
-
annotations:
|
|
4724
|
+
annotations: READ_ONLY,
|
|
4797
4725
|
description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
|
|
4798
4726
|
inputSchema: z8.object({
|
|
4799
4727
|
deploymentId: z8.string().uuid().describe("Deployment ID")
|
|
@@ -4805,7 +4733,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4805
4733
|
"lexq_deploy_overview",
|
|
4806
4734
|
{
|
|
4807
4735
|
title: "Deployment Overview",
|
|
4808
|
-
annotations:
|
|
4736
|
+
annotations: READ_ONLY,
|
|
4809
4737
|
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
4738
|
inputSchema: z8.object({})
|
|
4811
4739
|
},
|
|
@@ -4815,7 +4743,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4815
4743
|
"lexq_deploy_deployable",
|
|
4816
4744
|
{
|
|
4817
4745
|
title: "List Deployable Versions",
|
|
4818
|
-
annotations:
|
|
4746
|
+
annotations: READ_ONLY,
|
|
4819
4747
|
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
4748
|
inputSchema: z8.object({
|
|
4821
4749
|
groupId: z8.string().uuid().describe("Policy group ID")
|
|
@@ -4827,7 +4755,7 @@ function registerDeployTools(server, callApi) {
|
|
|
4827
4755
|
"lexq_deploy_diff",
|
|
4828
4756
|
{
|
|
4829
4757
|
title: "Deployment Diff",
|
|
4830
|
-
annotations:
|
|
4758
|
+
annotations: READ_ONLY,
|
|
4831
4759
|
description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
|
|
4832
4760
|
inputSchema: z8.object({
|
|
4833
4761
|
baseVersionId: z8.string().uuid().describe("Base version ID (typically the current live)"),
|
|
@@ -4856,7 +4784,7 @@ function registerProfileTools(server, callApi) {
|
|
|
4856
4784
|
"lexq_profile_overview",
|
|
4857
4785
|
{
|
|
4858
4786
|
title: "Group Latency Profile",
|
|
4859
|
-
annotations:
|
|
4787
|
+
annotations: READ_ONLY,
|
|
4860
4788
|
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
4789
|
inputSchema: z9.object({
|
|
4862
4790
|
groupId: z9.string().uuid().describe("Policy group ID"),
|
|
@@ -4876,7 +4804,7 @@ function registerProfileTools(server, callApi) {
|
|
|
4876
4804
|
"lexq_profile_rule",
|
|
4877
4805
|
{
|
|
4878
4806
|
title: "Rule Latency Detail",
|
|
4879
|
-
annotations:
|
|
4807
|
+
annotations: READ_ONLY,
|
|
4880
4808
|
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
4809
|
inputSchema: z9.object({
|
|
4882
4810
|
groupId: z9.string().uuid().describe("Policy group ID"),
|
|
@@ -4901,7 +4829,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4901
4829
|
"lexq_replay_decision",
|
|
4902
4830
|
{
|
|
4903
4831
|
title: "Replay a Decision",
|
|
4904
|
-
annotations:
|
|
4832
|
+
annotations: READ_ONLY,
|
|
4905
4833
|
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 and free of charge (TPS throttle only). A replay sends no webhook, notification, or event: rule actions produce no outward effects.",
|
|
4906
4834
|
inputSchema: z10.object({
|
|
4907
4835
|
traceId: z10.string().describe("Trace ID of the past execution to replay"),
|
|
@@ -4914,7 +4842,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4914
4842
|
"lexq_replay_start",
|
|
4915
4843
|
{
|
|
4916
4844
|
title: "Start Window Replay (Blast Radius)",
|
|
4917
|
-
annotations:
|
|
4845
|
+
annotations: DESTRUCTIVE,
|
|
4918
4846
|
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
4847
|
inputSchema: z10.object({
|
|
4920
4848
|
candidateVersionId: z10.string().uuid().describe("Version to re-evaluate against"),
|
|
@@ -4931,7 +4859,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4931
4859
|
"lexq_replay_status",
|
|
4932
4860
|
{
|
|
4933
4861
|
title: "Get Replay Job Status",
|
|
4934
|
-
annotations:
|
|
4862
|
+
annotations: READ_ONLY,
|
|
4935
4863
|
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
4864
|
inputSchema: z10.object({
|
|
4937
4865
|
jobId: z10.string().describe("Replay job ID from lexq_replay_start")
|
|
@@ -4943,7 +4871,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4943
4871
|
"lexq_replay_list",
|
|
4944
4872
|
{
|
|
4945
4873
|
title: "List Replay Jobs",
|
|
4946
|
-
annotations:
|
|
4874
|
+
annotations: READ_ONLY,
|
|
4947
4875
|
description: "List window replay job history (reverse-chronological). Lightweight items \u2014 use lexq_replay_status for summary and changed samples.",
|
|
4948
4876
|
inputSchema: z10.object({
|
|
4949
4877
|
page: z10.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -4956,7 +4884,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4956
4884
|
"lexq_replay_cancel",
|
|
4957
4885
|
{
|
|
4958
4886
|
title: "Cancel Replay Job",
|
|
4959
|
-
annotations:
|
|
4887
|
+
annotations: DESTRUCTIVE,
|
|
4960
4888
|
description: "Cooperatively cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
|
|
4961
4889
|
inputSchema: z10.object({
|
|
4962
4890
|
jobId: z10.string().describe("Replay job ID")
|
|
@@ -4968,14 +4896,17 @@ function registerReplayTools(server, callApi) {
|
|
|
4968
4896
|
"lexq_replay_export",
|
|
4969
4897
|
{
|
|
4970
4898
|
title: "Export Replay Result",
|
|
4971
|
-
annotations:
|
|
4972
|
-
description:
|
|
4899
|
+
annotations: READ_ONLY,
|
|
4900
|
+
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
4901
|
inputSchema: z10.object({
|
|
4974
4902
|
jobId: z10.string().describe("Replay job ID"),
|
|
4975
4903
|
format: z10.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
4976
4904
|
})
|
|
4977
4905
|
},
|
|
4978
|
-
async ({ jobId, format }) =>
|
|
4906
|
+
async ({ jobId, format }) => limitExport(
|
|
4907
|
+
await callApi("GET", `replay/jobs/${jobId}/export`, { params: { format } }),
|
|
4908
|
+
`lexq replay export --id ${jobId} --as ${format} --output <file>`
|
|
4909
|
+
)
|
|
4979
4910
|
);
|
|
4980
4911
|
}
|
|
4981
4912
|
|
|
@@ -4986,7 +4917,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
4986
4917
|
"lexq_history_list",
|
|
4987
4918
|
{
|
|
4988
4919
|
title: "List Execution History",
|
|
4989
|
-
annotations:
|
|
4920
|
+
annotations: READ_ONLY,
|
|
4990
4921
|
description: "List policy execution history. Shows trace ID, group, version, status, match result, and latency.",
|
|
4991
4922
|
inputSchema: z11.object({
|
|
4992
4923
|
page: z11.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -5014,7 +4945,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
5014
4945
|
"lexq_history_get",
|
|
5015
4946
|
{
|
|
5016
4947
|
title: "Get Execution Detail",
|
|
5017
|
-
annotations:
|
|
4948
|
+
annotations: READ_ONLY,
|
|
5018
4949
|
description: "Get full execution detail including inputFacts, mutatedFacts, generatedVariables, executionTraces, and decisionTraces.",
|
|
5019
4950
|
inputSchema: z11.object({
|
|
5020
4951
|
traceId: z11.string().describe("Trace ID from execution history")
|
|
@@ -5026,7 +4957,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
5026
4957
|
"lexq_history_stats",
|
|
5027
4958
|
{
|
|
5028
4959
|
title: "Execution Statistics",
|
|
5029
|
-
annotations:
|
|
4960
|
+
annotations: READ_ONLY,
|
|
5030
4961
|
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
4962
|
inputSchema: z11.object({
|
|
5032
4963
|
groupId: z11.string().uuid().optional().describe("Filter by policy group"),
|
|
@@ -5051,7 +4982,7 @@ function registerProvenanceTools(server, callApi) {
|
|
|
5051
4982
|
"lexq_provenance_get",
|
|
5052
4983
|
{
|
|
5053
4984
|
title: "Get Decision Provenance",
|
|
5054
|
-
annotations:
|
|
4985
|
+
annotations: READ_ONLY,
|
|
5055
4986
|
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
4987
|
inputSchema: z12.object({
|
|
5057
4988
|
traceId: z12.string().describe("Trace ID of the execution")
|
|
@@ -5063,7 +4994,7 @@ function registerProvenanceTools(server, callApi) {
|
|
|
5063
4994
|
"lexq_pii_reveals_list",
|
|
5064
4995
|
{
|
|
5065
4996
|
title: "List PII Reveal Audits",
|
|
5066
|
-
annotations:
|
|
4997
|
+
annotations: READ_ONLY,
|
|
5067
4998
|
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
4999
|
inputSchema: z12.object({
|
|
5069
5000
|
page: z12.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -5089,17 +5020,13 @@ function registerProvenanceTools(server, callApi) {
|
|
|
5089
5020
|
|
|
5090
5021
|
// src/mcp/tools/logs.ts
|
|
5091
5022
|
import { z as z13 } from "zod";
|
|
5092
|
-
var ACTION_NOTES = {
|
|
5093
|
-
RESOLVE: "mark as manually fixed",
|
|
5094
|
-
IGNORE: "skip intentionally"
|
|
5095
|
-
};
|
|
5096
5023
|
function registerLogTools(server, callApi) {
|
|
5097
5024
|
server.registerTool(
|
|
5098
5025
|
"lexq_logs_list",
|
|
5099
5026
|
{
|
|
5100
5027
|
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
|
|
5028
|
+
annotations: READ_ONLY,
|
|
5029
|
+
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
5030
|
inputSchema: z13.object({
|
|
5104
5031
|
page: z13.number().int().min(0).default(0).describe("Page number"),
|
|
5105
5032
|
size: z13.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
@@ -5124,44 +5051,14 @@ function registerLogTools(server, callApi) {
|
|
|
5124
5051
|
"lexq_logs_get",
|
|
5125
5052
|
{
|
|
5126
5053
|
title: "Get Failure Log",
|
|
5127
|
-
annotations:
|
|
5128
|
-
description:
|
|
5054
|
+
annotations: READ_ONLY,
|
|
5055
|
+
description: `Get failure log detail by ID. ${FAILURE_LOG_CONSOLE_NOTE}`,
|
|
5129
5056
|
inputSchema: z13.object({
|
|
5130
5057
|
logId: z13.string().uuid().describe("Failure log ID")
|
|
5131
5058
|
})
|
|
5132
5059
|
},
|
|
5133
5060
|
async ({ logId }) => callApi("GET", `failure-logs/${logId}`)
|
|
5134
5061
|
);
|
|
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
5062
|
}
|
|
5166
5063
|
|
|
5167
5064
|
// src/mcp/tools/webhook-subscriptions.ts
|
|
@@ -5175,7 +5072,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5175
5072
|
"lexq_webhook_subscriptions_list",
|
|
5176
5073
|
{
|
|
5177
5074
|
title: "List Webhook Subscriptions",
|
|
5178
|
-
annotations:
|
|
5075
|
+
annotations: READ_ONLY,
|
|
5179
5076
|
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
5077
|
inputSchema: z14.object({
|
|
5181
5078
|
page: z14.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -5191,7 +5088,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5191
5088
|
"lexq_webhook_subscriptions_get",
|
|
5192
5089
|
{
|
|
5193
5090
|
title: "Get Webhook Subscription",
|
|
5194
|
-
annotations:
|
|
5091
|
+
annotations: READ_ONLY,
|
|
5195
5092
|
description: "Get webhook subscription detail by ID.",
|
|
5196
5093
|
inputSchema: z14.object({
|
|
5197
5094
|
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
@@ -5203,7 +5100,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5203
5100
|
"lexq_webhook_subscriptions_save",
|
|
5204
5101
|
{
|
|
5205
5102
|
title: "Save Webhook Subscription",
|
|
5206
|
-
annotations: {
|
|
5103
|
+
annotations: { ...DESTRUCTIVE, openWorldHint: true },
|
|
5207
5104
|
description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: ${withNotes(FORMAT_NOTES2).join(", ")}.`,
|
|
5208
5105
|
inputSchema: z14.object({
|
|
5209
5106
|
id: z14.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
|
|
@@ -5221,7 +5118,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5221
5118
|
"lexq_webhook_subscriptions_delete",
|
|
5222
5119
|
{
|
|
5223
5120
|
title: "Delete Webhook Subscription",
|
|
5224
|
-
annotations:
|
|
5121
|
+
annotations: DESTRUCTIVE,
|
|
5225
5122
|
description: "Delete a webhook subscription by ID.",
|
|
5226
5123
|
inputSchema: z14.object({
|
|
5227
5124
|
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
@@ -5233,7 +5130,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5233
5130
|
"lexq_webhook_subscriptions_test",
|
|
5234
5131
|
{
|
|
5235
5132
|
title: "Test Webhook Subscription",
|
|
5236
|
-
annotations: {
|
|
5133
|
+
annotations: { ...DESTRUCTIVE, openWorldHint: true },
|
|
5237
5134
|
description: "Send a test event to verify webhook connectivity. Returns the HTTP status code and success/failure message.",
|
|
5238
5135
|
inputSchema: z14.object({
|
|
5239
5136
|
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
@@ -5250,7 +5147,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5250
5147
|
"lexq_domain_templates_list",
|
|
5251
5148
|
{
|
|
5252
5149
|
title: "List Domain Templates",
|
|
5253
|
-
annotations:
|
|
5150
|
+
annotations: READ_ONLY,
|
|
5254
5151
|
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
5152
|
inputSchema: z15.object({})
|
|
5256
5153
|
},
|
|
@@ -5260,7 +5157,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5260
5157
|
"lexq_domain_templates_preview",
|
|
5261
5158
|
{
|
|
5262
5159
|
title: "Preview Domain Template",
|
|
5263
|
-
annotations:
|
|
5160
|
+
annotations: READ_ONLY,
|
|
5264
5161
|
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
5162
|
inputSchema: z15.object({
|
|
5266
5163
|
template: z15.string().describe(
|
|
@@ -5274,7 +5171,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5274
5171
|
"lexq_domain_templates_apply",
|
|
5275
5172
|
{
|
|
5276
5173
|
title: "Apply Domain Template",
|
|
5277
|
-
annotations:
|
|
5174
|
+
annotations: ADDITIVE,
|
|
5278
5175
|
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
5176
|
inputSchema: z15.object({
|
|
5280
5177
|
template: z15.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
|
|
@@ -5309,6 +5206,13 @@ function registerAllTools(server, callApi) {
|
|
|
5309
5206
|
registerWebhookSubscriptionTools(server, callApi);
|
|
5310
5207
|
}
|
|
5311
5208
|
var SERVER_INFO = { name: "lexq", ...server_info_default };
|
|
5209
|
+
var SERVER_INSTRUCTIONS = [
|
|
5210
|
+
"LexQ evaluates business rules: a policy group holds versions, a version holds rules, and rules read input facts.",
|
|
5211
|
+
"- 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.",
|
|
5212
|
+
"- 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.",
|
|
5213
|
+
"- Before lexq_deploy_publish, run lexq_dry_run on the version with representative facts. A published version cannot go back to draft.",
|
|
5214
|
+
`- For anything these tools cannot do, point the user to the LexQ console at ${CONSOLE_URL}.`
|
|
5215
|
+
].join("\n");
|
|
5312
5216
|
|
|
5313
5217
|
// src/mcp/server.ts
|
|
5314
5218
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -5325,7 +5229,10 @@ function startMcpServer() {
|
|
|
5325
5229
|
const callApi = createCallApiFromConfig();
|
|
5326
5230
|
serveStdio(
|
|
5327
5231
|
() => {
|
|
5328
|
-
const server = new McpServer(
|
|
5232
|
+
const server = new McpServer(
|
|
5233
|
+
{ ...SERVER_INFO, version },
|
|
5234
|
+
{ instructions: SERVER_INSTRUCTIONS }
|
|
5235
|
+
);
|
|
5329
5236
|
registerAllTools(server, callApi);
|
|
5330
5237
|
registerLocalTools(server);
|
|
5331
5238
|
return server;
|