@lexq/cli 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CONTEXT.md +1 -1
- package/README.md +4 -3
- package/dist/index.js +320 -350
- package/dist/mcp/register.d.ts +7 -1
- package/dist/mcp/register.js +337 -320
- package/package.json +2 -1
- package/skills/lexq-execution/SKILL.md +8 -20
- package/skills/lexq-recipes/SKILL.md +9 -13
- package/skills/lexq-shared/SKILL.md +1 -1
package/dist/mcp/register.js
CHANGED
|
@@ -12,6 +12,135 @@ var server_info_default = {
|
|
|
12
12
|
]
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
// src/lib/console.ts
|
|
16
|
+
var CONSOLE_URL = "https://console.lexq.io";
|
|
17
|
+
var FAILURE_LOG_CONSOLE_NOTE = `Logs can be read here but not resolved or ignored. An organization admin does that in the LexQ console: Management \u2192 Failure Logs (${CONSOLE_URL}/management/failure-logs).`;
|
|
18
|
+
|
|
19
|
+
// src/lib/config.ts
|
|
20
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
21
|
+
import { homedir } from "os";
|
|
22
|
+
import { join } from "path";
|
|
23
|
+
var CONFIG_DIR = join(homedir(), ".lexq");
|
|
24
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
25
|
+
|
|
26
|
+
// src/lib/lossless-json.ts
|
|
27
|
+
import {
|
|
28
|
+
LosslessNumber,
|
|
29
|
+
isSafeNumber,
|
|
30
|
+
parse as losslessParse,
|
|
31
|
+
stringify as losslessStringify
|
|
32
|
+
} from "lossless-json";
|
|
33
|
+
var survivesDoubleRoundTrip = (literal) => isSafeNumber(literal);
|
|
34
|
+
var parseNumber = (value) => survivesDoubleRoundTrip(value) ? parseFloat(value) : new LosslessNumber(value);
|
|
35
|
+
var parseJson = (text) => losslessParse(text, void 0, parseNumber);
|
|
36
|
+
var stringifyJson = (value, space) => losslessStringify(value, void 0, space);
|
|
37
|
+
var isLosslessNumber = (value) => value instanceof LosslessNumber;
|
|
38
|
+
|
|
39
|
+
// src/lib/api-client.ts
|
|
40
|
+
var ApiError = class extends Error {
|
|
41
|
+
constructor(statusCode, errorCode, message) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.statusCode = statusCode;
|
|
44
|
+
this.errorCode = errorCode;
|
|
45
|
+
this.name = "ApiError";
|
|
46
|
+
}
|
|
47
|
+
statusCode;
|
|
48
|
+
errorCode;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// src/lib/value-domain-warning.ts
|
|
52
|
+
function formatValueDomainMismatchWarning(meta) {
|
|
53
|
+
const mismatch = meta?.valueDomainMismatch;
|
|
54
|
+
if (!mismatch || !Array.isArray(mismatch.versions) || mismatch.versions.length === 0) return null;
|
|
55
|
+
const n = mismatch.totalRuleCount;
|
|
56
|
+
const subject = n === 1 ? "1 rule compares" : `${n} rules compare`;
|
|
57
|
+
const lines = [
|
|
58
|
+
`\u26A0 ${subject} ${mismatch.factKey} with values the new value domain does not accept. The update was still saved.`
|
|
59
|
+
];
|
|
60
|
+
for (const version of mismatch.versions) {
|
|
61
|
+
lines.push(...versionLines(mismatch.factKey, version));
|
|
62
|
+
}
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
|
65
|
+
function versionLines(factKey, version) {
|
|
66
|
+
const draft = version.versionStatus === "DRAFT";
|
|
67
|
+
const lines = [
|
|
68
|
+
` ${quoted(version.groupName)} v${version.versionNo}, ${draft ? "draft" : "deployed"} (groupId ${version.groupId}, versionId ${version.versionId})`,
|
|
69
|
+
draft ? " Dry runs of this draft already reject calls that send these values (P-052), and live calls will too once it is published and deployed. Saving one of these rules again is refused (P-051) until each listed condition is edited or removed; first check whether each one is still needed." : " Calls to it are checked against the fact definitions captured when it was published, so this change does not affect them. A version cloned from it carries these rules; fix them in the clone before publishing it."
|
|
70
|
+
];
|
|
71
|
+
for (const rule of version.rules) {
|
|
72
|
+
const conditions = rule.conditions.map(
|
|
73
|
+
(c) => `${factKey} ${c.operator} ${escapeUnsafe(stringifyJson(c.value) ?? String(c.value))}`
|
|
74
|
+
).join("; ");
|
|
75
|
+
lines.push(` \u2022 ${quoted(rule.ruleName)} (ruleId ${rule.ruleId}): ${conditions}`);
|
|
76
|
+
}
|
|
77
|
+
const hidden = version.ruleCount - version.rules.length;
|
|
78
|
+
if (hidden > 0) {
|
|
79
|
+
lines.push(` \u2026 and ${hidden} more rule${hidden === 1 ? "" : "s"} in this version.`);
|
|
80
|
+
}
|
|
81
|
+
return lines;
|
|
82
|
+
}
|
|
83
|
+
function quoted(name) {
|
|
84
|
+
return escapeUnsafe(stringifyJson(String(name)) ?? '""');
|
|
85
|
+
}
|
|
86
|
+
var UNSAFE = /[\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
|
|
87
|
+
function escapeUnsafe(json) {
|
|
88
|
+
return json.replace(UNSAFE, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/mcp/tools/_shared.ts
|
|
92
|
+
var READ_ONLY = {
|
|
93
|
+
readOnlyHint: true,
|
|
94
|
+
destructiveHint: false,
|
|
95
|
+
openWorldHint: false
|
|
96
|
+
};
|
|
97
|
+
var ADDITIVE = {
|
|
98
|
+
readOnlyHint: false,
|
|
99
|
+
destructiveHint: false,
|
|
100
|
+
openWorldHint: false
|
|
101
|
+
};
|
|
102
|
+
var DESTRUCTIVE = {
|
|
103
|
+
readOnlyHint: false,
|
|
104
|
+
destructiveHint: true,
|
|
105
|
+
openWorldHint: false
|
|
106
|
+
};
|
|
107
|
+
function paginationParams(page, size) {
|
|
108
|
+
const params = {};
|
|
109
|
+
if (page !== void 0) params.page = String(page);
|
|
110
|
+
if (size !== void 0) params.size = String(size);
|
|
111
|
+
return params;
|
|
112
|
+
}
|
|
113
|
+
var MAX_EXPORT_CHARS = 1e5;
|
|
114
|
+
var formatCount = (count) => count.toLocaleString("en-US");
|
|
115
|
+
var EXPORT_LIMIT_NOTE = `An export longer than ${formatCount(MAX_EXPORT_CHARS)} characters is refused.`;
|
|
116
|
+
function limitExport(result, command, narrower) {
|
|
117
|
+
if (result.isError) return result;
|
|
118
|
+
const length = result.content.reduce((sum, block) => sum + block.text.length, 0);
|
|
119
|
+
if (length <= MAX_EXPORT_CHARS) return result;
|
|
120
|
+
const text = [
|
|
121
|
+
`The export is ${formatCount(length)} characters long, and this tool returns at most ${formatCount(MAX_EXPORT_CHARS)}.`,
|
|
122
|
+
narrower,
|
|
123
|
+
`To get the whole file, download it from the LexQ console (${CONSOLE_URL}) or with \`${command}\`.`
|
|
124
|
+
].filter(Boolean).join(" ");
|
|
125
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
126
|
+
}
|
|
127
|
+
function formatUnregisteredFactWarning(meta) {
|
|
128
|
+
const facts = meta?.unregisteredFacts ?? [];
|
|
129
|
+
if (facts.length === 0) return null;
|
|
130
|
+
const lines = facts.map((f) => {
|
|
131
|
+
if (f.inferredType) {
|
|
132
|
+
return ` \u2022 ${f.key} (${f.inferredType}) \u2014 register: lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: "${f.inferredType}" })`;
|
|
133
|
+
}
|
|
134
|
+
const choices = f.candidateTypes?.join(" | ") ?? "STRING | NUMBER | BOOLEAN";
|
|
135
|
+
return ` \u2022 ${f.key} (ambiguous) \u2014 pick a type (${choices}), then lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: <chosen> })`;
|
|
136
|
+
});
|
|
137
|
+
return [
|
|
138
|
+
`\u26A0 The saved rule references ${facts.length} fact(s) not defined in the schema. The rule was saved, but the engine does not check request values for these keys against a type or value domain, and lexq_requirements lists them with type UNKNOWN.`,
|
|
139
|
+
...lines,
|
|
140
|
+
`Register them before the version is published to have those values checked, or call lexq_facts_unregistered to review the version's full list.`
|
|
141
|
+
].join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
15
144
|
// src/mcp/tools/status.ts
|
|
16
145
|
import { z } from "zod";
|
|
17
146
|
function registerStatusTools(server, callApi) {
|
|
@@ -19,7 +148,7 @@ function registerStatusTools(server, callApi) {
|
|
|
19
148
|
"lexq_whoami",
|
|
20
149
|
{
|
|
21
150
|
title: "Who Am I",
|
|
22
|
-
annotations:
|
|
151
|
+
annotations: READ_ONLY,
|
|
23
152
|
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.',
|
|
24
153
|
inputSchema: z.object({})
|
|
25
154
|
},
|
|
@@ -31,12 +160,29 @@ function registerStatusTools(server, callApi) {
|
|
|
31
160
|
import { z as z2 } from "zod";
|
|
32
161
|
|
|
33
162
|
// src/types/enums.ts
|
|
163
|
+
var PolicyGroupStatus = ["ACTIVE", "DISABLED", "ARCHIVED"];
|
|
164
|
+
var LogicalOperator = ["AND", "OR"];
|
|
165
|
+
var MutateOperator = ["ASSIGN", "ADD", "SUB", "MUL", "DIV"];
|
|
166
|
+
var CalculationMethod = ["PERCENTAGE", "AMOUNT"];
|
|
167
|
+
var RoundingPolicy = [
|
|
168
|
+
"UP",
|
|
169
|
+
"DOWN",
|
|
170
|
+
"CEILING",
|
|
171
|
+
"FLOOR",
|
|
172
|
+
"HALF_UP",
|
|
173
|
+
"HALF_DOWN",
|
|
174
|
+
"HALF_EVEN"
|
|
175
|
+
];
|
|
34
176
|
var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
|
|
35
177
|
var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
|
|
36
178
|
var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
|
|
179
|
+
var DeploymentType = ["DEPLOY", "ROLLBACK", "UNDEPLOY"];
|
|
180
|
+
var ApiExecutionStatus = ["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"];
|
|
181
|
+
var ProfileCacheState = ["HIT", "MISS"];
|
|
182
|
+
var SimulationStatus = ["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"];
|
|
183
|
+
var SimulationMetricType = ["COUNT", "SUM", "AVG"];
|
|
37
184
|
var ExportFormat = ["CSV", "JSON"];
|
|
38
185
|
var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
|
|
39
|
-
var FailureAction = ["IGNORE", "RESOLVE"];
|
|
40
186
|
var TaskType = ["PLATFORM_WEBHOOK", "SCHEDULED_DEPLOYMENT"];
|
|
41
187
|
var PlatformEventType = [
|
|
42
188
|
"VERSION_PUBLISHED",
|
|
@@ -47,9 +193,12 @@ var PlatformEventType = [
|
|
|
47
193
|
"DEPLOY_SCHEDULE_CANCELED"
|
|
48
194
|
];
|
|
49
195
|
var WebhookPayloadFormat = ["GENERIC", "SLACK"];
|
|
196
|
+
var DeploymentScheduleStatus = ["PENDING", "EXECUTED", "CANCELED", "FAILED"];
|
|
50
197
|
|
|
51
198
|
// src/types/constants.ts
|
|
52
199
|
var MAX_ROUNDING_SCALE = 34;
|
|
200
|
+
var MIN_TRAFFIC_RATE = 1;
|
|
201
|
+
var MAX_TRAFFIC_RATE = 99;
|
|
53
202
|
var REPLAY_WINDOW_MAX_RECORDS = 5e4;
|
|
54
203
|
var DEFAULT_SIMULATION_RECORDS = 1e3;
|
|
55
204
|
var MAX_SIMULATION_RECORDS = 1e5;
|
|
@@ -66,6 +215,10 @@ var MAX_RULES_PER_VERSION = 100;
|
|
|
66
215
|
var MAX_CONDITIONS_PER_RULE = 25;
|
|
67
216
|
var MAX_CONDITION_DEPTH = 10;
|
|
68
217
|
var MAX_SNAPSHOT_BYTES = 131072;
|
|
218
|
+
var STATS_DEFAULT_WINDOW_DAYS = 30;
|
|
219
|
+
var STATS_MAX_WINDOW_DAYS = 31;
|
|
220
|
+
var FAILURE_LOG_LOOKBACK_DAYS = 7;
|
|
221
|
+
var PURGE_AFTER_DAYS = 30;
|
|
69
222
|
|
|
70
223
|
// src/mcp/tools/groups.ts
|
|
71
224
|
function registerGroupTools(server, callApi) {
|
|
@@ -73,7 +226,7 @@ function registerGroupTools(server, callApi) {
|
|
|
73
226
|
"lexq_groups_list",
|
|
74
227
|
{
|
|
75
228
|
title: "List Policy Groups",
|
|
76
|
-
annotations:
|
|
229
|
+
annotations: READ_ONLY,
|
|
77
230
|
description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
|
|
78
231
|
inputSchema: z2.object({})
|
|
79
232
|
},
|
|
@@ -83,7 +236,7 @@ function registerGroupTools(server, callApi) {
|
|
|
83
236
|
"lexq_groups_get",
|
|
84
237
|
{
|
|
85
238
|
title: "Get Policy Group",
|
|
86
|
-
annotations:
|
|
239
|
+
annotations: READ_ONLY,
|
|
87
240
|
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.",
|
|
88
241
|
inputSchema: z2.object({
|
|
89
242
|
groupId: z2.string().uuid().describe("Policy group ID")
|
|
@@ -95,7 +248,7 @@ function registerGroupTools(server, callApi) {
|
|
|
95
248
|
"lexq_groups_create",
|
|
96
249
|
{
|
|
97
250
|
title: "Create Policy Group",
|
|
98
|
-
annotations:
|
|
251
|
+
annotations: ADDITIVE,
|
|
99
252
|
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.`,
|
|
100
253
|
inputSchema: z2.object({
|
|
101
254
|
name: z2.string().describe("Group name (unique among non-ARCHIVED)"),
|
|
@@ -126,13 +279,13 @@ function registerGroupTools(server, callApi) {
|
|
|
126
279
|
"lexq_groups_update",
|
|
127
280
|
{
|
|
128
281
|
title: "Update Policy Group",
|
|
129
|
-
annotations:
|
|
282
|
+
annotations: DESTRUCTIVE,
|
|
130
283
|
description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
|
|
131
284
|
inputSchema: z2.object({
|
|
132
285
|
groupId: z2.string().uuid().describe("Policy group ID"),
|
|
133
286
|
name: z2.string().optional().describe("New name"),
|
|
134
287
|
description: z2.string().optional().describe("New description"),
|
|
135
|
-
status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
|
|
288
|
+
status: z2.enum(PolicyGroupStatus).extract(["ACTIVE", "DISABLED"]).optional().describe(
|
|
136
289
|
"DISABLED stops production execution of this group: execution calls return 403 (P-009), and a composite call that includes the group is rejected in full. The deployed version and any A/B test are kept, and dry run, Impact Simulation, and Decision Replay keep working, so a fix can be verified before setting ACTIVE again. To go back to the previous version, use lexq_deploy_rollback. To take the version out of production, use lexq_deploy_undeploy."
|
|
137
290
|
),
|
|
138
291
|
activationGroup: z2.string().optional().describe(
|
|
@@ -151,8 +304,8 @@ function registerGroupTools(server, callApi) {
|
|
|
151
304
|
"lexq_groups_delete",
|
|
152
305
|
{
|
|
153
306
|
title: "Delete Policy Group",
|
|
154
|
-
annotations:
|
|
155
|
-
description:
|
|
307
|
+
annotations: DESTRUCTIVE,
|
|
308
|
+
description: `Delete (archive) a policy group. A deployed group cannot be deleted (P-020); undeploy it first with lexq_deploy_undeploy, which also ends a running A/B test. This cannot be undone. The group no longer appears in lexq_groups_list and its name can be reused right away. Calls that change it (settings, versions, rules, starting an A/B test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted after ${PURGE_AFTER_DAYS} days.`,
|
|
156
309
|
inputSchema: z2.object({
|
|
157
310
|
groupId: z2.string().uuid().describe("Policy group ID")
|
|
158
311
|
})
|
|
@@ -163,7 +316,7 @@ function registerGroupTools(server, callApi) {
|
|
|
163
316
|
"lexq_groups_reorder",
|
|
164
317
|
{
|
|
165
318
|
title: "Reorder Policy Groups",
|
|
166
|
-
annotations:
|
|
319
|
+
annotations: DESTRUCTIVE,
|
|
167
320
|
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.",
|
|
168
321
|
inputSchema: z2.object({
|
|
169
322
|
groupIds: z2.array(z2.string().uuid()).describe("Group IDs in desired priority order (index 0 = priority 1)")
|
|
@@ -181,12 +334,14 @@ function registerGroupTools(server, callApi) {
|
|
|
181
334
|
"lexq_ab_test_start",
|
|
182
335
|
{
|
|
183
336
|
title: "Start A/B Test",
|
|
184
|
-
annotations:
|
|
185
|
-
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%.",
|
|
337
|
+
annotations: DESTRUCTIVE,
|
|
338
|
+
description: "Start an A/B test on a policy group. Requires a challenger version ID and traffic rate. The challenger must be a published version of the same group; a version from another group is rejected with P-016. The split is computed from context.trafficKey on each execution request; requests that omit it never reach the challenger and the test stays at 0%. Starting cancels the group's pending scheduled deployment. Starting while a test is already running replaces its challenger and traffic rate.",
|
|
186
339
|
inputSchema: z2.object({
|
|
187
340
|
groupId: z2.string().uuid().describe("Policy group ID"),
|
|
188
341
|
testVersionId: z2.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
|
|
189
|
-
trafficRate: z2.number().int().min(
|
|
342
|
+
trafficRate: z2.number().int().min(MIN_TRAFFIC_RATE).max(MAX_TRAFFIC_RATE).describe(
|
|
343
|
+
`Traffic percentage routed to challenger (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
|
|
344
|
+
)
|
|
190
345
|
})
|
|
191
346
|
},
|
|
192
347
|
async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/ab-test`, { body })
|
|
@@ -195,7 +350,7 @@ function registerGroupTools(server, callApi) {
|
|
|
195
350
|
"lexq_ab_test_stop",
|
|
196
351
|
{
|
|
197
352
|
title: "Stop A/B Test",
|
|
198
|
-
annotations:
|
|
353
|
+
annotations: DESTRUCTIVE,
|
|
199
354
|
description: "Stop a running A/B test. All traffic is restored to the control (current) version.",
|
|
200
355
|
inputSchema: z2.object({
|
|
201
356
|
groupId: z2.string().uuid().describe("Policy group ID")
|
|
@@ -207,11 +362,13 @@ function registerGroupTools(server, callApi) {
|
|
|
207
362
|
"lexq_ab_test_adjust",
|
|
208
363
|
{
|
|
209
364
|
title: "Adjust A/B Test",
|
|
210
|
-
annotations:
|
|
365
|
+
annotations: DESTRUCTIVE,
|
|
211
366
|
description: "Adjust traffic rate of a running A/B test.",
|
|
212
367
|
inputSchema: z2.object({
|
|
213
368
|
groupId: z2.string().uuid().describe("Policy group ID"),
|
|
214
|
-
trafficRate: z2.number().int().min(
|
|
369
|
+
trafficRate: z2.number().int().min(MIN_TRAFFIC_RATE).max(MAX_TRAFFIC_RATE).describe(
|
|
370
|
+
`New traffic percentage for challenger (${MIN_TRAFFIC_RATE}-${MAX_TRAFFIC_RATE})`
|
|
371
|
+
)
|
|
215
372
|
})
|
|
216
373
|
},
|
|
217
374
|
async ({ groupId, ...body }) => callApi("PATCH", `policy-groups/${groupId}/ab-test/traffic-rate`, { body })
|
|
@@ -221,102 +378,6 @@ function registerGroupTools(server, callApi) {
|
|
|
221
378
|
// src/mcp/tools/versions.ts
|
|
222
379
|
import { z as z3 } from "zod";
|
|
223
380
|
|
|
224
|
-
// src/lib/config.ts
|
|
225
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
226
|
-
import { homedir } from "os";
|
|
227
|
-
import { join } from "path";
|
|
228
|
-
var CONFIG_DIR = join(homedir(), ".lexq");
|
|
229
|
-
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
230
|
-
|
|
231
|
-
// src/lib/lossless-json.ts
|
|
232
|
-
import {
|
|
233
|
-
LosslessNumber,
|
|
234
|
-
isSafeNumber,
|
|
235
|
-
parse as losslessParse,
|
|
236
|
-
stringify as losslessStringify
|
|
237
|
-
} from "lossless-json";
|
|
238
|
-
var survivesDoubleRoundTrip = (literal) => isSafeNumber(literal);
|
|
239
|
-
var parseNumber = (value) => survivesDoubleRoundTrip(value) ? parseFloat(value) : new LosslessNumber(value);
|
|
240
|
-
var parseJson = (text) => losslessParse(text, void 0, parseNumber);
|
|
241
|
-
var stringifyJson = (value, space) => losslessStringify(value, void 0, space);
|
|
242
|
-
var isLosslessNumber = (value) => value instanceof LosslessNumber;
|
|
243
|
-
|
|
244
|
-
// src/lib/api-client.ts
|
|
245
|
-
var ApiError = class extends Error {
|
|
246
|
-
constructor(statusCode, errorCode, message) {
|
|
247
|
-
super(message);
|
|
248
|
-
this.statusCode = statusCode;
|
|
249
|
-
this.errorCode = errorCode;
|
|
250
|
-
this.name = "ApiError";
|
|
251
|
-
}
|
|
252
|
-
statusCode;
|
|
253
|
-
errorCode;
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// src/lib/value-domain-warning.ts
|
|
257
|
-
function formatValueDomainMismatchWarning(meta) {
|
|
258
|
-
const mismatch = meta?.valueDomainMismatch;
|
|
259
|
-
if (!mismatch || !Array.isArray(mismatch.versions) || mismatch.versions.length === 0) return null;
|
|
260
|
-
const n = mismatch.totalRuleCount;
|
|
261
|
-
const subject = n === 1 ? "1 rule compares" : `${n} rules compare`;
|
|
262
|
-
const lines = [
|
|
263
|
-
`\u26A0 ${subject} ${mismatch.factKey} with values the new value domain does not accept. The update was still saved.`
|
|
264
|
-
];
|
|
265
|
-
for (const version of mismatch.versions) {
|
|
266
|
-
lines.push(...versionLines(mismatch.factKey, version));
|
|
267
|
-
}
|
|
268
|
-
return lines.join("\n");
|
|
269
|
-
}
|
|
270
|
-
function versionLines(factKey, version) {
|
|
271
|
-
const draft = version.versionStatus === "DRAFT";
|
|
272
|
-
const lines = [
|
|
273
|
-
` ${quoted(version.groupName)} v${version.versionNo}, ${draft ? "draft" : "deployed"} (groupId ${version.groupId}, versionId ${version.versionId})`,
|
|
274
|
-
draft ? " Dry runs of this draft already reject calls that send these values (P-052), and live calls will too once it is published and deployed. Saving one of these rules again is refused (P-051) until each listed condition is edited or removed; first check whether each one is still needed." : " Calls to it are checked against the fact definitions captured when it was published, so this change does not affect them. A version cloned from it carries these rules; fix them in the clone before publishing it."
|
|
275
|
-
];
|
|
276
|
-
for (const rule of version.rules) {
|
|
277
|
-
const conditions = rule.conditions.map(
|
|
278
|
-
(c) => `${factKey} ${c.operator} ${escapeUnsafe(stringifyJson(c.value) ?? String(c.value))}`
|
|
279
|
-
).join("; ");
|
|
280
|
-
lines.push(` \u2022 ${quoted(rule.ruleName)} (ruleId ${rule.ruleId}): ${conditions}`);
|
|
281
|
-
}
|
|
282
|
-
const hidden = version.ruleCount - version.rules.length;
|
|
283
|
-
if (hidden > 0) {
|
|
284
|
-
lines.push(` \u2026 and ${hidden} more rule${hidden === 1 ? "" : "s"} in this version.`);
|
|
285
|
-
}
|
|
286
|
-
return lines;
|
|
287
|
-
}
|
|
288
|
-
function quoted(name) {
|
|
289
|
-
return escapeUnsafe(stringifyJson(String(name)) ?? '""');
|
|
290
|
-
}
|
|
291
|
-
var UNSAFE = /[\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
|
|
292
|
-
function escapeUnsafe(json) {
|
|
293
|
-
return json.replace(UNSAFE, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// src/mcp/tools/_shared.ts
|
|
297
|
-
function paginationParams(page, size) {
|
|
298
|
-
const params = {};
|
|
299
|
-
if (page !== void 0) params.page = String(page);
|
|
300
|
-
if (size !== void 0) params.size = String(size);
|
|
301
|
-
return params;
|
|
302
|
-
}
|
|
303
|
-
function formatUnregisteredFactWarning(meta) {
|
|
304
|
-
const facts = meta?.unregisteredFacts ?? [];
|
|
305
|
-
if (facts.length === 0) return null;
|
|
306
|
-
const lines = facts.map((f) => {
|
|
307
|
-
if (f.inferredType) {
|
|
308
|
-
return ` \u2022 ${f.key} (${f.inferredType}) \u2014 register: lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: "${f.inferredType}" })`;
|
|
309
|
-
}
|
|
310
|
-
const choices = f.candidateTypes?.join(" | ") ?? "STRING | NUMBER | BOOLEAN";
|
|
311
|
-
return ` \u2022 ${f.key} (ambiguous) \u2014 pick a type (${choices}), then lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: <chosen> })`;
|
|
312
|
-
});
|
|
313
|
-
return [
|
|
314
|
-
`\u26A0 The saved rule references ${facts.length} fact(s) not defined in the schema. The rule was saved \u2014 this does not block \u2014 but undefined facts skip type validation and the dry-run requirements analyzer.`,
|
|
315
|
-
...lines,
|
|
316
|
-
`Register them now so the rule validates as intended, or call lexq_facts_unregistered to review the version's full list.`
|
|
317
|
-
].join("\n");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
381
|
// src/lib/dates.ts
|
|
321
382
|
var ORG_DAY = "yyyy-MM-dd, a whole calendar day in the organization time zone (tenantTimezone in whoami), not UTC";
|
|
322
383
|
var START_DAY = `Start date, ${ORG_DAY}. Inclusive.`;
|
|
@@ -324,9 +385,6 @@ var END_DAY = `End date, ${ORG_DAY}. Inclusive: the whole day counts.`;
|
|
|
324
385
|
var WINDOW_START_DAY = `Window start date, ${ORG_DAY}. Inclusive.`;
|
|
325
386
|
var WINDOW_END_DAY = `Window end date, ${ORG_DAY}. Inclusive: the whole day counts.`;
|
|
326
387
|
var INSTANT = "ISO-8601 instant with Z or an offset, e.g. 2026-10-01T00:00:00Z or 2026-10-01T09:00:00+09:00. A bare date or a time without an offset is rejected.";
|
|
327
|
-
var STATS_DEFAULT_WINDOW_DAYS = 30;
|
|
328
|
-
var STATS_MAX_WINDOW_DAYS = 31;
|
|
329
|
-
var FAILURE_LOG_LOOKBACK_DAYS = 7;
|
|
330
388
|
|
|
331
389
|
// src/mcp/tools/versions.ts
|
|
332
390
|
function registerVersionTools(server, callApi) {
|
|
@@ -334,7 +392,7 @@ function registerVersionTools(server, callApi) {
|
|
|
334
392
|
"lexq_versions_list",
|
|
335
393
|
{
|
|
336
394
|
title: "List Policy Versions",
|
|
337
|
-
annotations:
|
|
395
|
+
annotations: READ_ONLY,
|
|
338
396
|
description: "List all versions of a policy group.",
|
|
339
397
|
inputSchema: z3.object({
|
|
340
398
|
groupId: z3.string().uuid().describe("Policy group ID"),
|
|
@@ -350,7 +408,7 @@ function registerVersionTools(server, callApi) {
|
|
|
350
408
|
"lexq_versions_get",
|
|
351
409
|
{
|
|
352
410
|
title: "Get Policy Version",
|
|
353
|
-
annotations:
|
|
411
|
+
annotations: READ_ONLY,
|
|
354
412
|
description: "Get a single version by ID, including its rules and fact requirements.",
|
|
355
413
|
inputSchema: z3.object({
|
|
356
414
|
groupId: z3.string().uuid().describe("Policy group ID"),
|
|
@@ -363,7 +421,7 @@ function registerVersionTools(server, callApi) {
|
|
|
363
421
|
"lexq_versions_create",
|
|
364
422
|
{
|
|
365
423
|
title: "Create Policy Version",
|
|
366
|
-
annotations:
|
|
424
|
+
annotations: ADDITIVE,
|
|
367
425
|
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.`,
|
|
368
426
|
inputSchema: z3.object({
|
|
369
427
|
groupId: z3.string().uuid().describe("Policy group ID"),
|
|
@@ -378,7 +436,7 @@ function registerVersionTools(server, callApi) {
|
|
|
378
436
|
"lexq_versions_update",
|
|
379
437
|
{
|
|
380
438
|
title: "Update Policy Version",
|
|
381
|
-
annotations:
|
|
439
|
+
annotations: DESTRUCTIVE,
|
|
382
440
|
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.",
|
|
383
441
|
inputSchema: z3.object({
|
|
384
442
|
groupId: z3.string().uuid().describe("Policy group ID"),
|
|
@@ -386,7 +444,9 @@ function registerVersionTools(server, callApi) {
|
|
|
386
444
|
commitMessage: z3.string().optional().describe("New commit message"),
|
|
387
445
|
effectiveFrom: z3.string().optional().describe(`Effective start, ${INSTANT} Omit it to keep the current start.`),
|
|
388
446
|
effectiveTo: z3.string().optional().describe(`Effective end, ${INSTANT} Omit it to keep the current end.`),
|
|
389
|
-
clearEffectiveFrom: z3.boolean().optional().describe(
|
|
447
|
+
clearEffectiveFrom: z3.boolean().optional().describe(
|
|
448
|
+
"Set to true to remove the effective start. Do not send effectiveFrom with it."
|
|
449
|
+
),
|
|
390
450
|
clearEffectiveTo: z3.boolean().optional().describe("Set to true to remove the effective end. Do not send effectiveTo with it.")
|
|
391
451
|
})
|
|
392
452
|
},
|
|
@@ -396,7 +456,7 @@ function registerVersionTools(server, callApi) {
|
|
|
396
456
|
"lexq_versions_delete",
|
|
397
457
|
{
|
|
398
458
|
title: "Delete Policy Version",
|
|
399
|
-
annotations:
|
|
459
|
+
annotations: DESTRUCTIVE,
|
|
400
460
|
description: "Delete a DRAFT version. Only DRAFT versions can be deleted.",
|
|
401
461
|
inputSchema: z3.object({
|
|
402
462
|
groupId: z3.string().uuid().describe("Policy group ID"),
|
|
@@ -409,7 +469,7 @@ function registerVersionTools(server, callApi) {
|
|
|
409
469
|
"lexq_versions_clone",
|
|
410
470
|
{
|
|
411
471
|
title: "Clone Policy Version",
|
|
412
|
-
annotations:
|
|
472
|
+
annotations: ADDITIVE,
|
|
413
473
|
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.`,
|
|
414
474
|
inputSchema: z3.object({
|
|
415
475
|
groupId: z3.string().uuid().describe("Policy group ID"),
|
|
@@ -427,12 +487,13 @@ function registerVersionTools(server, callApi) {
|
|
|
427
487
|
// src/mcp/tools/rules.ts
|
|
428
488
|
import { z as z4 } from "zod";
|
|
429
489
|
import dedent from "dedent";
|
|
490
|
+
var oneOf = (values) => values.map((v) => `"${v}"`).join("|");
|
|
430
491
|
function registerRuleTools(server, callApi) {
|
|
431
492
|
server.registerTool(
|
|
432
493
|
"lexq_rules_list",
|
|
433
494
|
{
|
|
434
495
|
title: "List Rules",
|
|
435
|
-
annotations:
|
|
496
|
+
annotations: READ_ONLY,
|
|
436
497
|
description: "List all rules in a version (priority ASC). Returns summary with conditionSummary and actionSummary.",
|
|
437
498
|
inputSchema: z4.object({
|
|
438
499
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
@@ -445,7 +506,7 @@ function registerRuleTools(server, callApi) {
|
|
|
445
506
|
"lexq_rules_get",
|
|
446
507
|
{
|
|
447
508
|
title: "Get Rule Detail",
|
|
448
|
-
annotations:
|
|
509
|
+
annotations: READ_ONLY,
|
|
449
510
|
description: "Get full rule detail including condition tree and action definitions.",
|
|
450
511
|
inputSchema: z4.object({
|
|
451
512
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
@@ -459,87 +520,39 @@ function registerRuleTools(server, callApi) {
|
|
|
459
520
|
"lexq_rules_create",
|
|
460
521
|
{
|
|
461
522
|
title: "Create Rule",
|
|
462
|
-
annotations:
|
|
523
|
+
annotations: ADDITIVE,
|
|
463
524
|
description: dedent`
|
|
464
|
-
Create a rule in a DRAFT version.
|
|
465
|
-
|
|
466
|
-
Before creating rules with new fact keys, call lexq_facts_list to check existing facts.
|
|
467
|
-
If a required key is missing, ask the user to confirm the type, isRequired, and description
|
|
468
|
-
before calling lexq_facts_create — registering facts enables type validation, Console UI
|
|
469
|
-
autocomplete, and the dry-run requirements analyzer.
|
|
525
|
+
Create a rule in a DRAFT version. It goes last in priority order; lexq_rules_reorder changes the order.
|
|
470
526
|
|
|
471
|
-
An action's targetVar
|
|
472
|
-
publishing a rule whose targetVar is not registered fails with ACT-032, so register it
|
|
473
|
-
with lexq_facts_create first. Keys a rule only reads (condition fields, refVar) may stay
|
|
474
|
-
unregistered.
|
|
527
|
+
An action's targetVar must be a registered fact (ACT-032 otherwise). Keys a rule only reads (condition fields, refVar) may stay unregistered.
|
|
475
528
|
|
|
476
|
-
|
|
477
|
-
|
|
529
|
+
Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: ${oneOf(LogicalOperator)}, children: [...] }
|
|
530
|
+
Value types: ${ValueType.join(", ")}
|
|
531
|
+
Limits: ${MAX_CONDITIONS_PER_RULE} SINGLE conditions per rule (GROUP nodes do not count), ${MAX_CONDITION_DEPTH} levels of nesting below the root, ${MAX_RULES_PER_VERSION} rules per version.
|
|
478
532
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
its type is rejected by the server — check the fact type before choosing an operator.
|
|
486
|
-
- STRING fact: EQUALS, NOT_EQUALS, CONTAINS, IN, NOT_IN
|
|
487
|
-
- NUMBER fact: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, IN, NOT_IN
|
|
488
|
-
- BOOLEAN fact: EQUALS, NOT_EQUALS
|
|
489
|
-
- LIST_* fact: HAS_ANY, HAS_ALL, HAS_NONE (only these)
|
|
490
|
-
|
|
491
|
-
HAS_* query list-typed facts. Value is always an array whose element type matches the fact:
|
|
492
|
-
- HAS_ANY: fact has at least one of the given values
|
|
493
|
-
- HAS_ALL: fact has all of the given values
|
|
494
|
-
- HAS_NONE: fact has none of the given values
|
|
533
|
+
Operators that work on each fact type:
|
|
534
|
+
- STRING: EQUALS, NOT_EQUALS, CONTAINS (substring), IN, NOT_IN
|
|
535
|
+
- NUMBER: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, IN, NOT_IN
|
|
536
|
+
- BOOLEAN: EQUALS, NOT_EQUALS
|
|
537
|
+
- LIST_STRING, LIST_NUMBER: HAS_ANY, HAS_ALL, HAS_NONE
|
|
538
|
+
IN and NOT_IN test whether the fact's value is in the array. HAS_ANY, HAS_ALL and HAS_NONE match when the fact's list holds at least one, all, or none of the array's values. The array holds values of the fact's type, or for a list fact, of its element type.
|
|
495
539
|
Example: { "type": "SINGLE", "field": "userTags", "operator": "HAS_ANY", "value": ["VIP","GOLD"], "valueType": "LIST_STRING" }
|
|
496
540
|
|
|
497
|
-
Do NOT use CONTAINS on a list fact — CONTAINS is substring match on STRING facts only.
|
|
498
|
-
IN is the mirror of HAS_*: IN takes a scalar fact with a list value; HAS_* takes lists on both sides.
|
|
499
|
-
|
|
500
541
|
Actions: [{ type, parameters }]
|
|
542
|
+
- MUTATE_FACT: { targetVar, operator: ${oneOf(MutateOperator)}, method: ${oneOf(CalculationMethod)}, operand: number, refVar?, rounding? }
|
|
543
|
+
Reads and writes targetVar. The request must send it (P-015 otherwise), and it must hold a number when the action runs. operand may be negative, and a percent may be over 100.
|
|
544
|
+
AMOUNT PERCENTAGE (base = refVar, or targetVar when omitted)
|
|
545
|
+
ASSIGN targetVar = operand targetVar = base × operand/100
|
|
546
|
+
ADD targetVar += operand targetVar += base × operand/100
|
|
547
|
+
SUB targetVar -= operand targetVar -= base × operand/100
|
|
548
|
+
MUL targetVar *= operand targetVar *= (operand/100 + 1)
|
|
549
|
+
DIV targetVar /= operand invalid
|
|
550
|
+
refVar is allowed only with PERCENTAGE and ASSIGN, ADD or SUB. DIV with AMOUNT needs a nonzero operand.
|
|
551
|
+
rounding: { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: ${oneOf(RoundingPolicy)} }. mode defaults to HALF_UP. Without rounding, the result keeps full precision.
|
|
552
|
+
- SET_FACT: { targetVar, value: string|number|boolean }. The request does not have to send targetVar.
|
|
553
|
+
- BLOCK: { reason? } sets isBlocked to true and blockReason to the reason, or "Policy Violation" without one. It does not stop later actions or rules; the caller enforces the block. Only BLOCK writes these two keys: a SET_FACT or MUTATE_FACT targeting either is rejected (ACT-033).
|
|
501
554
|
|
|
502
|
-
|
|
503
|
-
- MUTATE_FACT: { targetVar: string, operator: "ASSIGN"|"ADD"|"SUB"|"MUL"|"DIV", method: "PERCENTAGE"|"AMOUNT", operand: number, refVar?: string, rounding?: RoundingOption }
|
|
504
|
-
targetVar is the fact this action reads and writes. It must be a registered fact
|
|
505
|
-
(ACT-032 otherwise), and the request must send it (P-015 otherwise). Its value must be
|
|
506
|
-
a number when the action runs.
|
|
507
|
-
A missing required fact throws (no 0 default).
|
|
508
|
-
operand is the arithmetic operand; the unit is dictated by method (percent when
|
|
509
|
-
PERCENTAGE, absolute amount when AMOUNT). Ranges are not constrained — negative values
|
|
510
|
-
and >100 percentages are valid (refunds, surcharges).
|
|
511
|
-
refVar is the base for percentage calculation and is OPTIONAL — omit it to use targetVar
|
|
512
|
-
itself. It is only meaningful in PERCENTAGE × {ASSIGN, ADD, SUB}; specifying it in any
|
|
513
|
-
other cell is an error. Use it when the base differs from the target, e.g.
|
|
514
|
-
"points += orderTotal × 5%" → { targetVar: "points", refVar: "orderTotal",
|
|
515
|
-
operator: "ADD", method: "PERCENTAGE", operand: 5 }.
|
|
516
|
-
operator × method matrix:
|
|
517
|
-
ASSIGN targetVar = operand | targetVar = refVar × operand/100
|
|
518
|
-
ADD targetVar += operand | targetVar += refVar × operand/100
|
|
519
|
-
SUB targetVar -= operand | targetVar -= refVar × operand/100
|
|
520
|
-
MUL targetVar *= operand | targetVar *= (operand/100 + 1)
|
|
521
|
-
DIV targetVar /= operand | invalid
|
|
522
|
-
Constraints: DIV + PERCENTAGE is invalid (use MUL with the inverse). DIV + AMOUNT
|
|
523
|
-
requires operand !== 0.
|
|
524
|
-
- SET_FACT: { targetVar: string, value: string|number|boolean } targetVar must be a
|
|
525
|
-
registered fact (ACT-032 otherwise). The request does not have to send it: SET_FACT is
|
|
526
|
-
the only action whose targetVar may be absent from the input facts. MUTATE_FACT requires
|
|
527
|
-
its target to be present in the input facts.
|
|
528
|
-
- BLOCK: { reason?: string } Records a rejection decision. It does NOT halt rule execution —
|
|
529
|
-
subsequent actions and subsequent winning rules still run. Enforcement is the caller's
|
|
530
|
-
responsibility; the decision surfaces as isBlocked (true) and blockReason (reason, or
|
|
531
|
-
"Policy Violation" when omitted).
|
|
532
|
-
Only BLOCK writes those two keys: saving or publishing a rule whose SET_FACT or
|
|
533
|
-
MUTATE_FACT targets either one fails with ACT-033.
|
|
534
|
-
|
|
535
|
-
Evaluation order: every rule's condition is checked against the input facts before any
|
|
536
|
-
action runs, so a condition never sees a value an action writes in the same request. Then
|
|
537
|
-
the actions of the winning rules run in priority order. Each action works on the values
|
|
538
|
-
the actions before it left, in its own rule or an earlier one. To test a value an action
|
|
539
|
-
writes, send that value as an input fact in a second request. If a rule should run
|
|
540
|
-
whenever another one does, put its actions in that rule instead.
|
|
541
|
-
|
|
542
|
-
RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: "HALF_UP"|"HALF_DOWN"|"HALF_EVEN"|"FLOOR"|"CEILING"|"DOWN"|"UP" } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
|
|
555
|
+
Every condition is checked against the input facts before any action runs, so a condition never sees a value an action writes in the same request. The winning rules' actions then run in priority order, each on the values the actions before it left.
|
|
543
556
|
`,
|
|
544
557
|
inputSchema: z4.object({
|
|
545
558
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
@@ -558,7 +571,7 @@ function registerRuleTools(server, callApi) {
|
|
|
558
571
|
"lexq_rules_update",
|
|
559
572
|
{
|
|
560
573
|
title: "Update Rule",
|
|
561
|
-
annotations:
|
|
574
|
+
annotations: DESTRUCTIVE,
|
|
562
575
|
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.`,
|
|
563
576
|
inputSchema: z4.object({
|
|
564
577
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
@@ -580,28 +593,21 @@ function registerRuleTools(server, callApi) {
|
|
|
580
593
|
"lexq_rules_delete",
|
|
581
594
|
{
|
|
582
595
|
title: "Delete Rule",
|
|
583
|
-
annotations:
|
|
596
|
+
annotations: DESTRUCTIVE,
|
|
584
597
|
description: "Delete a rule from a DRAFT version.",
|
|
585
598
|
inputSchema: z4.object({
|
|
586
599
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
587
600
|
versionId: z4.string().uuid().describe("Version ID"),
|
|
588
|
-
ruleId: z4.string().uuid().describe("Rule ID")
|
|
589
|
-
force: z4.boolean().default(false).describe("Skip confirmation (for the last rule in a version)")
|
|
601
|
+
ruleId: z4.string().uuid().describe("Rule ID")
|
|
590
602
|
})
|
|
591
603
|
},
|
|
592
|
-
async ({ groupId, versionId, ruleId
|
|
593
|
-
const params = {};
|
|
594
|
-
if (force) params.force = "true";
|
|
595
|
-
return callApi("DELETE", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`, {
|
|
596
|
-
params
|
|
597
|
-
});
|
|
598
|
-
}
|
|
604
|
+
async ({ groupId, versionId, ruleId }) => callApi("DELETE", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`)
|
|
599
605
|
);
|
|
600
606
|
server.registerTool(
|
|
601
607
|
"lexq_rules_reorder",
|
|
602
608
|
{
|
|
603
609
|
title: "Reorder Rules",
|
|
604
|
-
annotations:
|
|
610
|
+
annotations: DESTRUCTIVE,
|
|
605
611
|
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).",
|
|
606
612
|
inputSchema: z4.object({
|
|
607
613
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
@@ -623,7 +629,7 @@ function registerRuleTools(server, callApi) {
|
|
|
623
629
|
"lexq_rules_toggle",
|
|
624
630
|
{
|
|
625
631
|
title: "Toggle Rule",
|
|
626
|
-
annotations:
|
|
632
|
+
annotations: DESTRUCTIVE,
|
|
627
633
|
description: "Enable or disable a rule without deleting it.",
|
|
628
634
|
inputSchema: z4.object({
|
|
629
635
|
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
@@ -650,13 +656,14 @@ var EXPORT_FORMATS = ExportFormat.map((name) => name.toLowerCase()).sort();
|
|
|
650
656
|
|
|
651
657
|
// src/mcp/tools/facts.ts
|
|
652
658
|
var VALUE_TYPES = ValueType;
|
|
659
|
+
var IS_REQUIRED_NOTE = "isRequired is stored on the definition only; the engine does not read it.";
|
|
653
660
|
function registerFactTools(server, callApi) {
|
|
654
661
|
server.registerTool(
|
|
655
662
|
"lexq_facts_list",
|
|
656
663
|
{
|
|
657
664
|
title: "List Fact Definitions",
|
|
658
|
-
annotations:
|
|
659
|
-
description:
|
|
665
|
+
annotations: READ_ONLY,
|
|
666
|
+
description: `List all fact definitions (input variable schema): key, name, type, description, PII and system flags, and value domain. ${IS_REQUIRED_NOTE}`,
|
|
660
667
|
inputSchema: z5.object({
|
|
661
668
|
page: z5.number().int().min(0).default(0).describe("Page number"),
|
|
662
669
|
size: z5.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
@@ -673,14 +680,14 @@ function registerFactTools(server, callApi) {
|
|
|
673
680
|
"lexq_facts_create",
|
|
674
681
|
{
|
|
675
682
|
title: "Create Fact Definition",
|
|
676
|
-
annotations:
|
|
677
|
-
description:
|
|
683
|
+
annotations: ADDITIVE,
|
|
684
|
+
description: `Register a new input variable. Key starts with a letter, then letters, numbers, and underscores (e.g. paymentAmount). Casing is not enforced. Types: ${VALUE_TYPES.join(", ")}.`,
|
|
678
685
|
inputSchema: z5.object({
|
|
679
686
|
key: z5.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
|
|
680
687
|
name: z5.string().describe("Display name"),
|
|
681
688
|
type: z5.enum(VALUE_TYPES).describe("Value type"),
|
|
682
689
|
description: z5.string().optional().describe("Description"),
|
|
683
|
-
isRequired: z5.boolean().default(false).describe(
|
|
690
|
+
isRequired: z5.boolean().default(false).describe(IS_REQUIRED_NOTE),
|
|
684
691
|
isPii: z5.boolean().default(false).describe(
|
|
685
692
|
"Mark as PII \u2014 masked on every read surface, revealable only in the console (audited)"
|
|
686
693
|
),
|
|
@@ -701,7 +708,7 @@ function registerFactTools(server, callApi) {
|
|
|
701
708
|
"lexq_facts_update",
|
|
702
709
|
{
|
|
703
710
|
title: "Update Fact Definition",
|
|
704
|
-
annotations:
|
|
711
|
+
annotations: DESTRUCTIVE,
|
|
705
712
|
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).",
|
|
706
713
|
inputSchema: z5.object({
|
|
707
714
|
factId: z5.string().uuid().describe("Fact definition ID"),
|
|
@@ -710,7 +717,7 @@ function registerFactTools(server, callApi) {
|
|
|
710
717
|
type: z5.enum(VALUE_TYPES).optional().describe(
|
|
711
718
|
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
|
|
712
719
|
),
|
|
713
|
-
isRequired: z5.boolean().optional().describe(
|
|
720
|
+
isRequired: z5.boolean().optional().describe(IS_REQUIRED_NOTE),
|
|
714
721
|
isPii: z5.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
|
|
715
722
|
valueDomain: z5.object({
|
|
716
723
|
allowedValues: z5.array(z5.union([z5.string(), z5.number()])).optional().describe(
|
|
@@ -729,7 +736,7 @@ function registerFactTools(server, callApi) {
|
|
|
729
736
|
"lexq_facts_delete",
|
|
730
737
|
{
|
|
731
738
|
title: "Delete Fact Definition",
|
|
732
|
-
annotations:
|
|
739
|
+
annotations: DESTRUCTIVE,
|
|
733
740
|
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.",
|
|
734
741
|
inputSchema: z5.object({
|
|
735
742
|
factId: z5.string().uuid().describe("Fact definition ID")
|
|
@@ -741,7 +748,7 @@ function registerFactTools(server, callApi) {
|
|
|
741
748
|
"lexq_facts_action_metadata",
|
|
742
749
|
{
|
|
743
750
|
title: "Get Action Runtime Fact Metadata",
|
|
744
|
-
annotations:
|
|
751
|
+
annotations: READ_ONLY,
|
|
745
752
|
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.",
|
|
746
753
|
inputSchema: z5.object({})
|
|
747
754
|
},
|
|
@@ -751,8 +758,8 @@ function registerFactTools(server, callApi) {
|
|
|
751
758
|
"lexq_facts_unregistered",
|
|
752
759
|
{
|
|
753
760
|
title: "List Unregistered Facts",
|
|
754
|
-
annotations:
|
|
755
|
-
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).
|
|
761
|
+
annotations: READ_ONLY,
|
|
762
|
+
description: "List facts referenced by a version's rules but not yet defined (read-only). Keys that rules only read do not block publish/deploy; a rule whose action targetVar is not a registered fact is rejected at save and publish with ACT-032. Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Registering one with lexq_facts_create before the version is published makes the engine check request values for it against its type (P-060) and any value domain (P-052).",
|
|
756
763
|
inputSchema: z5.object({
|
|
757
764
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
758
765
|
versionId: z5.string().uuid().describe("Version ID")
|
|
@@ -764,16 +771,20 @@ function registerFactTools(server, callApi) {
|
|
|
764
771
|
"lexq_facts_export",
|
|
765
772
|
{
|
|
766
773
|
title: "Export Fact Catalog",
|
|
767
|
-
annotations:
|
|
768
|
-
description:
|
|
774
|
+
annotations: READ_ONLY,
|
|
775
|
+
description: `Export the fact catalog. The two formats carry different things: CSV is the catalog as it stands, system facts included, for reading in a spreadsheet, without value domains; JSON lists each definition with only the fields a new definition takes (key, name, type, description, isRequired, isPii, valueDomain), without IDs. Returns the file contents as text. ${EXPORT_LIMIT_NOTE}`,
|
|
769
776
|
inputSchema: z5.object({
|
|
770
777
|
format: z5.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
|
|
771
778
|
keyword: z5.string().optional().describe("Filter by key or name")
|
|
772
779
|
})
|
|
773
780
|
},
|
|
774
|
-
async ({ format, keyword }) =>
|
|
775
|
-
|
|
776
|
-
|
|
781
|
+
async ({ format, keyword }) => limitExport(
|
|
782
|
+
await callApi("GET", "schema/facts/export", {
|
|
783
|
+
params: keyword ? { format, keyword } : { format }
|
|
784
|
+
}),
|
|
785
|
+
`lexq facts export --as ${format} --output <file>`,
|
|
786
|
+
"Pass keyword to export fewer facts."
|
|
787
|
+
)
|
|
777
788
|
);
|
|
778
789
|
}
|
|
779
790
|
|
|
@@ -785,7 +796,7 @@ function registerDeployTools(server, callApi) {
|
|
|
785
796
|
"lexq_deploy_publish",
|
|
786
797
|
{
|
|
787
798
|
title: "Publish Version",
|
|
788
|
-
annotations:
|
|
799
|
+
annotations: DESTRUCTIVE,
|
|
789
800
|
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.`,
|
|
790
801
|
inputSchema: z6.object({
|
|
791
802
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -799,7 +810,7 @@ function registerDeployTools(server, callApi) {
|
|
|
799
810
|
"lexq_deploy_live",
|
|
800
811
|
{
|
|
801
812
|
title: "Deploy to Live",
|
|
802
|
-
annotations:
|
|
813
|
+
annotations: DESTRUCTIVE,
|
|
803
814
|
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.",
|
|
804
815
|
inputSchema: z6.object({
|
|
805
816
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -815,7 +826,7 @@ function registerDeployTools(server, callApi) {
|
|
|
815
826
|
"lexq_deploy_rollback",
|
|
816
827
|
{
|
|
817
828
|
title: "Rollback Deployment",
|
|
818
|
-
annotations:
|
|
829
|
+
annotations: DESTRUCTIVE,
|
|
819
830
|
description: dedent2`
|
|
820
831
|
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).
|
|
821
832
|
|
|
@@ -836,7 +847,7 @@ function registerDeployTools(server, callApi) {
|
|
|
836
847
|
"lexq_deploy_undeploy",
|
|
837
848
|
{
|
|
838
849
|
title: "Undeploy",
|
|
839
|
-
annotations:
|
|
850
|
+
annotations: DESTRUCTIVE,
|
|
840
851
|
description: "Remove the live version from traffic. The version stays ACTIVE but no longer serves requests.",
|
|
841
852
|
inputSchema: z6.object({
|
|
842
853
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -851,7 +862,7 @@ function registerDeployTools(server, callApi) {
|
|
|
851
862
|
"lexq_deploy_schedule",
|
|
852
863
|
{
|
|
853
864
|
title: "Schedule Deployment",
|
|
854
|
-
annotations:
|
|
865
|
+
annotations: DESTRUCTIVE,
|
|
855
866
|
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).",
|
|
856
867
|
inputSchema: z6.object({
|
|
857
868
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
@@ -867,7 +878,7 @@ function registerDeployTools(server, callApi) {
|
|
|
867
878
|
"lexq_deploy_unschedule",
|
|
868
879
|
{
|
|
869
880
|
title: "Cancel Scheduled Deployment",
|
|
870
|
-
annotations:
|
|
881
|
+
annotations: DESTRUCTIVE,
|
|
871
882
|
description: "Cancel the pending scheduled deployment for a group. The version itself is not affected. Fails with P-039 if no pending schedule exists.",
|
|
872
883
|
inputSchema: z6.object({
|
|
873
884
|
groupId: z6.string().uuid().describe("Policy group ID")
|
|
@@ -879,8 +890,8 @@ function registerDeployTools(server, callApi) {
|
|
|
879
890
|
"lexq_deploy_schedules",
|
|
880
891
|
{
|
|
881
892
|
title: "List Scheduled Deployments",
|
|
882
|
-
annotations:
|
|
883
|
-
description:
|
|
893
|
+
annotations: READ_ONLY,
|
|
894
|
+
description: `List scheduled deployments across all groups (all statuses: ${DeploymentScheduleStatus.join(", ")}), newest first.`,
|
|
884
895
|
inputSchema: z6.object({
|
|
885
896
|
page: z6.number().int().min(0).default(0).describe("Page number"),
|
|
886
897
|
size: z6.number().int().min(1).max(100).default(20).describe("Page size")
|
|
@@ -892,13 +903,13 @@ function registerDeployTools(server, callApi) {
|
|
|
892
903
|
"lexq_deploy_history",
|
|
893
904
|
{
|
|
894
905
|
title: "Deployment History",
|
|
895
|
-
annotations:
|
|
906
|
+
annotations: READ_ONLY,
|
|
896
907
|
description: "List deployment history across all groups.",
|
|
897
908
|
inputSchema: z6.object({
|
|
898
909
|
page: z6.number().int().min(0).default(0).describe("Page number"),
|
|
899
910
|
size: z6.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
900
911
|
groupId: z6.string().uuid().optional().describe("Filter by group ID"),
|
|
901
|
-
types: z6.string().optional().describe(
|
|
912
|
+
types: z6.string().optional().describe(`Filter by deployment types (comma-separated: ${DeploymentType.join(",")})`),
|
|
902
913
|
startDate: z6.string().optional().describe(START_DAY),
|
|
903
914
|
endDate: z6.string().optional().describe(END_DAY)
|
|
904
915
|
})
|
|
@@ -916,7 +927,7 @@ function registerDeployTools(server, callApi) {
|
|
|
916
927
|
"lexq_deploy_detail",
|
|
917
928
|
{
|
|
918
929
|
title: "Deployment Detail",
|
|
919
|
-
annotations:
|
|
930
|
+
annotations: READ_ONLY,
|
|
920
931
|
description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
|
|
921
932
|
inputSchema: z6.object({
|
|
922
933
|
deploymentId: z6.string().uuid().describe("Deployment ID")
|
|
@@ -928,7 +939,7 @@ function registerDeployTools(server, callApi) {
|
|
|
928
939
|
"lexq_deploy_overview",
|
|
929
940
|
{
|
|
930
941
|
title: "Deployment Overview",
|
|
931
|
-
annotations:
|
|
942
|
+
annotations: READ_ONLY,
|
|
932
943
|
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.",
|
|
933
944
|
inputSchema: z6.object({})
|
|
934
945
|
},
|
|
@@ -938,7 +949,7 @@ function registerDeployTools(server, callApi) {
|
|
|
938
949
|
"lexq_deploy_deployable",
|
|
939
950
|
{
|
|
940
951
|
title: "List Deployable Versions",
|
|
941
|
-
annotations:
|
|
952
|
+
annotations: READ_ONLY,
|
|
942
953
|
description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
|
|
943
954
|
inputSchema: z6.object({
|
|
944
955
|
groupId: z6.string().uuid().describe("Policy group ID")
|
|
@@ -950,7 +961,7 @@ function registerDeployTools(server, callApi) {
|
|
|
950
961
|
"lexq_deploy_diff",
|
|
951
962
|
{
|
|
952
963
|
title: "Deployment Diff",
|
|
953
|
-
annotations:
|
|
964
|
+
annotations: READ_ONLY,
|
|
954
965
|
description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
|
|
955
966
|
inputSchema: z6.object({
|
|
956
967
|
baseVersionId: z6.string().uuid().describe("Base version ID (typically the current live)"),
|
|
@@ -978,7 +989,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
978
989
|
"lexq_dry_run",
|
|
979
990
|
{
|
|
980
991
|
title: "Dry Run",
|
|
981
|
-
annotations:
|
|
992
|
+
annotations: READ_ONLY,
|
|
982
993
|
description: dedent3`
|
|
983
994
|
Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
|
|
984
995
|
|
|
@@ -989,8 +1000,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
989
1000
|
executionTraces — per-rule match status
|
|
990
1001
|
decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
|
|
991
1002
|
|
|
992
|
-
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
|
|
993
|
-
Always dry-run before publishing to validate rule behavior.`,
|
|
1003
|
+
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }`,
|
|
994
1004
|
inputSchema: z7.object({
|
|
995
1005
|
versionId: z7.string().uuid().describe("Policy version ID to test against"),
|
|
996
1006
|
facts: z7.string().describe('JSON string of facts object, e.g. {"paymentAmount":100000}'),
|
|
@@ -1008,7 +1018,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1008
1018
|
"lexq_dry_run_compare",
|
|
1009
1019
|
{
|
|
1010
1020
|
title: "Dry Run Compare",
|
|
1011
|
-
annotations:
|
|
1021
|
+
annotations: READ_ONLY,
|
|
1012
1022
|
description: dedent3`
|
|
1013
1023
|
Compare dry run results between two versions using the same input facts. Useful for validating changes.
|
|
1014
1024
|
|
|
@@ -1033,7 +1043,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1033
1043
|
"lexq_requirements",
|
|
1034
1044
|
{
|
|
1035
1045
|
title: "Analyze Requirements",
|
|
1036
|
-
annotations:
|
|
1046
|
+
annotations: READ_ONLY,
|
|
1037
1047
|
description: "Analyze which input facts a version requires. Returns required keys, types, and an example request body.",
|
|
1038
1048
|
inputSchema: z7.object({
|
|
1039
1049
|
groupId: z7.string().uuid().describe("Policy group ID"),
|
|
@@ -1046,10 +1056,13 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1046
1056
|
"lexq_simulation_start",
|
|
1047
1057
|
{
|
|
1048
1058
|
title: "Start Simulation",
|
|
1049
|
-
annotations:
|
|
1059
|
+
annotations: DESTRUCTIVE,
|
|
1050
1060
|
description: dedent3`
|
|
1051
1061
|
Start an Impact Simulation against historical, uploaded, or inline data.
|
|
1052
1062
|
|
|
1063
|
+
Starting checks the plan's simulation usage limit. The records a simulation processes
|
|
1064
|
+
count toward that usage when it completes or is canceled.
|
|
1065
|
+
|
|
1053
1066
|
dataset.type and dataset.source are BOTH required, and must be paired:
|
|
1054
1067
|
HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
|
|
1055
1068
|
in the organization time zone, both inclusive)
|
|
@@ -1061,7 +1074,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1061
1074
|
options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
|
|
1062
1075
|
options.includeRuleStats: boolean
|
|
1063
1076
|
options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
|
|
1064
|
-
{ "targetVariable": "<fact>", "aggregationType":
|
|
1077
|
+
{ "targetVariable": "<fact>", "aggregationType": ${SimulationMetricType.map((t) => `"${t}"`).join(" | ")} }
|
|
1065
1078
|
|
|
1066
1079
|
Example (uploaded dataset):
|
|
1067
1080
|
{
|
|
@@ -1090,7 +1103,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1090
1103
|
"lexq_simulation_status",
|
|
1091
1104
|
{
|
|
1092
1105
|
title: "Simulation Status",
|
|
1093
|
-
annotations:
|
|
1106
|
+
annotations: READ_ONLY,
|
|
1094
1107
|
description: "Get simulation status and results. Poll until status is COMPLETED or FAILED.",
|
|
1095
1108
|
inputSchema: z7.object({
|
|
1096
1109
|
simulationId: z7.string().uuid().describe("Simulation ID")
|
|
@@ -1102,12 +1115,12 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1102
1115
|
"lexq_simulation_list",
|
|
1103
1116
|
{
|
|
1104
1117
|
title: "List Simulations",
|
|
1105
|
-
annotations:
|
|
1118
|
+
annotations: READ_ONLY,
|
|
1106
1119
|
description: "List simulation history with optional filters.",
|
|
1107
1120
|
inputSchema: z7.object({
|
|
1108
1121
|
page: z7.number().int().min(0).default(0).describe("Page number"),
|
|
1109
1122
|
size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
1110
|
-
status: z7.enum(
|
|
1123
|
+
status: z7.enum(SimulationStatus).optional().describe("Filter by status"),
|
|
1111
1124
|
from: z7.string().optional().describe(START_DAY),
|
|
1112
1125
|
to: z7.string().optional().describe(END_DAY)
|
|
1113
1126
|
})
|
|
@@ -1124,7 +1137,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1124
1137
|
"lexq_simulation_cancel",
|
|
1125
1138
|
{
|
|
1126
1139
|
title: "Cancel Simulation",
|
|
1127
|
-
annotations:
|
|
1140
|
+
annotations: DESTRUCTIVE,
|
|
1128
1141
|
description: "Cancel a running or pending simulation.",
|
|
1129
1142
|
inputSchema: z7.object({
|
|
1130
1143
|
simulationId: z7.string().uuid().describe("Simulation ID")
|
|
@@ -1136,22 +1149,25 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1136
1149
|
"lexq_simulation_export",
|
|
1137
1150
|
{
|
|
1138
1151
|
title: "Export Simulation",
|
|
1139
|
-
annotations:
|
|
1140
|
-
description:
|
|
1152
|
+
annotations: READ_ONLY,
|
|
1153
|
+
description: `Export simulation results as JSON or CSV. Returns the raw data. ${EXPORT_LIMIT_NOTE}`,
|
|
1141
1154
|
inputSchema: z7.object({
|
|
1142
1155
|
simulationId: z7.string().uuid().describe("Simulation ID"),
|
|
1143
1156
|
format: z7.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
1144
1157
|
})
|
|
1145
1158
|
},
|
|
1146
|
-
async ({ simulationId, format }) =>
|
|
1147
|
-
|
|
1148
|
-
|
|
1159
|
+
async ({ simulationId, format }) => limitExport(
|
|
1160
|
+
await callApi("GET", `analytics/simulations/${simulationId}/export`, {
|
|
1161
|
+
params: { format }
|
|
1162
|
+
}),
|
|
1163
|
+
`lexq analytics simulation export --id ${simulationId} --as ${format} --output <file>`
|
|
1164
|
+
)
|
|
1149
1165
|
);
|
|
1150
1166
|
server.registerTool(
|
|
1151
1167
|
"lexq_dataset_upload",
|
|
1152
1168
|
{
|
|
1153
1169
|
title: "Upload Dataset",
|
|
1154
|
-
annotations:
|
|
1170
|
+
annotations: ADDITIVE,
|
|
1155
1171
|
description: dedent3`
|
|
1156
1172
|
Upload inline CSV or JSON content as a simulation dataset.
|
|
1157
1173
|
The content is uploaded to S3 and a path is returned in the "path" field.
|
|
@@ -1194,7 +1210,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1194
1210
|
"lexq_dataset_template",
|
|
1195
1211
|
{
|
|
1196
1212
|
title: "Download Dataset Template",
|
|
1197
|
-
annotations:
|
|
1213
|
+
annotations: READ_ONLY,
|
|
1198
1214
|
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.",
|
|
1199
1215
|
inputSchema: z7.object({
|
|
1200
1216
|
groupId: z7.string().uuid().describe("Policy group ID"),
|
|
@@ -1210,6 +1226,15 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
1210
1226
|
|
|
1211
1227
|
// src/mcp/tools/profile.ts
|
|
1212
1228
|
import { z as z8 } from "zod";
|
|
1229
|
+
|
|
1230
|
+
// src/lib/percentiles.ts
|
|
1231
|
+
var PERCENTILE_MIN_N = {
|
|
1232
|
+
p50: TAIL_MIN_OBS * 2,
|
|
1233
|
+
p95: TAIL_MIN_OBS * 20,
|
|
1234
|
+
p99: TAIL_MIN_OBS * 100
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
// src/mcp/tools/profile.ts
|
|
1213
1238
|
var RELATIVE_THRESHOLD = `flagged = p50 \u2265 ${SLOW_MULTIPLIER}\xD7 median of per-rule p50s within the group; absolute thresholds are intentionally not supported.`;
|
|
1214
1239
|
function profileParams(opts) {
|
|
1215
1240
|
const params = {};
|
|
@@ -1224,8 +1249,8 @@ function registerProfileTools(server, callApi) {
|
|
|
1224
1249
|
"lexq_profile_overview",
|
|
1225
1250
|
{
|
|
1226
1251
|
title: "Group Latency Profile",
|
|
1227
|
-
annotations:
|
|
1228
|
-
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
|
|
1252
|
+
annotations: READ_ONLY,
|
|
1253
|
+
description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cache state (HIT = compiled ruleset cache hit, MISS = deep-load + compile), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD + ` Every percentile is accompanied by its sample count n; a percentile is withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 needs n \u2265 ${PERCENTILE_MIN_N.p50}, p95 n \u2265 ${PERCENTILE_MIN_N.p95}, p99 n \u2265 ${PERCENTILE_MIN_N.p99} \u2014 display gate, separate from the n \u2265 ${MIN_SAMPLES} judgment gate). Baselines report INSUFFICIENT_COHORT when fewer than ${MIN_COHORT_SIZE} rules qualify. Rule detail comes from a deterministic ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls; TOTAL is recorded for every call. Defaults: last ${PROFILE_DEFAULT_WINDOW_HOURS}h, live version, cacheState HIT.`,
|
|
1229
1254
|
inputSchema: z8.object({
|
|
1230
1255
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
1231
1256
|
versionId: z8.string().uuid().optional().describe("Version to inspect (default: live version)"),
|
|
@@ -1233,7 +1258,7 @@ function registerProfileTools(server, callApi) {
|
|
|
1233
1258
|
`Window start, ISO-8601 instant (e.g. 2026-07-01T00:00:00Z). Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
|
|
1234
1259
|
),
|
|
1235
1260
|
to: z8.string().optional().describe("Window end, ISO-8601 instant. Default: now"),
|
|
1236
|
-
cacheState: z8.enum(
|
|
1261
|
+
cacheState: z8.enum(ProfileCacheState).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
|
|
1237
1262
|
})
|
|
1238
1263
|
},
|
|
1239
1264
|
async ({ groupId, versionId, from, to, cacheState }) => callApi("GET", `policy-groups/${groupId}/profile`, {
|
|
@@ -1244,8 +1269,8 @@ function registerProfileTools(server, callApi) {
|
|
|
1244
1269
|
"lexq_profile_rule",
|
|
1245
1270
|
{
|
|
1246
1271
|
title: "Rule Latency Detail",
|
|
1247
|
-
annotations:
|
|
1248
|
-
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
|
|
1272
|
+
annotations: READ_ONLY,
|
|
1273
|
+
description: `Single-rule latency detail: merged phase \xD7 cacheState distributions plus a per-window time series (${LATENCY_WINDOW_MILLIS / 1e3}s windows). Missing windows are genuine gaps \u2014 never interpolated. Series points carry each window's own values; percentiles in merged distributions are withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 n \u2265 ${PERCENTILE_MIN_N.p50}, p95 n \u2265 ${PERCENTILE_MIN_N.p95}, p99 n \u2265 ${PERCENTILE_MIN_N.p99}). ` + RELATIVE_THRESHOLD,
|
|
1249
1274
|
inputSchema: z8.object({
|
|
1250
1275
|
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
1251
1276
|
ruleId: z8.string().uuid().describe("Rule ID (from lexq_profile_overview)"),
|
|
@@ -1269,7 +1294,7 @@ function registerReplayTools(server, callApi) {
|
|
|
1269
1294
|
"lexq_replay_decision",
|
|
1270
1295
|
{
|
|
1271
1296
|
title: "Replay a Decision",
|
|
1272
|
-
annotations:
|
|
1297
|
+
annotations: READ_ONLY,
|
|
1273
1298
|
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.",
|
|
1274
1299
|
inputSchema: z9.object({
|
|
1275
1300
|
traceId: z9.string().describe("Trace ID of the past execution to replay"),
|
|
@@ -1282,7 +1307,7 @@ function registerReplayTools(server, callApi) {
|
|
|
1282
1307
|
"lexq_replay_start",
|
|
1283
1308
|
{
|
|
1284
1309
|
title: "Start Window Replay (Blast Radius)",
|
|
1285
|
-
annotations:
|
|
1310
|
+
annotations: DESTRUCTIVE,
|
|
1286
1311
|
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.",
|
|
1287
1312
|
inputSchema: z9.object({
|
|
1288
1313
|
candidateVersionId: z9.string().uuid().describe("Version to re-evaluate against"),
|
|
@@ -1299,7 +1324,7 @@ function registerReplayTools(server, callApi) {
|
|
|
1299
1324
|
"lexq_replay_status",
|
|
1300
1325
|
{
|
|
1301
1326
|
title: "Get Replay Job Status",
|
|
1302
|
-
annotations:
|
|
1327
|
+
annotations: READ_ONLY,
|
|
1303
1328
|
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.",
|
|
1304
1329
|
inputSchema: z9.object({
|
|
1305
1330
|
jobId: z9.string().describe("Replay job ID from lexq_replay_start")
|
|
@@ -1311,7 +1336,7 @@ function registerReplayTools(server, callApi) {
|
|
|
1311
1336
|
"lexq_replay_list",
|
|
1312
1337
|
{
|
|
1313
1338
|
title: "List Replay Jobs",
|
|
1314
|
-
annotations:
|
|
1339
|
+
annotations: READ_ONLY,
|
|
1315
1340
|
description: "List window replay job history (reverse-chronological). Lightweight items \u2014 use lexq_replay_status for summary and changed samples.",
|
|
1316
1341
|
inputSchema: z9.object({
|
|
1317
1342
|
page: z9.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -1324,7 +1349,7 @@ function registerReplayTools(server, callApi) {
|
|
|
1324
1349
|
"lexq_replay_cancel",
|
|
1325
1350
|
{
|
|
1326
1351
|
title: "Cancel Replay Job",
|
|
1327
|
-
annotations:
|
|
1352
|
+
annotations: DESTRUCTIVE,
|
|
1328
1353
|
description: "Cooperatively cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
|
|
1329
1354
|
inputSchema: z9.object({
|
|
1330
1355
|
jobId: z9.string().describe("Replay job ID")
|
|
@@ -1336,14 +1361,17 @@ function registerReplayTools(server, callApi) {
|
|
|
1336
1361
|
"lexq_replay_export",
|
|
1337
1362
|
{
|
|
1338
1363
|
title: "Export Replay Result",
|
|
1339
|
-
annotations:
|
|
1340
|
-
description:
|
|
1364
|
+
annotations: READ_ONLY,
|
|
1365
|
+
description: `Export a COMPLETED window replay job. A running job is rejected \u2014 a partial result reads as the whole thing on the receiving end. The two formats carry different things: CSV holds the effect blast radius, whose columns are fixed; JSON holds the changed samples and action parameters, which nest and whose keys differ per tenant. JSON is not a superset of CSV. Returns the file contents as text. ${EXPORT_LIMIT_NOTE}`,
|
|
1341
1366
|
inputSchema: z9.object({
|
|
1342
1367
|
jobId: z9.string().describe("Replay job ID"),
|
|
1343
1368
|
format: z9.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
1344
1369
|
})
|
|
1345
1370
|
},
|
|
1346
|
-
async ({ jobId, format }) =>
|
|
1371
|
+
async ({ jobId, format }) => limitExport(
|
|
1372
|
+
await callApi("GET", `replay/jobs/${jobId}/export`, { params: { format } }),
|
|
1373
|
+
`lexq replay export --id ${jobId} --as ${format} --output <file>`
|
|
1374
|
+
)
|
|
1347
1375
|
);
|
|
1348
1376
|
}
|
|
1349
1377
|
|
|
@@ -1354,7 +1382,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
1354
1382
|
"lexq_history_list",
|
|
1355
1383
|
{
|
|
1356
1384
|
title: "List Execution History",
|
|
1357
|
-
annotations:
|
|
1385
|
+
annotations: READ_ONLY,
|
|
1358
1386
|
description: "List policy execution history. Shows trace ID, group, version, status, match result, and latency.",
|
|
1359
1387
|
inputSchema: z10.object({
|
|
1360
1388
|
page: z10.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -1362,7 +1390,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
1362
1390
|
traceId: z10.string().optional().describe("Filter by trace ID"),
|
|
1363
1391
|
groupId: z10.string().uuid().optional().describe("Filter by policy group"),
|
|
1364
1392
|
versionId: z10.string().uuid().optional().describe("Filter by version"),
|
|
1365
|
-
status: z10.enum(
|
|
1393
|
+
status: z10.enum(ApiExecutionStatus).optional().describe("Filter by execution status"),
|
|
1366
1394
|
startDate: z10.string().optional().describe(START_DAY),
|
|
1367
1395
|
endDate: z10.string().optional().describe(END_DAY)
|
|
1368
1396
|
})
|
|
@@ -1382,7 +1410,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
1382
1410
|
"lexq_history_get",
|
|
1383
1411
|
{
|
|
1384
1412
|
title: "Get Execution Detail",
|
|
1385
|
-
annotations:
|
|
1413
|
+
annotations: READ_ONLY,
|
|
1386
1414
|
description: "Get full execution detail including inputFacts, mutatedFacts, generatedVariables, executionTraces, and decisionTraces.",
|
|
1387
1415
|
inputSchema: z10.object({
|
|
1388
1416
|
traceId: z10.string().describe("Trace ID from execution history")
|
|
@@ -1394,7 +1422,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
1394
1422
|
"lexq_history_stats",
|
|
1395
1423
|
{
|
|
1396
1424
|
title: "Execution Statistics",
|
|
1397
|
-
annotations:
|
|
1425
|
+
annotations: READ_ONLY,
|
|
1398
1426
|
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.`,
|
|
1399
1427
|
inputSchema: z10.object({
|
|
1400
1428
|
groupId: z10.string().uuid().optional().describe("Filter by policy group"),
|
|
@@ -1419,7 +1447,7 @@ function registerProvenanceTools(server, callApi) {
|
|
|
1419
1447
|
"lexq_provenance_get",
|
|
1420
1448
|
{
|
|
1421
1449
|
title: "Get Decision Provenance",
|
|
1422
|
-
annotations:
|
|
1450
|
+
annotations: READ_ONLY,
|
|
1423
1451
|
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.",
|
|
1424
1452
|
inputSchema: z11.object({
|
|
1425
1453
|
traceId: z11.string().describe("Trace ID of the execution")
|
|
@@ -1431,7 +1459,7 @@ function registerProvenanceTools(server, callApi) {
|
|
|
1431
1459
|
"lexq_pii_reveals_list",
|
|
1432
1460
|
{
|
|
1433
1461
|
title: "List PII Reveal Audits",
|
|
1434
|
-
annotations:
|
|
1462
|
+
annotations: READ_ONLY,
|
|
1435
1463
|
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.",
|
|
1436
1464
|
inputSchema: z11.object({
|
|
1437
1465
|
page: z11.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -1462,8 +1490,8 @@ function registerLogTools(server, callApi) {
|
|
|
1462
1490
|
"lexq_logs_list",
|
|
1463
1491
|
{
|
|
1464
1492
|
title: "List Failure Logs",
|
|
1465
|
-
annotations:
|
|
1466
|
-
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
|
|
1493
|
+
annotations: READ_ONLY,
|
|
1494
|
+
description: `List system failure logs from background tasks (platform event webhooks, scheduled deployments). Without dates the window is ${FAILURE_LOG_LOOKBACK_DAYS} days back through today, in the organization time zone; pass startDate to look further back. ${FAILURE_LOG_CONSOLE_NOTE}`,
|
|
1467
1495
|
inputSchema: z12.object({
|
|
1468
1496
|
page: z12.number().int().min(0).default(0).describe("Page number"),
|
|
1469
1497
|
size: z12.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
@@ -1488,54 +1516,35 @@ function registerLogTools(server, callApi) {
|
|
|
1488
1516
|
"lexq_logs_get",
|
|
1489
1517
|
{
|
|
1490
1518
|
title: "Get Failure Log",
|
|
1491
|
-
annotations:
|
|
1492
|
-
description:
|
|
1519
|
+
annotations: READ_ONLY,
|
|
1520
|
+
description: `Get failure log detail by ID. ${FAILURE_LOG_CONSOLE_NOTE}`,
|
|
1493
1521
|
inputSchema: z12.object({
|
|
1494
1522
|
logId: z12.string().uuid().describe("Failure log ID")
|
|
1495
1523
|
})
|
|
1496
1524
|
},
|
|
1497
1525
|
async ({ logId }) => callApi("GET", `failure-logs/${logId}`)
|
|
1498
1526
|
);
|
|
1499
|
-
server.registerTool(
|
|
1500
|
-
"lexq_logs_action",
|
|
1501
|
-
{
|
|
1502
|
-
title: "Process Failure Log",
|
|
1503
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
1504
|
-
description: "Process a single failure log: RESOLVE (mark as manually fixed) or IGNORE (skip intentionally).",
|
|
1505
|
-
inputSchema: z12.object({
|
|
1506
|
-
logId: z12.string().uuid().describe("Failure log ID"),
|
|
1507
|
-
action: z12.enum(FailureAction).describe("Action to take")
|
|
1508
|
-
})
|
|
1509
|
-
},
|
|
1510
|
-
async ({ logId, action }) => callApi("POST", `failure-logs/${logId}/actions`, {
|
|
1511
|
-
params: { action }
|
|
1512
|
-
})
|
|
1513
|
-
);
|
|
1514
|
-
server.registerTool(
|
|
1515
|
-
"lexq_logs_bulk_action",
|
|
1516
|
-
{
|
|
1517
|
-
title: "Bulk Process Failure Logs",
|
|
1518
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
1519
|
-
description: "Process multiple failure logs at once. Provide an array of log IDs and the action.",
|
|
1520
|
-
inputSchema: z12.object({
|
|
1521
|
-
logIds: z12.array(z12.string().uuid()).describe("Array of failure log IDs"),
|
|
1522
|
-
action: z12.enum(FailureAction).describe("Action to apply to all logs")
|
|
1523
|
-
})
|
|
1524
|
-
},
|
|
1525
|
-
async ({ logIds, action }) => callApi("POST", "failure-logs/bulk-actions", {
|
|
1526
|
-
body: { logIds, action }
|
|
1527
|
-
})
|
|
1528
|
-
);
|
|
1529
1527
|
}
|
|
1530
1528
|
|
|
1531
1529
|
// src/mcp/tools/webhook-subscriptions.ts
|
|
1532
1530
|
import { z as z13 } from "zod";
|
|
1531
|
+
|
|
1532
|
+
// src/lib/enum-notes.ts
|
|
1533
|
+
function withNotes(notes) {
|
|
1534
|
+
return Object.entries(notes).map(([value, note]) => note ? `${value} (${note})` : value);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// src/mcp/tools/webhook-subscriptions.ts
|
|
1538
|
+
var FORMAT_NOTES = {
|
|
1539
|
+
GENERIC: "full JSON",
|
|
1540
|
+
SLACK: '{"text": "..."}'
|
|
1541
|
+
};
|
|
1533
1542
|
function registerWebhookSubscriptionTools(server, callApi) {
|
|
1534
1543
|
server.registerTool(
|
|
1535
1544
|
"lexq_webhook_subscriptions_list",
|
|
1536
1545
|
{
|
|
1537
1546
|
title: "List Webhook Subscriptions",
|
|
1538
|
-
annotations:
|
|
1547
|
+
annotations: READ_ONLY,
|
|
1539
1548
|
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.",
|
|
1540
1549
|
inputSchema: z13.object({
|
|
1541
1550
|
page: z13.number().int().min(0).default(0).describe("Page number"),
|
|
@@ -1551,7 +1560,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
1551
1560
|
"lexq_webhook_subscriptions_get",
|
|
1552
1561
|
{
|
|
1553
1562
|
title: "Get Webhook Subscription",
|
|
1554
|
-
annotations:
|
|
1563
|
+
annotations: READ_ONLY,
|
|
1555
1564
|
description: "Get webhook subscription detail by ID.",
|
|
1556
1565
|
inputSchema: z13.object({
|
|
1557
1566
|
id: z13.string().uuid().describe("Webhook subscription ID")
|
|
@@ -1563,8 +1572,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
1563
1572
|
"lexq_webhook_subscriptions_save",
|
|
1564
1573
|
{
|
|
1565
1574
|
title: "Save Webhook Subscription",
|
|
1566
|
-
annotations: {
|
|
1567
|
-
description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats:
|
|
1575
|
+
annotations: { ...DESTRUCTIVE, openWorldHint: true },
|
|
1576
|
+
description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: ${withNotes(FORMAT_NOTES).join(", ")}.`,
|
|
1568
1577
|
inputSchema: z13.object({
|
|
1569
1578
|
id: z13.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
|
|
1570
1579
|
name: z13.string().min(1).describe("Subscription name (unique per tenant)"),
|
|
@@ -1581,7 +1590,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
1581
1590
|
"lexq_webhook_subscriptions_delete",
|
|
1582
1591
|
{
|
|
1583
1592
|
title: "Delete Webhook Subscription",
|
|
1584
|
-
annotations:
|
|
1593
|
+
annotations: DESTRUCTIVE,
|
|
1585
1594
|
description: "Delete a webhook subscription by ID.",
|
|
1586
1595
|
inputSchema: z13.object({
|
|
1587
1596
|
id: z13.string().uuid().describe("Webhook subscription ID")
|
|
@@ -1593,7 +1602,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
1593
1602
|
"lexq_webhook_subscriptions_test",
|
|
1594
1603
|
{
|
|
1595
1604
|
title: "Test Webhook Subscription",
|
|
1596
|
-
annotations: {
|
|
1605
|
+
annotations: { ...DESTRUCTIVE, openWorldHint: true },
|
|
1597
1606
|
description: "Send a test event to verify webhook connectivity. Returns the HTTP status code and success/failure message.",
|
|
1598
1607
|
inputSchema: z13.object({
|
|
1599
1608
|
id: z13.string().uuid().describe("Webhook subscription ID")
|
|
@@ -1610,7 +1619,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
1610
1619
|
"lexq_domain_templates_list",
|
|
1611
1620
|
{
|
|
1612
1621
|
title: "List Domain Templates",
|
|
1613
|
-
annotations:
|
|
1622
|
+
annotations: READ_ONLY,
|
|
1614
1623
|
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.",
|
|
1615
1624
|
inputSchema: z14.object({})
|
|
1616
1625
|
},
|
|
@@ -1620,7 +1629,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
1620
1629
|
"lexq_domain_templates_preview",
|
|
1621
1630
|
{
|
|
1622
1631
|
title: "Preview Domain Template",
|
|
1623
|
-
annotations:
|
|
1632
|
+
annotations: READ_ONLY,
|
|
1624
1633
|
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.",
|
|
1625
1634
|
inputSchema: z14.object({
|
|
1626
1635
|
template: z14.string().describe(
|
|
@@ -1634,7 +1643,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
1634
1643
|
"lexq_domain_templates_apply",
|
|
1635
1644
|
{
|
|
1636
1645
|
title: "Apply Domain Template",
|
|
1637
|
-
annotations:
|
|
1646
|
+
annotations: ADDITIVE,
|
|
1638
1647
|
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.",
|
|
1639
1648
|
inputSchema: z14.object({
|
|
1640
1649
|
template: z14.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
|
|
@@ -1669,10 +1678,18 @@ function registerAllTools(server, callApi) {
|
|
|
1669
1678
|
registerWebhookSubscriptionTools(server, callApi);
|
|
1670
1679
|
}
|
|
1671
1680
|
var SERVER_INFO = { name: "lexq", ...server_info_default };
|
|
1681
|
+
var SERVER_INSTRUCTIONS = [
|
|
1682
|
+
"LexQ evaluates business rules: a policy group holds versions, a version holds rules, and rules read input facts.",
|
|
1683
|
+
"- Date filters (yyyy-MM-dd) are whole days in the organization's time zone, not UTC. Call lexq_whoami for tenantTimezone before working out the current date or a date range.",
|
|
1684
|
+
"- Before creating rules, call lexq_facts_list to see which facts exist and their types. When a rule needs a fact that is not defined, confirm its type and description with the user before calling lexq_facts_create.",
|
|
1685
|
+
"- Before lexq_deploy_publish, run lexq_dry_run on the version with representative facts. A published version cannot go back to draft.",
|
|
1686
|
+
`- For anything these tools cannot do, point the user to the LexQ console at ${CONSOLE_URL}.`
|
|
1687
|
+
].join("\n");
|
|
1672
1688
|
export {
|
|
1673
1689
|
ApiError,
|
|
1674
1690
|
LosslessNumber,
|
|
1675
1691
|
SERVER_INFO,
|
|
1692
|
+
SERVER_INSTRUCTIONS,
|
|
1676
1693
|
formatUnregisteredFactWarning,
|
|
1677
1694
|
formatValueDomainMismatchWarning,
|
|
1678
1695
|
isLosslessNumber,
|