@lexq/cli 0.3.0 → 0.4.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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > your deploy pipeline, prove every change with Impact Simulation, and ship without
6
6
  > a git push.
7
7
 
8
- **[Website](https://lexq.io)** · **[Docs](https://docs.lexq.io)** · **[Console](https://console.lexq.io)**
8
+ **[Website](https://lexq.io)** · **[Docs](https://docs.lexq.io)** · **[Console](https://console.lexq.io)** · **[MCP Registry](https://registry.modelcontextprotocol.io)**
9
9
 
10
10
  [![npm](https://img.shields.io/npm/v/@lexq/cli)](https://www.npmjs.com/package/@lexq/cli)
11
11
  [![License](https://img.shields.io/github/license/lexq-io/lexq-cli)](LICENSE)
@@ -146,7 +146,27 @@ Add to `claude_desktop_config.json`:
146
146
  ```
147
147
 
148
148
  > **Prerequisite:** `lexq auth login` must have been run once to store an
149
- > API key in `~/.lexq/config.json`.
149
+ > API key in `~/.lexq/config.json`, or `LEXQ_API_KEY` must be set in the client's
150
+ > environment block.
151
+
152
+ ### Registry
153
+
154
+ The server is listed on the official MCP Registry as **`io.lexq/lexq`**. Clients that
155
+ browse the registry find both the npm package and the hosted endpoint in that one entry.
156
+
157
+ ## Protocol Support
158
+
159
+ The local stdio server and the hosted Streamable HTTP endpoint both serve two MCP
160
+ revisions from one set of tool definitions, so a client sees the same tools either way.
161
+
162
+ | Revision | How a client reaches it |
163
+ |----------|-------------------------|
164
+ | `2026-07-28` | A per-request `_meta` envelope. No handshake. |
165
+ | `2025-11-25` | The `initialize` handshake. |
166
+
167
+ There is nothing to configure. The server answers in whichever revision the opening message
168
+ used, so a client can move to 2026-07-28 on its own schedule and one that has not moved
169
+ keeps working.
150
170
 
151
171
  ## AI Agent Skills
152
172
 
@@ -222,6 +242,21 @@ Config is stored at `~/.lexq/config.json`:
222
242
  }
223
243
  ```
224
244
 
245
+ The file holds an API key in plain text, so it is written `0600` inside a `0700` directory —
246
+ readable by its owner and nobody else on the machine. An install created by an older version
247
+ is repaired on the next save.
248
+
249
+ ### Environment variables
250
+
251
+ These are read by the MCP server (`lexq serve --mcp`) and take precedence over the stored
252
+ config. They let an editor or CI target an environment through the client's own config block,
253
+ without writing to `~/.lexq/config.json`. CLI commands use `--api-key` and `--base-url` instead.
254
+
255
+ | Variable | Overrides |
256
+ |----------|-----------|
257
+ | `LEXQ_API_KEY` | `apiKey` |
258
+ | `PARTNER_BASE_URL` | `baseUrl` |
259
+
225
260
  ## Development
226
261
 
227
262
  ```bash
@@ -235,10 +270,31 @@ pnpm start -- groups list
235
270
  ```bash
236
271
  pnpm typecheck # Type check
237
272
  pnpm lint # ESLint
273
+ pnpm build # Bundle to dist/
274
+ ```
275
+
276
+ Checks that guard a stated fact against the thing that owns it. All of them run in CI.
277
+
278
+ ```bash
279
+ pnpm enums:check # Generated enums match the engine contract
280
+ pnpm constants:check # Numbers written in prose match the contract
281
+ pnpm ab-key:check # Tool descriptions still name the A/B traffic key
282
+ pnpm surface:check # Nothing internal reached a public file
283
+ pnpm registry:check # server.json agrees with package.json
284
+ pnpm schemas:check # tools/list matches the recorded snapshot (needs a build)
238
285
  pnpm test:fact-key # Fact key grammar
239
286
  pnpm test:decimals # Metric precision display
287
+ pnpm test:export-format # Export byte fidelity
288
+ pnpm protocol:check # README matches the revisions the server serves
240
289
  ```
241
290
 
291
+ ## Security
292
+
293
+ Report a vulnerability through
294
+ [GitHub's private reporting form](https://github.com/lexq-io/lexq-cli/security/advisories/new).
295
+ The policy, including what is in scope, is in
296
+ [SECURITY.md](https://github.com/lexq-io/lexq-cli/blob/main/SECURITY.md).
297
+
242
298
  ## License
243
299
 
244
300
  [Apache-2.0](LICENSE)
package/dist/index.js CHANGED
@@ -3421,6 +3421,7 @@ function registerStatusTools(server, callApi) {
3421
3421
  "lexq_whoami",
3422
3422
  {
3423
3423
  title: "Who Am I",
3424
+ annotations: { readOnlyHint: true },
3424
3425
  description: "Show current authentication info (tenant ID, user ID, role).",
3425
3426
  inputSchema: z.object({})
3426
3427
  },
@@ -3435,6 +3436,7 @@ function registerGroupTools(server, callApi) {
3435
3436
  "lexq_groups_list",
3436
3437
  {
3437
3438
  title: "List Policy Groups",
3439
+ annotations: { readOnlyHint: true },
3438
3440
  description: "List all policy groups (tenant-wide, priority ASC).",
3439
3441
  inputSchema: z2.object({})
3440
3442
  },
@@ -3444,6 +3446,7 @@ function registerGroupTools(server, callApi) {
3444
3446
  "lexq_groups_get",
3445
3447
  {
3446
3448
  title: "Get Policy Group",
3449
+ annotations: { readOnlyHint: true },
3447
3450
  description: "Get a single policy group by ID.",
3448
3451
  inputSchema: z2.object({
3449
3452
  groupId: z2.string().uuid().describe("Policy group ID")
@@ -3455,6 +3458,7 @@ function registerGroupTools(server, callApi) {
3455
3458
  "lexq_groups_create",
3456
3459
  {
3457
3460
  title: "Create Policy Group",
3461
+ annotations: { readOnlyHint: false, destructiveHint: false },
3458
3462
  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.",
3459
3463
  inputSchema: z2.object({
3460
3464
  name: z2.string().describe("Group name (unique among non-ARCHIVED)"),
@@ -3485,6 +3489,7 @@ function registerGroupTools(server, callApi) {
3485
3489
  "lexq_groups_update",
3486
3490
  {
3487
3491
  title: "Update Policy Group",
3492
+ annotations: { readOnlyHint: false, destructiveHint: true },
3488
3493
  description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged.",
3489
3494
  inputSchema: z2.object({
3490
3495
  groupId: z2.string().uuid().describe("Policy group ID"),
@@ -3507,6 +3512,7 @@ function registerGroupTools(server, callApi) {
3507
3512
  "lexq_groups_delete",
3508
3513
  {
3509
3514
  title: "Delete Policy Group",
3515
+ annotations: { readOnlyHint: false, destructiveHint: true },
3510
3516
  description: "Archive a policy group. Only non-live groups can be deleted. This is irreversible.",
3511
3517
  inputSchema: z2.object({
3512
3518
  groupId: z2.string().uuid().describe("Policy group ID")
@@ -3518,6 +3524,7 @@ function registerGroupTools(server, callApi) {
3518
3524
  "lexq_groups_reorder",
3519
3525
  {
3520
3526
  title: "Reorder Policy Groups",
3527
+ annotations: { readOnlyHint: false, destructiveHint: true },
3521
3528
  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.",
3522
3529
  inputSchema: z2.object({
3523
3530
  groupIds: z2.array(z2.string().uuid()).describe("Group IDs in desired priority order (index 0 = priority 1)")
@@ -3535,6 +3542,7 @@ function registerGroupTools(server, callApi) {
3535
3542
  "lexq_ab_test_start",
3536
3543
  {
3537
3544
  title: "Start A/B Test",
3545
+ annotations: { readOnlyHint: false, destructiveHint: false },
3538
3546
  description: "Start an A/B test on a policy group. Requires a challenger version ID and traffic rate. 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%.",
3539
3547
  inputSchema: z2.object({
3540
3548
  groupId: z2.string().uuid().describe("Policy group ID"),
@@ -3548,6 +3556,7 @@ function registerGroupTools(server, callApi) {
3548
3556
  "lexq_ab_test_stop",
3549
3557
  {
3550
3558
  title: "Stop A/B Test",
3559
+ annotations: { readOnlyHint: false, destructiveHint: true },
3551
3560
  description: "Stop a running A/B test. All traffic is restored to the control (current) version.",
3552
3561
  inputSchema: z2.object({
3553
3562
  groupId: z2.string().uuid().describe("Policy group ID")
@@ -3559,6 +3568,7 @@ function registerGroupTools(server, callApi) {
3559
3568
  "lexq_ab_test_adjust",
3560
3569
  {
3561
3570
  title: "Adjust A/B Test",
3571
+ annotations: { readOnlyHint: false, destructiveHint: true },
3562
3572
  description: "Adjust traffic rate of a running A/B test.",
3563
3573
  inputSchema: z2.object({
3564
3574
  groupId: z2.string().uuid().describe("Policy group ID"),
@@ -3576,6 +3586,7 @@ function registerVersionTools(server, callApi) {
3576
3586
  "lexq_versions_list",
3577
3587
  {
3578
3588
  title: "List Policy Versions",
3589
+ annotations: { readOnlyHint: true },
3579
3590
  description: "List all versions of a policy group.",
3580
3591
  inputSchema: z3.object({
3581
3592
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -3591,6 +3602,7 @@ function registerVersionTools(server, callApi) {
3591
3602
  "lexq_versions_get",
3592
3603
  {
3593
3604
  title: "Get Policy Version",
3605
+ annotations: { readOnlyHint: true },
3594
3606
  description: "Get a single version by ID, including its rules and fact requirements.",
3595
3607
  inputSchema: z3.object({
3596
3608
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -3603,6 +3615,7 @@ function registerVersionTools(server, callApi) {
3603
3615
  "lexq_versions_create",
3604
3616
  {
3605
3617
  title: "Create Policy Version",
3618
+ annotations: { readOnlyHint: false, destructiveHint: false },
3606
3619
  description: "Create a new DRAFT version in a policy group. Optionally provide a commit message and effective date range.",
3607
3620
  inputSchema: z3.object({
3608
3621
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -3617,6 +3630,7 @@ function registerVersionTools(server, callApi) {
3617
3630
  "lexq_versions_update",
3618
3631
  {
3619
3632
  title: "Update Policy Version",
3633
+ annotations: { readOnlyHint: false, destructiveHint: true },
3620
3634
  description: "Update a DRAFT version. Only DRAFT versions can be modified. Only provided fields are changed.",
3621
3635
  inputSchema: z3.object({
3622
3636
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -3632,6 +3646,7 @@ function registerVersionTools(server, callApi) {
3632
3646
  "lexq_versions_delete",
3633
3647
  {
3634
3648
  title: "Delete Policy Version",
3649
+ annotations: { readOnlyHint: false, destructiveHint: true },
3635
3650
  description: "Delete a DRAFT version. Only DRAFT versions can be deleted.",
3636
3651
  inputSchema: z3.object({
3637
3652
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -3644,6 +3659,7 @@ function registerVersionTools(server, callApi) {
3644
3659
  "lexq_versions_clone",
3645
3660
  {
3646
3661
  title: "Clone Policy Version",
3662
+ annotations: { readOnlyHint: false, destructiveHint: false },
3647
3663
  description: "Clone an existing version to create a new DRAFT. Useful when the source version is already published.",
3648
3664
  inputSchema: z3.object({
3649
3665
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -3666,6 +3682,7 @@ function registerRuleTools(server, callApi) {
3666
3682
  "lexq_rules_list",
3667
3683
  {
3668
3684
  title: "List Rules",
3685
+ annotations: { readOnlyHint: true },
3669
3686
  description: "List all rules in a version (priority ASC). Returns summary with conditionSummary and actionSummary.",
3670
3687
  inputSchema: z4.object({
3671
3688
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -3678,6 +3695,7 @@ function registerRuleTools(server, callApi) {
3678
3695
  "lexq_rules_get",
3679
3696
  {
3680
3697
  title: "Get Rule Detail",
3698
+ annotations: { readOnlyHint: true },
3681
3699
  description: "Get full rule detail including condition tree and action definitions.",
3682
3700
  inputSchema: z4.object({
3683
3701
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -3691,6 +3709,7 @@ function registerRuleTools(server, callApi) {
3691
3709
  "lexq_rules_create",
3692
3710
  {
3693
3711
  title: "Create Rule",
3712
+ annotations: { readOnlyHint: false, destructiveHint: false },
3694
3713
  description: dedent15`
3695
3714
  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.
3696
3715
 
@@ -3769,6 +3788,7 @@ function registerRuleTools(server, callApi) {
3769
3788
  "lexq_rules_update",
3770
3789
  {
3771
3790
  title: "Update Rule",
3791
+ annotations: { readOnlyHint: false, destructiveHint: true },
3772
3792
  description: "Update an existing rule in a DRAFT version. Only provided fields are changed.",
3773
3793
  inputSchema: z4.object({
3774
3794
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -3790,6 +3810,7 @@ function registerRuleTools(server, callApi) {
3790
3810
  "lexq_rules_delete",
3791
3811
  {
3792
3812
  title: "Delete Rule",
3813
+ annotations: { readOnlyHint: false, destructiveHint: true },
3793
3814
  description: "Delete a rule from a DRAFT version.",
3794
3815
  inputSchema: z4.object({
3795
3816
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -3810,6 +3831,7 @@ function registerRuleTools(server, callApi) {
3810
3831
  "lexq_rules_reorder",
3811
3832
  {
3812
3833
  title: "Reorder Rules",
3834
+ annotations: { readOnlyHint: false, destructiveHint: true },
3813
3835
  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).",
3814
3836
  inputSchema: z4.object({
3815
3837
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -3831,6 +3853,7 @@ function registerRuleTools(server, callApi) {
3831
3853
  "lexq_rules_toggle",
3832
3854
  {
3833
3855
  title: "Toggle Rule",
3856
+ annotations: { readOnlyHint: false, destructiveHint: true },
3834
3857
  description: "Enable or disable a rule without deleting it.",
3835
3858
  inputSchema: z4.object({
3836
3859
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -3858,6 +3881,7 @@ function registerFactTools(server, callApi) {
3858
3881
  "lexq_facts_list",
3859
3882
  {
3860
3883
  title: "List Fact Definitions",
3884
+ annotations: { readOnlyHint: true },
3861
3885
  description: "List all fact definitions (input variable schema). Shows key, type, required, and PII status. Always check this before creating rules.",
3862
3886
  inputSchema: z5.object({
3863
3887
  page: z5.number().int().min(0).default(0).describe("Page number"),
@@ -3875,6 +3899,7 @@ function registerFactTools(server, callApi) {
3875
3899
  "lexq_facts_create",
3876
3900
  {
3877
3901
  title: "Create Fact Definition",
3902
+ annotations: { readOnlyHint: false, destructiveHint: false },
3878
3903
  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.",
3879
3904
  inputSchema: z5.object({
3880
3905
  key: z5.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
@@ -3893,6 +3918,7 @@ function registerFactTools(server, callApi) {
3893
3918
  "lexq_facts_update",
3894
3919
  {
3895
3920
  title: "Update Fact Definition",
3921
+ annotations: { readOnlyHint: false, destructiveHint: true },
3896
3922
  description: "Update a fact definition. The key is immutable. The type can change only while no rule references the fact; if any does, the call fails with FD-007 and reports the count. Only the fields you send are changed. System facts accept name, description, and PII only.",
3897
3923
  inputSchema: z5.object({
3898
3924
  factId: z5.string().uuid().describe("Fact definition ID"),
@@ -3911,6 +3937,7 @@ function registerFactTools(server, callApi) {
3911
3937
  "lexq_facts_delete",
3912
3938
  {
3913
3939
  title: "Delete Fact Definition",
3940
+ annotations: { readOnlyHint: false, destructiveHint: true },
3914
3941
  description: "Delete a fact definition. System facts cannot be deleted. Neither can a fact that any rule references: that call fails with FD-006 and reports the count. Remove the references first.",
3915
3942
  inputSchema: z5.object({
3916
3943
  factId: z5.string().uuid().describe("Fact definition ID")
@@ -3922,6 +3949,7 @@ function registerFactTools(server, callApi) {
3922
3949
  "lexq_facts_action_metadata",
3923
3950
  {
3924
3951
  title: "Get Action Runtime Fact Metadata",
3952
+ annotations: { readOnlyHint: true },
3925
3953
  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. Facts are supplied as input or written by a prior action in the same rule; only SET_FACT creates a fact from nothing. Static data, safe to cache in-session.",
3926
3954
  inputSchema: z5.object({})
3927
3955
  },
@@ -3931,6 +3959,7 @@ function registerFactTools(server, callApi) {
3931
3959
  "lexq_facts_unregistered",
3932
3960
  {
3933
3961
  title: "List Unregistered Facts",
3962
+ annotations: { readOnlyHint: true },
3934
3963
  description: "List facts referenced by a version's rules but not yet defined (read-only \u2014 does not block publish/deploy, INV-4). 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.",
3935
3964
  inputSchema: z5.object({
3936
3965
  groupId: z5.string().uuid().describe("Policy group ID"),
@@ -3943,6 +3972,7 @@ function registerFactTools(server, callApi) {
3943
3972
  "lexq_facts_export",
3944
3973
  {
3945
3974
  title: "Export Fact Catalog",
3975
+ annotations: { readOnlyHint: true },
3946
3976
  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.",
3947
3977
  inputSchema: z5.object({
3948
3978
  format: z5.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
@@ -3962,6 +3992,7 @@ function registerDeployTools(server, callApi) {
3962
3992
  "lexq_deploy_publish",
3963
3993
  {
3964
3994
  title: "Publish Version",
3995
+ annotations: { readOnlyHint: false, destructiveHint: false },
3965
3996
  description: "Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts referenced by rules do not block publishing (INV-4); call lexq_facts_unregistered first to review them.",
3966
3997
  inputSchema: z6.object({
3967
3998
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -3975,6 +4006,7 @@ function registerDeployTools(server, callApi) {
3975
4006
  "lexq_deploy_live",
3976
4007
  {
3977
4008
  title: "Deploy to Live",
4009
+ annotations: { readOnlyHint: false, destructiveHint: true },
3978
4010
  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. Undefined facts do not block deployment (INV-4); use lexq_facts_unregistered to review what the version references but has not defined.",
3979
4011
  inputSchema: z6.object({
3980
4012
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -3990,6 +4022,7 @@ function registerDeployTools(server, callApi) {
3990
4022
  "lexq_deploy_rollback",
3991
4023
  {
3992
4024
  title: "Rollback Deployment",
4025
+ annotations: { readOnlyHint: false, destructiveHint: true },
3993
4026
  description: "Rollback to the previous deployed version. Only available if there is a previous version.",
3994
4027
  inputSchema: z6.object({
3995
4028
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -4004,6 +4037,7 @@ function registerDeployTools(server, callApi) {
4004
4037
  "lexq_deploy_undeploy",
4005
4038
  {
4006
4039
  title: "Undeploy",
4040
+ annotations: { readOnlyHint: false, destructiveHint: true },
4007
4041
  description: "Remove the live version from traffic. The version stays ACTIVE but no longer serves requests.",
4008
4042
  inputSchema: z6.object({
4009
4043
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -4018,6 +4052,7 @@ function registerDeployTools(server, callApi) {
4018
4052
  "lexq_deploy_schedule",
4019
4053
  {
4020
4054
  title: "Schedule Deployment",
4055
+ annotations: { readOnlyHint: false, destructiveHint: false },
4021
4056
  description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). 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).",
4022
4057
  inputSchema: z6.object({
4023
4058
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -4033,6 +4068,7 @@ function registerDeployTools(server, callApi) {
4033
4068
  "lexq_deploy_unschedule",
4034
4069
  {
4035
4070
  title: "Cancel Scheduled Deployment",
4071
+ annotations: { readOnlyHint: false, destructiveHint: true },
4036
4072
  description: "Cancel the pending scheduled deployment for a group. The version itself is not affected. Fails with P-039 if no pending schedule exists.",
4037
4073
  inputSchema: z6.object({
4038
4074
  groupId: z6.string().uuid().describe("Policy group ID")
@@ -4044,6 +4080,7 @@ function registerDeployTools(server, callApi) {
4044
4080
  "lexq_deploy_schedules",
4045
4081
  {
4046
4082
  title: "List Scheduled Deployments",
4083
+ annotations: { readOnlyHint: true },
4047
4084
  description: "List scheduled deployments across all groups (all statuses: PENDING, EXECUTED, CANCELED, FAILED), newest first.",
4048
4085
  inputSchema: z6.object({
4049
4086
  page: z6.number().int().min(0).default(0).describe("Page number"),
@@ -4056,6 +4093,7 @@ function registerDeployTools(server, callApi) {
4056
4093
  "lexq_deploy_history",
4057
4094
  {
4058
4095
  title: "Deployment History",
4096
+ annotations: { readOnlyHint: true },
4059
4097
  description: "List deployment history across all groups.",
4060
4098
  inputSchema: z6.object({
4061
4099
  page: z6.number().int().min(0).default(0).describe("Page number"),
@@ -4079,6 +4117,7 @@ function registerDeployTools(server, callApi) {
4079
4117
  "lexq_deploy_detail",
4080
4118
  {
4081
4119
  title: "Deployment Detail",
4120
+ annotations: { readOnlyHint: true },
4082
4121
  description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
4083
4122
  inputSchema: z6.object({
4084
4123
  deploymentId: z6.string().uuid().describe("Deployment ID")
@@ -4090,6 +4129,7 @@ function registerDeployTools(server, callApi) {
4090
4129
  "lexq_deploy_overview",
4091
4130
  {
4092
4131
  title: "Deployment Overview",
4132
+ annotations: { readOnlyHint: true },
4093
4133
  description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer.",
4094
4134
  inputSchema: z6.object({})
4095
4135
  },
@@ -4099,6 +4139,7 @@ function registerDeployTools(server, callApi) {
4099
4139
  "lexq_deploy_deployable",
4100
4140
  {
4101
4141
  title: "List Deployable Versions",
4142
+ annotations: { readOnlyHint: true },
4102
4143
  description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
4103
4144
  inputSchema: z6.object({
4104
4145
  groupId: z6.string().uuid().describe("Policy group ID")
@@ -4110,6 +4151,7 @@ function registerDeployTools(server, callApi) {
4110
4151
  "lexq_deploy_diff",
4111
4152
  {
4112
4153
  title: "Deployment Diff",
4154
+ annotations: { readOnlyHint: true },
4113
4155
  description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
4114
4156
  inputSchema: z6.object({
4115
4157
  baseVersionId: z6.string().uuid().describe("Base version ID (typically the current live)"),
@@ -4130,6 +4172,7 @@ function registerAnalyticsTools(server, callApi) {
4130
4172
  "lexq_dry_run",
4131
4173
  {
4132
4174
  title: "Dry Run",
4175
+ annotations: { readOnlyHint: true },
4133
4176
  description: dedent16`
4134
4177
  Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
4135
4178
 
@@ -4159,6 +4202,7 @@ function registerAnalyticsTools(server, callApi) {
4159
4202
  "lexq_dry_run_compare",
4160
4203
  {
4161
4204
  title: "Dry Run Compare",
4205
+ annotations: { readOnlyHint: true },
4162
4206
  description: dedent16`
4163
4207
  Compare dry run results between two versions using the same input facts. Useful for validating changes.
4164
4208
 
@@ -4183,6 +4227,7 @@ function registerAnalyticsTools(server, callApi) {
4183
4227
  "lexq_requirements",
4184
4228
  {
4185
4229
  title: "Analyze Requirements",
4230
+ annotations: { readOnlyHint: true },
4186
4231
  description: "Analyze which input facts a version requires. Returns required keys, types, and an example request body.",
4187
4232
  inputSchema: z7.object({
4188
4233
  groupId: z7.string().uuid().describe("Policy group ID"),
@@ -4195,6 +4240,7 @@ function registerAnalyticsTools(server, callApi) {
4195
4240
  "lexq_simulation_start",
4196
4241
  {
4197
4242
  title: "Start Simulation",
4243
+ annotations: { readOnlyHint: false, destructiveHint: false },
4198
4244
  description: dedent16`
4199
4245
  Start an Impact Simulation against historical, uploaded, or inline data.
4200
4246
 
@@ -4236,6 +4282,7 @@ function registerAnalyticsTools(server, callApi) {
4236
4282
  "lexq_simulation_status",
4237
4283
  {
4238
4284
  title: "Simulation Status",
4285
+ annotations: { readOnlyHint: true },
4239
4286
  description: "Get simulation status and results. Poll until status is COMPLETED or FAILED.",
4240
4287
  inputSchema: z7.object({
4241
4288
  simulationId: z7.string().uuid().describe("Simulation ID")
@@ -4247,6 +4294,7 @@ function registerAnalyticsTools(server, callApi) {
4247
4294
  "lexq_simulation_list",
4248
4295
  {
4249
4296
  title: "List Simulations",
4297
+ annotations: { readOnlyHint: true },
4250
4298
  description: "List simulation history with optional filters.",
4251
4299
  inputSchema: z7.object({
4252
4300
  page: z7.number().int().min(0).default(0).describe("Page number"),
@@ -4268,6 +4316,7 @@ function registerAnalyticsTools(server, callApi) {
4268
4316
  "lexq_simulation_cancel",
4269
4317
  {
4270
4318
  title: "Cancel Simulation",
4319
+ annotations: { readOnlyHint: false, destructiveHint: true },
4271
4320
  description: "Cancel a running or pending simulation.",
4272
4321
  inputSchema: z7.object({
4273
4322
  simulationId: z7.string().uuid().describe("Simulation ID")
@@ -4279,6 +4328,7 @@ function registerAnalyticsTools(server, callApi) {
4279
4328
  "lexq_simulation_export",
4280
4329
  {
4281
4330
  title: "Export Simulation",
4331
+ annotations: { readOnlyHint: true },
4282
4332
  description: "Export simulation results as JSON or CSV. Returns the raw data.",
4283
4333
  inputSchema: z7.object({
4284
4334
  simulationId: z7.string().uuid().describe("Simulation ID"),
@@ -4293,6 +4343,7 @@ function registerAnalyticsTools(server, callApi) {
4293
4343
  "lexq_dataset_upload",
4294
4344
  {
4295
4345
  title: "Upload Dataset",
4346
+ annotations: { readOnlyHint: false, destructiveHint: false },
4296
4347
  description: dedent16`
4297
4348
  Upload inline CSV or JSON content as a simulation dataset.
4298
4349
  The content is uploaded to S3 and a path is returned in the "path" field.
@@ -4336,6 +4387,7 @@ function registerAnalyticsTools(server, callApi) {
4336
4387
  "lexq_dataset_template",
4337
4388
  {
4338
4389
  title: "Download Dataset Template",
4390
+ annotations: { readOnlyHint: true },
4339
4391
  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.",
4340
4392
  inputSchema: z7.object({
4341
4393
  groupId: z7.string().uuid().describe("Policy group ID"),
@@ -4365,6 +4417,7 @@ function registerProfileTools(server, callApi) {
4365
4417
  "lexq_profile_overview",
4366
4418
  {
4367
4419
  title: "Group Latency Profile",
4420
+ annotations: { readOnlyHint: true },
4368
4421
  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.`,
4369
4422
  inputSchema: z8.object({
4370
4423
  groupId: z8.string().uuid().describe("Policy group ID"),
@@ -4384,6 +4437,7 @@ function registerProfileTools(server, callApi) {
4384
4437
  "lexq_profile_rule",
4385
4438
  {
4386
4439
  title: "Rule Latency Detail",
4440
+ annotations: { readOnlyHint: true },
4387
4441
  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,
4388
4442
  inputSchema: z8.object({
4389
4443
  groupId: z8.string().uuid().describe("Policy group ID"),
@@ -4408,6 +4462,7 @@ function registerReplayTools(server, callApi) {
4408
4462
  "lexq_replay_decision",
4409
4463
  {
4410
4464
  title: "Replay a Decision",
4465
+ annotations: { readOnlyHint: true },
4411
4466
  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.",
4412
4467
  inputSchema: z9.object({
4413
4468
  traceId: z9.string().describe("Trace ID of the past execution to replay"),
@@ -4420,6 +4475,7 @@ function registerReplayTools(server, callApi) {
4420
4475
  "lexq_replay_start",
4421
4476
  {
4422
4477
  title: "Start Window Replay (Blast Radius)",
4478
+ annotations: { readOnlyHint: false, destructiveHint: false },
4423
4479
  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.",
4424
4480
  inputSchema: z9.object({
4425
4481
  candidateVersionId: z9.string().uuid().describe("Version to re-evaluate against"),
@@ -4436,6 +4492,7 @@ function registerReplayTools(server, callApi) {
4436
4492
  "lexq_replay_status",
4437
4493
  {
4438
4494
  title: "Get Replay Job Status",
4495
+ annotations: { readOnlyHint: true },
4439
4496
  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.",
4440
4497
  inputSchema: z9.object({
4441
4498
  jobId: z9.string().describe("Replay job ID from lexq_replay_start")
@@ -4447,6 +4504,7 @@ function registerReplayTools(server, callApi) {
4447
4504
  "lexq_replay_list",
4448
4505
  {
4449
4506
  title: "List Replay Jobs",
4507
+ annotations: { readOnlyHint: true },
4450
4508
  description: "List window replay job history (reverse-chronological). Lightweight items \u2014 use lexq_replay_status for summary and changed samples.",
4451
4509
  inputSchema: z9.object({
4452
4510
  page: z9.number().int().min(0).default(0).describe("Page number"),
@@ -4459,6 +4517,7 @@ function registerReplayTools(server, callApi) {
4459
4517
  "lexq_replay_cancel",
4460
4518
  {
4461
4519
  title: "Cancel Replay Job",
4520
+ annotations: { readOnlyHint: false, destructiveHint: true },
4462
4521
  description: "Cooperatively cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
4463
4522
  inputSchema: z9.object({
4464
4523
  jobId: z9.string().describe("Replay job ID")
@@ -4470,6 +4529,7 @@ function registerReplayTools(server, callApi) {
4470
4529
  "lexq_replay_export",
4471
4530
  {
4472
4531
  title: "Export Replay Result",
4532
+ annotations: { readOnlyHint: true },
4473
4533
  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.",
4474
4534
  inputSchema: z9.object({
4475
4535
  jobId: z9.string().describe("Replay job ID"),
@@ -4487,6 +4547,7 @@ function registerHistoryTools(server, callApi) {
4487
4547
  "lexq_history_list",
4488
4548
  {
4489
4549
  title: "List Execution History",
4550
+ annotations: { readOnlyHint: true },
4490
4551
  description: "List policy execution history. Shows trace ID, group, version, status, match result, and latency.",
4491
4552
  inputSchema: z10.object({
4492
4553
  page: z10.number().int().min(0).default(0).describe("Page number"),
@@ -4514,6 +4575,7 @@ function registerHistoryTools(server, callApi) {
4514
4575
  "lexq_history_get",
4515
4576
  {
4516
4577
  title: "Get Execution Detail",
4578
+ annotations: { readOnlyHint: true },
4517
4579
  description: "Get full execution detail including inputFacts, mutatedFacts, generatedVariables, executionTraces, and decisionTraces.",
4518
4580
  inputSchema: z10.object({
4519
4581
  traceId: z10.string().describe("Trace ID from execution history")
@@ -4525,6 +4587,7 @@ function registerHistoryTools(server, callApi) {
4525
4587
  "lexq_history_stats",
4526
4588
  {
4527
4589
  title: "Execution Statistics",
4590
+ annotations: { readOnlyHint: true },
4528
4591
  description: "Get execution KPIs: total executions, success/failure counts, success rate, and average latency.",
4529
4592
  inputSchema: z10.object({
4530
4593
  groupId: z10.string().uuid().optional().describe("Filter by policy group"),
@@ -4549,6 +4612,7 @@ function registerProvenanceTools(server, callApi) {
4549
4612
  "lexq_provenance_get",
4550
4613
  {
4551
4614
  title: "Get Decision Provenance",
4615
+ annotations: { readOnlyHint: true },
4552
4616
  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.",
4553
4617
  inputSchema: z11.object({
4554
4618
  traceId: z11.string().describe("Trace ID of the execution")
@@ -4560,6 +4624,7 @@ function registerProvenanceTools(server, callApi) {
4560
4624
  "lexq_pii_reveals_list",
4561
4625
  {
4562
4626
  title: "List PII Reveal Audits",
4627
+ annotations: { readOnlyHint: true },
4563
4628
  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.",
4564
4629
  inputSchema: z11.object({
4565
4630
  page: z11.number().int().min(0).default(0).describe("Page number"),
@@ -4590,6 +4655,7 @@ function registerLogTools(server, callApi) {
4590
4655
  "lexq_logs_list",
4591
4656
  {
4592
4657
  title: "List Failure Logs",
4658
+ annotations: { readOnlyHint: true },
4593
4659
  description: "List system failure logs from background tasks (platform event webhooks, scheduled deployments).",
4594
4660
  inputSchema: z12.object({
4595
4661
  page: z12.number().int().min(0).default(0).describe("Page number"),
@@ -4615,6 +4681,7 @@ function registerLogTools(server, callApi) {
4615
4681
  "lexq_logs_get",
4616
4682
  {
4617
4683
  title: "Get Failure Log",
4684
+ annotations: { readOnlyHint: true },
4618
4685
  description: "Get failure log detail by ID.",
4619
4686
  inputSchema: z12.object({
4620
4687
  logId: z12.string().uuid().describe("Failure log ID")
@@ -4626,6 +4693,7 @@ function registerLogTools(server, callApi) {
4626
4693
  "lexq_logs_action",
4627
4694
  {
4628
4695
  title: "Process Failure Log",
4696
+ annotations: { readOnlyHint: false, destructiveHint: false },
4629
4697
  description: "Process a single failure log: RESOLVE (mark as manually fixed) or IGNORE (skip intentionally).",
4630
4698
  inputSchema: z12.object({
4631
4699
  logId: z12.string().uuid().describe("Failure log ID"),
@@ -4640,6 +4708,7 @@ function registerLogTools(server, callApi) {
4640
4708
  "lexq_logs_bulk_action",
4641
4709
  {
4642
4710
  title: "Bulk Process Failure Logs",
4711
+ annotations: { readOnlyHint: false, destructiveHint: false },
4643
4712
  description: "Process multiple failure logs at once. Provide an array of log IDs and the action.",
4644
4713
  inputSchema: z12.object({
4645
4714
  logIds: z12.array(z12.string().uuid()).describe("Array of failure log IDs"),
@@ -4659,6 +4728,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
4659
4728
  "lexq_webhook_subscriptions_list",
4660
4729
  {
4661
4730
  title: "List Webhook Subscriptions",
4731
+ annotations: { readOnlyHint: true },
4662
4732
  description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications (publish, deploy, rollback, undeploy).",
4663
4733
  inputSchema: z13.object({
4664
4734
  page: z13.number().int().min(0).default(0).describe("Page number"),
@@ -4674,6 +4744,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
4674
4744
  "lexq_webhook_subscriptions_get",
4675
4745
  {
4676
4746
  title: "Get Webhook Subscription",
4747
+ annotations: { readOnlyHint: true },
4677
4748
  description: "Get webhook subscription detail by ID.",
4678
4749
  inputSchema: z13.object({
4679
4750
  id: z13.string().uuid().describe("Webhook subscription ID")
@@ -4685,6 +4756,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
4685
4756
  "lexq_webhook_subscriptions_save",
4686
4757
  {
4687
4758
  title: "Save Webhook Subscription",
4759
+ annotations: { readOnlyHint: false, destructiveHint: true },
4688
4760
  description: 'Create or update a webhook subscription. Omit id to create, provide id to update. Events: VERSION_PUBLISHED, DEPLOYED, ROLLED_BACK, UNDEPLOYED. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).',
4689
4761
  inputSchema: z13.object({
4690
4762
  id: z13.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
@@ -4702,6 +4774,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
4702
4774
  "lexq_webhook_subscriptions_delete",
4703
4775
  {
4704
4776
  title: "Delete Webhook Subscription",
4777
+ annotations: { readOnlyHint: false, destructiveHint: true },
4705
4778
  description: "Delete a webhook subscription by ID.",
4706
4779
  inputSchema: z13.object({
4707
4780
  id: z13.string().uuid().describe("Webhook subscription ID")
@@ -4713,6 +4786,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
4713
4786
  "lexq_webhook_subscriptions_test",
4714
4787
  {
4715
4788
  title: "Test Webhook Subscription",
4789
+ annotations: { readOnlyHint: false, destructiveHint: false },
4716
4790
  description: "Send a test event to verify webhook connectivity. Returns the HTTP status code and success/failure message.",
4717
4791
  inputSchema: z13.object({
4718
4792
  id: z13.string().uuid().describe("Webhook subscription ID")
@@ -4729,6 +4803,7 @@ function registerDomainTemplateTools(server, callApi) {
4729
4803
  "lexq_domain_templates_list",
4730
4804
  {
4731
4805
  title: "List Domain Templates",
4806
+ annotations: { readOnlyHint: true },
4732
4807
  description: "List all domain templates. A domain template is a curated, industry-specific starter pack of fact definitions and sample rules (e.g. ECOMMERCE). Each entry reports its key, status (ACTIVE or COMING_SOON), and a summary of what it provisions. Call this before preview or apply to discover which templates can currently be applied.",
4733
4808
  inputSchema: z14.object({})
4734
4809
  },
@@ -4738,6 +4813,7 @@ function registerDomainTemplateTools(server, callApi) {
4738
4813
  "lexq_domain_templates_preview",
4739
4814
  {
4740
4815
  title: "Preview Domain Template",
4816
+ annotations: { readOnlyHint: true },
4741
4817
  description: "Preview exactly what a domain template will provision before applying it: the fact definitions it registers, the sample rules it creates, and an apply plan. This is a read-only dry run \u2014 nothing is created. Only ACTIVE templates can be previewed.",
4742
4818
  inputSchema: z14.object({
4743
4819
  template: z14.string().describe(
@@ -4751,6 +4827,7 @@ function registerDomainTemplateTools(server, callApi) {
4751
4827
  "lexq_domain_templates_apply",
4752
4828
  {
4753
4829
  title: "Apply Domain Template",
4830
+ annotations: { readOnlyHint: false, destructiveHint: false },
4754
4831
  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. Existing facts are skipped \u2014 apply is additive and never overwrites existing schema. Run lexq_domain_templates_preview first to review what will be created. Only ACTIVE templates can be applied.",
4755
4832
  inputSchema: z14.object({
4756
4833
  template: z14.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
@@ -5,6 +5,7 @@ function registerStatusTools(server, callApi) {
5
5
  "lexq_whoami",
6
6
  {
7
7
  title: "Who Am I",
8
+ annotations: { readOnlyHint: true },
8
9
  description: "Show current authentication info (tenant ID, user ID, role).",
9
10
  inputSchema: z.object({})
10
11
  },
@@ -39,6 +40,7 @@ function registerGroupTools(server, callApi) {
39
40
  "lexq_groups_list",
40
41
  {
41
42
  title: "List Policy Groups",
43
+ annotations: { readOnlyHint: true },
42
44
  description: "List all policy groups (tenant-wide, priority ASC).",
43
45
  inputSchema: z2.object({})
44
46
  },
@@ -48,6 +50,7 @@ function registerGroupTools(server, callApi) {
48
50
  "lexq_groups_get",
49
51
  {
50
52
  title: "Get Policy Group",
53
+ annotations: { readOnlyHint: true },
51
54
  description: "Get a single policy group by ID.",
52
55
  inputSchema: z2.object({
53
56
  groupId: z2.string().uuid().describe("Policy group ID")
@@ -59,6 +62,7 @@ function registerGroupTools(server, callApi) {
59
62
  "lexq_groups_create",
60
63
  {
61
64
  title: "Create Policy Group",
65
+ annotations: { readOnlyHint: false, destructiveHint: false },
62
66
  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.",
63
67
  inputSchema: z2.object({
64
68
  name: z2.string().describe("Group name (unique among non-ARCHIVED)"),
@@ -89,6 +93,7 @@ function registerGroupTools(server, callApi) {
89
93
  "lexq_groups_update",
90
94
  {
91
95
  title: "Update Policy Group",
96
+ annotations: { readOnlyHint: false, destructiveHint: true },
92
97
  description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged.",
93
98
  inputSchema: z2.object({
94
99
  groupId: z2.string().uuid().describe("Policy group ID"),
@@ -111,6 +116,7 @@ function registerGroupTools(server, callApi) {
111
116
  "lexq_groups_delete",
112
117
  {
113
118
  title: "Delete Policy Group",
119
+ annotations: { readOnlyHint: false, destructiveHint: true },
114
120
  description: "Archive a policy group. Only non-live groups can be deleted. This is irreversible.",
115
121
  inputSchema: z2.object({
116
122
  groupId: z2.string().uuid().describe("Policy group ID")
@@ -122,6 +128,7 @@ function registerGroupTools(server, callApi) {
122
128
  "lexq_groups_reorder",
123
129
  {
124
130
  title: "Reorder Policy Groups",
131
+ annotations: { readOnlyHint: false, destructiveHint: true },
125
132
  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.",
126
133
  inputSchema: z2.object({
127
134
  groupIds: z2.array(z2.string().uuid()).describe("Group IDs in desired priority order (index 0 = priority 1)")
@@ -139,6 +146,7 @@ function registerGroupTools(server, callApi) {
139
146
  "lexq_ab_test_start",
140
147
  {
141
148
  title: "Start A/B Test",
149
+ annotations: { readOnlyHint: false, destructiveHint: false },
142
150
  description: "Start an A/B test on a policy group. Requires a challenger version ID and traffic rate. 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%.",
143
151
  inputSchema: z2.object({
144
152
  groupId: z2.string().uuid().describe("Policy group ID"),
@@ -152,6 +160,7 @@ function registerGroupTools(server, callApi) {
152
160
  "lexq_ab_test_stop",
153
161
  {
154
162
  title: "Stop A/B Test",
163
+ annotations: { readOnlyHint: false, destructiveHint: true },
155
164
  description: "Stop a running A/B test. All traffic is restored to the control (current) version.",
156
165
  inputSchema: z2.object({
157
166
  groupId: z2.string().uuid().describe("Policy group ID")
@@ -163,6 +172,7 @@ function registerGroupTools(server, callApi) {
163
172
  "lexq_ab_test_adjust",
164
173
  {
165
174
  title: "Adjust A/B Test",
175
+ annotations: { readOnlyHint: false, destructiveHint: true },
166
176
  description: "Adjust traffic rate of a running A/B test.",
167
177
  inputSchema: z2.object({
168
178
  groupId: z2.string().uuid().describe("Policy group ID"),
@@ -238,6 +248,7 @@ function registerVersionTools(server, callApi) {
238
248
  "lexq_versions_list",
239
249
  {
240
250
  title: "List Policy Versions",
251
+ annotations: { readOnlyHint: true },
241
252
  description: "List all versions of a policy group.",
242
253
  inputSchema: z3.object({
243
254
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -253,6 +264,7 @@ function registerVersionTools(server, callApi) {
253
264
  "lexq_versions_get",
254
265
  {
255
266
  title: "Get Policy Version",
267
+ annotations: { readOnlyHint: true },
256
268
  description: "Get a single version by ID, including its rules and fact requirements.",
257
269
  inputSchema: z3.object({
258
270
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -265,6 +277,7 @@ function registerVersionTools(server, callApi) {
265
277
  "lexq_versions_create",
266
278
  {
267
279
  title: "Create Policy Version",
280
+ annotations: { readOnlyHint: false, destructiveHint: false },
268
281
  description: "Create a new DRAFT version in a policy group. Optionally provide a commit message and effective date range.",
269
282
  inputSchema: z3.object({
270
283
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -279,6 +292,7 @@ function registerVersionTools(server, callApi) {
279
292
  "lexq_versions_update",
280
293
  {
281
294
  title: "Update Policy Version",
295
+ annotations: { readOnlyHint: false, destructiveHint: true },
282
296
  description: "Update a DRAFT version. Only DRAFT versions can be modified. Only provided fields are changed.",
283
297
  inputSchema: z3.object({
284
298
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -294,6 +308,7 @@ function registerVersionTools(server, callApi) {
294
308
  "lexq_versions_delete",
295
309
  {
296
310
  title: "Delete Policy Version",
311
+ annotations: { readOnlyHint: false, destructiveHint: true },
297
312
  description: "Delete a DRAFT version. Only DRAFT versions can be deleted.",
298
313
  inputSchema: z3.object({
299
314
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -306,6 +321,7 @@ function registerVersionTools(server, callApi) {
306
321
  "lexq_versions_clone",
307
322
  {
308
323
  title: "Clone Policy Version",
324
+ annotations: { readOnlyHint: false, destructiveHint: false },
309
325
  description: "Clone an existing version to create a new DRAFT. Useful when the source version is already published.",
310
326
  inputSchema: z3.object({
311
327
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -339,6 +355,7 @@ function registerRuleTools(server, callApi) {
339
355
  "lexq_rules_list",
340
356
  {
341
357
  title: "List Rules",
358
+ annotations: { readOnlyHint: true },
342
359
  description: "List all rules in a version (priority ASC). Returns summary with conditionSummary and actionSummary.",
343
360
  inputSchema: z4.object({
344
361
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -351,6 +368,7 @@ function registerRuleTools(server, callApi) {
351
368
  "lexq_rules_get",
352
369
  {
353
370
  title: "Get Rule Detail",
371
+ annotations: { readOnlyHint: true },
354
372
  description: "Get full rule detail including condition tree and action definitions.",
355
373
  inputSchema: z4.object({
356
374
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -364,6 +382,7 @@ function registerRuleTools(server, callApi) {
364
382
  "lexq_rules_create",
365
383
  {
366
384
  title: "Create Rule",
385
+ annotations: { readOnlyHint: false, destructiveHint: false },
367
386
  description: dedent`
368
387
  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.
369
388
 
@@ -442,6 +461,7 @@ function registerRuleTools(server, callApi) {
442
461
  "lexq_rules_update",
443
462
  {
444
463
  title: "Update Rule",
464
+ annotations: { readOnlyHint: false, destructiveHint: true },
445
465
  description: "Update an existing rule in a DRAFT version. Only provided fields are changed.",
446
466
  inputSchema: z4.object({
447
467
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -463,6 +483,7 @@ function registerRuleTools(server, callApi) {
463
483
  "lexq_rules_delete",
464
484
  {
465
485
  title: "Delete Rule",
486
+ annotations: { readOnlyHint: false, destructiveHint: true },
466
487
  description: "Delete a rule from a DRAFT version.",
467
488
  inputSchema: z4.object({
468
489
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -483,6 +504,7 @@ function registerRuleTools(server, callApi) {
483
504
  "lexq_rules_reorder",
484
505
  {
485
506
  title: "Reorder Rules",
507
+ annotations: { readOnlyHint: false, destructiveHint: true },
486
508
  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).",
487
509
  inputSchema: z4.object({
488
510
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -504,6 +526,7 @@ function registerRuleTools(server, callApi) {
504
526
  "lexq_rules_toggle",
505
527
  {
506
528
  title: "Toggle Rule",
529
+ annotations: { readOnlyHint: false, destructiveHint: true },
507
530
  description: "Enable or disable a rule without deleting it.",
508
531
  inputSchema: z4.object({
509
532
  groupId: z4.string().uuid().describe("Policy group ID"),
@@ -535,6 +558,7 @@ function registerFactTools(server, callApi) {
535
558
  "lexq_facts_list",
536
559
  {
537
560
  title: "List Fact Definitions",
561
+ annotations: { readOnlyHint: true },
538
562
  description: "List all fact definitions (input variable schema). Shows key, type, required, and PII status. Always check this before creating rules.",
539
563
  inputSchema: z5.object({
540
564
  page: z5.number().int().min(0).default(0).describe("Page number"),
@@ -552,6 +576,7 @@ function registerFactTools(server, callApi) {
552
576
  "lexq_facts_create",
553
577
  {
554
578
  title: "Create Fact Definition",
579
+ annotations: { readOnlyHint: false, destructiveHint: false },
555
580
  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.",
556
581
  inputSchema: z5.object({
557
582
  key: z5.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
@@ -570,6 +595,7 @@ function registerFactTools(server, callApi) {
570
595
  "lexq_facts_update",
571
596
  {
572
597
  title: "Update Fact Definition",
598
+ annotations: { readOnlyHint: false, destructiveHint: true },
573
599
  description: "Update a fact definition. The key is immutable. The type can change only while no rule references the fact; if any does, the call fails with FD-007 and reports the count. Only the fields you send are changed. System facts accept name, description, and PII only.",
574
600
  inputSchema: z5.object({
575
601
  factId: z5.string().uuid().describe("Fact definition ID"),
@@ -588,6 +614,7 @@ function registerFactTools(server, callApi) {
588
614
  "lexq_facts_delete",
589
615
  {
590
616
  title: "Delete Fact Definition",
617
+ annotations: { readOnlyHint: false, destructiveHint: true },
591
618
  description: "Delete a fact definition. System facts cannot be deleted. Neither can a fact that any rule references: that call fails with FD-006 and reports the count. Remove the references first.",
592
619
  inputSchema: z5.object({
593
620
  factId: z5.string().uuid().describe("Fact definition ID")
@@ -599,6 +626,7 @@ function registerFactTools(server, callApi) {
599
626
  "lexq_facts_action_metadata",
600
627
  {
601
628
  title: "Get Action Runtime Fact Metadata",
629
+ annotations: { readOnlyHint: true },
602
630
  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. Facts are supplied as input or written by a prior action in the same rule; only SET_FACT creates a fact from nothing. Static data, safe to cache in-session.",
603
631
  inputSchema: z5.object({})
604
632
  },
@@ -608,6 +636,7 @@ function registerFactTools(server, callApi) {
608
636
  "lexq_facts_unregistered",
609
637
  {
610
638
  title: "List Unregistered Facts",
639
+ annotations: { readOnlyHint: true },
611
640
  description: "List facts referenced by a version's rules but not yet defined (read-only \u2014 does not block publish/deploy, INV-4). 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.",
612
641
  inputSchema: z5.object({
613
642
  groupId: z5.string().uuid().describe("Policy group ID"),
@@ -620,6 +649,7 @@ function registerFactTools(server, callApi) {
620
649
  "lexq_facts_export",
621
650
  {
622
651
  title: "Export Fact Catalog",
652
+ annotations: { readOnlyHint: true },
623
653
  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.",
624
654
  inputSchema: z5.object({
625
655
  format: z5.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
@@ -639,6 +669,7 @@ function registerDeployTools(server, callApi) {
639
669
  "lexq_deploy_publish",
640
670
  {
641
671
  title: "Publish Version",
672
+ annotations: { readOnlyHint: false, destructiveHint: false },
642
673
  description: "Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts referenced by rules do not block publishing (INV-4); call lexq_facts_unregistered first to review them.",
643
674
  inputSchema: z6.object({
644
675
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -652,6 +683,7 @@ function registerDeployTools(server, callApi) {
652
683
  "lexq_deploy_live",
653
684
  {
654
685
  title: "Deploy to Live",
686
+ annotations: { readOnlyHint: false, destructiveHint: true },
655
687
  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. Undefined facts do not block deployment (INV-4); use lexq_facts_unregistered to review what the version references but has not defined.",
656
688
  inputSchema: z6.object({
657
689
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -667,6 +699,7 @@ function registerDeployTools(server, callApi) {
667
699
  "lexq_deploy_rollback",
668
700
  {
669
701
  title: "Rollback Deployment",
702
+ annotations: { readOnlyHint: false, destructiveHint: true },
670
703
  description: "Rollback to the previous deployed version. Only available if there is a previous version.",
671
704
  inputSchema: z6.object({
672
705
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -681,6 +714,7 @@ function registerDeployTools(server, callApi) {
681
714
  "lexq_deploy_undeploy",
682
715
  {
683
716
  title: "Undeploy",
717
+ annotations: { readOnlyHint: false, destructiveHint: true },
684
718
  description: "Remove the live version from traffic. The version stays ACTIVE but no longer serves requests.",
685
719
  inputSchema: z6.object({
686
720
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -695,6 +729,7 @@ function registerDeployTools(server, callApi) {
695
729
  "lexq_deploy_schedule",
696
730
  {
697
731
  title: "Schedule Deployment",
732
+ annotations: { readOnlyHint: false, destructiveHint: false },
698
733
  description: "Schedule an ACTIVE version with a future effective start date to auto-deploy at that time (Scheduled Deployment). 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).",
699
734
  inputSchema: z6.object({
700
735
  groupId: z6.string().uuid().describe("Policy group ID"),
@@ -710,6 +745,7 @@ function registerDeployTools(server, callApi) {
710
745
  "lexq_deploy_unschedule",
711
746
  {
712
747
  title: "Cancel Scheduled Deployment",
748
+ annotations: { readOnlyHint: false, destructiveHint: true },
713
749
  description: "Cancel the pending scheduled deployment for a group. The version itself is not affected. Fails with P-039 if no pending schedule exists.",
714
750
  inputSchema: z6.object({
715
751
  groupId: z6.string().uuid().describe("Policy group ID")
@@ -721,6 +757,7 @@ function registerDeployTools(server, callApi) {
721
757
  "lexq_deploy_schedules",
722
758
  {
723
759
  title: "List Scheduled Deployments",
760
+ annotations: { readOnlyHint: true },
724
761
  description: "List scheduled deployments across all groups (all statuses: PENDING, EXECUTED, CANCELED, FAILED), newest first.",
725
762
  inputSchema: z6.object({
726
763
  page: z6.number().int().min(0).default(0).describe("Page number"),
@@ -733,6 +770,7 @@ function registerDeployTools(server, callApi) {
733
770
  "lexq_deploy_history",
734
771
  {
735
772
  title: "Deployment History",
773
+ annotations: { readOnlyHint: true },
736
774
  description: "List deployment history across all groups.",
737
775
  inputSchema: z6.object({
738
776
  page: z6.number().int().min(0).default(0).describe("Page number"),
@@ -756,6 +794,7 @@ function registerDeployTools(server, callApi) {
756
794
  "lexq_deploy_detail",
757
795
  {
758
796
  title: "Deployment Detail",
797
+ annotations: { readOnlyHint: true },
759
798
  description: "Get detailed info about a specific deployment including snapshot hash and integrity check.",
760
799
  inputSchema: z6.object({
761
800
  deploymentId: z6.string().uuid().describe("Deployment ID")
@@ -767,6 +806,7 @@ function registerDeployTools(server, callApi) {
767
806
  "lexq_deploy_overview",
768
807
  {
769
808
  title: "Deployment Overview",
809
+ annotations: { readOnlyHint: true },
770
810
  description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer.",
771
811
  inputSchema: z6.object({})
772
812
  },
@@ -776,6 +816,7 @@ function registerDeployTools(server, callApi) {
776
816
  "lexq_deploy_deployable",
777
817
  {
778
818
  title: "List Deployable Versions",
819
+ annotations: { readOnlyHint: true },
779
820
  description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
780
821
  inputSchema: z6.object({
781
822
  groupId: z6.string().uuid().describe("Policy group ID")
@@ -787,6 +828,7 @@ function registerDeployTools(server, callApi) {
787
828
  "lexq_deploy_diff",
788
829
  {
789
830
  title: "Deployment Diff",
831
+ annotations: { readOnlyHint: true },
790
832
  description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
791
833
  inputSchema: z6.object({
792
834
  baseVersionId: z6.string().uuid().describe("Base version ID (typically the current live)"),
@@ -807,6 +849,7 @@ function registerAnalyticsTools(server, callApi) {
807
849
  "lexq_dry_run",
808
850
  {
809
851
  title: "Dry Run",
852
+ annotations: { readOnlyHint: true },
810
853
  description: dedent2`
811
854
  Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
812
855
 
@@ -836,6 +879,7 @@ function registerAnalyticsTools(server, callApi) {
836
879
  "lexq_dry_run_compare",
837
880
  {
838
881
  title: "Dry Run Compare",
882
+ annotations: { readOnlyHint: true },
839
883
  description: dedent2`
840
884
  Compare dry run results between two versions using the same input facts. Useful for validating changes.
841
885
 
@@ -860,6 +904,7 @@ function registerAnalyticsTools(server, callApi) {
860
904
  "lexq_requirements",
861
905
  {
862
906
  title: "Analyze Requirements",
907
+ annotations: { readOnlyHint: true },
863
908
  description: "Analyze which input facts a version requires. Returns required keys, types, and an example request body.",
864
909
  inputSchema: z7.object({
865
910
  groupId: z7.string().uuid().describe("Policy group ID"),
@@ -872,6 +917,7 @@ function registerAnalyticsTools(server, callApi) {
872
917
  "lexq_simulation_start",
873
918
  {
874
919
  title: "Start Simulation",
920
+ annotations: { readOnlyHint: false, destructiveHint: false },
875
921
  description: dedent2`
876
922
  Start an Impact Simulation against historical, uploaded, or inline data.
877
923
 
@@ -913,6 +959,7 @@ function registerAnalyticsTools(server, callApi) {
913
959
  "lexq_simulation_status",
914
960
  {
915
961
  title: "Simulation Status",
962
+ annotations: { readOnlyHint: true },
916
963
  description: "Get simulation status and results. Poll until status is COMPLETED or FAILED.",
917
964
  inputSchema: z7.object({
918
965
  simulationId: z7.string().uuid().describe("Simulation ID")
@@ -924,6 +971,7 @@ function registerAnalyticsTools(server, callApi) {
924
971
  "lexq_simulation_list",
925
972
  {
926
973
  title: "List Simulations",
974
+ annotations: { readOnlyHint: true },
927
975
  description: "List simulation history with optional filters.",
928
976
  inputSchema: z7.object({
929
977
  page: z7.number().int().min(0).default(0).describe("Page number"),
@@ -945,6 +993,7 @@ function registerAnalyticsTools(server, callApi) {
945
993
  "lexq_simulation_cancel",
946
994
  {
947
995
  title: "Cancel Simulation",
996
+ annotations: { readOnlyHint: false, destructiveHint: true },
948
997
  description: "Cancel a running or pending simulation.",
949
998
  inputSchema: z7.object({
950
999
  simulationId: z7.string().uuid().describe("Simulation ID")
@@ -956,6 +1005,7 @@ function registerAnalyticsTools(server, callApi) {
956
1005
  "lexq_simulation_export",
957
1006
  {
958
1007
  title: "Export Simulation",
1008
+ annotations: { readOnlyHint: true },
959
1009
  description: "Export simulation results as JSON or CSV. Returns the raw data.",
960
1010
  inputSchema: z7.object({
961
1011
  simulationId: z7.string().uuid().describe("Simulation ID"),
@@ -970,6 +1020,7 @@ function registerAnalyticsTools(server, callApi) {
970
1020
  "lexq_dataset_upload",
971
1021
  {
972
1022
  title: "Upload Dataset",
1023
+ annotations: { readOnlyHint: false, destructiveHint: false },
973
1024
  description: dedent2`
974
1025
  Upload inline CSV or JSON content as a simulation dataset.
975
1026
  The content is uploaded to S3 and a path is returned in the "path" field.
@@ -1013,6 +1064,7 @@ function registerAnalyticsTools(server, callApi) {
1013
1064
  "lexq_dataset_template",
1014
1065
  {
1015
1066
  title: "Download Dataset Template",
1067
+ annotations: { readOnlyHint: true },
1016
1068
  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.",
1017
1069
  inputSchema: z7.object({
1018
1070
  groupId: z7.string().uuid().describe("Policy group ID"),
@@ -1042,6 +1094,7 @@ function registerProfileTools(server, callApi) {
1042
1094
  "lexq_profile_overview",
1043
1095
  {
1044
1096
  title: "Group Latency Profile",
1097
+ annotations: { readOnlyHint: true },
1045
1098
  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.`,
1046
1099
  inputSchema: z8.object({
1047
1100
  groupId: z8.string().uuid().describe("Policy group ID"),
@@ -1061,6 +1114,7 @@ function registerProfileTools(server, callApi) {
1061
1114
  "lexq_profile_rule",
1062
1115
  {
1063
1116
  title: "Rule Latency Detail",
1117
+ annotations: { readOnlyHint: true },
1064
1118
  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,
1065
1119
  inputSchema: z8.object({
1066
1120
  groupId: z8.string().uuid().describe("Policy group ID"),
@@ -1085,6 +1139,7 @@ function registerReplayTools(server, callApi) {
1085
1139
  "lexq_replay_decision",
1086
1140
  {
1087
1141
  title: "Replay a Decision",
1142
+ annotations: { readOnlyHint: true },
1088
1143
  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.",
1089
1144
  inputSchema: z9.object({
1090
1145
  traceId: z9.string().describe("Trace ID of the past execution to replay"),
@@ -1097,6 +1152,7 @@ function registerReplayTools(server, callApi) {
1097
1152
  "lexq_replay_start",
1098
1153
  {
1099
1154
  title: "Start Window Replay (Blast Radius)",
1155
+ annotations: { readOnlyHint: false, destructiveHint: false },
1100
1156
  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.",
1101
1157
  inputSchema: z9.object({
1102
1158
  candidateVersionId: z9.string().uuid().describe("Version to re-evaluate against"),
@@ -1113,6 +1169,7 @@ function registerReplayTools(server, callApi) {
1113
1169
  "lexq_replay_status",
1114
1170
  {
1115
1171
  title: "Get Replay Job Status",
1172
+ annotations: { readOnlyHint: true },
1116
1173
  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.",
1117
1174
  inputSchema: z9.object({
1118
1175
  jobId: z9.string().describe("Replay job ID from lexq_replay_start")
@@ -1124,6 +1181,7 @@ function registerReplayTools(server, callApi) {
1124
1181
  "lexq_replay_list",
1125
1182
  {
1126
1183
  title: "List Replay Jobs",
1184
+ annotations: { readOnlyHint: true },
1127
1185
  description: "List window replay job history (reverse-chronological). Lightweight items \u2014 use lexq_replay_status for summary and changed samples.",
1128
1186
  inputSchema: z9.object({
1129
1187
  page: z9.number().int().min(0).default(0).describe("Page number"),
@@ -1136,6 +1194,7 @@ function registerReplayTools(server, callApi) {
1136
1194
  "lexq_replay_cancel",
1137
1195
  {
1138
1196
  title: "Cancel Replay Job",
1197
+ annotations: { readOnlyHint: false, destructiveHint: true },
1139
1198
  description: "Cooperatively cancel a PENDING or RUNNING window replay job. Other states are rejected. VIEWER role cannot cancel.",
1140
1199
  inputSchema: z9.object({
1141
1200
  jobId: z9.string().describe("Replay job ID")
@@ -1147,6 +1206,7 @@ function registerReplayTools(server, callApi) {
1147
1206
  "lexq_replay_export",
1148
1207
  {
1149
1208
  title: "Export Replay Result",
1209
+ annotations: { readOnlyHint: true },
1150
1210
  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.",
1151
1211
  inputSchema: z9.object({
1152
1212
  jobId: z9.string().describe("Replay job ID"),
@@ -1164,6 +1224,7 @@ function registerHistoryTools(server, callApi) {
1164
1224
  "lexq_history_list",
1165
1225
  {
1166
1226
  title: "List Execution History",
1227
+ annotations: { readOnlyHint: true },
1167
1228
  description: "List policy execution history. Shows trace ID, group, version, status, match result, and latency.",
1168
1229
  inputSchema: z10.object({
1169
1230
  page: z10.number().int().min(0).default(0).describe("Page number"),
@@ -1191,6 +1252,7 @@ function registerHistoryTools(server, callApi) {
1191
1252
  "lexq_history_get",
1192
1253
  {
1193
1254
  title: "Get Execution Detail",
1255
+ annotations: { readOnlyHint: true },
1194
1256
  description: "Get full execution detail including inputFacts, mutatedFacts, generatedVariables, executionTraces, and decisionTraces.",
1195
1257
  inputSchema: z10.object({
1196
1258
  traceId: z10.string().describe("Trace ID from execution history")
@@ -1202,6 +1264,7 @@ function registerHistoryTools(server, callApi) {
1202
1264
  "lexq_history_stats",
1203
1265
  {
1204
1266
  title: "Execution Statistics",
1267
+ annotations: { readOnlyHint: true },
1205
1268
  description: "Get execution KPIs: total executions, success/failure counts, success rate, and average latency.",
1206
1269
  inputSchema: z10.object({
1207
1270
  groupId: z10.string().uuid().optional().describe("Filter by policy group"),
@@ -1226,6 +1289,7 @@ function registerProvenanceTools(server, callApi) {
1226
1289
  "lexq_provenance_get",
1227
1290
  {
1228
1291
  title: "Get Decision Provenance",
1292
+ annotations: { readOnlyHint: true },
1229
1293
  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.",
1230
1294
  inputSchema: z11.object({
1231
1295
  traceId: z11.string().describe("Trace ID of the execution")
@@ -1237,6 +1301,7 @@ function registerProvenanceTools(server, callApi) {
1237
1301
  "lexq_pii_reveals_list",
1238
1302
  {
1239
1303
  title: "List PII Reveal Audits",
1304
+ annotations: { readOnlyHint: true },
1240
1305
  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.",
1241
1306
  inputSchema: z11.object({
1242
1307
  page: z11.number().int().min(0).default(0).describe("Page number"),
@@ -1267,6 +1332,7 @@ function registerLogTools(server, callApi) {
1267
1332
  "lexq_logs_list",
1268
1333
  {
1269
1334
  title: "List Failure Logs",
1335
+ annotations: { readOnlyHint: true },
1270
1336
  description: "List system failure logs from background tasks (platform event webhooks, scheduled deployments).",
1271
1337
  inputSchema: z12.object({
1272
1338
  page: z12.number().int().min(0).default(0).describe("Page number"),
@@ -1292,6 +1358,7 @@ function registerLogTools(server, callApi) {
1292
1358
  "lexq_logs_get",
1293
1359
  {
1294
1360
  title: "Get Failure Log",
1361
+ annotations: { readOnlyHint: true },
1295
1362
  description: "Get failure log detail by ID.",
1296
1363
  inputSchema: z12.object({
1297
1364
  logId: z12.string().uuid().describe("Failure log ID")
@@ -1303,6 +1370,7 @@ function registerLogTools(server, callApi) {
1303
1370
  "lexq_logs_action",
1304
1371
  {
1305
1372
  title: "Process Failure Log",
1373
+ annotations: { readOnlyHint: false, destructiveHint: false },
1306
1374
  description: "Process a single failure log: RESOLVE (mark as manually fixed) or IGNORE (skip intentionally).",
1307
1375
  inputSchema: z12.object({
1308
1376
  logId: z12.string().uuid().describe("Failure log ID"),
@@ -1317,6 +1385,7 @@ function registerLogTools(server, callApi) {
1317
1385
  "lexq_logs_bulk_action",
1318
1386
  {
1319
1387
  title: "Bulk Process Failure Logs",
1388
+ annotations: { readOnlyHint: false, destructiveHint: false },
1320
1389
  description: "Process multiple failure logs at once. Provide an array of log IDs and the action.",
1321
1390
  inputSchema: z12.object({
1322
1391
  logIds: z12.array(z12.string().uuid()).describe("Array of failure log IDs"),
@@ -1336,6 +1405,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
1336
1405
  "lexq_webhook_subscriptions_list",
1337
1406
  {
1338
1407
  title: "List Webhook Subscriptions",
1408
+ annotations: { readOnlyHint: true },
1339
1409
  description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications (publish, deploy, rollback, undeploy).",
1340
1410
  inputSchema: z13.object({
1341
1411
  page: z13.number().int().min(0).default(0).describe("Page number"),
@@ -1351,6 +1421,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
1351
1421
  "lexq_webhook_subscriptions_get",
1352
1422
  {
1353
1423
  title: "Get Webhook Subscription",
1424
+ annotations: { readOnlyHint: true },
1354
1425
  description: "Get webhook subscription detail by ID.",
1355
1426
  inputSchema: z13.object({
1356
1427
  id: z13.string().uuid().describe("Webhook subscription ID")
@@ -1362,6 +1433,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
1362
1433
  "lexq_webhook_subscriptions_save",
1363
1434
  {
1364
1435
  title: "Save Webhook Subscription",
1436
+ annotations: { readOnlyHint: false, destructiveHint: true },
1365
1437
  description: 'Create or update a webhook subscription. Omit id to create, provide id to update. Events: VERSION_PUBLISHED, DEPLOYED, ROLLED_BACK, UNDEPLOYED. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).',
1366
1438
  inputSchema: z13.object({
1367
1439
  id: z13.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
@@ -1379,6 +1451,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
1379
1451
  "lexq_webhook_subscriptions_delete",
1380
1452
  {
1381
1453
  title: "Delete Webhook Subscription",
1454
+ annotations: { readOnlyHint: false, destructiveHint: true },
1382
1455
  description: "Delete a webhook subscription by ID.",
1383
1456
  inputSchema: z13.object({
1384
1457
  id: z13.string().uuid().describe("Webhook subscription ID")
@@ -1390,6 +1463,7 @@ function registerWebhookSubscriptionTools(server, callApi) {
1390
1463
  "lexq_webhook_subscriptions_test",
1391
1464
  {
1392
1465
  title: "Test Webhook Subscription",
1466
+ annotations: { readOnlyHint: false, destructiveHint: false },
1393
1467
  description: "Send a test event to verify webhook connectivity. Returns the HTTP status code and success/failure message.",
1394
1468
  inputSchema: z13.object({
1395
1469
  id: z13.string().uuid().describe("Webhook subscription ID")
@@ -1406,6 +1480,7 @@ function registerDomainTemplateTools(server, callApi) {
1406
1480
  "lexq_domain_templates_list",
1407
1481
  {
1408
1482
  title: "List Domain Templates",
1483
+ annotations: { readOnlyHint: true },
1409
1484
  description: "List all domain templates. A domain template is a curated, industry-specific starter pack of fact definitions and sample rules (e.g. ECOMMERCE). Each entry reports its key, status (ACTIVE or COMING_SOON), and a summary of what it provisions. Call this before preview or apply to discover which templates can currently be applied.",
1410
1485
  inputSchema: z14.object({})
1411
1486
  },
@@ -1415,6 +1490,7 @@ function registerDomainTemplateTools(server, callApi) {
1415
1490
  "lexq_domain_templates_preview",
1416
1491
  {
1417
1492
  title: "Preview Domain Template",
1493
+ annotations: { readOnlyHint: true },
1418
1494
  description: "Preview exactly what a domain template will provision before applying it: the fact definitions it registers, the sample rules it creates, and an apply plan. This is a read-only dry run \u2014 nothing is created. Only ACTIVE templates can be previewed.",
1419
1495
  inputSchema: z14.object({
1420
1496
  template: z14.string().describe(
@@ -1428,6 +1504,7 @@ function registerDomainTemplateTools(server, callApi) {
1428
1504
  "lexq_domain_templates_apply",
1429
1505
  {
1430
1506
  title: "Apply Domain Template",
1507
+ annotations: { readOnlyHint: false, destructiveHint: false },
1431
1508
  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. Existing facts are skipped \u2014 apply is additive and never overwrites existing schema. Run lexq_domain_templates_preview first to review what will be created. Only ACTIVE templates can be applied.",
1432
1509
  inputSchema: z14.object({
1433
1510
  template: z14.string().describe("Domain template key to apply (e.g. ECOMMERCE)."),
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@lexq/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
+ "mcpName": "io.lexq/lexq",
4
5
  "description": "LexQ CLI — manage policies, simulate rules, and deploy from the terminal. Built for humans and AI agents.",
5
6
  "type": "module",
6
7
  "bin": {
@@ -32,6 +33,8 @@
32
33
  "constants:check": "node scripts/check-constants.mjs",
33
34
  "surface:check": "node scripts/check-public-surface.mjs",
34
35
  "ab-key:check": "node scripts/check-ab-key.mjs",
36
+ "registry:check": "node scripts/check-registry-manifest.mjs",
37
+ "protocol:check": "node scripts/check-protocol-revisions.mjs",
35
38
  "schemas:check": "node scripts/check-tool-schemas.mjs",
36
39
  "schemas:update": "node scripts/check-tool-schemas.mjs --update",
37
40
  "start": "node dist/index.js",