@lexq/cli 1.1.1 → 1.1.3
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 +2 -2
- package/CONTEXT.md +5 -5
- package/README.md +2 -2
- package/dist/index.js +70 -29
- package/dist/mcp/register.d.ts +11 -2
- package/dist/mcp/register.js +31 -10
- package/package.json +1 -1
- package/skills/lexq-groups/SKILL.md +38 -11
- package/skills/lexq-shared/SKILL.md +3 -0
package/AGENTS.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## What is this?
|
|
6
6
|
|
|
7
7
|
LexQ CLI (`@lexq/cli`, binary: `lexq`) manages a policy execution engine. Policies are business rules (if-then) that
|
|
8
|
-
evaluate input facts and produce actions
|
|
8
|
+
evaluate input facts and produce actions: change a number such as a discount, set a fact, or record a rejection.
|
|
9
9
|
|
|
10
10
|
The CLI also doubles as an **MCP server** — run `lexq serve --mcp` to expose the full command inventory as MCP tools to
|
|
11
11
|
any MCP-compatible AI client.
|
|
@@ -106,7 +106,7 @@ lexq deploy live --group-id $GID --version-id $VID --memo "Initial deploy"
|
|
|
106
106
|
|
|
107
107
|
## Example: Subscribe to Deployment Notifications
|
|
108
108
|
|
|
109
|
-
Webhook subscriptions fire on
|
|
109
|
+
Webhook subscriptions fire on deployment lifecycle events.
|
|
110
110
|
|
|
111
111
|
```bash
|
|
112
112
|
# Subscribe to Slack for deployment events
|
package/CONTEXT.md
CHANGED
|
@@ -64,11 +64,11 @@ Tenant
|
|
|
64
64
|
|
|
65
65
|
### Policy Group Status
|
|
66
66
|
|
|
67
|
-
| Value | Description
|
|
68
|
-
|
|
69
|
-
| `ACTIVE` | Normal operation
|
|
70
|
-
| `DISABLED` |
|
|
71
|
-
| `ARCHIVED` |
|
|
67
|
+
| Value | Description |
|
|
68
|
+
|------------|---------------------------------------------------------------------------------------------------------------------|
|
|
69
|
+
| `ACTIVE` | Normal operation |
|
|
70
|
+
| `DISABLED` | Production execution rejected (`P-009`); dry run, simulation, and replay still work |
|
|
71
|
+
| `ARCHIVED` | Deleted. Kept for audit if it was ever executed, deployed, or replayed; otherwise permanently deleted after 30 days |
|
|
72
72
|
|
|
73
73
|
### Policy Version Status
|
|
74
74
|
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ Requires **Node.js 22+**.
|
|
|
48
48
|
```bash
|
|
49
49
|
# 1. Authenticate
|
|
50
50
|
lexq auth login
|
|
51
|
-
# Enter your API key (create one at console.lexq.io → Management → API Keys)
|
|
51
|
+
# Enter your API key (create one with the Manage scope at console.lexq.io → Management → API Keys)
|
|
52
52
|
|
|
53
53
|
# 2. Verify
|
|
54
54
|
lexq auth whoami
|
|
@@ -280,7 +280,7 @@ pnpm enums:check # Generated enums match the engine contract
|
|
|
280
280
|
pnpm prose:check # Numbers and code tables in prose match the contract
|
|
281
281
|
pnpm ab-key:check # Tool descriptions still name the A/B traffic key
|
|
282
282
|
pnpm surface:check # Nothing internal reached a public file
|
|
283
|
-
pnpm registry:check # server.json agrees with package.json
|
|
283
|
+
pnpm registry:check # server.json agrees with package.json and src/mcp/server-info.json
|
|
284
284
|
pnpm schemas:check # tools/list matches the recorded snapshot (needs a build)
|
|
285
285
|
pnpm test:fact-key # Fact key grammar
|
|
286
286
|
pnpm test:decimals # Metric precision display
|
package/dist/index.js
CHANGED
|
@@ -163,6 +163,10 @@ import Table from "cli-table3";
|
|
|
163
163
|
function printJson(data) {
|
|
164
164
|
console.log(stringifyJson(data, 2));
|
|
165
165
|
}
|
|
166
|
+
function groupLabel(name, archivedAt, fallback = "\u2013") {
|
|
167
|
+
const label = name ?? fallback;
|
|
168
|
+
return archivedAt ? `(deleted) ${label}` : label;
|
|
169
|
+
}
|
|
166
170
|
function printTable(headers, rows, options) {
|
|
167
171
|
const table = new Table({
|
|
168
172
|
head: headers,
|
|
@@ -227,7 +231,8 @@ function registerAuthCommands(program) {
|
|
|
227
231
|
whoami Verify authentication and show account info
|
|
228
232
|
|
|
229
233
|
Getting Started:
|
|
230
|
-
1.
|
|
234
|
+
1. Create an API key with the Manage scope in the LexQ Console (Management → API Keys).
|
|
235
|
+
An "Execute only" key is for the execution API and does not work with this CLI.
|
|
231
236
|
2. Run: lexq auth login
|
|
232
237
|
3. Verify: lexq auth whoami
|
|
233
238
|
`
|
|
@@ -269,7 +274,7 @@ function registerAuthCommands(program) {
|
|
|
269
274
|
|
|
270
275
|
Example:
|
|
271
276
|
$ lexq auth whoami
|
|
272
|
-
{ "tenantId": "abc-123", "userId": "...", "role": "
|
|
277
|
+
{ "tenantId": "abc-123", "userId": "...", "role": "API_ADMIN", "tenantTimezone": "Asia/Seoul", "apiKey": "lexq****abcd", "baseUrl": "https://api.lexq.io/api/v1/partners" }
|
|
273
278
|
`
|
|
274
279
|
).action(async () => {
|
|
275
280
|
try {
|
|
@@ -310,7 +315,7 @@ function registerStatusCommand(program) {
|
|
|
310
315
|
|
|
311
316
|
Example:
|
|
312
317
|
$ lexq status
|
|
313
|
-
{ "status": "ok", "latencyMs": 142, "tenantId": "abc-123", "role": "
|
|
318
|
+
{ "status": "ok", "latencyMs": 142, "tenantId": "abc-123", "role": "API_ADMIN", "tenantTimezone": "Asia/Seoul" }
|
|
314
319
|
|
|
315
320
|
Use this to verify your API key is valid and the LexQ API is reachable.
|
|
316
321
|
tenantTimezone is the zone every yyyy-MM-dd date option is read in.
|
|
@@ -342,6 +347,8 @@ var MAX_ROUNDING_SCALE = 34;
|
|
|
342
347
|
var MIN_TRAFFIC_RATE = 1;
|
|
343
348
|
var MAX_TRAFFIC_RATE = 99;
|
|
344
349
|
var REPLAY_WINDOW_MAX_RECORDS = 5e4;
|
|
350
|
+
var DEFAULT_SIMULATION_RECORDS = 1e3;
|
|
351
|
+
var MAX_SIMULATION_RECORDS = 1e5;
|
|
345
352
|
var PROFILE_SAMPLE_PERMILLE = 10;
|
|
346
353
|
var LATENCY_WINDOW_MILLIS = 6e4;
|
|
347
354
|
var TAIL_MIN_OBS = 3;
|
|
@@ -384,18 +391,21 @@ function registerGroupCommands(program) {
|
|
|
384
391
|
It controls deployment lifecycle, conflict resolution, and A/B testing.
|
|
385
392
|
|
|
386
393
|
Commands:
|
|
387
|
-
list List
|
|
394
|
+
list List policy groups (deleted groups are not included)
|
|
388
395
|
get Get group detail by ID
|
|
389
396
|
create Create a new group
|
|
390
397
|
update Update group settings
|
|
391
|
-
reorder Change group priorities (drag &
|
|
398
|
+
reorder Change group priorities (drag & drop equivalent)
|
|
392
399
|
delete Archive a group
|
|
393
400
|
ab-test Manage A/B tests (start, stop, adjust)
|
|
394
401
|
|
|
395
|
-
Statuses: ACTIVE, DISABLED
|
|
402
|
+
Statuses: ACTIVE, DISABLED, ARCHIVED
|
|
403
|
+
DISABLED stops production execution (403 P-009) and keeps the deployed version and any A/B test.
|
|
404
|
+
Dry run, simulation, and replay still work. Set ACTIVE to resume.
|
|
405
|
+
ARCHIVED means deleted. Only 'lexq groups delete' sets it, and it cannot be undone.
|
|
396
406
|
`
|
|
397
407
|
);
|
|
398
|
-
groups.command("list").description("List
|
|
408
|
+
groups.command("list").description("List policy groups (deleted groups are not included)").action(async () => {
|
|
399
409
|
try {
|
|
400
410
|
const globalOpts = program.opts();
|
|
401
411
|
const format = globalOpts.format ?? "json";
|
|
@@ -490,6 +500,7 @@ ${data.length} total`);
|
|
|
490
500
|
$ lexq groups update --id <groupId> --json '{"description": "Updated", "status": "DISABLED"}'
|
|
491
501
|
|
|
492
502
|
All fields are optional — only provided fields are updated. priority is not settable here — use 'lexq groups reorder'.
|
|
503
|
+
A deleted group cannot be updated (409 P-059).
|
|
493
504
|
`
|
|
494
505
|
).action(async (opts) => {
|
|
495
506
|
try {
|
|
@@ -512,7 +523,15 @@ ${data.length} total`);
|
|
|
512
523
|
"after",
|
|
513
524
|
dedent3`
|
|
514
525
|
|
|
515
|
-
|
|
526
|
+
A deployed group cannot be deleted (P-020). Undeploy it first, which also ends a running A/B test.
|
|
527
|
+
|
|
528
|
+
A deleted group no longer appears in 'lexq groups list', and its name can be reused right away.
|
|
529
|
+
It cannot be restored, and calls that change it (settings, versions, rules, starting an A/B
|
|
530
|
+
test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever
|
|
531
|
+
executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted
|
|
532
|
+
after 30 days.
|
|
533
|
+
|
|
534
|
+
Use --force to skip the confirmation prompt.
|
|
516
535
|
`
|
|
517
536
|
).action(async (opts) => {
|
|
518
537
|
try {
|
|
@@ -1801,7 +1820,7 @@ function registerDeployCommands(program) {
|
|
|
1801
1820
|
data.content.map((d) => [
|
|
1802
1821
|
d.id.substring(0, 8),
|
|
1803
1822
|
d.deploymentType,
|
|
1804
|
-
d.policyGroupName,
|
|
1823
|
+
groupLabel(d.policyGroupName, d.policyGroupArchivedAt),
|
|
1805
1824
|
d.versionNo != null ? `v${d.versionNo}` : "\u2013",
|
|
1806
1825
|
d.deployedByName,
|
|
1807
1826
|
utcMinute(d.deployedAt)
|
|
@@ -2024,7 +2043,11 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2024
2043
|
["Status", "Group", "Version", "Scheduled For", "By", "Result"],
|
|
2025
2044
|
data.content.map((s) => [
|
|
2026
2045
|
s.status,
|
|
2027
|
-
|
|
2046
|
+
groupLabel(
|
|
2047
|
+
s.policyGroupName,
|
|
2048
|
+
s.policyGroupArchivedAt,
|
|
2049
|
+
s.policyGroupId.substring(0, 8)
|
|
2050
|
+
),
|
|
2028
2051
|
s.versionNo != null ? `v${s.versionNo}` : "\u2013",
|
|
2029
2052
|
utcMinute(s.scheduledFor),
|
|
2030
2053
|
s.scheduledByName,
|
|
@@ -2377,7 +2400,7 @@ function registerAnalyticsCommands(program) {
|
|
|
2377
2400
|
["ID", "Group", "Target", "Status", "Match", "Records", "Created"],
|
|
2378
2401
|
data.content.map((s) => [
|
|
2379
2402
|
s.simulationId.substring(0, 8),
|
|
2380
|
-
s.policyGroupName,
|
|
2403
|
+
groupLabel(s.policyGroupName, s.policyGroupArchivedAt),
|
|
2381
2404
|
s.targetVersionName ?? "\u2013",
|
|
2382
2405
|
s.status,
|
|
2383
2406
|
`${s.matchRate.toFixed(1)}%`,
|
|
@@ -2738,12 +2761,12 @@ function registerHistoryCommands(program) {
|
|
|
2738
2761
|
["Trace", "Group", "Version", "Status", "Matched", "Latency", "At"],
|
|
2739
2762
|
data.content.map((h) => [
|
|
2740
2763
|
h.traceId.substring(0, 12),
|
|
2741
|
-
h.policyGroupName
|
|
2764
|
+
groupLabel(h.policyGroupName, h.policyGroupArchivedAt),
|
|
2742
2765
|
h.policyVersionNo != null ? `v${h.policyVersionNo}` : "\u2013",
|
|
2743
2766
|
h.status,
|
|
2744
2767
|
h.isMatched ? "\u2713" : "\u2717",
|
|
2745
2768
|
`${h.latencyMs}ms`,
|
|
2746
|
-
utcMinute(h.
|
|
2769
|
+
utcMinute(h.executedAt)
|
|
2747
2770
|
]),
|
|
2748
2771
|
{ truncate: 20 }
|
|
2749
2772
|
);
|
|
@@ -3252,8 +3275,8 @@ function registerWebhookSubscriptionCommands(program) {
|
|
|
3252
3275
|
"after",
|
|
3253
3276
|
dedent14`
|
|
3254
3277
|
|
|
3255
|
-
Receive notifications
|
|
3256
|
-
|
|
3278
|
+
Receive notifications for deployment lifecycle events: publish, deploy, rollback, and
|
|
3279
|
+
undeploy, plus a deployment being scheduled or a schedule being canceled.
|
|
3257
3280
|
|
|
3258
3281
|
Commands:
|
|
3259
3282
|
list List all webhook subscriptions
|
|
@@ -3262,8 +3285,8 @@ function registerWebhookSubscriptionCommands(program) {
|
|
|
3262
3285
|
delete Delete a subscription
|
|
3263
3286
|
test Send a test event to verify connectivity
|
|
3264
3287
|
|
|
3265
|
-
|
|
3266
|
-
|
|
3288
|
+
Rule actions never call external systems. These subscriptions are how LexQ pushes deployment
|
|
3289
|
+
events to you.
|
|
3267
3290
|
`
|
|
3268
3291
|
);
|
|
3269
3292
|
webhooks.command("list").description("List webhook subscriptions").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
|
|
@@ -3348,7 +3371,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3348
3371
|
Fields:
|
|
3349
3372
|
name string Subscription name (required, unique per tenant)
|
|
3350
3373
|
webhookUrl string Webhook endpoint URL (required)
|
|
3351
|
-
subscribedEvents string[]
|
|
3374
|
+
subscribedEvents string[] ${PlatformEventType.join(", ")}
|
|
3352
3375
|
payloadFormat string GENERIC (full JSON) or SLACK ({"text":"..."}) [default: GENERIC]
|
|
3353
3376
|
secret string HMAC-SHA256 signing secret (optional)
|
|
3354
3377
|
isActive boolean Enable/disable (optional) [default: true]
|
|
@@ -3539,6 +3562,20 @@ function formatUnregisteredFactWarning(meta) {
|
|
|
3539
3562
|
].join("\n");
|
|
3540
3563
|
}
|
|
3541
3564
|
|
|
3565
|
+
// src/mcp/server-info.json
|
|
3566
|
+
var server_info_default = {
|
|
3567
|
+
title: "LexQ",
|
|
3568
|
+
description: "The Decision Operations Platform for engineering teams",
|
|
3569
|
+
websiteUrl: "https://lexq.io",
|
|
3570
|
+
icons: [
|
|
3571
|
+
{
|
|
3572
|
+
src: "https://lexq.io/logo.png",
|
|
3573
|
+
mimeType: "image/png",
|
|
3574
|
+
sizes: ["1024x1024"]
|
|
3575
|
+
}
|
|
3576
|
+
]
|
|
3577
|
+
};
|
|
3578
|
+
|
|
3542
3579
|
// src/mcp/tools/status.ts
|
|
3543
3580
|
import { z } from "zod";
|
|
3544
3581
|
function registerStatusTools(server, callApi) {
|
|
@@ -3562,7 +3599,7 @@ function registerGroupTools(server, callApi) {
|
|
|
3562
3599
|
{
|
|
3563
3600
|
title: "List Policy Groups",
|
|
3564
3601
|
annotations: { readOnlyHint: true },
|
|
3565
|
-
description: "List
|
|
3602
|
+
description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
|
|
3566
3603
|
inputSchema: z2.object({})
|
|
3567
3604
|
},
|
|
3568
3605
|
async () => callApi("GET", "policy-groups")
|
|
@@ -3615,12 +3652,14 @@ function registerGroupTools(server, callApi) {
|
|
|
3615
3652
|
{
|
|
3616
3653
|
title: "Update Policy Group",
|
|
3617
3654
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3618
|
-
description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged.",
|
|
3655
|
+
description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
|
|
3619
3656
|
inputSchema: z2.object({
|
|
3620
3657
|
groupId: z2.string().uuid().describe("Policy group ID"),
|
|
3621
3658
|
name: z2.string().optional().describe("New name"),
|
|
3622
3659
|
description: z2.string().optional().describe("New description"),
|
|
3623
|
-
status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
|
|
3660
|
+
status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
|
|
3661
|
+
"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."
|
|
3662
|
+
),
|
|
3624
3663
|
activationGroup: z2.string().optional().describe(
|
|
3625
3664
|
"Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
|
|
3626
3665
|
),
|
|
@@ -3638,7 +3677,7 @@ function registerGroupTools(server, callApi) {
|
|
|
3638
3677
|
{
|
|
3639
3678
|
title: "Delete Policy Group",
|
|
3640
3679
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3641
|
-
description: "
|
|
3680
|
+
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.",
|
|
3642
3681
|
inputSchema: z2.object({
|
|
3643
3682
|
groupId: z2.string().uuid().describe("Policy group ID")
|
|
3644
3683
|
})
|
|
@@ -4065,13 +4104,13 @@ function registerFactTools(server, callApi) {
|
|
|
4065
4104
|
{
|
|
4066
4105
|
title: "Update Fact Definition",
|
|
4067
4106
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4068
|
-
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. Only the fields you send are changed. System facts accept name, description, and PII only.",
|
|
4107
|
+
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 accept name, description, and PII only.",
|
|
4069
4108
|
inputSchema: z5.object({
|
|
4070
4109
|
factId: z5.string().uuid().describe("Fact definition ID"),
|
|
4071
4110
|
name: z5.string().optional().describe("Display name"),
|
|
4072
4111
|
description: z5.string().optional().describe("Description"),
|
|
4073
4112
|
type: z5.enum(VALUE_TYPES).optional().describe(
|
|
4074
|
-
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule references the fact."
|
|
4113
|
+
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
|
|
4075
4114
|
),
|
|
4076
4115
|
isRequired: z5.boolean().optional().describe("Required flag"),
|
|
4077
4116
|
isPii: z5.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
|
|
@@ -4093,7 +4132,7 @@ function registerFactTools(server, callApi) {
|
|
|
4093
4132
|
{
|
|
4094
4133
|
title: "Delete Fact Definition",
|
|
4095
4134
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4096
|
-
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. Remove the references first.",
|
|
4135
|
+
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.",
|
|
4097
4136
|
inputSchema: z5.object({
|
|
4098
4137
|
factId: z5.string().uuid().describe("Fact definition ID")
|
|
4099
4138
|
})
|
|
@@ -4405,7 +4444,8 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
4405
4444
|
UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
|
|
4406
4445
|
MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
|
|
4407
4446
|
|
|
4408
|
-
options.maxRecords: number (max
|
|
4447
|
+
options.maxRecords: number (max ${MAX_SIMULATION_RECORDS}). When omitted, HISTORICAL and UPLOADED run at
|
|
4448
|
+
most ${DEFAULT_SIMULATION_RECORDS} records and MANUAL runs every record sent.
|
|
4409
4449
|
options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
|
|
4410
4450
|
options.includeRuleStats: boolean
|
|
4411
4451
|
options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
|
|
@@ -4885,7 +4925,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
4885
4925
|
{
|
|
4886
4926
|
title: "List Webhook Subscriptions",
|
|
4887
4927
|
annotations: { readOnlyHint: true },
|
|
4888
|
-
description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications
|
|
4928
|
+
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.",
|
|
4889
4929
|
inputSchema: z13.object({
|
|
4890
4930
|
page: z13.number().int().min(0).default(0).describe("Page number"),
|
|
4891
4931
|
size: z13.number().int().min(1).max(100).default(20).describe("Page size")
|
|
@@ -4913,7 +4953,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
4913
4953
|
{
|
|
4914
4954
|
title: "Save Webhook Subscription",
|
|
4915
4955
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4916
|
-
description:
|
|
4956
|
+
description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).`,
|
|
4917
4957
|
inputSchema: z13.object({
|
|
4918
4958
|
id: z13.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
|
|
4919
4959
|
name: z13.string().min(1).describe("Subscription name (unique per tenant)"),
|
|
@@ -5017,6 +5057,7 @@ function registerAllTools(server, callApi) {
|
|
|
5017
5057
|
registerDomainTemplateTools(server, callApi);
|
|
5018
5058
|
registerWebhookSubscriptionTools(server, callApi);
|
|
5019
5059
|
}
|
|
5060
|
+
var SERVER_INFO = { name: "lexq", ...server_info_default };
|
|
5020
5061
|
|
|
5021
5062
|
// src/mcp/server.ts
|
|
5022
5063
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -5033,7 +5074,7 @@ function startMcpServer() {
|
|
|
5033
5074
|
const callApi = createCallApiFromConfig();
|
|
5034
5075
|
serveStdio(
|
|
5035
5076
|
() => {
|
|
5036
|
-
const server = new McpServer({
|
|
5077
|
+
const server = new McpServer({ ...SERVER_INFO, version });
|
|
5037
5078
|
registerAllTools(server, callApi);
|
|
5038
5079
|
return server;
|
|
5039
5080
|
},
|
package/dist/mcp/register.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/server';
|
|
1
|
+
import { Implementation, McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { LosslessNumber } from 'lossless-json';
|
|
3
3
|
export { LosslessNumber } from 'lossless-json';
|
|
4
4
|
|
|
@@ -63,5 +63,14 @@ declare const isLosslessNumber: (value: unknown) => value is LosslessNumber;
|
|
|
63
63
|
* @param callApi - API caller function (config-based for CLI, Bearer-based for HTTP)
|
|
64
64
|
*/
|
|
65
65
|
declare function registerAllTools(server: McpServer, callApi: CallApi): void;
|
|
66
|
+
/**
|
|
67
|
+
* How the server introduces itself when a client connects: a title, a description and an
|
|
68
|
+
* icon the client can show, and a link to its website. A host serving these tools adds its
|
|
69
|
+
* own version.
|
|
70
|
+
*
|
|
71
|
+
* The values are kept in server-info.json rather than here because server.json lists them
|
|
72
|
+
* again for the MCP Registry, and `pnpm registry:check` has to read both without a build.
|
|
73
|
+
*/
|
|
74
|
+
declare const SERVER_INFO: Omit<Implementation, 'version'>;
|
|
66
75
|
|
|
67
|
-
export { ApiError, type CallApi, type McpToolResult, formatUnregisteredFactWarning, isLosslessNumber, paginationParams, parseJson, registerAllTools, stringifyJson };
|
|
76
|
+
export { ApiError, type CallApi, type McpToolResult, SERVER_INFO, formatUnregisteredFactWarning, isLosslessNumber, paginationParams, parseJson, registerAllTools, stringifyJson };
|
package/dist/mcp/register.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
// src/mcp/server-info.json
|
|
2
|
+
var server_info_default = {
|
|
3
|
+
title: "LexQ",
|
|
4
|
+
description: "The Decision Operations Platform for engineering teams",
|
|
5
|
+
websiteUrl: "https://lexq.io",
|
|
6
|
+
icons: [
|
|
7
|
+
{
|
|
8
|
+
src: "https://lexq.io/logo.png",
|
|
9
|
+
mimeType: "image/png",
|
|
10
|
+
sizes: ["1024x1024"]
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
};
|
|
14
|
+
|
|
1
15
|
// src/mcp/tools/status.ts
|
|
2
16
|
import { z } from "zod";
|
|
3
17
|
function registerStatusTools(server, callApi) {
|
|
@@ -37,6 +51,8 @@ var WebhookPayloadFormat = ["GENERIC", "SLACK"];
|
|
|
37
51
|
// src/types/constants.ts
|
|
38
52
|
var MAX_ROUNDING_SCALE = 34;
|
|
39
53
|
var REPLAY_WINDOW_MAX_RECORDS = 5e4;
|
|
54
|
+
var DEFAULT_SIMULATION_RECORDS = 1e3;
|
|
55
|
+
var MAX_SIMULATION_RECORDS = 1e5;
|
|
40
56
|
var PROFILE_SAMPLE_PERMILLE = 10;
|
|
41
57
|
var LATENCY_WINDOW_MILLIS = 6e4;
|
|
42
58
|
var TAIL_MIN_OBS = 3;
|
|
@@ -58,7 +74,7 @@ function registerGroupTools(server, callApi) {
|
|
|
58
74
|
{
|
|
59
75
|
title: "List Policy Groups",
|
|
60
76
|
annotations: { readOnlyHint: true },
|
|
61
|
-
description: "List
|
|
77
|
+
description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
|
|
62
78
|
inputSchema: z2.object({})
|
|
63
79
|
},
|
|
64
80
|
async () => callApi("GET", "policy-groups")
|
|
@@ -111,12 +127,14 @@ function registerGroupTools(server, callApi) {
|
|
|
111
127
|
{
|
|
112
128
|
title: "Update Policy Group",
|
|
113
129
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
114
|
-
description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged.",
|
|
130
|
+
description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
|
|
115
131
|
inputSchema: z2.object({
|
|
116
132
|
groupId: z2.string().uuid().describe("Policy group ID"),
|
|
117
133
|
name: z2.string().optional().describe("New name"),
|
|
118
134
|
description: z2.string().optional().describe("New description"),
|
|
119
|
-
status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
|
|
135
|
+
status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
|
|
136
|
+
"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
|
+
),
|
|
120
138
|
activationGroup: z2.string().optional().describe(
|
|
121
139
|
"Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
|
|
122
140
|
),
|
|
@@ -134,7 +152,7 @@ function registerGroupTools(server, callApi) {
|
|
|
134
152
|
{
|
|
135
153
|
title: "Delete Policy Group",
|
|
136
154
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
137
|
-
description: "
|
|
155
|
+
description: "Delete (archive) a policy group. A deployed group cannot be deleted (P-020); undeploy it first with lexq_deploy_undeploy, which also ends a running A/B test. This cannot be undone. The group no longer appears in lexq_groups_list and its name can be reused right away. Calls that change it (settings, versions, rules, starting an A/B test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted after 30 days.",
|
|
138
156
|
inputSchema: z2.object({
|
|
139
157
|
groupId: z2.string().uuid().describe("Policy group ID")
|
|
140
158
|
})
|
|
@@ -634,13 +652,13 @@ function registerFactTools(server, callApi) {
|
|
|
634
652
|
{
|
|
635
653
|
title: "Update Fact Definition",
|
|
636
654
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
637
|
-
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. Only the fields you send are changed. System facts accept name, description, and PII only.",
|
|
655
|
+
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 accept name, description, and PII only.",
|
|
638
656
|
inputSchema: z5.object({
|
|
639
657
|
factId: z5.string().uuid().describe("Fact definition ID"),
|
|
640
658
|
name: z5.string().optional().describe("Display name"),
|
|
641
659
|
description: z5.string().optional().describe("Description"),
|
|
642
660
|
type: z5.enum(VALUE_TYPES).optional().describe(
|
|
643
|
-
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule references the fact."
|
|
661
|
+
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
|
|
644
662
|
),
|
|
645
663
|
isRequired: z5.boolean().optional().describe("Required flag"),
|
|
646
664
|
isPii: z5.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
|
|
@@ -662,7 +680,7 @@ function registerFactTools(server, callApi) {
|
|
|
662
680
|
{
|
|
663
681
|
title: "Delete Fact Definition",
|
|
664
682
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
665
|
-
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. Remove the references first.",
|
|
683
|
+
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.",
|
|
666
684
|
inputSchema: z5.object({
|
|
667
685
|
factId: z5.string().uuid().describe("Fact definition ID")
|
|
668
686
|
})
|
|
@@ -974,7 +992,8 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
974
992
|
UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
|
|
975
993
|
MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
|
|
976
994
|
|
|
977
|
-
options.maxRecords: number (max
|
|
995
|
+
options.maxRecords: number (max ${MAX_SIMULATION_RECORDS}). When omitted, HISTORICAL and UPLOADED run at
|
|
996
|
+
most ${DEFAULT_SIMULATION_RECORDS} records and MANUAL runs every record sent.
|
|
978
997
|
options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
|
|
979
998
|
options.includeRuleStats: boolean
|
|
980
999
|
options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
|
|
@@ -1454,7 +1473,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
1454
1473
|
{
|
|
1455
1474
|
title: "List Webhook Subscriptions",
|
|
1456
1475
|
annotations: { readOnlyHint: true },
|
|
1457
|
-
description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications
|
|
1476
|
+
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.",
|
|
1458
1477
|
inputSchema: z13.object({
|
|
1459
1478
|
page: z13.number().int().min(0).default(0).describe("Page number"),
|
|
1460
1479
|
size: z13.number().int().min(1).max(100).default(20).describe("Page size")
|
|
@@ -1482,7 +1501,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
1482
1501
|
{
|
|
1483
1502
|
title: "Save Webhook Subscription",
|
|
1484
1503
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
1485
|
-
description:
|
|
1504
|
+
description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).`,
|
|
1486
1505
|
inputSchema: z13.object({
|
|
1487
1506
|
id: z13.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
|
|
1488
1507
|
name: z13.string().min(1).describe("Subscription name (unique per tenant)"),
|
|
@@ -1586,9 +1605,11 @@ function registerAllTools(server, callApi) {
|
|
|
1586
1605
|
registerDomainTemplateTools(server, callApi);
|
|
1587
1606
|
registerWebhookSubscriptionTools(server, callApi);
|
|
1588
1607
|
}
|
|
1608
|
+
var SERVER_INFO = { name: "lexq", ...server_info_default };
|
|
1589
1609
|
export {
|
|
1590
1610
|
ApiError,
|
|
1591
1611
|
LosslessNumber,
|
|
1612
|
+
SERVER_INFO,
|
|
1592
1613
|
formatUnregisteredFactWarning,
|
|
1593
1614
|
isLosslessNumber,
|
|
1594
1615
|
paginationParams,
|
package/package.json
CHANGED
|
@@ -17,9 +17,24 @@ controls conflict resolution when multiple groups interact.
|
|
|
17
17
|
| `DISABLED` | `ARCHIVED` | **no** |
|
|
18
18
|
|
|
19
19
|
- **ACTIVE** — executions are processed.
|
|
20
|
-
- **DISABLED** —
|
|
20
|
+
- **DISABLED** — production execution stops. Execution calls return `P-009` (403), and a composite call that includes
|
|
21
|
+
the group is rejected in full. The live version and any A/B test are kept, and dry run, Impact Simulation, and
|
|
22
|
+
Decision Replay keep working, so you can verify a fix before setting `ACTIVE` again.
|
|
21
23
|
- **ARCHIVED** — removed from execution. Reached only through `lexq groups delete`, not by setting `status` directly.
|
|
22
24
|
|
|
25
|
+
### Disable, Roll Back, or Undeploy?
|
|
26
|
+
|
|
27
|
+
| Situation | Use | Callers get |
|
|
28
|
+
|-----------------------------------------------------------------------|------------------------|------------------------------------------------------------------------|
|
|
29
|
+
| The previously deployed version is correct | `lexq deploy rollback` | Decisions from the previous version |
|
|
30
|
+
| This version has to leave production | `lexq deploy undeploy` | `P-019` (400). Composite calls skip the group. A running A/B test ends |
|
|
31
|
+
| Stop now without knowing the cause, then resume with the same version | `status: DISABLED` | `P-009` (403). Composite calls that include the group are rejected |
|
|
32
|
+
|
|
33
|
+
Do not disable a group for a planned end, such as a promotion with a known end date. Set the version's `effectiveTo`
|
|
34
|
+
instead. When that time passes, the group switches to its newest `ACTIVE` version that is still in effect, or is
|
|
35
|
+
undeployed if there is none. Composite calls keep working either way. A disabled group rejects every composite call
|
|
36
|
+
that includes it.
|
|
37
|
+
|
|
23
38
|
## CRUD Commands
|
|
24
39
|
|
|
25
40
|
### List Groups
|
|
@@ -93,20 +108,31 @@ lexq groups delete --id <groupId>
|
|
|
93
108
|
lexq groups delete --id <groupId> --force
|
|
94
109
|
```
|
|
95
110
|
|
|
96
|
-
**`delete` archives — it does not erase.** The group becomes `ARCHIVED` and stops
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
**`delete` archives — it does not erase right away.** The group becomes `ARCHIVED` and stops
|
|
112
|
+
executing. If it was ever executed, deployed, or replayed, it is kept for audit, along with its
|
|
113
|
+
versions and rules. Otherwise it is permanently deleted after 30 days, together with its
|
|
114
|
+
versions, rules, simulations, and schedules. Deleting an already-archived group is a no-op.
|
|
99
115
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
| Condition | What to do first |
|
|
103
|
-
|------------------------|-----------------------------------------------------|
|
|
104
|
-
| The group is deployed | `lexq deploy undeploy --group-id <gid>` |
|
|
105
|
-
| An A/B test is running | `lexq groups ab-test stop --group-id <gid> --force` |
|
|
116
|
+
A deployed group cannot be deleted (`P-020`). Undeploy it first with
|
|
117
|
+
`lexq deploy undeploy --group-id <gid> --memo "<reason>"`, which also ends a running A/B test.
|
|
106
118
|
|
|
107
119
|
Archiving cancels any pending scheduled deployment and closes the priority gap — remaining groups
|
|
108
120
|
are renumbered 1…N.
|
|
109
121
|
|
|
122
|
+
A deleted group no longer appears in `lexq groups list` and cannot be restored or changed.
|
|
123
|
+
Changing its settings, versions, or rules, starting an A/B test, and publishing, deploying,
|
|
124
|
+
rolling back, or scheduling a deployment all return `409` `P-059`. To start over, create a new
|
|
125
|
+
group.
|
|
126
|
+
|
|
127
|
+
**The name can be reused right away.** Records keep the group's ID and the name it had, so after
|
|
128
|
+
a new group takes the name, deployment history, scheduled deployments, execution history,
|
|
129
|
+
simulations, and replays can list two groups under that name. These records carry
|
|
130
|
+
`policyGroupArchivedAt`, the time the group was deleted (`null` if it has not been). In the
|
|
131
|
+
tables of `lexq deploy history`, `deploy schedules`, `history list`, and
|
|
132
|
+
`analytics simulation list` (`--format table`), a deleted group's name shows as
|
|
133
|
+
`(deleted) <name>`. To see one group's deployments or executions only, pass its ID to
|
|
134
|
+
`lexq deploy history --group-id` or `lexq history list --group-id`.
|
|
135
|
+
|
|
110
136
|
## Conflict Resolution
|
|
111
137
|
|
|
112
138
|
### Activation Mode (across groups in the same `activationGroup`)
|
|
@@ -246,4 +272,5 @@ lets you reuse the existing group instead of handling the error.
|
|
|
246
272
|
lexq groups update --id <groupId> --json '{"status": "DISABLED"}'
|
|
247
273
|
```
|
|
248
274
|
|
|
249
|
-
This immediately stops
|
|
275
|
+
This immediately stops production execution for the group. Dry run, Impact Simulation, and Decision Replay keep
|
|
276
|
+
working. No undeploy needed.
|
|
@@ -19,6 +19,9 @@ lexq auth login
|
|
|
19
19
|
lexq auth whoami
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
Create the key with the **Manage** scope in the Console (Management → API Keys). An **Execute only** key is
|
|
23
|
+
for calling the execution API from your application and does not work with this CLI.
|
|
24
|
+
|
|
22
25
|
The API key is passed as `X-API-KEY` header on every request. You can also override per-command:
|
|
23
26
|
|
|
24
27
|
```bash
|