@lexq/cli 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +825 -731
- package/dist/mcp/register.d.ts +6 -6
- package/dist/mcp/register.js +13 -7
- package/package.json +5 -4
- package/skills/lexq-simulation/SKILL.md +5 -0
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { readFileSync as readFileSync4 } from "fs";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
|
-
import { dirname as dirname2, join as
|
|
7
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/commands/auth.ts
|
|
10
10
|
import "commander";
|
|
@@ -85,6 +85,11 @@ var ApiError = class extends Error {
|
|
|
85
85
|
statusCode;
|
|
86
86
|
errorCode;
|
|
87
87
|
};
|
|
88
|
+
function describeForm(form) {
|
|
89
|
+
return [...form.entries()].map(
|
|
90
|
+
([name, value]) => typeof value === "string" ? ` ${name}: ${value}` : ` ${name}: ${value.name} (${value.size} bytes, ${value.type})`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
88
93
|
async function doFetch(method, path, options) {
|
|
89
94
|
const config = loadConfig();
|
|
90
95
|
const baseUrl = options.baseUrl ?? config.baseUrl;
|
|
@@ -99,26 +104,32 @@ async function doFetch(method, path, options) {
|
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
const headers = { "X-API-KEY": apiKey, Accept: "application/json" };
|
|
102
|
-
if (options.body) headers["Content-Type"] = "application/json";
|
|
107
|
+
if (options.body && !options.form) headers["Content-Type"] = "application/json";
|
|
103
108
|
if (options.dryRun) {
|
|
104
109
|
const masked = apiKey.length > 8 ? apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length - 4) : "****";
|
|
105
110
|
console.log(`${method} ${url.toString()}`);
|
|
106
111
|
console.log("Headers:");
|
|
107
112
|
console.log(` X-API-KEY: ${masked}`);
|
|
108
|
-
console.log(` Content-Type: application/json`);
|
|
109
|
-
if (options.
|
|
113
|
+
console.log(` Content-Type: ${options.form ? "multipart/form-data" : "application/json"}`);
|
|
114
|
+
if (options.form) {
|
|
115
|
+
console.log("Body:");
|
|
116
|
+
for (const line of describeForm(options.form)) console.log(line);
|
|
117
|
+
} else if (options.body) {
|
|
110
118
|
console.log("Body:");
|
|
111
119
|
console.log(` ${stringifyJson(options.body, 2) ?? ""}`);
|
|
112
120
|
}
|
|
113
121
|
console.log("\n(Use without --dry-run to execute)");
|
|
114
122
|
process.exit(0);
|
|
115
123
|
}
|
|
116
|
-
if (options.verbose)
|
|
124
|
+
if (options.verbose) {
|
|
125
|
+
console.error(`\u2192 ${method} ${url.toString()}`);
|
|
126
|
+
if (options.form) for (const line of describeForm(options.form)) console.error(line);
|
|
127
|
+
}
|
|
117
128
|
const startTime = Date.now();
|
|
118
129
|
const response = await fetch(url.toString(), {
|
|
119
130
|
method,
|
|
120
131
|
headers,
|
|
121
|
-
body: options.body ? stringifyJson(options.body) : void 0
|
|
132
|
+
body: options.form ?? (options.body ? stringifyJson(options.body) : void 0)
|
|
122
133
|
});
|
|
123
134
|
if (options.verbose) {
|
|
124
135
|
console.error(`\u2190 ${response.status} ${response.statusText} (${Date.now() - startTime}ms)`);
|
|
@@ -132,7 +143,7 @@ function isFileResponse(response) {
|
|
|
132
143
|
return contentType.includes("text/csv") || contentType.includes("application/octet-stream");
|
|
133
144
|
}
|
|
134
145
|
function assertOk(response, json) {
|
|
135
|
-
if (
|
|
146
|
+
if (json.result !== "SUCCESS") {
|
|
136
147
|
throw new ApiError(
|
|
137
148
|
response.status,
|
|
138
149
|
json.errorCode,
|
|
@@ -140,12 +151,25 @@ function assertOk(response, json) {
|
|
|
140
151
|
);
|
|
141
152
|
}
|
|
142
153
|
}
|
|
154
|
+
async function refusal(response) {
|
|
155
|
+
let json;
|
|
156
|
+
try {
|
|
157
|
+
json = parseJson(await response.text());
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
return new ApiError(
|
|
161
|
+
response.status,
|
|
162
|
+
json?.errorCode ?? null,
|
|
163
|
+
json?.message ?? `Request failed with status ${response.status}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
143
166
|
async function apiRequest(method, path, options = {}) {
|
|
144
167
|
const { data } = await apiRequestWithMeta(method, path, options);
|
|
145
168
|
return data;
|
|
146
169
|
}
|
|
147
170
|
async function apiRequestWithMeta(method, path, options = {}) {
|
|
148
171
|
const response = await doFetch(method, path, options);
|
|
172
|
+
if (!response.ok) throw await refusal(response);
|
|
149
173
|
if (isFileResponse(response)) {
|
|
150
174
|
return { data: await response.text(), meta: null };
|
|
151
175
|
}
|
|
@@ -349,6 +373,7 @@ var MAX_TRAFFIC_RATE = 99;
|
|
|
349
373
|
var REPLAY_WINDOW_MAX_RECORDS = 5e4;
|
|
350
374
|
var DEFAULT_SIMULATION_RECORDS = 1e3;
|
|
351
375
|
var MAX_SIMULATION_RECORDS = 1e5;
|
|
376
|
+
var MAX_DATASET_SIZE = 10485760;
|
|
352
377
|
var PROFILE_SAMPLE_PERMILLE = 10;
|
|
353
378
|
var LATENCY_WINDOW_MILLIS = 6e4;
|
|
354
379
|
var TAIL_MIN_OBS = 3;
|
|
@@ -2157,8 +2182,30 @@ async function warnUnregisteredFacts(globalOpts, groupId, versionId) {
|
|
|
2157
2182
|
|
|
2158
2183
|
// src/commands/analytics.ts
|
|
2159
2184
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2185
|
+
import { basename } from "path";
|
|
2160
2186
|
import "commander";
|
|
2161
2187
|
import dedent8 from "dedent";
|
|
2188
|
+
|
|
2189
|
+
// src/lib/dataset-upload.ts
|
|
2190
|
+
import { extname } from "path";
|
|
2191
|
+
var DATASET_CONTENT_TYPES = {
|
|
2192
|
+
".csv": "text/csv",
|
|
2193
|
+
".json": "application/json"
|
|
2194
|
+
};
|
|
2195
|
+
function datasetContentType(filename) {
|
|
2196
|
+
return DATASET_CONTENT_TYPES[extname(filename).toLowerCase()];
|
|
2197
|
+
}
|
|
2198
|
+
function uploadDataset(bytes, filename, options) {
|
|
2199
|
+
const form = new FormData();
|
|
2200
|
+
const type = datasetContentType(filename) ?? "application/octet-stream";
|
|
2201
|
+
form.append("file", new Blob([bytes], { type }), filename);
|
|
2202
|
+
return apiRequest("POST", "analytics/datasets/upload", {
|
|
2203
|
+
...options,
|
|
2204
|
+
form
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// src/commands/analytics.ts
|
|
2162
2209
|
function registerAnalyticsCommands(program) {
|
|
2163
2210
|
const analytics = program.command("analytics").description("Dry run, simulation, and requirements").addHelpText(
|
|
2164
2211
|
"after",
|
|
@@ -2577,41 +2624,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2577
2624
|
).action(async (opts) => {
|
|
2578
2625
|
try {
|
|
2579
2626
|
const globalOpts = program.opts();
|
|
2580
|
-
const config = loadConfig();
|
|
2581
|
-
const baseUrl = globalOpts.baseUrl ?? config.baseUrl;
|
|
2582
|
-
const apiKey = globalOpts.apiKey ?? config.apiKey;
|
|
2583
|
-
if (!apiKey) {
|
|
2584
|
-
throw new Error('Not authenticated. Run "lexq auth login" first.');
|
|
2585
|
-
}
|
|
2586
2627
|
const filePath = opts.file;
|
|
2587
|
-
const
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
else if (ext === "json") contentType = "application/json";
|
|
2593
|
-
const blob = new Blob([fileBuffer], { type: contentType });
|
|
2594
|
-
const formData = new FormData();
|
|
2595
|
-
formData.append("file", blob, fileName);
|
|
2596
|
-
const url = new URL(
|
|
2597
|
-
"analytics/datasets/upload",
|
|
2598
|
-
baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"
|
|
2599
|
-
);
|
|
2600
|
-
if (globalOpts.verbose) {
|
|
2601
|
-
console.error(`\u2192 POST ${url.toString()}`);
|
|
2602
|
-
console.error(` File: ${filePath} (${fileBuffer.length} bytes)`);
|
|
2603
|
-
}
|
|
2604
|
-
const response = await fetch(url.toString(), {
|
|
2605
|
-
method: "POST",
|
|
2606
|
-
headers: { "X-API-KEY": apiKey },
|
|
2607
|
-
body: formData
|
|
2628
|
+
const result = await uploadDataset(readFileSync2(filePath), basename(filePath), {
|
|
2629
|
+
apiKey: globalOpts.apiKey,
|
|
2630
|
+
baseUrl: globalOpts.baseUrl,
|
|
2631
|
+
dryRun: globalOpts.dryRun,
|
|
2632
|
+
verbose: globalOpts.verbose
|
|
2608
2633
|
});
|
|
2609
|
-
if (!response.ok) {
|
|
2610
|
-
const errorText = await response.text();
|
|
2611
|
-
throw new Error(`Upload failed (${response.status}): ${errorText}`);
|
|
2612
|
-
}
|
|
2613
|
-
const envelope = await response.json();
|
|
2614
|
-
const result = envelope.data;
|
|
2615
2634
|
console.log(`\u2713 Dataset uploaded: ${result.path}`);
|
|
2616
2635
|
console.log(` File: ${result.filename} (${result.size} bytes)`);
|
|
2617
2636
|
printJson(result);
|
|
@@ -3548,56 +3567,36 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3548
3567
|
|
|
3549
3568
|
// src/commands/serve.ts
|
|
3550
3569
|
import "commander";
|
|
3551
|
-
import
|
|
3570
|
+
import dedent19 from "dedent";
|
|
3552
3571
|
|
|
3553
3572
|
// src/mcp/server.ts
|
|
3554
3573
|
import { readFileSync as readFileSync3 } from "fs";
|
|
3555
|
-
import { dirname, join as
|
|
3574
|
+
import { dirname, join as join3 } from "path";
|
|
3556
3575
|
import { fileURLToPath } from "url";
|
|
3557
3576
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
3558
3577
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
3559
3578
|
|
|
3560
3579
|
// src/mcp/tools/_shared.ts
|
|
3580
|
+
function localApiOptions() {
|
|
3581
|
+
const stored = loadConfig();
|
|
3582
|
+
return {
|
|
3583
|
+
baseUrl: process.env.PARTNER_BASE_URL ?? stored.baseUrl,
|
|
3584
|
+
apiKey: process.env.LEXQ_API_KEY ?? stored.apiKey
|
|
3585
|
+
};
|
|
3586
|
+
}
|
|
3561
3587
|
function createCallApiFromConfig() {
|
|
3562
3588
|
return async (method, path, opts) => {
|
|
3563
3589
|
try {
|
|
3564
|
-
|
|
3565
|
-
const config = {
|
|
3566
|
-
...stored,
|
|
3567
|
-
baseUrl: process.env.PARTNER_BASE_URL ?? stored.baseUrl,
|
|
3568
|
-
apiKey: process.env.LEXQ_API_KEY ?? stored.apiKey
|
|
3569
|
-
};
|
|
3590
|
+
let form;
|
|
3570
3591
|
if (opts?.upload) {
|
|
3571
|
-
|
|
3572
|
-
path,
|
|
3573
|
-
config.baseUrl.endsWith("/") ? config.baseUrl : config.baseUrl + "/"
|
|
3574
|
-
);
|
|
3575
|
-
if (opts.params) {
|
|
3576
|
-
for (const [key, value] of Object.entries(opts.params)) {
|
|
3577
|
-
if (value !== void 0) url.searchParams.set(key, value);
|
|
3578
|
-
}
|
|
3579
|
-
}
|
|
3580
|
-
if (!config.apiKey) {
|
|
3581
|
-
throw new Error('Not authenticated. Run "lexq auth login" first.');
|
|
3582
|
-
}
|
|
3592
|
+
form = new FormData();
|
|
3583
3593
|
const blob = new Blob([opts.upload.content], { type: "text/plain" });
|
|
3584
|
-
|
|
3585
|
-
formData.append(opts.upload.fieldName, blob, opts.upload.filename);
|
|
3586
|
-
const response = await fetch(url.toString(), {
|
|
3587
|
-
method,
|
|
3588
|
-
headers: { "X-API-KEY": config.apiKey },
|
|
3589
|
-
body: formData
|
|
3590
|
-
});
|
|
3591
|
-
const data2 = parseJson(await response.text());
|
|
3592
|
-
return { content: [{ type: "text", text: stringifyJson(data2, 2) ?? "" }] };
|
|
3594
|
+
form.append(opts.upload.fieldName, blob, opts.upload.filename);
|
|
3593
3595
|
}
|
|
3594
|
-
const clientOpts = {
|
|
3595
|
-
apiKey: config.apiKey,
|
|
3596
|
-
baseUrl: config.baseUrl
|
|
3597
|
-
};
|
|
3598
3596
|
const { data, meta } = await apiRequestWithMeta(method, path, {
|
|
3599
|
-
...
|
|
3597
|
+
...localApiOptions(),
|
|
3600
3598
|
body: opts?.body,
|
|
3599
|
+
form,
|
|
3601
3600
|
params: opts?.params
|
|
3602
3601
|
});
|
|
3603
3602
|
const content = [
|
|
@@ -3640,181 +3639,514 @@ function formatUnregisteredFactWarning(meta) {
|
|
|
3640
3639
|
].join("\n");
|
|
3641
3640
|
}
|
|
3642
3641
|
|
|
3643
|
-
// src/mcp/
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
{
|
|
3650
|
-
src: "https://lexq.io/logo.png",
|
|
3651
|
-
mimeType: "image/png",
|
|
3652
|
-
sizes: ["1024x1024"]
|
|
3653
|
-
}
|
|
3654
|
-
]
|
|
3655
|
-
};
|
|
3642
|
+
// src/mcp/local-tools.ts
|
|
3643
|
+
import { readFile, realpath, stat } from "fs/promises";
|
|
3644
|
+
import { homedir as homedir2 } from "os";
|
|
3645
|
+
import { basename as basename2, isAbsolute, join as join2 } from "path";
|
|
3646
|
+
import { z as z2 } from "zod";
|
|
3647
|
+
import dedent16 from "dedent";
|
|
3656
3648
|
|
|
3657
|
-
// src/mcp/tools/
|
|
3649
|
+
// src/mcp/tools/analytics.ts
|
|
3658
3650
|
import { z } from "zod";
|
|
3659
|
-
|
|
3651
|
+
import dedent15 from "dedent";
|
|
3652
|
+
function datasetBlock(path) {
|
|
3653
|
+
const dataset = { type: "UPLOADED", source: "S3_BUCKET", path };
|
|
3654
|
+
return {
|
|
3655
|
+
type: "text",
|
|
3656
|
+
text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
function registerAnalyticsTools(server, callApi) {
|
|
3660
3660
|
server.registerTool(
|
|
3661
|
-
"
|
|
3661
|
+
"lexq_dry_run",
|
|
3662
3662
|
{
|
|
3663
|
-
title: "
|
|
3663
|
+
title: "Dry Run",
|
|
3664
3664
|
annotations: { readOnlyHint: true },
|
|
3665
|
-
description:
|
|
3666
|
-
|
|
3665
|
+
description: dedent15`
|
|
3666
|
+
Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
|
|
3667
|
+
|
|
3668
|
+
Returns:
|
|
3669
|
+
inputFacts — normalized input facts
|
|
3670
|
+
mutatedFacts — input facts changed by rule actions (e.g. MUTATE_FACT mutates paymentAmount)
|
|
3671
|
+
generatedVariables — system-generated values; every fact in mutatedFacts gets a paired {factName}__delta key (signed difference)
|
|
3672
|
+
executionTraces — per-rule match status
|
|
3673
|
+
decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
|
|
3674
|
+
|
|
3675
|
+
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
|
|
3676
|
+
Always dry-run before publishing to validate rule behavior.`,
|
|
3677
|
+
inputSchema: z.object({
|
|
3678
|
+
versionId: z.string().uuid().describe("Policy version ID to test against"),
|
|
3679
|
+
facts: z.string().describe('JSON string of facts object, e.g. {"paymentAmount":100000}'),
|
|
3680
|
+
includeDebugInfo: z.boolean().default(true).describe("Include execution and decision traces")
|
|
3681
|
+
})
|
|
3667
3682
|
},
|
|
3668
|
-
async (
|
|
3683
|
+
async ({ versionId, facts, includeDebugInfo }) => {
|
|
3684
|
+
const parsedFacts = parseJson(facts);
|
|
3685
|
+
return callApi("POST", `analytics/dry-run/versions/${versionId}`, {
|
|
3686
|
+
body: { facts: parsedFacts, includeDebugInfo }
|
|
3687
|
+
});
|
|
3688
|
+
}
|
|
3669
3689
|
);
|
|
3670
|
-
}
|
|
3671
|
-
|
|
3672
|
-
// src/mcp/tools/groups.ts
|
|
3673
|
-
import { z as z2 } from "zod";
|
|
3674
|
-
function registerGroupTools(server, callApi) {
|
|
3675
3690
|
server.registerTool(
|
|
3676
|
-
"
|
|
3691
|
+
"lexq_dry_run_compare",
|
|
3677
3692
|
{
|
|
3678
|
-
title: "
|
|
3693
|
+
title: "Dry Run Compare",
|
|
3679
3694
|
annotations: { readOnlyHint: true },
|
|
3680
|
-
description:
|
|
3681
|
-
|
|
3695
|
+
description: dedent15`
|
|
3696
|
+
Compare dry run results between two versions using the same input facts. Useful for validating changes.
|
|
3697
|
+
|
|
3698
|
+
Returns:
|
|
3699
|
+
resultA / resultB — full DryRunResponse for each version
|
|
3700
|
+
diff.mutatedDiff — changes in mutatedFacts between A and B (key → {before, after})
|
|
3701
|
+
diff.generatedDiff — changes in generatedVariables between A and B`,
|
|
3702
|
+
inputSchema: z.object({
|
|
3703
|
+
versionIdA: z.string().uuid().describe("Baseline version ID"),
|
|
3704
|
+
versionIdB: z.string().uuid().describe("Candidate version ID"),
|
|
3705
|
+
facts: z.string().describe("JSON string of facts object")
|
|
3706
|
+
})
|
|
3682
3707
|
},
|
|
3683
|
-
async (
|
|
3708
|
+
async ({ versionIdA, versionIdB, facts }) => {
|
|
3709
|
+
const parsedFacts = parseJson(facts);
|
|
3710
|
+
return callApi("POST", "analytics/dry-run/compare", {
|
|
3711
|
+
body: { versionIdA, versionIdB, facts: parsedFacts }
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3684
3714
|
);
|
|
3685
3715
|
server.registerTool(
|
|
3686
|
-
"
|
|
3716
|
+
"lexq_requirements",
|
|
3687
3717
|
{
|
|
3688
|
-
title: "
|
|
3718
|
+
title: "Analyze Requirements",
|
|
3689
3719
|
annotations: { readOnlyHint: true },
|
|
3690
|
-
description: "
|
|
3691
|
-
inputSchema:
|
|
3692
|
-
groupId:
|
|
3720
|
+
description: "Analyze which input facts a version requires. Returns required keys, types, and an example request body.",
|
|
3721
|
+
inputSchema: z.object({
|
|
3722
|
+
groupId: z.string().uuid().describe("Policy group ID"),
|
|
3723
|
+
versionId: z.string().uuid().describe("Version ID")
|
|
3693
3724
|
})
|
|
3694
3725
|
},
|
|
3695
|
-
async ({ groupId }) => callApi("GET", `
|
|
3726
|
+
async ({ groupId, versionId }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/requirements`)
|
|
3696
3727
|
);
|
|
3697
3728
|
server.registerTool(
|
|
3698
|
-
"
|
|
3729
|
+
"lexq_simulation_start",
|
|
3699
3730
|
{
|
|
3700
|
-
title: "
|
|
3731
|
+
title: "Start Simulation",
|
|
3701
3732
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3702
|
-
description: `
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
)
|
|
3733
|
+
description: dedent15`
|
|
3734
|
+
Start an Impact Simulation against historical, uploaded, or inline data.
|
|
3735
|
+
|
|
3736
|
+
dataset.type and dataset.source are BOTH required, and must be paired:
|
|
3737
|
+
HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
|
|
3738
|
+
in the organization time zone, both inclusive)
|
|
3739
|
+
UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
|
|
3740
|
+
MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
|
|
3741
|
+
|
|
3742
|
+
options.maxRecords: number (max ${MAX_SIMULATION_RECORDS}). When omitted, HISTORICAL and UPLOADED run at
|
|
3743
|
+
most ${DEFAULT_SIMULATION_RECORDS} records and MANUAL runs every record sent.
|
|
3744
|
+
options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
|
|
3745
|
+
options.includeRuleStats: boolean
|
|
3746
|
+
options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
|
|
3747
|
+
{ "targetVariable": "<fact>", "aggregationType": "COUNT" | "SUM" | "AVG" }
|
|
3748
|
+
|
|
3749
|
+
Example (uploaded dataset):
|
|
3750
|
+
{
|
|
3751
|
+
"policyVersionId": "<uuid>",
|
|
3752
|
+
"dataset": { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<path from lexq_dataset_upload>" },
|
|
3753
|
+
"options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true, "maxRecords": 10000 }
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
Example (historical):
|
|
3757
|
+
{
|
|
3758
|
+
"policyVersionId": "<uuid>",
|
|
3759
|
+
"dataset": { "type": "HISTORICAL", "source": "EXECUTION_LOGS", "from": "2026-01-01", "to": "2026-01-31" },
|
|
3760
|
+
"options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true }
|
|
3761
|
+
}
|
|
3762
|
+
`,
|
|
3763
|
+
inputSchema: z.object({
|
|
3764
|
+
body: z.string().describe("JSON string of SimulationRequest")
|
|
3714
3765
|
})
|
|
3715
3766
|
},
|
|
3716
|
-
async (
|
|
3717
|
-
const
|
|
3718
|
-
|
|
3719
|
-
};
|
|
3720
|
-
if (args.description !== void 0) body.description = args.description;
|
|
3721
|
-
if (args.activationMode !== void 0) body.activationMode = args.activationMode;
|
|
3722
|
-
if (args.activationStrategy !== void 0) body.activationStrategy = args.activationStrategy;
|
|
3723
|
-
if (args.executionLimit !== void 0) body.executionLimit = args.executionLimit;
|
|
3724
|
-
if (args.activationGroup !== void 0) body.activationGroup = args.activationGroup;
|
|
3725
|
-
return callApi("POST", "policy-groups", { body });
|
|
3767
|
+
async ({ body }) => {
|
|
3768
|
+
const parsed = parseJson(body);
|
|
3769
|
+
return callApi("POST", "analytics/simulations", { body: parsed });
|
|
3726
3770
|
}
|
|
3727
3771
|
);
|
|
3728
3772
|
server.registerTool(
|
|
3729
|
-
"
|
|
3730
|
-
{
|
|
3731
|
-
title: "Update Policy Group",
|
|
3732
|
-
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3733
|
-
description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
|
|
3734
|
-
inputSchema: z2.object({
|
|
3735
|
-
groupId: z2.string().uuid().describe("Policy group ID"),
|
|
3736
|
-
name: z2.string().optional().describe("New name"),
|
|
3737
|
-
description: z2.string().optional().describe("New description"),
|
|
3738
|
-
status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
|
|
3739
|
-
"DISABLED stops production execution of this group: execution calls return 403 (P-009), and a composite call that includes the group is rejected in full. The deployed version and any A/B test are kept, and dry run, Impact Simulation, and Decision Replay keep working, so a fix can be verified before setting ACTIVE again. To go back to the previous version, use lexq_deploy_rollback. To take the version out of production, use lexq_deploy_undeploy."
|
|
3740
|
-
),
|
|
3741
|
-
activationGroup: z2.string().optional().describe(
|
|
3742
|
-
"Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
|
|
3743
|
-
),
|
|
3744
|
-
activationMode: z2.enum(ConflictResolutionMode).optional().describe("Conflict resolution mode"),
|
|
3745
|
-
activationStrategy: z2.enum(ConflictResolutionStrategy).optional().describe("Ranking used to pick the winning groups"),
|
|
3746
|
-
executionLimit: z2.number().int().min(1).optional().describe(
|
|
3747
|
-
"How many policy groups sharing this activationGroup may run. Counts groups, not rules."
|
|
3748
|
-
)
|
|
3749
|
-
})
|
|
3750
|
-
},
|
|
3751
|
-
async ({ groupId, ...body }) => callApi("PUT", `policy-groups/${groupId}`, { body })
|
|
3752
|
-
);
|
|
3753
|
-
server.registerTool(
|
|
3754
|
-
"lexq_groups_delete",
|
|
3773
|
+
"lexq_simulation_status",
|
|
3755
3774
|
{
|
|
3756
|
-
title: "
|
|
3757
|
-
annotations: { readOnlyHint:
|
|
3758
|
-
description: "
|
|
3759
|
-
inputSchema:
|
|
3760
|
-
|
|
3775
|
+
title: "Simulation Status",
|
|
3776
|
+
annotations: { readOnlyHint: true },
|
|
3777
|
+
description: "Get simulation status and results. Poll until status is COMPLETED or FAILED.",
|
|
3778
|
+
inputSchema: z.object({
|
|
3779
|
+
simulationId: z.string().uuid().describe("Simulation ID")
|
|
3761
3780
|
})
|
|
3762
3781
|
},
|
|
3763
|
-
async ({
|
|
3782
|
+
async ({ simulationId }) => callApi("GET", `analytics/simulations/${simulationId}`)
|
|
3764
3783
|
);
|
|
3765
3784
|
server.registerTool(
|
|
3766
|
-
"
|
|
3785
|
+
"lexq_simulation_list",
|
|
3767
3786
|
{
|
|
3768
|
-
title: "
|
|
3769
|
-
annotations: { readOnlyHint:
|
|
3770
|
-
description: "
|
|
3771
|
-
inputSchema:
|
|
3772
|
-
|
|
3787
|
+
title: "List Simulations",
|
|
3788
|
+
annotations: { readOnlyHint: true },
|
|
3789
|
+
description: "List simulation history with optional filters.",
|
|
3790
|
+
inputSchema: z.object({
|
|
3791
|
+
page: z.number().int().min(0).default(0).describe("Page number"),
|
|
3792
|
+
size: z.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
3793
|
+
status: z.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"]).optional().describe("Filter by status"),
|
|
3794
|
+
from: z.string().optional().describe(START_DAY),
|
|
3795
|
+
to: z.string().optional().describe(END_DAY)
|
|
3773
3796
|
})
|
|
3774
3797
|
},
|
|
3775
|
-
async ({
|
|
3776
|
-
const
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
return callApi("
|
|
3798
|
+
async ({ page, size, status, from, to }) => {
|
|
3799
|
+
const params = paginationParams(page, size);
|
|
3800
|
+
if (status) params.status = status;
|
|
3801
|
+
if (from) params.from = from;
|
|
3802
|
+
if (to) params.to = to;
|
|
3803
|
+
return callApi("GET", "analytics/simulations", { params });
|
|
3781
3804
|
}
|
|
3782
3805
|
);
|
|
3783
3806
|
server.registerTool(
|
|
3784
|
-
"
|
|
3807
|
+
"lexq_simulation_cancel",
|
|
3785
3808
|
{
|
|
3786
|
-
title: "
|
|
3787
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
3788
|
-
description: "
|
|
3789
|
-
inputSchema:
|
|
3790
|
-
|
|
3791
|
-
testVersionId: z2.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
|
|
3792
|
-
trafficRate: z2.number().int().min(1).max(99).describe("Traffic percentage routed to challenger (1-99)")
|
|
3809
|
+
title: "Cancel Simulation",
|
|
3810
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3811
|
+
description: "Cancel a running or pending simulation.",
|
|
3812
|
+
inputSchema: z.object({
|
|
3813
|
+
simulationId: z.string().uuid().describe("Simulation ID")
|
|
3793
3814
|
})
|
|
3794
3815
|
},
|
|
3795
|
-
async ({
|
|
3816
|
+
async ({ simulationId }) => callApi("DELETE", `analytics/simulations/${simulationId}`)
|
|
3796
3817
|
);
|
|
3797
3818
|
server.registerTool(
|
|
3798
|
-
"
|
|
3819
|
+
"lexq_simulation_export",
|
|
3799
3820
|
{
|
|
3800
|
-
title: "
|
|
3801
|
-
annotations: { readOnlyHint:
|
|
3802
|
-
description: "
|
|
3803
|
-
inputSchema:
|
|
3804
|
-
|
|
3821
|
+
title: "Export Simulation",
|
|
3822
|
+
annotations: { readOnlyHint: true },
|
|
3823
|
+
description: "Export simulation results as JSON or CSV. Returns the raw data.",
|
|
3824
|
+
inputSchema: z.object({
|
|
3825
|
+
simulationId: z.string().uuid().describe("Simulation ID"),
|
|
3826
|
+
format: z.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
3805
3827
|
})
|
|
3806
3828
|
},
|
|
3807
|
-
async ({
|
|
3829
|
+
async ({ simulationId, format }) => callApi("GET", `analytics/simulations/${simulationId}/export`, {
|
|
3830
|
+
params: { format }
|
|
3831
|
+
})
|
|
3808
3832
|
);
|
|
3809
3833
|
server.registerTool(
|
|
3810
|
-
"
|
|
3834
|
+
"lexq_dataset_upload",
|
|
3811
3835
|
{
|
|
3812
|
-
title: "
|
|
3836
|
+
title: "Upload Dataset",
|
|
3837
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3838
|
+
description: dedent15`
|
|
3839
|
+
Upload inline CSV or JSON content as a simulation dataset.
|
|
3840
|
+
The content is uploaded to S3 and a path is returned in the "path" field.
|
|
3841
|
+
|
|
3842
|
+
To use the returned path in lexq_simulation_start, set:
|
|
3843
|
+
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
3844
|
+
|
|
3845
|
+
The content travels inside the tool call, so this suits small datasets, and the hosted server at
|
|
3846
|
+
mcp.lexq.io limits the size of a request. For a larger file, use lexq_dataset_upload_file on the
|
|
3847
|
+
local server (lexq serve --mcp), or upload it with the CLI or the console and pass the returned path.
|
|
3848
|
+
|
|
3849
|
+
CSV example:
|
|
3850
|
+
userId,paymentAmount
|
|
3851
|
+
user_001,150000
|
|
3852
|
+
user_002,50000
|
|
3853
|
+
|
|
3854
|
+
JSON example: [{"userId":"user_001","paymentAmount":150000}, {"userId":"user_002","paymentAmount":50000}]
|
|
3855
|
+
`,
|
|
3856
|
+
inputSchema: z.object({
|
|
3857
|
+
content: z.string().describe("CSV or JSON content as string"),
|
|
3858
|
+
filename: z.string().default("dataset.csv").describe("Filename with extension (.csv or .json)")
|
|
3859
|
+
})
|
|
3860
|
+
},
|
|
3861
|
+
async ({ content, filename }) => {
|
|
3862
|
+
const result = await callApi("POST", "analytics/datasets/upload", {
|
|
3863
|
+
upload: { content, filename, fieldName: "file" }
|
|
3864
|
+
});
|
|
3865
|
+
if (!result.isError) {
|
|
3866
|
+
try {
|
|
3867
|
+
const uploaded = parseJson(result.content[0]?.text ?? "{}");
|
|
3868
|
+
const path = uploaded.data?.path ?? uploaded.path;
|
|
3869
|
+
if (typeof path === "string" && path) result.content.push(datasetBlock(path));
|
|
3870
|
+
} catch {
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
return result;
|
|
3874
|
+
}
|
|
3875
|
+
);
|
|
3876
|
+
server.registerTool(
|
|
3877
|
+
"lexq_dataset_template",
|
|
3878
|
+
{
|
|
3879
|
+
title: "Download Dataset Template",
|
|
3880
|
+
annotations: { readOnlyHint: true },
|
|
3881
|
+
description: "Generate a sample CSV or JSON template based on the required facts of a version. Use this to understand the expected data format before uploading a dataset.",
|
|
3882
|
+
inputSchema: z.object({
|
|
3883
|
+
groupId: z.string().uuid().describe("Policy group ID"),
|
|
3884
|
+
versionId: z.string().uuid().describe("Version ID"),
|
|
3885
|
+
format: z.enum(EXPORT_FORMATS).default("csv").describe("Template format")
|
|
3886
|
+
})
|
|
3887
|
+
},
|
|
3888
|
+
async ({ groupId, versionId, format }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/dataset-template`, {
|
|
3889
|
+
params: { format }
|
|
3890
|
+
})
|
|
3891
|
+
);
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
// src/mcp/local-tools.ts
|
|
3895
|
+
var MAX_DATASET_MB = MAX_DATASET_SIZE / 1024 / 1024;
|
|
3896
|
+
var NOT_ABSOLUTE = "file must be an absolute path on this computer. A leading ~/ stands for the home directory.";
|
|
3897
|
+
var NOT_A_DATASET = "file must be a .csv or .json file, and a symbolic link must point to one.";
|
|
3898
|
+
var TOO_LARGE = `file is larger than ${MAX_DATASET_MB} MB.`;
|
|
3899
|
+
var UNREADABLE = "file does not exist, or is not a regular file this server can read.";
|
|
3900
|
+
var NOT_AN_ARRAY = "a .json file must hold an array of records.";
|
|
3901
|
+
function refuse(message) {
|
|
3902
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
|
|
3903
|
+
}
|
|
3904
|
+
function startsWithArray(bytes) {
|
|
3905
|
+
let i = bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191 ? 3 : 0;
|
|
3906
|
+
while (bytes[i] === 32 || bytes[i] === 9 || bytes[i] === 10 || bytes[i] === 13) i++;
|
|
3907
|
+
return bytes[i] === 91;
|
|
3908
|
+
}
|
|
3909
|
+
async function readDatasetFile(file) {
|
|
3910
|
+
const named = file.startsWith("~/") ? join2(homedir2(), file.slice(2)) : file;
|
|
3911
|
+
if (!isAbsolute(named) || /^[\\/]{2}/.test(named)) return { error: NOT_ABSOLUTE };
|
|
3912
|
+
if (!datasetContentType(named)) return { error: NOT_A_DATASET };
|
|
3913
|
+
let real;
|
|
3914
|
+
try {
|
|
3915
|
+
real = await realpath(named);
|
|
3916
|
+
} catch {
|
|
3917
|
+
return { error: UNREADABLE };
|
|
3918
|
+
}
|
|
3919
|
+
const type = datasetContentType(real);
|
|
3920
|
+
if (!type) return { error: NOT_A_DATASET };
|
|
3921
|
+
try {
|
|
3922
|
+
const info = await stat(real);
|
|
3923
|
+
if (!info.isFile()) return { error: UNREADABLE };
|
|
3924
|
+
if (info.size > MAX_DATASET_SIZE) return { error: TOO_LARGE };
|
|
3925
|
+
const bytes = await readFile(real);
|
|
3926
|
+
if (type === "application/json" && !startsWithArray(bytes)) return { error: NOT_AN_ARRAY };
|
|
3927
|
+
return { bytes, filename: basename2(real) };
|
|
3928
|
+
} catch {
|
|
3929
|
+
return { error: UNREADABLE };
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
function registerLocalTools(server) {
|
|
3933
|
+
server.registerTool(
|
|
3934
|
+
"lexq_dataset_upload_file",
|
|
3935
|
+
{
|
|
3936
|
+
title: "Upload Dataset File",
|
|
3937
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3938
|
+
description: dedent16`
|
|
3939
|
+
Upload a CSV or JSON file from this computer as a simulation dataset, up to ${MAX_DATASET_MB} MB.
|
|
3940
|
+
The server reads the file from disk, so its content never passes through the conversation.
|
|
3941
|
+
Only the local server (lexq serve --mcp) has this tool.
|
|
3942
|
+
|
|
3943
|
+
file: the absolute path of a .csv or .json file. A leading ~/ stands for the home directory.
|
|
3944
|
+
A symbolic link is followed, and the file it points to must also end in .csv or .json.
|
|
3945
|
+
|
|
3946
|
+
CSV: a header row of fact keys, then one row per record. JSON: an array of objects.
|
|
3947
|
+
|
|
3948
|
+
Returns the stored path and a ready-to-use dataset block for lexq_simulation_start:
|
|
3949
|
+
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
3950
|
+
`,
|
|
3951
|
+
inputSchema: z2.object({
|
|
3952
|
+
file: z2.string().min(1).describe(
|
|
3953
|
+
"Absolute path of a .csv or .json file. A leading ~/ stands for the home directory."
|
|
3954
|
+
)
|
|
3955
|
+
})
|
|
3956
|
+
},
|
|
3957
|
+
async ({ file }) => {
|
|
3958
|
+
const read = await readDatasetFile(file);
|
|
3959
|
+
if ("error" in read) return refuse(read.error);
|
|
3960
|
+
try {
|
|
3961
|
+
const uploaded = await uploadDataset(read.bytes, read.filename, localApiOptions());
|
|
3962
|
+
return {
|
|
3963
|
+
content: [
|
|
3964
|
+
{ type: "text", text: stringifyJson(uploaded, 2) ?? "" },
|
|
3965
|
+
datasetBlock(uploaded.path)
|
|
3966
|
+
]
|
|
3967
|
+
};
|
|
3968
|
+
} catch (error) {
|
|
3969
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
);
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
// src/mcp/server-info.json
|
|
3976
|
+
var server_info_default = {
|
|
3977
|
+
title: "LexQ",
|
|
3978
|
+
description: "The Decision Operations Platform for engineering teams",
|
|
3979
|
+
websiteUrl: "https://lexq.io",
|
|
3980
|
+
icons: [
|
|
3981
|
+
{
|
|
3982
|
+
src: "https://lexq.io/logo.png",
|
|
3983
|
+
mimeType: "image/png",
|
|
3984
|
+
sizes: ["1024x1024"]
|
|
3985
|
+
}
|
|
3986
|
+
]
|
|
3987
|
+
};
|
|
3988
|
+
|
|
3989
|
+
// src/mcp/tools/status.ts
|
|
3990
|
+
import { z as z3 } from "zod";
|
|
3991
|
+
function registerStatusTools(server, callApi) {
|
|
3992
|
+
server.registerTool(
|
|
3993
|
+
"lexq_whoami",
|
|
3994
|
+
{
|
|
3995
|
+
title: "Who Am I",
|
|
3996
|
+
annotations: { readOnlyHint: true },
|
|
3997
|
+
description: 'Show current authentication info (tenant ID, user ID, role) and tenantTimezone, the organization time zone that every yyyy-MM-dd date filter uses. Work out "today" or "yesterday" in that zone, not in UTC.',
|
|
3998
|
+
inputSchema: z3.object({})
|
|
3999
|
+
},
|
|
4000
|
+
async () => callApi("GET", "whoami")
|
|
4001
|
+
);
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
// src/mcp/tools/groups.ts
|
|
4005
|
+
import { z as z4 } from "zod";
|
|
4006
|
+
function registerGroupTools(server, callApi) {
|
|
4007
|
+
server.registerTool(
|
|
4008
|
+
"lexq_groups_list",
|
|
4009
|
+
{
|
|
4010
|
+
title: "List Policy Groups",
|
|
4011
|
+
annotations: { readOnlyHint: true },
|
|
4012
|
+
description: "List policy groups (tenant-wide, priority ASC). Deleted groups are not included.",
|
|
4013
|
+
inputSchema: z4.object({})
|
|
4014
|
+
},
|
|
4015
|
+
async () => callApi("GET", "policy-groups")
|
|
4016
|
+
);
|
|
4017
|
+
server.registerTool(
|
|
4018
|
+
"lexq_groups_get",
|
|
4019
|
+
{
|
|
4020
|
+
title: "Get Policy Group",
|
|
4021
|
+
annotations: { readOnlyHint: true },
|
|
4022
|
+
description: "Get a single policy group by ID. Its currentVersionId and currentDeploymentId are what lexq_deploy_rollback expects, and previousVersionName is the version a rollback restores.",
|
|
4023
|
+
inputSchema: z4.object({
|
|
4024
|
+
groupId: z4.string().uuid().describe("Policy group ID")
|
|
4025
|
+
})
|
|
4026
|
+
},
|
|
4027
|
+
async ({ groupId }) => callApi("GET", `policy-groups/${groupId}`)
|
|
4028
|
+
);
|
|
4029
|
+
server.registerTool(
|
|
4030
|
+
"lexq_groups_create",
|
|
4031
|
+
{
|
|
4032
|
+
title: "Create Policy Group",
|
|
4033
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4034
|
+
description: `Create a new policy group. Requires name. Priority is auto-assigned (appended last, tenant-wide); use lexq_groups_reorder to change order. Optionally set conflict resolution, activation group, and description. Policy groups that share an activationGroup form a cluster and must share the same activationMode / activationStrategy / executionLimit; executionLimit is how many of those groups run, not how many rules. A tenant can have at most ${MAX_GROUPS_PER_TENANT} groups that are not ARCHIVED; archive groups you no longer use to free a slot.`,
|
|
4035
|
+
inputSchema: z4.object({
|
|
4036
|
+
name: z4.string().describe("Group name (unique among non-ARCHIVED)"),
|
|
4037
|
+
description: z4.string().optional().describe("Group description"),
|
|
4038
|
+
activationMode: z4.enum(ConflictResolutionMode).optional().describe("Conflict resolution mode"),
|
|
4039
|
+
activationStrategy: z4.enum(ConflictResolutionStrategy).optional().describe("Ranking used to pick the winning groups when mode is EXCLUSIVE or MAX_N"),
|
|
4040
|
+
executionLimit: z4.number().int().min(1).optional().describe(
|
|
4041
|
+
"How many policy groups sharing this activationGroup may run (required when mode is MAX_N). Counts groups, not rules: every rule of a winning group runs."
|
|
4042
|
+
),
|
|
4043
|
+
activationGroup: z4.string().optional().describe(
|
|
4044
|
+
"Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
|
|
4045
|
+
)
|
|
4046
|
+
})
|
|
4047
|
+
},
|
|
4048
|
+
async (args) => {
|
|
4049
|
+
const body = {
|
|
4050
|
+
name: args.name
|
|
4051
|
+
};
|
|
4052
|
+
if (args.description !== void 0) body.description = args.description;
|
|
4053
|
+
if (args.activationMode !== void 0) body.activationMode = args.activationMode;
|
|
4054
|
+
if (args.activationStrategy !== void 0) body.activationStrategy = args.activationStrategy;
|
|
4055
|
+
if (args.executionLimit !== void 0) body.executionLimit = args.executionLimit;
|
|
4056
|
+
if (args.activationGroup !== void 0) body.activationGroup = args.activationGroup;
|
|
4057
|
+
return callApi("POST", "policy-groups", { body });
|
|
4058
|
+
}
|
|
4059
|
+
);
|
|
4060
|
+
server.registerTool(
|
|
4061
|
+
"lexq_groups_update",
|
|
4062
|
+
{
|
|
4063
|
+
title: "Update Policy Group",
|
|
4064
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4065
|
+
description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
|
|
4066
|
+
inputSchema: z4.object({
|
|
4067
|
+
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
4068
|
+
name: z4.string().optional().describe("New name"),
|
|
4069
|
+
description: z4.string().optional().describe("New description"),
|
|
4070
|
+
status: z4.enum(["ACTIVE", "DISABLED"]).optional().describe(
|
|
4071
|
+
"DISABLED stops production execution of this group: execution calls return 403 (P-009), and a composite call that includes the group is rejected in full. The deployed version and any A/B test are kept, and dry run, Impact Simulation, and Decision Replay keep working, so a fix can be verified before setting ACTIVE again. To go back to the previous version, use lexq_deploy_rollback. To take the version out of production, use lexq_deploy_undeploy."
|
|
4072
|
+
),
|
|
4073
|
+
activationGroup: z4.string().optional().describe(
|
|
4074
|
+
"Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
|
|
4075
|
+
),
|
|
4076
|
+
activationMode: z4.enum(ConflictResolutionMode).optional().describe("Conflict resolution mode"),
|
|
4077
|
+
activationStrategy: z4.enum(ConflictResolutionStrategy).optional().describe("Ranking used to pick the winning groups"),
|
|
4078
|
+
executionLimit: z4.number().int().min(1).optional().describe(
|
|
4079
|
+
"How many policy groups sharing this activationGroup may run. Counts groups, not rules."
|
|
4080
|
+
)
|
|
4081
|
+
})
|
|
4082
|
+
},
|
|
4083
|
+
async ({ groupId, ...body }) => callApi("PUT", `policy-groups/${groupId}`, { body })
|
|
4084
|
+
);
|
|
4085
|
+
server.registerTool(
|
|
4086
|
+
"lexq_groups_delete",
|
|
4087
|
+
{
|
|
4088
|
+
title: "Delete Policy Group",
|
|
4089
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4090
|
+
description: "Delete (archive) a policy group. A deployed group cannot be deleted (P-020); undeploy it first with lexq_deploy_undeploy, which also ends a running A/B test. This cannot be undone. The group no longer appears in lexq_groups_list and its name can be reused right away. Calls that change it (settings, versions, rules, starting an A/B test, publish, deploy, rollback, scheduling a deployment) return 409 P-059. If it was ever executed, deployed, or replayed, it is kept for audit; otherwise it is permanently deleted after 30 days.",
|
|
4091
|
+
inputSchema: z4.object({
|
|
4092
|
+
groupId: z4.string().uuid().describe("Policy group ID")
|
|
4093
|
+
})
|
|
4094
|
+
},
|
|
4095
|
+
async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}`)
|
|
4096
|
+
);
|
|
4097
|
+
server.registerTool(
|
|
4098
|
+
"lexq_groups_reorder",
|
|
4099
|
+
{
|
|
4100
|
+
title: "Reorder Policy Groups",
|
|
4101
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4102
|
+
description: "Reorder policy groups by priority. Priority is tenant-wide and flat (1...N continuous); array index 0 = priority 1 (highest precedence). activationGroup is not affected \u2014 this only changes priority.",
|
|
4103
|
+
inputSchema: z4.object({
|
|
4104
|
+
groupIds: z4.array(z4.string().uuid()).describe("Group IDs in desired priority order (index 0 = priority 1)")
|
|
4105
|
+
})
|
|
4106
|
+
},
|
|
4107
|
+
async ({ groupIds }) => {
|
|
4108
|
+
const groups = groupIds.map((groupId, index) => ({
|
|
4109
|
+
groupId,
|
|
4110
|
+
priority: index + 1
|
|
4111
|
+
}));
|
|
4112
|
+
return callApi("PATCH", "policy-groups/reorder", { body: { groups } });
|
|
4113
|
+
}
|
|
4114
|
+
);
|
|
4115
|
+
server.registerTool(
|
|
4116
|
+
"lexq_ab_test_start",
|
|
4117
|
+
{
|
|
4118
|
+
title: "Start A/B Test",
|
|
4119
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4120
|
+
description: "Start an A/B test on a policy group. Requires a challenger version ID and traffic rate. The challenger must be a published version of the same group; a version from another group is rejected with P-016. The split is computed from context.trafficKey on each execution request; requests that omit it never reach the challenger and the test stays at 0%.",
|
|
4121
|
+
inputSchema: z4.object({
|
|
4122
|
+
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
4123
|
+
testVersionId: z4.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
|
|
4124
|
+
trafficRate: z4.number().int().min(1).max(99).describe("Traffic percentage routed to challenger (1-99)")
|
|
4125
|
+
})
|
|
4126
|
+
},
|
|
4127
|
+
async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/ab-test`, { body })
|
|
4128
|
+
);
|
|
4129
|
+
server.registerTool(
|
|
4130
|
+
"lexq_ab_test_stop",
|
|
4131
|
+
{
|
|
4132
|
+
title: "Stop A/B Test",
|
|
4133
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4134
|
+
description: "Stop a running A/B test. All traffic is restored to the control (current) version.",
|
|
4135
|
+
inputSchema: z4.object({
|
|
4136
|
+
groupId: z4.string().uuid().describe("Policy group ID")
|
|
4137
|
+
})
|
|
4138
|
+
},
|
|
4139
|
+
async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}/ab-test`)
|
|
4140
|
+
);
|
|
4141
|
+
server.registerTool(
|
|
4142
|
+
"lexq_ab_test_adjust",
|
|
4143
|
+
{
|
|
4144
|
+
title: "Adjust A/B Test",
|
|
3813
4145
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3814
4146
|
description: "Adjust traffic rate of a running A/B test.",
|
|
3815
|
-
inputSchema:
|
|
3816
|
-
groupId:
|
|
3817
|
-
trafficRate:
|
|
4147
|
+
inputSchema: z4.object({
|
|
4148
|
+
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
4149
|
+
trafficRate: z4.number().int().min(1).max(99).describe("New traffic percentage for challenger (1-99)")
|
|
3818
4150
|
})
|
|
3819
4151
|
},
|
|
3820
4152
|
async ({ groupId, ...body }) => callApi("PATCH", `policy-groups/${groupId}/ab-test/traffic-rate`, { body })
|
|
@@ -3822,7 +4154,7 @@ function registerGroupTools(server, callApi) {
|
|
|
3822
4154
|
}
|
|
3823
4155
|
|
|
3824
4156
|
// src/mcp/tools/versions.ts
|
|
3825
|
-
import { z as
|
|
4157
|
+
import { z as z5 } from "zod";
|
|
3826
4158
|
function registerVersionTools(server, callApi) {
|
|
3827
4159
|
server.registerTool(
|
|
3828
4160
|
"lexq_versions_list",
|
|
@@ -3830,10 +4162,10 @@ function registerVersionTools(server, callApi) {
|
|
|
3830
4162
|
title: "List Policy Versions",
|
|
3831
4163
|
annotations: { readOnlyHint: true },
|
|
3832
4164
|
description: "List all versions of a policy group.",
|
|
3833
|
-
inputSchema:
|
|
3834
|
-
groupId:
|
|
3835
|
-
page:
|
|
3836
|
-
size:
|
|
4165
|
+
inputSchema: z5.object({
|
|
4166
|
+
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4167
|
+
page: z5.number().int().min(0).default(0).describe("Page number"),
|
|
4168
|
+
size: z5.number().int().min(1).max(100).default(20).describe("Page size")
|
|
3837
4169
|
})
|
|
3838
4170
|
},
|
|
3839
4171
|
async ({ groupId, page, size }) => callApi("GET", `policy-groups/${groupId}/versions`, {
|
|
@@ -3846,9 +4178,9 @@ function registerVersionTools(server, callApi) {
|
|
|
3846
4178
|
title: "Get Policy Version",
|
|
3847
4179
|
annotations: { readOnlyHint: true },
|
|
3848
4180
|
description: "Get a single version by ID, including its rules and fact requirements.",
|
|
3849
|
-
inputSchema:
|
|
3850
|
-
groupId:
|
|
3851
|
-
versionId:
|
|
4181
|
+
inputSchema: z5.object({
|
|
4182
|
+
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4183
|
+
versionId: z5.string().uuid().describe("Version ID")
|
|
3852
4184
|
})
|
|
3853
4185
|
},
|
|
3854
4186
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}`)
|
|
@@ -3859,11 +4191,11 @@ function registerVersionTools(server, callApi) {
|
|
|
3859
4191
|
title: "Create Policy Version",
|
|
3860
4192
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3861
4193
|
description: `Create a new DRAFT version in a policy group. Optionally provide a commit message and effective date range. A group can hold at most ${MAX_DRAFT_VERSIONS_PER_GROUP} DRAFT versions at a time; published versions do not count. Publish or delete drafts to make room.`,
|
|
3862
|
-
inputSchema:
|
|
3863
|
-
groupId:
|
|
3864
|
-
commitMessage:
|
|
3865
|
-
effectiveFrom:
|
|
3866
|
-
effectiveTo:
|
|
4194
|
+
inputSchema: z5.object({
|
|
4195
|
+
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4196
|
+
commitMessage: z5.string().optional().describe("Commit message describing this version"),
|
|
4197
|
+
effectiveFrom: z5.string().optional().describe(`Effective start, ${INSTANT}`),
|
|
4198
|
+
effectiveTo: z5.string().optional().describe(`Effective end, ${INSTANT}`)
|
|
3867
4199
|
})
|
|
3868
4200
|
},
|
|
3869
4201
|
async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/versions`, { body })
|
|
@@ -3874,14 +4206,14 @@ function registerVersionTools(server, callApi) {
|
|
|
3874
4206
|
title: "Update Policy Version",
|
|
3875
4207
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3876
4208
|
description: "Update a DRAFT version. Only DRAFT versions can be modified. Omitted fields keep their current values. To remove an end of the effective window, set clearEffectiveFrom or clearEffectiveTo to true instead of sending a value.",
|
|
3877
|
-
inputSchema:
|
|
3878
|
-
groupId:
|
|
3879
|
-
versionId:
|
|
3880
|
-
commitMessage:
|
|
3881
|
-
effectiveFrom:
|
|
3882
|
-
effectiveTo:
|
|
3883
|
-
clearEffectiveFrom:
|
|
3884
|
-
clearEffectiveTo:
|
|
4209
|
+
inputSchema: z5.object({
|
|
4210
|
+
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4211
|
+
versionId: z5.string().uuid().describe("Version ID"),
|
|
4212
|
+
commitMessage: z5.string().optional().describe("New commit message"),
|
|
4213
|
+
effectiveFrom: z5.string().optional().describe(`Effective start, ${INSTANT} Omit it to keep the current start.`),
|
|
4214
|
+
effectiveTo: z5.string().optional().describe(`Effective end, ${INSTANT} Omit it to keep the current end.`),
|
|
4215
|
+
clearEffectiveFrom: z5.boolean().optional().describe("Set to true to remove the effective start. Do not send effectiveFrom with it."),
|
|
4216
|
+
clearEffectiveTo: z5.boolean().optional().describe("Set to true to remove the effective end. Do not send effectiveTo with it.")
|
|
3885
4217
|
})
|
|
3886
4218
|
},
|
|
3887
4219
|
async ({ groupId, versionId, ...body }) => callApi("PUT", `policy-groups/${groupId}/versions/${versionId}`, { body })
|
|
@@ -3892,9 +4224,9 @@ function registerVersionTools(server, callApi) {
|
|
|
3892
4224
|
title: "Delete Policy Version",
|
|
3893
4225
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3894
4226
|
description: "Delete a DRAFT version. Only DRAFT versions can be deleted.",
|
|
3895
|
-
inputSchema:
|
|
3896
|
-
groupId:
|
|
3897
|
-
versionId:
|
|
4227
|
+
inputSchema: z5.object({
|
|
4228
|
+
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4229
|
+
versionId: z5.string().uuid().describe("Version ID")
|
|
3898
4230
|
})
|
|
3899
4231
|
},
|
|
3900
4232
|
async ({ groupId, versionId }) => callApi("DELETE", `policy-groups/${groupId}/versions/${versionId}`)
|
|
@@ -3905,9 +4237,9 @@ function registerVersionTools(server, callApi) {
|
|
|
3905
4237
|
title: "Clone Policy Version",
|
|
3906
4238
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3907
4239
|
description: `Clone an existing version to create a new DRAFT. Useful when the source version is already published. The new DRAFT counts toward the limit of ${MAX_DRAFT_VERSIONS_PER_GROUP} DRAFT versions per group, and a source with more than ${MAX_RULES_PER_VERSION} rules cannot be cloned.`,
|
|
3908
|
-
inputSchema:
|
|
3909
|
-
groupId:
|
|
3910
|
-
versionId:
|
|
4240
|
+
inputSchema: z5.object({
|
|
4241
|
+
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4242
|
+
versionId: z5.string().uuid().describe("Source version ID to clone")
|
|
3911
4243
|
})
|
|
3912
4244
|
},
|
|
3913
4245
|
async ({ groupId, versionId }) => {
|
|
@@ -3919,8 +4251,8 @@ function registerVersionTools(server, callApi) {
|
|
|
3919
4251
|
}
|
|
3920
4252
|
|
|
3921
4253
|
// src/mcp/tools/rules.ts
|
|
3922
|
-
import { z as
|
|
3923
|
-
import
|
|
4254
|
+
import { z as z6 } from "zod";
|
|
4255
|
+
import dedent17 from "dedent";
|
|
3924
4256
|
function registerRuleTools(server, callApi) {
|
|
3925
4257
|
server.registerTool(
|
|
3926
4258
|
"lexq_rules_list",
|
|
@@ -3928,9 +4260,9 @@ function registerRuleTools(server, callApi) {
|
|
|
3928
4260
|
title: "List Rules",
|
|
3929
4261
|
annotations: { readOnlyHint: true },
|
|
3930
4262
|
description: "List all rules in a version (priority ASC). Returns summary with conditionSummary and actionSummary.",
|
|
3931
|
-
inputSchema:
|
|
3932
|
-
groupId:
|
|
3933
|
-
versionId:
|
|
4263
|
+
inputSchema: z6.object({
|
|
4264
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4265
|
+
versionId: z6.string().uuid().describe("Version ID")
|
|
3934
4266
|
})
|
|
3935
4267
|
},
|
|
3936
4268
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/rules`)
|
|
@@ -3941,10 +4273,10 @@ function registerRuleTools(server, callApi) {
|
|
|
3941
4273
|
title: "Get Rule Detail",
|
|
3942
4274
|
annotations: { readOnlyHint: true },
|
|
3943
4275
|
description: "Get full rule detail including condition tree and action definitions.",
|
|
3944
|
-
inputSchema:
|
|
3945
|
-
groupId:
|
|
3946
|
-
versionId:
|
|
3947
|
-
ruleId:
|
|
4276
|
+
inputSchema: z6.object({
|
|
4277
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4278
|
+
versionId: z6.string().uuid().describe("Version ID"),
|
|
4279
|
+
ruleId: z6.string().uuid().describe("Rule ID")
|
|
3948
4280
|
})
|
|
3949
4281
|
},
|
|
3950
4282
|
async ({ groupId, versionId, ruleId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`)
|
|
@@ -3954,7 +4286,7 @@ function registerRuleTools(server, callApi) {
|
|
|
3954
4286
|
{
|
|
3955
4287
|
title: "Create Rule",
|
|
3956
4288
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3957
|
-
description:
|
|
4289
|
+
description: dedent17`
|
|
3958
4290
|
Create a rule in a DRAFT version. Requires name, condition tree, and actions array. priority is auto-assigned (appended last); use lexq_rules_reorder to change order.
|
|
3959
4291
|
|
|
3960
4292
|
Before creating rules with new fact keys, call lexq_facts_list to check existing facts.
|
|
@@ -4035,10 +4367,10 @@ function registerRuleTools(server, callApi) {
|
|
|
4035
4367
|
|
|
4036
4368
|
RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: "HALF_UP"|"HALF_DOWN"|"HALF_EVEN"|"FLOOR"|"CEILING"|"DOWN"|"UP" } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
|
|
4037
4369
|
`,
|
|
4038
|
-
inputSchema:
|
|
4039
|
-
groupId:
|
|
4040
|
-
versionId:
|
|
4041
|
-
rule:
|
|
4370
|
+
inputSchema: z6.object({
|
|
4371
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4372
|
+
versionId: z6.string().uuid().describe("Version ID"),
|
|
4373
|
+
rule: z6.string().describe(
|
|
4042
4374
|
"JSON string of CreateRuleRequest: { name, condition, actions, mutexGroup?, mutexMode?, mutexStrategy?, mutexLimit?, isEnabled? }"
|
|
4043
4375
|
)
|
|
4044
4376
|
})
|
|
@@ -4054,11 +4386,11 @@ function registerRuleTools(server, callApi) {
|
|
|
4054
4386
|
title: "Update Rule",
|
|
4055
4387
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4056
4388
|
description: `Update an existing rule in a DRAFT version. Only provided fields are changed. A new condition tree must stay within ${MAX_CONDITIONS_PER_RULE} conditions (SINGLE nodes) and ${MAX_CONDITION_DEPTH} levels of nesting below the root. Saving re-checks every action in the rule, including ones you did not change, and fails with ACT-032 if any action's targetVar is not a registered fact.`,
|
|
4057
|
-
inputSchema:
|
|
4058
|
-
groupId:
|
|
4059
|
-
versionId:
|
|
4060
|
-
ruleId:
|
|
4061
|
-
rule:
|
|
4389
|
+
inputSchema: z6.object({
|
|
4390
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4391
|
+
versionId: z6.string().uuid().describe("Version ID"),
|
|
4392
|
+
ruleId: z6.string().uuid().describe("Rule ID"),
|
|
4393
|
+
rule: z6.string().describe(
|
|
4062
4394
|
"JSON string of UpdateRuleRequest: { name?, condition?, actions?, mutexGroup?, mutexMode?, mutexStrategy?, mutexLimit?, isEnabled? }"
|
|
4063
4395
|
)
|
|
4064
4396
|
})
|
|
@@ -4076,11 +4408,11 @@ function registerRuleTools(server, callApi) {
|
|
|
4076
4408
|
title: "Delete Rule",
|
|
4077
4409
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4078
4410
|
description: "Delete a rule from a DRAFT version.",
|
|
4079
|
-
inputSchema:
|
|
4080
|
-
groupId:
|
|
4081
|
-
versionId:
|
|
4082
|
-
ruleId:
|
|
4083
|
-
force:
|
|
4411
|
+
inputSchema: z6.object({
|
|
4412
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4413
|
+
versionId: z6.string().uuid().describe("Version ID"),
|
|
4414
|
+
ruleId: z6.string().uuid().describe("Rule ID"),
|
|
4415
|
+
force: z6.boolean().default(false).describe("Skip confirmation (for the last rule in a version)")
|
|
4084
4416
|
})
|
|
4085
4417
|
},
|
|
4086
4418
|
async ({ groupId, versionId, ruleId, force }) => {
|
|
@@ -4097,10 +4429,10 @@ function registerRuleTools(server, callApi) {
|
|
|
4097
4429
|
title: "Reorder Rules",
|
|
4098
4430
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4099
4431
|
description: "Reorder rules by specifying rule IDs in desired order. Priorities are assigned 1...N (1-based, continuous); array index 0 = priority 1 (highest precedence).",
|
|
4100
|
-
inputSchema:
|
|
4101
|
-
groupId:
|
|
4102
|
-
versionId:
|
|
4103
|
-
ruleIds:
|
|
4432
|
+
inputSchema: z6.object({
|
|
4433
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4434
|
+
versionId: z6.string().uuid().describe("Version ID"),
|
|
4435
|
+
ruleIds: z6.array(z6.string().uuid()).describe("Rule IDs in desired priority order")
|
|
4104
4436
|
})
|
|
4105
4437
|
},
|
|
4106
4438
|
async ({ groupId, versionId, ruleIds }) => {
|
|
@@ -4119,11 +4451,11 @@ function registerRuleTools(server, callApi) {
|
|
|
4119
4451
|
title: "Toggle Rule",
|
|
4120
4452
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4121
4453
|
description: "Enable or disable a rule without deleting it.",
|
|
4122
|
-
inputSchema:
|
|
4123
|
-
groupId:
|
|
4124
|
-
versionId:
|
|
4125
|
-
ruleId:
|
|
4126
|
-
isEnabled:
|
|
4454
|
+
inputSchema: z6.object({
|
|
4455
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4456
|
+
versionId: z6.string().uuid().describe("Version ID"),
|
|
4457
|
+
ruleId: z6.string().uuid().describe("Rule ID"),
|
|
4458
|
+
isEnabled: z6.boolean().describe("true to enable, false to disable")
|
|
4127
4459
|
})
|
|
4128
4460
|
},
|
|
4129
4461
|
async ({ groupId, versionId, ruleId, isEnabled }) => callApi("PATCH", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}/enabled`, {
|
|
@@ -4133,7 +4465,7 @@ function registerRuleTools(server, callApi) {
|
|
|
4133
4465
|
}
|
|
4134
4466
|
|
|
4135
4467
|
// src/mcp/tools/facts.ts
|
|
4136
|
-
import { z as
|
|
4468
|
+
import { z as z7 } from "zod";
|
|
4137
4469
|
|
|
4138
4470
|
// src/types/facts.ts
|
|
4139
4471
|
var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
@@ -4147,10 +4479,10 @@ function registerFactTools(server, callApi) {
|
|
|
4147
4479
|
title: "List Fact Definitions",
|
|
4148
4480
|
annotations: { readOnlyHint: true },
|
|
4149
4481
|
description: "List all fact definitions (input variable schema). Shows key, type, required, and PII status. Always check this before creating rules.",
|
|
4150
|
-
inputSchema:
|
|
4151
|
-
page:
|
|
4152
|
-
size:
|
|
4153
|
-
keyword:
|
|
4482
|
+
inputSchema: z7.object({
|
|
4483
|
+
page: z7.number().int().min(0).default(0).describe("Page number"),
|
|
4484
|
+
size: z7.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
4485
|
+
keyword: z7.string().optional().describe("Search keyword")
|
|
4154
4486
|
})
|
|
4155
4487
|
},
|
|
4156
4488
|
async ({ page, size, keyword }) => {
|
|
@@ -4165,21 +4497,21 @@ function registerFactTools(server, callApi) {
|
|
|
4165
4497
|
title: "Create Fact Definition",
|
|
4166
4498
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4167
4499
|
description: "Register a new input variable. Key starts with a letter, then letters, numbers, and underscores (e.g. paymentAmount). Casing is not enforced. Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER.",
|
|
4168
|
-
inputSchema:
|
|
4169
|
-
key:
|
|
4170
|
-
name:
|
|
4171
|
-
type:
|
|
4172
|
-
description:
|
|
4173
|
-
isRequired:
|
|
4174
|
-
isPii:
|
|
4500
|
+
inputSchema: z7.object({
|
|
4501
|
+
key: z7.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
|
|
4502
|
+
name: z7.string().describe("Display name"),
|
|
4503
|
+
type: z7.enum(VALUE_TYPES).describe("Value type"),
|
|
4504
|
+
description: z7.string().optional().describe("Description"),
|
|
4505
|
+
isRequired: z7.boolean().default(false).describe("Whether this fact is required for rule evaluation"),
|
|
4506
|
+
isPii: z7.boolean().default(false).describe(
|
|
4175
4507
|
"Mark as PII \u2014 masked on every read surface, revealable only in the console (audited)"
|
|
4176
4508
|
),
|
|
4177
|
-
valueDomain:
|
|
4178
|
-
allowedValues:
|
|
4509
|
+
valueDomain: z7.object({
|
|
4510
|
+
allowedValues: z7.array(z7.union([z7.string(), z7.number()])).optional().describe(
|
|
4179
4511
|
"Accepted values. For list types the value domain applies to each element, not to the list as a whole."
|
|
4180
4512
|
),
|
|
4181
|
-
min:
|
|
4182
|
-
max:
|
|
4513
|
+
min: z7.number().optional().describe("Lower bound, inclusive. Numeric types only."),
|
|
4514
|
+
max: z7.number().optional().describe("Upper bound, inclusive. Numeric types only.")
|
|
4183
4515
|
}).optional().describe(
|
|
4184
4516
|
"Which values this fact accepts. Omit for no value domain. STRING and LIST_STRING take allowedValues only; NUMBER and LIST_NUMBER take all three; BOOLEAN takes none. Declaring it lets the rule editor offer a dropdown and rejects a rule literal that could never match."
|
|
4185
4517
|
)
|
|
@@ -4193,21 +4525,21 @@ function registerFactTools(server, callApi) {
|
|
|
4193
4525
|
title: "Update Fact Definition",
|
|
4194
4526
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4195
4527
|
description: "Update a fact definition. The key is immutable. The type can change only while no rule references the fact; if any does, the call fails with FD-007 and reports the count. Rules in deleted groups do not count. Only the fields you send are changed. System facts (isBlocked, blockReason) accept changes to name, description, PII, and the value domain; isRequired is ignored, and their type cannot change (FD-003).",
|
|
4196
|
-
inputSchema:
|
|
4197
|
-
factId:
|
|
4198
|
-
name:
|
|
4199
|
-
description:
|
|
4200
|
-
type:
|
|
4528
|
+
inputSchema: z7.object({
|
|
4529
|
+
factId: z7.string().uuid().describe("Fact definition ID"),
|
|
4530
|
+
name: z7.string().optional().describe("Display name"),
|
|
4531
|
+
description: z7.string().optional().describe('Description. Send "" to remove it.'),
|
|
4532
|
+
type: z7.enum(VALUE_TYPES).optional().describe(
|
|
4201
4533
|
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
|
|
4202
4534
|
),
|
|
4203
|
-
isRequired:
|
|
4204
|
-
isPii:
|
|
4205
|
-
valueDomain:
|
|
4206
|
-
allowedValues:
|
|
4535
|
+
isRequired: z7.boolean().optional().describe("Required flag"),
|
|
4536
|
+
isPii: z7.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)"),
|
|
4537
|
+
valueDomain: z7.object({
|
|
4538
|
+
allowedValues: z7.array(z7.union([z7.string(), z7.number()])).optional().describe(
|
|
4207
4539
|
"Accepted values. For list types the value domain applies to each element, not to the list as a whole."
|
|
4208
4540
|
),
|
|
4209
|
-
min:
|
|
4210
|
-
max:
|
|
4541
|
+
min: z7.number().optional().describe("Lower bound, inclusive. Numeric types only."),
|
|
4542
|
+
max: z7.number().optional().describe("Upper bound, inclusive. Numeric types only.")
|
|
4211
4543
|
}).optional().describe(
|
|
4212
4544
|
"Which values this fact accepts. Three states: omit the field to leave the value domain alone, send {} to remove it, or send a populated object to replace it. Narrowing it never blocks the update and does not rewrite rules. When rules compare this fact with a value the new value domain excludes, the result ends with a warning that lists them by version: drafts and the versions currently deployed."
|
|
4213
4545
|
)
|
|
@@ -4221,8 +4553,8 @@ function registerFactTools(server, callApi) {
|
|
|
4221
4553
|
title: "Delete Fact Definition",
|
|
4222
4554
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4223
4555
|
description: "Delete a fact definition. System facts cannot be deleted. Neither can a fact that any rule references: that call fails with FD-006 and reports the count. Rules in deleted groups do not count. Remove the references first.",
|
|
4224
|
-
inputSchema:
|
|
4225
|
-
factId:
|
|
4556
|
+
inputSchema: z7.object({
|
|
4557
|
+
factId: z7.string().uuid().describe("Fact definition ID")
|
|
4226
4558
|
})
|
|
4227
4559
|
},
|
|
4228
4560
|
async ({ factId }) => callApi("DELETE", `schema/facts/${factId}`)
|
|
@@ -4233,7 +4565,7 @@ function registerFactTools(server, callApi) {
|
|
|
4233
4565
|
title: "Get Action Runtime Fact Metadata",
|
|
4234
4566
|
annotations: { readOnlyHint: true },
|
|
4235
4567
|
description: "Retrieve runtime fact requirements per Action type. For each Action, shows which input facts must be present in the execution payload \u2014 e.g. MUTATE_FACT always requires its targetVar fact, plus refVar when one is specified. The factRequired flag describes the FACT, not the parameter: refVar is an optional parameter, but if you specify it the named fact must exist. A required fact absent at runtime throws \u2014 the engine never defaults to 0. These facts must be sent in the request (P-015 otherwise), even when an earlier action in the same rule writes them. SET_FACT requires no input fact: it is the only action whose targetVar the request does not have to send, and that targetVar must be a registered fact (ACT-032). Static data, safe to cache in-session.",
|
|
4236
|
-
inputSchema:
|
|
4568
|
+
inputSchema: z7.object({})
|
|
4237
4569
|
},
|
|
4238
4570
|
async () => callApi("GET", "schema/action-metadata")
|
|
4239
4571
|
);
|
|
@@ -4243,9 +4575,9 @@ function registerFactTools(server, callApi) {
|
|
|
4243
4575
|
title: "List Unregistered Facts",
|
|
4244
4576
|
annotations: { readOnlyHint: true },
|
|
4245
4577
|
description: "List facts referenced by a version's rules but not yet defined (read-only). Keys that rules only read do not block publish/deploy; a rule whose action targetVar is not a registered fact is rejected at save and publish with ACT-032. Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Register them with lexq_facts_create to enable type validation and the dry-run requirements analyzer.",
|
|
4246
|
-
inputSchema:
|
|
4247
|
-
groupId:
|
|
4248
|
-
versionId:
|
|
4578
|
+
inputSchema: z7.object({
|
|
4579
|
+
groupId: z7.string().uuid().describe("Policy group ID"),
|
|
4580
|
+
versionId: z7.string().uuid().describe("Version ID")
|
|
4249
4581
|
})
|
|
4250
4582
|
},
|
|
4251
4583
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
|
|
@@ -4256,9 +4588,9 @@ function registerFactTools(server, callApi) {
|
|
|
4256
4588
|
title: "Export Fact Catalog",
|
|
4257
4589
|
annotations: { readOnlyHint: true },
|
|
4258
4590
|
description: "Export the fact catalog. The two formats carry different things: CSV is the catalog as it stands, system facts included, for reading in a spreadsheet; JSON matches the shape that batch create accepts, so it can be fed straight back in, which is why it leaves out the fields that endpoint does not take. Returns the file contents as text.",
|
|
4259
|
-
inputSchema:
|
|
4260
|
-
format:
|
|
4261
|
-
keyword:
|
|
4591
|
+
inputSchema: z7.object({
|
|
4592
|
+
format: z7.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
|
|
4593
|
+
keyword: z7.string().optional().describe("Filter by key or name")
|
|
4262
4594
|
})
|
|
4263
4595
|
},
|
|
4264
4596
|
async ({ format, keyword }) => callApi("GET", "schema/facts/export", {
|
|
@@ -4268,8 +4600,8 @@ function registerFactTools(server, callApi) {
|
|
|
4268
4600
|
}
|
|
4269
4601
|
|
|
4270
4602
|
// src/mcp/tools/deploy.ts
|
|
4271
|
-
import
|
|
4272
|
-
import { z as
|
|
4603
|
+
import dedent18 from "dedent";
|
|
4604
|
+
import { z as z8 } from "zod";
|
|
4273
4605
|
function registerDeployTools(server, callApi) {
|
|
4274
4606
|
server.registerTool(
|
|
4275
4607
|
"lexq_deploy_publish",
|
|
@@ -4277,10 +4609,10 @@ function registerDeployTools(server, callApi) {
|
|
|
4277
4609
|
title: "Publish Version",
|
|
4278
4610
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4279
4611
|
description: `Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts that rules only read do not block publishing; call lexq_facts_unregistered first to review them. Publishing fails if an action's targetVar is not a registered fact (ACT-032), if the version's compiled rules exceed ${MAX_SNAPSHOT_BYTES / 1024} KB, or if its effective end has already passed (P-058); move the end into the future with lexq_versions_update first.`,
|
|
4280
|
-
inputSchema:
|
|
4281
|
-
groupId:
|
|
4282
|
-
versionId:
|
|
4283
|
-
memo:
|
|
4612
|
+
inputSchema: z8.object({
|
|
4613
|
+
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
4614
|
+
versionId: z8.string().uuid().describe("Version ID to publish"),
|
|
4615
|
+
memo: z8.string().min(1).describe("Publish memo (required)")
|
|
4284
4616
|
})
|
|
4285
4617
|
},
|
|
4286
4618
|
async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/versions/${versionId}/publish`, { body: { memo } })
|
|
@@ -4291,10 +4623,10 @@ function registerDeployTools(server, callApi) {
|
|
|
4291
4623
|
title: "Deploy to Live",
|
|
4292
4624
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4293
4625
|
description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately. Versions whose effective start date has not arrived are rejected (P-037) \u2014 use lexq_deploy_schedule for those. Versions whose effective end has passed are rejected (P-058). Undefined facts do not block deployment; use lexq_facts_unregistered to review what the version references but has not defined.",
|
|
4294
|
-
inputSchema:
|
|
4295
|
-
groupId:
|
|
4296
|
-
versionId:
|
|
4297
|
-
memo:
|
|
4626
|
+
inputSchema: z8.object({
|
|
4627
|
+
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
4628
|
+
versionId: z8.string().uuid().describe("Version ID to deploy"),
|
|
4629
|
+
memo: z8.string().min(1).describe("Deployment memo (required)")
|
|
4298
4630
|
})
|
|
4299
4631
|
},
|
|
4300
4632
|
async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/deploy`, {
|
|
@@ -4306,16 +4638,16 @@ function registerDeployTools(server, callApi) {
|
|
|
4306
4638
|
{
|
|
4307
4639
|
title: "Rollback Deployment",
|
|
4308
4640
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4309
|
-
description:
|
|
4641
|
+
description: dedent18`
|
|
4310
4642
|
Roll the group back to the version that was live before the current one. Call lexq_groups_get first: currentVersionName is live now, previousVersionName is what a rollback restores, and a rollback is rejected while canRollback is false. A rollback needs a previous version that is still ACTIVE and whose effective end has not passed (P-058).
|
|
4311
4643
|
|
|
4312
4644
|
Pass the currentVersionId and currentDeploymentId from that call as expectedCurrentVersionId and expectedCurrentDeploymentId. If anyone deployed, rolled back or undeployed in between, the rollback is rejected with 409 and nothing changes: P-062 if another version or no version is live, P-063 if the same version is live but from a newer deployment. Read the group again and check with the user before retrying.
|
|
4313
4645
|
`,
|
|
4314
|
-
inputSchema:
|
|
4315
|
-
groupId:
|
|
4316
|
-
expectedCurrentVersionId:
|
|
4317
|
-
expectedCurrentDeploymentId:
|
|
4318
|
-
memo:
|
|
4646
|
+
inputSchema: z8.object({
|
|
4647
|
+
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
4648
|
+
expectedCurrentVersionId: z8.string().uuid().describe("currentVersionId from your last read of the group"),
|
|
4649
|
+
expectedCurrentDeploymentId: z8.string().uuid().describe("currentDeploymentId from your last read of the group"),
|
|
4650
|
+
memo: z8.string().min(1).describe("Rollback reason (required)")
|
|
4319
4651
|
})
|
|
4320
4652
|
},
|
|
4321
4653
|
async ({ groupId, expectedCurrentVersionId, expectedCurrentDeploymentId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
|
|
@@ -4328,372 +4660,133 @@ function registerDeployTools(server, callApi) {
|
|
|
4328
4660
|
title: "Undeploy",
|
|
4329
4661
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4330
4662
|
description: "Remove the live version from traffic. The version stays ACTIVE but no longer serves requests.",
|
|
4331
|
-
inputSchema:
|
|
4332
|
-
groupId:
|
|
4333
|
-
memo:
|
|
4663
|
+
inputSchema: z8.object({
|
|
4664
|
+
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
4665
|
+
memo: z8.string().min(1).describe("Undeploy reason (required)")
|
|
4334
4666
|
})
|
|
4335
4667
|
},
|
|
4336
4668
|
async ({ groupId, memo }) => callApi("POST", `policy-groups/${groupId}/undeploy`, {
|
|
4337
|
-
body: { memo }
|
|
4338
|
-
})
|
|
4339
|
-
);
|
|
4340
|
-
server.registerTool(
|
|
4341
|
-
"lexq_deploy_schedule",
|
|
4342
|
-
{
|
|
4343
|
-
title: "Schedule Deployment",
|
|
4344
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4345
|
-
description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). The time is the version's effectiveFrom, set on the DRAFT before publishing; this call takes no time of its own. One pending schedule per group; manual deploy/rollback/undeploy, starting an A/B test, or archiving the group cancels it. The snapshot hash is sealed at scheduling and re-verified at execution (fail-closed).",
|
|
4346
|
-
inputSchema: z6.object({
|
|
4347
|
-
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4348
|
-
versionId: z6.string().uuid().describe("ACTIVE version ID with a future effective start date"),
|
|
4349
|
-
memo: z6.string().min(1).describe("Schedule memo (required)")
|
|
4350
|
-
})
|
|
4351
|
-
},
|
|
4352
|
-
async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/schedule`, {
|
|
4353
|
-
body: { versionId, memo }
|
|
4354
|
-
})
|
|
4355
|
-
);
|
|
4356
|
-
server.registerTool(
|
|
4357
|
-
"lexq_deploy_unschedule",
|
|
4358
|
-
{
|
|
4359
|
-
title: "Cancel Scheduled Deployment",
|
|
4360
|
-
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4361
|
-
description: "Cancel the pending scheduled deployment for a group. The version itself is not affected. Fails with P-039 if no pending schedule exists.",
|
|
4362
|
-
inputSchema: z6.object({
|
|
4363
|
-
groupId: z6.string().uuid().describe("Policy group ID")
|
|
4364
|
-
})
|
|
4365
|
-
},
|
|
4366
|
-
async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}/schedule`)
|
|
4367
|
-
);
|
|
4368
|
-
server.registerTool(
|
|
4369
|
-
"lexq_deploy_schedules",
|
|
4370
|
-
{
|
|
4371
|
-
title: "List Scheduled Deployments",
|
|
4372
|
-
annotations: { readOnlyHint: true },
|
|
4373
|
-
description: "List scheduled deployments across all groups (all statuses: PENDING, EXECUTED, CANCELED, FAILED), newest first.",
|
|
4374
|
-
inputSchema: z6.object({
|
|
4375
|
-
page: z6.number().int().min(0).default(0).describe("Page number"),
|
|
4376
|
-
size: z6.number().int().min(1).max(100).default(20).describe("Page size")
|
|
4377
|
-
})
|
|
4378
|
-
},
|
|
4379
|
-
async ({ page, size }) => callApi("GET", "policy-groups/schedules", { params: paginationParams(page, size) })
|
|
4380
|
-
);
|
|
4381
|
-
server.registerTool(
|
|
4382
|
-
"lexq_deploy_history",
|
|
4383
|
-
{
|
|
4384
|
-
title: "Deployment History",
|
|
4385
|
-
annotations: { readOnlyHint: true },
|
|
4386
|
-
description: "List deployment history across all groups.",
|
|
4387
|
-
inputSchema: z6.object({
|
|
4388
|
-
page: z6.number().int().min(0).default(0).describe("Page number"),
|
|
4389
|
-
size: z6.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
4390
|
-
groupId: z6.string().uuid().optional().describe("Filter by group ID"),
|
|
4391
|
-
types: z6.string().optional().describe("Filter by deployment types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)"),
|
|
4392
|
-
startDate: z6.string().optional().describe(START_DAY),
|
|
4393
|
-
endDate: z6.string().optional().describe(END_DAY)
|
|
4394
|
-
})
|
|
4395
|
-
},
|
|
4396
|
-
async ({ page, size, groupId, types, startDate, endDate }) => {
|
|
4397
|
-
const params = paginationParams(page, size);
|
|
4398
|
-
if (groupId) params.groupId = groupId;
|
|
4399
|
-
if (types) params.types = types;
|
|
4400
|
-
if (startDate) params.startDate = startDate;
|
|
4401
|
-
if (endDate) params.endDate = endDate;
|
|
4402
|
-
return callApi("GET", "deployments", { params });
|
|
4403
|
-
}
|
|
4404
|
-
);
|
|
4405
|
-
server.registerTool(
|
|
4406
|
-
"lexq_deploy_detail",
|
|
4407
|
-
{
|
|
4408
|
-
title: "Deployment Detail",
|
|
4409
|
-
annotations: { readOnlyHint: true },
|
|
4410
|
-
description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
|
|
4411
|
-
inputSchema: z6.object({
|
|
4412
|
-
deploymentId: z6.string().uuid().describe("Deployment ID")
|
|
4413
|
-
})
|
|
4414
|
-
},
|
|
4415
|
-
async ({ deploymentId }) => callApi("GET", `deployments/${deploymentId}`)
|
|
4416
|
-
);
|
|
4417
|
-
server.registerTool(
|
|
4418
|
-
"lexq_deploy_overview",
|
|
4419
|
-
{
|
|
4420
|
-
title: "Deployment Overview",
|
|
4421
|
-
annotations: { readOnlyHint: true },
|
|
4422
|
-
description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer. Each group also carries currentDeploymentId, previousVersionName (the version a rollback restores) and canRollback, as in lexq_groups_get.",
|
|
4423
|
-
inputSchema: z6.object({})
|
|
4424
|
-
},
|
|
4425
|
-
async () => callApi("GET", "deployments/overview")
|
|
4426
|
-
);
|
|
4427
|
-
server.registerTool(
|
|
4428
|
-
"lexq_deploy_deployable",
|
|
4429
|
-
{
|
|
4430
|
-
title: "List Deployable Versions",
|
|
4431
|
-
annotations: { readOnlyHint: true },
|
|
4432
|
-
description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
|
|
4433
|
-
inputSchema: z6.object({
|
|
4434
|
-
groupId: z6.string().uuid().describe("Policy group ID")
|
|
4435
|
-
})
|
|
4436
|
-
},
|
|
4437
|
-
async ({ groupId }) => callApi("GET", `deployments/groups/${groupId}/deployable-versions`)
|
|
4438
|
-
);
|
|
4439
|
-
server.registerTool(
|
|
4440
|
-
"lexq_deploy_diff",
|
|
4441
|
-
{
|
|
4442
|
-
title: "Deployment Diff",
|
|
4443
|
-
annotations: { readOnlyHint: true },
|
|
4444
|
-
description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
|
|
4445
|
-
inputSchema: z6.object({
|
|
4446
|
-
baseVersionId: z6.string().uuid().describe("Base version ID (typically the current live)"),
|
|
4447
|
-
targetVersionId: z6.string().uuid().describe("Target version ID (the one you want to deploy)")
|
|
4448
|
-
})
|
|
4449
|
-
},
|
|
4450
|
-
async ({ baseVersionId, targetVersionId }) => callApi("GET", "deployments/diff", {
|
|
4451
|
-
params: { baseVersionId, targetVersionId }
|
|
4452
|
-
})
|
|
4453
|
-
);
|
|
4454
|
-
}
|
|
4455
|
-
|
|
4456
|
-
// src/mcp/tools/analytics.ts
|
|
4457
|
-
import { z as z7 } from "zod";
|
|
4458
|
-
import dedent17 from "dedent";
|
|
4459
|
-
function registerAnalyticsTools(server, callApi) {
|
|
4460
|
-
server.registerTool(
|
|
4461
|
-
"lexq_dry_run",
|
|
4462
|
-
{
|
|
4463
|
-
title: "Dry Run",
|
|
4464
|
-
annotations: { readOnlyHint: true },
|
|
4465
|
-
description: dedent17`
|
|
4466
|
-
Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
|
|
4467
|
-
|
|
4468
|
-
Returns:
|
|
4469
|
-
inputFacts — normalized input facts
|
|
4470
|
-
mutatedFacts — input facts changed by rule actions (e.g. MUTATE_FACT mutates paymentAmount)
|
|
4471
|
-
generatedVariables — system-generated values; every fact in mutatedFacts gets a paired {factName}__delta key (signed difference)
|
|
4472
|
-
executionTraces — per-rule match status
|
|
4473
|
-
decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
|
|
4474
|
-
|
|
4475
|
-
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
|
|
4476
|
-
Always dry-run before publishing to validate rule behavior.`,
|
|
4477
|
-
inputSchema: z7.object({
|
|
4478
|
-
versionId: z7.string().uuid().describe("Policy version ID to test against"),
|
|
4479
|
-
facts: z7.string().describe('JSON string of facts object, e.g. {"paymentAmount":100000}'),
|
|
4480
|
-
includeDebugInfo: z7.boolean().default(true).describe("Include execution and decision traces")
|
|
4481
|
-
})
|
|
4482
|
-
},
|
|
4483
|
-
async ({ versionId, facts, includeDebugInfo }) => {
|
|
4484
|
-
const parsedFacts = parseJson(facts);
|
|
4485
|
-
return callApi("POST", `analytics/dry-run/versions/${versionId}`, {
|
|
4486
|
-
body: { facts: parsedFacts, includeDebugInfo }
|
|
4487
|
-
});
|
|
4488
|
-
}
|
|
4489
|
-
);
|
|
4490
|
-
server.registerTool(
|
|
4491
|
-
"lexq_dry_run_compare",
|
|
4492
|
-
{
|
|
4493
|
-
title: "Dry Run Compare",
|
|
4494
|
-
annotations: { readOnlyHint: true },
|
|
4495
|
-
description: dedent17`
|
|
4496
|
-
Compare dry run results between two versions using the same input facts. Useful for validating changes.
|
|
4497
|
-
|
|
4498
|
-
Returns:
|
|
4499
|
-
resultA / resultB — full DryRunResponse for each version
|
|
4500
|
-
diff.mutatedDiff — changes in mutatedFacts between A and B (key → {before, after})
|
|
4501
|
-
diff.generatedDiff — changes in generatedVariables between A and B`,
|
|
4502
|
-
inputSchema: z7.object({
|
|
4503
|
-
versionIdA: z7.string().uuid().describe("Baseline version ID"),
|
|
4504
|
-
versionIdB: z7.string().uuid().describe("Candidate version ID"),
|
|
4505
|
-
facts: z7.string().describe("JSON string of facts object")
|
|
4506
|
-
})
|
|
4507
|
-
},
|
|
4508
|
-
async ({ versionIdA, versionIdB, facts }) => {
|
|
4509
|
-
const parsedFacts = parseJson(facts);
|
|
4510
|
-
return callApi("POST", "analytics/dry-run/compare", {
|
|
4511
|
-
body: { versionIdA, versionIdB, facts: parsedFacts }
|
|
4512
|
-
});
|
|
4513
|
-
}
|
|
4669
|
+
body: { memo }
|
|
4670
|
+
})
|
|
4514
4671
|
);
|
|
4515
4672
|
server.registerTool(
|
|
4516
|
-
"
|
|
4673
|
+
"lexq_deploy_schedule",
|
|
4517
4674
|
{
|
|
4518
|
-
title: "
|
|
4519
|
-
annotations: { readOnlyHint:
|
|
4520
|
-
description: "
|
|
4521
|
-
inputSchema:
|
|
4522
|
-
groupId:
|
|
4523
|
-
versionId:
|
|
4675
|
+
title: "Schedule Deployment",
|
|
4676
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4677
|
+
description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). The time is the version's effectiveFrom, set on the DRAFT before publishing; this call takes no time of its own. One pending schedule per group; manual deploy/rollback/undeploy, starting an A/B test, or archiving the group cancels it. The snapshot hash is sealed at scheduling and re-verified at execution (fail-closed).",
|
|
4678
|
+
inputSchema: z8.object({
|
|
4679
|
+
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
4680
|
+
versionId: z8.string().uuid().describe("ACTIVE version ID with a future effective start date"),
|
|
4681
|
+
memo: z8.string().min(1).describe("Schedule memo (required)")
|
|
4524
4682
|
})
|
|
4525
4683
|
},
|
|
4526
|
-
async ({ groupId, versionId }) => callApi("
|
|
4684
|
+
async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/schedule`, {
|
|
4685
|
+
body: { versionId, memo }
|
|
4686
|
+
})
|
|
4527
4687
|
);
|
|
4528
4688
|
server.registerTool(
|
|
4529
|
-
"
|
|
4689
|
+
"lexq_deploy_unschedule",
|
|
4530
4690
|
{
|
|
4531
|
-
title: "
|
|
4532
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
4533
|
-
description:
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
dataset.type and dataset.source are BOTH required, and must be paired:
|
|
4537
|
-
HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
|
|
4538
|
-
in the organization time zone, both inclusive)
|
|
4539
|
-
UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
|
|
4540
|
-
MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
|
|
4541
|
-
|
|
4542
|
-
options.maxRecords: number (max ${MAX_SIMULATION_RECORDS}). When omitted, HISTORICAL and UPLOADED run at
|
|
4543
|
-
most ${DEFAULT_SIMULATION_RECORDS} records and MANUAL runs every record sent.
|
|
4544
|
-
options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
|
|
4545
|
-
options.includeRuleStats: boolean
|
|
4546
|
-
options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
|
|
4547
|
-
{ "targetVariable": "<fact>", "aggregationType": "COUNT" | "SUM" | "AVG" }
|
|
4548
|
-
|
|
4549
|
-
Example (uploaded dataset):
|
|
4550
|
-
{
|
|
4551
|
-
"policyVersionId": "<uuid>",
|
|
4552
|
-
"dataset": { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<path from lexq_dataset_upload>" },
|
|
4553
|
-
"options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true, "maxRecords": 10000 }
|
|
4554
|
-
}
|
|
4555
|
-
|
|
4556
|
-
Example (historical):
|
|
4557
|
-
{
|
|
4558
|
-
"policyVersionId": "<uuid>",
|
|
4559
|
-
"dataset": { "type": "HISTORICAL", "source": "EXECUTION_LOGS", "from": "2026-01-01", "to": "2026-01-31" },
|
|
4560
|
-
"options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true }
|
|
4561
|
-
}
|
|
4562
|
-
`,
|
|
4563
|
-
inputSchema: z7.object({
|
|
4564
|
-
body: z7.string().describe("JSON string of SimulationRequest")
|
|
4691
|
+
title: "Cancel Scheduled Deployment",
|
|
4692
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4693
|
+
description: "Cancel the pending scheduled deployment for a group. The version itself is not affected. Fails with P-039 if no pending schedule exists.",
|
|
4694
|
+
inputSchema: z8.object({
|
|
4695
|
+
groupId: z8.string().uuid().describe("Policy group ID")
|
|
4565
4696
|
})
|
|
4566
4697
|
},
|
|
4567
|
-
async ({
|
|
4568
|
-
const parsed = parseJson(body);
|
|
4569
|
-
return callApi("POST", "analytics/simulations", { body: parsed });
|
|
4570
|
-
}
|
|
4698
|
+
async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}/schedule`)
|
|
4571
4699
|
);
|
|
4572
4700
|
server.registerTool(
|
|
4573
|
-
"
|
|
4701
|
+
"lexq_deploy_schedules",
|
|
4574
4702
|
{
|
|
4575
|
-
title: "
|
|
4703
|
+
title: "List Scheduled Deployments",
|
|
4576
4704
|
annotations: { readOnlyHint: true },
|
|
4577
|
-
description: "
|
|
4578
|
-
inputSchema:
|
|
4579
|
-
|
|
4705
|
+
description: "List scheduled deployments across all groups (all statuses: PENDING, EXECUTED, CANCELED, FAILED), newest first.",
|
|
4706
|
+
inputSchema: z8.object({
|
|
4707
|
+
page: z8.number().int().min(0).default(0).describe("Page number"),
|
|
4708
|
+
size: z8.number().int().min(1).max(100).default(20).describe("Page size")
|
|
4580
4709
|
})
|
|
4581
4710
|
},
|
|
4582
|
-
async ({
|
|
4711
|
+
async ({ page, size }) => callApi("GET", "policy-groups/schedules", { params: paginationParams(page, size) })
|
|
4583
4712
|
);
|
|
4584
4713
|
server.registerTool(
|
|
4585
|
-
"
|
|
4714
|
+
"lexq_deploy_history",
|
|
4586
4715
|
{
|
|
4587
|
-
title: "
|
|
4716
|
+
title: "Deployment History",
|
|
4588
4717
|
annotations: { readOnlyHint: true },
|
|
4589
|
-
description: "List
|
|
4590
|
-
inputSchema:
|
|
4591
|
-
page:
|
|
4592
|
-
size:
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4718
|
+
description: "List deployment history across all groups.",
|
|
4719
|
+
inputSchema: z8.object({
|
|
4720
|
+
page: z8.number().int().min(0).default(0).describe("Page number"),
|
|
4721
|
+
size: z8.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
4722
|
+
groupId: z8.string().uuid().optional().describe("Filter by group ID"),
|
|
4723
|
+
types: z8.string().optional().describe("Filter by deployment types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)"),
|
|
4724
|
+
startDate: z8.string().optional().describe(START_DAY),
|
|
4725
|
+
endDate: z8.string().optional().describe(END_DAY)
|
|
4596
4726
|
})
|
|
4597
4727
|
},
|
|
4598
|
-
async ({ page, size,
|
|
4728
|
+
async ({ page, size, groupId, types, startDate, endDate }) => {
|
|
4599
4729
|
const params = paginationParams(page, size);
|
|
4600
|
-
if (
|
|
4601
|
-
if (
|
|
4602
|
-
if (
|
|
4603
|
-
|
|
4730
|
+
if (groupId) params.groupId = groupId;
|
|
4731
|
+
if (types) params.types = types;
|
|
4732
|
+
if (startDate) params.startDate = startDate;
|
|
4733
|
+
if (endDate) params.endDate = endDate;
|
|
4734
|
+
return callApi("GET", "deployments", { params });
|
|
4604
4735
|
}
|
|
4605
4736
|
);
|
|
4606
4737
|
server.registerTool(
|
|
4607
|
-
"
|
|
4738
|
+
"lexq_deploy_detail",
|
|
4608
4739
|
{
|
|
4609
|
-
title: "
|
|
4610
|
-
annotations: { readOnlyHint:
|
|
4611
|
-
description: "
|
|
4612
|
-
inputSchema:
|
|
4613
|
-
|
|
4740
|
+
title: "Deployment Detail",
|
|
4741
|
+
annotations: { readOnlyHint: true },
|
|
4742
|
+
description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
|
|
4743
|
+
inputSchema: z8.object({
|
|
4744
|
+
deploymentId: z8.string().uuid().describe("Deployment ID")
|
|
4614
4745
|
})
|
|
4615
4746
|
},
|
|
4616
|
-
async ({
|
|
4747
|
+
async ({ deploymentId }) => callApi("GET", `deployments/${deploymentId}`)
|
|
4617
4748
|
);
|
|
4618
4749
|
server.registerTool(
|
|
4619
|
-
"
|
|
4750
|
+
"lexq_deploy_overview",
|
|
4620
4751
|
{
|
|
4621
|
-
title: "
|
|
4752
|
+
title: "Deployment Overview",
|
|
4622
4753
|
annotations: { readOnlyHint: true },
|
|
4623
|
-
description: "
|
|
4624
|
-
inputSchema:
|
|
4625
|
-
simulationId: z7.string().uuid().describe("Simulation ID"),
|
|
4626
|
-
format: z7.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
4627
|
-
})
|
|
4754
|
+
description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer. Each group also carries currentDeploymentId, previousVersionName (the version a rollback restores) and canRollback, as in lexq_groups_get.",
|
|
4755
|
+
inputSchema: z8.object({})
|
|
4628
4756
|
},
|
|
4629
|
-
async (
|
|
4630
|
-
params: { format }
|
|
4631
|
-
})
|
|
4757
|
+
async () => callApi("GET", "deployments/overview")
|
|
4632
4758
|
);
|
|
4633
4759
|
server.registerTool(
|
|
4634
|
-
"
|
|
4760
|
+
"lexq_deploy_deployable",
|
|
4635
4761
|
{
|
|
4636
|
-
title: "
|
|
4637
|
-
annotations: { readOnlyHint:
|
|
4638
|
-
description:
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
To use the returned path in lexq_simulation_start, set:
|
|
4643
|
-
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
4644
|
-
|
|
4645
|
-
CSV example:
|
|
4646
|
-
userId,paymentAmount
|
|
4647
|
-
user_001,150000
|
|
4648
|
-
user_002,50000
|
|
4649
|
-
|
|
4650
|
-
JSON example: [{"userId":"user_001","paymentAmount":150000}, {"userId":"user_002","paymentAmount":50000}]
|
|
4651
|
-
`,
|
|
4652
|
-
inputSchema: z7.object({
|
|
4653
|
-
content: z7.string().describe("CSV or JSON content as string"),
|
|
4654
|
-
filename: z7.string().default("dataset.csv").describe("Filename with extension (.csv or .json)")
|
|
4762
|
+
title: "List Deployable Versions",
|
|
4763
|
+
annotations: { readOnlyHint: true },
|
|
4764
|
+
description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
|
|
4765
|
+
inputSchema: z8.object({
|
|
4766
|
+
groupId: z8.string().uuid().describe("Policy group ID")
|
|
4655
4767
|
})
|
|
4656
4768
|
},
|
|
4657
|
-
async ({
|
|
4658
|
-
const result = await callApi("POST", "analytics/datasets/upload", {
|
|
4659
|
-
upload: { content, filename, fieldName: "file" }
|
|
4660
|
-
});
|
|
4661
|
-
if (!result.isError) {
|
|
4662
|
-
try {
|
|
4663
|
-
const uploaded = parseJson(result.content[0]?.text ?? "{}");
|
|
4664
|
-
if (uploaded.path) {
|
|
4665
|
-
const dataset = { type: "UPLOADED", source: "S3_BUCKET", path: uploaded.path };
|
|
4666
|
-
result.content.push({
|
|
4667
|
-
type: "text",
|
|
4668
|
-
text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
|
|
4669
|
-
});
|
|
4670
|
-
}
|
|
4671
|
-
} catch {
|
|
4672
|
-
}
|
|
4673
|
-
}
|
|
4674
|
-
return result;
|
|
4675
|
-
}
|
|
4769
|
+
async ({ groupId }) => callApi("GET", `deployments/groups/${groupId}/deployable-versions`)
|
|
4676
4770
|
);
|
|
4677
4771
|
server.registerTool(
|
|
4678
|
-
"
|
|
4772
|
+
"lexq_deploy_diff",
|
|
4679
4773
|
{
|
|
4680
|
-
title: "
|
|
4774
|
+
title: "Deployment Diff",
|
|
4681
4775
|
annotations: { readOnlyHint: true },
|
|
4682
|
-
description: "
|
|
4683
|
-
inputSchema:
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
format: z7.enum(EXPORT_FORMATS).default("csv").describe("Template format")
|
|
4776
|
+
description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
|
|
4777
|
+
inputSchema: z8.object({
|
|
4778
|
+
baseVersionId: z8.string().uuid().describe("Base version ID (typically the current live)"),
|
|
4779
|
+
targetVersionId: z8.string().uuid().describe("Target version ID (the one you want to deploy)")
|
|
4687
4780
|
})
|
|
4688
4781
|
},
|
|
4689
|
-
async ({
|
|
4690
|
-
params: {
|
|
4782
|
+
async ({ baseVersionId, targetVersionId }) => callApi("GET", "deployments/diff", {
|
|
4783
|
+
params: { baseVersionId, targetVersionId }
|
|
4691
4784
|
})
|
|
4692
4785
|
);
|
|
4693
4786
|
}
|
|
4694
4787
|
|
|
4695
4788
|
// src/mcp/tools/profile.ts
|
|
4696
|
-
import { z as
|
|
4789
|
+
import { z as z9 } from "zod";
|
|
4697
4790
|
var RELATIVE_THRESHOLD = `flagged = p50 \u2265 ${SLOW_MULTIPLIER}\xD7 median of per-rule p50s within the group; absolute thresholds are intentionally not supported.`;
|
|
4698
4791
|
function profileParams(opts) {
|
|
4699
4792
|
const params = {};
|
|
@@ -4710,14 +4803,14 @@ function registerProfileTools(server, callApi) {
|
|
|
4710
4803
|
title: "Group Latency Profile",
|
|
4711
4804
|
annotations: { readOnlyHint: true },
|
|
4712
4805
|
description: "Per-rule latency profile of a policy group over a time window: group TOTAL distribution split by cache state (HIT = compiled ruleset cache hit, MISS = deep-load + compile), a per-rule CONDITION/ACTION percentile table, and slow-rule flags. " + RELATIVE_THRESHOLD + ` Every percentile is accompanied by its sample count n; a percentile is withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 needs n \u2265 6, p95 n \u2265 60, p99 n \u2265 300 \u2014 display gate, separate from the n \u2265 ${MIN_SAMPLES} judgment gate). Baselines report INSUFFICIENT_COHORT when fewer than ${MIN_COHORT_SIZE} rules qualify. Rule detail comes from a deterministic ${PROFILE_SAMPLE_PERMILLE / 10}% sample of calls; TOTAL is recorded for every call. Defaults: last ${PROFILE_DEFAULT_WINDOW_HOURS}h, live version, cacheState HIT.`,
|
|
4713
|
-
inputSchema:
|
|
4714
|
-
groupId:
|
|
4715
|
-
versionId:
|
|
4716
|
-
from:
|
|
4806
|
+
inputSchema: z9.object({
|
|
4807
|
+
groupId: z9.string().uuid().describe("Policy group ID"),
|
|
4808
|
+
versionId: z9.string().uuid().optional().describe("Version to inspect (default: live version)"),
|
|
4809
|
+
from: z9.string().optional().describe(
|
|
4717
4810
|
`Window start, ISO-8601 instant (e.g. 2026-07-01T00:00:00Z). Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
|
|
4718
4811
|
),
|
|
4719
|
-
to:
|
|
4720
|
-
cacheState:
|
|
4812
|
+
to: z9.string().optional().describe("Window end, ISO-8601 instant. Default: now"),
|
|
4813
|
+
cacheState: z9.enum(["HIT", "MISS"]).optional().describe("Cache dimension for the rule table and judgment (default: HIT)")
|
|
4721
4814
|
})
|
|
4722
4815
|
},
|
|
4723
4816
|
async ({ groupId, versionId, from, to, cacheState }) => callApi("GET", `policy-groups/${groupId}/profile`, {
|
|
@@ -4730,14 +4823,14 @@ function registerProfileTools(server, callApi) {
|
|
|
4730
4823
|
title: "Rule Latency Detail",
|
|
4731
4824
|
annotations: { readOnlyHint: true },
|
|
4732
4825
|
description: `Single-rule latency detail: merged phase \xD7 cacheState distributions plus a per-window time series (${LATENCY_WINDOW_MILLIS / 1e3}s windows). Missing windows are genuine gaps \u2014 never interpolated. Series points carry each window's own values; percentiles in merged distributions are withheld (null) unless n\xD7(1\u2212q) \u2265 ${TAIL_MIN_OBS} (p50 n \u2265 6, p95 n \u2265 60, p99 n \u2265 300). ` + RELATIVE_THRESHOLD,
|
|
4733
|
-
inputSchema:
|
|
4734
|
-
groupId:
|
|
4735
|
-
ruleId:
|
|
4736
|
-
versionId:
|
|
4737
|
-
from:
|
|
4826
|
+
inputSchema: z9.object({
|
|
4827
|
+
groupId: z9.string().uuid().describe("Policy group ID"),
|
|
4828
|
+
ruleId: z9.string().uuid().describe("Rule ID (from lexq_profile_overview)"),
|
|
4829
|
+
versionId: z9.string().uuid().optional().describe("Version to inspect (default: live version)"),
|
|
4830
|
+
from: z9.string().optional().describe(
|
|
4738
4831
|
`Window start, ISO-8601 instant. Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
|
|
4739
4832
|
),
|
|
4740
|
-
to:
|
|
4833
|
+
to: z9.string().optional().describe("Window end, ISO-8601 instant. Default: now")
|
|
4741
4834
|
})
|
|
4742
4835
|
},
|
|
4743
4836
|
async ({ groupId, ruleId, versionId, from, to }) => callApi("GET", `policy-groups/${groupId}/profile/rules/${ruleId}`, {
|
|
@@ -4747,7 +4840,7 @@ function registerProfileTools(server, callApi) {
|
|
|
4747
4840
|
}
|
|
4748
4841
|
|
|
4749
4842
|
// src/mcp/tools/replay.ts
|
|
4750
|
-
import { z as
|
|
4843
|
+
import { z as z10 } from "zod";
|
|
4751
4844
|
function registerReplayTools(server, callApi) {
|
|
4752
4845
|
server.registerTool(
|
|
4753
4846
|
"lexq_replay_decision",
|
|
@@ -4755,9 +4848,9 @@ function registerReplayTools(server, callApi) {
|
|
|
4755
4848
|
title: "Replay a Decision",
|
|
4756
4849
|
annotations: { readOnlyHint: true },
|
|
4757
4850
|
description: "Re-evaluate a past execution (traceId) against a candidate version and return the decision diff (decisionChanged, effect changes, fired rules) plus a determinism verdict. Synchronous and free of charge (TPS throttle only). A replay sends no webhook, notification, or event: rule actions produce no outward effects.",
|
|
4758
|
-
inputSchema:
|
|
4759
|
-
traceId:
|
|
4760
|
-
candidateVersionId:
|
|
4851
|
+
inputSchema: z10.object({
|
|
4852
|
+
traceId: z10.string().describe("Trace ID of the past execution to replay"),
|
|
4853
|
+
candidateVersionId: z10.string().uuid().describe("Version to re-evaluate against")
|
|
4761
4854
|
})
|
|
4762
4855
|
},
|
|
4763
4856
|
async ({ traceId, candidateVersionId }) => callApi("POST", "replay/decisions", { body: { traceId, candidateVersionId } })
|
|
@@ -4768,11 +4861,11 @@ function registerReplayTools(server, callApi) {
|
|
|
4768
4861
|
title: "Start Window Replay (Blast Radius)",
|
|
4769
4862
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4770
4863
|
description: "Submit an async job that replays a date window of past executions against a candidate version and measures the blast radius (how many decisions change). Billed per replayed record (REPLAY metric); VIEWER role cannot submit. Poll with lexq_replay_status.",
|
|
4771
|
-
inputSchema:
|
|
4772
|
-
candidateVersionId:
|
|
4773
|
-
from:
|
|
4774
|
-
to:
|
|
4775
|
-
maxRecords:
|
|
4864
|
+
inputSchema: z10.object({
|
|
4865
|
+
candidateVersionId: z10.string().uuid().describe("Version to re-evaluate against"),
|
|
4866
|
+
from: z10.string().describe(WINDOW_START_DAY),
|
|
4867
|
+
to: z10.string().describe(WINDOW_END_DAY),
|
|
4868
|
+
maxRecords: z10.number().int().min(1).optional().describe(
|
|
4776
4869
|
`Sample cap (server default applies; hard cap ${REPLAY_WINDOW_MAX_RECORDS / 1e3}k)`
|
|
4777
4870
|
)
|
|
4778
4871
|
})
|
|
@@ -4785,8 +4878,8 @@ function registerReplayTools(server, callApi) {
|
|
|
4785
4878
|
title: "Get Replay Job Status",
|
|
4786
4879
|
annotations: { readOnlyHint: true },
|
|
4787
4880
|
description: "Poll a window replay job. RUNNING shows progress 0\u2013100; COMPLETED fills summary and changedSamples; FAILED carries errorMessage. capped=true means the window exceeded the sample cap and only part was replayed.",
|
|
4788
|
-
inputSchema:
|
|
4789
|
-
jobId:
|
|
4881
|
+
inputSchema: z10.object({
|
|
4882
|
+
jobId: z10.string().describe("Replay job ID from lexq_replay_start")
|
|
4790
4883
|
})
|
|
4791
4884
|
},
|
|
4792
4885
|
async ({ jobId }) => callApi("GET", `replay/jobs/${jobId}`)
|
|
@@ -4797,9 +4890,9 @@ function registerReplayTools(server, callApi) {
|
|
|
4797
4890
|
title: "List Replay Jobs",
|
|
4798
4891
|
annotations: { readOnlyHint: true },
|
|
4799
4892
|
description: "List window replay job history (reverse-chronological). Lightweight items \u2014 use lexq_replay_status for summary and changed samples.",
|
|
4800
|
-
inputSchema:
|
|
4801
|
-
page:
|
|
4802
|
-
size:
|
|
4893
|
+
inputSchema: z10.object({
|
|
4894
|
+
page: z10.number().int().min(0).default(0).describe("Page number"),
|
|
4895
|
+
size: z10.number().int().min(1).max(100).default(20).describe("Page size")
|
|
4803
4896
|
})
|
|
4804
4897
|
},
|
|
4805
4898
|
async ({ page, size }) => callApi("GET", "replay/jobs", { params: paginationParams(page, size) })
|
|
@@ -4810,8 +4903,8 @@ function registerReplayTools(server, callApi) {
|
|
|
4810
4903
|
title: "Cancel Replay Job",
|
|
4811
4904
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4812
4905
|
description: "Cooperatively cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
|
|
4813
|
-
inputSchema:
|
|
4814
|
-
jobId:
|
|
4906
|
+
inputSchema: z10.object({
|
|
4907
|
+
jobId: z10.string().describe("Replay job ID")
|
|
4815
4908
|
})
|
|
4816
4909
|
},
|
|
4817
4910
|
async ({ jobId }) => callApi("POST", `replay/jobs/${jobId}/cancel`)
|
|
@@ -4822,9 +4915,9 @@ function registerReplayTools(server, callApi) {
|
|
|
4822
4915
|
title: "Export Replay Result",
|
|
4823
4916
|
annotations: { readOnlyHint: true },
|
|
4824
4917
|
description: "Export a COMPLETED window replay job. A running job is rejected \u2014 a partial result reads as the whole thing on the receiving end. The two formats carry different things: CSV holds the effect blast radius, whose columns are fixed; JSON holds the changed samples and action parameters, which nest and whose keys differ per tenant. JSON is not a superset of CSV. Returns the file contents as text.",
|
|
4825
|
-
inputSchema:
|
|
4826
|
-
jobId:
|
|
4827
|
-
format:
|
|
4918
|
+
inputSchema: z10.object({
|
|
4919
|
+
jobId: z10.string().describe("Replay job ID"),
|
|
4920
|
+
format: z10.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
4828
4921
|
})
|
|
4829
4922
|
},
|
|
4830
4923
|
async ({ jobId, format }) => callApi("GET", `replay/jobs/${jobId}/export`, { params: { format } })
|
|
@@ -4832,7 +4925,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4832
4925
|
}
|
|
4833
4926
|
|
|
4834
4927
|
// src/mcp/tools/history.ts
|
|
4835
|
-
import { z as
|
|
4928
|
+
import { z as z11 } from "zod";
|
|
4836
4929
|
function registerHistoryTools(server, callApi) {
|
|
4837
4930
|
server.registerTool(
|
|
4838
4931
|
"lexq_history_list",
|
|
@@ -4840,15 +4933,15 @@ function registerHistoryTools(server, callApi) {
|
|
|
4840
4933
|
title: "List Execution History",
|
|
4841
4934
|
annotations: { readOnlyHint: true },
|
|
4842
4935
|
description: "List policy execution history. Shows trace ID, group, version, status, match result, and latency.",
|
|
4843
|
-
inputSchema:
|
|
4844
|
-
page:
|
|
4845
|
-
size:
|
|
4846
|
-
traceId:
|
|
4847
|
-
groupId:
|
|
4848
|
-
versionId:
|
|
4849
|
-
status:
|
|
4850
|
-
startDate:
|
|
4851
|
-
endDate:
|
|
4936
|
+
inputSchema: z11.object({
|
|
4937
|
+
page: z11.number().int().min(0).default(0).describe("Page number"),
|
|
4938
|
+
size: z11.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
4939
|
+
traceId: z11.string().optional().describe("Filter by trace ID"),
|
|
4940
|
+
groupId: z11.string().uuid().optional().describe("Filter by policy group"),
|
|
4941
|
+
versionId: z11.string().uuid().optional().describe("Filter by version"),
|
|
4942
|
+
status: z11.enum(["SUCCESS", "NO_MATCH", "ERROR", "TIMEOUT"]).optional().describe("Filter by execution status"),
|
|
4943
|
+
startDate: z11.string().optional().describe(START_DAY),
|
|
4944
|
+
endDate: z11.string().optional().describe(END_DAY)
|
|
4852
4945
|
})
|
|
4853
4946
|
},
|
|
4854
4947
|
async ({ page, size, traceId, groupId, versionId, status, startDate, endDate }) => {
|
|
@@ -4868,8 +4961,8 @@ function registerHistoryTools(server, callApi) {
|
|
|
4868
4961
|
title: "Get Execution Detail",
|
|
4869
4962
|
annotations: { readOnlyHint: true },
|
|
4870
4963
|
description: "Get full execution detail including inputFacts, mutatedFacts, generatedVariables, executionTraces, and decisionTraces.",
|
|
4871
|
-
inputSchema:
|
|
4872
|
-
traceId:
|
|
4964
|
+
inputSchema: z11.object({
|
|
4965
|
+
traceId: z11.string().describe("Trace ID from execution history")
|
|
4873
4966
|
})
|
|
4874
4967
|
},
|
|
4875
4968
|
async ({ traceId }) => callApi("GET", `execution/history/${traceId}`)
|
|
@@ -4880,10 +4973,10 @@ function registerHistoryTools(server, callApi) {
|
|
|
4880
4973
|
title: "Execution Statistics",
|
|
4881
4974
|
annotations: { readOnlyHint: true },
|
|
4882
4975
|
description: `Get execution KPIs: total executions, success/failure counts, success rate, and average latency. Without dates the window is the last ${STATS_DEFAULT_WINDOW_DAYS} days through today, in the organization time zone. At most ${STATS_MAX_WINDOW_DAYS} days per call; a longer range is rejected with AN-039. startDate alone runs through today.`,
|
|
4883
|
-
inputSchema:
|
|
4884
|
-
groupId:
|
|
4885
|
-
startDate:
|
|
4886
|
-
endDate:
|
|
4976
|
+
inputSchema: z11.object({
|
|
4977
|
+
groupId: z11.string().uuid().optional().describe("Filter by policy group"),
|
|
4978
|
+
startDate: z11.string().optional().describe(START_DAY),
|
|
4979
|
+
endDate: z11.string().optional().describe(END_DAY)
|
|
4887
4980
|
})
|
|
4888
4981
|
},
|
|
4889
4982
|
async ({ groupId, startDate, endDate }) => {
|
|
@@ -4897,7 +4990,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
4897
4990
|
}
|
|
4898
4991
|
|
|
4899
4992
|
// src/mcp/tools/provenance.ts
|
|
4900
|
-
import { z as
|
|
4993
|
+
import { z as z12 } from "zod";
|
|
4901
4994
|
function registerProvenanceTools(server, callApi) {
|
|
4902
4995
|
server.registerTool(
|
|
4903
4996
|
"lexq_provenance_get",
|
|
@@ -4905,8 +4998,8 @@ function registerProvenanceTools(server, callApi) {
|
|
|
4905
4998
|
title: "Get Decision Provenance",
|
|
4906
4999
|
annotations: { readOnlyHint: true },
|
|
4907
5000
|
description: "Get the lineage of a single decision: what was decided, deterministic why per rule, input facts (PII facts are masked as \u2022\u2022\u2022\u2022\u2022\u2022 with maskedKeys listing them \u2014 values are revealable only in the console, audited), the authored/published/deployed responsibility chain, and the rule snapshot fingerprint.",
|
|
4908
|
-
inputSchema:
|
|
4909
|
-
traceId:
|
|
5001
|
+
inputSchema: z12.object({
|
|
5002
|
+
traceId: z12.string().describe("Trace ID of the execution")
|
|
4910
5003
|
})
|
|
4911
5004
|
},
|
|
4912
5005
|
async ({ traceId }) => callApi("GET", `provenance/${traceId}`)
|
|
@@ -4917,14 +5010,14 @@ function registerProvenanceTools(server, callApi) {
|
|
|
4917
5010
|
title: "List PII Reveal Audits",
|
|
4918
5011
|
annotations: { readOnlyHint: true },
|
|
4919
5012
|
description: "List the PII reveal audit ledger \u2014 who revealed which fact of which trace, and when. Metadata only; revealed values are never stored or returned. Use for monthly access-log inspection and SIEM collection.",
|
|
4920
|
-
inputSchema:
|
|
4921
|
-
page:
|
|
4922
|
-
size:
|
|
4923
|
-
traceId:
|
|
4924
|
-
revealedBy:
|
|
4925
|
-
factKey:
|
|
4926
|
-
startDate:
|
|
4927
|
-
endDate:
|
|
5013
|
+
inputSchema: z12.object({
|
|
5014
|
+
page: z12.number().int().min(0).default(0).describe("Page number"),
|
|
5015
|
+
size: z12.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
5016
|
+
traceId: z12.string().optional().describe("Filter by trace ID (exact match)"),
|
|
5017
|
+
revealedBy: z12.string().optional().describe("Filter by operator ID (exact match)"),
|
|
5018
|
+
factKey: z12.string().optional().describe("Filter by fact key (partial match, case-insensitive)"),
|
|
5019
|
+
startDate: z12.string().optional().describe(START_DAY),
|
|
5020
|
+
endDate: z12.string().optional().describe(END_DAY)
|
|
4928
5021
|
})
|
|
4929
5022
|
},
|
|
4930
5023
|
async ({ page, size, traceId, revealedBy, factKey, startDate, endDate }) => {
|
|
@@ -4940,7 +5033,7 @@ function registerProvenanceTools(server, callApi) {
|
|
|
4940
5033
|
}
|
|
4941
5034
|
|
|
4942
5035
|
// src/mcp/tools/logs.ts
|
|
4943
|
-
import { z as
|
|
5036
|
+
import { z as z13 } from "zod";
|
|
4944
5037
|
function registerLogTools(server, callApi) {
|
|
4945
5038
|
server.registerTool(
|
|
4946
5039
|
"lexq_logs_list",
|
|
@@ -4948,14 +5041,14 @@ function registerLogTools(server, callApi) {
|
|
|
4948
5041
|
title: "List Failure Logs",
|
|
4949
5042
|
annotations: { readOnlyHint: true },
|
|
4950
5043
|
description: `List system failure logs from background tasks (platform event webhooks, scheduled deployments). Without dates the window is ${FAILURE_LOG_LOOKBACK_DAYS} days back through today, in the organization time zone; pass startDate to look further back.`,
|
|
4951
|
-
inputSchema:
|
|
4952
|
-
page:
|
|
4953
|
-
size:
|
|
4954
|
-
taskType:
|
|
4955
|
-
status:
|
|
4956
|
-
keyword:
|
|
4957
|
-
startDate:
|
|
4958
|
-
endDate:
|
|
5044
|
+
inputSchema: z13.object({
|
|
5045
|
+
page: z13.number().int().min(0).default(0).describe("Page number"),
|
|
5046
|
+
size: z13.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
5047
|
+
taskType: z13.enum(TaskType).optional().describe("Task type"),
|
|
5048
|
+
status: z13.enum(FailureStatus).optional().describe("Log status"),
|
|
5049
|
+
keyword: z13.string().optional().describe("Search in refId, refSubId, errorMessage"),
|
|
5050
|
+
startDate: z13.string().optional().describe(START_DAY),
|
|
5051
|
+
endDate: z13.string().optional().describe(END_DAY)
|
|
4959
5052
|
})
|
|
4960
5053
|
},
|
|
4961
5054
|
async ({ page, size, taskType, status, keyword, startDate, endDate }) => {
|
|
@@ -4974,8 +5067,8 @@ function registerLogTools(server, callApi) {
|
|
|
4974
5067
|
title: "Get Failure Log",
|
|
4975
5068
|
annotations: { readOnlyHint: true },
|
|
4976
5069
|
description: "Get failure log detail by ID.",
|
|
4977
|
-
inputSchema:
|
|
4978
|
-
logId:
|
|
5070
|
+
inputSchema: z13.object({
|
|
5071
|
+
logId: z13.string().uuid().describe("Failure log ID")
|
|
4979
5072
|
})
|
|
4980
5073
|
},
|
|
4981
5074
|
async ({ logId }) => callApi("GET", `failure-logs/${logId}`)
|
|
@@ -4986,9 +5079,9 @@ function registerLogTools(server, callApi) {
|
|
|
4986
5079
|
title: "Process Failure Log",
|
|
4987
5080
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4988
5081
|
description: "Process a single failure log: RESOLVE (mark as manually fixed) or IGNORE (skip intentionally).",
|
|
4989
|
-
inputSchema:
|
|
4990
|
-
logId:
|
|
4991
|
-
action:
|
|
5082
|
+
inputSchema: z13.object({
|
|
5083
|
+
logId: z13.string().uuid().describe("Failure log ID"),
|
|
5084
|
+
action: z13.enum(FailureAction).describe("Action to take")
|
|
4992
5085
|
})
|
|
4993
5086
|
},
|
|
4994
5087
|
async ({ logId, action }) => callApi("POST", `failure-logs/${logId}/actions`, {
|
|
@@ -5001,9 +5094,9 @@ function registerLogTools(server, callApi) {
|
|
|
5001
5094
|
title: "Bulk Process Failure Logs",
|
|
5002
5095
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
5003
5096
|
description: "Process multiple failure logs at once. Provide an array of log IDs and the action.",
|
|
5004
|
-
inputSchema:
|
|
5005
|
-
logIds:
|
|
5006
|
-
action:
|
|
5097
|
+
inputSchema: z13.object({
|
|
5098
|
+
logIds: z13.array(z13.string().uuid()).describe("Array of failure log IDs"),
|
|
5099
|
+
action: z13.enum(FailureAction).describe("Action to apply to all logs")
|
|
5007
5100
|
})
|
|
5008
5101
|
},
|
|
5009
5102
|
async ({ logIds, action }) => callApi("POST", "failure-logs/bulk-actions", {
|
|
@@ -5013,7 +5106,7 @@ function registerLogTools(server, callApi) {
|
|
|
5013
5106
|
}
|
|
5014
5107
|
|
|
5015
5108
|
// src/mcp/tools/webhook-subscriptions.ts
|
|
5016
|
-
import { z as
|
|
5109
|
+
import { z as z14 } from "zod";
|
|
5017
5110
|
function registerWebhookSubscriptionTools(server, callApi) {
|
|
5018
5111
|
server.registerTool(
|
|
5019
5112
|
"lexq_webhook_subscriptions_list",
|
|
@@ -5021,9 +5114,9 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5021
5114
|
title: "List Webhook Subscriptions",
|
|
5022
5115
|
annotations: { readOnlyHint: true },
|
|
5023
5116
|
description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications: publish, deploy, rollback, and undeploy, plus a deployment being scheduled or a schedule being canceled.",
|
|
5024
|
-
inputSchema:
|
|
5025
|
-
page:
|
|
5026
|
-
size:
|
|
5117
|
+
inputSchema: z14.object({
|
|
5118
|
+
page: z14.number().int().min(0).default(0).describe("Page number"),
|
|
5119
|
+
size: z14.number().int().min(1).max(100).default(20).describe("Page size")
|
|
5027
5120
|
})
|
|
5028
5121
|
},
|
|
5029
5122
|
async ({ page, size }) => {
|
|
@@ -5037,8 +5130,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5037
5130
|
title: "Get Webhook Subscription",
|
|
5038
5131
|
annotations: { readOnlyHint: true },
|
|
5039
5132
|
description: "Get webhook subscription detail by ID.",
|
|
5040
|
-
inputSchema:
|
|
5041
|
-
id:
|
|
5133
|
+
inputSchema: z14.object({
|
|
5134
|
+
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
5042
5135
|
})
|
|
5043
5136
|
},
|
|
5044
5137
|
async ({ id }) => callApi("GET", `webhook-subscriptions/${id}`)
|
|
@@ -5049,14 +5142,14 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5049
5142
|
title: "Save Webhook Subscription",
|
|
5050
5143
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
5051
5144
|
description: `Create or update a webhook subscription. Omit id to create, provide id to update. Events: ${PlatformEventType.join(", ")}. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).`,
|
|
5052
|
-
inputSchema:
|
|
5053
|
-
id:
|
|
5054
|
-
name:
|
|
5055
|
-
webhookUrl:
|
|
5056
|
-
subscribedEvents:
|
|
5057
|
-
payloadFormat:
|
|
5058
|
-
secret:
|
|
5059
|
-
isActive:
|
|
5145
|
+
inputSchema: z14.object({
|
|
5146
|
+
id: z14.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
|
|
5147
|
+
name: z14.string().min(1).describe("Subscription name (unique per tenant)"),
|
|
5148
|
+
webhookUrl: z14.string().url().describe("Webhook endpoint URL"),
|
|
5149
|
+
subscribedEvents: z14.array(z14.enum(PlatformEventType)).min(1).describe("Events to subscribe to"),
|
|
5150
|
+
payloadFormat: z14.enum(WebhookPayloadFormat).optional().default("GENERIC").describe("Payload format"),
|
|
5151
|
+
secret: z14.string().optional().describe("HMAC-SHA256 signing secret"),
|
|
5152
|
+
isActive: z14.boolean().optional().default(true).describe("Whether the subscription is active")
|
|
5060
5153
|
})
|
|
5061
5154
|
},
|
|
5062
5155
|
async ({ ...body }) => callApi("POST", "webhook-subscriptions", { body })
|
|
@@ -5067,8 +5160,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5067
5160
|
title: "Delete Webhook Subscription",
|
|
5068
5161
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
5069
5162
|
description: "Delete a webhook subscription by ID.",
|
|
5070
|
-
inputSchema:
|
|
5071
|
-
id:
|
|
5163
|
+
inputSchema: z14.object({
|
|
5164
|
+
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
5072
5165
|
})
|
|
5073
5166
|
},
|
|
5074
5167
|
async ({ id }) => callApi("DELETE", `webhook-subscriptions/${id}`)
|
|
@@ -5079,8 +5172,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5079
5172
|
title: "Test Webhook Subscription",
|
|
5080
5173
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
5081
5174
|
description: "Send a test event to verify webhook connectivity. Returns the HTTP status code and success/failure message.",
|
|
5082
|
-
inputSchema:
|
|
5083
|
-
id:
|
|
5175
|
+
inputSchema: z14.object({
|
|
5176
|
+
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
5084
5177
|
})
|
|
5085
5178
|
},
|
|
5086
5179
|
async ({ id }) => callApi("POST", `webhook-subscriptions/${id}/test`)
|
|
@@ -5088,7 +5181,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5088
5181
|
}
|
|
5089
5182
|
|
|
5090
5183
|
// src/mcp/tools/domain-templates.ts
|
|
5091
|
-
import { z as
|
|
5184
|
+
import { z as z15 } from "zod";
|
|
5092
5185
|
function registerDomainTemplateTools(server, callApi) {
|
|
5093
5186
|
server.registerTool(
|
|
5094
5187
|
"lexq_domain_templates_list",
|
|
@@ -5096,7 +5189,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5096
5189
|
title: "List Domain Templates",
|
|
5097
5190
|
annotations: { readOnlyHint: true },
|
|
5098
5191
|
description: "List all domain templates. A domain template is a starter pack of fact definitions and sample rules for a common decision, such as checkout pricing (ECOMMERCE) or transfer approval (FINTECH). Each entry reports its key, whether it can be applied (isAvailable), and how many facts and sample rules it provisions. Call this before preview or apply to see which templates exist.",
|
|
5099
|
-
inputSchema:
|
|
5192
|
+
inputSchema: z15.object({})
|
|
5100
5193
|
},
|
|
5101
5194
|
async () => callApi("GET", "domain-templates")
|
|
5102
5195
|
);
|
|
@@ -5106,8 +5199,8 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5106
5199
|
title: "Preview Domain Template",
|
|
5107
5200
|
annotations: { readOnlyHint: true },
|
|
5108
5201
|
description: "Preview exactly what a domain template will provision before applying it: the fact definitions it registers (an input with a value domain carries its allowed values or range), the sample rules it creates with a description of each, and an apply plan. This is a read-only dry run; nothing is created. factConflicts lists keys this tenant already has with a different type, or with different allowed values or a different range on an input fact. While it is not empty, apply fails with P-061 and creates nothing: delete those facts or change them to match the template, then apply. plan is null for a template that cannot be applied yet.",
|
|
5109
|
-
inputSchema:
|
|
5110
|
-
template:
|
|
5202
|
+
inputSchema: z15.object({
|
|
5203
|
+
template: z15.string().describe(
|
|
5111
5204
|
"Domain template key (e.g. ECOMMERCE). Use lexq_domain_templates_list to see the keys."
|
|
5112
5205
|
)
|
|
5113
5206
|
})
|
|
@@ -5120,9 +5213,9 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5120
5213
|
title: "Apply Domain Template",
|
|
5121
5214
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
5122
5215
|
description: "Apply a domain template to the current tenant. Creates the template's fact definitions and a new policy group pre-populated with its sample rules as a DRAFT version. Apply never overwrites existing schema: a key that already exists with the same definition is kept as it is. Run lexq_domain_templates_preview first. If its factConflicts is not empty, apply fails with P-061 and creates nothing until those facts are deleted or changed to match the template. A template whose isAvailable is false cannot be applied.",
|
|
5123
|
-
inputSchema:
|
|
5124
|
-
template:
|
|
5125
|
-
customName:
|
|
5216
|
+
inputSchema: z15.object({
|
|
5217
|
+
template: z15.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
|
|
5218
|
+
customName: z15.string().optional().describe(
|
|
5126
5219
|
"Optional custom name for the policy group that gets created. If omitted, the template's default name is used."
|
|
5127
5220
|
)
|
|
5128
5221
|
})
|
|
@@ -5158,7 +5251,7 @@ var SERVER_INFO = { name: "lexq", ...server_info_default };
|
|
|
5158
5251
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5159
5252
|
function getVersion() {
|
|
5160
5253
|
try {
|
|
5161
|
-
const pkg = JSON.parse(readFileSync3(
|
|
5254
|
+
const pkg = JSON.parse(readFileSync3(join3(__dirname, "..", "package.json"), "utf-8"));
|
|
5162
5255
|
return pkg.version;
|
|
5163
5256
|
} catch {
|
|
5164
5257
|
return "0.1.0";
|
|
@@ -5171,6 +5264,7 @@ function startMcpServer() {
|
|
|
5171
5264
|
() => {
|
|
5172
5265
|
const server = new McpServer({ ...SERVER_INFO, version });
|
|
5173
5266
|
registerAllTools(server, callApi);
|
|
5267
|
+
registerLocalTools(server);
|
|
5174
5268
|
return server;
|
|
5175
5269
|
},
|
|
5176
5270
|
{
|
|
@@ -5188,7 +5282,7 @@ function startMcpServer() {
|
|
|
5188
5282
|
function registerServeCommand(program) {
|
|
5189
5283
|
program.command("serve").description("Start LexQ as a server for AI agent integrations").option("--mcp", "Start as MCP (Model Context Protocol) server over stdio").addHelpText(
|
|
5190
5284
|
"after",
|
|
5191
|
-
|
|
5285
|
+
dedent19`
|
|
5192
5286
|
|
|
5193
5287
|
Example:
|
|
5194
5288
|
$ lexq serve --mcp
|
|
@@ -5219,11 +5313,11 @@ function registerServeCommand(program) {
|
|
|
5219
5313
|
|
|
5220
5314
|
// src/commands/domain-templates.ts
|
|
5221
5315
|
import "commander";
|
|
5222
|
-
import
|
|
5316
|
+
import dedent20 from "dedent";
|
|
5223
5317
|
function registerDomainTemplateCommands(program) {
|
|
5224
5318
|
const templates = program.command("domain-templates").description("Browse and apply domain templates").addHelpText(
|
|
5225
5319
|
"after",
|
|
5226
|
-
|
|
5320
|
+
dedent20`
|
|
5227
5321
|
|
|
5228
5322
|
A domain template is an industry-specific starter pack of fact
|
|
5229
5323
|
definitions and sample rules. Applying one provisions a ready-to-use
|
|
@@ -5271,7 +5365,7 @@ function registerDomainTemplateCommands(program) {
|
|
|
5271
5365
|
});
|
|
5272
5366
|
templates.command("preview").description("Preview what a domain template provisions").requiredOption("--template <key>", "Domain template key (e.g. ECOMMERCE)").addHelpText(
|
|
5273
5367
|
"after",
|
|
5274
|
-
|
|
5368
|
+
dedent20`
|
|
5275
5369
|
|
|
5276
5370
|
Read-only dry run — shows the fact definitions and sample rules the
|
|
5277
5371
|
template will create. Nothing is provisioned.
|
|
@@ -5300,7 +5394,7 @@ function registerDomainTemplateCommands(program) {
|
|
|
5300
5394
|
});
|
|
5301
5395
|
templates.command("apply").description("Apply a domain template to the current tenant").requiredOption("--template <key>", "Domain template key (e.g. ECOMMERCE)").option("--name <n>", "Custom name for the policy group that gets created").option("--force", "Skip confirmation prompt").addHelpText(
|
|
5302
5396
|
"after",
|
|
5303
|
-
|
|
5397
|
+
dedent20`
|
|
5304
5398
|
|
|
5305
5399
|
Creates the template's fact definitions and a new DRAFT policy group
|
|
5306
5400
|
populated with its sample rules. A fact that already exists with the
|
|
@@ -5354,7 +5448,7 @@ function registerDomainTemplateCommands(program) {
|
|
|
5354
5448
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
5355
5449
|
function getVersion2() {
|
|
5356
5450
|
try {
|
|
5357
|
-
const pkg = JSON.parse(readFileSync4(
|
|
5451
|
+
const pkg = JSON.parse(readFileSync4(join4(__dirname2, "..", "package.json"), "utf-8"));
|
|
5358
5452
|
return pkg.version;
|
|
5359
5453
|
} catch {
|
|
5360
5454
|
return "0.0.0";
|