@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.
@@ -2,6 +2,12 @@ import { Implementation, McpServer } from '@modelcontextprotocol/server';
2
2
  import { LosslessNumber } from 'lossless-json';
3
3
  export { LosslessNumber } from 'lossless-json';
4
4
 
5
+ declare class ApiError extends Error {
6
+ readonly statusCode: number;
7
+ readonly errorCode: string | null;
8
+ constructor(statusCode: number, errorCode: string | null, message: string);
9
+ }
10
+
5
11
  interface McpToolResult {
6
12
  [key: string]: unknown;
7
13
  content: Array<{
@@ -61,12 +67,6 @@ declare function formatUnregisteredFactWarning(meta: unknown): string | null;
61
67
  */
62
68
  declare function formatValueDomainMismatchWarning(meta: unknown): string | null;
63
69
 
64
- declare class ApiError extends Error {
65
- readonly statusCode: number;
66
- readonly errorCode: string | null;
67
- constructor(statusCode: number, errorCode: string | null, message: string);
68
- }
69
-
70
70
  /** Parses JSON text without rounding numbers that a double cannot hold. */
71
71
  declare const parseJson: (text: string) => unknown;
72
72
  /**
@@ -31,9 +31,27 @@ function registerStatusTools(server, callApi) {
31
31
  import { z as z2 } from "zod";
32
32
 
33
33
  // src/types/enums.ts
34
+ var PolicyGroupStatus = ["ACTIVE", "DISABLED", "ARCHIVED"];
35
+ var LogicalOperator = ["AND", "OR"];
36
+ var MutateOperator = ["ASSIGN", "ADD", "SUB", "MUL", "DIV"];
37
+ var CalculationMethod = ["PERCENTAGE", "AMOUNT"];
38
+ var RoundingPolicy = [
39
+ "UP",
40
+ "DOWN",
41
+ "CEILING",
42
+ "FLOOR",
43
+ "HALF_UP",
44
+ "HALF_DOWN",
45
+ "HALF_EVEN"
46
+ ];
34
47
  var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
35
48
  var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
36
49
  var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
50
+ var DeploymentType = ["DEPLOY", "ROLLBACK", "UNDEPLOY"];
51
+ var ApiExecutionStatus = ["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"];
52
+ var ProfileCacheState = ["HIT", "MISS"];
53
+ var SimulationStatus = ["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"];
54
+ var SimulationMetricType = ["COUNT", "SUM", "AVG"];
37
55
  var ExportFormat = ["CSV", "JSON"];
38
56
  var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
39
57
  var FailureAction = ["IGNORE", "RESOLVE"];
@@ -47,9 +65,12 @@ var PlatformEventType = [
47
65
  "DEPLOY_SCHEDULE_CANCELED"
48
66
  ];
49
67
  var WebhookPayloadFormat = ["GENERIC", "SLACK"];
68
+ var DeploymentScheduleStatus = ["PENDING", "EXECUTED", "CANCELED", "FAILED"];
50
69
 
51
70
  // src/types/constants.ts
52
71
  var MAX_ROUNDING_SCALE = 34;
72
+ var MIN_TRAFFIC_RATE = 1;
73
+ var MAX_TRAFFIC_RATE = 99;
53
74
  var REPLAY_WINDOW_MAX_RECORDS = 5e4;
54
75
  var DEFAULT_SIMULATION_RECORDS = 1e3;
55
76
  var MAX_SIMULATION_RECORDS = 1e5;
@@ -66,6 +87,10 @@ var MAX_RULES_PER_VERSION = 100;
66
87
  var MAX_CONDITIONS_PER_RULE = 25;
67
88
  var MAX_CONDITION_DEPTH = 10;
68
89
  var MAX_SNAPSHOT_BYTES = 131072;
90
+ var STATS_DEFAULT_WINDOW_DAYS = 30;
91
+ var STATS_MAX_WINDOW_DAYS = 31;
92
+ var FAILURE_LOG_LOOKBACK_DAYS = 7;
93
+ var PURGE_AFTER_DAYS = 30;
69
94
 
70
95
  // src/mcp/tools/groups.ts
71
96
  function registerGroupTools(server, callApi) {
@@ -132,7 +157,7 @@ function registerGroupTools(server, callApi) {
132
157
  groupId: z2.string().uuid().describe("Policy group ID"),
133
158
  name: z2.string().optional().describe("New name"),
134
159
  description: z2.string().optional().describe("New description"),
135
- status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
160
+ status: z2.enum(PolicyGroupStatus).extract(["ACTIVE", "DISABLED"]).optional().describe(
136
161
  "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."
137
162
  ),
138
163
  activationGroup: z2.string().optional().describe(
@@ -152,7 +177,7 @@ function registerGroupTools(server, callApi) {
152
177
  {
153
178
  title: "Delete Policy Group",
154
179
  annotations: { readOnlyHint: false, destructiveHint: true },
155
- 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 30 days.",
180
+ 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.`,
156
181
  inputSchema: z2.object({
157
182
  groupId: z2.string().uuid().describe("Policy group ID")
158
183
  })
@@ -186,7 +211,9 @@ function registerGroupTools(server, callApi) {
186
211
  inputSchema: z2.object({
187
212
  groupId: z2.string().uuid().describe("Policy group ID"),
188
213
  testVersionId: z2.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
189
- trafficRate: z2.number().int().min(1).max(99).describe("Traffic percentage routed to challenger (1-99)")
214
+ trafficRate: z2.number().int().min(MIN_TRAFFIC_RATE).max(MAX_TRAFFIC_RATE).describe(
215
+ `Traffic percentage routed to challenger (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
216
+ )
190
217
  })
191
218
  },
192
219
  async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/ab-test`, { body })
@@ -211,7 +238,9 @@ function registerGroupTools(server, callApi) {
211
238
  description: "Adjust traffic rate of a running A/B test.",
212
239
  inputSchema: z2.object({
213
240
  groupId: z2.string().uuid().describe("Policy group ID"),
214
- trafficRate: z2.number().int().min(1).max(99).describe("New traffic percentage for challenger (1-99)")
241
+ trafficRate: z2.number().int().min(MIN_TRAFFIC_RATE).max(MAX_TRAFFIC_RATE).describe(
242
+ `New traffic percentage for challenger (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
243
+ )
215
244
  })
216
245
  },
217
246
  async ({ groupId, ...body }) => callApi("PATCH", `policy-groups/${groupId}/ab-test/traffic-rate`, { body })
@@ -324,9 +353,6 @@ var END_DAY = `End date, ${ORG_DAY}. Inclusive: the whole day counts.`;
324
353
  var WINDOW_START_DAY = `Window start date, ${ORG_DAY}. Inclusive.`;
325
354
  var WINDOW_END_DAY = `Window end date, ${ORG_DAY}. Inclusive: the whole day counts.`;
326
355
  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.";
327
- var STATS_DEFAULT_WINDOW_DAYS = 30;
328
- var STATS_MAX_WINDOW_DAYS = 31;
329
- var FAILURE_LOG_LOOKBACK_DAYS = 7;
330
356
 
331
357
  // src/mcp/tools/versions.ts
332
358
  function registerVersionTools(server, callApi) {
@@ -427,6 +453,7 @@ function registerVersionTools(server, callApi) {
427
453
  // src/mcp/tools/rules.ts
428
454
  import { z as z4 } from "zod";
429
455
  import dedent from "dedent";
456
+ var oneOf = (values) => values.map((v) => `"${v}"`).join("|");
430
457
  function registerRuleTools(server, callApi) {
431
458
  server.registerTool(
432
459
  "lexq_rules_list",
@@ -476,8 +503,8 @@ function registerRuleTools(server, callApi) {
476
503
  After saving, lexq_facts_unregistered lists any keys this version references but has not
477
504
  defined (non-blocking, version-wide) — use it to decide what to register.
478
505
 
479
- Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
480
- Value types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
506
+ Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: ${oneOf(LogicalOperator)}, children: [...] }
507
+ Value types: ${ValueType.join(", ")}
481
508
  Limits: a condition tree may hold at most ${MAX_CONDITIONS_PER_RULE} conditions (SINGLE nodes; GROUP nodes do not count)
482
509
  and nest at most ${MAX_CONDITION_DEPTH} levels below the root. A version holds at most ${MAX_RULES_PER_VERSION} rules.
483
510
 
@@ -500,7 +527,7 @@ function registerRuleTools(server, callApi) {
500
527
  Actions: [{ type, parameters }]
501
528
 
502
529
  Action parameter schemas:
503
- - MUTATE_FACT: { targetVar: string, operator: "ASSIGN"|"ADD"|"SUB"|"MUL"|"DIV", method: "PERCENTAGE"|"AMOUNT", operand: number, refVar?: string, rounding?: RoundingOption }
530
+ - MUTATE_FACT: { targetVar: string, operator: ${oneOf(MutateOperator)}, method: ${oneOf(CalculationMethod)}, operand: number, refVar?: string, rounding?: RoundingOption }
504
531
  targetVar is the fact this action reads and writes. It must be a registered fact
505
532
  (ACT-032 otherwise), and the request must send it (P-015 otherwise). Its value must be
506
533
  a number when the action runs.
@@ -539,7 +566,7 @@ function registerRuleTools(server, callApi) {
539
566
  writes, send that value as an input fact in a second request. If a rule should run
540
567
  whenever another one does, put its actions in that rule instead.
541
568
 
542
- RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: "HALF_UP"|"HALF_DOWN"|"HALF_EVEN"|"FLOOR"|"CEILING"|"DOWN"|"UP" } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
569
+ 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).
543
570
  `,
544
571
  inputSchema: z4.object({
545
572
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -674,7 +701,7 @@ function registerFactTools(server, callApi) {
674
701
  {
675
702
  title: "Create Fact Definition",
676
703
  annotations: { readOnlyHint: false, destructiveHint: false },
677
- description: "Register a new input variable. Key starts with a letter, then letters, numbers, and underscores (e.g. paymentAmount). Casing is not enforced. Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER.",
704
+ 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(", ")}.`,
678
705
  inputSchema: z5.object({
679
706
  key: z5.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
680
707
  name: z5.string().describe("Display name"),
@@ -880,7 +907,7 @@ function registerDeployTools(server, callApi) {
880
907
  {
881
908
  title: "List Scheduled Deployments",
882
909
  annotations: { readOnlyHint: true },
883
- description: "List scheduled deployments across all groups (all statuses: PENDING, EXECUTED, CANCELED, FAILED), newest first.",
910
+ description: `List scheduled deployments across all groups (all statuses: ${DeploymentScheduleStatus.join(", ")}), newest first.`,
884
911
  inputSchema: z6.object({
885
912
  page: z6.number().int().min(0).default(0).describe("Page number"),
886
913
  size: z6.number().int().min(1).max(100).default(20).describe("Page size")
@@ -898,7 +925,7 @@ function registerDeployTools(server, callApi) {
898
925
  page: z6.number().int().min(0).default(0).describe("Page number"),
899
926
  size: z6.number().int().min(1).max(100).default(20).describe("Page size"),
900
927
  groupId: z6.string().uuid().optional().describe("Filter by group ID"),
901
- types: z6.string().optional().describe("Filter by deployment types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)"),
928
+ types: z6.string().optional().describe(`Filter by deployment types (comma-separated: ${DeploymentType.join(",")})`),
902
929
  startDate: z6.string().optional().describe(START_DAY),
903
930
  endDate: z6.string().optional().describe(END_DAY)
904
931
  })
@@ -966,6 +993,13 @@ function registerDeployTools(server, callApi) {
966
993
  // src/mcp/tools/analytics.ts
967
994
  import { z as z7 } from "zod";
968
995
  import dedent3 from "dedent";
996
+ function datasetBlock(path) {
997
+ const dataset = { type: "UPLOADED", source: "S3_BUCKET", path };
998
+ return {
999
+ type: "text",
1000
+ text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
1001
+ };
1002
+ }
969
1003
  function registerAnalyticsTools(server, callApi) {
970
1004
  server.registerTool(
971
1005
  "lexq_dry_run",
@@ -1054,7 +1088,7 @@ function registerAnalyticsTools(server, callApi) {
1054
1088
  options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
1055
1089
  options.includeRuleStats: boolean
1056
1090
  options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
1057
- { "targetVariable": "<fact>", "aggregationType": "COUNT" | "SUM" | "AVG" }
1091
+ { "targetVariable": "<fact>", "aggregationType": ${SimulationMetricType.map((t) => `"${t}"`).join(" | ")} }
1058
1092
 
1059
1093
  Example (uploaded dataset):
1060
1094
  {
@@ -1100,7 +1134,7 @@ function registerAnalyticsTools(server, callApi) {
1100
1134
  inputSchema: z7.object({
1101
1135
  page: z7.number().int().min(0).default(0).describe("Page number"),
1102
1136
  size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
1103
- status: z7.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"]).optional().describe("Filter by status"),
1137
+ status: z7.enum(SimulationStatus).optional().describe("Filter by status"),
1104
1138
  from: z7.string().optional().describe(START_DAY),
1105
1139
  to: z7.string().optional().describe(END_DAY)
1106
1140
  })
@@ -1152,6 +1186,10 @@ function registerAnalyticsTools(server, callApi) {
1152
1186
  To use the returned path in lexq_simulation_start, set:
1153
1187
  dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
1154
1188
 
1189
+ The content travels inside the tool call, so this suits small datasets, and the hosted server at
1190
+ mcp.lexq.io limits the size of a request. For a larger file, use lexq_dataset_upload_file on the
1191
+ local server (lexq serve --mcp), or upload it with the CLI or the console and pass the returned path.
1192
+
1155
1193
  CSV example:
1156
1194
  userId,paymentAmount
1157
1195
  user_001,150000
@@ -1171,13 +1209,8 @@ function registerAnalyticsTools(server, callApi) {
1171
1209
  if (!result.isError) {
1172
1210
  try {
1173
1211
  const uploaded = parseJson(result.content[0]?.text ?? "{}");
1174
- if (uploaded.path) {
1175
- const dataset = { type: "UPLOADED", source: "S3_BUCKET", path: uploaded.path };
1176
- result.content.push({
1177
- type: "text",
1178
- text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
1179
- });
1180
- }
1212
+ const path = uploaded.data?.path ?? uploaded.path;
1213
+ if (typeof path === "string" && path) result.content.push(datasetBlock(path));
1181
1214
  } catch {
1182
1215
  }
1183
1216
  }
@@ -1204,6 +1237,15 @@ function registerAnalyticsTools(server, callApi) {
1204
1237
 
1205
1238
  // src/mcp/tools/profile.ts
1206
1239
  import { z as z8 } from "zod";
1240
+
1241
+ // src/lib/percentiles.ts
1242
+ var PERCENTILE_MIN_N = {
1243
+ p50: TAIL_MIN_OBS * 2,
1244
+ p95: TAIL_MIN_OBS * 20,
1245
+ p99: TAIL_MIN_OBS * 100
1246
+ };
1247
+
1248
+ // src/mcp/tools/profile.ts
1207
1249
  var RELATIVE_THRESHOLD = `flagged = p50 \u2265 ${SLOW_MULTIPLIER}\xD7 median of per-rule p50s within the group; absolute thresholds are intentionally not supported.`;
1208
1250
  function profileParams(opts) {
1209
1251
  const params = {};
@@ -1219,7 +1261,7 @@ function registerProfileTools(server, callApi) {
1219
1261
  {
1220
1262
  title: "Group Latency Profile",
1221
1263
  annotations: { readOnlyHint: true },
1222
- 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 6, p95 n \u2265 60, p99 n \u2265 300 \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.`,
1264
+ 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.`,
1223
1265
  inputSchema: z8.object({
1224
1266
  groupId: z8.string().uuid().describe("Policy group ID"),
1225
1267
  versionId: z8.string().uuid().optional().describe("Version to inspect (default: live version)"),
@@ -1227,7 +1269,7 @@ function registerProfileTools(server, callApi) {
1227
1269
  `Window start, ISO-8601 instant (e.g. 2026-07-01T00:00:00Z). Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
1228
1270
  ),
1229
1271
  to: z8.string().optional().describe("Window end, ISO-8601 instant. Default: now"),
1230
- cacheState: z8.enum(["HIT", "MISS"]).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
1272
+ cacheState: z8.enum(ProfileCacheState).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
1231
1273
  })
1232
1274
  },
1233
1275
  async ({ groupId, versionId, from, to, cacheState }) => callApi("GET", `policy-groups/${groupId}/profile`, {
@@ -1239,7 +1281,7 @@ function registerProfileTools(server, callApi) {
1239
1281
  {
1240
1282
  title: "Rule Latency Detail",
1241
1283
  annotations: { readOnlyHint: true },
1242
- 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 6, p95 n \u2265 60, p99 n \u2265 300). ` + RELATIVE_THRESHOLD,
1284
+ 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,
1243
1285
  inputSchema: z8.object({
1244
1286
  groupId: z8.string().uuid().describe("Policy group ID"),
1245
1287
  ruleId: z8.string().uuid().describe("Rule ID (from lexq_profile_overview)"),
@@ -1356,7 +1398,7 @@ function registerHistoryTools(server, callApi) {
1356
1398
  traceId: z10.string().optional().describe("Filter by trace ID"),
1357
1399
  groupId: z10.string().uuid().optional().describe("Filter by policy group"),
1358
1400
  versionId: z10.string().uuid().optional().describe("Filter by version"),
1359
- status: z10.enum(["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"]).optional().describe("Filter by execution status"),
1401
+ status: z10.enum(ApiExecutionStatus).optional().describe("Filter by execution status"),
1360
1402
  startDate: z10.string().optional().describe(START_DAY),
1361
1403
  endDate: z10.string().optional().describe(END_DAY)
1362
1404
  })
@@ -1451,6 +1493,17 @@ function registerProvenanceTools(server, callApi) {
1451
1493
 
1452
1494
  // src/mcp/tools/logs.ts
1453
1495
  import { z as z12 } from "zod";
1496
+
1497
+ // src/lib/enum-notes.ts
1498
+ function withNotes(notes) {
1499
+ return Object.entries(notes).map(([value, note]) => note ? `${value} (${note})` : value);
1500
+ }
1501
+
1502
+ // src/mcp/tools/logs.ts
1503
+ var ACTION_NOTES = {
1504
+ RESOLVE: "mark as manually fixed",
1505
+ IGNORE: "skip intentionally"
1506
+ };
1454
1507
  function registerLogTools(server, callApi) {
1455
1508
  server.registerTool(
1456
1509
  "lexq_logs_list",
@@ -1495,7 +1548,7 @@ function registerLogTools(server, callApi) {
1495
1548
  {
1496
1549
  title: "Process Failure Log",
1497
1550
  annotations: { readOnlyHint: false, destructiveHint: false },
1498
- description: "Process a single failure log: RESOLVE (mark as manually fixed) or IGNORE (skip intentionally).",
1551
+ description: `Process a single failure log: ${withNotes(ACTION_NOTES).join(" or ")}.`,
1499
1552
  inputSchema: z12.object({
1500
1553
  logId: z12.string().uuid().describe("Failure log ID"),
1501
1554
  action: z12.enum(FailureAction).describe("Action to take")
@@ -1524,6 +1577,10 @@ function registerLogTools(server, callApi) {
1524
1577
 
1525
1578
  // src/mcp/tools/webhook-subscriptions.ts
1526
1579
  import { z as z13 } from "zod";
1580
+ var FORMAT_NOTES = {
1581
+ GENERIC: "full JSON",
1582
+ SLACK: '{"text": "..."}'
1583
+ };
1527
1584
  function registerWebhookSubscriptionTools(server, callApi) {
1528
1585
  server.registerTool(
1529
1586
  "lexq_webhook_subscriptions_list",
@@ -1558,7 +1615,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
1558
1615
  {
1559
1616
  title: "Save Webhook Subscription",
1560
1617
  annotations: { readOnlyHint: false, destructiveHint: true },
1561
- description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).`,
1618
+ description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: ${withNotes(FORMAT_NOTES).join(", ")}.`,
1562
1619
  inputSchema: z13.object({
1563
1620
  id: z13.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
1564
1621
  name: z13.string().min(1).describe("Subscription name (unique per tenant)"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexq/cli",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "mcpName": "io.lexq/lexq",
5
5
  "description": "The Decision Operations Platform for engineering teams",
6
6
  "type": "module",
@@ -45,6 +45,7 @@
45
45
  "test:value-domain": "pnpm build && node tests/value-domain.mjs",
46
46
  "test:fact-update": "pnpm build && node tests/fact-update.mjs",
47
47
  "test:rollback": "pnpm build && node tests/rollback.mjs",
48
+ "test:dataset-upload": "pnpm build && node tests/dataset-upload.mjs",
48
49
  "prepublishOnly": "pnpm build",
49
50
  "knip": "knip",
50
51
  "format": "prettier --write \"src/**/*.ts\"",
@@ -76,16 +77,16 @@
76
77
  },
77
78
  "packageManager": "pnpm@10.28.0",
78
79
  "dependencies": {
79
- "@modelcontextprotocol/server": "^2.0.0",
80
+ "@modelcontextprotocol/server": "^2.3.1",
80
81
  "cli-table3": "^0.6.5",
81
82
  "commander": "^13.1.0",
82
83
  "dedent": "^1.7.2",
83
84
  "lossless-json": "4.3.1",
84
- "zod": "^4.5.4"
85
+ "zod": "^4.6.7"
85
86
  },
86
87
  "devDependencies": {
87
88
  "@eslint/js": "^9.20.0",
88
- "@modelcontextprotocol/client": "^2.0.0",
89
+ "@modelcontextprotocol/client": "^2.3.1",
89
90
  "@types/node": "^22.12.0",
90
91
  "eslint": "^10.2.0",
91
92
  "knip": "^6.29.0",
@@ -280,6 +280,11 @@ lexq analytics simulation start --json '{
280
280
  **CSV format:** Header row with fact keys, data rows with values. Types auto-detected.
281
281
  **JSON format:** Array of objects `[{"key": "value"}, ...]`
282
282
 
283
+ Dataset files can be up to 10 MB. Over MCP, the local server (`lexq serve --mcp`) has
284
+ `lexq_dataset_upload_file`, which takes the absolute path of a `.csv` or `.json` file and reads it from
285
+ disk. The hosted server has only `lexq_dataset_upload`, which takes the content inline and suits small
286
+ datasets. For a larger file there, upload it with the command above and pass the returned `path`.
287
+
283
288
  ### Check Status (Poll)
284
289
 
285
290
  ```bash