@lexq/cli 1.1.7 → 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 +859 -736
- package/dist/mcp/register.d.ts +6 -6
- package/dist/mcp/register.js +30 -17
- package/package.json +12 -6
- package/skills/lexq-recipes/SKILL.md +11 -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;
|
|
@@ -1773,7 +1798,13 @@ function registerDeployCommands(program) {
|
|
|
1773
1798
|
process.exit(1);
|
|
1774
1799
|
}
|
|
1775
1800
|
});
|
|
1776
|
-
deploy.command("rollback").description("Rollback to the previous deployed version").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--memo <memo>", "Rollback reason").option(
|
|
1801
|
+
deploy.command("rollback").description("Rollback to the previous deployed version").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--memo <memo>", "Rollback reason").option(
|
|
1802
|
+
"--expected-current-version-id <versionId>",
|
|
1803
|
+
"currentVersionId from your last read of the group"
|
|
1804
|
+
).option(
|
|
1805
|
+
"--expected-current-deployment-id <deploymentId>",
|
|
1806
|
+
"currentDeploymentId from your last read of the group"
|
|
1807
|
+
).option("--force", "Skip confirmation prompt").addHelpText(
|
|
1777
1808
|
"after",
|
|
1778
1809
|
dedent7`
|
|
1779
1810
|
|
|
@@ -1781,8 +1812,20 @@ function registerDeployCommands(program) {
|
|
|
1781
1812
|
Only available if the previous version is still ACTIVE and its effective end
|
|
1782
1813
|
has not passed (P-058).
|
|
1783
1814
|
|
|
1815
|
+
Run lexq groups get first: currentVersionName is live now and
|
|
1816
|
+
previousVersionName is what a rollback restores. Pass the group's
|
|
1817
|
+
currentVersionId and currentDeploymentId with the two --expected-* options.
|
|
1818
|
+
The server checks only the values you pass. If anyone deployed, rolled back
|
|
1819
|
+
or undeployed in between, the rollback is rejected with 409 and nothing
|
|
1820
|
+
changes: P-062 if another version or no version is live, P-063 if the same
|
|
1821
|
+
version is live but from a newer deployment. Read the group again before
|
|
1822
|
+
retrying.
|
|
1823
|
+
|
|
1784
1824
|
Example:
|
|
1785
|
-
$ lexq
|
|
1825
|
+
$ lexq groups get --id <gid>
|
|
1826
|
+
$ lexq deploy rollback --group-id <gid> --memo "High error rate" \
|
|
1827
|
+
--expected-current-version-id <currentVersionId> \
|
|
1828
|
+
--expected-current-deployment-id <currentDeploymentId> --force
|
|
1786
1829
|
`
|
|
1787
1830
|
).action(async (opts) => {
|
|
1788
1831
|
try {
|
|
@@ -1802,7 +1845,11 @@ function registerDeployCommands(program) {
|
|
|
1802
1845
|
baseUrl: globalOpts.baseUrl,
|
|
1803
1846
|
dryRun: globalOpts.dryRun,
|
|
1804
1847
|
verbose: globalOpts.verbose,
|
|
1805
|
-
body: {
|
|
1848
|
+
body: {
|
|
1849
|
+
memo: opts.memo,
|
|
1850
|
+
expectedCurrentVersionId: opts.expectedCurrentVersionId,
|
|
1851
|
+
expectedCurrentDeploymentId: opts.expectedCurrentDeploymentId
|
|
1852
|
+
}
|
|
1806
1853
|
});
|
|
1807
1854
|
console.log(`\u2713 Group ${opts.groupId} rolled back.`);
|
|
1808
1855
|
} catch (error) {
|
|
@@ -2135,8 +2182,30 @@ async function warnUnregisteredFacts(globalOpts, groupId, versionId) {
|
|
|
2135
2182
|
|
|
2136
2183
|
// src/commands/analytics.ts
|
|
2137
2184
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2185
|
+
import { basename } from "path";
|
|
2138
2186
|
import "commander";
|
|
2139
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
|
|
2140
2209
|
function registerAnalyticsCommands(program) {
|
|
2141
2210
|
const analytics = program.command("analytics").description("Dry run, simulation, and requirements").addHelpText(
|
|
2142
2211
|
"after",
|
|
@@ -2159,7 +2228,7 @@ function registerAnalyticsCommands(program) {
|
|
|
2159
2228
|
dedent8`
|
|
2160
2229
|
|
|
2161
2230
|
Examples:
|
|
2162
|
-
$ lexq analytics dry-run --version-id <vid> --debug
|
|
2231
|
+
$ lexq analytics dry-run --version-id <vid> --debug \
|
|
2163
2232
|
--json '{"facts": {"paymentAmount": 150000, "customerTier": "VIP"}}'
|
|
2164
2233
|
|
|
2165
2234
|
$ lexq analytics dry-run --version-id <vid> --file test-input.json
|
|
@@ -2555,41 +2624,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2555
2624
|
).action(async (opts) => {
|
|
2556
2625
|
try {
|
|
2557
2626
|
const globalOpts = program.opts();
|
|
2558
|
-
const config = loadConfig();
|
|
2559
|
-
const baseUrl = globalOpts.baseUrl ?? config.baseUrl;
|
|
2560
|
-
const apiKey = globalOpts.apiKey ?? config.apiKey;
|
|
2561
|
-
if (!apiKey) {
|
|
2562
|
-
throw new Error('Not authenticated. Run "lexq auth login" first.');
|
|
2563
|
-
}
|
|
2564
2627
|
const filePath = opts.file;
|
|
2565
|
-
const
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
else if (ext === "json") contentType = "application/json";
|
|
2571
|
-
const blob = new Blob([fileBuffer], { type: contentType });
|
|
2572
|
-
const formData = new FormData();
|
|
2573
|
-
formData.append("file", blob, fileName);
|
|
2574
|
-
const url = new URL(
|
|
2575
|
-
"analytics/datasets/upload",
|
|
2576
|
-
baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"
|
|
2577
|
-
);
|
|
2578
|
-
if (globalOpts.verbose) {
|
|
2579
|
-
console.error(`\u2192 POST ${url.toString()}`);
|
|
2580
|
-
console.error(` File: ${filePath} (${fileBuffer.length} bytes)`);
|
|
2581
|
-
}
|
|
2582
|
-
const response = await fetch(url.toString(), {
|
|
2583
|
-
method: "POST",
|
|
2584
|
-
headers: { "X-API-KEY": apiKey },
|
|
2585
|
-
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
|
|
2586
2633
|
});
|
|
2587
|
-
if (!response.ok) {
|
|
2588
|
-
const errorText = await response.text();
|
|
2589
|
-
throw new Error(`Upload failed (${response.status}): ${errorText}`);
|
|
2590
|
-
}
|
|
2591
|
-
const envelope = await response.json();
|
|
2592
|
-
const result = envelope.data;
|
|
2593
2634
|
console.log(`\u2713 Dataset uploaded: ${result.path}`);
|
|
2594
2635
|
console.log(` File: ${result.filename} (${result.size} bytes)`);
|
|
2595
2636
|
printJson(result);
|
|
@@ -3526,56 +3567,36 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
3526
3567
|
|
|
3527
3568
|
// src/commands/serve.ts
|
|
3528
3569
|
import "commander";
|
|
3529
|
-
import
|
|
3570
|
+
import dedent19 from "dedent";
|
|
3530
3571
|
|
|
3531
3572
|
// src/mcp/server.ts
|
|
3532
3573
|
import { readFileSync as readFileSync3 } from "fs";
|
|
3533
|
-
import { dirname, join as
|
|
3574
|
+
import { dirname, join as join3 } from "path";
|
|
3534
3575
|
import { fileURLToPath } from "url";
|
|
3535
3576
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
3536
3577
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
3537
3578
|
|
|
3538
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
|
+
}
|
|
3539
3587
|
function createCallApiFromConfig() {
|
|
3540
3588
|
return async (method, path, opts) => {
|
|
3541
3589
|
try {
|
|
3542
|
-
|
|
3543
|
-
const config = {
|
|
3544
|
-
...stored,
|
|
3545
|
-
baseUrl: process.env.PARTNER_BASE_URL ?? stored.baseUrl,
|
|
3546
|
-
apiKey: process.env.LEXQ_API_KEY ?? stored.apiKey
|
|
3547
|
-
};
|
|
3590
|
+
let form;
|
|
3548
3591
|
if (opts?.upload) {
|
|
3549
|
-
|
|
3550
|
-
path,
|
|
3551
|
-
config.baseUrl.endsWith("/") ? config.baseUrl : config.baseUrl + "/"
|
|
3552
|
-
);
|
|
3553
|
-
if (opts.params) {
|
|
3554
|
-
for (const [key, value] of Object.entries(opts.params)) {
|
|
3555
|
-
if (value !== void 0) url.searchParams.set(key, value);
|
|
3556
|
-
}
|
|
3557
|
-
}
|
|
3558
|
-
if (!config.apiKey) {
|
|
3559
|
-
throw new Error('Not authenticated. Run "lexq auth login" first.');
|
|
3560
|
-
}
|
|
3592
|
+
form = new FormData();
|
|
3561
3593
|
const blob = new Blob([opts.upload.content], { type: "text/plain" });
|
|
3562
|
-
|
|
3563
|
-
formData.append(opts.upload.fieldName, blob, opts.upload.filename);
|
|
3564
|
-
const response = await fetch(url.toString(), {
|
|
3565
|
-
method,
|
|
3566
|
-
headers: { "X-API-KEY": config.apiKey },
|
|
3567
|
-
body: formData
|
|
3568
|
-
});
|
|
3569
|
-
const data2 = parseJson(await response.text());
|
|
3570
|
-
return { content: [{ type: "text", text: stringifyJson(data2, 2) ?? "" }] };
|
|
3594
|
+
form.append(opts.upload.fieldName, blob, opts.upload.filename);
|
|
3571
3595
|
}
|
|
3572
|
-
const clientOpts = {
|
|
3573
|
-
apiKey: config.apiKey,
|
|
3574
|
-
baseUrl: config.baseUrl
|
|
3575
|
-
};
|
|
3576
3596
|
const { data, meta } = await apiRequestWithMeta(method, path, {
|
|
3577
|
-
...
|
|
3597
|
+
...localApiOptions(),
|
|
3578
3598
|
body: opts?.body,
|
|
3599
|
+
form,
|
|
3579
3600
|
params: opts?.params
|
|
3580
3601
|
});
|
|
3581
3602
|
const content = [
|
|
@@ -3618,181 +3639,514 @@ function formatUnregisteredFactWarning(meta) {
|
|
|
3618
3639
|
].join("\n");
|
|
3619
3640
|
}
|
|
3620
3641
|
|
|
3621
|
-
// src/mcp/
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
{
|
|
3628
|
-
src: "https://lexq.io/logo.png",
|
|
3629
|
-
mimeType: "image/png",
|
|
3630
|
-
sizes: ["1024x1024"]
|
|
3631
|
-
}
|
|
3632
|
-
]
|
|
3633
|
-
};
|
|
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";
|
|
3634
3648
|
|
|
3635
|
-
// src/mcp/tools/
|
|
3649
|
+
// src/mcp/tools/analytics.ts
|
|
3636
3650
|
import { z } from "zod";
|
|
3637
|
-
|
|
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) {
|
|
3638
3660
|
server.registerTool(
|
|
3639
|
-
"
|
|
3661
|
+
"lexq_dry_run",
|
|
3640
3662
|
{
|
|
3641
|
-
title: "
|
|
3663
|
+
title: "Dry Run",
|
|
3642
3664
|
annotations: { readOnlyHint: true },
|
|
3643
|
-
description:
|
|
3644
|
-
|
|
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
|
+
})
|
|
3645
3682
|
},
|
|
3646
|
-
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
|
+
}
|
|
3647
3689
|
);
|
|
3648
|
-
}
|
|
3649
|
-
|
|
3650
|
-
// src/mcp/tools/groups.ts
|
|
3651
|
-
import { z as z2 } from "zod";
|
|
3652
|
-
function registerGroupTools(server, callApi) {
|
|
3653
3690
|
server.registerTool(
|
|
3654
|
-
"
|
|
3691
|
+
"lexq_dry_run_compare",
|
|
3655
3692
|
{
|
|
3656
|
-
title: "
|
|
3693
|
+
title: "Dry Run Compare",
|
|
3657
3694
|
annotations: { readOnlyHint: true },
|
|
3658
|
-
description:
|
|
3659
|
-
|
|
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
|
+
})
|
|
3660
3707
|
},
|
|
3661
|
-
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
|
+
}
|
|
3662
3714
|
);
|
|
3663
3715
|
server.registerTool(
|
|
3664
|
-
"
|
|
3716
|
+
"lexq_requirements",
|
|
3665
3717
|
{
|
|
3666
|
-
title: "
|
|
3718
|
+
title: "Analyze Requirements",
|
|
3667
3719
|
annotations: { readOnlyHint: true },
|
|
3668
|
-
description: "
|
|
3669
|
-
inputSchema:
|
|
3670
|
-
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")
|
|
3671
3724
|
})
|
|
3672
3725
|
},
|
|
3673
|
-
async ({ groupId }) => callApi("GET", `
|
|
3726
|
+
async ({ groupId, versionId }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/requirements`)
|
|
3674
3727
|
);
|
|
3675
3728
|
server.registerTool(
|
|
3676
|
-
"
|
|
3729
|
+
"lexq_simulation_start",
|
|
3677
3730
|
{
|
|
3678
|
-
title: "
|
|
3731
|
+
title: "Start Simulation",
|
|
3679
3732
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3680
|
-
description: `
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
)
|
|
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")
|
|
3692
3765
|
})
|
|
3693
3766
|
},
|
|
3694
|
-
async (
|
|
3695
|
-
const
|
|
3696
|
-
|
|
3697
|
-
};
|
|
3698
|
-
if (args.description !== void 0) body.description = args.description;
|
|
3699
|
-
if (args.activationMode !== void 0) body.activationMode = args.activationMode;
|
|
3700
|
-
if (args.activationStrategy !== void 0) body.activationStrategy = args.activationStrategy;
|
|
3701
|
-
if (args.executionLimit !== void 0) body.executionLimit = args.executionLimit;
|
|
3702
|
-
if (args.activationGroup !== void 0) body.activationGroup = args.activationGroup;
|
|
3703
|
-
return callApi("POST", "policy-groups", { body });
|
|
3767
|
+
async ({ body }) => {
|
|
3768
|
+
const parsed = parseJson(body);
|
|
3769
|
+
return callApi("POST", "analytics/simulations", { body: parsed });
|
|
3704
3770
|
}
|
|
3705
3771
|
);
|
|
3706
3772
|
server.registerTool(
|
|
3707
|
-
"
|
|
3708
|
-
{
|
|
3709
|
-
title: "Update Policy Group",
|
|
3710
|
-
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3711
|
-
description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged. A deleted group cannot be updated (409 P-059).",
|
|
3712
|
-
inputSchema: z2.object({
|
|
3713
|
-
groupId: z2.string().uuid().describe("Policy group ID"),
|
|
3714
|
-
name: z2.string().optional().describe("New name"),
|
|
3715
|
-
description: z2.string().optional().describe("New description"),
|
|
3716
|
-
status: z2.enum(["ACTIVE", "DISABLED"]).optional().describe(
|
|
3717
|
-
"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."
|
|
3718
|
-
),
|
|
3719
|
-
activationGroup: z2.string().optional().describe(
|
|
3720
|
-
"Activation group (Execution Group) cluster key. Policy groups sharing this key compete, and group priority picks the winners."
|
|
3721
|
-
),
|
|
3722
|
-
activationMode: z2.enum(ConflictResolutionMode).optional().describe("Conflict resolution mode"),
|
|
3723
|
-
activationStrategy: z2.enum(ConflictResolutionStrategy).optional().describe("Ranking used to pick the winning groups"),
|
|
3724
|
-
executionLimit: z2.number().int().min(1).optional().describe(
|
|
3725
|
-
"How many policy groups sharing this activationGroup may run. Counts groups, not rules."
|
|
3726
|
-
)
|
|
3727
|
-
})
|
|
3728
|
-
},
|
|
3729
|
-
async ({ groupId, ...body }) => callApi("PUT", `policy-groups/${groupId}`, { body })
|
|
3730
|
-
);
|
|
3731
|
-
server.registerTool(
|
|
3732
|
-
"lexq_groups_delete",
|
|
3773
|
+
"lexq_simulation_status",
|
|
3733
3774
|
{
|
|
3734
|
-
title: "
|
|
3735
|
-
annotations: { readOnlyHint:
|
|
3736
|
-
description: "
|
|
3737
|
-
inputSchema:
|
|
3738
|
-
|
|
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")
|
|
3739
3780
|
})
|
|
3740
3781
|
},
|
|
3741
|
-
async ({
|
|
3782
|
+
async ({ simulationId }) => callApi("GET", `analytics/simulations/${simulationId}`)
|
|
3742
3783
|
);
|
|
3743
3784
|
server.registerTool(
|
|
3744
|
-
"
|
|
3785
|
+
"lexq_simulation_list",
|
|
3745
3786
|
{
|
|
3746
|
-
title: "
|
|
3747
|
-
annotations: { readOnlyHint:
|
|
3748
|
-
description: "
|
|
3749
|
-
inputSchema:
|
|
3750
|
-
|
|
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)
|
|
3751
3796
|
})
|
|
3752
3797
|
},
|
|
3753
|
-
async ({
|
|
3754
|
-
const
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
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 });
|
|
3759
3804
|
}
|
|
3760
3805
|
);
|
|
3761
3806
|
server.registerTool(
|
|
3762
|
-
"
|
|
3807
|
+
"lexq_simulation_cancel",
|
|
3763
3808
|
{
|
|
3764
|
-
title: "
|
|
3765
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
3766
|
-
description: "
|
|
3767
|
-
inputSchema:
|
|
3768
|
-
|
|
3769
|
-
testVersionId: z2.string().uuid().describe("Challenger version ID. Must be a published version of this group"),
|
|
3770
|
-
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")
|
|
3771
3814
|
})
|
|
3772
3815
|
},
|
|
3773
|
-
async ({
|
|
3816
|
+
async ({ simulationId }) => callApi("DELETE", `analytics/simulations/${simulationId}`)
|
|
3774
3817
|
);
|
|
3775
3818
|
server.registerTool(
|
|
3776
|
-
"
|
|
3819
|
+
"lexq_simulation_export",
|
|
3777
3820
|
{
|
|
3778
|
-
title: "
|
|
3779
|
-
annotations: { readOnlyHint:
|
|
3780
|
-
description: "
|
|
3781
|
-
inputSchema:
|
|
3782
|
-
|
|
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")
|
|
3783
3827
|
})
|
|
3784
3828
|
},
|
|
3785
|
-
async ({
|
|
3829
|
+
async ({ simulationId, format }) => callApi("GET", `analytics/simulations/${simulationId}/export`, {
|
|
3830
|
+
params: { format }
|
|
3831
|
+
})
|
|
3786
3832
|
);
|
|
3787
3833
|
server.registerTool(
|
|
3788
|
-
"
|
|
3834
|
+
"lexq_dataset_upload",
|
|
3835
|
+
{
|
|
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",
|
|
3789
4143
|
{
|
|
3790
4144
|
title: "Adjust A/B Test",
|
|
3791
4145
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3792
4146
|
description: "Adjust traffic rate of a running A/B test.",
|
|
3793
|
-
inputSchema:
|
|
3794
|
-
groupId:
|
|
3795
|
-
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)")
|
|
3796
4150
|
})
|
|
3797
4151
|
},
|
|
3798
4152
|
async ({ groupId, ...body }) => callApi("PATCH", `policy-groups/${groupId}/ab-test/traffic-rate`, { body })
|
|
@@ -3800,7 +4154,7 @@ function registerGroupTools(server, callApi) {
|
|
|
3800
4154
|
}
|
|
3801
4155
|
|
|
3802
4156
|
// src/mcp/tools/versions.ts
|
|
3803
|
-
import { z as
|
|
4157
|
+
import { z as z5 } from "zod";
|
|
3804
4158
|
function registerVersionTools(server, callApi) {
|
|
3805
4159
|
server.registerTool(
|
|
3806
4160
|
"lexq_versions_list",
|
|
@@ -3808,10 +4162,10 @@ function registerVersionTools(server, callApi) {
|
|
|
3808
4162
|
title: "List Policy Versions",
|
|
3809
4163
|
annotations: { readOnlyHint: true },
|
|
3810
4164
|
description: "List all versions of a policy group.",
|
|
3811
|
-
inputSchema:
|
|
3812
|
-
groupId:
|
|
3813
|
-
page:
|
|
3814
|
-
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")
|
|
3815
4169
|
})
|
|
3816
4170
|
},
|
|
3817
4171
|
async ({ groupId, page, size }) => callApi("GET", `policy-groups/${groupId}/versions`, {
|
|
@@ -3824,9 +4178,9 @@ function registerVersionTools(server, callApi) {
|
|
|
3824
4178
|
title: "Get Policy Version",
|
|
3825
4179
|
annotations: { readOnlyHint: true },
|
|
3826
4180
|
description: "Get a single version by ID, including its rules and fact requirements.",
|
|
3827
|
-
inputSchema:
|
|
3828
|
-
groupId:
|
|
3829
|
-
versionId:
|
|
4181
|
+
inputSchema: z5.object({
|
|
4182
|
+
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4183
|
+
versionId: z5.string().uuid().describe("Version ID")
|
|
3830
4184
|
})
|
|
3831
4185
|
},
|
|
3832
4186
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}`)
|
|
@@ -3837,11 +4191,11 @@ function registerVersionTools(server, callApi) {
|
|
|
3837
4191
|
title: "Create Policy Version",
|
|
3838
4192
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3839
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.`,
|
|
3840
|
-
inputSchema:
|
|
3841
|
-
groupId:
|
|
3842
|
-
commitMessage:
|
|
3843
|
-
effectiveFrom:
|
|
3844
|
-
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}`)
|
|
3845
4199
|
})
|
|
3846
4200
|
},
|
|
3847
4201
|
async ({ groupId, ...body }) => callApi("POST", `policy-groups/${groupId}/versions`, { body })
|
|
@@ -3852,14 +4206,14 @@ function registerVersionTools(server, callApi) {
|
|
|
3852
4206
|
title: "Update Policy Version",
|
|
3853
4207
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3854
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.",
|
|
3855
|
-
inputSchema:
|
|
3856
|
-
groupId:
|
|
3857
|
-
versionId:
|
|
3858
|
-
commitMessage:
|
|
3859
|
-
effectiveFrom:
|
|
3860
|
-
effectiveTo:
|
|
3861
|
-
clearEffectiveFrom:
|
|
3862
|
-
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.")
|
|
3863
4217
|
})
|
|
3864
4218
|
},
|
|
3865
4219
|
async ({ groupId, versionId, ...body }) => callApi("PUT", `policy-groups/${groupId}/versions/${versionId}`, { body })
|
|
@@ -3870,9 +4224,9 @@ function registerVersionTools(server, callApi) {
|
|
|
3870
4224
|
title: "Delete Policy Version",
|
|
3871
4225
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3872
4226
|
description: "Delete a DRAFT version. Only DRAFT versions can be deleted.",
|
|
3873
|
-
inputSchema:
|
|
3874
|
-
groupId:
|
|
3875
|
-
versionId:
|
|
4227
|
+
inputSchema: z5.object({
|
|
4228
|
+
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
4229
|
+
versionId: z5.string().uuid().describe("Version ID")
|
|
3876
4230
|
})
|
|
3877
4231
|
},
|
|
3878
4232
|
async ({ groupId, versionId }) => callApi("DELETE", `policy-groups/${groupId}/versions/${versionId}`)
|
|
@@ -3883,9 +4237,9 @@ function registerVersionTools(server, callApi) {
|
|
|
3883
4237
|
title: "Clone Policy Version",
|
|
3884
4238
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3885
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.`,
|
|
3886
|
-
inputSchema:
|
|
3887
|
-
groupId:
|
|
3888
|
-
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")
|
|
3889
4243
|
})
|
|
3890
4244
|
},
|
|
3891
4245
|
async ({ groupId, versionId }) => {
|
|
@@ -3897,8 +4251,8 @@ function registerVersionTools(server, callApi) {
|
|
|
3897
4251
|
}
|
|
3898
4252
|
|
|
3899
4253
|
// src/mcp/tools/rules.ts
|
|
3900
|
-
import { z as
|
|
3901
|
-
import
|
|
4254
|
+
import { z as z6 } from "zod";
|
|
4255
|
+
import dedent17 from "dedent";
|
|
3902
4256
|
function registerRuleTools(server, callApi) {
|
|
3903
4257
|
server.registerTool(
|
|
3904
4258
|
"lexq_rules_list",
|
|
@@ -3906,9 +4260,9 @@ function registerRuleTools(server, callApi) {
|
|
|
3906
4260
|
title: "List Rules",
|
|
3907
4261
|
annotations: { readOnlyHint: true },
|
|
3908
4262
|
description: "List all rules in a version (priority ASC). Returns summary with conditionSummary and actionSummary.",
|
|
3909
|
-
inputSchema:
|
|
3910
|
-
groupId:
|
|
3911
|
-
versionId:
|
|
4263
|
+
inputSchema: z6.object({
|
|
4264
|
+
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4265
|
+
versionId: z6.string().uuid().describe("Version ID")
|
|
3912
4266
|
})
|
|
3913
4267
|
},
|
|
3914
4268
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/rules`)
|
|
@@ -3919,10 +4273,10 @@ function registerRuleTools(server, callApi) {
|
|
|
3919
4273
|
title: "Get Rule Detail",
|
|
3920
4274
|
annotations: { readOnlyHint: true },
|
|
3921
4275
|
description: "Get full rule detail including condition tree and action definitions.",
|
|
3922
|
-
inputSchema:
|
|
3923
|
-
groupId:
|
|
3924
|
-
versionId:
|
|
3925
|
-
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")
|
|
3926
4280
|
})
|
|
3927
4281
|
},
|
|
3928
4282
|
async ({ groupId, versionId, ruleId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`)
|
|
@@ -3932,7 +4286,7 @@ function registerRuleTools(server, callApi) {
|
|
|
3932
4286
|
{
|
|
3933
4287
|
title: "Create Rule",
|
|
3934
4288
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3935
|
-
description:
|
|
4289
|
+
description: dedent17`
|
|
3936
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.
|
|
3937
4291
|
|
|
3938
4292
|
Before creating rules with new fact keys, call lexq_facts_list to check existing facts.
|
|
@@ -4013,10 +4367,10 @@ function registerRuleTools(server, callApi) {
|
|
|
4013
4367
|
|
|
4014
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).
|
|
4015
4369
|
`,
|
|
4016
|
-
inputSchema:
|
|
4017
|
-
groupId:
|
|
4018
|
-
versionId:
|
|
4019
|
-
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(
|
|
4020
4374
|
"JSON string of CreateRuleRequest: { name, condition, actions, mutexGroup?, mutexMode?, mutexStrategy?, mutexLimit?, isEnabled? }"
|
|
4021
4375
|
)
|
|
4022
4376
|
})
|
|
@@ -4032,11 +4386,11 @@ function registerRuleTools(server, callApi) {
|
|
|
4032
4386
|
title: "Update Rule",
|
|
4033
4387
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4034
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.`,
|
|
4035
|
-
inputSchema:
|
|
4036
|
-
groupId:
|
|
4037
|
-
versionId:
|
|
4038
|
-
ruleId:
|
|
4039
|
-
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(
|
|
4040
4394
|
"JSON string of UpdateRuleRequest: { name?, condition?, actions?, mutexGroup?, mutexMode?, mutexStrategy?, mutexLimit?, isEnabled? }"
|
|
4041
4395
|
)
|
|
4042
4396
|
})
|
|
@@ -4054,11 +4408,11 @@ function registerRuleTools(server, callApi) {
|
|
|
4054
4408
|
title: "Delete Rule",
|
|
4055
4409
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4056
4410
|
description: "Delete a rule from a DRAFT version.",
|
|
4057
|
-
inputSchema:
|
|
4058
|
-
groupId:
|
|
4059
|
-
versionId:
|
|
4060
|
-
ruleId:
|
|
4061
|
-
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)")
|
|
4062
4416
|
})
|
|
4063
4417
|
},
|
|
4064
4418
|
async ({ groupId, versionId, ruleId, force }) => {
|
|
@@ -4075,10 +4429,10 @@ function registerRuleTools(server, callApi) {
|
|
|
4075
4429
|
title: "Reorder Rules",
|
|
4076
4430
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4077
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).",
|
|
4078
|
-
inputSchema:
|
|
4079
|
-
groupId:
|
|
4080
|
-
versionId:
|
|
4081
|
-
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")
|
|
4082
4436
|
})
|
|
4083
4437
|
},
|
|
4084
4438
|
async ({ groupId, versionId, ruleIds }) => {
|
|
@@ -4097,11 +4451,11 @@ function registerRuleTools(server, callApi) {
|
|
|
4097
4451
|
title: "Toggle Rule",
|
|
4098
4452
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4099
4453
|
description: "Enable or disable a rule without deleting it.",
|
|
4100
|
-
inputSchema:
|
|
4101
|
-
groupId:
|
|
4102
|
-
versionId:
|
|
4103
|
-
ruleId:
|
|
4104
|
-
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")
|
|
4105
4459
|
})
|
|
4106
4460
|
},
|
|
4107
4461
|
async ({ groupId, versionId, ruleId, isEnabled }) => callApi("PATCH", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}/enabled`, {
|
|
@@ -4111,7 +4465,7 @@ function registerRuleTools(server, callApi) {
|
|
|
4111
4465
|
}
|
|
4112
4466
|
|
|
4113
4467
|
// src/mcp/tools/facts.ts
|
|
4114
|
-
import { z as
|
|
4468
|
+
import { z as z7 } from "zod";
|
|
4115
4469
|
|
|
4116
4470
|
// src/types/facts.ts
|
|
4117
4471
|
var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
@@ -4125,10 +4479,10 @@ function registerFactTools(server, callApi) {
|
|
|
4125
4479
|
title: "List Fact Definitions",
|
|
4126
4480
|
annotations: { readOnlyHint: true },
|
|
4127
4481
|
description: "List all fact definitions (input variable schema). Shows key, type, required, and PII status. Always check this before creating rules.",
|
|
4128
|
-
inputSchema:
|
|
4129
|
-
page:
|
|
4130
|
-
size:
|
|
4131
|
-
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")
|
|
4132
4486
|
})
|
|
4133
4487
|
},
|
|
4134
4488
|
async ({ page, size, keyword }) => {
|
|
@@ -4143,21 +4497,21 @@ function registerFactTools(server, callApi) {
|
|
|
4143
4497
|
title: "Create Fact Definition",
|
|
4144
4498
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4145
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.",
|
|
4146
|
-
inputSchema:
|
|
4147
|
-
key:
|
|
4148
|
-
name:
|
|
4149
|
-
type:
|
|
4150
|
-
description:
|
|
4151
|
-
isRequired:
|
|
4152
|
-
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(
|
|
4153
4507
|
"Mark as PII \u2014 masked on every read surface, revealable only in the console (audited)"
|
|
4154
4508
|
),
|
|
4155
|
-
valueDomain:
|
|
4156
|
-
allowedValues:
|
|
4509
|
+
valueDomain: z7.object({
|
|
4510
|
+
allowedValues: z7.array(z7.union([z7.string(), z7.number()])).optional().describe(
|
|
4157
4511
|
"Accepted values. For list types the value domain applies to each element, not to the list as a whole."
|
|
4158
4512
|
),
|
|
4159
|
-
min:
|
|
4160
|
-
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.")
|
|
4161
4515
|
}).optional().describe(
|
|
4162
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."
|
|
4163
4517
|
)
|
|
@@ -4171,21 +4525,21 @@ function registerFactTools(server, callApi) {
|
|
|
4171
4525
|
title: "Update Fact Definition",
|
|
4172
4526
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4173
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).",
|
|
4174
|
-
inputSchema:
|
|
4175
|
-
factId:
|
|
4176
|
-
name:
|
|
4177
|
-
description:
|
|
4178
|
-
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(
|
|
4179
4533
|
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule outside a deleted group references the fact."
|
|
4180
4534
|
),
|
|
4181
|
-
isRequired:
|
|
4182
|
-
isPii:
|
|
4183
|
-
valueDomain:
|
|
4184
|
-
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(
|
|
4185
4539
|
"Accepted values. For list types the value domain applies to each element, not to the list as a whole."
|
|
4186
4540
|
),
|
|
4187
|
-
min:
|
|
4188
|
-
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.")
|
|
4189
4543
|
}).optional().describe(
|
|
4190
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."
|
|
4191
4545
|
)
|
|
@@ -4199,8 +4553,8 @@ function registerFactTools(server, callApi) {
|
|
|
4199
4553
|
title: "Delete Fact Definition",
|
|
4200
4554
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4201
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.",
|
|
4202
|
-
inputSchema:
|
|
4203
|
-
factId:
|
|
4556
|
+
inputSchema: z7.object({
|
|
4557
|
+
factId: z7.string().uuid().describe("Fact definition ID")
|
|
4204
4558
|
})
|
|
4205
4559
|
},
|
|
4206
4560
|
async ({ factId }) => callApi("DELETE", `schema/facts/${factId}`)
|
|
@@ -4211,7 +4565,7 @@ function registerFactTools(server, callApi) {
|
|
|
4211
4565
|
title: "Get Action Runtime Fact Metadata",
|
|
4212
4566
|
annotations: { readOnlyHint: true },
|
|
4213
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.",
|
|
4214
|
-
inputSchema:
|
|
4568
|
+
inputSchema: z7.object({})
|
|
4215
4569
|
},
|
|
4216
4570
|
async () => callApi("GET", "schema/action-metadata")
|
|
4217
4571
|
);
|
|
@@ -4221,9 +4575,9 @@ function registerFactTools(server, callApi) {
|
|
|
4221
4575
|
title: "List Unregistered Facts",
|
|
4222
4576
|
annotations: { readOnlyHint: true },
|
|
4223
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.",
|
|
4224
|
-
inputSchema:
|
|
4225
|
-
groupId:
|
|
4226
|
-
versionId:
|
|
4578
|
+
inputSchema: z7.object({
|
|
4579
|
+
groupId: z7.string().uuid().describe("Policy group ID"),
|
|
4580
|
+
versionId: z7.string().uuid().describe("Version ID")
|
|
4227
4581
|
})
|
|
4228
4582
|
},
|
|
4229
4583
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
|
|
@@ -4234,9 +4588,9 @@ function registerFactTools(server, callApi) {
|
|
|
4234
4588
|
title: "Export Fact Catalog",
|
|
4235
4589
|
annotations: { readOnlyHint: true },
|
|
4236
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.",
|
|
4237
|
-
inputSchema:
|
|
4238
|
-
format:
|
|
4239
|
-
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")
|
|
4240
4594
|
})
|
|
4241
4595
|
},
|
|
4242
4596
|
async ({ format, keyword }) => callApi("GET", "schema/facts/export", {
|
|
@@ -4246,7 +4600,8 @@ function registerFactTools(server, callApi) {
|
|
|
4246
4600
|
}
|
|
4247
4601
|
|
|
4248
4602
|
// src/mcp/tools/deploy.ts
|
|
4249
|
-
import
|
|
4603
|
+
import dedent18 from "dedent";
|
|
4604
|
+
import { z as z8 } from "zod";
|
|
4250
4605
|
function registerDeployTools(server, callApi) {
|
|
4251
4606
|
server.registerTool(
|
|
4252
4607
|
"lexq_deploy_publish",
|
|
@@ -4254,10 +4609,10 @@ function registerDeployTools(server, callApi) {
|
|
|
4254
4609
|
title: "Publish Version",
|
|
4255
4610
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4256
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.`,
|
|
4257
|
-
inputSchema:
|
|
4258
|
-
groupId:
|
|
4259
|
-
versionId:
|
|
4260
|
-
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)")
|
|
4261
4616
|
})
|
|
4262
4617
|
},
|
|
4263
4618
|
async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/versions/${versionId}/publish`, { body: { memo } })
|
|
@@ -4268,10 +4623,10 @@ function registerDeployTools(server, callApi) {
|
|
|
4268
4623
|
title: "Deploy to Live",
|
|
4269
4624
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4270
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.",
|
|
4271
|
-
inputSchema:
|
|
4272
|
-
groupId:
|
|
4273
|
-
versionId:
|
|
4274
|
-
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)")
|
|
4275
4630
|
})
|
|
4276
4631
|
},
|
|
4277
4632
|
async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/deploy`, {
|
|
@@ -4283,14 +4638,20 @@ function registerDeployTools(server, callApi) {
|
|
|
4283
4638
|
{
|
|
4284
4639
|
title: "Rollback Deployment",
|
|
4285
4640
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4286
|
-
description:
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4641
|
+
description: dedent18`
|
|
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).
|
|
4643
|
+
|
|
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.
|
|
4645
|
+
`,
|
|
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)")
|
|
4290
4651
|
})
|
|
4291
4652
|
},
|
|
4292
|
-
async ({ groupId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
|
|
4293
|
-
body: { memo }
|
|
4653
|
+
async ({ groupId, expectedCurrentVersionId, expectedCurrentDeploymentId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
|
|
4654
|
+
body: { memo, expectedCurrentVersionId, expectedCurrentDeploymentId }
|
|
4294
4655
|
})
|
|
4295
4656
|
);
|
|
4296
4657
|
server.registerTool(
|
|
@@ -4299,372 +4660,133 @@ function registerDeployTools(server, callApi) {
|
|
|
4299
4660
|
title: "Undeploy",
|
|
4300
4661
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4301
4662
|
description: "Remove the live version from traffic. The version stays ACTIVE but no longer serves requests.",
|
|
4302
|
-
inputSchema:
|
|
4303
|
-
groupId:
|
|
4304
|
-
memo:
|
|
4305
|
-
})
|
|
4306
|
-
},
|
|
4307
|
-
async ({ groupId, memo }) => callApi("POST", `policy-groups/${groupId}/undeploy`, {
|
|
4308
|
-
body: { memo }
|
|
4309
|
-
})
|
|
4310
|
-
);
|
|
4311
|
-
server.registerTool(
|
|
4312
|
-
"lexq_deploy_schedule",
|
|
4313
|
-
{
|
|
4314
|
-
title: "Schedule Deployment",
|
|
4315
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4316
|
-
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).",
|
|
4317
|
-
inputSchema: z6.object({
|
|
4318
|
-
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4319
|
-
versionId: z6.string().uuid().describe("ACTIVE version ID with a future effective start date"),
|
|
4320
|
-
memo: z6.string().min(1).describe("Schedule memo (required)")
|
|
4321
|
-
})
|
|
4322
|
-
},
|
|
4323
|
-
async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/schedule`, {
|
|
4324
|
-
body: { versionId, memo }
|
|
4325
|
-
})
|
|
4326
|
-
);
|
|
4327
|
-
server.registerTool(
|
|
4328
|
-
"lexq_deploy_unschedule",
|
|
4329
|
-
{
|
|
4330
|
-
title: "Cancel Scheduled Deployment",
|
|
4331
|
-
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4332
|
-
description: "Cancel the pending scheduled deployment for a group. The version itself is not affected. Fails with P-039 if no pending schedule exists.",
|
|
4333
|
-
inputSchema: z6.object({
|
|
4334
|
-
groupId: z6.string().uuid().describe("Policy group ID")
|
|
4335
|
-
})
|
|
4336
|
-
},
|
|
4337
|
-
async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}/schedule`)
|
|
4338
|
-
);
|
|
4339
|
-
server.registerTool(
|
|
4340
|
-
"lexq_deploy_schedules",
|
|
4341
|
-
{
|
|
4342
|
-
title: "List Scheduled Deployments",
|
|
4343
|
-
annotations: { readOnlyHint: true },
|
|
4344
|
-
description: "List scheduled deployments across all groups (all statuses: PENDING, EXECUTED, CANCELED, FAILED), newest first.",
|
|
4345
|
-
inputSchema: z6.object({
|
|
4346
|
-
page: z6.number().int().min(0).default(0).describe("Page number"),
|
|
4347
|
-
size: z6.number().int().min(1).max(100).default(20).describe("Page size")
|
|
4348
|
-
})
|
|
4349
|
-
},
|
|
4350
|
-
async ({ page, size }) => callApi("GET", "policy-groups/schedules", { params: paginationParams(page, size) })
|
|
4351
|
-
);
|
|
4352
|
-
server.registerTool(
|
|
4353
|
-
"lexq_deploy_history",
|
|
4354
|
-
{
|
|
4355
|
-
title: "Deployment History",
|
|
4356
|
-
annotations: { readOnlyHint: true },
|
|
4357
|
-
description: "List deployment history across all groups.",
|
|
4358
|
-
inputSchema: z6.object({
|
|
4359
|
-
page: z6.number().int().min(0).default(0).describe("Page number"),
|
|
4360
|
-
size: z6.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
4361
|
-
groupId: z6.string().uuid().optional().describe("Filter by group ID"),
|
|
4362
|
-
types: z6.string().optional().describe("Filter by deployment types (comma-separated: DEPLOY,ROLLBACK,UNDEPLOY)"),
|
|
4363
|
-
startDate: z6.string().optional().describe(START_DAY),
|
|
4364
|
-
endDate: z6.string().optional().describe(END_DAY)
|
|
4365
|
-
})
|
|
4366
|
-
},
|
|
4367
|
-
async ({ page, size, groupId, types, startDate, endDate }) => {
|
|
4368
|
-
const params = paginationParams(page, size);
|
|
4369
|
-
if (groupId) params.groupId = groupId;
|
|
4370
|
-
if (types) params.types = types;
|
|
4371
|
-
if (startDate) params.startDate = startDate;
|
|
4372
|
-
if (endDate) params.endDate = endDate;
|
|
4373
|
-
return callApi("GET", "deployments", { params });
|
|
4374
|
-
}
|
|
4375
|
-
);
|
|
4376
|
-
server.registerTool(
|
|
4377
|
-
"lexq_deploy_detail",
|
|
4378
|
-
{
|
|
4379
|
-
title: "Deployment Detail",
|
|
4380
|
-
annotations: { readOnlyHint: true },
|
|
4381
|
-
description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
|
|
4382
|
-
inputSchema: z6.object({
|
|
4383
|
-
deploymentId: z6.string().uuid().describe("Deployment ID")
|
|
4384
|
-
})
|
|
4385
|
-
},
|
|
4386
|
-
async ({ deploymentId }) => callApi("GET", `deployments/${deploymentId}`)
|
|
4387
|
-
);
|
|
4388
|
-
server.registerTool(
|
|
4389
|
-
"lexq_deploy_overview",
|
|
4390
|
-
{
|
|
4391
|
-
title: "Deployment Overview",
|
|
4392
|
-
annotations: { readOnlyHint: true },
|
|
4393
|
-
description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer.",
|
|
4394
|
-
inputSchema: z6.object({})
|
|
4395
|
-
},
|
|
4396
|
-
async () => callApi("GET", "deployments/overview")
|
|
4397
|
-
);
|
|
4398
|
-
server.registerTool(
|
|
4399
|
-
"lexq_deploy_deployable",
|
|
4400
|
-
{
|
|
4401
|
-
title: "List Deployable Versions",
|
|
4402
|
-
annotations: { readOnlyHint: true },
|
|
4403
|
-
description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
|
|
4404
|
-
inputSchema: z6.object({
|
|
4405
|
-
groupId: z6.string().uuid().describe("Policy group ID")
|
|
4406
|
-
})
|
|
4407
|
-
},
|
|
4408
|
-
async ({ groupId }) => callApi("GET", `deployments/groups/${groupId}/deployable-versions`)
|
|
4409
|
-
);
|
|
4410
|
-
server.registerTool(
|
|
4411
|
-
"lexq_deploy_diff",
|
|
4412
|
-
{
|
|
4413
|
-
title: "Deployment Diff",
|
|
4414
|
-
annotations: { readOnlyHint: true },
|
|
4415
|
-
description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
|
|
4416
|
-
inputSchema: z6.object({
|
|
4417
|
-
baseVersionId: z6.string().uuid().describe("Base version ID (typically the current live)"),
|
|
4418
|
-
targetVersionId: z6.string().uuid().describe("Target version ID (the one you want to deploy)")
|
|
4419
|
-
})
|
|
4420
|
-
},
|
|
4421
|
-
async ({ baseVersionId, targetVersionId }) => callApi("GET", "deployments/diff", {
|
|
4422
|
-
params: { baseVersionId, targetVersionId }
|
|
4423
|
-
})
|
|
4424
|
-
);
|
|
4425
|
-
}
|
|
4426
|
-
|
|
4427
|
-
// src/mcp/tools/analytics.ts
|
|
4428
|
-
import { z as z7 } from "zod";
|
|
4429
|
-
import dedent16 from "dedent";
|
|
4430
|
-
function registerAnalyticsTools(server, callApi) {
|
|
4431
|
-
server.registerTool(
|
|
4432
|
-
"lexq_dry_run",
|
|
4433
|
-
{
|
|
4434
|
-
title: "Dry Run",
|
|
4435
|
-
annotations: { readOnlyHint: true },
|
|
4436
|
-
description: dedent16`
|
|
4437
|
-
Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
|
|
4438
|
-
|
|
4439
|
-
Returns:
|
|
4440
|
-
inputFacts — normalized input facts
|
|
4441
|
-
mutatedFacts — input facts changed by rule actions (e.g. MUTATE_FACT mutates paymentAmount)
|
|
4442
|
-
generatedVariables — system-generated values; every fact in mutatedFacts gets a paired {factName}__delta key (signed difference)
|
|
4443
|
-
executionTraces — per-rule match status
|
|
4444
|
-
decisionTraces — per-rule decision (SELECTED / NO_MATCH / BLOCKED / etc.)
|
|
4445
|
-
|
|
4446
|
-
Example input: { "facts": { "paymentAmount": 100000, "customerTier": "VIP" } }
|
|
4447
|
-
Always dry-run before publishing to validate rule behavior.`,
|
|
4448
|
-
inputSchema: z7.object({
|
|
4449
|
-
versionId: z7.string().uuid().describe("Policy version ID to test against"),
|
|
4450
|
-
facts: z7.string().describe('JSON string of facts object, e.g. {"paymentAmount":100000}'),
|
|
4451
|
-
includeDebugInfo: z7.boolean().default(true).describe("Include execution and decision traces")
|
|
4452
|
-
})
|
|
4453
|
-
},
|
|
4454
|
-
async ({ versionId, facts, includeDebugInfo }) => {
|
|
4455
|
-
const parsedFacts = parseJson(facts);
|
|
4456
|
-
return callApi("POST", `analytics/dry-run/versions/${versionId}`, {
|
|
4457
|
-
body: { facts: parsedFacts, includeDebugInfo }
|
|
4458
|
-
});
|
|
4459
|
-
}
|
|
4460
|
-
);
|
|
4461
|
-
server.registerTool(
|
|
4462
|
-
"lexq_dry_run_compare",
|
|
4463
|
-
{
|
|
4464
|
-
title: "Dry Run Compare",
|
|
4465
|
-
annotations: { readOnlyHint: true },
|
|
4466
|
-
description: dedent16`
|
|
4467
|
-
Compare dry run results between two versions using the same input facts. Useful for validating changes.
|
|
4468
|
-
|
|
4469
|
-
Returns:
|
|
4470
|
-
resultA / resultB — full DryRunResponse for each version
|
|
4471
|
-
diff.mutatedDiff — changes in mutatedFacts between A and B (key → {before, after})
|
|
4472
|
-
diff.generatedDiff — changes in generatedVariables between A and B`,
|
|
4473
|
-
inputSchema: z7.object({
|
|
4474
|
-
versionIdA: z7.string().uuid().describe("Baseline version ID"),
|
|
4475
|
-
versionIdB: z7.string().uuid().describe("Candidate version ID"),
|
|
4476
|
-
facts: z7.string().describe("JSON string of facts object")
|
|
4477
|
-
})
|
|
4478
|
-
},
|
|
4479
|
-
async ({ versionIdA, versionIdB, facts }) => {
|
|
4480
|
-
const parsedFacts = parseJson(facts);
|
|
4481
|
-
return callApi("POST", "analytics/dry-run/compare", {
|
|
4482
|
-
body: { versionIdA, versionIdB, facts: parsedFacts }
|
|
4483
|
-
});
|
|
4484
|
-
}
|
|
4663
|
+
inputSchema: z8.object({
|
|
4664
|
+
groupId: z8.string().uuid().describe("Policy group ID"),
|
|
4665
|
+
memo: z8.string().min(1).describe("Undeploy reason (required)")
|
|
4666
|
+
})
|
|
4667
|
+
},
|
|
4668
|
+
async ({ groupId, memo }) => callApi("POST", `policy-groups/${groupId}/undeploy`, {
|
|
4669
|
+
body: { memo }
|
|
4670
|
+
})
|
|
4485
4671
|
);
|
|
4486
4672
|
server.registerTool(
|
|
4487
|
-
"
|
|
4673
|
+
"lexq_deploy_schedule",
|
|
4488
4674
|
{
|
|
4489
|
-
title: "
|
|
4490
|
-
annotations: { readOnlyHint:
|
|
4491
|
-
description: "
|
|
4492
|
-
inputSchema:
|
|
4493
|
-
groupId:
|
|
4494
|
-
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)")
|
|
4495
4682
|
})
|
|
4496
4683
|
},
|
|
4497
|
-
async ({ groupId, versionId }) => callApi("
|
|
4684
|
+
async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/schedule`, {
|
|
4685
|
+
body: { versionId, memo }
|
|
4686
|
+
})
|
|
4498
4687
|
);
|
|
4499
4688
|
server.registerTool(
|
|
4500
|
-
"
|
|
4689
|
+
"lexq_deploy_unschedule",
|
|
4501
4690
|
{
|
|
4502
|
-
title: "
|
|
4503
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
4504
|
-
description:
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
dataset.type and dataset.source are BOTH required, and must be paired:
|
|
4508
|
-
HISTORICAL → source EXECUTION_LOGS, with dataset.from / dataset.to (yyyy-MM-dd, whole days
|
|
4509
|
-
in the organization time zone, both inclusive)
|
|
4510
|
-
UPLOADED → source S3_BUCKET, with dataset.path (the path returned by lexq_dataset_upload)
|
|
4511
|
-
MANUAL → source REQUEST_BODY, with dataset.manualData (array of fact records)
|
|
4512
|
-
|
|
4513
|
-
options.maxRecords: number (max ${MAX_SIMULATION_RECORDS}). When omitted, HISTORICAL and UPLOADED run at
|
|
4514
|
-
most ${DEFAULT_SIMULATION_RECORDS} records and MANUAL runs every record sent.
|
|
4515
|
-
options.baselinePolicyVersionId: uuid (optional, for baseline comparison)
|
|
4516
|
-
options.includeRuleStats: boolean
|
|
4517
|
-
options.metricConfig: optional — omit for plain execution count. To aggregate a fact, pass
|
|
4518
|
-
{ "targetVariable": "<fact>", "aggregationType": "COUNT" | "SUM" | "AVG" }
|
|
4519
|
-
|
|
4520
|
-
Example (uploaded dataset):
|
|
4521
|
-
{
|
|
4522
|
-
"policyVersionId": "<uuid>",
|
|
4523
|
-
"dataset": { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<path from lexq_dataset_upload>" },
|
|
4524
|
-
"options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true, "maxRecords": 10000 }
|
|
4525
|
-
}
|
|
4526
|
-
|
|
4527
|
-
Example (historical):
|
|
4528
|
-
{
|
|
4529
|
-
"policyVersionId": "<uuid>",
|
|
4530
|
-
"dataset": { "type": "HISTORICAL", "source": "EXECUTION_LOGS", "from": "2026-01-01", "to": "2026-01-31" },
|
|
4531
|
-
"options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true }
|
|
4532
|
-
}
|
|
4533
|
-
`,
|
|
4534
|
-
inputSchema: z7.object({
|
|
4535
|
-
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")
|
|
4536
4696
|
})
|
|
4537
4697
|
},
|
|
4538
|
-
async ({
|
|
4539
|
-
const parsed = parseJson(body);
|
|
4540
|
-
return callApi("POST", "analytics/simulations", { body: parsed });
|
|
4541
|
-
}
|
|
4698
|
+
async ({ groupId }) => callApi("DELETE", `policy-groups/${groupId}/schedule`)
|
|
4542
4699
|
);
|
|
4543
4700
|
server.registerTool(
|
|
4544
|
-
"
|
|
4701
|
+
"lexq_deploy_schedules",
|
|
4545
4702
|
{
|
|
4546
|
-
title: "
|
|
4703
|
+
title: "List Scheduled Deployments",
|
|
4547
4704
|
annotations: { readOnlyHint: true },
|
|
4548
|
-
description: "
|
|
4549
|
-
inputSchema:
|
|
4550
|
-
|
|
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")
|
|
4551
4709
|
})
|
|
4552
4710
|
},
|
|
4553
|
-
async ({
|
|
4711
|
+
async ({ page, size }) => callApi("GET", "policy-groups/schedules", { params: paginationParams(page, size) })
|
|
4554
4712
|
);
|
|
4555
4713
|
server.registerTool(
|
|
4556
|
-
"
|
|
4714
|
+
"lexq_deploy_history",
|
|
4557
4715
|
{
|
|
4558
|
-
title: "
|
|
4716
|
+
title: "Deployment History",
|
|
4559
4717
|
annotations: { readOnlyHint: true },
|
|
4560
|
-
description: "List
|
|
4561
|
-
inputSchema:
|
|
4562
|
-
page:
|
|
4563
|
-
size:
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
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)
|
|
4567
4726
|
})
|
|
4568
4727
|
},
|
|
4569
|
-
async ({ page, size,
|
|
4728
|
+
async ({ page, size, groupId, types, startDate, endDate }) => {
|
|
4570
4729
|
const params = paginationParams(page, size);
|
|
4571
|
-
if (
|
|
4572
|
-
if (
|
|
4573
|
-
if (
|
|
4574
|
-
|
|
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 });
|
|
4575
4735
|
}
|
|
4576
4736
|
);
|
|
4577
4737
|
server.registerTool(
|
|
4578
|
-
"
|
|
4738
|
+
"lexq_deploy_detail",
|
|
4579
4739
|
{
|
|
4580
|
-
title: "
|
|
4581
|
-
annotations: { readOnlyHint:
|
|
4582
|
-
description: "
|
|
4583
|
-
inputSchema:
|
|
4584
|
-
|
|
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")
|
|
4585
4745
|
})
|
|
4586
4746
|
},
|
|
4587
|
-
async ({
|
|
4747
|
+
async ({ deploymentId }) => callApi("GET", `deployments/${deploymentId}`)
|
|
4588
4748
|
);
|
|
4589
4749
|
server.registerTool(
|
|
4590
|
-
"
|
|
4750
|
+
"lexq_deploy_overview",
|
|
4591
4751
|
{
|
|
4592
|
-
title: "
|
|
4752
|
+
title: "Deployment Overview",
|
|
4593
4753
|
annotations: { readOnlyHint: true },
|
|
4594
|
-
description: "
|
|
4595
|
-
inputSchema:
|
|
4596
|
-
simulationId: z7.string().uuid().describe("Simulation ID"),
|
|
4597
|
-
format: z7.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
4598
|
-
})
|
|
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({})
|
|
4599
4756
|
},
|
|
4600
|
-
async (
|
|
4601
|
-
params: { format }
|
|
4602
|
-
})
|
|
4757
|
+
async () => callApi("GET", "deployments/overview")
|
|
4603
4758
|
);
|
|
4604
4759
|
server.registerTool(
|
|
4605
|
-
"
|
|
4760
|
+
"lexq_deploy_deployable",
|
|
4606
4761
|
{
|
|
4607
|
-
title: "
|
|
4608
|
-
annotations: { readOnlyHint:
|
|
4609
|
-
description:
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
To use the returned path in lexq_simulation_start, set:
|
|
4614
|
-
dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
|
|
4615
|
-
|
|
4616
|
-
CSV example:
|
|
4617
|
-
userId,paymentAmount
|
|
4618
|
-
user_001,150000
|
|
4619
|
-
user_002,50000
|
|
4620
|
-
|
|
4621
|
-
JSON example: [{"userId":"user_001","paymentAmount":150000}, {"userId":"user_002","paymentAmount":50000}]
|
|
4622
|
-
`,
|
|
4623
|
-
inputSchema: z7.object({
|
|
4624
|
-
content: z7.string().describe("CSV or JSON content as string"),
|
|
4625
|
-
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")
|
|
4626
4767
|
})
|
|
4627
4768
|
},
|
|
4628
|
-
async ({
|
|
4629
|
-
const result = await callApi("POST", "analytics/datasets/upload", {
|
|
4630
|
-
upload: { content, filename, fieldName: "file" }
|
|
4631
|
-
});
|
|
4632
|
-
if (!result.isError) {
|
|
4633
|
-
try {
|
|
4634
|
-
const uploaded = parseJson(result.content[0]?.text ?? "{}");
|
|
4635
|
-
if (uploaded.path) {
|
|
4636
|
-
const dataset = { type: "UPLOADED", source: "S3_BUCKET", path: uploaded.path };
|
|
4637
|
-
result.content.push({
|
|
4638
|
-
type: "text",
|
|
4639
|
-
text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
|
|
4640
|
-
});
|
|
4641
|
-
}
|
|
4642
|
-
} catch {
|
|
4643
|
-
}
|
|
4644
|
-
}
|
|
4645
|
-
return result;
|
|
4646
|
-
}
|
|
4769
|
+
async ({ groupId }) => callApi("GET", `deployments/groups/${groupId}/deployable-versions`)
|
|
4647
4770
|
);
|
|
4648
4771
|
server.registerTool(
|
|
4649
|
-
"
|
|
4772
|
+
"lexq_deploy_diff",
|
|
4650
4773
|
{
|
|
4651
|
-
title: "
|
|
4774
|
+
title: "Deployment Diff",
|
|
4652
4775
|
annotations: { readOnlyHint: true },
|
|
4653
|
-
description: "
|
|
4654
|
-
inputSchema:
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
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)")
|
|
4658
4780
|
})
|
|
4659
4781
|
},
|
|
4660
|
-
async ({
|
|
4661
|
-
params: {
|
|
4782
|
+
async ({ baseVersionId, targetVersionId }) => callApi("GET", "deployments/diff", {
|
|
4783
|
+
params: { baseVersionId, targetVersionId }
|
|
4662
4784
|
})
|
|
4663
4785
|
);
|
|
4664
4786
|
}
|
|
4665
4787
|
|
|
4666
4788
|
// src/mcp/tools/profile.ts
|
|
4667
|
-
import { z as
|
|
4789
|
+
import { z as z9 } from "zod";
|
|
4668
4790
|
var RELATIVE_THRESHOLD = `flagged = p50 \u2265 ${SLOW_MULTIPLIER}\xD7 median of per-rule p50s within the group; absolute thresholds are intentionally not supported.`;
|
|
4669
4791
|
function profileParams(opts) {
|
|
4670
4792
|
const params = {};
|
|
@@ -4681,14 +4803,14 @@ function registerProfileTools(server, callApi) {
|
|
|
4681
4803
|
title: "Group Latency Profile",
|
|
4682
4804
|
annotations: { readOnlyHint: true },
|
|
4683
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.`,
|
|
4684
|
-
inputSchema:
|
|
4685
|
-
groupId:
|
|
4686
|
-
versionId:
|
|
4687
|
-
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(
|
|
4688
4810
|
`Window start, ISO-8601 instant (e.g. 2026-07-01T00:00:00Z). Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
|
|
4689
4811
|
),
|
|
4690
|
-
to:
|
|
4691
|
-
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)")
|
|
4692
4814
|
})
|
|
4693
4815
|
},
|
|
4694
4816
|
async ({ groupId, versionId, from, to, cacheState }) => callApi("GET", `policy-groups/${groupId}/profile`, {
|
|
@@ -4701,14 +4823,14 @@ function registerProfileTools(server, callApi) {
|
|
|
4701
4823
|
title: "Rule Latency Detail",
|
|
4702
4824
|
annotations: { readOnlyHint: true },
|
|
4703
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,
|
|
4704
|
-
inputSchema:
|
|
4705
|
-
groupId:
|
|
4706
|
-
ruleId:
|
|
4707
|
-
versionId:
|
|
4708
|
-
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(
|
|
4709
4831
|
`Window start, ISO-8601 instant. Default: ${PROFILE_DEFAULT_WINDOW_HOURS}h ago`
|
|
4710
4832
|
),
|
|
4711
|
-
to:
|
|
4833
|
+
to: z9.string().optional().describe("Window end, ISO-8601 instant. Default: now")
|
|
4712
4834
|
})
|
|
4713
4835
|
},
|
|
4714
4836
|
async ({ groupId, ruleId, versionId, from, to }) => callApi("GET", `policy-groups/${groupId}/profile/rules/${ruleId}`, {
|
|
@@ -4718,7 +4840,7 @@ function registerProfileTools(server, callApi) {
|
|
|
4718
4840
|
}
|
|
4719
4841
|
|
|
4720
4842
|
// src/mcp/tools/replay.ts
|
|
4721
|
-
import { z as
|
|
4843
|
+
import { z as z10 } from "zod";
|
|
4722
4844
|
function registerReplayTools(server, callApi) {
|
|
4723
4845
|
server.registerTool(
|
|
4724
4846
|
"lexq_replay_decision",
|
|
@@ -4726,9 +4848,9 @@ function registerReplayTools(server, callApi) {
|
|
|
4726
4848
|
title: "Replay a Decision",
|
|
4727
4849
|
annotations: { readOnlyHint: true },
|
|
4728
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.",
|
|
4729
|
-
inputSchema:
|
|
4730
|
-
traceId:
|
|
4731
|
-
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")
|
|
4732
4854
|
})
|
|
4733
4855
|
},
|
|
4734
4856
|
async ({ traceId, candidateVersionId }) => callApi("POST", "replay/decisions", { body: { traceId, candidateVersionId } })
|
|
@@ -4739,11 +4861,11 @@ function registerReplayTools(server, callApi) {
|
|
|
4739
4861
|
title: "Start Window Replay (Blast Radius)",
|
|
4740
4862
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4741
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.",
|
|
4742
|
-
inputSchema:
|
|
4743
|
-
candidateVersionId:
|
|
4744
|
-
from:
|
|
4745
|
-
to:
|
|
4746
|
-
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(
|
|
4747
4869
|
`Sample cap (server default applies; hard cap ${REPLAY_WINDOW_MAX_RECORDS / 1e3}k)`
|
|
4748
4870
|
)
|
|
4749
4871
|
})
|
|
@@ -4756,8 +4878,8 @@ function registerReplayTools(server, callApi) {
|
|
|
4756
4878
|
title: "Get Replay Job Status",
|
|
4757
4879
|
annotations: { readOnlyHint: true },
|
|
4758
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.",
|
|
4759
|
-
inputSchema:
|
|
4760
|
-
jobId:
|
|
4881
|
+
inputSchema: z10.object({
|
|
4882
|
+
jobId: z10.string().describe("Replay job ID from lexq_replay_start")
|
|
4761
4883
|
})
|
|
4762
4884
|
},
|
|
4763
4885
|
async ({ jobId }) => callApi("GET", `replay/jobs/${jobId}`)
|
|
@@ -4768,9 +4890,9 @@ function registerReplayTools(server, callApi) {
|
|
|
4768
4890
|
title: "List Replay Jobs",
|
|
4769
4891
|
annotations: { readOnlyHint: true },
|
|
4770
4892
|
description: "List window replay job history (reverse-chronological). Lightweight items \u2014 use lexq_replay_status for summary and changed samples.",
|
|
4771
|
-
inputSchema:
|
|
4772
|
-
page:
|
|
4773
|
-
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")
|
|
4774
4896
|
})
|
|
4775
4897
|
},
|
|
4776
4898
|
async ({ page, size }) => callApi("GET", "replay/jobs", { params: paginationParams(page, size) })
|
|
@@ -4781,8 +4903,8 @@ function registerReplayTools(server, callApi) {
|
|
|
4781
4903
|
title: "Cancel Replay Job",
|
|
4782
4904
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
4783
4905
|
description: "Cooperatively cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
|
|
4784
|
-
inputSchema:
|
|
4785
|
-
jobId:
|
|
4906
|
+
inputSchema: z10.object({
|
|
4907
|
+
jobId: z10.string().describe("Replay job ID")
|
|
4786
4908
|
})
|
|
4787
4909
|
},
|
|
4788
4910
|
async ({ jobId }) => callApi("POST", `replay/jobs/${jobId}/cancel`)
|
|
@@ -4793,9 +4915,9 @@ function registerReplayTools(server, callApi) {
|
|
|
4793
4915
|
title: "Export Replay Result",
|
|
4794
4916
|
annotations: { readOnlyHint: true },
|
|
4795
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.",
|
|
4796
|
-
inputSchema:
|
|
4797
|
-
jobId:
|
|
4798
|
-
format:
|
|
4918
|
+
inputSchema: z10.object({
|
|
4919
|
+
jobId: z10.string().describe("Replay job ID"),
|
|
4920
|
+
format: z10.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
4799
4921
|
})
|
|
4800
4922
|
},
|
|
4801
4923
|
async ({ jobId, format }) => callApi("GET", `replay/jobs/${jobId}/export`, { params: { format } })
|
|
@@ -4803,7 +4925,7 @@ function registerReplayTools(server, callApi) {
|
|
|
4803
4925
|
}
|
|
4804
4926
|
|
|
4805
4927
|
// src/mcp/tools/history.ts
|
|
4806
|
-
import { z as
|
|
4928
|
+
import { z as z11 } from "zod";
|
|
4807
4929
|
function registerHistoryTools(server, callApi) {
|
|
4808
4930
|
server.registerTool(
|
|
4809
4931
|
"lexq_history_list",
|
|
@@ -4811,15 +4933,15 @@ function registerHistoryTools(server, callApi) {
|
|
|
4811
4933
|
title: "List Execution History",
|
|
4812
4934
|
annotations: { readOnlyHint: true },
|
|
4813
4935
|
description: "List policy execution history. Shows trace ID, group, version, status, match result, and latency.",
|
|
4814
|
-
inputSchema:
|
|
4815
|
-
page:
|
|
4816
|
-
size:
|
|
4817
|
-
traceId:
|
|
4818
|
-
groupId:
|
|
4819
|
-
versionId:
|
|
4820
|
-
status:
|
|
4821
|
-
startDate:
|
|
4822
|
-
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)
|
|
4823
4945
|
})
|
|
4824
4946
|
},
|
|
4825
4947
|
async ({ page, size, traceId, groupId, versionId, status, startDate, endDate }) => {
|
|
@@ -4839,8 +4961,8 @@ function registerHistoryTools(server, callApi) {
|
|
|
4839
4961
|
title: "Get Execution Detail",
|
|
4840
4962
|
annotations: { readOnlyHint: true },
|
|
4841
4963
|
description: "Get full execution detail including inputFacts, mutatedFacts, generatedVariables, executionTraces, and decisionTraces.",
|
|
4842
|
-
inputSchema:
|
|
4843
|
-
traceId:
|
|
4964
|
+
inputSchema: z11.object({
|
|
4965
|
+
traceId: z11.string().describe("Trace ID from execution history")
|
|
4844
4966
|
})
|
|
4845
4967
|
},
|
|
4846
4968
|
async ({ traceId }) => callApi("GET", `execution/history/${traceId}`)
|
|
@@ -4851,10 +4973,10 @@ function registerHistoryTools(server, callApi) {
|
|
|
4851
4973
|
title: "Execution Statistics",
|
|
4852
4974
|
annotations: { readOnlyHint: true },
|
|
4853
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.`,
|
|
4854
|
-
inputSchema:
|
|
4855
|
-
groupId:
|
|
4856
|
-
startDate:
|
|
4857
|
-
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)
|
|
4858
4980
|
})
|
|
4859
4981
|
},
|
|
4860
4982
|
async ({ groupId, startDate, endDate }) => {
|
|
@@ -4868,7 +4990,7 @@ function registerHistoryTools(server, callApi) {
|
|
|
4868
4990
|
}
|
|
4869
4991
|
|
|
4870
4992
|
// src/mcp/tools/provenance.ts
|
|
4871
|
-
import { z as
|
|
4993
|
+
import { z as z12 } from "zod";
|
|
4872
4994
|
function registerProvenanceTools(server, callApi) {
|
|
4873
4995
|
server.registerTool(
|
|
4874
4996
|
"lexq_provenance_get",
|
|
@@ -4876,8 +4998,8 @@ function registerProvenanceTools(server, callApi) {
|
|
|
4876
4998
|
title: "Get Decision Provenance",
|
|
4877
4999
|
annotations: { readOnlyHint: true },
|
|
4878
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.",
|
|
4879
|
-
inputSchema:
|
|
4880
|
-
traceId:
|
|
5001
|
+
inputSchema: z12.object({
|
|
5002
|
+
traceId: z12.string().describe("Trace ID of the execution")
|
|
4881
5003
|
})
|
|
4882
5004
|
},
|
|
4883
5005
|
async ({ traceId }) => callApi("GET", `provenance/${traceId}`)
|
|
@@ -4888,14 +5010,14 @@ function registerProvenanceTools(server, callApi) {
|
|
|
4888
5010
|
title: "List PII Reveal Audits",
|
|
4889
5011
|
annotations: { readOnlyHint: true },
|
|
4890
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.",
|
|
4891
|
-
inputSchema:
|
|
4892
|
-
page:
|
|
4893
|
-
size:
|
|
4894
|
-
traceId:
|
|
4895
|
-
revealedBy:
|
|
4896
|
-
factKey:
|
|
4897
|
-
startDate:
|
|
4898
|
-
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)
|
|
4899
5021
|
})
|
|
4900
5022
|
},
|
|
4901
5023
|
async ({ page, size, traceId, revealedBy, factKey, startDate, endDate }) => {
|
|
@@ -4911,7 +5033,7 @@ function registerProvenanceTools(server, callApi) {
|
|
|
4911
5033
|
}
|
|
4912
5034
|
|
|
4913
5035
|
// src/mcp/tools/logs.ts
|
|
4914
|
-
import { z as
|
|
5036
|
+
import { z as z13 } from "zod";
|
|
4915
5037
|
function registerLogTools(server, callApi) {
|
|
4916
5038
|
server.registerTool(
|
|
4917
5039
|
"lexq_logs_list",
|
|
@@ -4919,14 +5041,14 @@ function registerLogTools(server, callApi) {
|
|
|
4919
5041
|
title: "List Failure Logs",
|
|
4920
5042
|
annotations: { readOnlyHint: true },
|
|
4921
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.`,
|
|
4922
|
-
inputSchema:
|
|
4923
|
-
page:
|
|
4924
|
-
size:
|
|
4925
|
-
taskType:
|
|
4926
|
-
status:
|
|
4927
|
-
keyword:
|
|
4928
|
-
startDate:
|
|
4929
|
-
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)
|
|
4930
5052
|
})
|
|
4931
5053
|
},
|
|
4932
5054
|
async ({ page, size, taskType, status, keyword, startDate, endDate }) => {
|
|
@@ -4945,8 +5067,8 @@ function registerLogTools(server, callApi) {
|
|
|
4945
5067
|
title: "Get Failure Log",
|
|
4946
5068
|
annotations: { readOnlyHint: true },
|
|
4947
5069
|
description: "Get failure log detail by ID.",
|
|
4948
|
-
inputSchema:
|
|
4949
|
-
logId:
|
|
5070
|
+
inputSchema: z13.object({
|
|
5071
|
+
logId: z13.string().uuid().describe("Failure log ID")
|
|
4950
5072
|
})
|
|
4951
5073
|
},
|
|
4952
5074
|
async ({ logId }) => callApi("GET", `failure-logs/${logId}`)
|
|
@@ -4957,9 +5079,9 @@ function registerLogTools(server, callApi) {
|
|
|
4957
5079
|
title: "Process Failure Log",
|
|
4958
5080
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4959
5081
|
description: "Process a single failure log: RESOLVE (mark as manually fixed) or IGNORE (skip intentionally).",
|
|
4960
|
-
inputSchema:
|
|
4961
|
-
logId:
|
|
4962
|
-
action:
|
|
5082
|
+
inputSchema: z13.object({
|
|
5083
|
+
logId: z13.string().uuid().describe("Failure log ID"),
|
|
5084
|
+
action: z13.enum(FailureAction).describe("Action to take")
|
|
4963
5085
|
})
|
|
4964
5086
|
},
|
|
4965
5087
|
async ({ logId, action }) => callApi("POST", `failure-logs/${logId}/actions`, {
|
|
@@ -4972,9 +5094,9 @@ function registerLogTools(server, callApi) {
|
|
|
4972
5094
|
title: "Bulk Process Failure Logs",
|
|
4973
5095
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
4974
5096
|
description: "Process multiple failure logs at once. Provide an array of log IDs and the action.",
|
|
4975
|
-
inputSchema:
|
|
4976
|
-
logIds:
|
|
4977
|
-
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")
|
|
4978
5100
|
})
|
|
4979
5101
|
},
|
|
4980
5102
|
async ({ logIds, action }) => callApi("POST", "failure-logs/bulk-actions", {
|
|
@@ -4984,7 +5106,7 @@ function registerLogTools(server, callApi) {
|
|
|
4984
5106
|
}
|
|
4985
5107
|
|
|
4986
5108
|
// src/mcp/tools/webhook-subscriptions.ts
|
|
4987
|
-
import { z as
|
|
5109
|
+
import { z as z14 } from "zod";
|
|
4988
5110
|
function registerWebhookSubscriptionTools(server, callApi) {
|
|
4989
5111
|
server.registerTool(
|
|
4990
5112
|
"lexq_webhook_subscriptions_list",
|
|
@@ -4992,9 +5114,9 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
4992
5114
|
title: "List Webhook Subscriptions",
|
|
4993
5115
|
annotations: { readOnlyHint: true },
|
|
4994
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.",
|
|
4995
|
-
inputSchema:
|
|
4996
|
-
page:
|
|
4997
|
-
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")
|
|
4998
5120
|
})
|
|
4999
5121
|
},
|
|
5000
5122
|
async ({ page, size }) => {
|
|
@@ -5008,8 +5130,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5008
5130
|
title: "Get Webhook Subscription",
|
|
5009
5131
|
annotations: { readOnlyHint: true },
|
|
5010
5132
|
description: "Get webhook subscription detail by ID.",
|
|
5011
|
-
inputSchema:
|
|
5012
|
-
id:
|
|
5133
|
+
inputSchema: z14.object({
|
|
5134
|
+
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
5013
5135
|
})
|
|
5014
5136
|
},
|
|
5015
5137
|
async ({ id }) => callApi("GET", `webhook-subscriptions/${id}`)
|
|
@@ -5020,14 +5142,14 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5020
5142
|
title: "Save Webhook Subscription",
|
|
5021
5143
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
5022
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": "..."}).`,
|
|
5023
|
-
inputSchema:
|
|
5024
|
-
id:
|
|
5025
|
-
name:
|
|
5026
|
-
webhookUrl:
|
|
5027
|
-
subscribedEvents:
|
|
5028
|
-
payloadFormat:
|
|
5029
|
-
secret:
|
|
5030
|
-
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")
|
|
5031
5153
|
})
|
|
5032
5154
|
},
|
|
5033
5155
|
async ({ ...body }) => callApi("POST", "webhook-subscriptions", { body })
|
|
@@ -5038,8 +5160,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5038
5160
|
title: "Delete Webhook Subscription",
|
|
5039
5161
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
5040
5162
|
description: "Delete a webhook subscription by ID.",
|
|
5041
|
-
inputSchema:
|
|
5042
|
-
id:
|
|
5163
|
+
inputSchema: z14.object({
|
|
5164
|
+
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
5043
5165
|
})
|
|
5044
5166
|
},
|
|
5045
5167
|
async ({ id }) => callApi("DELETE", `webhook-subscriptions/${id}`)
|
|
@@ -5050,8 +5172,8 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5050
5172
|
title: "Test Webhook Subscription",
|
|
5051
5173
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
5052
5174
|
description: "Send a test event to verify webhook connectivity. Returns the HTTP status code and success/failure message.",
|
|
5053
|
-
inputSchema:
|
|
5054
|
-
id:
|
|
5175
|
+
inputSchema: z14.object({
|
|
5176
|
+
id: z14.string().uuid().describe("Webhook subscription ID")
|
|
5055
5177
|
})
|
|
5056
5178
|
},
|
|
5057
5179
|
async ({ id }) => callApi("POST", `webhook-subscriptions/${id}/test`)
|
|
@@ -5059,7 +5181,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
|
|
|
5059
5181
|
}
|
|
5060
5182
|
|
|
5061
5183
|
// src/mcp/tools/domain-templates.ts
|
|
5062
|
-
import { z as
|
|
5184
|
+
import { z as z15 } from "zod";
|
|
5063
5185
|
function registerDomainTemplateTools(server, callApi) {
|
|
5064
5186
|
server.registerTool(
|
|
5065
5187
|
"lexq_domain_templates_list",
|
|
@@ -5067,7 +5189,7 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5067
5189
|
title: "List Domain Templates",
|
|
5068
5190
|
annotations: { readOnlyHint: true },
|
|
5069
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.",
|
|
5070
|
-
inputSchema:
|
|
5192
|
+
inputSchema: z15.object({})
|
|
5071
5193
|
},
|
|
5072
5194
|
async () => callApi("GET", "domain-templates")
|
|
5073
5195
|
);
|
|
@@ -5077,8 +5199,8 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5077
5199
|
title: "Preview Domain Template",
|
|
5078
5200
|
annotations: { readOnlyHint: true },
|
|
5079
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.",
|
|
5080
|
-
inputSchema:
|
|
5081
|
-
template:
|
|
5202
|
+
inputSchema: z15.object({
|
|
5203
|
+
template: z15.string().describe(
|
|
5082
5204
|
"Domain template key (e.g. ECOMMERCE). Use lexq_domain_templates_list to see the keys."
|
|
5083
5205
|
)
|
|
5084
5206
|
})
|
|
@@ -5091,9 +5213,9 @@ function registerDomainTemplateTools(server, callApi) {
|
|
|
5091
5213
|
title: "Apply Domain Template",
|
|
5092
5214
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
5093
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.",
|
|
5094
|
-
inputSchema:
|
|
5095
|
-
template:
|
|
5096
|
-
customName:
|
|
5216
|
+
inputSchema: z15.object({
|
|
5217
|
+
template: z15.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
|
|
5218
|
+
customName: z15.string().optional().describe(
|
|
5097
5219
|
"Optional custom name for the policy group that gets created. If omitted, the template's default name is used."
|
|
5098
5220
|
)
|
|
5099
5221
|
})
|
|
@@ -5129,7 +5251,7 @@ var SERVER_INFO = { name: "lexq", ...server_info_default };
|
|
|
5129
5251
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5130
5252
|
function getVersion() {
|
|
5131
5253
|
try {
|
|
5132
|
-
const pkg = JSON.parse(readFileSync3(
|
|
5254
|
+
const pkg = JSON.parse(readFileSync3(join3(__dirname, "..", "package.json"), "utf-8"));
|
|
5133
5255
|
return pkg.version;
|
|
5134
5256
|
} catch {
|
|
5135
5257
|
return "0.1.0";
|
|
@@ -5142,6 +5264,7 @@ function startMcpServer() {
|
|
|
5142
5264
|
() => {
|
|
5143
5265
|
const server = new McpServer({ ...SERVER_INFO, version });
|
|
5144
5266
|
registerAllTools(server, callApi);
|
|
5267
|
+
registerLocalTools(server);
|
|
5145
5268
|
return server;
|
|
5146
5269
|
},
|
|
5147
5270
|
{
|
|
@@ -5159,7 +5282,7 @@ function startMcpServer() {
|
|
|
5159
5282
|
function registerServeCommand(program) {
|
|
5160
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(
|
|
5161
5284
|
"after",
|
|
5162
|
-
|
|
5285
|
+
dedent19`
|
|
5163
5286
|
|
|
5164
5287
|
Example:
|
|
5165
5288
|
$ lexq serve --mcp
|
|
@@ -5190,11 +5313,11 @@ function registerServeCommand(program) {
|
|
|
5190
5313
|
|
|
5191
5314
|
// src/commands/domain-templates.ts
|
|
5192
5315
|
import "commander";
|
|
5193
|
-
import
|
|
5316
|
+
import dedent20 from "dedent";
|
|
5194
5317
|
function registerDomainTemplateCommands(program) {
|
|
5195
5318
|
const templates = program.command("domain-templates").description("Browse and apply domain templates").addHelpText(
|
|
5196
5319
|
"after",
|
|
5197
|
-
|
|
5320
|
+
dedent20`
|
|
5198
5321
|
|
|
5199
5322
|
A domain template is an industry-specific starter pack of fact
|
|
5200
5323
|
definitions and sample rules. Applying one provisions a ready-to-use
|
|
@@ -5242,7 +5365,7 @@ function registerDomainTemplateCommands(program) {
|
|
|
5242
5365
|
});
|
|
5243
5366
|
templates.command("preview").description("Preview what a domain template provisions").requiredOption("--template <key>", "Domain template key (e.g. ECOMMERCE)").addHelpText(
|
|
5244
5367
|
"after",
|
|
5245
|
-
|
|
5368
|
+
dedent20`
|
|
5246
5369
|
|
|
5247
5370
|
Read-only dry run — shows the fact definitions and sample rules the
|
|
5248
5371
|
template will create. Nothing is provisioned.
|
|
@@ -5271,7 +5394,7 @@ function registerDomainTemplateCommands(program) {
|
|
|
5271
5394
|
});
|
|
5272
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(
|
|
5273
5396
|
"after",
|
|
5274
|
-
|
|
5397
|
+
dedent20`
|
|
5275
5398
|
|
|
5276
5399
|
Creates the template's fact definitions and a new DRAFT policy group
|
|
5277
5400
|
populated with its sample rules. A fact that already exists with the
|
|
@@ -5325,7 +5448,7 @@ function registerDomainTemplateCommands(program) {
|
|
|
5325
5448
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
5326
5449
|
function getVersion2() {
|
|
5327
5450
|
try {
|
|
5328
|
-
const pkg = JSON.parse(readFileSync4(
|
|
5451
|
+
const pkg = JSON.parse(readFileSync4(join4(__dirname2, "..", "package.json"), "utf-8"));
|
|
5329
5452
|
return pkg.version;
|
|
5330
5453
|
} catch {
|
|
5331
5454
|
return "0.0.0";
|