llm-cli-gateway 2.2.0 → 2.3.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/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to the llm-cli-gateway project.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [2.3.0] - 2026-06-08: MCP tool annotations and client safety hints
8
+
9
+ ### Added
10
+
11
+ - MCP tool annotations for all 37 tools (per MCP spec + tool-design best
12
+ practice): display `title` plus `readOnlyHint`/`destructiveHint`/
13
+ `idempotentHint`/`openWorldHint` on every registration. 14 pure-read tools
14
+ marked read-only/closed-world; `cli_upgrade`, `session_delete`,
15
+ `session_clear_all`, `llm_job_cancel` marked destructive; every
16
+ provider-spawning tool (requests, fork, validation) marked open-world with
17
+ destructive potential (spawned agentic CLIs can modify the environment).
18
+ Clients can use the hints for confirmation UX and safe auto-approval. New
19
+ invariant test pins titles, the exact destructive/read-only/open-world
20
+ sets, and the readOnly+destructive contradiction ban.
21
+
7
22
  ## [2.2.0] - 2026-06-07: MCP tool-surface usability — self-describing tools
8
23
 
9
24
  ### Added
@@ -247,7 +262,7 @@ to end with a verdaccio reproduction.
247
262
  - Consumer `npm ls` exits ELSPROBLEMS: the pinned `tar-stream@3.1.7` sits
248
263
  outside `tar-fs`'s `^2.1.4` range. Inherent to the out-of-range pin; disappears
