@lexq/cli 2.0.0 → 2.1.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/dist/index.js +919 -762
- package/dist/mcp/register.d.ts +6 -6
- package/dist/mcp/register.js +86 -29
- package/package.json +5 -4
- package/skills/lexq-simulation/SKILL.md +5 -0
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { readFileSync as readFileSync4 } from "fs";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
|
-
import { dirname as dirname2, join as
|
|
7
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/commands/auth.ts
|
|
10
10
|
import "commander";
|
|
@@ -85,6 +85,11 @@ var ApiError = class extends Error {
|
|
|
85
85
|
statusCode;
|
|
86
86
|
errorCode;
|
|
87
87
|
};
|
|
88
|
+
function describeForm(form) {
|
|
89
|
+
return [...form.entries()].map(
|
|
90
|
+
([name, value]) => typeof value === "string" ? ` ${name}: ${value}` : ` ${name}: ${value.name} (${value.size} bytes, ${value.type})`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
88
93
|
async function doFetch(method, path, options) {
|
|
89
94
|
const config = loadConfig();
|
|
90
95
|
const baseUrl = options.baseUrl ?? config.baseUrl;
|
|
@@ -99,26 +104,32 @@ async function doFetch(method, path, options) {
|
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
const headers = { "X-API-KEY": apiKey, Accept: "application/json" };
|
|
102
|
-
if (options.body) headers["Content-Type"] = "application/json";
|
|
107
|
+
if (options.body && !options.form) headers["Content-Type"] = "application/json";
|
|
103
108
|
if (options.dryRun) {
|
|
104
109
|
const masked = apiKey.length > 8 ? apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length - 4) : "****";
|
|
105
110
|
console.log(`${method} ${url.toString()}`);
|
|
106
111
|
console.log("Headers:");
|
|
107
112
|
console.log(` X-API-KEY: ${masked}`);
|
|
108
|
-
console.log(` Content-Type: application/json`);
|
|
109
|
-
if (options.
|
|
113
|
+
console.log(` Content-Type: ${options.form ? "multipart/form-data" : "application/json"}`);
|
|
114
|
+
if (options.form) {
|
|
115
|
+
console.log("Body:");
|
|
116
|
+
for (const line of describeForm(options.form)) console.log(line);
|
|
117
|
+
} else if (options.body) {
|
|
110
118
|
console.log("Body:");
|
|
111
119
|
console.log(` ${stringifyJson(options.body, 2) ?? ""}`);
|
|
112
120
|
}
|
|
113
121
|
console.log("\n(Use without --dry-run to execute)");
|
|
114
122
|
process.exit(0);
|
|
115
123
|
}
|
|
116
|
-
if (options.verbose)
|
|
124
|
+
if (options.verbose) {
|
|
125
|
+
console.error(`\u2192 ${method} ${url.toString()}`);
|
|
126
|
+
if (options.form) for (const line of describeForm(options.form)) console.error(line);
|
|
127
|
+
}
|
|
117
128
|
const startTime = Date.now();
|
|
118
129
|
const response = await fetch(url.toString(), {
|
|
119
130
|
method,
|
|
120
131
|
headers,
|
|
121
|
-
body: options.body ? stringifyJson(options.body) : void 0
|
|
132
|
+
body: options.form ?? (options.body ? stringifyJson(options.body) : void 0)
|
|
122
133
|
});
|
|
123
134
|
if (options.verbose) {
|
|
124
135
|
console.error(`\u2190 ${response.status} ${response.statusText} (${Date.now() - startTime}ms)`);
|
|
@@ -132,7 +143,7 @@ function isFileResponse(response) {
|
|
|
132
143
|
return contentType.includes("text/csv") || contentType.includes("application/octet-stream");
|
|
133
144
|
}
|
|
134
145
|
function assertOk(response, json) {
|
|
135
|
-
if (
|
|
146
|
+
if (json.result !== "SUCCESS") {
|
|
136
147
|
throw new ApiError(
|
|
137
148
|
response.status,
|
|
138
149
|
json.errorCode,
|
|
@@ -140,12 +151,25 @@ function assertOk(response, json) {
|
|
|
140
151
|
);
|
|
141
152
|
}
|
|
142
153
|
}
|
|
154
|
+
async function refusal(response) {
|
|
155
|
+
let json;
|
|
156
|
+
try {
|
|
157
|
+
json = parseJson(await response.text());
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
return new ApiError(
|
|
161
|
+
response.status,
|
|
162
|
+
json?.errorCode ?? null,
|
|
163
|
+
json?.message ?? `Request failed with status ${response.status}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
143
166
|
async function apiRequest(method, path, options = {}) {
|
|
144
167
|
const { data } = await apiRequestWithMeta(method, path, options);
|
|
145
168
|
return data;
|
|
146
169
|
}
|
|
147
170
|
async function apiRequestWithMeta(method, path, options = {}) {
|
|
148
171
|
const response = await doFetch(method, path, options);
|
|
172
|
+
if (!response.ok) throw await refusal(response);
|
|
149
173
|
if (isFileResponse(response)) {
|
|
150
174
|
return { data: await response.text(), meta: null };
|
|
151
175
|
}
|
|
@@ -349,6 +373,7 @@ var MAX_TRAFFIC_RATE = 99;
|
|
|
349
373
|
var REPLAY_WINDOW_MAX_RECORDS = 5e4;
|
|
350
374
|
var DEFAULT_SIMULATION_RECORDS = 1e3;
|
|
351
375
|
var MAX_SIMULATION_RECORDS = 1e5;
|
|
376
|
+
var MAX_DATASET_SIZE = 10485760;
|
|
352
377
|
var PROFILE_SAMPLE_PERMILLE = 10;
|
|
353
378
|
var LATENCY_WINDOW_MILLIS = 6e4;
|
|
354
379
|
var TAIL_MIN_OBS = 3;
|
|
@@ -362,11 +387,57 @@ var MAX_RULES_PER_VERSION = 100;
|
|
|
362
387
|
var MAX_CONDITIONS_PER_RULE = 25;
|
|
363
388
|
var MAX_CONDITION_DEPTH = 10;
|
|
364
389
|
var MAX_SNAPSHOT_BYTES = 131072;
|
|
390
|
+
var STATS_DEFAULT_WINDOW_DAYS = 30;
|
|
391
|
+
var STATS_MAX_WINDOW_DAYS = 31;
|
|
392
|
+
var FAILURE_LOG_LOOKBACK_DAYS = 7;
|
|
393
|
+
var PURGE_AFTER_DAYS = 30;
|
|
394
|
+
var DESCRIPTION_MAX_LENGTH = 255;
|
|
395
|
+
var COMMIT_MESSAGE_MAX_LENGTH = 500;
|
|
396
|
+
var SCHEDULE_TICK_MAX_MILLIS = 6e4;
|
|
365
397
|
|
|
366
398
|
// src/commands/groups.ts
|
|
367
399
|
import "commander";
|
|
368
400
|
import dedent3 from "dedent";
|
|
369
401
|
|
|
402
|
+
// src/types/enums.ts
|
|
403
|
+
var PolicyGroupStatus = ["ACTIVE", "DISABLED", "ARCHIVED"];
|
|
404
|
+
var LogicalOperator = ["AND", "OR"];
|
|
405
|
+
var MutateOperator = ["ASSIGN", "ADD", "SUB", "MUL", "DIV"];
|
|
406
|
+
var CalculationMethod = ["PERCENTAGE", "AMOUNT"];
|
|
407
|
+
var RoundingPolicy = [
|
|
408
|
+
"UP",
|
|
409
|
+
"DOWN",
|
|
410
|
+
"CEILING",
|
|
411
|
+
"FLOOR",
|
|
412
|
+
"HALF_UP",
|
|
413
|
+
"HALF_DOWN",
|
|
414
|
+
"HALF_EVEN"
|
|
415
|
+
];
|
|
416
|
+
var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
|
|
417
|
+
var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
|
|
418
|
+
var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
|
|
419
|
+
var DeploymentType = ["DEPLOY", "ROLLBACK", "UNDEPLOY"];
|
|
420
|
+
var ApiExecutionStatus = ["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"];
|
|
421
|
+
var ProfileCacheState = ["HIT", "MISS"];
|
|
422
|
+
var SimulationStatus = ["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"];
|
|
423
|
+
var SimulationDatasetType = ["HISTORICAL", "UPLOADED", "MANUAL"];
|
|
424
|
+
var SimulationDatasetSource = ["EXECUTION_LOGS", "S3_BUCKET", "REQUEST_BODY"];
|
|
425
|
+
var SimulationMetricType = ["COUNT", "SUM", "AVG"];
|
|
426
|
+
var ExportFormat = ["CSV", "JSON"];
|
|
427
|
+
var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
|
|
428
|
+
var FailureAction = ["IGNORE", "RESOLVE"];
|
|
429
|
+
var TaskType = ["PLATFORM_WEBHOOK", "SCHEDULED_DEPLOYMENT"];
|
|
430
|
+
var PlatformEventType = [
|
|
431
|
+
"VERSION_PUBLISHED",
|
|
432
|
+
"DEPLOYED",
|
|
433
|
+
"ROLLED_BACK",
|
|
434
|
+
"UNDEPLOYED",
|
|
435
|
+
"DEPLOY_SCHEDULED",
|
|
436
|
+
"DEPLOY_SCHEDULE_CANCELED"
|
|
437
|
+
];
|
|
438
|
+
var WebhookPayloadFormat = ["GENERIC", "SLACK"];
|
|
439
|
+
var DeploymentScheduleStatus = ["PENDING", "EXECUTED", "CANCELED", "FAILED"];
|
|
440
|
+
|
|
370
441
|
// src/lib/dates.ts
|
|
371
442
|
var ORG_DAY = "yyyy-MM-dd, a whole calendar day in the organization time zone (tenantTimezone in whoami), not UTC";
|
|
372
443
|
var START_DAY = `Start date, ${ORG_DAY}. Inclusive.`;
|
|
@@ -374,9 +445,6 @@ var END_DAY = `End date, ${ORG_DAY}. Inclusive: the whole day counts.`;
|
|
|
374
445
|
var WINDOW_START_DAY = `Window start date, ${ORG_DAY}. Inclusive.`;
|
|
375
446
|
var WINDOW_END_DAY = `Window end date, ${ORG_DAY}. Inclusive: the whole day counts.`;
|
|
376
447
|
var INSTANT = "ISO-8601 instant with Z or an offset, e.g. 2026-10-01T00:00:00Z or 2026-10-01T09:00:00+09:00. A bare date or a time without an offset is rejected.";
|
|
377
|
-
var STATS_DEFAULT_WINDOW_DAYS = 30;
|
|
378
|
-
var STATS_MAX_WINDOW_DAYS = 31;
|
|
379
|
-
var FAILURE_LOG_LOOKBACK_DAYS = 7;
|
|
380
448
|
function utcMinute(instant) {
|
|
381
449
|
return instant ? `${new Date(instant).toISOString().substring(0, 16)}Z` : "\u2013";
|
|
382
450
|
}
|
|
@@ -399,7 +467,7 @@ function registerGroupCommands(program) {
|
|
|
399
467
|
delete Archive a group
|
|
400
468
|
ab-test Manage A/B tests (start, stop, adjust)
|
|
401
469
|
|
|
402
|
-
Statuses:
|
|
470
|
+
Statuses: ${PolicyGroupStatus.join(", ")}
|
|
403
471
|
DISABLED stops production execution (403 P-009) and keeps the deployed version and any A/B test.
|
|
404
472
|
Dry run, simulation, and replay still work. Set ACTIVE to resume.
|
|
405
473
|
ARCHIVED means deleted. Only 'lexq groups delete' sets it, and it cannot be undone.
|
|
@@ -465,9 +533,9 @@ ${data.length} total`);
|
|
|
465
533
|
|
|
466
534
|
Fields:
|
|
467
535
|
name string Group name (required, unique per tenant)
|
|
468
|
-
description string Description (optional, max
|
|
536
|
+
description string Description (optional, max ${DESCRIPTION_MAX_LENGTH} chars)
|
|
469
537
|
activationGroup string Execution Group — conflict-resolution cluster key (optional)
|
|
470
|
-
activationMode string
|
|
538
|
+
activationMode string ${ConflictResolutionMode.join(" | ")} [default: NONE]
|
|
471
539
|
activationStrategy string HIGHEST_PRIORITY [default, and currently the only value]
|
|
472
540
|
executionLimit number Max rules to fire in MAX_N mode (optional)
|
|
473
541
|
status string ACTIVE | DISABLED [default: ACTIVE]
|
|
@@ -529,7 +597,7 @@ ${data.length} total`);
|
|
|
529
597
|
It cannot be restored, and calls that change it (settings, versions, rules, starting an A/B
|
|
530
598
|
test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever
|
|
531
599
|
executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted
|
|
532
|
-
after
|
|
600
|
+
after ${PURGE_AFTER_DAYS} days.
|
|
533
601
|
|
|
534
602
|
Use --force to skip the confirmation prompt.
|
|
535
603
|
`
|
|
@@ -801,7 +869,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
801
869
|
}'
|
|
802
870
|
|
|
803
871
|
Fields:
|
|
804
|
-
commitMessage string Version description (optional, max
|
|
872
|
+
commitMessage string Version description (optional, max ${COMMIT_MESSAGE_MAX_LENGTH} chars)
|
|
805
873
|
effectiveFrom instant Start of effective period (optional, with Z or an offset)
|
|
806
874
|
effectiveTo instant End of effective period (optional, auto-expires)
|
|
807
875
|
`
|
|
@@ -1066,11 +1134,11 @@ ${data.length} total`);
|
|
|
1066
1134
|
refVar is the percentage base and is optional — omit to use targetVar itself.
|
|
1067
1135
|
It is only valid in PERCENTAGE × {ASSIGN, ADD, SUB}.
|
|
1068
1136
|
|
|
1069
|
-
Value Types:
|
|
1137
|
+
Value Types: ${ValueType.join(", ")}
|
|
1070
1138
|
|
|
1071
1139
|
Mutex (optional — rule-level conflict resolution):
|
|
1072
1140
|
mutexGroup string Logical grouping key (e.g., "best-discount")
|
|
1073
|
-
mutexMode string
|
|
1141
|
+
mutexMode string ${ConflictResolutionMode.join(" | ")} [default: NONE]
|
|
1074
1142
|
mutexStrategy string HIGHEST_PRIORITY (currently the only value)
|
|
1075
1143
|
mutexLimit number Max rules to fire in MAX_N mode (required when MAX_N)
|
|
1076
1144
|
|
|
@@ -1279,27 +1347,6 @@ function escapeUnsafe(json) {
|
|
|
1279
1347
|
|
|
1280
1348
|
// src/lib/export.ts
|
|
1281
1349
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
1282
|
-
|
|
1283
|
-
// src/types/enums.ts
|
|
1284
|
-
var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
|
|
1285
|
-
var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
|
|
1286
|
-
var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
|
|
1287
|
-
var ProfileCacheState = ["HIT", "MISS"];
|
|
1288
|
-
var ExportFormat = ["CSV", "JSON"];
|
|
1289
|
-
var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
|
|
1290
|
-
var FailureAction = ["IGNORE", "RESOLVE"];
|
|
1291
|
-
var TaskType = ["PLATFORM_WEBHOOK", "SCHEDULED_DEPLOYMENT"];
|
|
1292
|
-
var PlatformEventType = [
|
|
1293
|
-
"VERSION_PUBLISHED",
|
|
1294
|
-
"DEPLOYED",
|
|
1295
|
-
"ROLLED_BACK",
|
|
1296
|
-
"UNDEPLOYED",
|
|
1297
|
-
"DEPLOY_SCHEDULED",
|
|
1298
|
-
"DEPLOY_SCHEDULE_CANCELED"
|
|
1299
|
-
];
|
|
1300
|
-
var WebhookPayloadFormat = ["GENERIC", "SLACK"];
|
|
1301
|
-
|
|
1302
|
-
// src/lib/export.ts
|
|
1303
1350
|
var EXPORT_FORMATS = ExportFormat.map((name) => name.toLowerCase()).sort();
|
|
1304
1351
|
function parseFormat(value) {
|
|
1305
1352
|
if (typeof value === "string" && EXPORT_FORMATS.includes(value)) {
|
|
@@ -1434,7 +1481,7 @@ ${data.length} unregistered`);
|
|
|
1434
1481
|
process.exit(1);
|
|
1435
1482
|
}
|
|
1436
1483
|
});
|
|
1437
|
-
facts.command("create").description("Create a new fact definition").option("--key <key>", "Fact key (letters, numbers, underscores)").option("--name <n>", "Display name").option("--type <type>",
|
|
1484
|
+
facts.command("create").description("Create a new fact definition").option("--key <key>", "Fact key (letters, numbers, underscores)").option("--name <n>", "Display name").option("--type <type>", `Value type: ${ValueType.join(", ")}`).option("--description <desc>", "Description").option("--required", "Mark as required", false).option("--pii", "Mark as PII \u2014 value is masked on every read surface", false).option("--allowed-values <csv>", "Comma-separated list of accepted values").option("--min <number>", "Lower bound, inclusive (numeric types only)").option("--max <number>", "Upper bound, inclusive (numeric types only)").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
|
|
1438
1485
|
"after",
|
|
1439
1486
|
dedent6`
|
|
1440
1487
|
|
|
@@ -1454,7 +1501,7 @@ ${data.length} unregistered`);
|
|
|
1454
1501
|
$ lexq facts create --key tier --name "Tier" --type STRING \
|
|
1455
1502
|
--allowed-values "GOLD, SILVER, BRONZE"
|
|
1456
1503
|
|
|
1457
|
-
Value Types:
|
|
1504
|
+
Value Types: ${ValueType.join(", ")}
|
|
1458
1505
|
Key Format: starts with a letter, then letters, numbers, and underscores.
|
|
1459
1506
|
Casing is yours to choose (e.g., paymentAmount, payment_amount)
|
|
1460
1507
|
|
|
@@ -1483,7 +1530,7 @@ ${data.length} unregistered`);
|
|
|
1483
1530
|
process.exit(1);
|
|
1484
1531
|
}
|
|
1485
1532
|
});
|
|
1486
|
-
facts.command("update").description("Update a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--name <n>", "Display name").option("--description <desc>", 'Description (pass "" to remove it)').option("--required", "Mark as required").option("--no-required", "Mark as not required").option("--pii", "Mark as PII (enables masking)").option("--no-pii", "Unmark as PII (disables masking \u2014 value becomes visible)").option("--type <type>",
|
|
1533
|
+
facts.command("update").description("Update a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--name <n>", "Display name").option("--description <desc>", 'Description (pass "" to remove it)').option("--required", "Mark as required").option("--no-required", "Mark as not required").option("--pii", "Mark as PII (enables masking)").option("--no-pii", "Unmark as PII (disables masking \u2014 value becomes visible)").option("--type <type>", `Value type: ${ValueType.join(", ")}`).option("--allowed-values <csv>", "Comma-separated list of accepted values").option("--min <number>", "Lower bound, inclusive (numeric types only)").option("--max <number>", "Upper bound, inclusive (numeric types only)").option("--clear-value-domain", "Remove the value domain entirely").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
|
|
1487
1534
|
"after",
|
|
1488
1535
|
dedent6`
|
|
1489
1536
|
|
|
@@ -1867,7 +1914,7 @@ function registerDeployCommands(program) {
|
|
|
1867
1914
|
process.exit(1);
|
|
1868
1915
|
}
|
|
1869
1916
|
});
|
|
1870
|
-
deploy.command("history").description("List deployment history").option("--group-id <groupId>", "Filter by policy group").option("--types <types>",
|
|
1917
|
+
deploy.command("history").description("List deployment history").option("--group-id <groupId>", "Filter by policy group").option("--types <types>", `Filter by types (comma-separated: ${DeploymentType.join(",")})`).option("--start-date <date>", START_DAY).option("--end-date <date>", END_DAY).option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
|
|
1871
1918
|
"after",
|
|
1872
1919
|
dedent7`
|
|
1873
1920
|
|
|
@@ -2027,7 +2074,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2027
2074
|
dedent7`
|
|
2028
2075
|
|
|
2029
2076
|
The version must be ACTIVE with a future effective start date; the system
|
|
2030
|
-
deploys it automatically at that time (within one scheduler tick,
|
|
2077
|
+
deploys it automatically at that time (within one scheduler tick, ≤${SCHEDULE_TICK_MAX_MILLIS / 1e3}s).
|
|
2031
2078
|
That time is the version's effectiveFrom, set on the DRAFT before publishing;
|
|
2032
2079
|
this command takes no time of its own.
|
|
2033
2080
|
One pending schedule per group. Manual deploy/rollback/undeploy, starting
|
|
@@ -2157,8 +2204,30 @@ async function warnUnregisteredFacts(globalOpts, groupId, versionId) {
|
|
|
2157
2204
|
|
|
2158
2205
|
// src/commands/analytics.ts
|
|
2159
2206
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2207
|
+
import { basename } from "path";
|
|
2160
2208
|
import "commander";
|
|
2161
2209
|
import dedent8 from "dedent";
|
|
2210
|
+
|
|
2211
|
+
// src/lib/dataset-upload.ts
|
|
2212
|
+
import { extname } from "path";
|
|
2213
|
+
var DATASET_CONTENT_TYPES = {
|
|
2214
|
+
".csv": "text/csv",
|
|
2215
|
+
".json": "application/json"
|
|
2216
|
+
};
|
|
2217
|
+
function datasetContentType(filename) {
|
|
2218
|
+
return DATASET_CONTENT_TYPES[extname(filename).toLowerCase()];
|
|
2219
|
+
}
|
|
2220
|
+
function uploadDataset(bytes, filename, options) {
|
|
2221
|
+
const form = new FormData();
|
|
2222
|
+
const type = datasetContentType(filename) ?? "application/octet-stream";
|
|
2223
|
+
form.append("file", new Blob([bytes], { type }), filename);
|
|
2224
|
+
return apiRequest("POST", "analytics/datasets/upload", {
|
|
2225
|
+
...options,
|
|
2226
|
+
form
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// src/commands/analytics.ts
|
|
2162
2231
|
function registerAnalyticsCommands(program) {
|
|
2163
2232
|
const analytics = program.command("analytics").description("Dry run, simulation, and requirements").addHelpText(
|
|
2164
2233
|
"after",
|
|
@@ -2330,8 +2399,8 @@ function registerAnalyticsCommands(program) {
|
|
|
2330
2399
|
|
|
2331
2400
|
$ lexq analytics simulation start --file simulation.json
|
|
2332
2401
|
|
|
2333
|
-
Dataset types:
|
|
2334
|
-
Dataset sources:
|
|
2402
|
+
Dataset types: ${SimulationDatasetType.join(", ")}
|
|
2403
|
+
Dataset sources: ${SimulationDatasetSource.join(", ")}
|
|
2335
2404
|
HISTORICAL from / to are yyyy-MM-dd, whole days in the organization time zone, both inclusive.
|
|
2336
2405
|
`
|
|
2337
2406
|
).action(async (opts) => {
|
|
@@ -2452,7 +2521,7 @@ function registerAnalyticsCommands(program) {
|
|
|
2452
2521
|
process.exit(1);
|
|
2453
2522
|
}
|
|
2454
2523
|
});
|
|
2455
|
-
sim.command("list").description("List simulation history").option("--status <status>",
|
|
2524
|
+
sim.command("list").description("List simulation history").option("--status <status>", `Filter by status (${SimulationStatus.join(", ")})`).option("--from <date>", START_DAY).option("--to <date>", END_DAY).option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
|
|
2456
2525
|
try {
|
|
2457
2526
|
const globalOpts = program.opts();
|
|
2458
2527
|
const format = globalOpts.format ?? "json";
|
|
@@ -2577,41 +2646,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2577
2646
|
).action(async (opts) => {
|
|
2578
2647
|
try {
|
|
2579
2648
|
const globalOpts = program.opts();
|
|
2580
|
-
const config = loadConfig();
|
|
2581
|
-
const baseUrl = globalOpts.baseUrl ?? config.baseUrl;
|
|
2582
|
-
const apiKey = globalOpts.apiKey ?? config.apiKey;
|
|
2583
|
-
if (!apiKey) {
|
|
2584
|
-
throw new Error('Not authenticated. Run "lexq auth login" first.');
|
|
2585
|
-
}
|
|
2586
2649
|
const filePath = opts.file;
|
|
2587
|
-
const
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
else if (ext === "json") contentType = "application/json";
|
|
2593
|
-
const blob = new Blob([fileBuffer], { type: contentType });
|
|
2594
|
-
const formData = new FormData();
|
|
2595
|
-
formData.append("file", blob, fileName);
|
|
2596
|
-
const url = new URL(
|
|
2597
|
-
"analytics/datasets/upload",
|
|
2598
|
-
baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"
|
|
2599
|
-
);
|
|
2600
|
-
if (globalOpts.verbose) {
|
|
2601
|
-
console.error(`\u2192 POST ${url.toString()}`);
|
|
2602
|
-
console.error(` File: ${filePath} (${fileBuffer.length} bytes)`);
|
|
2603
|
-
}
|
|
2604
|
-
const response = await fetch(url.toString(), {
|
|
2605
|
-
method: "POST",
|
|
2606
|
-
headers: { "X-API-KEY": apiKey },
|
|
2607
|
-
body: formData
|
|
2650
|
+
const result = await uploadDataset(readFileSync2(filePath), basename(filePath), {
|
|
2651
|
+
apiKey: globalOpts.apiKey,
|
|
2652
|
+
baseUrl: globalOpts.baseUrl,
|
|
2653
|
+
dryRun: globalOpts.dryRun,
|
|
2654
|
+
verbose: globalOpts.verbose
|
|
2608
2655
|
});
|
|
2609
|
-
if (!response.ok) {
|
|
2610
|
-
const errorText = await response.text();
|
|
2611
|
-
throw new Error(`Upload failed (${response.status}): ${errorText}`);
|
|
2612
|
-
}
|
|
2613
|
-
const envelope = await response.json();
|
|
2614
|
-
const result = envelope.data;
|
|
2615
2656
|
console.log(`\u2713 Dataset uploaded: ${result.path}`);
|
|
2616
2657
|
console.log(` File: ${result.filename} (${result.size} bytes)`);
|
|
2617
2658
|
printJson(result);
|
|
@@ -2684,6 +2725,15 @@ function resolveBody(opts) {
|
|
|
2684
2725
|
// src/commands/profile.ts
|
|
2685
2726
|
import "commander";
|
|
2686
2727
|
import dedent9 from "dedent";
|
|
2728
|
+
|
|
2729
|
+
// src/lib/percentiles.ts
|
|
2730
|
+
var PERCENTILE_MIN_N = {
|
|
2731
|
+
p50: TAIL_MIN_OBS * 2,
|
|
2732
|
+
p95: TAIL_MIN_OBS * 20,
|
|
2733
|
+
p99: TAIL_MIN_OBS * 100
|
|
2734
|
+
};
|
|
2735
|
+
|
|
2736
|
+
// src/commands/profile.ts
|
|
2687
2737
|
var ms = (nanos) => nanos == null ? "\u2013" : (nanos / 1e6).toFixed(2);
|
|
2688
2738
|
var msWithUnit = (nanos) => nanos == null ? "\u2013" : `${(nanos / 1e6).toFixed(2)}ms`;
|
|
2689
2739
|
function registerProfileCommands(program) {
|
|
@@ -2693,15 +2743,18 @@ function registerProfileCommands(program) {
|
|
|
2693
2743
|
).option("--version-id <versionId>", "Version to inspect (default: live version)").option(
|
|
2694
2744
|
"--from <instant>",
|
|
2695
2745
|
`Window start, ISO-8601 instant (default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago)`
|
|
2696
|
-
).option("--to <instant>", "Window end, ISO-8601 instant (default: now)").option(
|
|
2746
|
+
).option("--to <instant>", "Window end, ISO-8601 instant (default: now)").option(
|
|
2747
|
+
"--cache <state>",
|
|
2748
|
+
`Cache dimension for the rule table: ${ProfileCacheState.join(" | ")} (default: HIT)`
|
|
2749
|
+
).addHelpText(
|
|
2697
2750
|
"after",
|
|
2698
2751
|
dedent9`
|
|
2699
2752
|
|
|
2700
2753
|
Slow-rule judgment is relative only: flagged = p50 ≥ ${SLOW_MULTIPLIER}× the median of
|
|
2701
2754
|
per-rule p50s within the group. Absolute ms thresholds are intentionally
|
|
2702
|
-
not supported. Each percentile is withheld (–) unless n×(1−q) ≥
|
|
2703
|
-
p50 from n ≥
|
|
2704
|
-
on every call, rule detail from a deterministic
|
|
2755
|
+
not supported. Each percentile is withheld (–) unless n×(1−q) ≥ ${TAIL_MIN_OBS} —
|
|
2756
|
+
p50 from n ≥ ${PERCENTILE_MIN_N.p50}, p95 from n ≥ ${PERCENTILE_MIN_N.p95}, p99 from n ≥ ${PERCENTILE_MIN_N.p99}. TOTAL is recorded
|
|
2757
|
+
on every call, rule detail from a deterministic ${PROFILE_SAMPLE_PERMILLE / 10}% sample.
|
|
2705
2758
|
|
|
2706
2759
|
Examples:
|
|
2707
2760
|
$ lexq profile <groupId>
|
|
@@ -2799,10 +2852,10 @@ function registerHistoryCommands(program) {
|
|
|
2799
2852
|
get Get full execution detail (request facts, traces, decisions)
|
|
2800
2853
|
stats Aggregate statistics (success rate, latency, counts)
|
|
2801
2854
|
|
|
2802
|
-
Statuses:
|
|
2855
|
+
Statuses: ${ApiExecutionStatus.join(", ")}
|
|
2803
2856
|
`
|
|
2804
2857
|
);
|
|
2805
|
-
history.command("list").description("List execution history").option("--trace-id <traceId>", "Filter by trace ID").option("--group-id <groupId>", "Filter by policy group").option("--version-id <versionId>", "Filter by version").option("--status <status>",
|
|
2858
|
+
history.command("list").description("List execution history").option("--trace-id <traceId>", "Filter by trace ID").option("--group-id <groupId>", "Filter by policy group").option("--version-id <versionId>", "Filter by version").option("--status <status>", `Filter by status (${ApiExecutionStatus.join(", ")})`).option("--start-date <date>", START_DAY).option("--end-date <date>", END_DAY).option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
|
|
2806
2859
|
"after",
|
|
2807
2860
|
dedent10`
|
|
2808
2861
|
|
|
@@ -3195,6 +3248,18 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3195
3248
|
// src/commands/logs.ts
|
|
3196
3249
|
import "commander";
|
|
3197
3250
|
import dedent13 from "dedent";
|
|
3251
|
+
|
|
3252
|
+
// src/lib/enum-notes.ts
|
|
3253
|
+
function withNotes(notes) {
|
|
3254
|
+
return Object.entries(notes).map(([value, note]) => note ? `${value} (${note})` : value);
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
// src/commands/logs.ts
|
|
3258
|
+
var STATUS_NOTES = {
|
|
3259
|
+
PENDING: "needs attention",
|
|
3260
|
+
RESOLVED: "",
|
|
3261
|
+
IGNORED: ""
|
|
3262
|
+
};
|
|
3198
3263
|
function registerLogCommands(program) {
|
|
3199
3264
|
const logs = program.command("logs").description("Failure logs").addHelpText(
|
|
3200
3265
|
"after",
|
|
@@ -3205,13 +3270,13 @@ function registerLogCommands(program) {
|
|
|
3205
3270
|
Commands:
|
|
3206
3271
|
list List failure logs with filters
|
|
3207
3272
|
get Get failure log detail (includes the original payload)
|
|
3208
|
-
action Process a single log (
|
|
3273
|
+
action Process a single log (${FailureAction.join(", ")})
|
|
3209
3274
|
bulk-action Process multiple logs at once
|
|
3210
3275
|
|
|
3211
|
-
Statuses:
|
|
3276
|
+
Statuses: ${withNotes(STATUS_NOTES).join(", ")}
|
|
3212
3277
|
`
|
|
3213
3278
|
);
|
|
3214
|
-
logs.command("list").description("List failure logs").option("--task-type <taskType>", `Filter by task type (${TaskType.join(", ")})`).option("--status <status>",
|
|
3279
|
+
logs.command("list").description("List failure logs").option("--task-type <taskType>", `Filter by task type (${TaskType.join(", ")})`).option("--status <status>", `Filter by status (${FailureStatus.join(", ")})`).option("--keyword <keyword>", "Search keyword").option("--start-date <date>", START_DAY).option("--end-date <date>", END_DAY).option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
|
|
3215
3280
|
"after",
|
|
3216
3281
|
dedent13`
|
|
3217
3282
|
|
|
@@ -3267,7 +3332,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3267
3332
|
dedent13`
|
|
3268
3333
|
|
|
3269
3334
|
Includes the full payload that was used for the failed operation.
|
|
3270
|
-
Use this to inspect what went wrong before deciding to
|
|
3335
|
+
Use this to inspect what went wrong before deciding to ${FailureAction.join(" or ")}.
|
|
3271
3336
|
`
|
|
3272
3337
|
).action(async (opts) => {
|
|
3273
3338
|
try {
|
|
@@ -3284,7 +3349,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3284
3349
|
process.exit(1);
|
|
3285
3350
|
}
|
|
3286
3351
|
});
|
|
3287
|
-
logs.command("action").description(
|
|
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(
|
|
3288
3353
|
"after",
|
|
3289
3354
|
dedent13`
|
|
3290
3355
|
|
|
@@ -3315,7 +3380,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3315
3380
|
process.exit(1);
|
|
3316
3381
|
}
|
|
3317
3382
|
});
|
|
3318
|
-
logs.command("bulk-action").description("Bulk process failure logs").requiredOption("--ids <logIds>", "Comma-separated log IDs").requiredOption("--action <action>",
|
|
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(
|
|
3319
3384
|
"after",
|
|
3320
3385
|
dedent13`
|
|
3321
3386
|
|
|
@@ -3346,6 +3411,10 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3346
3411
|
// src/commands/webhook-subscriptions.ts
|
|
3347
3412
|
import "commander";
|
|
3348
3413
|
import dedent14 from "dedent";
|
|
3414
|
+
var FORMAT_NOTES = {
|
|
3415
|
+
GENERIC: "full JSON",
|
|
3416
|
+
SLACK: '{"text":"..."}'
|
|
3417
|
+
};
|
|
3349
3418
|
function registerWebhookSubscriptionCommands(program) {
|
|
3350
3419
|
const webhooks = program.command("webhook-subscriptions").description("Manage platform event webhook subscriptions").addHelpText(
|
|
3351
3420
|
"after",
|
|
@@ -3448,7 +3517,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3448
3517
|
name string Subscription name (required, unique per tenant)
|
|
3449
3518
|
webhookUrl string Webhook endpoint URL (required)
|
|
3450
3519
|
subscribedEvents string[] ${PlatformEventType.join(", ")}
|
|
3451
|
-
payloadFormat string
|
|
3520
|
+
payloadFormat string ${withNotes(FORMAT_NOTES).join(" or ")} [default: GENERIC]
|
|
3452
3521
|
secret string HMAC-SHA256 signing secret (optional)
|
|
3453
3522
|
isActive boolean Enable/disable (optional) [default: true]
|
|
3454
3523
|
id string Provide to update, omit to create
|
|
@@ -3548,56 +3617,36 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3548
3617
|
|
|
3549
3618
|
// src/commands/serve.ts
|
|
3550
3619
|
import "commander";
|
|
3551
|
-
import
|
|
3620
|
+
import dedent19 from "dedent";
|
|
3552
3621
|
|
|
3553
3622
|
// src/mcp/server.ts
|
|
3554
3623
|
import { readFileSync as readFileSync3 } from "fs";
|
|
3555
|
-
import { dirname, join as
|
|
3624
|
+
import { dirname, join as join3 } from "path";
|
|
3556
3625
|
import { fileURLToPath } from "url";
|
|
3557
3626
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
3558
3627
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
3559
3628
|
|
|
3560
3629
|
// src/mcp/tools/_shared.ts
|
|
3630
|
+
function localApiOptions() {
|
|
3631
|
+
const stored = loadConfig();
|
|
3632
|
+
return {
|
|
3633
|
+
baseUrl: process.env.PARTNER_BASE_URL ?? stored.baseUrl,
|
|
3634
|
+
apiKey: process.env.LEXQ_API_KEY ?? stored.apiKey
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3561
3637
|
function createCallApiFromConfig() {
|
|
3562
3638
|
return async (method, path, opts) => {
|
|
3563
3639
|
try {
|
|
3564
|
-
|
|
3565
|
-
const config = {
|
|
3566
|
-
...stored,
|
|
3567
|
-
baseUrl: process.env.PARTNER_BASE_URL ?? stored.baseUrl,
|
|
3568
|
-
apiKey: process.env.LEXQ_API_KEY ?? stored.apiKey
|
|
3569
|
-
};
|
|
3640
|
+
let form;
|
|
3570
3641
|
if (opts?.upload) {
|
|
3571
|
-
|
|
3572
|
-
path,
|
|
3573
|
-
config.baseUrl.endsWith("/") ? config.baseUrl : config.baseUrl + "/"
|
|
3574
|
-
);
|
|
3575
|
-
if (opts.params) {
|
|
3576
|
-
for (const [key, value] of Object.entries(opts.params)) {
|
|
3577
|
-
if (value !== void 0) url.searchParams.set(key, value);
|
|
3578
|
-
}
|
|
3579
|
-
}
|
|
3580
|
-
if (!config.apiKey) {
|
|
3581
|
-
throw new Error('Not authenticated. Run "lexq auth login" first.');
|
|
3582
|
-
}
|
|
3642
|
+
form = new FormData();
|
|
3583
3643
|
const blob = new Blob([opts.upload.content], { type: "text/plain" });
|
|
3584
|
-
|
|
3585
|
-
formData.append(opts.upload.fieldName, blob, opts.upload.filename);
|
|
3586
|
-
const response = await fetch(url.toString(), {
|
|
3587
|
-
method,
|
|
3588
|
-
headers: { "X-API-KEY": config.apiKey },
|
|
3589
|
-
body: formData
|
|
3590
|
-
});
|
|
3591
|
-
const data2 = parseJson(await response.text());
|
|
3592
|
-
return { content: [{ type: "text", text: stringifyJson(data2, 2) ?? "" }] };
|
|
3644
|
+
form.append(opts.upload.fieldName, blob, opts.upload.filename);
|
|
3593
3645
|
}
|
|
3594
|
-
const clientOpts = {
|
|
3595
|
-
apiKey: config.apiKey,
|
|
3596
|
-
baseUrl: config.baseUrl
|
|
3597
|
-
};
|
|
3598
3646
|
const { data, meta } = await apiRequestWithMeta(method, path, {
|
|
3599
|
-
...
|
|
3647
|
+
...localApiOptions(),
|
|
3600
3648
|
body: opts?.body,
|
|
3649
|
+
form,
|
|
3601
3650
|
params: opts?.params
|
|
3602
3651
|
});
|
|
3603
3652
|
const content = [
|
|
@@ -3640,124 +3689,457 @@ function formatUnregisteredFactWarning(meta) {
|
|
|
3640
3689
|
].join("\n");
|
|
3641
3690
|
}
|
|
3642
3691
|
|
|
3643
|
-
// src/mcp/
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
{
|
|
3650
|
-
src: "https://lexq.io/logo.png",
|
|
3651
|
-
mimeType: "image/png",
|
|
3652
|
-
sizes: ["1024x1024"]
|
|
3653
|
-
}
|
|
3654
|
-
]
|
|
3655
|
-
};
|
|
3692
|
+
// src/mcp/local-tools.ts
|
|
3693
|
+
import { readFile, realpath, stat } from "fs/promises";
|
|
3694
|
+
import { homedir as homedir2 } from "os";
|
|
3695
|
+
import { basename as basename2, isAbsolute, join as join2 } from "path";
|
|
3696
|
+
import { z as z2 } from "zod";
|
|
3697
|
+
import dedent16 from "dedent";
|
|
3656
3698
|
|
|
3657
|
-
// src/mcp/tools/
|
|
3699
|
+
// src/mcp/tools/analytics.ts
|
|
3658
3700
|
import { z } from "zod";
|
|
3659
|
-
|
|
3701
|
+
import dedent15 from "dedent";
|
|
3702
|
+
function datasetBlock(path) {
|
|
3703
|
+
const dataset = { type: "UPLOADED", source: "S3_BUCKET", path };
|
|
3704
|
+
return {
|
|
3705
|
+
type: "text",
|
|
3706
|
+
text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
|
|
3707
|
+
};
|
|
3708
|
+
}
|
|
3709
|
+
function registerAnalyticsTools(server, callApi) {
|
|
3660
3710
|
server.registerTool(
|
|
3661
|
-
"
|
|
3711
|
+
"lexq_dry_run",
|
|
3662
3712
|
{
|
|
3663
|
-
title: "
|
|
3713
|
+
title: "Dry Run",
|
|
3664
3714
|
annotations: { readOnlyHint: true },
|
|
3665
|
-
description:
|
|
3666
|
-
|
|
3715
|
+
description: dedent15`
|
|
3716
|
+
Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
|
|
3717
|
+
|
|
3718
|
+
Returns:
|
|
3719
|
+
inputFacts — normalized input facts
|
|
3720
|
+
mutatedFacts — input facts changed by rule actions (e.g. MUTATE_FACT mutates paymentAmount)
|
|
3721
|
+
generatedVariables — system-generated values; every fact in mutatedFacts gets a paired {factName}__delta key (signed difference)
|
|
3722
|
+
executionTraces — per-rule match status
|
|
3723
|
+
decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
|
|
3724
|
+
|
|
3725
|
+
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
|
|
3726
|
+
Always dry-run before publishing to validate rule behavior.`,
|
|
3727
|
+
inputSchema: z.object({
|
|
3728
|
+
versionId: z.string().uuid().describe("Policy version ID to test against"),
|
|
3729
|
+
facts: z.string().describe('JSON string of facts object, e.g. {"paymentAmount":100000}'),
|
|
3730
|
+
includeDebugInfo: z.boolean().default(true).describe("Include execution and decision traces")
|
|
3731
|
+
})
|
|
3667
3732
|
},
|
|
3668
|
-
async (
|
|
3733
|
+
async ({ versionId, facts, includeDebugInfo }) => {
|
|
3734
|
+
const parsedFacts = parseJson(facts);
|
|
3735
|
+
return callApi("POST", `analytics/dry-run/versions/${versionId}`, {
|
|
3736
|
+
body: { facts: parsedFacts, includeDebugInfo }
|
|
3737
|
+
});
|
|
3738
|
+
}
|
|
3669
3739
|
);
|
|
3670
|
-
}
|
|
3671
|
-
|
|
3672
|
-
// src/mcp/tools/groups.ts
|
|
3673
|
-
import { z as z2 } from "zod";
|
|
3674
|
-
function registerGroupTools(server, callApi) {
|
|
3675
3740
|
server.registerTool(
|
|
3676
|
-
"
|
|
3741
|
+
"lexq_dry_run_compare",
|
|
3677
3742
|
{
|
|
3678
|
-
title: "
|
|
3743
|
+
title: "Dry Run Compare",
|
|
3679
3744
|
annotations: { readOnlyHint: true },
|
|
3680
|
-
description:
|
|
3681
|
-
|
|
3745
|
+
description: dedent15`
|
|
3746
|
+
Compare dry run results between two versions using the same input facts. Useful for validating changes.
|
|
3747
|
+
|
|
3748
|
+
Returns:
|
|
3749
|
+
resultA / resultB — full DryRunResponse for each version
|
|
3750
|
+
diff.mutatedDiff — changes in mutatedFacts between A and B (key → {before, after})
|
|
3751
|
+
diff.generatedDiff — changes in generatedVariables between A and B`,
|
|
3752
|
+
inputSchema: z.object({
|
|
3753
|
+
versionIdA: z.string().uuid().describe("Baseline version ID"),
|
|
3754
|
+
versionIdB: z.string().uuid().describe("Candidate version ID"),
|
|
3755
|
+
facts: z.string().describe("JSON string of facts object")
|
|
3756
|
+
})
|
|
3682
3757
|
},
|
|
3683
|
-
async (
|
|
3758
|
+
async ({ versionIdA, versionIdB, facts }) => {
|
|
3759
|
+
const parsedFacts = parseJson(facts);
|
|
3760
|
+
return callApi("POST", "analytics/dry-run/compare", {
|
|
3761
|
+
body: { versionIdA, versionIdB, facts: parsedFacts }
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3684
3764
|
);
|
|
3685
3765
|
server.registerTool(
|
|
3686
|
-
"
|
|
3766
|
+
"lexq_requirements",
|
|
3687
3767
|
{
|
|
3688
|
-
title: "
|
|
3768
|
+
title: "Analyze Requirements",
|
|
3689
3769
|
annotations: { readOnlyHint: true },
|
|
3690
|
-
description: "
|
|
3691
|
-
inputSchema:
|
|
3692
|
-
groupId:
|
|
3770
|
+
description: "Analyze which input facts a version requires. Returns required keys, types, and an example request body.",
|
|
3771
|
+
inputSchema: z.object({
|
|
3772
|
+
groupId: z.string().uuid().describe("Policy group ID"),
|
|
3773
|
+
versionId: z.string().uuid().describe("Version ID")
|
|
3693
3774
|
})
|
|
3694
3775
|
},
|
|
3695
|
-
async ({ groupId }) => callApi("GET", `
|
|
3776
|
+
async ({ groupId, versionId }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/requirements`)
|
|
3696
3777
|
);
|
|
3697
3778
|
server.registerTool(
|
|
3698
|
-
"
|
|
3779
|
+
"lexq_simulation_start",
|
|
3699
3780
|
{
|
|
3700
|
-
title: "
|
|
3781
|
+
title: "Start Simulation",
|
|
3701
3782
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3702
|
-
description: `
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
)
|
|
3783
|
+
description: dedent15`
|
|
3784
|
+
Start an Impact Simulation against historical, uploaded, or inline data.
|
|
3785
|
+
|
|
3786
|
+
dataset.type and dataset.source are BOTH required, and must be paired:
|
|
3787
|
+
HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
|
|
3788
|
+
in the organization time zone, both inclusive)
|
|
3789
|
+
UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
|
|
3790
|
+
MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
|
|
3791
|
+
|
|
3792
|
+
options.maxRecords: number (max ${MAX_SIMULATION_RECORDS}). When omitted, HISTORICAL and UPLOADED run at
|
|
3793
|
+
most ${DEFAULT_SIMULATION_RECORDS} records and MANUAL runs every record sent.
|
|
3794
|
+
options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
|
|
3795
|
+
options.includeRuleStats: boolean
|
|
3796
|
+
options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
|
|
3797
|
+
{ "targetVariable": "<fact>", "aggregationType": ${SimulationMetricType.map((t) => `"${t}"`).join(" | ")} }
|
|
3798
|
+
|
|
3799
|
+
Example (uploaded dataset):
|
|
3800
|
+
{
|
|
3801
|
+
"policyVersionId": "<uuid>",
|
|
3802
|
+
"dataset": { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<path from lexq_dataset_upload>" },
|
|
3803
|
+
"options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true, "maxRecords": 10000 }
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
Example (historical):
|
|
3807
|
+
{
|
|
3808
|
+
"policyVersionId": "<uuid>",
|
|
3809
|
+
"dataset": { "type": "HISTORICAL", "source": "EXECUTION_LOGS", "from": "2026-01-01", "to": "2026-01-31" },
|
|
3810
|
+
"options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true }
|
|
3811
|
+
}
|
|
3812
|
+
`,
|
|
3813
|
+
inputSchema: z.object({
|
|
3814
|
+
body: z.string().describe("JSON string of SimulationRequest")
|
|
3714
3815
|
})
|
|
3715
3816
|
},
|
|
3716
|
-
async (
|
|
3717
|
-
const
|
|
3718
|
-
|
|
3719
|
-
};
|
|
3720
|
-
if (args.description !== void 0) body.description = args.description;
|
|
3721
|
-
if (args.activationMode !== void 0) body.activationMode = args.activationMode;
|
|
3722
|
-
if (args.activationStrategy !== void 0) body.activationStrategy = args.activationStrategy;
|
|
3723
|
-
if (args.executionLimit !== void 0) body.executionLimit = args.executionLimit;
|
|
3724
|
-
if (args.activationGroup !== void 0) body.activationGroup = args.activationGroup;
|
|
3725
|
-
return callApi("POST", "policy-groups", { body });
|
|
3817
|
+
async ({ body }) => {
|
|
3818
|
+
const parsed = parseJson(body);
|
|
3819
|
+
return callApi("POST", "analytics/simulations", { body: parsed });
|
|
3726
3820
|
}
|
|
3727
3821
|
);
|
|
3728
3822
|
server.registerTool(
|
|
3729
|
-
"
|
|
3823
|
+
"lexq_simulation_status",
|
|
3730
3824
|
{
|
|
3731
|
-
title: "
|
|
3732
|
-
annotations: { readOnlyHint:
|
|
3733
|
-
description: "
|
|
3734
|
-
inputSchema:
|
|
3735
|
-
|
|
3736
|
-
name: z2.string().optional().describe("New name"),
|
|
3737
|
-
description: z2.string().optional().describe("New description"),
|
|
3738
|
-
status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
|
|
3739
|
-
"DISABLED stops production execution of this group: execution calls return 403 (P-009), and a composite call that includes the group is rejected in full. The deployed version and any A/B test are kept, and dry run, Impact Simulation, and Decision Replay keep working, so a fix can be verified before setting ACTIVE again. To go back to the previous version, use lexq_deploy_rollback. To take the version out of production, use lexq_deploy_undeploy."
|
|
3740
|
-
),
|
|
3741
|
-
activationGroup: z2.string().optional().describe(
|
|
3742
|
-
"Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
|
|
3743
|
-
),
|
|
3744
|
-
activationMode: z2.enum(ConflictResolutionMode).optional().describe("Conflict resolution mode"),
|
|
3745
|
-
activationStrategy: z2.enum(ConflictResolutionStrategy).optional().describe("Ranking used to pick the winning groups"),
|
|
3746
|
-
executionLimit: z2.number().int().min(1).optional().describe(
|
|
3747
|
-
"How many policy groups sharing this activationGroup may run. Counts groups, not rules."
|
|
3748
|
-
)
|
|
3825
|
+
title: "Simulation Status",
|
|
3826
|
+
annotations: { readOnlyHint: true },
|
|
3827
|
+
description: "Get simulation status and results. Poll until status is COMPLETED or FAILED.",
|
|
3828
|
+
inputSchema: z.object({
|
|
3829
|
+
simulationId: z.string().uuid().describe("Simulation ID")
|
|
3749
3830
|
})
|
|
3750
3831
|
},
|
|
3751
|
-
async ({
|
|
3832
|
+
async ({ simulationId }) => callApi("GET", `analytics/simulations/${simulationId}`)
|
|
3752
3833
|
);
|
|
3753
3834
|
server.registerTool(
|
|
3754
|
-
"
|
|
3835
|
+
"lexq_simulation_list",
|
|
3755
3836
|
{
|
|
3756
|
-
title: "
|
|
3837
|
+
title: "List Simulations",
|
|
3838
|
+
annotations: { readOnlyHint: true },
|
|
3839
|
+
description: "List simulation history with optional filters.",
|
|
3840
|
+
inputSchema: z.object({
|
|
3841
|
+
page: z.number().int().min(0).default(0).describe("Page number"),
|
|
3842
|
+
size: z.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
3843
|
+
status: z.enum(SimulationStatus).optional().describe("Filter by status"),
|
|
3844
|
+
from: z.string().optional().describe(START_DAY),
|
|
3845
|
+
to: z.string().optional().describe(END_DAY)
|
|
3846
|
+
})
|
|
3847
|
+
},
|
|
3848
|
+
async ({ page, size, status, from, to }) => {
|
|
3849
|
+
const params = paginationParams(page, size);
|
|
3850
|
+
if (status) params.status = status;
|
|
3851
|
+
if (from) params.from = from;
|
|
3852
|
+
if (to) params.to = to;
|
|
3853
|
+
return callApi("GET", "analytics/simulations", { params });
|
|
3854
|
+
}
|
|
3855
|
+
);
|
|
3856
|
+
server.registerTool(
|
|
3857
|
+
"lexq_simulation_cancel",
|
|
3858
|
+
{
|
|
3859
|
+
title: "Cancel Simulation",
|
|
3757
3860
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3758
|
-
description: "
|
|
3861
|
+
description: "Cancel a running or pending simulation.",
|
|
3862
|
+
inputSchema: z.object({
|
|
3863
|
+
simulationId: z.string().uuid().describe("Simulation ID")
|
|
3864
|
+
})
|
|
3865
|
+
},
|
|
3866
|
+
async ({ simulationId }) => callApi("DELETE", `analytics/simulations/${simulationId}`)
|
|
3867
|
+
);
|
|
3868
|
+
server.registerTool(
|
|
3869
|
+
"lexq_simulation_export",
|
|
3870
|
+
{
|
|
3871
|
+
title: "Export Simulation",
|
|
3872
|
+
annotations: { readOnlyHint: true },
|
|
3873
|
+
description: "Export simulation results as JSON or CSV. Returns the raw data.",
|
|
3874
|
+
inputSchema: z.object({
|
|
3875
|
+
simulationId: z.string().uuid().describe("Simulation ID"),
|
|
3876
|
+
format: z.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
3877
|
+
})
|
|
3878
|
+
},
|
|
3879
|
+
async ({ simulationId, format }) => callApi("GET", `analytics/simulations/${simulationId}/export`, {
|
|
3880
|
+
params: { format }
|
|
3881
|
+
})
|
|
3882
|
+
);
|
|
3883
|
+
server.registerTool(
|
|
3884
|
+
"lexq_dataset_upload",
|
|
3885
|
+
{
|
|
3886
|
+
title: "Upload Dataset",
|
|
3887
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3888
|
+
description: dedent15`
|
|
3889
|
+
Upload inline CSV or JSON content as a simulation dataset.
|
|
3890
|
+
The content is uploaded to S3 and a path is returned in the "path" field.
|
|
3891
|
+
|
|
3892
|
+
To use the returned path in lexq_simulation_start, set:
|
|
3893
|
+
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
3894
|
+
|
|
3895
|
+
The content travels inside the tool call, so this suits small datasets, and the hosted server at
|
|
3896
|
+
mcp.lexq.io limits the size of a request. For a larger file, use lexq_dataset_upload_file on the
|
|
3897
|
+
local server (lexq serve --mcp), or upload it with the CLI or the console and pass the returned path.
|
|
3898
|
+
|
|
3899
|
+
CSV example:
|
|
3900
|
+
userId,paymentAmount
|
|
3901
|
+
user_001,150000
|
|
3902
|
+
user_002,50000
|
|
3903
|
+
|
|
3904
|
+
JSON example: [{"userId":"user_001","paymentAmount":150000}, {"userId":"user_002","paymentAmount":50000}]
|
|
3905
|
+
`,
|
|
3906
|
+
inputSchema: z.object({
|
|
3907
|
+
content: z.string().describe("CSV or JSON content as string"),
|
|
3908
|
+
filename: z.string().default("dataset.csv").describe("Filename with extension (.csv or .json)")
|
|
3909
|
+
})
|
|
3910
|
+
},
|
|
3911
|
+
async ({ content, filename }) => {
|
|
3912
|
+
const result = await callApi("POST", "analytics/datasets/upload", {
|
|
3913
|
+
upload: { content, filename, fieldName: "file" }
|
|
3914
|
+
});
|
|
3915
|
+
if (!result.isError) {
|
|
3916
|
+
try {
|
|
3917
|
+
const uploaded = parseJson(result.content[0]?.text ?? "{}");
|
|
3918
|
+
const path = uploaded.data?.path ?? uploaded.path;
|
|
3919
|
+
if (typeof path === "string" && path) result.content.push(datasetBlock(path));
|
|
3920
|
+
} catch {
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
return result;
|
|
3924
|
+
}
|
|
3925
|
+
);
|
|
3926
|
+
server.registerTool(
|
|
3927
|
+
"lexq_dataset_template",
|
|
3928
|
+
{
|
|
3929
|
+
title: "Download Dataset Template",
|
|
3930
|
+
annotations: { readOnlyHint: true },
|
|
3931
|
+
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
|
+
inputSchema: z.object({
|
|
3933
|
+
groupId: z.string().uuid().describe("Policy group ID"),
|
|
3934
|
+
versionId: z.string().uuid().describe("Version ID"),
|
|
3935
|
+
format: z.enum(EXPORT_FORMATS).default("csv").describe("Template format")
|
|
3936
|
+
})
|
|
3937
|
+
},
|
|
3938
|
+
async ({ groupId, versionId, format }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/dataset-template`, {
|
|
3939
|
+
params: { format }
|
|
3940
|
+
})
|
|
3941
|
+
);
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
// src/mcp/local-tools.ts
|
|
3945
|
+
var MAX_DATASET_MB = MAX_DATASET_SIZE / 1024 / 1024;
|
|
3946
|
+
var NOT_ABSOLUTE = "file must be an absolute path on this computer. A leading ~/ stands for the home directory.";
|
|
3947
|
+
var NOT_A_DATASET = "file must be a .csv or .json file, and a symbolic link must point to one.";
|
|
3948
|
+
var TOO_LARGE = `file is larger than ${MAX_DATASET_MB} MB.`;
|
|
3949
|
+
var UNREADABLE = "file does not exist, or is not a regular file this server can read.";
|
|
3950
|
+
var NOT_AN_ARRAY = "a .json file must hold an array of records.";
|
|
3951
|
+
function refuse(message) {
|
|
3952
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
|
|
3953
|
+
}
|
|
3954
|
+
function startsWithArray(bytes) {
|
|
3955
|
+
let i = bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191 ? 3 : 0;
|
|
3956
|
+
while (bytes[i] === 32 || bytes[i] === 9 || bytes[i] === 10 || bytes[i] === 13) i++;
|
|
3957
|
+
return bytes[i] === 91;
|
|
3958
|
+
}
|
|
3959
|
+
async function readDatasetFile(file) {
|
|
3960
|
+
const named = file.startsWith("~/") ? join2(homedir2(), file.slice(2)) : file;
|
|
3961
|
+
if (!isAbsolute(named) || /^[\\/]{2}/.test(named)) return { error: NOT_ABSOLUTE };
|
|
3962
|
+
if (!datasetContentType(named)) return { error: NOT_A_DATASET };
|
|
3963
|
+
let real;
|
|
3964
|
+
try {
|
|
3965
|
+
real = await realpath(named);
|
|
3966
|
+
} catch {
|
|
3967
|
+
return { error: UNREADABLE };
|
|
3968
|
+
}
|
|
3969
|
+
const type = datasetContentType(real);
|
|
3970
|
+
if (!type) return { error: NOT_A_DATASET };
|
|
3971
|
+
try {
|
|
3972
|
+
const info = await stat(real);
|
|
3973
|
+
if (!info.isFile()) return { error: UNREADABLE };
|
|
3974
|
+
if (info.size > MAX_DATASET_SIZE) return { error: TOO_LARGE };
|
|
3975
|
+
const bytes = await readFile(real);
|
|
3976
|
+
if (type === "application/json" && !startsWithArray(bytes)) return { error: NOT_AN_ARRAY };
|
|
3977
|
+
return { bytes, filename: basename2(real) };
|
|
3978
|
+
} catch {
|
|
3979
|
+
return { error: UNREADABLE };
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
function registerLocalTools(server) {
|
|
3983
|
+
server.registerTool(
|
|
3984
|
+
"lexq_dataset_upload_file",
|
|
3985
|
+
{
|
|
3986
|
+
title: "Upload Dataset File",
|
|
3987
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3988
|
+
description: dedent16`
|
|
3989
|
+
Upload a CSV or JSON file from this computer as a simulation dataset, up to ${MAX_DATASET_MB} MB.
|
|
3990
|
+
The server reads the file from disk, so its content never passes through the conversation.
|
|
3991
|
+
Only the local server (lexq serve --mcp) has this tool.
|
|
3992
|
+
|
|
3993
|
+
file: the absolute path of a .csv or .json file. A leading ~/ stands for the home directory.
|
|
3994
|
+
A symbolic link is followed, and the file it points to must also end in .csv or .json.
|
|
3995
|
+
|
|
3996
|
+
CSV: a header row of fact keys, then one row per record. JSON: an array of objects.
|
|
3997
|
+
|
|
3998
|
+
Returns the stored path and a ready-to-use dataset block for lexq_simulation_start:
|
|
3999
|
+
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
4000
|
+
`,
|
|
3759
4001
|
inputSchema: z2.object({
|
|
3760
|
-
|
|
4002
|
+
file: z2.string().min(1).describe(
|
|
4003
|
+
"Absolute path of a .csv or .json file. A leading ~/ stands for the home directory."
|
|
4004
|
+
)
|
|
4005
|
+
})
|
|
4006
|
+
},
|
|
4007
|
+
async ({ file }) => {
|
|
4008
|
+
const read = await readDatasetFile(file);
|
|
4009
|
+
if ("error" in read) return refuse(read.error);
|
|
4010
|
+
try {
|
|
4011
|
+
const uploaded = await uploadDataset(read.bytes, read.filename, localApiOptions());
|
|
4012
|
+
return {
|
|
4013
|
+
content: [
|
|
4014
|
+
{ type: "text", text: stringifyJson(uploaded, 2) ?? "" },
|
|
4015
|
+
datasetBlock(uploaded.path)
|
|
4016
|
+
]
|
|
4017
|
+
};
|
|
4018
|
+
} catch (error) {
|
|
4019
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
);
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
// src/mcp/server-info.json
|
|
4026
|
+
var server_info_default = {
|
|
4027
|
+
title: "LexQ",
|
|
4028
|
+
description: "The Decision Operations Platform for engineering teams",
|
|
4029
|
+
websiteUrl: "https://lexq.io",
|
|
4030
|
+
icons: [
|
|
4031
|
+
{
|
|
4032
|
+
src: "https://lexq.io/logo.png",
|
|
4033
|
+
mimeType: "image/png",
|
|
4034
|
+
sizes: ["1024x1024"]
|
|
4035
|
+
}
|
|
4036
|
+
]
|
|
4037
|
+
};
|
|
4038
|
+
|
|
4039
|
+
// src/mcp/tools/status.ts
|
|
4040
|
+
import { z as z3 } from "zod";
|
|
4041
|
+
function registerStatusTools(server, callApi) {
|
|
4042
|
+
server.registerTool(
|
|
4043
|
+
"lexq_whoami",
|
|
4044
|
+
{
|
|
4045
|
+
title: "Who Am I",
|
|
4046
|
+
annotations: { readOnlyHint: true },
|
|
4047
|
+
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
|
+
inputSchema: z3.object({})
|
|
4049
|
+
},
|
|
4050
|
+
async () => callApi("GET", "whoami")
|
|
4051
|
+
);
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
// src/mcp/tools/groups.ts
|
|
4055
|
+
import { z as z4 } from "zod";
|
|
4056
|
+
function registerGroupTools(server, callApi) {
|
|
4057
|
+
server.registerTool(
|
|
4058
|
+
"lexq_groups_list",
|
|
4059
|
+
{
|
|
4060
|
+
title: "List Policy Groups",
|
|
4061
|
+
annotations: { readOnlyHint: true },
|
|
4062
|
+
description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
|
|
4063
|
+
inputSchema: z4.object({})
|
|
4064
|
+
},
|
|
4065
|
+
async () => callApi("GET", "policy-groups")
|
|
4066
|
+
);
|
|
4067
|
+
server.registerTool(
|
|
4068
|
+
"lexq_groups_get",
|
|
4069
|
+
{
|
|
4070
|
+
title: "Get Policy Group",
|
|
4071
|
+
annotations: { readOnlyHint: true },
|
|
4072
|
+
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
|
+
inputSchema: z4.object({
|
|
4074
|
+
groupId: z4.string().uuid().describe("Policy group ID")
|
|
4075
|
+
})
|
|
4076
|
+
},
|
|
4077
|
+
async ({ groupId }) => callApi("GET", `policy-groups/${groupId}`)
|
|
4078
|
+
);
|
|
4079
|
+
server.registerTool(
|
|
4080
|
+
"lexq_groups_create",
|
|
4081
|
+
{
|
|
4082
|
+
title: "Create Policy Group",
|
|
4083
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4084
|
+
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
|
+
inputSchema: z4.object({
|
|
4086
|
+
name: z4.string().describe("Group name (unique among non-ARCHIVED)"),
|
|
4087
|
+
description: z4.string().optional().describe("Group description"),
|
|
4088
|
+
activationMode: z4.enum(ConflictResolutionMode).optional().describe("Conflict resolution mode"),
|
|
4089
|
+
activationStrategy: z4.enum(ConflictResolutionStrategy).optional().describe("Ranking used to pick the winning groups when mode is EXCLUSIVE or MAX_N"),
|
|
4090
|
+
executionLimit: z4.number().int().min(1).optional().describe(
|
|
4091
|
+
"How many policy groups sharing this activationGroup may run (required when mode is MAX_N). Counts groups, not rules: every rule of a winning group runs."
|
|
4092
|
+
),
|
|
4093
|
+
activationGroup: z4.string().optional().describe(
|
|
4094
|
+
"Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
|
|
4095
|
+
)
|
|
4096
|
+
})
|
|
4097
|
+
},
|
|
4098
|
+
async (args) => {
|
|
4099
|
+
const body = {
|
|
4100
|
+
name: args.name
|
|
4101
|
+
};
|
|
4102
|
+
if (args.description !== void 0) body.description = args.description;
|
|
4103
|
+
if (args.activationMode !== void 0) body.activationMode = args.activationMode;
|
|
4104
|
+
if (args.activationStrategy !== void 0) body.activationStrategy = args.activationStrategy;
|
|
4105
|
+
if (args.executionLimit !== void 0) body.executionLimit = args.executionLimit;
|
|
4106
|
+
if (args.activationGroup !== void 0) body.activationGroup = args.activationGroup;
|
|
4107
|
+
return callApi("POST", "policy-groups", { body });
|
|
4108
|
+
}
|
|
4109
|
+
);
|
|
4110
|
+
server.registerTool(
|
|
4111
|
+
"lexq_groups_update",
|
|
4112
|
+
{
|
|
4113
|
+
title: "Update Policy Group",
|
|
4114
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4115
|
+
description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
|
|
4116
|
+
inputSchema: z4.object({
|
|
4117
|
+
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
4118
|
+
name: z4.string().optional().describe("New name"),
|
|
4119
|
+
description: z4.string().optional().describe("New description"),
|
|
4120
|
+
status: z4.enum(PolicyGroupStatus).extract(["ACTIVE", "DISABLED"]).optional().describe(
|
|
4121
|
+
"DISABLED stops production execution of this group: execution calls return 403 (P-009), and a composite call that includes the group is rejected in full. The deployed version and any A/B test are kept, and dry run, Impact Simulation, and Decision Replay keep working, so a fix can be verified before setting ACTIVE again. To go back to the previous version, use lexq_deploy_rollback. To take the version out of production, use lexq_deploy_undeploy."
|
|
4122
|
+
),
|
|
4123
|
+
activationGroup: z4.string().optional().describe(
|
|
4124
|
+
"Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
|
|
4125
|
+
),
|
|
4126
|
+
activationMode: z4.enum(ConflictResolutionMode).optional().describe("Conflict resolution mode"),
|
|
4127
|
+
activationStrategy: z4.enum(ConflictResolutionStrategy).optional().describe("Ranking used to pick the winning groups"),
|
|
4128
|
+
executionLimit: z4.number().int().min(1).optional().describe(
|
|
4129
|
+
"How many policy groups sharing this activationGroup may run. Counts groups, not rules."
|
|
4130
|
+
)
|
|
4131
|
+
})
|
|
4132
|
+
},
|
|
4133
|
+
async ({ groupId, ...body }) => callApi("PUT", `policy-groups/${groupId}`, { body })
|
|
4134
|
+
);
|
|
4135
|
+
server.registerTool(
|
|
4136
|
+
"lexq_groups_delete",
|
|
4137
|
+
{
|
|
4138
|
+
title: "Delete Policy Group",
|
|
4139
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4140
|
+
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
|
+
inputSchema: z4.object({
|
|
4142
|
+
groupId: z4.string().uuid().describe("Policy group ID")
|
|
3761
4143
|
})
|
|
3762
4144
|
},
|
|
3763
4145
|
async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}`)
|
|
@@ -3768,8 +4150,8 @@ function registerGroupTools(server, callApi) {
|
|
|
3768
4150
|
title: "Reorder Policy Groups",
|
|
3769
4151
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3770
4152
|
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.",
|
|
3771
|
-
inputSchema:
|
|
3772
|
-
groupIds:
|
|
4153
|
+
inputSchema: z4.object({
|
|
4154
|
+
groupIds: z4.array(z4.string().uuid()).describe("Group IDs in desired priority order (index 0 = priority 1)")
|
|
3773
4155
|
})
|
|
3774
4156
|
},
|
|
3775
4157
|
async ({ groupIds }) => {
|
|
@@ -3786,10 +4168,12 @@ function registerGroupTools(server, callApi) {
|
|
|
3786
4168
|
title: "Start A/B Test",
|
|
3787
4169
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3788
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%.",
|
|
3789
|
-
inputSchema:
|
|
3790
|
-
groupId:
|
|
3791
|
-
testVersionId:
|
|
3792
|
-
trafficRate:
|
|
4171
|
+
inputSchema: z4.object({
|
|
4172
|
+
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
4173
|
+
testVersionId: z4.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
|
|
4174
|
+
trafficRate: z4.number().int().min(MIN_TRAFFIC_RATE).max(MAX_TRAFFIC_RATE).describe(
|
|
4175
|
+
`Traffic percentage routed to challenger (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
|
|
4176
|
+
)
|
|
3793
4177
|
})
|
|
3794
4178
|
},
|
|
3795
4179
|
async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/ab-test`, { body })
|
|
@@ -3800,8 +4184,8 @@ function registerGroupTools(server, callApi) {
|
|
|
3800
4184
|
title: "Stop A/B Test",
|
|
3801
4185
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3802
4186
|
description: "Stop a running A/B test. All traffic is restored to the control (current) version.",
|
|
3803
|
-
inputSchema:
|
|
3804
|
-
groupId:
|
|
4187
|
+
inputSchema: z4.object({
|
|
4188
|
+
groupId: z4.string().uuid().describe("Policy group ID")
|
|
3805
4189
|
})
|
|
3806
4190
|
},
|
|
3807
4191
|
async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}/ab-test`)
|
|
@@ -3812,9 +4196,11 @@ function registerGroupTools(server, callApi) {
|
|
|
3812
4196
|
title: "Adjust A/B Test",
|
|
3813
4197
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3814
4198
|
description: "Adjust traffic rate of a running A/B test.",
|
|
3815
|
-
inputSchema:
|
|
3816
|
-
groupId:
|
|
3817
|
-
trafficRate:
|
|
4199
|
+
inputSchema: z4.object({
|
|
4200
|
+
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
4201
|
+
trafficRate: z4.number().int().min(MIN_TRAFFIC_RATE).max(MAX_TRAFFIC_RATE).describe(
|
|
4202
|
+
`New traffic percentage for challenger (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
|
|
4203
|
+
)
|
|
3818
4204
|
})
|
|
3819
4205
|
},
|
|
3820
4206
|
async ({ groupId, ...body }) => callApi("PATCH", `policy-groups/${groupId}/ab-test/traffic-rate`, { body })
|
|
@@ -3822,7 +4208,7 @@ function registerGroupTools(server, callApi) {
|
|
|
3822
4208
|
}
|
|
3823
4209
|
|
|
3824
4210
|
// src/mcp/tools/versions.ts
|
|
3825
|
-
import { z as
|
|
4211
|
+
import { z as z5 } from "zod";
|
|
3826
4212
|
function registerVersionTools(server, callApi) {
|
|
3827
4213
|
server.registerTool(
|
|
3828
4214
|
"lexq_versions_list",
|
|
@@ -3830,10 +4216,10 @@ function registerVersionTools(server, callApi) {
|
|
|
3830
4216
|
title: "List Policy Versions",
|
|
3831
4217
|
annotations: { readOnlyHint: true },
|
|
3832
4218
|
description: "List all versions of a policy group.",
|
|
3833
|
-
inputSchema:
|
|
3834
|
-
groupId:
|
|
3835
|
-
page:
|
|
3836
|
-
size:
|
|
4219
|
+
inputSchema: z5.object({
|
|
4220
|
+
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4221
|
+
page: z5.number().int().min(0).default(0).describe("Page number"),
|
|
4222
|
+
size: z5.number().int().min(1).max(100).default(20).describe("Page size")
|
|
3837
4223
|
})
|
|
3838
4224
|
},
|
|
3839
4225
|
async ({ groupId, page, size }) => callApi("GET", `policy-groups/${groupId}/versions`, {
|
|
@@ -3846,9 +4232,9 @@ function registerVersionTools(server, callApi) {
|
|
|
3846
4232
|
title: "Get Policy Version",
|
|
3847
4233
|
annotations: { readOnlyHint: true },
|
|
3848
4234
|
description: "Get a single version by ID, including its rules and fact requirements.",
|
|
3849
|
-
inputSchema:
|
|
3850
|
-
groupId:
|
|
3851
|
-
versionId:
|
|
4235
|
+
inputSchema: z5.object({
|
|
4236
|
+
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4237
|
+
versionId: z5.string().uuid().describe("Version ID")
|
|
3852
4238
|
})
|
|
3853
4239
|
},
|
|
3854
4240
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}`)
|
|
@@ -3859,11 +4245,11 @@ function registerVersionTools(server, callApi) {
|
|
|
3859
4245
|
title: "Create Policy Version",
|
|
3860
4246
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3861
4247
|
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.`,
|
|
3862
|
-
inputSchema:
|
|
3863
|
-
groupId:
|
|
3864
|
-
commitMessage:
|
|
3865
|
-
effectiveFrom:
|
|
3866
|
-
effectiveTo:
|
|
4248
|
+
inputSchema: z5.object({
|
|
4249
|
+
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4250
|
+
commitMessage: z5.string().optional().describe("Commit message describing this version"),
|
|
4251
|
+
effectiveFrom: z5.string().optional().describe(`Effective start, ${INSTANT}`),
|
|
4252
|
+
effectiveTo: z5.string().optional().describe(`Effective end, ${INSTANT}`)
|
|
3867
4253
|
})
|
|
3868
4254
|
},
|
|
3869
4255
|
async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/versions`, { body })
|
|
@@ -3874,14 +4260,14 @@ function registerVersionTools(server, callApi) {
|
|
|
3874
4260
|
title: "Update Policy Version",
|
|
3875
4261
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3876
4262
|
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.",
|
|
3877
|
-
inputSchema:
|
|
3878
|
-
groupId:
|
|
3879
|
-
versionId:
|
|
3880
|
-
commitMessage:
|
|
3881
|
-
effectiveFrom:
|
|
3882
|
-
effectiveTo:
|
|
3883
|
-
clearEffectiveFrom:
|
|
3884
|
-
clearEffectiveTo:
|
|
4263
|
+
inputSchema: z5.object({
|
|
4264
|
+
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4265
|
+
versionId: z5.string().uuid().describe("Version ID"),
|
|
4266
|
+
commitMessage: z5.string().optional().describe("New commit message"),
|
|
4267
|
+
effectiveFrom: z5.string().optional().describe(`Effective start, ${INSTANT} Omit it to keep the current start.`),
|
|
4268
|
+
effectiveTo: z5.string().optional().describe(`Effective end, ${INSTANT} Omit it to keep the current end.`),
|
|
4269
|
+
clearEffectiveFrom: z5.boolean().optional().describe("Set to true to remove the effective start. Do not send effectiveFrom with it."),
|
|
4270
|
+
clearEffectiveTo: z5.boolean().optional().describe("Set to true to remove the effective end. Do not send effectiveTo with it.")
|
|
3885
4271
|
})
|
|
3886
4272
|
},
|
|
3887
4273
|
async ({ groupId, versionId, ...body }) => callApi("PUT", `policy-groups/${groupId}/versions/${versionId}`, { body })
|
|
@@ -3892,9 +4278,9 @@ function registerVersionTools(server, callApi) {
|
|
|
3892
4278
|
title: "Delete Policy Version",
|
|
3893
4279
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3894
4280
|
description: "Delete a DRAFT version. Only DRAFT versions can be deleted.",
|
|
3895
|
-
inputSchema:
|
|
3896
|
-
groupId:
|
|
3897
|
-
versionId:
|
|
4281
|
+
inputSchema: z5.object({
|
|
4282
|
+
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4283
|
+
versionId: z5.string().uuid().describe("Version ID")
|
|
3898
4284
|
})
|
|
3899
4285
|
},
|
|
3900
4286
|
async ({ groupId, versionId }) => callApi("DELETE", `policy-groups/${groupId}/versions/${versionId}`)
|
|
@@ -3905,9 +4291,9 @@ function registerVersionTools(server, callApi) {
|
|
|
3905
4291
|
title: "Clone Policy Version",
|
|
3906
4292
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3907
4293
|
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.`,
|
|
3908
|
-
inputSchema:
|
|
3909
|
-
groupId:
|
|
3910
|
-
versionId:
|
|
4294
|
+
inputSchema: z5.object({
|
|
4295
|
+
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4296
|
+
versionId: z5.string().uuid().describe("Source version ID to clone")
|
|
3911
4297
|
})
|
|
3912
4298
|
},
|
|
3913
4299
|
async ({ groupId, versionId }) => {
|
|
@@ -3919,8 +4305,9 @@ function registerVersionTools(server, callApi) {
|
|
|
3919
4305
|
}
|
|
3920
4306
|
|
|
3921
4307
|
// src/mcp/tools/rules.ts
|
|
3922
|
-
import { z as
|
|
3923
|
-
import
|
|
4308
|
+
import { z as z6 } from "zod";
|
|
4309
|
+
import dedent17 from "dedent";
|
|
4310
|
+
var oneOf = (values) => values.map((v) => `"${v}"`).join("|");
|
|
3924
4311
|
function registerRuleTools(server, callApi) {
|
|
3925
4312
|
server.registerTool(
|
|
3926
4313
|
"lexq_rules_list",
|
|
@@ -3928,9 +4315,9 @@ function registerRuleTools(server, callApi) {
|
|
|
3928
4315
|
title: "List Rules",
|
|
3929
4316
|
annotations: { readOnlyHint: true },
|
|
3930
4317
|
description: "List all rules in a version (priority ASC). Returns summary with conditionSummary and actionSummary.",
|
|
3931
|
-
inputSchema:
|
|
3932
|
-
groupId:
|
|
3933
|
-
versionId:
|
|
4318
|
+
inputSchema: z6.object({
|
|
4319
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4320
|
+
versionId: z6.string().uuid().describe("Version ID")
|
|
3934
4321
|
})
|
|
3935
4322
|
},
|
|
3936
4323
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/rules`)
|
|
@@ -3941,10 +4328,10 @@ function registerRuleTools(server, callApi) {
|
|
|
3941
4328
|
title: "Get Rule Detail",
|
|
3942
4329
|
annotations: { readOnlyHint: true },
|
|
3943
4330
|
description: "Get full rule detail including condition tree and action definitions.",
|
|
3944
|
-
inputSchema:
|
|
3945
|
-
groupId:
|
|
3946
|
-
versionId:
|
|
3947
|
-
ruleId:
|
|
4331
|
+
inputSchema: z6.object({
|
|
4332
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4333
|
+
versionId: z6.string().uuid().describe("Version ID"),
|
|
4334
|
+
ruleId: z6.string().uuid().describe("Rule ID")
|
|
3948
4335
|
})
|
|
3949
4336
|
},
|
|
3950
4337
|
async ({ groupId, versionId, ruleId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`)
|
|
@@ -3954,7 +4341,7 @@ function registerRuleTools(server, callApi) {
|
|
|
3954
4341
|
{
|
|
3955
4342
|
title: "Create Rule",
|
|
3956
4343
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3957
|
-
description:
|
|
4344
|
+
description: dedent17`
|
|
3958
4345
|
Create a rule in a DRAFT version. Requires name, condition tree, and actions array. priority is auto-assigned (appended last); use lexq_rules_reorder to change order.
|
|
3959
4346
|
|
|
3960
4347
|
Before creating rules with new fact keys, call lexq_facts_list to check existing facts.
|
|
@@ -3970,8 +4357,8 @@ function registerRuleTools(server, callApi) {
|
|
|
3970
4357
|
After saving, lexq_facts_unregistered lists any keys this version references but has not
|
|
3971
4358
|
defined (non-blocking, version-wide) — use it to decide what to register.
|
|
3972
4359
|
|
|
3973
|
-
Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator:
|
|
3974
|
-
Value types:
|
|
4360
|
+
Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: ${oneOf(LogicalOperator)}, children: [...] }
|
|
4361
|
+
Value types: ${ValueType.join(", ")}
|
|
3975
4362
|
Limits: a condition tree may hold at most ${MAX_CONDITIONS_PER_RULE} conditions (SINGLE nodes; GROUP nodes do not count)
|
|
3976
4363
|
and nest at most ${MAX_CONDITION_DEPTH} levels below the root. A version holds at most ${MAX_RULES_PER_VERSION} rules.
|
|
3977
4364
|
|
|
@@ -3994,7 +4381,7 @@ function registerRuleTools(server, callApi) {
|
|
|
3994
4381
|
Actions: [{ type, parameters }]
|
|
3995
4382
|
|
|
3996
4383
|
Action parameter schemas:
|
|
3997
|
-
- MUTATE_FACT: { targetVar: string, operator:
|
|
4384
|
+
- MUTATE_FACT: { targetVar: string, operator: ${oneOf(MutateOperator)}, method: ${oneOf(CalculationMethod)}, operand: number, refVar?: string, rounding?: RoundingOption }
|
|
3998
4385
|
targetVar is the fact this action reads and writes. It must be a registered fact
|
|
3999
4386
|
(ACT-032 otherwise), and the request must send it (P-015 otherwise). Its value must be
|
|
4000
4387
|
a number when the action runs.
|
|
@@ -4033,12 +4420,12 @@ function registerRuleTools(server, callApi) {
|
|
|
4033
4420
|
writes, send that value as an input fact in a second request. If a rule should run
|
|
4034
4421
|
whenever another one does, put its actions in that rule instead.
|
|
4035
4422
|
|
|
4036
|
-
RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?:
|
|
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).
|
|
4037
4424
|
`,
|
|
4038
|
-
inputSchema:
|
|
4039
|
-
groupId:
|
|
4040
|
-
versionId:
|
|
4041
|
-
rule:
|
|
4425
|
+
inputSchema: z6.object({
|
|
4426
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4427
|
+
versionId: z6.string().uuid().describe("Version ID"),
|
|
4428
|
+
rule: z6.string().describe(
|
|
4042
4429
|
"JSON string of CreateRuleRequest: { name, condition, actions, mutexGroup?, mutexMode?, mutexStrategy?, mutexLimit?, isEnabled? }"
|
|
4043
4430
|
)
|
|
4044
4431
|
})
|
|
@@ -4054,11 +4441,11 @@ function registerRuleTools(server, callApi) {
|
|
|
4054
4441
|
title: "Update Rule",
|
|
4055
4442
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4056
4443
|
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.`,
|
|
4057
|
-
inputSchema:
|
|
4058
|
-
groupId:
|
|
4059
|
-
versionId:
|
|
4060
|
-
ruleId:
|
|
4061
|
-
rule:
|
|
4444
|
+
inputSchema: z6.object({
|
|
4445
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4446
|
+
versionId: z6.string().uuid().describe("Version ID"),
|
|
4447
|
+
ruleId: z6.string().uuid().describe("Rule ID"),
|
|
4448
|
+
rule: z6.string().describe(
|
|
4062
4449
|
"JSON string of UpdateRuleRequest: { name?, condition?, actions?, mutexGroup?, mutexMode?, mutexStrategy?, mutexLimit?, isEnabled? }"
|
|
4063
4450
|
)
|
|
4064
4451
|
})
|
|
@@ -4076,11 +4463,11 @@ function registerRuleTools(server, callApi) {
|
|
|
4076
4463
|
title: "Delete Rule",
|
|
4077
4464
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4078
4465
|
description: "Delete a rule from a DRAFT version.",
|
|
4079
|
-
inputSchema:
|
|
4080
|
-
groupId:
|
|
4081
|
-
versionId:
|
|
4082
|
-
ruleId:
|
|
4083
|
-
force:
|
|
4466
|
+
inputSchema: z6.object({
|
|
4467
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4468
|
+
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)")
|
|
4084
4471
|
})
|
|
4085
4472
|
},
|
|
4086
4473
|
async ({ groupId, versionId, ruleId, force }) => {
|
|
@@ -4097,10 +4484,10 @@ function registerRuleTools(server, callApi) {
|
|
|
4097
4484
|
title: "Reorder Rules",
|
|
4098
4485
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4099
4486
|
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).",
|
|
4100
|
-
inputSchema:
|
|
4101
|
-
groupId:
|
|
4102
|
-
versionId:
|
|
4103
|
-
ruleIds:
|
|
4487
|
+
inputSchema: z6.object({
|
|
4488
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4489
|
+
versionId: z6.string().uuid().describe("Version ID"),
|
|
4490
|
+
ruleIds: z6.array(z6.string().uuid()).describe("Rule IDs in desired priority order")
|
|
4104
4491
|
})
|
|
4105
4492
|
},
|
|
4106
4493
|
async ({ groupId, versionId, ruleIds }) => {
|
|
@@ -4119,11 +4506,11 @@ function registerRuleTools(server, callApi) {
|
|
|
4119
4506
|
title: "Toggle Rule",
|
|
4120
4507
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4121
4508
|
description: "Enable or disable a rule without deleting it.",
|
|
4122
|
-
inputSchema:
|
|
4123
|
-
groupId:
|
|
4124
|
-
versionId:
|
|
4125
|
-
ruleId:
|
|
4126
|
-
isEnabled:
|
|
4509
|
+
inputSchema: z6.object({
|
|
4510
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4511
|
+
versionId: z6.string().uuid().describe("Version ID"),
|
|
4512
|
+
ruleId: z6.string().uuid().describe("Rule ID"),
|
|
4513
|
+
isEnabled: z6.boolean().describe("true to enable, false to disable")
|
|
4127
4514
|
})
|
|
4128
4515
|
},
|
|
4129
4516
|
async ({ groupId, versionId, ruleId, isEnabled }) => callApi("PATCH", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}/enabled`, {
|
|
@@ -4133,7 +4520,7 @@ function registerRuleTools(server, callApi) {
|
|
|
4133
4520
|
}
|
|
4134
4521
|
|
|
4135
4522
|
// src/mcp/tools/facts.ts
|
|
4136
|
-
import { z as
|
|
4523
|
+
import { z as z7 } from "zod";
|
|
4137
4524
|
|
|
4138
4525
|
// src/types/facts.ts
|
|
4139
4526
|
var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
@@ -4147,10 +4534,10 @@ function registerFactTools(server, callApi) {
|
|
|
4147
4534
|
title: "List Fact Definitions",
|
|
4148
4535
|
annotations: { readOnlyHint: true },
|
|
4149
4536
|
description: "List all fact definitions (input variable schema). Shows key, type, required, and PII status. Always check this before creating rules.",
|
|
4150
|
-
inputSchema:
|
|
4151
|
-
page:
|
|
4152
|
-
size:
|
|
4153
|
-
keyword:
|
|
4537
|
+
inputSchema: z7.object({
|
|
4538
|
+
page: z7.number().int().min(0).default(0).describe("Page number"),
|
|
4539
|
+
size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
4540
|
+
keyword: z7.string().optional().describe("Search keyword")
|
|
4154
4541
|
})
|
|
4155
4542
|
},
|
|
4156
4543
|
async ({ page, size, keyword }) => {
|
|
@@ -4164,22 +4551,22 @@ function registerFactTools(server, callApi) {
|
|
|
4164
4551
|
{
|
|
4165
4552
|
title: "Create Fact Definition",
|
|
4166
4553
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4167
|
-
description:
|
|
4168
|
-
inputSchema:
|
|
4169
|
-
key:
|
|
4170
|
-
name:
|
|
4171
|
-
type:
|
|
4172
|
-
description:
|
|
4173
|
-
isRequired:
|
|
4174
|
-
isPii:
|
|
4554
|
+
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
|
+
inputSchema: z7.object({
|
|
4556
|
+
key: z7.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
|
|
4557
|
+
name: z7.string().describe("Display name"),
|
|
4558
|
+
type: z7.enum(VALUE_TYPES).describe("Value type"),
|
|
4559
|
+
description: z7.string().optional().describe("Description"),
|
|
4560
|
+
isRequired: z7.boolean().default(false).describe("Whether this fact is required for rule evaluation"),
|
|
4561
|
+
isPii: z7.boolean().default(false).describe(
|
|
4175
4562
|
"Mark as PII \u2014 masked on every read surface, revealable only in the console (audited)"
|
|
4176
4563
|
),
|
|
4177
|
-
valueDomain:
|
|
4178
|
-
allowedValues:
|
|
4564
|
+
valueDomain: z7.object({
|
|
4565
|
+
allowedValues: z7.array(z7.union([z7.string(), z7.number()])).optional().describe(
|
|
4179
4566
|
"Accepted values. For list types the value domain applies to each element, not to the list as a whole."
|
|
4180
4567
|
),
|
|
4181
|
-
min:
|
|
4182
|
-
max:
|
|
4568
|
+
min: z7.number().optional().describe("Lower bound, inclusive. Numeric types only."),
|
|
4569
|
+
max: z7.number().optional().describe("Upper bound, inclusive. Numeric types only.")
|
|
4183
4570
|
}).optional().describe(
|
|
4184
4571
|
"Which values this fact accepts. Omit for no value domain. STRING and LIST_STRING take allowedValues only; NUMBER and LIST_NUMBER take all three; BOOLEAN takes none. Declaring it lets the rule editor offer a dropdown and rejects a rule literal that could never match."
|
|
4185
4572
|
)
|
|
@@ -4193,21 +4580,21 @@ function registerFactTools(server, callApi) {
|
|
|
4193
4580
|
title: "Update Fact Definition",
|
|
4194
4581
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4195
4582
|
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).",
|
|
4196
|
-
inputSchema:
|
|
4197
|
-
factId:
|
|
4198
|
-
name:
|
|
4199
|
-
description:
|
|
4200
|
-
type:
|
|
4583
|
+
inputSchema: z7.object({
|
|
4584
|
+
factId: z7.string().uuid().describe("Fact definition ID"),
|
|
4585
|
+
name: z7.string().optional().describe("Display name"),
|
|
4586
|
+
description: z7.string().optional().describe('Description. Send "" to remove it.'),
|
|
4587
|
+
type: z7.enum(VALUE_TYPES).optional().describe(
|
|
4201
4588
|
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
|
|
4202
4589
|
),
|
|
4203
|
-
isRequired:
|
|
4204
|
-
isPii:
|
|
4205
|
-
valueDomain:
|
|
4206
|
-
allowedValues:
|
|
4590
|
+
isRequired: z7.boolean().optional().describe("Required flag"),
|
|
4591
|
+
isPii: z7.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
|
|
4592
|
+
valueDomain: z7.object({
|
|
4593
|
+
allowedValues: z7.array(z7.union([z7.string(), z7.number()])).optional().describe(
|
|
4207
4594
|
"Accepted values. For list types the value domain applies to each element, not to the list as a whole."
|
|
4208
4595
|
),
|
|
4209
|
-
min:
|
|
4210
|
-
max:
|
|
4596
|
+
min: z7.number().optional().describe("Lower bound, inclusive. Numeric types only."),
|
|
4597
|
+
max: z7.number().optional().describe("Upper bound, inclusive. Numeric types only.")
|
|
4211
4598
|
}).optional().describe(
|
|
4212
4599
|
"Which values this fact accepts. Three states: omit the field to leave the value domain alone, send {} to remove it, or send a populated object to replace it. Narrowing it never blocks the update and does not rewrite rules. When rules compare this fact with a value the new value domain excludes, the result ends with a warning that lists them by version: drafts and the versions currently deployed."
|
|
4213
4600
|
)
|
|
@@ -4221,8 +4608,8 @@ function registerFactTools(server, callApi) {
|
|
|
4221
4608
|
title: "Delete Fact Definition",
|
|
4222
4609
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4223
4610
|
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.",
|
|
4224
|
-
inputSchema:
|
|
4225
|
-
factId:
|
|
4611
|
+
inputSchema: z7.object({
|
|
4612
|
+
factId: z7.string().uuid().describe("Fact definition ID")
|
|
4226
4613
|
})
|
|
4227
4614
|
},
|
|
4228
4615
|
async ({ factId }) => callApi("DELETE", `schema/facts/${factId}`)
|
|
@@ -4233,7 +4620,7 @@ function registerFactTools(server, callApi) {
|
|
|
4233
4620
|
title: "Get Action Runtime Fact Metadata",
|
|
4234
4621
|
annotations: { readOnlyHint: true },
|
|
4235
4622
|
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.",
|
|
4236
|
-
inputSchema:
|
|
4623
|
+
inputSchema: z7.object({})
|
|
4237
4624
|
},
|
|
4238
4625
|
async () => callApi("GET", "schema/action-metadata")
|
|
4239
4626
|
);
|
|
@@ -4243,9 +4630,9 @@ function registerFactTools(server, callApi) {
|
|
|
4243
4630
|
title: "List Unregistered Facts",
|
|
4244
4631
|
annotations: { readOnlyHint: true },
|
|
4245
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). Register them with lexq_facts_create to enable type validation and the dry-run requirements analyzer.",
|
|
4246
|
-
inputSchema:
|
|
4247
|
-
groupId:
|
|
4248
|
-
versionId:
|
|
4633
|
+
inputSchema: z7.object({
|
|
4634
|
+
groupId: z7.string().uuid().describe("Policy group ID"),
|
|
4635
|
+
versionId: z7.string().uuid().describe("Version ID")
|
|
4249
4636
|
})
|
|
4250
4637
|
},
|
|
4251
4638
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
|
|
@@ -4256,9 +4643,9 @@ function registerFactTools(server, callApi) {
|
|
|
4256
4643
|
title: "Export Fact Catalog",
|
|
4257
4644
|
annotations: { readOnlyHint: true },
|
|
4258
4645
|
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; JSON matches the shape that batch create accepts, so it can be fed straight back in, which is why it leaves out the fields that endpoint does not take. Returns the file contents as text.",
|
|
4259
|
-
inputSchema:
|
|
4260
|
-
format:
|
|
4261
|
-
keyword:
|
|
4646
|
+
inputSchema: z7.object({
|
|
4647
|
+
format: z7.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
|
|
4648
|
+
keyword: z7.string().optional().describe("Filter by key or name")
|
|
4262
4649
|
})
|
|
4263
4650
|
},
|
|
4264
4651
|
async ({ format, keyword }) => callApi("GET", "schema/facts/export", {
|
|
@@ -4268,8 +4655,8 @@ function registerFactTools(server, callApi) {
|
|
|
4268
4655
|
}
|
|
4269
4656
|
|
|
4270
4657
|
// src/mcp/tools/deploy.ts
|
|
4271
|
-
import
|
|
4272
|
-
import { z as
|
|
4658
|
+
import dedent18 from "dedent";
|
|
4659
|
+
import { z as z8 } from "zod";
|
|
4273
4660
|
function registerDeployTools(server, callApi) {
|
|
4274
4661
|
server.registerTool(
|
|
4275
4662
|
"lexq_deploy_publish",
|
|
@@ -4277,10 +4664,10 @@ function registerDeployTools(server, callApi) {
|
|
|
4277
4664
|
title: "Publish Version",
|
|
4278
4665
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4279
4666
|
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.`,
|
|
4280
|
-
inputSchema:
|
|
4281
|
-
groupId:
|
|
4282
|
-
versionId:
|
|
4283
|
-
memo:
|
|
4667
|
+
inputSchema: z8.object({
|
|
4668
|
+
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
4669
|
+
versionId: z8.string().uuid().describe("Version ID to publish"),
|
|
4670
|
+
memo: z8.string().min(1).describe("Publish memo (required)")
|
|
4284
4671
|
})
|
|
4285
4672
|
},
|
|
4286
4673
|
async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/versions/${versionId}/publish`, { body: { memo } })
|
|
@@ -4291,10 +4678,10 @@ function registerDeployTools(server, callApi) {
|
|
|
4291
4678
|
title: "Deploy to Live",
|
|
4292
4679
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4293
4680
|
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.",
|
|
4294
|
-
inputSchema:
|
|
4295
|
-
groupId:
|
|
4296
|
-
versionId:
|
|
4297
|
-
memo:
|
|
4681
|
+
inputSchema: z8.object({
|
|
4682
|
+
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
4683
|
+
versionId: z8.string().uuid().describe("Version ID to deploy"),
|
|
4684
|
+
memo: z8.string().min(1).describe("Deployment memo (required)")
|
|
4298
4685
|
})
|
|
4299
4686
|
},
|
|
4300
4687
|
async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/deploy`, {
|
|
@@ -4306,16 +4693,16 @@ function registerDeployTools(server, callApi) {
|
|
|
4306
4693
|
{
|
|
4307
4694
|
title: "Rollback Deployment",
|
|
4308
4695
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4309
|
-
description:
|
|
4696
|
+
description: dedent18`
|
|
4310
4697
|
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).
|
|
4311
4698
|
|
|
4312
4699
|
Pass the currentVersionId and currentDeploymentId from that call as expectedCurrentVersionId and expectedCurrentDeploymentId. If anyone deployed, rolled back or undeployed in between, the rollback is rejected with 409 and nothing changes: P-062 if another version or no version is live, P-063 if the same version is live but from a newer deployment. Read the group again and check with the user before retrying.
|
|
4313
4700
|
`,
|
|
4314
|
-
inputSchema:
|
|
4315
|
-
groupId:
|
|
4316
|
-
expectedCurrentVersionId:
|
|
4317
|
-
expectedCurrentDeploymentId:
|
|
4318
|
-
memo:
|
|
4701
|
+
inputSchema: z8.object({
|
|
4702
|
+
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
4703
|
+
expectedCurrentVersionId: z8.string().uuid().describe("currentVersionId from your last read of the group"),
|
|
4704
|
+
expectedCurrentDeploymentId: z8.string().uuid().describe("currentDeploymentId from your last read of the group"),
|
|
4705
|
+
memo: z8.string().min(1).describe("Rollback reason (required)")
|
|
4319
4706
|
})
|
|
4320
4707
|
},
|
|
4321
4708
|
async ({ groupId, expectedCurrentVersionId, expectedCurrentDeploymentId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
|
|
@@ -4328,372 +4715,133 @@ function registerDeployTools(server, callApi) {
|
|
|
4328
4715
|
title: "Undeploy",
|
|
4329
4716
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4330
4717
|
description: "Remove the live version from traffic. The version stays ACTIVE but no longer serves requests.",
|
|
4331
|
-
inputSchema:
|
|
4332
|
-
groupId:
|
|
4333
|
-
memo:
|
|
4718
|
+
inputSchema: z8.object({
|
|
4719
|
+
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
4720
|
+
memo: z8.string().min(1).describe("Undeploy reason (required)")
|
|
4334
4721
|
})
|
|
4335
4722
|
},
|
|
4336
4723
|
async ({ groupId, memo }) => callApi("POST", `policy-groups/${groupId}/undeploy`, {
|
|
4337
|
-
body: { memo }
|
|
4338
|
-
})
|
|
4339
|
-
);
|
|
4340
|
-
server.registerTool(
|
|
4341
|
-
"lexq_deploy_schedule",
|
|
4342
|
-
{
|
|
4343
|
-
title: "Schedule Deployment",
|
|
4344
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4345
|
-
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).",
|
|
4346
|
-
inputSchema: z6.object({
|
|
4347
|
-
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4348
|
-
versionId: z6.string().uuid().describe("ACTIVE version ID with a future effective start date"),
|
|
4349
|
-
memo: z6.string().min(1).describe("Schedule memo (required)")
|
|
4350
|
-
})
|
|
4351
|
-
},
|
|
4352
|
-
async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/schedule`, {
|
|
4353
|
-
body: { versionId, memo }
|
|
4354
|
-
})
|
|
4355
|
-
);
|
|
4356
|
-
server.registerTool(
|
|
4357
|
-
"lexq_deploy_unschedule",
|
|
4358
|
-
{
|
|
4359
|
-
title: "Cancel Scheduled Deployment",
|
|
4360
|
-
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4361
|
-
description: "Cancel the pending scheduled deployment for a group. The version itself is not affected. Fails with P-039 if no pending schedule exists.",
|
|
4362
|
-
inputSchema: z6.object({
|
|
4363
|
-
groupId: z6.string().uuid().describe("Policy group ID")
|
|
4364
|
-
})
|
|
4365
|
-
},
|
|
4366
|
-
async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}/schedule`)
|
|
4367
|
-
);
|
|
4368
|
-
server.registerTool(
|
|
4369
|
-
"lexq_deploy_schedules",
|
|
4370
|
-
{
|
|
4371
|
-
title: "List Scheduled Deployments",
|
|
4372
|
-
annotations: { readOnlyHint: true },
|
|
4373
|
-
description: "List scheduled deployments across all groups (all statuses: PENDING, EXECUTED, CANCELED, FAILED), newest first.",
|
|
4374
|
-
inputSchema: z6.object({
|
|
4375
|
-
page: z6.number().int().min(0).default(0).describe("Page number"),
|
|
4376
|
-
size: z6.number().int().min(1).max(100).default(20).describe("Page size")
|
|
4377
|
-
})
|
|
4378
|
-
},
|
|
4379
|
-
async ({ page, size }) => callApi("GET", "policy-groups/schedules", { params: paginationParams(page, size) })
|
|
4380
|
-
);
|
|
4381
|
-
server.registerTool(
|
|
4382
|
-
"lexq_deploy_history",
|
|
4383
|
-
{
|
|
4384
|
-
title: "Deployment History",
|
|
4385
|
-
annotations: { readOnlyHint: true },
|
|
4386
|
-
description: "List deployment history across all groups.",
|
|
4387
|
-
inputSchema: z6.object({
|
|
4388
|
-
page: z6.number().int().min(0).default(0).describe("Page number"),
|
|
4389
|
-
size: z6.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
4390
|
-
groupId: z6.string().uuid().optional().describe("Filter by group ID"),
|
|
4391
|
-
types: z6.string().optional().describe("Filter by deployment types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)"),
|
|
4392
|
-
startDate: z6.string().optional().describe(START_DAY),
|
|
4393
|
-
endDate: z6.string().optional().describe(END_DAY)
|
|
4394
|
-
})
|
|
4395
|
-
},
|
|
4396
|
-
async ({ page, size, groupId, types, startDate, endDate }) => {
|
|
4397
|
-
const params = paginationParams(page, size);
|
|
4398
|
-
if (groupId) params.groupId = groupId;
|
|
4399
|
-
if (types) params.types = types;
|
|
4400
|
-
if (startDate) params.startDate = startDate;
|
|
4401
|
-
if (endDate) params.endDate = endDate;
|
|
4402
|
-
return callApi("GET", "deployments", { params });
|
|
4403
|
-
}
|
|
4404
|
-
);
|
|
4405
|
-
server.registerTool(
|
|
4406
|
-
"lexq_deploy_detail",
|
|
4407
|
-
{
|
|
4408
|
-
title: "Deployment Detail",
|
|
4409
|
-
annotations: { readOnlyHint: true },
|
|
4410
|
-
description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
|
|
4411
|
-
inputSchema: z6.object({
|
|
4412
|
-
deploymentId: z6.string().uuid().describe("Deployment ID")
|
|
4413
|
-
})
|
|
4414
|
-
},
|
|
4415
|
-
async ({ deploymentId }) => callApi("GET", `deployments/${deploymentId}`)
|
|
4416
|
-
);
|
|
4417
|
-
server.registerTool(
|
|
4418
|
-
"lexq_deploy_overview",
|
|
4419
|
-
{
|
|
4420
|
-
title: "Deployment Overview",
|
|
4421
|
-
annotations: { readOnlyHint: true },
|
|
4422
|
-
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.",
|
|
4423
|
-
inputSchema: z6.object({})
|
|
4424
|
-
},
|
|
4425
|
-
async () => callApi("GET", "deployments/overview")
|
|
4426
|
-
);
|
|
4427
|
-
server.registerTool(
|
|
4428
|
-
"lexq_deploy_deployable",
|
|
4429
|
-
{
|
|
4430
|
-
title: "List Deployable Versions",
|
|
4431
|
-
annotations: { readOnlyHint: true },
|
|
4432
|
-
description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
|
|
4433
|
-
inputSchema: z6.object({
|
|
4434
|
-
groupId: z6.string().uuid().describe("Policy group ID")
|
|
4435
|
-
})
|
|
4436
|
-
},
|
|
4437
|
-
async ({ groupId }) => callApi("GET", `deployments/groups/${groupId}/deployable-versions`)
|
|
4438
|
-
);
|
|
4439
|
-
server.registerTool(
|
|
4440
|
-
"lexq_deploy_diff",
|
|
4441
|
-
{
|
|
4442
|
-
title: "Deployment Diff",
|
|
4443
|
-
annotations: { readOnlyHint: true },
|
|
4444
|
-
description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
|
|
4445
|
-
inputSchema: z6.object({
|
|
4446
|
-
baseVersionId: z6.string().uuid().describe("Base version ID (typically the current live)"),
|
|
4447
|
-
targetVersionId: z6.string().uuid().describe("Target version ID (the one you want to deploy)")
|
|
4448
|
-
})
|
|
4449
|
-
},
|
|
4450
|
-
async ({ baseVersionId, targetVersionId }) => callApi("GET", "deployments/diff", {
|
|
4451
|
-
params: { baseVersionId, targetVersionId }
|
|
4452
|
-
})
|
|
4453
|
-
);
|
|
4454
|
-
}
|
|
4455
|
-
|
|
4456
|
-
// src/mcp/tools/analytics.ts
|
|
4457
|
-
import { z as z7 } from "zod";
|
|
4458
|
-
import dedent17 from "dedent";
|
|
4459
|
-
function registerAnalyticsTools(server, callApi) {
|
|
4460
|
-
server.registerTool(
|
|
4461
|
-
"lexq_dry_run",
|
|
4462
|
-
{
|
|
4463
|
-
title: "Dry Run",
|
|
4464
|
-
annotations: { readOnlyHint: true },
|
|
4465
|
-
description: dedent17`
|
|
4466
|
-
Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
|
|
4467
|
-
|
|
4468
|
-
Returns:
|
|
4469
|
-
inputFacts — normalized input facts
|
|
4470
|
-
mutatedFacts — input facts changed by rule actions (e.g. MUTATE_FACT mutates paymentAmount)
|
|
4471
|
-
generatedVariables — system-generated values; every fact in mutatedFacts gets a paired {factName}__delta key (signed difference)
|
|
4472
|
-
executionTraces — per-rule match status
|
|
4473
|
-
decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
|
|
4474
|
-
|
|
4475
|
-
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
|
|
4476
|
-
Always dry-run before publishing to validate rule behavior.`,
|
|
4477
|
-
inputSchema: z7.object({
|
|
4478
|
-
versionId: z7.string().uuid().describe("Policy version ID to test against"),
|
|
4479
|
-
facts: z7.string().describe('JSON string of facts object, e.g. {"paymentAmount":100000}'),
|
|
4480
|
-
includeDebugInfo: z7.boolean().default(true).describe("Include execution and decision traces")
|
|
4481
|
-
})
|
|
4482
|
-
},
|
|
4483
|
-
async ({ versionId, facts, includeDebugInfo }) => {
|
|
4484
|
-
const parsedFacts = parseJson(facts);
|
|
4485
|
-
return callApi("POST", `analytics/dry-run/versions/${versionId}`, {
|
|
4486
|
-
body: { facts: parsedFacts, includeDebugInfo }
|
|
4487
|
-
});
|
|
4488
|
-
}
|
|
4489
|
-
);
|
|
4490
|
-
server.registerTool(
|
|
4491
|
-
"lexq_dry_run_compare",
|
|
4492
|
-
{
|
|
4493
|
-
title: "Dry Run Compare",
|
|
4494
|
-
annotations: { readOnlyHint: true },
|
|
4495
|
-
description: dedent17`
|
|
4496
|
-
Compare dry run results between two versions using the same input facts. Useful for validating changes.
|
|
4497
|
-
|
|
4498
|
-
Returns:
|
|
4499
|
-
resultA / resultB — full DryRunResponse for each version
|
|
4500
|
-
diff.mutatedDiff — changes in mutatedFacts between A and B (key → {before, after})
|
|
4501
|
-
diff.generatedDiff — changes in generatedVariables between A and B`,
|
|
4502
|
-
inputSchema: z7.object({
|
|
4503
|
-
versionIdA: z7.string().uuid().describe("Baseline version ID"),
|
|
4504
|
-
versionIdB: z7.string().uuid().describe("Candidate version ID"),
|
|
4505
|
-
facts: z7.string().describe("JSON string of facts object")
|
|
4506
|
-
})
|
|
4507
|
-
},
|
|
4508
|
-
async ({ versionIdA, versionIdB, facts }) => {
|
|
4509
|
-
const parsedFacts = parseJson(facts);
|
|
4510
|
-
return callApi("POST", "analytics/dry-run/compare", {
|
|
4511
|
-
body: { versionIdA, versionIdB, facts: parsedFacts }
|
|
4512
|
-
});
|
|
4513
|
-
}
|
|
4724
|
+
body: { memo }
|
|
4725
|
+
})
|
|
4514
4726
|
);
|
|
4515
4727
|
server.registerTool(
|
|
4516
|
-
"
|
|
4728
|
+
"lexq_deploy_schedule",
|
|
4517
4729
|
{
|
|
4518
|
-
title: "
|
|
4519
|
-
annotations: { readOnlyHint:
|
|
4520
|
-
description: "
|
|
4521
|
-
inputSchema:
|
|
4522
|
-
groupId:
|
|
4523
|
-
versionId:
|
|
4730
|
+
title: "Schedule Deployment",
|
|
4731
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4732
|
+
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
|
+
inputSchema: z8.object({
|
|
4734
|
+
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
4735
|
+
versionId: z8.string().uuid().describe("ACTIVE version ID with a future effective start date"),
|
|
4736
|
+
memo: z8.string().min(1).describe("Schedule memo (required)")
|
|
4524
4737
|
})
|
|
4525
4738
|
},
|
|
4526
|
-
async ({ groupId, versionId }) => callApi("
|
|
4739
|
+
async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/schedule`, {
|
|
4740
|
+
body: { versionId, memo }
|
|
4741
|
+
})
|
|
4527
4742
|
);
|
|
4528
4743
|
server.registerTool(
|
|
4529
|
-
"
|
|
4744
|
+
"lexq_deploy_unschedule",
|
|
4530
4745
|
{
|
|
4531
|
-
title: "
|
|
4532
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
4533
|
-
description:
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
dataset.type and dataset.source are BOTH required, and must be paired:
|
|
4537
|
-
HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
|
|
4538
|
-
in the organization time zone, both inclusive)
|
|
4539
|
-
UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
|
|
4540
|
-
MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
|
|
4541
|
-
|
|
4542
|
-
options.maxRecords: number (max ${MAX_SIMULATION_RECORDS}). When omitted, HISTORICAL and UPLOADED run at
|
|
4543
|
-
most ${DEFAULT_SIMULATION_RECORDS} records and MANUAL runs every record sent.
|
|
4544
|
-
options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
|
|
4545
|
-
options.includeRuleStats: boolean
|
|
4546
|
-
options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
|
|
4547
|
-
{ "targetVariable": "<fact>", "aggregationType": "COUNT" | "SUM" | "AVG" }
|
|
4548
|
-
|
|
4549
|
-
Example (uploaded dataset):
|
|
4550
|
-
{
|
|
4551
|
-
"policyVersionId": "<uuid>",
|
|
4552
|
-
"dataset": { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<path from lexq_dataset_upload>" },
|
|
4553
|
-
"options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true, "maxRecords": 10000 }
|
|
4554
|
-
}
|
|
4555
|
-
|
|
4556
|
-
Example (historical):
|
|
4557
|
-
{
|
|
4558
|
-
"policyVersionId": "<uuid>",
|
|
4559
|
-
"dataset": { "type": "HISTORICAL", "source": "EXECUTION_LOGS", "from": "2026-01-01", "to": "2026-01-31" },
|
|
4560
|
-
"options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true }
|
|
4561
|
-
}
|
|
4562
|
-
`,
|
|
4563
|
-
inputSchema: z7.object({
|
|
4564
|
-
body: z7.string().describe("JSON string of SimulationRequest")
|
|
4746
|
+
title: "Cancel Scheduled Deployment",
|
|
4747
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4748
|
+
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
|
+
inputSchema: z8.object({
|
|
4750
|
+
groupId: z8.string().uuid().describe("Policy group ID")
|
|
4565
4751
|
})
|
|
4566
4752
|
},
|
|
4567
|
-
async ({
|
|
4568
|
-
const parsed = parseJson(body);
|
|
4569
|
-
return callApi("POST", "analytics/simulations", { body: parsed });
|
|
4570
|
-
}
|
|
4753
|
+
async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}/schedule`)
|
|
4571
4754
|
);
|
|
4572
4755
|
server.registerTool(
|
|
4573
|
-
"
|
|
4756
|
+
"lexq_deploy_schedules",
|
|
4574
4757
|
{
|
|
4575
|
-
title: "
|
|
4758
|
+
title: "List Scheduled Deployments",
|
|
4576
4759
|
annotations: { readOnlyHint: true },
|
|
4577
|
-
description:
|
|
4578
|
-
inputSchema:
|
|
4579
|
-
|
|
4760
|
+
description: `List scheduled deployments across all groups (all statuses: ${DeploymentScheduleStatus.join(", ")}), newest first.`,
|
|
4761
|
+
inputSchema: z8.object({
|
|
4762
|
+
page: z8.number().int().min(0).default(0).describe("Page number"),
|
|
4763
|
+
size: z8.number().int().min(1).max(100).default(20).describe("Page size")
|
|
4580
4764
|
})
|
|
4581
4765
|
},
|
|
4582
|
-
async ({
|
|
4766
|
+
async ({ page, size }) => callApi("GET", "policy-groups/schedules", { params: paginationParams(page, size) })
|
|
4583
4767
|
);
|
|
4584
4768
|
server.registerTool(
|
|
4585
|
-
"
|
|
4769
|
+
"lexq_deploy_history",
|
|
4586
4770
|
{
|
|
4587
|
-
title: "
|
|
4771
|
+
title: "Deployment History",
|
|
4588
4772
|
annotations: { readOnlyHint: true },
|
|
4589
|
-
description: "List
|
|
4590
|
-
inputSchema:
|
|
4591
|
-
page:
|
|
4592
|
-
size:
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4773
|
+
description: "List deployment history across all groups.",
|
|
4774
|
+
inputSchema: z8.object({
|
|
4775
|
+
page: z8.number().int().min(0).default(0).describe("Page number"),
|
|
4776
|
+
size: z8.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
4777
|
+
groupId: z8.string().uuid().optional().describe("Filter by group ID"),
|
|
4778
|
+
types: z8.string().optional().describe(`Filter by deployment types (comma-separated: ${DeploymentType.join(",")})`),
|
|
4779
|
+
startDate: z8.string().optional().describe(START_DAY),
|
|
4780
|
+
endDate: z8.string().optional().describe(END_DAY)
|
|
4596
4781
|
})
|
|
4597
4782
|
},
|
|
4598
|
-
async ({ page, size,
|
|
4783
|
+
async ({ page, size, groupId, types, startDate, endDate }) => {
|
|
4599
4784
|
const params = paginationParams(page, size);
|
|
4600
|
-
if (
|
|
4601
|
-
if (
|
|
4602
|
-
if (
|
|
4603
|
-
|
|
4785
|
+
if (groupId) params.groupId = groupId;
|
|
4786
|
+
if (types) params.types = types;
|
|
4787
|
+
if (startDate) params.startDate = startDate;
|
|
4788
|
+
if (endDate) params.endDate = endDate;
|
|
4789
|
+
return callApi("GET", "deployments", { params });
|
|
4604
4790
|
}
|
|
4605
4791
|
);
|
|
4606
4792
|
server.registerTool(
|
|
4607
|
-
"
|
|
4793
|
+
"lexq_deploy_detail",
|
|
4608
4794
|
{
|
|
4609
|
-
title: "
|
|
4610
|
-
annotations: { readOnlyHint:
|
|
4611
|
-
description: "
|
|
4612
|
-
inputSchema:
|
|
4613
|
-
|
|
4795
|
+
title: "Deployment Detail",
|
|
4796
|
+
annotations: { readOnlyHint: true },
|
|
4797
|
+
description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
|
|
4798
|
+
inputSchema: z8.object({
|
|
4799
|
+
deploymentId: z8.string().uuid().describe("Deployment ID")
|
|
4614
4800
|
})
|
|
4615
4801
|
},
|
|
4616
|
-
async ({
|
|
4802
|
+
async ({ deploymentId }) => callApi("GET", `deployments/${deploymentId}`)
|
|
4617
4803
|
);
|
|
4618
4804
|
server.registerTool(
|
|
4619
|
-
"
|
|
4805
|
+
"lexq_deploy_overview",
|
|
4620
4806
|
{
|
|
4621
|
-
title: "
|
|
4807
|
+
title: "Deployment Overview",
|
|
4622
4808
|
annotations: { readOnlyHint: true },
|
|
4623
|
-
description: "
|
|
4624
|
-
inputSchema:
|
|
4625
|
-
simulationId: z7.string().uuid().describe("Simulation ID"),
|
|
4626
|
-
format: z7.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
4627
|
-
})
|
|
4809
|
+
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
|
+
inputSchema: z8.object({})
|
|
4628
4811
|
},
|
|
4629
|
-
async (
|
|
4630
|
-
params: { format }
|
|
4631
|
-
})
|
|
4812
|
+
async () => callApi("GET", "deployments/overview")
|
|
4632
4813
|
);
|
|
4633
4814
|
server.registerTool(
|
|
4634
|
-
"
|
|
4815
|
+
"lexq_deploy_deployable",
|
|
4635
4816
|
{
|
|
4636
|
-
title: "
|
|
4637
|
-
annotations: { readOnlyHint:
|
|
4638
|
-
description:
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
To use the returned path in lexq_simulation_start, set:
|
|
4643
|
-
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
4644
|
-
|
|
4645
|
-
CSV example:
|
|
4646
|
-
userId,paymentAmount
|
|
4647
|
-
user_001,150000
|
|
4648
|
-
user_002,50000
|
|
4649
|
-
|
|
4650
|
-
JSON example: [{"userId":"user_001","paymentAmount":150000}, {"userId":"user_002","paymentAmount":50000}]
|
|
4651
|
-
`,
|
|
4652
|
-
inputSchema: z7.object({
|
|
4653
|
-
content: z7.string().describe("CSV or JSON content as string"),
|
|
4654
|
-
filename: z7.string().default("dataset.csv").describe("Filename with extension (.csv or .json)")
|
|
4817
|
+
title: "List Deployable Versions",
|
|
4818
|
+
annotations: { readOnlyHint: true },
|
|
4819
|
+
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
|
+
inputSchema: z8.object({
|
|
4821
|
+
groupId: z8.string().uuid().describe("Policy group ID")
|
|
4655
4822
|
})
|
|
4656
4823
|
},
|
|
4657
|
-
async ({
|
|
4658
|
-
const result = await callApi("POST", "analytics/datasets/upload", {
|
|
4659
|
-
upload: { content, filename, fieldName: "file" }
|
|
4660
|
-
});
|
|
4661
|
-
if (!result.isError) {
|
|
4662
|
-
try {
|
|
4663
|
-
const uploaded = parseJson(result.content[0]?.text ?? "{}");
|
|
4664
|
-
if (uploaded.path) {
|
|
4665
|
-
const dataset = { type: "UPLOADED", source: "S3_BUCKET", path: uploaded.path };
|
|
4666
|
-
result.content.push({
|
|
4667
|
-
type: "text",
|
|
4668
|
-
text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
|
|
4669
|
-
});
|
|
4670
|
-
}
|
|
4671
|
-
} catch {
|
|
4672
|
-
}
|
|
4673
|
-
}
|
|
4674
|
-
return result;
|
|
4675
|
-
}
|
|
4824
|
+
async ({ groupId }) => callApi("GET", `deployments/groups/${groupId}/deployable-versions`)
|
|
4676
4825
|
);
|
|
4677
4826
|
server.registerTool(
|
|
4678
|
-
"
|
|
4827
|
+
"lexq_deploy_diff",
|
|
4679
4828
|
{
|
|
4680
|
-
title: "
|
|
4829
|
+
title: "Deployment Diff",
|
|
4681
4830
|
annotations: { readOnlyHint: true },
|
|
4682
|
-
description: "
|
|
4683
|
-
inputSchema:
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
format: z7.enum(EXPORT_FORMATS).default("csv").describe("Template format")
|
|
4831
|
+
description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
|
|
4832
|
+
inputSchema: z8.object({
|
|
4833
|
+
baseVersionId: z8.string().uuid().describe("Base version ID (typically the current live)"),
|
|
4834
|
+
targetVersionId: z8.string().uuid().describe("Target version ID (the one you want to deploy)")
|
|
4687
4835
|
})
|
|
4688
4836
|
},
|
|
4689
|
-
async ({
|
|
4690
|
-
params: {
|
|
4837
|
+
async ({ baseVersionId, targetVersionId }) => callApi("GET", "deployments/diff", {
|
|
4838
|
+
params: { baseVersionId, targetVersionId }
|
|
4691
4839
|
})
|
|
4692
4840
|
);
|
|
4693
4841
|
}
|
|
4694
4842
|
|
|
4695
4843
|
// src/mcp/tools/profile.ts
|
|
4696
|
-
import { z as
|
|
4844
|
+
import { z as z9 } from "zod";
|
|
4697
4845
|
var RELATIVE_THRESHOLD = `flagged = p50 \u2265 ${SLOW_MULTIPLIER}\xD7 median of per-rule p50s within the group; absolute thresholds are intentionally not supported.`;
|
|
4698
4846
|
function profileParams(opts) {
|
|
4699
4847
|
const params = {};
|
|
@@ -4709,15 +4857,15 @@ function registerProfileTools(server, callApi) {
|
|
|
4709
4857
|
{
|
|
4710
4858
|
title: "Group Latency Profile",
|
|
4711
4859
|
annotations: { readOnlyHint: true },
|
|
4712
|
-
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
|
|
4713
|
-
inputSchema:
|
|
4714
|
-
groupId:
|
|
4715
|
-
versionId:
|
|
4716
|
-
from:
|
|
4860
|
+
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
|
+
inputSchema: z9.object({
|
|
4862
|
+
groupId: z9.string().uuid().describe("Policy group ID"),
|
|
4863
|
+
versionId: z9.string().uuid().optional().describe("Version to inspect (default: live version)"),
|
|
4864
|
+
from: z9.string().optional().describe(
|
|
4717
4865
|
`Window start, ISO-8601 instant (e.g. 2026-07-01T00:00:00Z). Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
|
|
4718
4866
|
),
|
|
4719
|
-
to:
|
|
4720
|
-
cacheState:
|
|
4867
|
+
to: z9.string().optional().describe("Window end, ISO-8601 instant. Default: now"),
|
|
4868
|
+
cacheState: z9.enum(ProfileCacheState).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
|
|
4721
4869
|
})
|
|
4722
4870
|
},
|
|
4723
4871
|
async ({ groupId, versionId, from, to, cacheState }) => callApi("GET", `policy-groups/${groupId}/profile`, {
|
|
@@ -4729,15 +4877,15 @@ function registerProfileTools(server, callApi) {
|
|
|
4729
4877
|
{
|
|
4730
4878
|
title: "Rule Latency Detail",
|
|
4731
4879
|
annotations: { readOnlyHint: true },
|
|
4732
|
-
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
|
|
4733
|
-
inputSchema:
|
|
4734
|
-
groupId:
|
|
4735
|
-
ruleId:
|
|
4736
|
-
versionId:
|
|
4737
|
-
from:
|
|
4880
|
+
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
|
+
inputSchema: z9.object({
|
|
4882
|
+
groupId: z9.string().uuid().describe("Policy group ID"),
|
|
4883
|
+
ruleId: z9.string().uuid().describe("Rule ID (from lexq_profile_overview)"),
|
|
4884
|
+
versionId: z9.string().uuid().optional().describe("Version to inspect (default: live version)"),
|
|
4885
|
+
from: z9.string().optional().describe(
|
|
4738
4886
|
`Window start, ISO-8601 instant. Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
|
|
4739
4887
|
),
|
|
4740
|
-
to:
|
|
4888
|
+
to: z9.string().optional().describe("Window end, ISO-8601 instant. Default: now")
|
|
4741
4889
|
})
|
|
4742
4890
|
},
|
|
4743
4891
|
async ({ groupId, ruleId, versionId, from, to }) => callApi("GET", `policy-groups/${groupId}/profile/rules/${ruleId}`, {
|
|
@@ -4747,7 +4895,7 @@ function registerProfileTools(server, callApi) {
|
|
|
4747
4895
|
}
|
|
4748
4896
|
|
|
4749
4897
|
// src/mcp/tools/replay.ts
|
|
4750
|
-
import { z as
|
|
4898
|
+
import { z as z10 } from "zod";
|
|
4751
4899
|
function registerReplayTools(server, callApi) {
|
|
4752
4900
|
server.registerTool(
|
|
4753
4901
|
"lexq_replay_decision",
|
|
@@ -4755,9 +4903,9 @@ function registerReplayTools(server, callApi) {
|
|
|
4755
4903
|
title: "Replay a Decision",
|
|
4756
4904
|
annotations: { readOnlyHint: true },
|
|
4757
4905
|
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.",
|
|
4758
|
-
inputSchema:
|
|
4759
|
-
traceId:
|
|
4760
|
-
candidateVersionId:
|
|
4906
|
+
inputSchema: z10.object({
|
|
4907
|
+
traceId: z10.string().describe("Trace ID of the past execution to replay"),
|
|
4908
|
+
candidateVersionId: z10.string().uuid().describe("Version to re-evaluate against")
|
|
4761
4909
|
})
|
|
4762
4910
|
},
|
|
4763
4911
|
async ({ traceId, candidateVersionId }) => callApi("POST", "replay/decisions", { body: { traceId, candidateVersionId } })
|
|
@@ -4768,11 +4916,11 @@ function registerReplayTools(server, callApi) {
|
|
|
4768
4916
|
title: "Start Window Replay (Blast Radius)",
|
|
4769
4917
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4770
4918
|
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.",
|
|
4771
|
-
inputSchema:
|
|
4772
|
-
candidateVersionId:
|
|
4773
|
-
from:
|
|
4774
|
-
to:
|
|
4775
|
-
maxRecords:
|
|
4919
|
+
inputSchema: z10.object({
|
|
4920
|
+
candidateVersionId: z10.string().uuid().describe("Version to re-evaluate against"),
|
|
4921
|
+
from: z10.string().describe(WINDOW_START_DAY),
|
|
4922
|
+
to: z10.string().describe(WINDOW_END_DAY),
|
|
4923
|
+
maxRecords: z10.number().int().min(1).optional().describe(
|
|
4776
4924
|
`Sample cap (server default applies; hard cap ${REPLAY_WINDOW_MAX_RECORDS / 1e3}k)`
|
|
4777
4925
|
)
|
|
4778
4926
|
})
|
|
@@ -4785,8 +4933,8 @@ function registerReplayTools(server, callApi) {
|
|
|
4785
4933
|
title: "Get Replay Job Status",
|
|
4786
4934
|
annotations: { readOnlyHint: true },
|
|
4787
4935
|
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.",
|
|
4788
|
-
inputSchema:
|
|
4789
|
-
jobId:
|
|
4936
|
+
inputSchema: z10.object({
|
|
4937
|
+
jobId: z10.string().describe("Replay job ID from lexq_replay_start")
|
|
4790
4938
|
})
|
|
4791
4939
|
},
|
|
4792
4940
|
async ({ jobId }) => callApi("GET", `replay/jobs/${jobId}`)
|
|
@@ -4797,9 +4945,9 @@ function registerReplayTools(server, callApi) {
|
|
|
4797
4945
|
title: "List Replay Jobs",
|
|
4798
4946
|
annotations: { readOnlyHint: true },
|
|
4799
4947
|
description: "List window replay job history (reverse-chronological). Lightweight items \u2014 use lexq_replay_status for summary and changed samples.",
|
|
4800
|
-
inputSchema:
|
|
4801
|
-
page:
|
|
4802
|
-
size:
|
|
4948
|
+
inputSchema: z10.object({
|
|
4949
|
+
page: z10.number().int().min(0).default(0).describe("Page number"),
|
|
4950
|
+
size: z10.number().int().min(1).max(100).default(20).describe("Page size")
|
|
4803
4951
|
})
|
|
4804
4952
|
},
|
|
4805
4953
|
async ({ page, size }) => callApi("GET", "replay/jobs", { params: paginationParams(page, size) })
|
|
@@ -4810,8 +4958,8 @@ function registerReplayTools(server, callApi) {
|
|
|
4810
4958
|
title: "Cancel Replay Job",
|
|
4811
4959
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4812
4960
|
description: "Cooperatively cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
|
|
4813
|
-
inputSchema:
|
|
4814
|
-
jobId:
|
|
4961
|
+
inputSchema: z10.object({
|
|
4962
|
+
jobId: z10.string().describe("Replay job ID")
|
|
4815
4963
|
})
|
|
4816
4964
|
},
|
|
4817
4965
|
async ({ jobId }) => callApi("POST", `replay/jobs/${jobId}/cancel`)
|
|
@@ -4822,9 +4970,9 @@ function registerReplayTools(server, callApi) {
|
|
|
4822
4970
|
title: "Export Replay Result",
|
|
4823
4971
|
annotations: { readOnlyHint: true },
|
|
4824
4972
|
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.",
|
|
4825
|
-
inputSchema:
|
|
4826
|
-
jobId:
|
|
4827
|
-
format:
|
|
4973
|
+
inputSchema: z10.object({
|
|
4974
|
+
jobId: z10.string().describe("Replay job ID"),
|
|
4975
|
+
format: z10.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
4828
4976
|
})
|
|
4829
4977
|
},
|
|
4830
4978
|
async ({ jobId, format }) => callApi("GET", `replay/jobs/${jobId}/export`, { params: { format } })
|
|
@@ -4832,7 +4980,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4832
4980
|
}
|
|
4833
4981
|
|
|
4834
4982
|
// src/mcp/tools/history.ts
|
|
4835
|
-
import { z as
|
|
4983
|
+
import { z as z11 } from "zod";
|
|
4836
4984
|
function registerHistoryTools(server, callApi) {
|
|
4837
4985
|
server.registerTool(
|
|
4838
4986
|
"lexq_history_list",
|
|
@@ -4840,15 +4988,15 @@ function registerHistoryTools(server, callApi) {
|
|
|
4840
4988
|
title: "List Execution History",
|
|
4841
4989
|
annotations: { readOnlyHint: true },
|
|
4842
4990
|
description: "List policy execution history. Shows trace ID, group, version, status, match result, and latency.",
|
|
4843
|
-
inputSchema:
|
|
4844
|
-
page:
|
|
4845
|
-
size:
|
|
4846
|
-
traceId:
|
|
4847
|
-
groupId:
|
|
4848
|
-
versionId:
|
|
4849
|
-
status:
|
|
4850
|
-
startDate:
|
|
4851
|
-
endDate:
|
|
4991
|
+
inputSchema: z11.object({
|
|
4992
|
+
page: z11.number().int().min(0).default(0).describe("Page number"),
|
|
4993
|
+
size: z11.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
4994
|
+
traceId: z11.string().optional().describe("Filter by trace ID"),
|
|
4995
|
+
groupId: z11.string().uuid().optional().describe("Filter by policy group"),
|
|
4996
|
+
versionId: z11.string().uuid().optional().describe("Filter by version"),
|
|
4997
|
+
status: z11.enum(ApiExecutionStatus).optional().describe("Filter by execution status"),
|
|
4998
|
+
startDate: z11.string().optional().describe(START_DAY),
|
|
4999
|
+
endDate: z11.string().optional().describe(END_DAY)
|
|
4852
5000
|
})
|
|
4853
5001
|
},
|
|
4854
5002
|
async ({ page, size, traceId, groupId, versionId, status, startDate, endDate }) => {
|
|
@@ -4868,8 +5016,8 @@ function registerHistoryTools(server, callApi) {
|
|
|
4868
5016
|
title: "Get Execution Detail",
|
|
4869
5017
|
annotations: { readOnlyHint: true },
|
|
4870
5018
|
description: "Get full execution detail including inputFacts, mutatedFacts, generatedVariables, executionTraces, and decisionTraces.",
|
|
4871
|
-
inputSchema:
|
|
4872
|
-
traceId:
|
|
5019
|
+
inputSchema: z11.object({
|
|
5020
|
+
traceId: z11.string().describe("Trace ID from execution history")
|
|
4873
5021
|
})
|
|
4874
5022
|
},
|
|
4875
5023
|
async ({ traceId }) => callApi("GET", `execution/history/${traceId}`)
|
|
@@ -4880,10 +5028,10 @@ function registerHistoryTools(server, callApi) {
|
|
|
4880
5028
|
title: "Execution Statistics",
|
|
4881
5029
|
annotations: { readOnlyHint: true },
|
|
4882
5030
|
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.`,
|
|
4883
|
-
inputSchema:
|
|
4884
|
-
groupId:
|
|
4885
|
-
startDate:
|
|
4886
|
-
endDate:
|
|
5031
|
+
inputSchema: z11.object({
|
|
5032
|
+
groupId: z11.string().uuid().optional().describe("Filter by policy group"),
|
|
5033
|
+
startDate: z11.string().optional().describe(START_DAY),
|
|
5034
|
+
endDate: z11.string().optional().describe(END_DAY)
|
|
4887
5035
|
})
|
|
4888
5036
|
},
|
|
4889
5037
|
async ({ groupId, startDate, endDate }) => {
|
|
@@ -4897,7 +5045,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
4897
5045
|
}
|
|
4898
5046
|
|
|
4899
5047
|
// src/mcp/tools/provenance.ts
|
|
4900
|
-
import { z as
|
|
5048
|
+
import { z as z12 } from "zod";
|
|
4901
5049
|
function registerProvenanceTools(server, callApi) {
|
|
4902
5050
|
server.registerTool(
|
|
4903
5051
|
"lexq_provenance_get",
|
|
@@ -4905,8 +5053,8 @@ function registerProvenanceTools(server, callApi) {
|
|
|
4905
5053
|
title: "Get Decision Provenance",
|
|
4906
5054
|
annotations: { readOnlyHint: true },
|
|
4907
5055
|
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.",
|
|
4908
|
-
inputSchema:
|
|
4909
|
-
traceId:
|
|
5056
|
+
inputSchema: z12.object({
|
|
5057
|
+
traceId: z12.string().describe("Trace ID of the execution")
|
|
4910
5058
|
})
|
|
4911
5059
|
},
|
|
4912
5060
|
async ({ traceId }) => callApi("GET", `provenance/${traceId}`)
|
|
@@ -4917,14 +5065,14 @@ function registerProvenanceTools(server, callApi) {
|
|
|
4917
5065
|
title: "List PII Reveal Audits",
|
|
4918
5066
|
annotations: { readOnlyHint: true },
|
|
4919
5067
|
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.",
|
|
4920
|
-
inputSchema:
|
|
4921
|
-
page:
|
|
4922
|
-
size:
|
|
4923
|
-
traceId:
|
|
4924
|
-
revealedBy:
|
|
4925
|
-
factKey:
|
|
4926
|
-
startDate:
|
|
4927
|
-
endDate:
|
|
5068
|
+
inputSchema: z12.object({
|
|
5069
|
+
page: z12.number().int().min(0).default(0).describe("Page number"),
|
|
5070
|
+
size: z12.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
5071
|
+
traceId: z12.string().optional().describe("Filter by trace ID (exact match)"),
|
|
5072
|
+
revealedBy: z12.string().optional().describe("Filter by operator ID (exact match)"),
|
|
5073
|
+
factKey: z12.string().optional().describe("Filter by fact key (partial match, case-insensitive)"),
|
|
5074
|
+
startDate: z12.string().optional().describe(START_DAY),
|
|
5075
|
+
endDate: z12.string().optional().describe(END_DAY)
|
|
4928
5076
|
})
|
|
4929
5077
|
},
|
|
4930
5078
|
async ({ page, size, traceId, revealedBy, factKey, startDate, endDate }) => {
|
|
@@ -4940,7 +5088,11 @@ function registerProvenanceTools(server, callApi) {
|
|
|
4940
5088
|
}
|
|
4941
5089
|
|
|
4942
5090
|
// src/mcp/tools/logs.ts
|
|
4943
|
-
import { z as
|
|
5091
|
+
import { z as z13 } from "zod";
|
|
5092
|
+
var ACTION_NOTES = {
|
|
5093
|
+
RESOLVE: "mark as manually fixed",
|
|
5094
|
+
IGNORE: "skip intentionally"
|
|
5095
|
+
};
|
|
4944
5096
|
function registerLogTools(server, callApi) {
|
|
4945
5097
|
server.registerTool(
|
|
4946
5098
|
"lexq_logs_list",
|
|
@@ -4948,14 +5100,14 @@ function registerLogTools(server, callApi) {
|
|
|
4948
5100
|
title: "List Failure Logs",
|
|
4949
5101
|
annotations: { readOnlyHint: true },
|
|
4950
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.`,
|
|
4951
|
-
inputSchema:
|
|
4952
|
-
page:
|
|
4953
|
-
size:
|
|
4954
|
-
taskType:
|
|
4955
|
-
status:
|
|
4956
|
-
keyword:
|
|
4957
|
-
startDate:
|
|
4958
|
-
endDate:
|
|
5103
|
+
inputSchema: z13.object({
|
|
5104
|
+
page: z13.number().int().min(0).default(0).describe("Page number"),
|
|
5105
|
+
size: z13.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
5106
|
+
taskType: z13.enum(TaskType).optional().describe("Task type"),
|
|
5107
|
+
status: z13.enum(FailureStatus).optional().describe("Log status"),
|
|
5108
|
+
keyword: z13.string().optional().describe("Search in refId, refSubId, errorMessage"),
|
|
5109
|
+
startDate: z13.string().optional().describe(START_DAY),
|
|
5110
|
+
endDate: z13.string().optional().describe(END_DAY)
|
|
4959
5111
|
})
|
|
4960
5112
|
},
|
|
4961
5113
|
async ({ page, size, taskType, status, keyword, startDate, endDate }) => {
|
|
@@ -4974,8 +5126,8 @@ function registerLogTools(server, callApi) {
|
|
|
4974
5126
|
title: "Get Failure Log",
|
|
4975
5127
|
annotations: { readOnlyHint: true },
|
|
4976
5128
|
description: "Get failure log detail by ID.",
|
|
4977
|
-
inputSchema:
|
|
4978
|
-
logId:
|
|
5129
|
+
inputSchema: z13.object({
|
|
5130
|
+
logId: z13.string().uuid().describe("Failure log ID")
|
|
4979
5131
|
})
|
|
4980
5132
|
},
|
|
4981
5133
|
async ({ logId }) => callApi("GET", `failure-logs/${logId}`)
|
|
@@ -4985,10 +5137,10 @@ function registerLogTools(server, callApi) {
|
|
|
4985
5137
|
{
|
|
4986
5138
|
title: "Process Failure Log",
|
|
4987
5139
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4988
|
-
description:
|
|
4989
|
-
inputSchema:
|
|
4990
|
-
logId:
|
|
4991
|
-
action:
|
|
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")
|
|
4992
5144
|
})
|
|
4993
5145
|
},
|
|
4994
5146
|
async ({ logId, action }) => callApi("POST", `failure-logs/${logId}/actions`, {
|
|
@@ -5001,9 +5153,9 @@ function registerLogTools(server, callApi) {
|
|
|
5001
5153
|
title: "Bulk Process Failure Logs",
|
|
5002
5154
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
5003
5155
|
description: "Process multiple failure logs at once. Provide an array of log IDs and the action.",
|
|
5004
|
-
inputSchema:
|
|
5005
|
-
logIds:
|
|
5006
|
-
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")
|
|
5007
5159
|
})
|
|
5008
5160
|
},
|
|
5009
5161
|
async ({ logIds, action }) => callApi("POST", "failure-logs/bulk-actions", {
|
|
@@ -5013,7 +5165,11 @@ function registerLogTools(server, callApi) {
|
|
|
5013
5165
|
}
|
|
5014
5166
|
|
|
5015
5167
|
// src/mcp/tools/webhook-subscriptions.ts
|
|
5016
|
-
import { z as
|
|
5168
|
+
import { z as z14 } from "zod";
|
|
5169
|
+
var FORMAT_NOTES2 = {
|
|
5170
|
+
GENERIC: "full JSON",
|
|
5171
|
+
SLACK: '{"text": "..."}'
|
|
5172
|
+
};
|
|
5017
5173
|
function registerWebhookSubscriptionTools(server, callApi) {
|
|
5018
5174
|
server.registerTool(
|
|
5019
5175
|
"lexq_webhook_subscriptions_list",
|
|
@@ -5021,9 +5177,9 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5021
5177
|
title: "List Webhook Subscriptions",
|
|
5022
5178
|
annotations: { readOnlyHint: true },
|
|
5023
5179
|
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.",
|
|
5024
|
-
inputSchema:
|
|
5025
|
-
page:
|
|
5026
|
-
size:
|
|
5180
|
+
inputSchema: z14.object({
|
|
5181
|
+
page: z14.number().int().min(0).default(0).describe("Page number"),
|
|
5182
|
+
size: z14.number().int().min(1).max(100).default(20).describe("Page size")
|
|
5027
5183
|
})
|
|
5028
5184
|
},
|
|
5029
5185
|
async ({ page, size }) => {
|
|
@@ -5037,8 +5193,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5037
5193
|
title: "Get Webhook Subscription",
|
|
5038
5194
|
annotations: { readOnlyHint: true },
|
|
5039
5195
|
description: "Get webhook subscription detail by ID.",
|
|
5040
|
-
inputSchema:
|
|
5041
|
-
id:
|
|
5196
|
+
inputSchema: z14.object({
|
|
5197
|
+
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
5042
5198
|
})
|
|
5043
5199
|
},
|
|
5044
5200
|
async ({ id }) => callApi("GET", `webhook-subscriptions/${id}`)
|
|
@@ -5048,15 +5204,15 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5048
5204
|
{
|
|
5049
5205
|
title: "Save Webhook Subscription",
|
|
5050
5206
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
5051
|
-
description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats:
|
|
5052
|
-
inputSchema:
|
|
5053
|
-
id:
|
|
5054
|
-
name:
|
|
5055
|
-
webhookUrl:
|
|
5056
|
-
subscribedEvents:
|
|
5057
|
-
payloadFormat:
|
|
5058
|
-
secret:
|
|
5059
|
-
isActive:
|
|
5207
|
+
description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: ${withNotes(FORMAT_NOTES2).join(", ")}.`,
|
|
5208
|
+
inputSchema: z14.object({
|
|
5209
|
+
id: z14.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
|
|
5210
|
+
name: z14.string().min(1).describe("Subscription name (unique per tenant)"),
|
|
5211
|
+
webhookUrl: z14.string().url().describe("Webhook endpoint URL"),
|
|
5212
|
+
subscribedEvents: z14.array(z14.enum(PlatformEventType)).min(1).describe("Events to subscribe to"),
|
|
5213
|
+
payloadFormat: z14.enum(WebhookPayloadFormat).optional().default("GENERIC").describe("Payload format"),
|
|
5214
|
+
secret: z14.string().optional().describe("HMAC-SHA256 signing secret"),
|
|
5215
|
+
isActive: z14.boolean().optional().default(true).describe("Whether the subscription is active")
|
|
5060
5216
|
})
|
|
5061
5217
|
},
|
|
5062
5218
|
async ({ ...body }) => callApi("POST", "webhook-subscriptions", { body })
|
|
@@ -5067,8 +5223,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5067
5223
|
title: "Delete Webhook Subscription",
|
|
5068
5224
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
5069
5225
|
description: "Delete a webhook subscription by ID.",
|
|
5070
|
-
inputSchema:
|
|
5071
|
-
id:
|
|
5226
|
+
inputSchema: z14.object({
|
|
5227
|
+
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
5072
5228
|
})
|
|
5073
5229
|
},
|
|
5074
5230
|
async ({ id }) => callApi("DELETE", `webhook-subscriptions/${id}`)
|
|
@@ -5079,8 +5235,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5079
5235
|
title: "Test Webhook Subscription",
|
|
5080
5236
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
5081
5237
|
description: "Send a test event to verify webhook connectivity. Returns the HTTP status code and success/failure message.",
|
|
5082
|
-
inputSchema:
|
|
5083
|
-
id:
|
|
5238
|
+
inputSchema: z14.object({
|
|
5239
|
+
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
5084
5240
|
})
|
|
5085
5241
|
},
|
|
5086
5242
|
async ({ id }) => callApi("POST", `webhook-subscriptions/${id}/test`)
|
|
@@ -5088,7 +5244,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5088
5244
|
}
|
|
5089
5245
|
|
|
5090
5246
|
// src/mcp/tools/domain-templates.ts
|
|
5091
|
-
import { z as
|
|
5247
|
+
import { z as z15 } from "zod";
|
|
5092
5248
|
function registerDomainTemplateTools(server, callApi) {
|
|
5093
5249
|
server.registerTool(
|
|
5094
5250
|
"lexq_domain_templates_list",
|
|
@@ -5096,7 +5252,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5096
5252
|
title: "List Domain Templates",
|
|
5097
5253
|
annotations: { readOnlyHint: true },
|
|
5098
5254
|
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.",
|
|
5099
|
-
inputSchema:
|
|
5255
|
+
inputSchema: z15.object({})
|
|
5100
5256
|
},
|
|
5101
5257
|
async () => callApi("GET", "domain-templates")
|
|
5102
5258
|
);
|
|
@@ -5106,8 +5262,8 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5106
5262
|
title: "Preview Domain Template",
|
|
5107
5263
|
annotations: { readOnlyHint: true },
|
|
5108
5264
|
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.",
|
|
5109
|
-
inputSchema:
|
|
5110
|
-
template:
|
|
5265
|
+
inputSchema: z15.object({
|
|
5266
|
+
template: z15.string().describe(
|
|
5111
5267
|
"Domain template key (e.g. ECOMMERCE). Use lexq_domain_templates_list to see the keys."
|
|
5112
5268
|
)
|
|
5113
5269
|
})
|
|
@@ -5120,9 +5276,9 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5120
5276
|
title: "Apply Domain Template",
|
|
5121
5277
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
5122
5278
|
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.",
|
|
5123
|
-
inputSchema:
|
|
5124
|
-
template:
|
|
5125
|
-
customName:
|
|
5279
|
+
inputSchema: z15.object({
|
|
5280
|
+
template: z15.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
|
|
5281
|
+
customName: z15.string().optional().describe(
|
|
5126
5282
|
"Optional custom name for the policy group that gets created. If omitted, the template's default name is used."
|
|
5127
5283
|
)
|
|
5128
5284
|
})
|
|
@@ -5158,7 +5314,7 @@ var SERVER_INFO = { name: "lexq", ...server_info_default };
|
|
|
5158
5314
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5159
5315
|
function getVersion() {
|
|
5160
5316
|
try {
|
|
5161
|
-
const pkg = JSON.parse(readFileSync3(
|
|
5317
|
+
const pkg = JSON.parse(readFileSync3(join3(__dirname, "..", "package.json"), "utf-8"));
|
|
5162
5318
|
return pkg.version;
|
|
5163
5319
|
} catch {
|
|
5164
5320
|
return "0.1.0";
|
|
@@ -5171,6 +5327,7 @@ function startMcpServer() {
|
|
|
5171
5327
|
() => {
|
|
5172
5328
|
const server = new McpServer({ ...SERVER_INFO, version });
|
|
5173
5329
|
registerAllTools(server, callApi);
|
|
5330
|
+
registerLocalTools(server);
|
|
5174
5331
|
return server;
|
|
5175
5332
|
},
|
|
5176
5333
|
{
|
|
@@ -5188,7 +5345,7 @@ function startMcpServer() {
|
|
|
5188
5345
|
function registerServeCommand(program) {
|
|
5189
5346
|
program.command("serve").description("Start LexQ as a server for AI agent integrations").option("--mcp", "Start as MCP (Model Context Protocol) server over stdio").addHelpText(
|
|
5190
5347
|
"after",
|
|
5191
|
-
|
|
5348
|
+
dedent19`
|
|
5192
5349
|
|
|
5193
5350
|
Example:
|
|
5194
5351
|
$ lexq serve --mcp
|
|
@@ -5219,11 +5376,11 @@ function registerServeCommand(program) {
|
|
|
5219
5376
|
|
|
5220
5377
|
// src/commands/domain-templates.ts
|
|
5221
5378
|
import "commander";
|
|
5222
|
-
import
|
|
5379
|
+
import dedent20 from "dedent";
|
|
5223
5380
|
function registerDomainTemplateCommands(program) {
|
|
5224
5381
|
const templates = program.command("domain-templates").description("Browse and apply domain templates").addHelpText(
|
|
5225
5382
|
"after",
|
|
5226
|
-
|
|
5383
|
+
dedent20`
|
|
5227
5384
|
|
|
5228
5385
|
A domain template is an industry-specific starter pack of fact
|
|
5229
5386
|
definitions and sample rules. Applying one provisions a ready-to-use
|
|
@@ -5271,7 +5428,7 @@ function registerDomainTemplateCommands(program) {
|
|
|
5271
5428
|
});
|
|
5272
5429
|
templates.command("preview").description("Preview what a domain template provisions").requiredOption("--template <key>", "Domain template key (e.g. ECOMMERCE)").addHelpText(
|
|
5273
5430
|
"after",
|
|
5274
|
-
|
|
5431
|
+
dedent20`
|
|
5275
5432
|
|
|
5276
5433
|
Read-only dry run — shows the fact definitions and sample rules the
|
|
5277
5434
|
template will create. Nothing is provisioned.
|
|
@@ -5300,7 +5457,7 @@ function registerDomainTemplateCommands(program) {
|
|
|
5300
5457
|
});
|
|
5301
5458
|
templates.command("apply").description("Apply a domain template to the current tenant").requiredOption("--template <key>", "Domain template key (e.g. ECOMMERCE)").option("--name <n>", "Custom name for the policy group that gets created").option("--force", "Skip confirmation prompt").addHelpText(
|
|
5302
5459
|
"after",
|
|
5303
|
-
|
|
5460
|
+
dedent20`
|
|
5304
5461
|
|
|
5305
5462
|
Creates the template's fact definitions and a new DRAFT policy group
|
|
5306
5463
|
populated with its sample rules. A fact that already exists with the
|
|
@@ -5354,7 +5511,7 @@ function registerDomainTemplateCommands(program) {
|
|
|
5354
5511
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
5355
5512
|
function getVersion2() {
|
|
5356
5513
|
try {
|
|
5357
|
-
const pkg = JSON.parse(readFileSync4(
|
|
5514
|
+
const pkg = JSON.parse(readFileSync4(join4(__dirname2, "..", "package.json"), "utf-8"));
|
|
5358
5515
|
return pkg.version;
|
|
5359
5516
|
} catch {
|
|
5360
5517
|
return "0.0.0";
|