@lexq/cli 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 join3 } from "path";
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.body) {
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) console.error(`\u2192 ${method} ${url.toString()}`);
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 (!response.ok || json.result !== "SUCCESS") {
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;
@@ -2157,8 +2182,30 @@ async function warnUnregisteredFacts(globalOpts, groupId, versionId) {
2157
2182
 
2158
2183
  // src/commands/analytics.ts
2159
2184
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
2185
+ import { basename } from "path";
2160
2186
  import "commander";
2161
2187
  import dedent8 from "dedent";
2188
+
2189
+ // src/lib/dataset-upload.ts
2190
+ import { extname } from "path";
2191
+ var DATASET_CONTENT_TYPES = {
2192
+ ".csv": "text/csv",
2193
+ ".json": "application/json"
2194
+ };
2195
+ function datasetContentType(filename) {
2196
+ return DATASET_CONTENT_TYPES[extname(filename).toLowerCase()];
2197
+ }
2198
+ function uploadDataset(bytes, filename, options) {
2199
+ const form = new FormData();
2200
+ const type = datasetContentType(filename) ?? "application/octet-stream";
2201
+ form.append("file", new Blob([bytes], { type }), filename);
2202
+ return apiRequest("POST", "analytics/datasets/upload", {
2203
+ ...options,
2204
+ form
2205
+ });
2206
+ }
2207
+
2208
+ // src/commands/analytics.ts
2162
2209
  function registerAnalyticsCommands(program) {
2163
2210
  const analytics = program.command("analytics").description("Dry run, simulation, and requirements").addHelpText(
2164
2211
  "after",
@@ -2577,41 +2624,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
2577
2624
  ).action(async (opts) => {
2578
2625
  try {
2579
2626
  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
2627
  const filePath = opts.file;
2587
- const fileBuffer = readFileSync2(filePath);
2588
- const fileName = filePath.split("/").pop() ?? "dataset";
2589
- const ext = fileName.split(".").pop()?.toLowerCase();
2590
- let contentType = "application/octet-stream";
2591
- if (ext === "csv") contentType = "text/csv";
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
2628
+ const result = await uploadDataset(readFileSync2(filePath), basename(filePath), {
2629
+ apiKey: globalOpts.apiKey,
2630
+ baseUrl: globalOpts.baseUrl,
2631
+ dryRun: globalOpts.dryRun,
2632
+ verbose: globalOpts.verbose
2608
2633
  });
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
2634
  console.log(`\u2713 Dataset uploaded: ${result.path}`);
2616
2635
  console.log(` File: ${result.filename} (${result.size} bytes)`);
2617
2636
  printJson(result);
@@ -3548,56 +3567,36 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3548
3567
 
3549
3568
  // src/commands/serve.ts
3550
3569
  import "commander";
3551
- import dedent18 from "dedent";
3570
+ import dedent19 from "dedent";
3552
3571
 
3553
3572
  // src/mcp/server.ts
3554
3573
  import { readFileSync as readFileSync3 } from "fs";
3555
- import { dirname, join as join2 } from "path";
3574
+ import { dirname, join as join3 } from "path";
3556
3575
  import { fileURLToPath } from "url";
3557
3576
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
3558
3577
  import { McpServer } from "@modelcontextprotocol/server";
3559
3578
 
3560
3579
  // src/mcp/tools/_shared.ts
3580
+ function localApiOptions() {
3581
+ const stored = loadConfig();
3582
+ return {
3583
+ baseUrl: process.env.PARTNER_BASE_URL ?? stored.baseUrl,
3584
+ apiKey: process.env.LEXQ_API_KEY ?? stored.apiKey
3585
+ };
3586
+ }
3561
3587
  function createCallApiFromConfig() {
3562
3588
  return async (method, path, opts) => {
3563
3589
  try {
3564
- const stored = loadConfig();
3565
- const config = {
3566
- ...stored,
3567
- baseUrl: process.env.PARTNER_BASE_URL ?? stored.baseUrl,
3568
- apiKey: process.env.LEXQ_API_KEY ?? stored.apiKey
3569
- };
3590
+ let form;
3570
3591
  if (opts?.upload) {
3571
- const url = new URL(
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
- }
3592
+ form = new FormData();
3583
3593
  const blob = new Blob([opts.upload.content], { type: "text/plain" });
3584
- const formData = new FormData();
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) ?? "" }] };
3594
+ form.append(opts.upload.fieldName, blob, opts.upload.filename);
3593
3595
  }
3594
- const clientOpts = {
3595
- apiKey: config.apiKey,
3596
- baseUrl: config.baseUrl
3597
- };
3598
3596
  const { data, meta } = await apiRequestWithMeta(method, path, {
3599
- ...clientOpts,
3597
+ ...localApiOptions(),
3600
3598
  body: opts?.body,
3599
+ form,
3601
3600
  params: opts?.params
3602
3601
  });
3603
3602
  const content = [
@@ -3640,181 +3639,514 @@ function formatUnregisteredFactWarning(meta) {
3640
3639
  ].join("\n");
3641
3640
  }
3642
3641
 
3643
- // src/mcp/server-info.json
3644
- var server_info_default = {
3645
- title: "LexQ",
3646
- description: "The Decision Operations Platform for engineering teams",
3647
- websiteUrl: "https://lexq.io",
3648
- icons: [
3649
- {
3650
- src: "https://lexq.io/logo.png",
3651
- mimeType: "image/png",
3652
- sizes: ["1024x1024"]
3653
- }
3654
- ]
3655
- };
3642
+ // src/mcp/local-tools.ts
3643
+ import { readFile, realpath, stat } from "fs/promises";
3644
+ import { homedir as homedir2 } from "os";
3645
+ import { basename as basename2, isAbsolute, join as join2 } from "path";
3646
+ import { z as z2 } from "zod";
3647
+ import dedent16 from "dedent";
3656
3648
 
3657
- // src/mcp/tools/status.ts
3649
+ // src/mcp/tools/analytics.ts
3658
3650
  import { z } from "zod";
3659
- function registerStatusTools(server, callApi) {
3651
+ import dedent15 from "dedent";
3652
+ function datasetBlock(path) {
3653
+ const dataset = { type: "UPLOADED", source: "S3_BUCKET", path };
3654
+ return {
3655
+ type: "text",
3656
+ text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
3657
+ };
3658
+ }
3659
+ function registerAnalyticsTools(server, callApi) {
3660
3660
  server.registerTool(
3661
- "lexq_whoami",
3661
+ "lexq_dry_run",
3662
3662
  {
3663
- title: "Who Am I",
3663
+ title: "Dry Run",
3664
3664
  annotations: { readOnlyHint: true },
3665
- 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.',
3666
- inputSchema: z.object({})
3665
+ description: dedent15`
3666
+ Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
3667
+
3668
+ Returns:
3669
+ inputFacts — normalized input facts
3670
+ mutatedFacts — input facts changed by rule actions (e.g. MUTATE_FACT mutates paymentAmount)
3671
+ generatedVariables — system-generated values; every fact in mutatedFacts gets a paired {factName}__delta key (signed difference)
3672
+ executionTraces — per-rule match status
3673
+ decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
3674
+
3675
+ Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
3676
+ Always dry-run before publishing to validate rule behavior.`,
3677
+ inputSchema: z.object({
3678
+ versionId: z.string().uuid().describe("Policy version ID to test against"),
3679
+ facts: z.string().describe('JSON string of facts object, e.g. {"paymentAmount":100000}'),
3680
+ includeDebugInfo: z.boolean().default(true).describe("Include execution and decision traces")
3681
+ })
3667
3682
  },
3668
- async () => callApi("GET", "whoami")
3683
+ async ({ versionId, facts, includeDebugInfo }) => {
3684
+ const parsedFacts = parseJson(facts);
3685
+ return callApi("POST", `analytics/dry-run/versions/${versionId}`, {
3686
+ body: { facts: parsedFacts, includeDebugInfo }
3687
+ });
3688
+ }
3669
3689
  );
3670
- }
3671
-
3672
- // src/mcp/tools/groups.ts
3673
- import { z as z2 } from "zod";
3674
- function registerGroupTools(server, callApi) {
3675
3690
  server.registerTool(
3676
- "lexq_groups_list",
3691
+ "lexq_dry_run_compare",
3677
3692
  {
3678
- title: "List Policy Groups",
3693
+ title: "Dry Run Compare",
3679
3694
  annotations: { readOnlyHint: true },
3680
- description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
3681
- inputSchema: z2.object({})
3695
+ description: dedent15`
3696
+ Compare dry run results between two versions using the same input facts. Useful for validating changes.
3697
+
3698
+ Returns:
3699
+ resultA / resultB — full DryRunResponse for each version
3700
+ diff.mutatedDiff — changes in mutatedFacts between A and B (key → {before, after})
3701
+ diff.generatedDiff — changes in generatedVariables between A and B`,
3702
+ inputSchema: z.object({
3703
+ versionIdA: z.string().uuid().describe("Baseline version ID"),
3704
+ versionIdB: z.string().uuid().describe("Candidate version ID"),
3705
+ facts: z.string().describe("JSON string of facts object")
3706
+ })
3682
3707
  },
3683
- async () => callApi("GET", "policy-groups")
3708
+ async ({ versionIdA, versionIdB, facts }) => {
3709
+ const parsedFacts = parseJson(facts);
3710
+ return callApi("POST", "analytics/dry-run/compare", {
3711
+ body: { versionIdA, versionIdB, facts: parsedFacts }
3712
+ });
3713
+ }
3684
3714
  );
3685
3715
  server.registerTool(
3686
- "lexq_groups_get",
3716
+ "lexq_requirements",
3687
3717
  {
3688
- title: "Get Policy Group",
3718
+ title: "Analyze Requirements",
3689
3719
  annotations: { readOnlyHint: true },
3690
- 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.",
3691
- inputSchema: z2.object({
3692
- groupId: z2.string().uuid().describe("Policy group ID")
3720
+ description: "Analyze which input facts a version requires. Returns required keys, types, and an example request body.",
3721
+ inputSchema: z.object({
3722
+ groupId: z.string().uuid().describe("Policy group ID"),
3723
+ versionId: z.string().uuid().describe("Version ID")
3693
3724
  })
3694
3725
  },
3695
- async ({ groupId }) => callApi("GET", `policy-groups/${groupId}`)
3726
+ async ({ groupId, versionId }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/requirements`)
3696
3727
  );
3697
3728
  server.registerTool(
3698
- "lexq_groups_create",
3729
+ "lexq_simulation_start",
3699
3730
  {
3700
- title: "Create Policy Group",
3731
+ title: "Start Simulation",
3701
3732
  annotations: { readOnlyHint: false, destructiveHint: false },
3702
- 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.`,
3703
- inputSchema: z2.object({
3704
- name: z2.string().describe("Group name (unique among non-ARCHIVED)"),
3705
- description: z2.string().optional().describe("Group description"),
3706
- activationMode: z2.enum(ConflictResolutionMode).optional().describe("Conflict resolution mode"),
3707
- activationStrategy: z2.enum(ConflictResolutionStrategy).optional().describe("Ranking used to pick the winning groups when mode is EXCLUSIVE or MAX_N"),
3708
- executionLimit: z2.number().int().min(1).optional().describe(
3709
- "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."
3710
- ),
3711
- activationGroup: z2.string().optional().describe(
3712
- "Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
3713
- )
3733
+ description: dedent15`
3734
+ Start an Impact Simulation against historical, uploaded, or inline data.
3735
+
3736
+ dataset.type and dataset.source are BOTH required, and must be paired:
3737
+ HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
3738
+ in the organization time zone, both inclusive)
3739
+ UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
3740
+ MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
3741
+
3742
+ options.maxRecords: number (max ${MAX_SIMULATION_RECORDS}). When omitted, HISTORICAL and UPLOADED run at
3743
+ most ${DEFAULT_SIMULATION_RECORDS} records and MANUAL runs every record sent.
3744
+ options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
3745
+ options.includeRuleStats: boolean
3746
+ options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
3747
+ { "targetVariable": "<fact>", "aggregationType": "COUNT" | "SUM" | "AVG" }
3748
+
3749
+ Example (uploaded dataset):
3750
+ {
3751
+ "policyVersionId": "<uuid>",
3752
+ "dataset": { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<path from lexq_dataset_upload>" },
3753
+ "options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true, "maxRecords": 10000 }
3754
+ }
3755
+
3756
+ Example (historical):
3757
+ {
3758
+ "policyVersionId": "<uuid>",
3759
+ "dataset": { "type": "HISTORICAL", "source": "EXECUTION_LOGS", "from": "2026-01-01", "to": "2026-01-31" },
3760
+ "options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true }
3761
+ }
3762
+ `,
3763
+ inputSchema: z.object({
3764
+ body: z.string().describe("JSON string of SimulationRequest")
3714
3765
  })
3715
3766
  },
3716
- async (args) => {
3717
- const body = {
3718
- name: args.name
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 });
3767
+ async ({ body }) => {
3768
+ const parsed = parseJson(body);
3769
+ return callApi("POST", "analytics/simulations", { body: parsed });
3726
3770
  }
3727
3771
  );
3728
3772
  server.registerTool(
3729
- "lexq_groups_update",
3730
- {
3731
- title: "Update Policy Group",
3732
- annotations: { readOnlyHint: false, destructiveHint: true },
3733
- description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
3734
- inputSchema: z2.object({
3735
- groupId: z2.string().uuid().describe("Policy group ID"),
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
- )
3749
- })
3750
- },
3751
- async ({ groupId, ...body }) => callApi("PUT", `policy-groups/${groupId}`, { body })
3752
- );
3753
- server.registerTool(
3754
- "lexq_groups_delete",
3773
+ "lexq_simulation_status",
3755
3774
  {
3756
- title: "Delete Policy Group",
3757
- annotations: { readOnlyHint: false, destructiveHint: true },
3758
- 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.",
3759
- inputSchema: z2.object({
3760
- groupId: z2.string().uuid().describe("Policy group ID")
3775
+ title: "Simulation Status",
3776
+ annotations: { readOnlyHint: true },
3777
+ description: "Get simulation status and results. Poll until status is COMPLETED or FAILED.",
3778
+ inputSchema: z.object({
3779
+ simulationId: z.string().uuid().describe("Simulation ID")
3761
3780
  })
3762
3781
  },
3763
- async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}`)
3782
+ async ({ simulationId }) => callApi("GET", `analytics/simulations/${simulationId}`)
3764
3783
  );
3765
3784
  server.registerTool(
3766
- "lexq_groups_reorder",
3785
+ "lexq_simulation_list",
3767
3786
  {
3768
- title: "Reorder Policy Groups",
3769
- annotations: { readOnlyHint: false, destructiveHint: true },
3770
- 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: z2.object({
3772
- groupIds: z2.array(z2.string().uuid()).describe("Group IDs in desired priority order (index 0 = priority 1)")
3787
+ title: "List Simulations",
3788
+ annotations: { readOnlyHint: true },
3789
+ description: "List simulation history with optional filters.",
3790
+ inputSchema: z.object({
3791
+ page: z.number().int().min(0).default(0).describe("Page number"),
3792
+ size: z.number().int().min(1).max(100).default(20).describe("Page size"),
3793
+ status: z.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"]).optional().describe("Filter by status"),
3794
+ from: z.string().optional().describe(START_DAY),
3795
+ to: z.string().optional().describe(END_DAY)
3773
3796
  })
3774
3797
  },
3775
- async ({ groupIds }) => {
3776
- const groups = groupIds.map((groupId, index) => ({
3777
- groupId,
3778
- priority: index + 1
3779
- }));
3780
- return callApi("PATCH", "policy-groups/reorder", { body: { groups } });
3798
+ async ({ page, size, status, from, to }) => {
3799
+ const params = paginationParams(page, size);
3800
+ if (status) params.status = status;
3801
+ if (from) params.from = from;
3802
+ if (to) params.to = to;
3803
+ return callApi("GET", "analytics/simulations", { params });
3781
3804
  }
3782
3805
  );
3783
3806
  server.registerTool(
3784
- "lexq_ab_test_start",
3807
+ "lexq_simulation_cancel",
3785
3808
  {
3786
- title: "Start A/B Test",
3787
- annotations: { readOnlyHint: false, destructiveHint: false },
3788
- 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: z2.object({
3790
- groupId: z2.string().uuid().describe("Policy group ID"),
3791
- testVersionId: z2.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
3792
- trafficRate: z2.number().int().min(1).max(99).describe("Traffic percentage routed to challenger (1-99)")
3809
+ title: "Cancel Simulation",
3810
+ annotations: { readOnlyHint: false, destructiveHint: true },
3811
+ description: "Cancel a running or pending simulation.",
3812
+ inputSchema: z.object({
3813
+ simulationId: z.string().uuid().describe("Simulation ID")
3793
3814
  })
3794
3815
  },
3795
- async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/ab-test`, { body })
3816
+ async ({ simulationId }) => callApi("DELETE", `analytics/simulations/${simulationId}`)
3796
3817
  );
3797
3818
  server.registerTool(
3798
- "lexq_ab_test_stop",
3819
+ "lexq_simulation_export",
3799
3820
  {
3800
- title: "Stop A/B Test",
3801
- annotations: { readOnlyHint: false, destructiveHint: true },
3802
- description: "Stop a running A/B test. All traffic is restored to the control (current) version.",
3803
- inputSchema: z2.object({
3804
- groupId: z2.string().uuid().describe("Policy group ID")
3821
+ title: "Export Simulation",
3822
+ annotations: { readOnlyHint: true },
3823
+ description: "Export simulation results as JSON or CSV. Returns the raw data.",
3824
+ inputSchema: z.object({
3825
+ simulationId: z.string().uuid().describe("Simulation ID"),
3826
+ format: z.enum(EXPORT_FORMATS).default("json").describe("Export format")
3805
3827
  })
3806
3828
  },
3807
- async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}/ab-test`)
3829
+ async ({ simulationId, format }) => callApi("GET", `analytics/simulations/${simulationId}/export`, {
3830
+ params: { format }
3831
+ })
3808
3832
  );
3809
3833
  server.registerTool(
3810
- "lexq_ab_test_adjust",
3834
+ "lexq_dataset_upload",
3811
3835
  {
3812
- title: "Adjust A/B Test",
3836
+ title: "Upload Dataset",
3837
+ annotations: { readOnlyHint: false, destructiveHint: false },
3838
+ description: dedent15`
3839
+ Upload inline CSV or JSON content as a simulation dataset.
3840
+ The content is uploaded to S3 and a path is returned in the "path" field.
3841
+
3842
+ To use the returned path in lexq_simulation_start, set:
3843
+ dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
3844
+
3845
+ The content travels inside the tool call, so this suits small datasets, and the hosted server at
3846
+ mcp.lexq.io limits the size of a request. For a larger file, use lexq_dataset_upload_file on the
3847
+ local server (lexq serve --mcp), or upload it with the CLI or the console and pass the returned path.
3848
+
3849
+ CSV example:
3850
+ userId,paymentAmount
3851
+ user_001,150000
3852
+ user_002,50000
3853
+
3854
+ JSON example: [{"userId":"user_001","paymentAmount":150000}, {"userId":"user_002","paymentAmount":50000}]
3855
+ `,
3856
+ inputSchema: z.object({
3857
+ content: z.string().describe("CSV or JSON content as string"),
3858
+ filename: z.string().default("dataset.csv").describe("Filename with extension (.csv or .json)")
3859
+ })
3860
+ },
3861
+ async ({ content, filename }) => {
3862
+ const result = await callApi("POST", "analytics/datasets/upload", {
3863
+ upload: { content, filename, fieldName: "file" }
3864
+ });
3865
+ if (!result.isError) {
3866
+ try {
3867
+ const uploaded = parseJson(result.content[0]?.text ?? "{}");
3868
+ const path = uploaded.data?.path ?? uploaded.path;
3869
+ if (typeof path === "string" && path) result.content.push(datasetBlock(path));
3870
+ } catch {
3871
+ }
3872
+ }
3873
+ return result;
3874
+ }
3875
+ );
3876
+ server.registerTool(
3877
+ "lexq_dataset_template",
3878
+ {
3879
+ title: "Download Dataset Template",
3880
+ annotations: { readOnlyHint: true },
3881
+ 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.",
3882
+ inputSchema: z.object({
3883
+ groupId: z.string().uuid().describe("Policy group ID"),
3884
+ versionId: z.string().uuid().describe("Version ID"),
3885
+ format: z.enum(EXPORT_FORMATS).default("csv").describe("Template format")
3886
+ })
3887
+ },
3888
+ async ({ groupId, versionId, format }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/dataset-template`, {
3889
+ params: { format }
3890
+ })
3891
+ );
3892
+ }
3893
+
3894
+ // src/mcp/local-tools.ts
3895
+ var MAX_DATASET_MB = MAX_DATASET_SIZE / 1024 / 1024;
3896
+ var NOT_ABSOLUTE = "file must be an absolute path on this computer. A leading ~/ stands for the home directory.";
3897
+ var NOT_A_DATASET = "file must be a .csv or .json file, and a symbolic link must point to one.";
3898
+ var TOO_LARGE = `file is larger than ${MAX_DATASET_MB} MB.`;
3899
+ var UNREADABLE = "file does not exist, or is not a regular file this server can read.";
3900
+ var NOT_AN_ARRAY = "a .json file must hold an array of records.";
3901
+ function refuse(message) {
3902
+ return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
3903
+ }
3904
+ function startsWithArray(bytes) {
3905
+ let i = bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191 ? 3 : 0;
3906
+ while (bytes[i] === 32 || bytes[i] === 9 || bytes[i] === 10 || bytes[i] === 13) i++;
3907
+ return bytes[i] === 91;
3908
+ }
3909
+ async function readDatasetFile(file) {
3910
+ const named = file.startsWith("~/") ? join2(homedir2(), file.slice(2)) : file;
3911
+ if (!isAbsolute(named) || /^[\\/]{2}/.test(named)) return { error: NOT_ABSOLUTE };
3912
+ if (!datasetContentType(named)) return { error: NOT_A_DATASET };
3913
+ let real;
3914
+ try {
3915
+ real = await realpath(named);
3916
+ } catch {
3917
+ return { error: UNREADABLE };
3918
+ }
3919
+ const type = datasetContentType(real);
3920
+ if (!type) return { error: NOT_A_DATASET };
3921
+ try {
3922
+ const info = await stat(real);
3923
+ if (!info.isFile()) return { error: UNREADABLE };
3924
+ if (info.size > MAX_DATASET_SIZE) return { error: TOO_LARGE };
3925
+ const bytes = await readFile(real);
3926
+ if (type === "application/json" && !startsWithArray(bytes)) return { error: NOT_AN_ARRAY };
3927
+ return { bytes, filename: basename2(real) };
3928
+ } catch {
3929
+ return { error: UNREADABLE };
3930
+ }
3931
+ }
3932
+ function registerLocalTools(server) {
3933
+ server.registerTool(
3934
+ "lexq_dataset_upload_file",
3935
+ {
3936
+ title: "Upload Dataset File",
3937
+ annotations: { readOnlyHint: false, destructiveHint: false },
3938
+ description: dedent16`
3939
+ Upload a CSV or JSON file from this computer as a simulation dataset, up to ${MAX_DATASET_MB} MB.
3940
+ The server reads the file from disk, so its content never passes through the conversation.
3941
+ Only the local server (lexq serve --mcp) has this tool.
3942
+
3943
+ file: the absolute path of a .csv or .json file. A leading ~/ stands for the home directory.
3944
+ A symbolic link is followed, and the file it points to must also end in .csv or .json.
3945
+
3946
+ CSV: a header row of fact keys, then one row per record. JSON: an array of objects.
3947
+
3948
+ Returns the stored path and a ready-to-use dataset block for lexq_simulation_start:
3949
+ dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
3950
+ `,
3951
+ inputSchema: z2.object({
3952
+ file: z2.string().min(1).describe(
3953
+ "Absolute path of a .csv or .json file. A leading ~/ stands for the home directory."
3954
+ )
3955
+ })
3956
+ },
3957
+ async ({ file }) => {
3958
+ const read = await readDatasetFile(file);
3959
+ if ("error" in read) return refuse(read.error);
3960
+ try {
3961
+ const uploaded = await uploadDataset(read.bytes, read.filename, localApiOptions());
3962
+ return {
3963
+ content: [
3964
+ { type: "text", text: stringifyJson(uploaded, 2) ?? "" },
3965
+ datasetBlock(uploaded.path)
3966
+ ]
3967
+ };
3968
+ } catch (error) {
3969
+ return refuse(error instanceof Error ? error.message : String(error));
3970
+ }
3971
+ }
3972
+ );
3973
+ }
3974
+
3975
+ // src/mcp/server-info.json
3976
+ var server_info_default = {
3977
+ title: "LexQ",
3978
+ description: "The Decision Operations Platform for engineering teams",
3979
+ websiteUrl: "https://lexq.io",
3980
+ icons: [
3981
+ {
3982
+ src: "https://lexq.io/logo.png",
3983
+ mimeType: "image/png",
3984
+ sizes: ["1024x1024"]
3985
+ }
3986
+ ]
3987
+ };
3988
+
3989
+ // src/mcp/tools/status.ts
3990
+ import { z as z3 } from "zod";
3991
+ function registerStatusTools(server, callApi) {
3992
+ server.registerTool(
3993
+ "lexq_whoami",
3994
+ {
3995
+ title: "Who Am I",
3996
+ annotations: { readOnlyHint: true },
3997
+ 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.',
3998
+ inputSchema: z3.object({})
3999
+ },
4000
+ async () => callApi("GET", "whoami")
4001
+ );
4002
+ }
4003
+
4004
+ // src/mcp/tools/groups.ts
4005
+ import { z as z4 } from "zod";
4006
+ function registerGroupTools(server, callApi) {
4007
+ server.registerTool(
4008
+ "lexq_groups_list",
4009
+ {
4010
+ title: "List Policy Groups",
4011
+ annotations: { readOnlyHint: true },
4012
+ description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
4013
+ inputSchema: z4.object({})
4014
+ },
4015
+ async () => callApi("GET", "policy-groups")
4016
+ );
4017
+ server.registerTool(
4018
+ "lexq_groups_get",
4019
+ {
4020
+ title: "Get Policy Group",
4021
+ annotations: { readOnlyHint: true },
4022
+ 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.",
4023
+ inputSchema: z4.object({
4024
+ groupId: z4.string().uuid().describe("Policy group ID")
4025
+ })
4026
+ },
4027
+ async ({ groupId }) => callApi("GET", `policy-groups/${groupId}`)
4028
+ );
4029
+ server.registerTool(
4030
+ "lexq_groups_create",
4031
+ {
4032
+ title: "Create Policy Group",
4033
+ annotations: { readOnlyHint: false, destructiveHint: false },
4034
+ 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.`,
4035
+ inputSchema: z4.object({
4036
+ name: z4.string().describe("Group name (unique among non-ARCHIVED)"),
4037
+ description: z4.string().optional().describe("Group description"),
4038
+ activationMode: z4.enum(ConflictResolutionMode).optional().describe("Conflict resolution mode"),
4039
+ activationStrategy: z4.enum(ConflictResolutionStrategy).optional().describe("Ranking used to pick the winning groups when mode is EXCLUSIVE or MAX_N"),
4040
+ executionLimit: z4.number().int().min(1).optional().describe(
4041
+ "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."
4042
+ ),
4043
+ activationGroup: z4.string().optional().describe(
4044
+ "Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
4045
+ )
4046
+ })
4047
+ },
4048
+ async (args) => {
4049
+ const body = {
4050
+ name: args.name
4051
+ };
4052
+ if (args.description !== void 0) body.description = args.description;
4053
+ if (args.activationMode !== void 0) body.activationMode = args.activationMode;
4054
+ if (args.activationStrategy !== void 0) body.activationStrategy = args.activationStrategy;
4055
+ if (args.executionLimit !== void 0) body.executionLimit = args.executionLimit;
4056
+ if (args.activationGroup !== void 0) body.activationGroup = args.activationGroup;
4057
+ return callApi("POST", "policy-groups", { body });
4058
+ }
4059
+ );
4060
+ server.registerTool(
4061
+ "lexq_groups_update",
4062
+ {
4063
+ title: "Update Policy Group",
4064
+ annotations: { readOnlyHint: false, destructiveHint: true },
4065
+ description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
4066
+ inputSchema: z4.object({
4067
+ groupId: z4.string().uuid().describe("Policy group ID"),
4068
+ name: z4.string().optional().describe("New name"),
4069
+ description: z4.string().optional().describe("New description"),
4070
+ status: z4.enum(["ACTIVE", "DISABLED"]).optional().describe(
4071
+ "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."
4072
+ ),
4073
+ activationGroup: z4.string().optional().describe(
4074
+ "Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
4075
+ ),
4076
+ activationMode: z4.enum(ConflictResolutionMode).optional().describe("Conflict resolution mode"),
4077
+ activationStrategy: z4.enum(ConflictResolutionStrategy).optional().describe("Ranking used to pick the winning groups"),
4078
+ executionLimit: z4.number().int().min(1).optional().describe(
4079
+ "How many policy groups sharing this activationGroup may run. Counts groups, not rules."
4080
+ )
4081
+ })
4082
+ },
4083
+ async ({ groupId, ...body }) => callApi("PUT", `policy-groups/${groupId}`, { body })
4084
+ );
4085
+ server.registerTool(
4086
+ "lexq_groups_delete",
4087
+ {
4088
+ title: "Delete Policy Group",
4089
+ annotations: { readOnlyHint: false, destructiveHint: true },
4090
+ 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.",
4091
+ inputSchema: z4.object({
4092
+ groupId: z4.string().uuid().describe("Policy group ID")
4093
+ })
4094
+ },
4095
+ async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}`)
4096
+ );
4097
+ server.registerTool(
4098
+ "lexq_groups_reorder",
4099
+ {
4100
+ title: "Reorder Policy Groups",
4101
+ annotations: { readOnlyHint: false, destructiveHint: true },
4102
+ 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.",
4103
+ inputSchema: z4.object({
4104
+ groupIds: z4.array(z4.string().uuid()).describe("Group IDs in desired priority order (index 0 = priority 1)")
4105
+ })
4106
+ },
4107
+ async ({ groupIds }) => {
4108
+ const groups = groupIds.map((groupId, index) => ({
4109
+ groupId,
4110
+ priority: index + 1
4111
+ }));
4112
+ return callApi("PATCH", "policy-groups/reorder", { body: { groups } });
4113
+ }
4114
+ );
4115
+ server.registerTool(
4116
+ "lexq_ab_test_start",
4117
+ {
4118
+ title: "Start A/B Test",
4119
+ annotations: { readOnlyHint: false, destructiveHint: false },
4120
+ 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%.",
4121
+ inputSchema: z4.object({
4122
+ groupId: z4.string().uuid().describe("Policy group ID"),
4123
+ testVersionId: z4.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
4124
+ trafficRate: z4.number().int().min(1).max(99).describe("Traffic percentage routed to challenger (1-99)")
4125
+ })
4126
+ },
4127
+ async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/ab-test`, { body })
4128
+ );
4129
+ server.registerTool(
4130
+ "lexq_ab_test_stop",
4131
+ {
4132
+ title: "Stop A/B Test",
4133
+ annotations: { readOnlyHint: false, destructiveHint: true },
4134
+ description: "Stop a running A/B test. All traffic is restored to the control (current) version.",
4135
+ inputSchema: z4.object({
4136
+ groupId: z4.string().uuid().describe("Policy group ID")
4137
+ })
4138
+ },
4139
+ async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}/ab-test`)
4140
+ );
4141
+ server.registerTool(
4142
+ "lexq_ab_test_adjust",
4143
+ {
4144
+ title: "Adjust A/B Test",
3813
4145
  annotations: { readOnlyHint: false, destructiveHint: true },
3814
4146
  description: "Adjust traffic rate of a running A/B test.",
3815
- inputSchema: z2.object({
3816
- groupId: z2.string().uuid().describe("Policy group ID"),
3817
- trafficRate: z2.number().int().min(1).max(99).describe("New traffic percentage for challenger (1-99)")
4147
+ inputSchema: z4.object({
4148
+ groupId: z4.string().uuid().describe("Policy group ID"),
4149
+ trafficRate: z4.number().int().min(1).max(99).describe("New traffic percentage for challenger (1-99)")
3818
4150
  })
3819
4151
  },
3820
4152
  async ({ groupId, ...body }) => callApi("PATCH", `policy-groups/${groupId}/ab-test/traffic-rate`, { body })
@@ -3822,7 +4154,7 @@ function registerGroupTools(server, callApi) {
3822
4154
  }
3823
4155
 
3824
4156
  // src/mcp/tools/versions.ts
3825
- import { z as z3 } from "zod";
4157
+ import { z as z5 } from "zod";
3826
4158
  function registerVersionTools(server, callApi) {
3827
4159
  server.registerTool(
3828
4160
  "lexq_versions_list",
@@ -3830,10 +4162,10 @@ function registerVersionTools(server, callApi) {
3830
4162
  title: "List Policy Versions",
3831
4163
  annotations: { readOnlyHint: true },
3832
4164
  description: "List all versions of a policy group.",
3833
- inputSchema: z3.object({
3834
- groupId: z3.string().uuid().describe("Policy group ID"),
3835
- page: z3.number().int().min(0).default(0).describe("Page number"),
3836
- size: z3.number().int().min(1).max(100).default(20).describe("Page size")
4165
+ inputSchema: z5.object({
4166
+ groupId: z5.string().uuid().describe("Policy group ID"),
4167
+ page: z5.number().int().min(0).default(0).describe("Page number"),
4168
+ size: z5.number().int().min(1).max(100).default(20).describe("Page size")
3837
4169
  })
3838
4170
  },
3839
4171
  async ({ groupId, page, size }) => callApi("GET", `policy-groups/${groupId}/versions`, {
@@ -3846,9 +4178,9 @@ function registerVersionTools(server, callApi) {
3846
4178
  title: "Get Policy Version",
3847
4179
  annotations: { readOnlyHint: true },
3848
4180
  description: "Get a single version by ID, including its rules and fact requirements.",
3849
- inputSchema: z3.object({
3850
- groupId: z3.string().uuid().describe("Policy group ID"),
3851
- versionId: z3.string().uuid().describe("Version ID")
4181
+ inputSchema: z5.object({
4182
+ groupId: z5.string().uuid().describe("Policy group ID"),
4183
+ versionId: z5.string().uuid().describe("Version ID")
3852
4184
  })
3853
4185
  },
3854
4186
  async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}`)
@@ -3859,11 +4191,11 @@ function registerVersionTools(server, callApi) {
3859
4191
  title: "Create Policy Version",
3860
4192
  annotations: { readOnlyHint: false, destructiveHint: false },
3861
4193
  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: z3.object({
3863
- groupId: z3.string().uuid().describe("Policy group ID"),
3864
- commitMessage: z3.string().optional().describe("Commit message describing this version"),
3865
- effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT}`),
3866
- effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT}`)
4194
+ inputSchema: z5.object({
4195
+ groupId: z5.string().uuid().describe("Policy group ID"),
4196
+ commitMessage: z5.string().optional().describe("Commit message describing this version"),
4197
+ effectiveFrom: z5.string().optional().describe(`Effective start, ${INSTANT}`),
4198
+ effectiveTo: z5.string().optional().describe(`Effective end, ${INSTANT}`)
3867
4199
  })
3868
4200
  },
3869
4201
  async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/versions`, { body })
@@ -3874,14 +4206,14 @@ function registerVersionTools(server, callApi) {
3874
4206
  title: "Update Policy Version",
3875
4207
  annotations: { readOnlyHint: false, destructiveHint: true },
3876
4208
  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: z3.object({
3878
- groupId: z3.string().uuid().describe("Policy group ID"),
3879
- versionId: z3.string().uuid().describe("Version ID"),
3880
- commitMessage: z3.string().optional().describe("New commit message"),
3881
- effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT} Omit it to keep the current start.`),
3882
- effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT} Omit it to keep the current end.`),
3883
- clearEffectiveFrom: z3.boolean().optional().describe("Set to true to remove the effective start. Do not send effectiveFrom with it."),
3884
- clearEffectiveTo: z3.boolean().optional().describe("Set to true to remove the effective end. Do not send effectiveTo with it.")
4209
+ inputSchema: z5.object({
4210
+ groupId: z5.string().uuid().describe("Policy group ID"),
4211
+ versionId: z5.string().uuid().describe("Version ID"),
4212
+ commitMessage: z5.string().optional().describe("New commit message"),
4213
+ effectiveFrom: z5.string().optional().describe(`Effective start, ${INSTANT} Omit it to keep the current start.`),
4214
+ effectiveTo: z5.string().optional().describe(`Effective end, ${INSTANT} Omit it to keep the current end.`),
4215
+ clearEffectiveFrom: z5.boolean().optional().describe("Set to true to remove the effective start. Do not send effectiveFrom with it."),
4216
+ clearEffectiveTo: z5.boolean().optional().describe("Set to true to remove the effective end. Do not send effectiveTo with it.")
3885
4217
  })
3886
4218
  },
3887
4219
  async ({ groupId, versionId, ...body }) => callApi("PUT", `policy-groups/${groupId}/versions/${versionId}`, { body })
@@ -3892,9 +4224,9 @@ function registerVersionTools(server, callApi) {
3892
4224
  title: "Delete Policy Version",
3893
4225
  annotations: { readOnlyHint: false, destructiveHint: true },
3894
4226
  description: "Delete a DRAFT version. Only DRAFT versions can be deleted.",
3895
- inputSchema: z3.object({
3896
- groupId: z3.string().uuid().describe("Policy group ID"),
3897
- versionId: z3.string().uuid().describe("Version ID")
4227
+ inputSchema: z5.object({
4228
+ groupId: z5.string().uuid().describe("Policy group ID"),
4229
+ versionId: z5.string().uuid().describe("Version ID")
3898
4230
  })
3899
4231
  },
3900
4232
  async ({ groupId, versionId }) => callApi("DELETE", `policy-groups/${groupId}/versions/${versionId}`)
@@ -3905,9 +4237,9 @@ function registerVersionTools(server, callApi) {
3905
4237
  title: "Clone Policy Version",
3906
4238
  annotations: { readOnlyHint: false, destructiveHint: false },
3907
4239
  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: z3.object({
3909
- groupId: z3.string().uuid().describe("Policy group ID"),
3910
- versionId: z3.string().uuid().describe("Source version ID to clone")
4240
+ inputSchema: z5.object({
4241
+ groupId: z5.string().uuid().describe("Policy group ID"),
4242
+ versionId: z5.string().uuid().describe("Source version ID to clone")
3911
4243
  })
3912
4244
  },
3913
4245
  async ({ groupId, versionId }) => {
@@ -3919,8 +4251,8 @@ function registerVersionTools(server, callApi) {
3919
4251
  }
3920
4252
 
3921
4253
  // src/mcp/tools/rules.ts
3922
- import { z as z4 } from "zod";
3923
- import dedent15 from "dedent";
4254
+ import { z as z6 } from "zod";
4255
+ import dedent17 from "dedent";
3924
4256
  function registerRuleTools(server, callApi) {
3925
4257
  server.registerTool(
3926
4258
  "lexq_rules_list",
@@ -3928,9 +4260,9 @@ function registerRuleTools(server, callApi) {
3928
4260
  title: "List Rules",
3929
4261
  annotations: { readOnlyHint: true },
3930
4262
  description: "List all rules in a version (priority ASC). Returns summary with conditionSummary and actionSummary.",
3931
- inputSchema: z4.object({
3932
- groupId: z4.string().uuid().describe("Policy group ID"),
3933
- versionId: z4.string().uuid().describe("Version ID")
4263
+ inputSchema: z6.object({
4264
+ groupId: z6.string().uuid().describe("Policy group ID"),
4265
+ versionId: z6.string().uuid().describe("Version ID")
3934
4266
  })
3935
4267
  },
3936
4268
  async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/rules`)
@@ -3941,10 +4273,10 @@ function registerRuleTools(server, callApi) {
3941
4273
  title: "Get Rule Detail",
3942
4274
  annotations: { readOnlyHint: true },
3943
4275
  description: "Get full rule detail including condition tree and action definitions.",
3944
- inputSchema: z4.object({
3945
- groupId: z4.string().uuid().describe("Policy group ID"),
3946
- versionId: z4.string().uuid().describe("Version ID"),
3947
- ruleId: z4.string().uuid().describe("Rule ID")
4276
+ inputSchema: z6.object({
4277
+ groupId: z6.string().uuid().describe("Policy group ID"),
4278
+ versionId: z6.string().uuid().describe("Version ID"),
4279
+ ruleId: z6.string().uuid().describe("Rule ID")
3948
4280
  })
3949
4281
  },
3950
4282
  async ({ groupId, versionId, ruleId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`)
@@ -3954,7 +4286,7 @@ function registerRuleTools(server, callApi) {
3954
4286
  {
3955
4287
  title: "Create Rule",
3956
4288
  annotations: { readOnlyHint: false, destructiveHint: false },
3957
- description: dedent15`
4289
+ description: dedent17`
3958
4290
  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
4291
 
3960
4292
  Before creating rules with new fact keys, call lexq_facts_list to check existing facts.
@@ -4035,10 +4367,10 @@ function registerRuleTools(server, callApi) {
4035
4367
 
4036
4368
  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).
4037
4369
  `,
4038
- inputSchema: z4.object({
4039
- groupId: z4.string().uuid().describe("Policy group ID"),
4040
- versionId: z4.string().uuid().describe("Version ID"),
4041
- rule: z4.string().describe(
4370
+ inputSchema: z6.object({
4371
+ groupId: z6.string().uuid().describe("Policy group ID"),
4372
+ versionId: z6.string().uuid().describe("Version ID"),
4373
+ rule: z6.string().describe(
4042
4374
  "JSON string of CreateRuleRequest: { name, condition, actions, mutexGroup?, mutexMode?, mutexStrategy?, mutexLimit?, isEnabled? }"
4043
4375
  )
4044
4376
  })
@@ -4054,11 +4386,11 @@ function registerRuleTools(server, callApi) {
4054
4386
  title: "Update Rule",
4055
4387
  annotations: { readOnlyHint: false, destructiveHint: true },
4056
4388
  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: z4.object({
4058
- groupId: z4.string().uuid().describe("Policy group ID"),
4059
- versionId: z4.string().uuid().describe("Version ID"),
4060
- ruleId: z4.string().uuid().describe("Rule ID"),
4061
- rule: z4.string().describe(
4389
+ inputSchema: z6.object({
4390
+ groupId: z6.string().uuid().describe("Policy group ID"),
4391
+ versionId: z6.string().uuid().describe("Version ID"),
4392
+ ruleId: z6.string().uuid().describe("Rule ID"),
4393
+ rule: z6.string().describe(
4062
4394
  "JSON string of UpdateRuleRequest: { name?, condition?, actions?, mutexGroup?, mutexMode?, mutexStrategy?, mutexLimit?, isEnabled? }"
4063
4395
  )
4064
4396
  })
@@ -4076,11 +4408,11 @@ function registerRuleTools(server, callApi) {
4076
4408
  title: "Delete Rule",
4077
4409
  annotations: { readOnlyHint: false, destructiveHint: true },
4078
4410
  description: "Delete a rule from a DRAFT version.",
4079
- inputSchema: z4.object({
4080
- groupId: z4.string().uuid().describe("Policy group ID"),
4081
- versionId: z4.string().uuid().describe("Version ID"),
4082
- ruleId: z4.string().uuid().describe("Rule ID"),
4083
- force: z4.boolean().default(false).describe("Skip confirmation (for the last rule in a version)")
4411
+ inputSchema: z6.object({
4412
+ groupId: z6.string().uuid().describe("Policy group ID"),
4413
+ versionId: z6.string().uuid().describe("Version ID"),
4414
+ ruleId: z6.string().uuid().describe("Rule ID"),
4415
+ force: z6.boolean().default(false).describe("Skip confirmation (for the last rule in a version)")
4084
4416
  })
4085
4417
  },
4086
4418
  async ({ groupId, versionId, ruleId, force }) => {
@@ -4097,10 +4429,10 @@ function registerRuleTools(server, callApi) {
4097
4429
  title: "Reorder Rules",
4098
4430
  annotations: { readOnlyHint: false, destructiveHint: true },
4099
4431
  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: z4.object({
4101
- groupId: z4.string().uuid().describe("Policy group ID"),
4102
- versionId: z4.string().uuid().describe("Version ID"),
4103
- ruleIds: z4.array(z4.string().uuid()).describe("Rule IDs in desired priority order")
4432
+ inputSchema: z6.object({
4433
+ groupId: z6.string().uuid().describe("Policy group ID"),
4434
+ versionId: z6.string().uuid().describe("Version ID"),
4435
+ ruleIds: z6.array(z6.string().uuid()).describe("Rule IDs in desired priority order")
4104
4436
  })
4105
4437
  },
4106
4438
  async ({ groupId, versionId, ruleIds }) => {
@@ -4119,11 +4451,11 @@ function registerRuleTools(server, callApi) {
4119
4451
  title: "Toggle Rule",
4120
4452
  annotations: { readOnlyHint: false, destructiveHint: true },
4121
4453
  description: "Enable or disable a rule without deleting it.",
4122
- inputSchema: z4.object({
4123
- groupId: z4.string().uuid().describe("Policy group ID"),
4124
- versionId: z4.string().uuid().describe("Version ID"),
4125
- ruleId: z4.string().uuid().describe("Rule ID"),
4126
- isEnabled: z4.boolean().describe("true to enable, false to disable")
4454
+ inputSchema: z6.object({
4455
+ groupId: z6.string().uuid().describe("Policy group ID"),
4456
+ versionId: z6.string().uuid().describe("Version ID"),
4457
+ ruleId: z6.string().uuid().describe("Rule ID"),
4458
+ isEnabled: z6.boolean().describe("true to enable, false to disable")
4127
4459
  })
4128
4460
  },
4129
4461
  async ({ groupId, versionId, ruleId, isEnabled }) => callApi("PATCH", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}/enabled`, {
@@ -4133,7 +4465,7 @@ function registerRuleTools(server, callApi) {
4133
4465
  }
4134
4466
 
4135
4467
  // src/mcp/tools/facts.ts
4136
- import { z as z5 } from "zod";
4468
+ import { z as z7 } from "zod";
4137
4469
 
4138
4470
  // src/types/facts.ts
4139
4471
  var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
@@ -4147,10 +4479,10 @@ function registerFactTools(server, callApi) {
4147
4479
  title: "List Fact Definitions",
4148
4480
  annotations: { readOnlyHint: true },
4149
4481
  description: "List all fact definitions (input variable schema). Shows key, type, required, and PII status. Always check this before creating rules.",
4150
- inputSchema: z5.object({
4151
- page: z5.number().int().min(0).default(0).describe("Page number"),
4152
- size: z5.number().int().min(1).max(100).default(20).describe("Page size"),
4153
- keyword: z5.string().optional().describe("Search keyword")
4482
+ inputSchema: z7.object({
4483
+ page: z7.number().int().min(0).default(0).describe("Page number"),
4484
+ size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
4485
+ keyword: z7.string().optional().describe("Search keyword")
4154
4486
  })
4155
4487
  },
4156
4488
  async ({ page, size, keyword }) => {
@@ -4165,21 +4497,21 @@ function registerFactTools(server, callApi) {
4165
4497
  title: "Create Fact Definition",
4166
4498
  annotations: { readOnlyHint: false, destructiveHint: false },
4167
4499
  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.",
4168
- inputSchema: z5.object({
4169
- key: z5.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
4170
- name: z5.string().describe("Display name"),
4171
- type: z5.enum(VALUE_TYPES).describe("Value type"),
4172
- description: z5.string().optional().describe("Description"),
4173
- isRequired: z5.boolean().default(false).describe("Whether this fact is required for rule evaluation"),
4174
- isPii: z5.boolean().default(false).describe(
4500
+ inputSchema: z7.object({
4501
+ key: z7.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
4502
+ name: z7.string().describe("Display name"),
4503
+ type: z7.enum(VALUE_TYPES).describe("Value type"),
4504
+ description: z7.string().optional().describe("Description"),
4505
+ isRequired: z7.boolean().default(false).describe("Whether this fact is required for rule evaluation"),
4506
+ isPii: z7.boolean().default(false).describe(
4175
4507
  "Mark as PII \u2014 masked on every read surface, revealable only in the console (audited)"
4176
4508
  ),
4177
- valueDomain: z5.object({
4178
- allowedValues: z5.array(z5.union([z5.string(), z5.number()])).optional().describe(
4509
+ valueDomain: z7.object({
4510
+ allowedValues: z7.array(z7.union([z7.string(), z7.number()])).optional().describe(
4179
4511
  "Accepted values. For list types the value domain applies to each element, not to the list as a whole."
4180
4512
  ),
4181
- min: z5.number().optional().describe("Lower bound, inclusive. Numeric types only."),
4182
- max: z5.number().optional().describe("Upper bound, inclusive. Numeric types only.")
4513
+ min: z7.number().optional().describe("Lower bound, inclusive. Numeric types only."),
4514
+ max: z7.number().optional().describe("Upper bound, inclusive. Numeric types only.")
4183
4515
  }).optional().describe(
4184
4516
  "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
4517
  )
@@ -4193,21 +4525,21 @@ function registerFactTools(server, callApi) {
4193
4525
  title: "Update Fact Definition",
4194
4526
  annotations: { readOnlyHint: false, destructiveHint: true },
4195
4527
  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: z5.object({
4197
- factId: z5.string().uuid().describe("Fact definition ID"),
4198
- name: z5.string().optional().describe("Display name"),
4199
- description: z5.string().optional().describe('Description. Send "" to remove it.'),
4200
- type: z5.enum(VALUE_TYPES).optional().describe(
4528
+ inputSchema: z7.object({
4529
+ factId: z7.string().uuid().describe("Fact definition ID"),
4530
+ name: z7.string().optional().describe("Display name"),
4531
+ description: z7.string().optional().describe('Description. Send "" to remove it.'),
4532
+ type: z7.enum(VALUE_TYPES).optional().describe(
4201
4533
  "Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
4202
4534
  ),
4203
- isRequired: z5.boolean().optional().describe("Required flag"),
4204
- isPii: z5.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
4205
- valueDomain: z5.object({
4206
- allowedValues: z5.array(z5.union([z5.string(), z5.number()])).optional().describe(
4535
+ isRequired: z7.boolean().optional().describe("Required flag"),
4536
+ isPii: z7.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
4537
+ valueDomain: z7.object({
4538
+ allowedValues: z7.array(z7.union([z7.string(), z7.number()])).optional().describe(
4207
4539
  "Accepted values. For list types the value domain applies to each element, not to the list as a whole."
4208
4540
  ),
4209
- min: z5.number().optional().describe("Lower bound, inclusive. Numeric types only."),
4210
- max: z5.number().optional().describe("Upper bound, inclusive. Numeric types only.")
4541
+ min: z7.number().optional().describe("Lower bound, inclusive. Numeric types only."),
4542
+ max: z7.number().optional().describe("Upper bound, inclusive. Numeric types only.")
4211
4543
  }).optional().describe(
4212
4544
  "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
4545
  )
@@ -4221,8 +4553,8 @@ function registerFactTools(server, callApi) {
4221
4553
  title: "Delete Fact Definition",
4222
4554
  annotations: { readOnlyHint: false, destructiveHint: true },
4223
4555
  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: z5.object({
4225
- factId: z5.string().uuid().describe("Fact definition ID")
4556
+ inputSchema: z7.object({
4557
+ factId: z7.string().uuid().describe("Fact definition ID")
4226
4558
  })
4227
4559
  },
4228
4560
  async ({ factId }) => callApi("DELETE", `schema/facts/${factId}`)
@@ -4233,7 +4565,7 @@ function registerFactTools(server, callApi) {
4233
4565
  title: "Get Action Runtime Fact Metadata",
4234
4566
  annotations: { readOnlyHint: true },
4235
4567
  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: z5.object({})
4568
+ inputSchema: z7.object({})
4237
4569
  },
4238
4570
  async () => callApi("GET", "schema/action-metadata")
4239
4571
  );
@@ -4243,9 +4575,9 @@ function registerFactTools(server, callApi) {
4243
4575
  title: "List Unregistered Facts",
4244
4576
  annotations: { readOnlyHint: true },
4245
4577
  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: z5.object({
4247
- groupId: z5.string().uuid().describe("Policy group ID"),
4248
- versionId: z5.string().uuid().describe("Version ID")
4578
+ inputSchema: z7.object({
4579
+ groupId: z7.string().uuid().describe("Policy group ID"),
4580
+ versionId: z7.string().uuid().describe("Version ID")
4249
4581
  })
4250
4582
  },
4251
4583
  async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
@@ -4256,9 +4588,9 @@ function registerFactTools(server, callApi) {
4256
4588
  title: "Export Fact Catalog",
4257
4589
  annotations: { readOnlyHint: true },
4258
4590
  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: z5.object({
4260
- format: z5.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
4261
- keyword: z5.string().optional().describe("Filter by key or name")
4591
+ inputSchema: z7.object({
4592
+ format: z7.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
4593
+ keyword: z7.string().optional().describe("Filter by key or name")
4262
4594
  })
4263
4595
  },
4264
4596
  async ({ format, keyword }) => callApi("GET", "schema/facts/export", {
@@ -4268,8 +4600,8 @@ function registerFactTools(server, callApi) {
4268
4600
  }
4269
4601
 
4270
4602
  // src/mcp/tools/deploy.ts
4271
- import dedent16 from "dedent";
4272
- import { z as z6 } from "zod";
4603
+ import dedent18 from "dedent";
4604
+ import { z as z8 } from "zod";
4273
4605
  function registerDeployTools(server, callApi) {
4274
4606
  server.registerTool(
4275
4607
  "lexq_deploy_publish",
@@ -4277,10 +4609,10 @@ function registerDeployTools(server, callApi) {
4277
4609
  title: "Publish Version",
4278
4610
  annotations: { readOnlyHint: false, destructiveHint: false },
4279
4611
  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: z6.object({
4281
- groupId: z6.string().uuid().describe("Policy group ID"),
4282
- versionId: z6.string().uuid().describe("Version ID to publish"),
4283
- memo: z6.string().min(1).describe("Publish memo (required)")
4612
+ inputSchema: z8.object({
4613
+ groupId: z8.string().uuid().describe("Policy group ID"),
4614
+ versionId: z8.string().uuid().describe("Version ID to publish"),
4615
+ memo: z8.string().min(1).describe("Publish memo (required)")
4284
4616
  })
4285
4617
  },
4286
4618
  async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/versions/${versionId}/publish`, { body: { memo } })
@@ -4291,10 +4623,10 @@ function registerDeployTools(server, callApi) {
4291
4623
  title: "Deploy to Live",
4292
4624
  annotations: { readOnlyHint: false, destructiveHint: true },
4293
4625
  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: z6.object({
4295
- groupId: z6.string().uuid().describe("Policy group ID"),
4296
- versionId: z6.string().uuid().describe("Version ID to deploy"),
4297
- memo: z6.string().min(1).describe("Deployment memo (required)")
4626
+ inputSchema: z8.object({
4627
+ groupId: z8.string().uuid().describe("Policy group ID"),
4628
+ versionId: z8.string().uuid().describe("Version ID to deploy"),
4629
+ memo: z8.string().min(1).describe("Deployment memo (required)")
4298
4630
  })
4299
4631
  },
4300
4632
  async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/deploy`, {
@@ -4306,16 +4638,16 @@ function registerDeployTools(server, callApi) {
4306
4638
  {
4307
4639
  title: "Rollback Deployment",
4308
4640
  annotations: { readOnlyHint: false, destructiveHint: true },
4309
- description: dedent16`
4641
+ description: dedent18`
4310
4642
  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
4643
 
4312
4644
  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
4645
  `,
4314
- inputSchema: z6.object({
4315
- groupId: z6.string().uuid().describe("Policy group ID"),
4316
- expectedCurrentVersionId: z6.string().uuid().describe("currentVersionId from your last read of the group"),
4317
- expectedCurrentDeploymentId: z6.string().uuid().describe("currentDeploymentId from your last read of the group"),
4318
- memo: z6.string().min(1).describe("Rollback reason (required)")
4646
+ inputSchema: z8.object({
4647
+ groupId: z8.string().uuid().describe("Policy group ID"),
4648
+ expectedCurrentVersionId: z8.string().uuid().describe("currentVersionId from your last read of the group"),
4649
+ expectedCurrentDeploymentId: z8.string().uuid().describe("currentDeploymentId from your last read of the group"),
4650
+ memo: z8.string().min(1).describe("Rollback reason (required)")
4319
4651
  })
4320
4652
  },
4321
4653
  async ({ groupId, expectedCurrentVersionId, expectedCurrentDeploymentId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
@@ -4328,372 +4660,133 @@ function registerDeployTools(server, callApi) {
4328
4660
  title: "Undeploy",
4329
4661
  annotations: { readOnlyHint: false, destructiveHint: true },
4330
4662
  description: "Remove the live version from traffic. The version stays ACTIVE but no longer serves requests.",
4331
- inputSchema: z6.object({
4332
- groupId: z6.string().uuid().describe("Policy group ID"),
4333
- memo: z6.string().min(1).describe("Undeploy reason (required)")
4663
+ inputSchema: z8.object({
4664
+ groupId: z8.string().uuid().describe("Policy group ID"),
4665
+ memo: z8.string().min(1).describe("Undeploy reason (required)")
4334
4666
  })
4335
4667
  },
4336
4668
  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
- }
4669
+ body: { memo }
4670
+ })
4514
4671
  );
4515
4672
  server.registerTool(
4516
- "lexq_requirements",
4673
+ "lexq_deploy_schedule",
4517
4674
  {
4518
- title: "Analyze Requirements",
4519
- annotations: { readOnlyHint: true },
4520
- description: "Analyze which input facts a version requires. Returns required keys, types, and an example request body.",
4521
- inputSchema: z7.object({
4522
- groupId: z7.string().uuid().describe("Policy group ID"),
4523
- versionId: z7.string().uuid().describe("Version ID")
4675
+ title: "Schedule Deployment",
4676
+ annotations: { readOnlyHint: false, destructiveHint: false },
4677
+ 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).",
4678
+ inputSchema: z8.object({
4679
+ groupId: z8.string().uuid().describe("Policy group ID"),
4680
+ versionId: z8.string().uuid().describe("ACTIVE version ID with a future effective start date"),
4681
+ memo: z8.string().min(1).describe("Schedule memo (required)")
4524
4682
  })
4525
4683
  },
4526
- async ({ groupId, versionId }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/requirements`)
4684
+ async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/schedule`, {
4685
+ body: { versionId, memo }
4686
+ })
4527
4687
  );
4528
4688
  server.registerTool(
4529
- "lexq_simulation_start",
4689
+ "lexq_deploy_unschedule",
4530
4690
  {
4531
- title: "Start Simulation",
4532
- annotations: { readOnlyHint: false, destructiveHint: false },
4533
- description: dedent17`
4534
- Start an Impact Simulation against historical, uploaded, or inline data.
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")
4691
+ title: "Cancel Scheduled Deployment",
4692
+ annotations: { readOnlyHint: false, destructiveHint: true },
4693
+ description: "Cancel the pending scheduled deployment for a group. The version itself is not affected. Fails with P-039 if no pending schedule exists.",
4694
+ inputSchema: z8.object({
4695
+ groupId: z8.string().uuid().describe("Policy group ID")
4565
4696
  })
4566
4697
  },
4567
- async ({ body }) => {
4568
- const parsed = parseJson(body);
4569
- return callApi("POST", "analytics/simulations", { body: parsed });
4570
- }
4698
+ async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}/schedule`)
4571
4699
  );
4572
4700
  server.registerTool(
4573
- "lexq_simulation_status",
4701
+ "lexq_deploy_schedules",
4574
4702
  {
4575
- title: "Simulation Status",
4703
+ title: "List Scheduled Deployments",
4576
4704
  annotations: { readOnlyHint: true },
4577
- description: "Get simulation status and results. Poll until status is COMPLETED or FAILED.",
4578
- inputSchema: z7.object({
4579
- simulationId: z7.string().uuid().describe("Simulation ID")
4705
+ description: "List scheduled deployments across all groups (all statuses: PENDING, EXECUTED, CANCELED, FAILED), newest first.",
4706
+ inputSchema: z8.object({
4707
+ page: z8.number().int().min(0).default(0).describe("Page number"),
4708
+ size: z8.number().int().min(1).max(100).default(20).describe("Page size")
4580
4709
  })
4581
4710
  },
4582
- async ({ simulationId }) => callApi("GET", `analytics/simulations/${simulationId}`)
4711
+ async ({ page, size }) => callApi("GET", "policy-groups/schedules", { params: paginationParams(page, size) })
4583
4712
  );
4584
4713
  server.registerTool(
4585
- "lexq_simulation_list",
4714
+ "lexq_deploy_history",
4586
4715
  {
4587
- title: "List Simulations",
4716
+ title: "Deployment History",
4588
4717
  annotations: { readOnlyHint: true },
4589
- description: "List simulation history with optional filters.",
4590
- inputSchema: z7.object({
4591
- page: z7.number().int().min(0).default(0).describe("Page number"),
4592
- size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
4593
- status: z7.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"]).optional().describe("Filter by status"),
4594
- from: z7.string().optional().describe(START_DAY),
4595
- to: z7.string().optional().describe(END_DAY)
4718
+ description: "List deployment history across all groups.",
4719
+ inputSchema: z8.object({
4720
+ page: z8.number().int().min(0).default(0).describe("Page number"),
4721
+ size: z8.number().int().min(1).max(100).default(20).describe("Page size"),
4722
+ groupId: z8.string().uuid().optional().describe("Filter by group ID"),
4723
+ types: z8.string().optional().describe("Filter by deployment types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)"),
4724
+ startDate: z8.string().optional().describe(START_DAY),
4725
+ endDate: z8.string().optional().describe(END_DAY)
4596
4726
  })
4597
4727
  },
4598
- async ({ page, size, status, from, to }) => {
4728
+ async ({ page, size, groupId, types, startDate, endDate }) => {
4599
4729
  const params = paginationParams(page, size);
4600
- if (status) params.status = status;
4601
- if (from) params.from = from;
4602
- if (to) params.to = to;
4603
- return callApi("GET", "analytics/simulations", { params });
4730
+ if (groupId) params.groupId = groupId;
4731
+ if (types) params.types = types;
4732
+ if (startDate) params.startDate = startDate;
4733
+ if (endDate) params.endDate = endDate;
4734
+ return callApi("GET", "deployments", { params });
4604
4735
  }
4605
4736
  );
4606
4737
  server.registerTool(
4607
- "lexq_simulation_cancel",
4738
+ "lexq_deploy_detail",
4608
4739
  {
4609
- title: "Cancel Simulation",
4610
- annotations: { readOnlyHint: false, destructiveHint: true },
4611
- description: "Cancel a running or pending simulation.",
4612
- inputSchema: z7.object({
4613
- simulationId: z7.string().uuid().describe("Simulation ID")
4740
+ title: "Deployment Detail",
4741
+ annotations: { readOnlyHint: true },
4742
+ description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
4743
+ inputSchema: z8.object({
4744
+ deploymentId: z8.string().uuid().describe("Deployment ID")
4614
4745
  })
4615
4746
  },
4616
- async ({ simulationId }) => callApi("DELETE", `analytics/simulations/${simulationId}`)
4747
+ async ({ deploymentId }) => callApi("GET", `deployments/${deploymentId}`)
4617
4748
  );
4618
4749
  server.registerTool(
4619
- "lexq_simulation_export",
4750
+ "lexq_deploy_overview",
4620
4751
  {
4621
- title: "Export Simulation",
4752
+ title: "Deployment Overview",
4622
4753
  annotations: { readOnlyHint: true },
4623
- description: "Export simulation results as JSON or CSV. Returns the raw data.",
4624
- inputSchema: z7.object({
4625
- simulationId: z7.string().uuid().describe("Simulation ID"),
4626
- format: z7.enum(EXPORT_FORMATS).default("json").describe("Export format")
4627
- })
4754
+ 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.",
4755
+ inputSchema: z8.object({})
4628
4756
  },
4629
- async ({ simulationId, format }) => callApi("GET", `analytics/simulations/${simulationId}/export`, {
4630
- params: { format }
4631
- })
4757
+ async () => callApi("GET", "deployments/overview")
4632
4758
  );
4633
4759
  server.registerTool(
4634
- "lexq_dataset_upload",
4760
+ "lexq_deploy_deployable",
4635
4761
  {
4636
- title: "Upload Dataset",
4637
- annotations: { readOnlyHint: false, destructiveHint: false },
4638
- description: dedent17`
4639
- Upload inline CSV or JSON content as a simulation dataset.
4640
- The content is uploaded to S3 and a path is returned in the "path" field.
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)")
4762
+ title: "List Deployable Versions",
4763
+ annotations: { readOnlyHint: true },
4764
+ description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
4765
+ inputSchema: z8.object({
4766
+ groupId: z8.string().uuid().describe("Policy group ID")
4655
4767
  })
4656
4768
  },
4657
- async ({ content, filename }) => {
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
- }
4769
+ async ({ groupId }) => callApi("GET", `deployments/groups/${groupId}/deployable-versions`)
4676
4770
  );
4677
4771
  server.registerTool(
4678
- "lexq_dataset_template",
4772
+ "lexq_deploy_diff",
4679
4773
  {
4680
- title: "Download Dataset Template",
4774
+ title: "Deployment Diff",
4681
4775
  annotations: { readOnlyHint: true },
4682
- 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.",
4683
- inputSchema: z7.object({
4684
- groupId: z7.string().uuid().describe("Policy group ID"),
4685
- versionId: z7.string().uuid().describe("Version ID"),
4686
- format: z7.enum(EXPORT_FORMATS).default("csv").describe("Template format")
4776
+ description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
4777
+ inputSchema: z8.object({
4778
+ baseVersionId: z8.string().uuid().describe("Base version ID (typically the current live)"),
4779
+ targetVersionId: z8.string().uuid().describe("Target version ID (the one you want to deploy)")
4687
4780
  })
4688
4781
  },
4689
- async ({ groupId, versionId, format }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/dataset-template`, {
4690
- params: { format }
4782
+ async ({ baseVersionId, targetVersionId }) => callApi("GET", "deployments/diff", {
4783
+ params: { baseVersionId, targetVersionId }
4691
4784
  })
4692
4785
  );
4693
4786
  }
4694
4787
 
4695
4788
  // src/mcp/tools/profile.ts
4696
- import { z as z8 } from "zod";
4789
+ import { z as z9 } from "zod";
4697
4790
  var RELATIVE_THRESHOLD = `flagged = p50 \u2265 ${SLOW_MULTIPLIER}\xD7 median of per-rule p50s within the group; absolute thresholds are intentionally not supported.`;
4698
4791
  function profileParams(opts) {
4699
4792
  const params = {};
@@ -4710,14 +4803,14 @@ function registerProfileTools(server, callApi) {
4710
4803
  title: "Group Latency Profile",
4711
4804
  annotations: { readOnlyHint: true },
4712
4805
  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.`,
4713
- inputSchema: z8.object({
4714
- groupId: z8.string().uuid().describe("Policy group ID"),
4715
- versionId: z8.string().uuid().optional().describe("Version to inspect (default: live version)"),
4716
- from: z8.string().optional().describe(
4806
+ inputSchema: z9.object({
4807
+ groupId: z9.string().uuid().describe("Policy group ID"),
4808
+ versionId: z9.string().uuid().optional().describe("Version to inspect (default: live version)"),
4809
+ from: z9.string().optional().describe(
4717
4810
  `Window start, ISO-8601 instant (e.g. 2026-07-01T00:00:00Z). Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
4718
4811
  ),
4719
- to: z8.string().optional().describe("Window end, ISO-8601 instant. Default: now"),
4720
- cacheState: z8.enum(["HIT", "MISS"]).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
4812
+ to: z9.string().optional().describe("Window end, ISO-8601 instant. Default: now"),
4813
+ cacheState: z9.enum(["HIT", "MISS"]).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
4721
4814
  })
4722
4815
  },
4723
4816
  async ({ groupId, versionId, from, to, cacheState }) => callApi("GET", `policy-groups/${groupId}/profile`, {
@@ -4730,14 +4823,14 @@ function registerProfileTools(server, callApi) {
4730
4823
  title: "Rule Latency Detail",
4731
4824
  annotations: { readOnlyHint: true },
4732
4825
  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,
4733
- inputSchema: z8.object({
4734
- groupId: z8.string().uuid().describe("Policy group ID"),
4735
- ruleId: z8.string().uuid().describe("Rule ID (from lexq_profile_overview)"),
4736
- versionId: z8.string().uuid().optional().describe("Version to inspect (default: live version)"),
4737
- from: z8.string().optional().describe(
4826
+ inputSchema: z9.object({
4827
+ groupId: z9.string().uuid().describe("Policy group ID"),
4828
+ ruleId: z9.string().uuid().describe("Rule ID (from lexq_profile_overview)"),
4829
+ versionId: z9.string().uuid().optional().describe("Version to inspect (default: live version)"),
4830
+ from: z9.string().optional().describe(
4738
4831
  `Window start, ISO-8601 instant. Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
4739
4832
  ),
4740
- to: z8.string().optional().describe("Window end, ISO-8601 instant. Default: now")
4833
+ to: z9.string().optional().describe("Window end, ISO-8601 instant. Default: now")
4741
4834
  })
4742
4835
  },
4743
4836
  async ({ groupId, ruleId, versionId, from, to }) => callApi("GET", `policy-groups/${groupId}/profile/rules/${ruleId}`, {
@@ -4747,7 +4840,7 @@ function registerProfileTools(server, callApi) {
4747
4840
  }
4748
4841
 
4749
4842
  // src/mcp/tools/replay.ts
4750
- import { z as z9 } from "zod";
4843
+ import { z as z10 } from "zod";
4751
4844
  function registerReplayTools(server, callApi) {
4752
4845
  server.registerTool(
4753
4846
  "lexq_replay_decision",
@@ -4755,9 +4848,9 @@ function registerReplayTools(server, callApi) {
4755
4848
  title: "Replay a Decision",
4756
4849
  annotations: { readOnlyHint: true },
4757
4850
  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: z9.object({
4759
- traceId: z9.string().describe("Trace ID of the past execution to replay"),
4760
- candidateVersionId: z9.string().uuid().describe("Version to re-evaluate against")
4851
+ inputSchema: z10.object({
4852
+ traceId: z10.string().describe("Trace ID of the past execution to replay"),
4853
+ candidateVersionId: z10.string().uuid().describe("Version to re-evaluate against")
4761
4854
  })
4762
4855
  },
4763
4856
  async ({ traceId, candidateVersionId }) => callApi("POST", "replay/decisions", { body: { traceId, candidateVersionId } })
@@ -4768,11 +4861,11 @@ function registerReplayTools(server, callApi) {
4768
4861
  title: "Start Window Replay (Blast Radius)",
4769
4862
  annotations: { readOnlyHint: false, destructiveHint: false },
4770
4863
  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: z9.object({
4772
- candidateVersionId: z9.string().uuid().describe("Version to re-evaluate against"),
4773
- from: z9.string().describe(WINDOW_START_DAY),
4774
- to: z9.string().describe(WINDOW_END_DAY),
4775
- maxRecords: z9.number().int().min(1).optional().describe(
4864
+ inputSchema: z10.object({
4865
+ candidateVersionId: z10.string().uuid().describe("Version to re-evaluate against"),
4866
+ from: z10.string().describe(WINDOW_START_DAY),
4867
+ to: z10.string().describe(WINDOW_END_DAY),
4868
+ maxRecords: z10.number().int().min(1).optional().describe(
4776
4869
  `Sample cap (server default applies; hard cap ${REPLAY_WINDOW_MAX_RECORDS / 1e3}k)`
4777
4870
  )
4778
4871
  })
@@ -4785,8 +4878,8 @@ function registerReplayTools(server, callApi) {
4785
4878
  title: "Get Replay Job Status",
4786
4879
  annotations: { readOnlyHint: true },
4787
4880
  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: z9.object({
4789
- jobId: z9.string().describe("Replay job ID from lexq_replay_start")
4881
+ inputSchema: z10.object({
4882
+ jobId: z10.string().describe("Replay job ID from lexq_replay_start")
4790
4883
  })
4791
4884
  },
4792
4885
  async ({ jobId }) => callApi("GET", `replay/jobs/${jobId}`)
@@ -4797,9 +4890,9 @@ function registerReplayTools(server, callApi) {
4797
4890
  title: "List Replay Jobs",
4798
4891
  annotations: { readOnlyHint: true },
4799
4892
  description: "List window replay job history (reverse-chronological). Lightweight items \u2014 use lexq_replay_status for summary and changed samples.",
4800
- inputSchema: z9.object({
4801
- page: z9.number().int().min(0).default(0).describe("Page number"),
4802
- size: z9.number().int().min(1).max(100).default(20).describe("Page size")
4893
+ inputSchema: z10.object({
4894
+ page: z10.number().int().min(0).default(0).describe("Page number"),
4895
+ size: z10.number().int().min(1).max(100).default(20).describe("Page size")
4803
4896
  })
4804
4897
  },
4805
4898
  async ({ page, size }) => callApi("GET", "replay/jobs", { params: paginationParams(page, size) })
@@ -4810,8 +4903,8 @@ function registerReplayTools(server, callApi) {
4810
4903
  title: "Cancel Replay Job",
4811
4904
  annotations: { readOnlyHint: false, destructiveHint: true },
4812
4905
  description: "Cooperatively cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
4813
- inputSchema: z9.object({
4814
- jobId: z9.string().describe("Replay job ID")
4906
+ inputSchema: z10.object({
4907
+ jobId: z10.string().describe("Replay job ID")
4815
4908
  })
4816
4909
  },
4817
4910
  async ({ jobId }) => callApi("POST", `replay/jobs/${jobId}/cancel`)
@@ -4822,9 +4915,9 @@ function registerReplayTools(server, callApi) {
4822
4915
  title: "Export Replay Result",
4823
4916
  annotations: { readOnlyHint: true },
4824
4917
  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: z9.object({
4826
- jobId: z9.string().describe("Replay job ID"),
4827
- format: z9.enum(EXPORT_FORMATS).default("json").describe("Export format")
4918
+ inputSchema: z10.object({
4919
+ jobId: z10.string().describe("Replay job ID"),
4920
+ format: z10.enum(EXPORT_FORMATS).default("json").describe("Export format")
4828
4921
  })
4829
4922
  },
4830
4923
  async ({ jobId, format }) => callApi("GET", `replay/jobs/${jobId}/export`, { params: { format } })
@@ -4832,7 +4925,7 @@ function registerReplayTools(server, callApi) {
4832
4925
  }
4833
4926
 
4834
4927
  // src/mcp/tools/history.ts
4835
- import { z as z10 } from "zod";
4928
+ import { z as z11 } from "zod";
4836
4929
  function registerHistoryTools(server, callApi) {
4837
4930
  server.registerTool(
4838
4931
  "lexq_history_list",
@@ -4840,15 +4933,15 @@ function registerHistoryTools(server, callApi) {
4840
4933
  title: "List Execution History",
4841
4934
  annotations: { readOnlyHint: true },
4842
4935
  description: "List policy execution history. Shows trace ID, group, version, status, match result, and latency.",
4843
- inputSchema: z10.object({
4844
- page: z10.number().int().min(0).default(0).describe("Page number"),
4845
- size: z10.number().int().min(1).max(100).default(20).describe("Page size"),
4846
- traceId: z10.string().optional().describe("Filter by trace ID"),
4847
- groupId: z10.string().uuid().optional().describe("Filter by policy group"),
4848
- versionId: z10.string().uuid().optional().describe("Filter by version"),
4849
- status: z10.enum(["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"]).optional().describe("Filter by execution status"),
4850
- startDate: z10.string().optional().describe(START_DAY),
4851
- endDate: z10.string().optional().describe(END_DAY)
4936
+ inputSchema: z11.object({
4937
+ page: z11.number().int().min(0).default(0).describe("Page number"),
4938
+ size: z11.number().int().min(1).max(100).default(20).describe("Page size"),
4939
+ traceId: z11.string().optional().describe("Filter by trace ID"),
4940
+ groupId: z11.string().uuid().optional().describe("Filter by policy group"),
4941
+ versionId: z11.string().uuid().optional().describe("Filter by version"),
4942
+ status: z11.enum(["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"]).optional().describe("Filter by execution status"),
4943
+ startDate: z11.string().optional().describe(START_DAY),
4944
+ endDate: z11.string().optional().describe(END_DAY)
4852
4945
  })
4853
4946
  },
4854
4947
  async ({ page, size, traceId, groupId, versionId, status, startDate, endDate }) => {
@@ -4868,8 +4961,8 @@ function registerHistoryTools(server, callApi) {
4868
4961
  title: "Get Execution Detail",
4869
4962
  annotations: { readOnlyHint: true },
4870
4963
  description: "Get full execution detail including inputFacts, mutatedFacts, generatedVariables, executionTraces, and decisionTraces.",
4871
- inputSchema: z10.object({
4872
- traceId: z10.string().describe("Trace ID from execution history")
4964
+ inputSchema: z11.object({
4965
+ traceId: z11.string().describe("Trace ID from execution history")
4873
4966
  })
4874
4967
  },
4875
4968
  async ({ traceId }) => callApi("GET", `execution/history/${traceId}`)
@@ -4880,10 +4973,10 @@ function registerHistoryTools(server, callApi) {
4880
4973
  title: "Execution Statistics",
4881
4974
  annotations: { readOnlyHint: true },
4882
4975
  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: z10.object({
4884
- groupId: z10.string().uuid().optional().describe("Filter by policy group"),
4885
- startDate: z10.string().optional().describe(START_DAY),
4886
- endDate: z10.string().optional().describe(END_DAY)
4976
+ inputSchema: z11.object({
4977
+ groupId: z11.string().uuid().optional().describe("Filter by policy group"),
4978
+ startDate: z11.string().optional().describe(START_DAY),
4979
+ endDate: z11.string().optional().describe(END_DAY)
4887
4980
  })
4888
4981
  },
4889
4982
  async ({ groupId, startDate, endDate }) => {
@@ -4897,7 +4990,7 @@ function registerHistoryTools(server, callApi) {
4897
4990
  }
4898
4991
 
4899
4992
  // src/mcp/tools/provenance.ts
4900
- import { z as z11 } from "zod";
4993
+ import { z as z12 } from "zod";
4901
4994
  function registerProvenanceTools(server, callApi) {
4902
4995
  server.registerTool(
4903
4996
  "lexq_provenance_get",
@@ -4905,8 +4998,8 @@ function registerProvenanceTools(server, callApi) {
4905
4998
  title: "Get Decision Provenance",
4906
4999
  annotations: { readOnlyHint: true },
4907
5000
  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: z11.object({
4909
- traceId: z11.string().describe("Trace ID of the execution")
5001
+ inputSchema: z12.object({
5002
+ traceId: z12.string().describe("Trace ID of the execution")
4910
5003
  })
4911
5004
  },
4912
5005
  async ({ traceId }) => callApi("GET", `provenance/${traceId}`)
@@ -4917,14 +5010,14 @@ function registerProvenanceTools(server, callApi) {
4917
5010
  title: "List PII Reveal Audits",
4918
5011
  annotations: { readOnlyHint: true },
4919
5012
  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: z11.object({
4921
- page: z11.number().int().min(0).default(0).describe("Page number"),
4922
- size: z11.number().int().min(1).max(100).default(20).describe("Page size"),
4923
- traceId: z11.string().optional().describe("Filter by trace ID (exact match)"),
4924
- revealedBy: z11.string().optional().describe("Filter by operator ID (exact match)"),
4925
- factKey: z11.string().optional().describe("Filter by fact key (partial match, case-insensitive)"),
4926
- startDate: z11.string().optional().describe(START_DAY),
4927
- endDate: z11.string().optional().describe(END_DAY)
5013
+ inputSchema: z12.object({
5014
+ page: z12.number().int().min(0).default(0).describe("Page number"),
5015
+ size: z12.number().int().min(1).max(100).default(20).describe("Page size"),
5016
+ traceId: z12.string().optional().describe("Filter by trace ID (exact match)"),
5017
+ revealedBy: z12.string().optional().describe("Filter by operator ID (exact match)"),
5018
+ factKey: z12.string().optional().describe("Filter by fact key (partial match, case-insensitive)"),
5019
+ startDate: z12.string().optional().describe(START_DAY),
5020
+ endDate: z12.string().optional().describe(END_DAY)
4928
5021
  })
4929
5022
  },
4930
5023
  async ({ page, size, traceId, revealedBy, factKey, startDate, endDate }) => {
@@ -4940,7 +5033,7 @@ function registerProvenanceTools(server, callApi) {
4940
5033
  }
4941
5034
 
4942
5035
  // src/mcp/tools/logs.ts
4943
- import { z as z12 } from "zod";
5036
+ import { z as z13 } from "zod";
4944
5037
  function registerLogTools(server, callApi) {
4945
5038
  server.registerTool(
4946
5039
  "lexq_logs_list",
@@ -4948,14 +5041,14 @@ function registerLogTools(server, callApi) {
4948
5041
  title: "List Failure Logs",
4949
5042
  annotations: { readOnlyHint: true },
4950
5043
  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: z12.object({
4952
- page: z12.number().int().min(0).default(0).describe("Page number"),
4953
- size: z12.number().int().min(1).max(100).default(20).describe("Page size"),
4954
- taskType: z12.enum(TaskType).optional().describe("Task type"),
4955
- status: z12.enum(FailureStatus).optional().describe("Log status"),
4956
- keyword: z12.string().optional().describe("Search in refId, refSubId, errorMessage"),
4957
- startDate: z12.string().optional().describe(START_DAY),
4958
- endDate: z12.string().optional().describe(END_DAY)
5044
+ inputSchema: z13.object({
5045
+ page: z13.number().int().min(0).default(0).describe("Page number"),
5046
+ size: z13.number().int().min(1).max(100).default(20).describe("Page size"),
5047
+ taskType: z13.enum(TaskType).optional().describe("Task type"),
5048
+ status: z13.enum(FailureStatus).optional().describe("Log status"),
5049
+ keyword: z13.string().optional().describe("Search in refId, refSubId, errorMessage"),
5050
+ startDate: z13.string().optional().describe(START_DAY),
5051
+ endDate: z13.string().optional().describe(END_DAY)
4959
5052
  })
4960
5053
  },
4961
5054
  async ({ page, size, taskType, status, keyword, startDate, endDate }) => {
@@ -4974,8 +5067,8 @@ function registerLogTools(server, callApi) {
4974
5067
  title: "Get Failure Log",
4975
5068
  annotations: { readOnlyHint: true },
4976
5069
  description: "Get failure log detail by ID.",
4977
- inputSchema: z12.object({
4978
- logId: z12.string().uuid().describe("Failure log ID")
5070
+ inputSchema: z13.object({
5071
+ logId: z13.string().uuid().describe("Failure log ID")
4979
5072
  })
4980
5073
  },
4981
5074
  async ({ logId }) => callApi("GET", `failure-logs/${logId}`)
@@ -4986,9 +5079,9 @@ function registerLogTools(server, callApi) {
4986
5079
  title: "Process Failure Log",
4987
5080
  annotations: { readOnlyHint: false, destructiveHint: false },
4988
5081
  description: "Process a single failure log: RESOLVE (mark as manually fixed) or IGNORE (skip intentionally).",
4989
- inputSchema: z12.object({
4990
- logId: z12.string().uuid().describe("Failure log ID"),
4991
- action: z12.enum(FailureAction).describe("Action to take")
5082
+ inputSchema: z13.object({
5083
+ logId: z13.string().uuid().describe("Failure log ID"),
5084
+ action: z13.enum(FailureAction).describe("Action to take")
4992
5085
  })
4993
5086
  },
4994
5087
  async ({ logId, action }) => callApi("POST", `failure-logs/${logId}/actions`, {
@@ -5001,9 +5094,9 @@ function registerLogTools(server, callApi) {
5001
5094
  title: "Bulk Process Failure Logs",
5002
5095
  annotations: { readOnlyHint: false, destructiveHint: false },
5003
5096
  description: "Process multiple failure logs at once. Provide an array of log IDs and the action.",
5004
- inputSchema: z12.object({
5005
- logIds: z12.array(z12.string().uuid()).describe("Array of failure log IDs"),
5006
- action: z12.enum(FailureAction).describe("Action to apply to all logs")
5097
+ inputSchema: z13.object({
5098
+ logIds: z13.array(z13.string().uuid()).describe("Array of failure log IDs"),
5099
+ action: z13.enum(FailureAction).describe("Action to apply to all logs")
5007
5100
  })
5008
5101
  },
5009
5102
  async ({ logIds, action }) => callApi("POST", "failure-logs/bulk-actions", {
@@ -5013,7 +5106,7 @@ function registerLogTools(server, callApi) {
5013
5106
  }
5014
5107
 
5015
5108
  // src/mcp/tools/webhook-subscriptions.ts
5016
- import { z as z13 } from "zod";
5109
+ import { z as z14 } from "zod";
5017
5110
  function registerWebhookSubscriptionTools(server, callApi) {
5018
5111
  server.registerTool(
5019
5112
  "lexq_webhook_subscriptions_list",
@@ -5021,9 +5114,9 @@ function registerWebhookSubscriptionTools(server, callApi) {
5021
5114
  title: "List Webhook Subscriptions",
5022
5115
  annotations: { readOnlyHint: true },
5023
5116
  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: z13.object({
5025
- page: z13.number().int().min(0).default(0).describe("Page number"),
5026
- size: z13.number().int().min(1).max(100).default(20).describe("Page size")
5117
+ inputSchema: z14.object({
5118
+ page: z14.number().int().min(0).default(0).describe("Page number"),
5119
+ size: z14.number().int().min(1).max(100).default(20).describe("Page size")
5027
5120
  })
5028
5121
  },
5029
5122
  async ({ page, size }) => {
@@ -5037,8 +5130,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
5037
5130
  title: "Get Webhook Subscription",
5038
5131
  annotations: { readOnlyHint: true },
5039
5132
  description: "Get webhook subscription detail by ID.",
5040
- inputSchema: z13.object({
5041
- id: z13.string().uuid().describe("Webhook subscription ID")
5133
+ inputSchema: z14.object({
5134
+ id: z14.string().uuid().describe("Webhook subscription ID")
5042
5135
  })
5043
5136
  },
5044
5137
  async ({ id }) => callApi("GET", `webhook-subscriptions/${id}`)
@@ -5049,14 +5142,14 @@ function registerWebhookSubscriptionTools(server, callApi) {
5049
5142
  title: "Save Webhook Subscription",
5050
5143
  annotations: { readOnlyHint: false, destructiveHint: true },
5051
5144
  description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).`,
5052
- inputSchema: z13.object({
5053
- id: z13.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
5054
- name: z13.string().min(1).describe("Subscription name (unique per tenant)"),
5055
- webhookUrl: z13.string().url().describe("Webhook endpoint URL"),
5056
- subscribedEvents: z13.array(z13.enum(PlatformEventType)).min(1).describe("Events to subscribe to"),
5057
- payloadFormat: z13.enum(WebhookPayloadFormat).optional().default("GENERIC").describe("Payload format"),
5058
- secret: z13.string().optional().describe("HMAC-SHA256 signing secret"),
5059
- isActive: z13.boolean().optional().default(true).describe("Whether the subscription is active")
5145
+ inputSchema: z14.object({
5146
+ id: z14.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
5147
+ name: z14.string().min(1).describe("Subscription name (unique per tenant)"),
5148
+ webhookUrl: z14.string().url().describe("Webhook endpoint URL"),
5149
+ subscribedEvents: z14.array(z14.enum(PlatformEventType)).min(1).describe("Events to subscribe to"),
5150
+ payloadFormat: z14.enum(WebhookPayloadFormat).optional().default("GENERIC").describe("Payload format"),
5151
+ secret: z14.string().optional().describe("HMAC-SHA256 signing secret"),
5152
+ isActive: z14.boolean().optional().default(true).describe("Whether the subscription is active")
5060
5153
  })
5061
5154
  },
5062
5155
  async ({ ...body }) => callApi("POST", "webhook-subscriptions", { body })
@@ -5067,8 +5160,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
5067
5160
  title: "Delete Webhook Subscription",
5068
5161
  annotations: { readOnlyHint: false, destructiveHint: true },
5069
5162
  description: "Delete a webhook subscription by ID.",
5070
- inputSchema: z13.object({
5071
- id: z13.string().uuid().describe("Webhook subscription ID")
5163
+ inputSchema: z14.object({
5164
+ id: z14.string().uuid().describe("Webhook subscription ID")
5072
5165
  })
5073
5166
  },
5074
5167
  async ({ id }) => callApi("DELETE", `webhook-subscriptions/${id}`)
@@ -5079,8 +5172,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
5079
5172
  title: "Test Webhook Subscription",
5080
5173
  annotations: { readOnlyHint: false, destructiveHint: false },
5081
5174
  description: "Send a test event to verify webhook connectivity. Returns the HTTP status code and success/failure message.",
5082
- inputSchema: z13.object({
5083
- id: z13.string().uuid().describe("Webhook subscription ID")
5175
+ inputSchema: z14.object({
5176
+ id: z14.string().uuid().describe("Webhook subscription ID")
5084
5177
  })
5085
5178
  },
5086
5179
  async ({ id }) => callApi("POST", `webhook-subscriptions/${id}/test`)
@@ -5088,7 +5181,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
5088
5181
  }
5089
5182
 
5090
5183
  // src/mcp/tools/domain-templates.ts
5091
- import { z as z14 } from "zod";
5184
+ import { z as z15 } from "zod";
5092
5185
  function registerDomainTemplateTools(server, callApi) {
5093
5186
  server.registerTool(
5094
5187
  "lexq_domain_templates_list",
@@ -5096,7 +5189,7 @@ function registerDomainTemplateTools(server, callApi) {
5096
5189
  title: "List Domain Templates",
5097
5190
  annotations: { readOnlyHint: true },
5098
5191
  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: z14.object({})
5192
+ inputSchema: z15.object({})
5100
5193
  },
5101
5194
  async () => callApi("GET", "domain-templates")
5102
5195
  );
@@ -5106,8 +5199,8 @@ function registerDomainTemplateTools(server, callApi) {
5106
5199
  title: "Preview Domain Template",
5107
5200
  annotations: { readOnlyHint: true },
5108
5201
  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: z14.object({
5110
- template: z14.string().describe(
5202
+ inputSchema: z15.object({
5203
+ template: z15.string().describe(
5111
5204
  "Domain template key (e.g. ECOMMERCE). Use lexq_domain_templates_list to see the keys."
5112
5205
  )
5113
5206
  })
@@ -5120,9 +5213,9 @@ function registerDomainTemplateTools(server, callApi) {
5120
5213
  title: "Apply Domain Template",
5121
5214
  annotations: { readOnlyHint: false, destructiveHint: false },
5122
5215
  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: z14.object({
5124
- template: z14.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
5125
- customName: z14.string().optional().describe(
5216
+ inputSchema: z15.object({
5217
+ template: z15.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
5218
+ customName: z15.string().optional().describe(
5126
5219
  "Optional custom name for the policy group that gets created. If omitted, the template's default name is used."
5127
5220
  )
5128
5221
  })
@@ -5158,7 +5251,7 @@ var SERVER_INFO = { name: "lexq", ...server_info_default };
5158
5251
  var __dirname = dirname(fileURLToPath(import.meta.url));
5159
5252
  function getVersion() {
5160
5253
  try {
5161
- const pkg = JSON.parse(readFileSync3(join2(__dirname, "..", "package.json"), "utf-8"));
5254
+ const pkg = JSON.parse(readFileSync3(join3(__dirname, "..", "package.json"), "utf-8"));
5162
5255
  return pkg.version;
5163
5256
  } catch {
5164
5257
  return "0.1.0";
@@ -5171,6 +5264,7 @@ function startMcpServer() {
5171
5264
  () => {
5172
5265
  const server = new McpServer({ ...SERVER_INFO, version });
5173
5266
  registerAllTools(server, callApi);
5267
+ registerLocalTools(server);
5174
5268
  return server;
5175
5269
  },
5176
5270
  {
@@ -5188,7 +5282,7 @@ function startMcpServer() {
5188
5282
  function registerServeCommand(program) {
5189
5283
  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
5284
  "after",
5191
- dedent18`
5285
+ dedent19`
5192
5286
 
5193
5287
  Example:
5194
5288
  $ lexq serve --mcp
@@ -5219,11 +5313,11 @@ function registerServeCommand(program) {
5219
5313
 
5220
5314
  // src/commands/domain-templates.ts
5221
5315
  import "commander";
5222
- import dedent19 from "dedent";
5316
+ import dedent20 from "dedent";
5223
5317
  function registerDomainTemplateCommands(program) {
5224
5318
  const templates = program.command("domain-templates").description("Browse and apply domain templates").addHelpText(
5225
5319
  "after",
5226
- dedent19`
5320
+ dedent20`
5227
5321
 
5228
5322
  A domain template is an industry-specific starter pack of fact
5229
5323
  definitions and sample rules. Applying one provisions a ready-to-use
@@ -5271,7 +5365,7 @@ function registerDomainTemplateCommands(program) {
5271
5365
  });
5272
5366
  templates.command("preview").description("Preview what a domain template provisions").requiredOption("--template <key>", "Domain template key (e.g. ECOMMERCE)").addHelpText(
5273
5367
  "after",
5274
- dedent19`
5368
+ dedent20`
5275
5369
 
5276
5370
  Read-only dry run — shows the fact definitions and sample rules the
5277
5371
  template will create. Nothing is provisioned.
@@ -5300,7 +5394,7 @@ function registerDomainTemplateCommands(program) {
5300
5394
  });
5301
5395
  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
5396
  "after",
5303
- dedent19`
5397
+ dedent20`
5304
5398
 
5305
5399
  Creates the template's fact definitions and a new DRAFT policy group
5306
5400
  populated with its sample rules. A fact that already exists with the
@@ -5354,7 +5448,7 @@ function registerDomainTemplateCommands(program) {
5354
5448
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
5355
5449
  function getVersion2() {
5356
5450
  try {
5357
- const pkg = JSON.parse(readFileSync4(join3(__dirname2, "..", "package.json"), "utf-8"));
5451
+ const pkg = JSON.parse(readFileSync4(join4(__dirname2, "..", "package.json"), "utf-8"));
5358
5452
  return pkg.version;
5359
5453
  } catch {
5360
5454
  return "0.0.0";