249
264
  in 2.0.0 (Phase B / node:sqlite) when the `better-sqlite3 → prebuild-install
250
- → tar-fs` chain leaves the prod graph entirely.
265
+ → tar-fs` chain leaves the prod graph entirely.
251
266
  - Local-tarball installs still resolve `tar-stream@2.2.0` (shrinkwrap ignored on
252
267
  that path); the audit's advisory carve-out stays until Phase B.
253
268
 
package/dist/index.js CHANGED
@@ -2718,6 +2718,12 @@ export function createGatewayServer(deps = {}) {
2718
2718
  .boolean()
2719
2719
  .default(false)
2720
2720
  .describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
2721
+ }, {
2722
+ title: "Claude Code request",
2723
+ readOnlyHint: false,
2724
+ destructiveHint: true,
2725
+ idempotentHint: false,
2726
+ openWorldHint: true,
2721
2727
  }, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, noSessionPersistence, settingSources, settings, tools, worktree, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, }) => {
2722
2728
  const startTime = Date.now();
2723
2729
  if (systemPrompt !== undefined && appendSystemPrompt !== undefined) {
@@ -3019,6 +3025,12 @@ export function createGatewayServer(deps = {}) {
3019
3025
  .optional()
3020
3026
  .describe("Codex --add-dir <DIR>: additional writable workspace directories. Emitted once per entry on new sessions only; resume inherits the original session's writable-dir policy."),
3021
3027
  worktree: WORKTREE_SCHEMA.optional(),
3028
+ }, {
3029
+ title: "Codex request",
3030
+ readOnlyHint: false,
3031
+ destructiveHint: true,
3032
+ idempotentHint: false,
3033
+ openWorldHint: true,
3022
3034
  }, async ({ prompt, promptParts, model, fullAuto, sandboxMode, askForApproval, useLegacyFullAutoFlag, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, resumeLatest, createNewSession, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, outputFormat, outputSchema, search, profile, configOverrides, ephemeral, images, ignoreUserConfig, ignoreRules, workingDir, addDir, worktree, }) => {
3023
3035
  const startTime = Date.now();
3024
3036
  const prep = prepareCodexRequest({
@@ -3191,6 +3203,12 @@ export function createGatewayServer(deps = {}) {
3191
3203
  .max(3_600_000)
3192
3204
  .optional()
3193
3205
  .describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
3206
+ }, {
3207
+ title: "Fork Codex session",
3208
+ readOnlyHint: false,
3209
+ destructiveHint: true,
3210
+ idempotentHint: false,
3211
+ openWorldHint: true,
3194
3212
  }, async ({ prompt, sessionId, forkLast, model, sandboxMode, askForApproval, correlationId, idleTimeoutMs, }) => {
3195
3213
  const corrId = correlationId || randomUUID();
3196
3214
  const startTime = Date.now();
@@ -3317,6 +3335,12 @@ export function createGatewayServer(deps = {}) {
3317
3335
  .optional()
3318
3336
  .describe("Emit `--yolo` to auto-approve all actions. Equivalent to approvalMode 'yolo'; routed through the same approval gate. Under mcp_managed the gate still decides."),
3319
3337
  worktree: WORKTREE_SCHEMA.optional(),
3338
+ }, {
3339
+ title: "Gemini request",
3340
+ readOnlyHint: false,
3341
+ destructiveHint: true,
3342
+ idempotentHint: false,
3343
+ openWorldHint: true,
3320
3344
  }, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, yolo, worktree, }) => {
3321
3345
  return handleGeminiRequest({ sessionManager, logger, runtime }, {
3322
3346
  prompt,
@@ -3521,6 +3545,12 @@ export function createGatewayServer(deps = {}) {
3521
3545
  .optional()
3522
3546
  .describe("Grok -w/--worktree: native CLI worktree flag (`true` → bare `--worktree`, string → named). NOT gateway slice λ `worktree`."),
3523
3547
  worktree: WORKTREE_SCHEMA.optional(),
3548
+ }, {
3549
+ title: "Grok request",
3550
+ readOnlyHint: false,
3551
+ destructiveHint: true,
3552
+ idempotentHint: false,
3553
+ openWorldHint: true,
3524
3554
  }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, maxTurns, workingDir, sandbox, rules, systemPromptOverride, allow, deny, compactionMode, compactionDetail, agent, bestOfN, check, disableWebSearch, todoGate, verbatim, agents, promptFile, promptJson, single, experimentalMemory, noAltScreen, noMemory, noPlan, noSubagents, oauth, restoreCode, leaderSocket, nativeWorktree, worktree, }) => {
3525
3555
  return handleGrokRequest({ sessionManager, logger, runtime }, {
3526
3556
  prompt,
@@ -3655,6 +3685,12 @@ export function createGatewayServer(deps = {}) {
3655
3685
  .optional()
3656
3686
  .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance (Vibe states this flag may be specified multiple times)."),
3657
3687
  worktree: WORKTREE_SCHEMA.optional(),
3688
+ }, {
3689
+ title: "Mistral Vibe request",
3690
+ readOnlyHint: false,
3691
+ destructiveHint: true,
3692
+ idempotentHint: false,
3693
+ openWorldHint: true,
3658
3694
  }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, maxTokens, workingDir, addDir, worktree, }) => {
3659
3695
  return handleMistralRequest({ sessionManager, logger, runtime }, {
3660
3696
  prompt,
@@ -3823,6 +3859,12 @@ export function createGatewayServer(deps = {}) {
3823
3859
  .boolean()
3824
3860
  .default(false)
3825
3861
  .describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
3862
+ }, {
3863
+ title: "Claude Code request (async job)",
3864
+ readOnlyHint: false,
3865
+ destructiveHint: true,
3866
+ idempotentHint: false,
3867
+ openWorldHint: true,
3826
3868
  }, async ({ prompt, promptParts, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, permissionMode, agent, agents, forkSession, systemPrompt, appendSystemPrompt, maxBudgetUsd, maxTurns, effort, excludeDynamicSystemPromptSections, fallbackModel, jsonSchema, addDir, noSessionPersistence, settingSources, settings, tools, worktree, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, }) => {
3827
3869
  if (systemPrompt !== undefined && appendSystemPrompt !== undefined) {
3828
3870
  return createErrorResponse("claude", 1, "", correlationId, new Error("systemPrompt and appendSystemPrompt are mutually exclusive; use one or the other (not both)."));
@@ -4035,6 +4077,12 @@ export function createGatewayServer(deps = {}) {
4035
4077
  .optional()
4036
4078
  .describe("Codex --add-dir <DIR>: additional writable workspace directories (repeat per entry). New sessions only."),
4037
4079
  worktree: WORKTREE_SCHEMA.optional(),
4080
+ }, {
4081
+ title: "Codex request (async job)",
4082
+ readOnlyHint: false,
4083
+ destructiveHint: true,
4084
+ idempotentHint: false,
4085
+ openWorldHint: true,
4038
4086
  }, async ({ prompt, promptParts, model, fullAuto, sandboxMode, askForApproval, useLegacyFullAutoFlag, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, resumeLatest, createNewSession, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, outputFormat, outputSchema, search, profile, configOverrides, ephemeral, images, ignoreUserConfig, ignoreRules, workingDir, addDir, worktree, }) => {
4039
4087
  return handleCodexRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4040
4088
  prompt,
@@ -4138,6 +4186,12 @@ export function createGatewayServer(deps = {}) {
4138
4186
  .optional()
4139
4187
  .describe("Emit `--yolo` to auto-approve all actions. Equivalent to approvalMode 'yolo'; routed through the same approval gate. Under mcp_managed the gate still decides."),
4140
4188
  worktree: WORKTREE_SCHEMA.optional(),
4189
+ }, {
4190
+ title: "Gemini request (async job)",
4191
+ readOnlyHint: false,
4192
+ destructiveHint: true,
4193
+ idempotentHint: false,
4194
+ openWorldHint: true,
4141
4195
  }, async ({ prompt, promptParts, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, outputFormat, sandbox, policyFiles, adminPolicyFiles, attachments, skipTrust, yolo, worktree, }) => {
4142
4196
  return handleGeminiRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4143
4197
  prompt,
@@ -4343,6 +4397,12 @@ export function createGatewayServer(deps = {}) {
4343
4397
  .optional()
4344
4398
  .describe("Grok -w/--worktree: native CLI worktree flag (`true` → bare `--worktree`, string → named). NOT gateway slice λ `worktree`."),
4345
4399
  worktree: WORKTREE_SCHEMA.optional(),
4400
+ }, {
4401
+ title: "Grok request (async job)",
4402
+ readOnlyHint: false,
4403
+ destructiveHint: true,
4404
+ idempotentHint: false,
4405
+ openWorldHint: true,
4346
4406
  }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, maxTurns, workingDir, sandbox, rules, systemPromptOverride, allow, deny, compactionMode, compactionDetail, agent, bestOfN, check, disableWebSearch, todoGate, verbatim, agents, promptFile, promptJson, single, experimentalMemory, noAltScreen, noMemory, noPlan, noSubagents, oauth, restoreCode, leaderSocket, nativeWorktree, worktree, }) => {
4347
4407
  return handleGrokRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4348
4408
  prompt,
@@ -4475,6 +4535,12 @@ export function createGatewayServer(deps = {}) {
4475
4535
  .optional()
4476
4536
  .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance."),
4477
4537
  worktree: WORKTREE_SCHEMA.optional(),
4538
+ }, {
4539
+ title: "Mistral Vibe request (async job)",
4540
+ readOnlyHint: false,
4541
+ destructiveHint: true,
4542
+ idempotentHint: false,
4543
+ openWorldHint: true,
4478
4544
  }, async ({ prompt, promptParts, model, outputFormat, sessionId, resumeLatest, createNewSession, permissionMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, trust, maxTurns, maxPrice, maxTokens, workingDir, addDir, worktree, }) => {
4479
4545
  return handleMistralRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
4480
4546
  prompt,
@@ -4505,6 +4571,12 @@ export function createGatewayServer(deps = {}) {
4505
4571
  });
4506
4572
  server.tool("llm_job_status", "Check lifecycle status (running|completed|failed|canceled|orphaned) of a gateway async or deferred-sync job by jobId.", {
4507
4573
  jobId: z.string().describe("Async job ID from *_request_async"),
4574
+ }, {
4575
+ title: "Async job status",
4576
+ readOnlyHint: true,
4577
+ destructiveHint: false,
4578
+ idempotentHint: true,
4579
+ openWorldHint: false,
4508
4580
  }, async ({ jobId }) => {
4509
4581
  const job = asyncJobManager.getJobSnapshot(jobId);
4510
4582
  if (!job) {
@@ -4543,6 +4615,12 @@ export function createGatewayServer(deps = {}) {
4543
4615
  .max(2000000)
4544
4616
  .default(200000)
4545
4617
  .describe("Max chars returned per stream"),
4618
+ }, {
4619
+ title: "Async job result",
4620
+ readOnlyHint: true,
4621
+ destructiveHint: false,
4622
+ idempotentHint: true,
4623
+ openWorldHint: false,
4546
4624
  }, async ({ jobId, maxChars }) => {
4547
4625
  const result = asyncJobManager.getJobResult(jobId, maxChars);
4548
4626
  if (!result) {
@@ -4590,6 +4668,12 @@ export function createGatewayServer(deps = {}) {
4590
4668
  });
4591
4669
  server.tool("llm_job_cancel", "Cancel a running gateway async or deferred-sync job by jobId.", {
4592
4670
  jobId: z.string().describe("Async job ID from *_request_async"),
4671
+ }, {
4672
+ title: "Cancel async job",
4673
+ readOnlyHint: false,
4674
+ destructiveHint: true,
4675
+ idempotentHint: true,
4676
+ openWorldHint: false,
4593
4677
  }, async ({ jobId }) => {
4594
4678
  const cancel = asyncJobManager.cancelJob(jobId);
4595
4679
  if (!cancel.canceled) {
@@ -4636,6 +4720,12 @@ export function createGatewayServer(deps = {}) {
4636
4720
  .boolean()
4637
4721
  .default(false)
4638
4722
  .describe("Include the full persisted prompt text in the result"),
4723
+ }, {
4724
+ title: "Persisted request lookup",
4725
+ readOnlyHint: true,
4726
+ destructiveHint: false,
4727
+ idempotentHint: true,
4728
+ openWorldHint: false,
4639
4729
  }, async ({ correlationId, maxChars, includePrompt }) => {
4640
4730
  const record = readPersistedRequest(flightRecorder, correlationId, {
4641
4731
  maxChars,
@@ -4666,7 +4756,13 @@ export function createGatewayServer(deps = {}) {
4666
4756
  ],
4667
4757
  };
4668
4758
  });
4669
- server.tool("llm_process_health", "Report gateway process health: async-job manager state plus the resolved persistence configuration and paths.", {}, async () => {
4759
+ server.tool("llm_process_health", "Report gateway process health: async-job manager state plus the resolved persistence configuration and paths.", {}, {
4760
+ title: "Gateway process health",
4761
+ readOnlyHint: true,
4762
+ destructiveHint: false,
4763
+ idempotentHint: true,
4764
+ openWorldHint: false,
4765
+ }, async () => {
4670
4766
  const health = asyncJobManager.getJobHealth();
4671
4767
  const persistenceBlock = {
4672
4768
  backend: persistence.backend,
@@ -4702,6 +4798,12 @@ export function createGatewayServer(deps = {}) {
4702
4798
  .enum(["claude", "codex", "gemini", "grok", "mistral"])
4703
4799
  .optional()
4704
4800
  .describe("Optional CLI filter"),
4801
+ }, {
4802
+ title: "Approval decisions",
4803
+ readOnlyHint: true,
4804
+ destructiveHint: false,
4805
+ idempotentHint: true,
4806
+ openWorldHint: false,
4705
4807
  }, async ({ limit, cli }) => {
4706
4808
  const approvals = approvalManager.list(limit, cli);
4707
4809
  return {
@@ -4721,6 +4823,12 @@ export function createGatewayServer(deps = {}) {
4721
4823
  cli: z
4722
4824
  .preprocess(value => (value === "" || value === null ? undefined : value), z.enum(["claude", "codex", "gemini", "grok", "mistral"]).optional())
4723
4825
  .describe("CLI filter (claude|codex|gemini|grok|mistral)"),
4826
+ }, {
4827
+ title: "Provider models",
4828
+ readOnlyHint: true,
4829
+ destructiveHint: false,
4830
+ idempotentHint: true,
4831
+ openWorldHint: false,
4724
4832
  }, async ({ cli }) => {
4725
4833
  const cliInfo = getAvailableCliInfo();
4726
4834
  const result = cli ? { [cli]: cliInfo[cli] } : cliInfo;
@@ -4730,6 +4838,12 @@ export function createGatewayServer(deps = {}) {
4730
4838
  cli: z
4731
4839
  .preprocess(value => (value === "" || value === null ? undefined : value), z.enum(["claude", "codex", "gemini", "grok", "mistral"]).optional())
4732
4840
  .describe("CLI filter (claude|codex|gemini|grok|mistral)"),
4841
+ }, {
4842
+ title: "Provider CLI versions",
4843
+ readOnlyHint: true,
4844
+ destructiveHint: false,
4845
+ idempotentHint: true,
4846
+ openWorldHint: false,
4733
4847
  }, async ({ cli }) => {
4734
4848
  const versions = await getCliVersions(cli);
4735
4849
  return { content: [{ type: "text", text: JSON.stringify({ versions }, null, 2) }] };
@@ -4742,6 +4856,12 @@ export function createGatewayServer(deps = {}) {
4742
4856
  .boolean()
4743
4857
  .default(false)
4744
4858
  .describe("When true, run local --help probes and compare advertised flags against the declared contract. Strongly recommended after any provider CLI upgrade to detect drift."),
4859
+ }, {
4860
+ title: "Provider CLI contracts",
4861
+ readOnlyHint: true,
4862
+ destructiveHint: false,
4863
+ idempotentHint: true,
4864
+ openWorldHint: false,
4745
4865
  }, async ({ cli, probeInstalled }) => {
4746
4866
  const report = buildUpstreamContractReport({ cli, probeInstalled });
4747
4867
  return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
@@ -4764,6 +4884,12 @@ export function createGatewayServer(deps = {}) {
4764
4884
  .max(3_600_000)
4765
4885
  .optional()
4766
4886
  .describe("Upgrade timeout in ms when dryRun=false"),
4887
+ }, {
4888
+ title: "Upgrade provider CLI",
4889
+ readOnlyHint: false,
4890
+ destructiveHint: true,
4891
+ idempotentHint: false,
4892
+ openWorldHint: true,
4767
4893
  }, async ({ cli, target, dryRun, timeoutMs }) => {
4768
4894
  try {
4769
4895
  const result = await runCliUpgrade({ cli, target, dryRun, timeoutMs, logger });
@@ -4799,6 +4925,12 @@ export function createGatewayServer(deps = {}) {
4799
4925
  cli: SESSION_PROVIDER_ENUM.describe("CLI type (claude|codex|gemini|grok|mistral)"),
4800
4926
  description: z.string().optional().describe("Session description"),
4801
4927
  setAsActive: z.boolean().default(true).describe("Set as active session"),
4928
+ }, {
4929
+ title: "Create session record",
4930
+ readOnlyHint: false,
4931
+ destructiveHint: false,
4932
+ idempotentHint: false,
4933
+ openWorldHint: false,
4802
4934
  }, async ({ cli, description, setAsActive }) => {
4803
4935
  try {
4804
4936
  const session = await sessionManager.createSession(cli, description);
@@ -4830,6 +4962,12 @@ export function createGatewayServer(deps = {}) {
4830
4962
  });
4831
4963
  server.tool("session_list", "List gateway session records and the active session per CLI, optionally filtered by CLI.", {
4832
4964
  cli: SESSION_PROVIDER_ENUM.optional().describe("CLI filter (claude|codex|gemini|grok|mistral)"),
4965
+ }, {
4966
+ title: "List sessions",
4967
+ readOnlyHint: true,
4968
+ destructiveHint: false,
4969
+ idempotentHint: true,
4970
+ openWorldHint: false,
4833
4971
  }, async ({ cli }) => {
4834
4972
  try {
4835
4973
  const sessions = await sessionManager.listSessions(cli);
@@ -4874,6 +5012,12 @@ export function createGatewayServer(deps = {}) {
4874
5012
  server.tool("session_set_active", "Set or clear the active session for a CLI; the active session is used when a request omits sessionId.", {
4875
5013
  cli: SESSION_PROVIDER_ENUM.describe("CLI type (claude|codex|gemini|grok|mistral)"),
4876
5014
  sessionId: z.string().nullable().describe("Session ID (null to clear)"),
5015
+ }, {
5016
+ title: "Set active session",
5017
+ readOnlyHint: false,
5018
+ destructiveHint: false,
5019
+ idempotentHint: true,
5020
+ openWorldHint: false,
4877
5021
  }, async ({ cli, sessionId }) => {
4878
5022
  try {
4879
5023
  const success = await sessionManager.setActiveSession(cli, sessionId || null);
@@ -4911,6 +5055,12 @@ export function createGatewayServer(deps = {}) {
4911
5055
  });
4912
5056
  server.tool("session_delete", "Delete a gateway session record by ID (also removes any gateway-owned worktree attached to it).", {
4913
5057
  sessionId: z.string().describe("Session ID"),
5058
+ }, {
5059
+ title: "Delete session",
5060
+ readOnlyHint: false,
5061
+ destructiveHint: true,
5062
+ idempotentHint: true,
5063
+ openWorldHint: false,
4914
5064
  }, async ({ sessionId }) => {
4915
5065
  try {
4916
5066
  const session = await sessionManager.getSession(sessionId);
@@ -4952,6 +5102,12 @@ export function createGatewayServer(deps = {}) {
4952
5102
  });
4953
5103
  server.tool("session_get", "Get one gateway session record by session ID, including recent request history when available.", {
4954
5104
  sessionId: z.string().describe("Session ID"),
5105
+ }, {
5106
+ title: "Get session",
5107
+ readOnlyHint: true,
5108
+ destructiveHint: false,
5109
+ idempotentHint: true,
5110
+ openWorldHint: false,
4955
5111
  }, async ({ sessionId }) => {
4956
5112
  try {
4957
5113
  const session = await sessionManager.getSession(sessionId);
@@ -5015,6 +5171,12 @@ export function createGatewayServer(deps = {}) {
5015
5171
  });
5016
5172
  server.tool("session_clear_all", "Delete all gateway session records, optionally scoped to one CLI.", {
5017
5173
  cli: SESSION_PROVIDER_ENUM.optional().describe("CLI filter (claude|codex|gemini|grok|mistral)"),
5174
+ }, {
5175
+ title: "Clear sessions",
5176
+ readOnlyHint: false,
5177
+ destructiveHint: true,
5178
+ idempotentHint: true,
5179
+ openWorldHint: false,
5018
5180
  }, async ({ cli }) => {
5019
5181
  try {
5020
5182
  const count = await sessionManager.clearAllSessions(cli);
@@ -57,6 +57,12 @@ export function registerValidationTools(server, deps) {
57
57
  judgeModel: providerSchema
58
58
  .optional()
59
59
  .describe("Optional provider to run an explicit judge synthesis job."),
60
+ }, {
61
+ title: "Multi-model validation",
62
+ readOnlyHint: false,
63
+ destructiveHint: true,
64
+ idempotentHint: false,
65
+ openWorldHint: true,
60
66
  }, async ({ question, models, focus, judgeModel }) => textResponse({
61
67
  success: true,
62
68
  tool: "validate_with_models",
@@ -73,6 +79,12 @@ export function registerValidationTools(server, deps) {
73
79
  answer: z.string().min(1).describe("Answer to review."),
74
80
  question: z.string().optional().describe("Original question, if available."),
75
81
  model: providerSchema.default("codex").describe("Provider to ask for the second opinion."),
82
+ }, {
83
+ title: "Second opinion",
84
+ readOnlyHint: false,
85
+ destructiveHint: true,
86
+ idempotentHint: false,
87
+ openWorldHint: true,
76
88
  }, async ({ answer, question, model }) => textResponse({
77
89
  success: true,
78
90
  tool: "second_opinion",
@@ -87,6 +99,12 @@ export function registerValidationTools(server, deps) {
87
99
  server.tool("compare_answers", "Summarize agreement/differences between caller-provided answers LOCALLY — does not call any provider.", {
88
100
  question: z.string().min(1).describe("Question the answers respond to."),
89
101
  answers: z.array(z.string().min(1)).min(2).describe("Two or more answers to compare."),
102
+ }, {
103
+ title: "Compare answers (local)",
104
+ readOnlyHint: true,
105
+ destructiveHint: false,
106
+ idempotentHint: true,
107
+ openWorldHint: false,
90
108
  }, async ({ question, answers }) => textResponse({
91
109
  success: true,
92
110
  tool: "compare_answers",
@@ -106,6 +124,12 @@ export function registerValidationTools(server, deps) {
106
124
  .default("normal")
107
125
  .describe("How aggressively to review."),
108
126
  models: providerListSchema.describe("Providers to ask for adversarial review."),
127
+ }, {
128
+ title: "Red-team review",
129
+ readOnlyHint: false,
130
+ destructiveHint: true,
131
+ idempotentHint: false,
132
+ openWorldHint: true,
109
133
  }, async ({ content, riskLevel, models }) => textResponse({
110
134
  success: true,
111
135
  tool: "red_team_review",
@@ -120,6 +144,12 @@ export function registerValidationTools(server, deps) {
120
144
  server.tool("consensus_check", "Ask provider CLIs whether they agree or disagree with a claim (starts validation jobs).", {
121
145
  claim: z.string().min(1).describe("Claim to check across providers."),
122
146
  models: providerListSchema.describe("Providers to ask for agreement or disagreement."),
147
+ }, {
148
+ title: "Consensus check",
149
+ readOnlyHint: false,
150
+ destructiveHint: true,
151
+ idempotentHint: false,
152
+ openWorldHint: true,
123
153
  }, async ({ claim, models }) => textResponse({
124
154
  success: true,
125
155
  tool: "consensus_check",
@@ -133,6 +163,12 @@ export function registerValidationTools(server, deps) {
133
163
  server.tool("ask_model", "Ask one provider CLI a question through the simplified validation surface (starts a validation job).", {
134
164
  question: z.string().min(1).describe("Question for one provider."),
135
165
  model: providerSchema.default("claude").describe("Provider to ask."),
166
+ }, {
167
+ title: "Ask one model",
168
+ readOnlyHint: false,
169
+ destructiveHint: true,
170
+ idempotentHint: false,
171
+ openWorldHint: true,
136
172
  }, async ({ question, model }) => textResponse({
137
173
  success: true,
138
174
  tool: "ask_model",
@@ -150,6 +186,12 @@ export function registerValidationTools(server, deps) {
150
186
  .min(1)
151
187
  .describe("Terminal normalized provider results from job_result."),
152
188
  judgeModel: providerSchema.default("codex").describe("Provider to run the judge synthesis."),
189
+ }, {
190
+ title: "Synthesize validation",
191
+ readOnlyHint: false,
192
+ destructiveHint: true,
193
+ idempotentHint: false,
194
+ openWorldHint: true,
153
195
  }, async ({ question, providerResults, judgeModel }) => textResponse({
154
196
  success: true,
155
197
  tool: "synthesize_validation",
@@ -160,9 +202,21 @@ export function registerValidationTools(server, deps) {
160
202
  judgeProvider: judgeModel,
161
203
  }),
162
204
  }));
163
- server.tool("list_available_models", "List models and capabilities for every available provider CLI (takes no arguments; complements per-provider list_models).", {}, async () => textResponse({ success: true, models: getAvailableCliInfo() }));
205
+ server.tool("list_available_models", "List models and capabilities for every available provider CLI (takes no arguments; complements per-provider list_models).", {}, {
206
+ title: "All provider models",
207
+ readOnlyHint: true,
208
+ destructiveHint: false,
209
+ idempotentHint: true,
210
+ openWorldHint: false,
211
+ }, async () => textResponse({ success: true, models: getAvailableCliInfo() }));
164
212
  server.tool("job_status", "Check a VALIDATION job's status (jobs started by validate_with_models/ask_model/etc.) — distinct from llm_job_status, which tracks provider request jobs.", {
165
213
  jobId: z.string().min(1).describe("Validation job ID."),
214
+ }, {
215
+ title: "Validation job status",
216
+ readOnlyHint: true,
217
+ destructiveHint: false,
218
+ idempotentHint: true,
219
+ openWorldHint: false,
166
220
  }, async ({ jobId }) => {
167
221
  const job = deps.asyncJobManager.getJobSnapshot(jobId);
168
222
  if (!job) {
@@ -182,6 +236,12 @@ export function registerValidationTools(server, deps) {
182
236
  .max(2000000)
183
237
  .default(200000)
184
238
  .describe("Maximum result size."),
239
+ }, {
240
+ title: "Validation job result",
241
+ readOnlyHint: true,
242
+ destructiveHint: false,
243
+ idempotentHint: true,
244
+ openWorldHint: false,
185
245
  }, async ({ jobId, provider, maxChars }) => {
186
246
  const result = deps.asyncJobManager.getJobResult(jobId, maxChars);
187
247
  if (!result) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "llm-cli-gateway",
3
- "version": "2.0.0",
3
+ "version": "2.3.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "llm-cli-gateway",
9
- "version": "2.0.0",
9
+ "version": "2.3.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@modelcontextprotocol/sdk": "^1.29.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-cli-gateway",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "mcpName": "io.github.verivus-oss/llm-cli-gateway",
5
5
  "description": "MCP server providing unified access to Claude Code, Codex, Gemini, Grok, and Mistral Vibe CLIs with session management, retry logic, async job orchestration, durable job results, and cross-LLM validation.",
6
6
  "license": "MIT",