@iris-eval/mcp-server 0.8.2 → 0.10.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.
Files changed (131) hide show
  1. package/README.md +9 -2
  2. package/dist/capabilities.d.ts +64 -0
  3. package/dist/capabilities.js +65 -0
  4. package/dist/config/defaults.js +17 -0
  5. package/dist/custom-rule-store.d.ts +4 -0
  6. package/dist/custom-rule-store.js +8 -3
  7. package/dist/dashboard/assets/{index-CyzO6OC7.js → index-CeJbaq6m.js} +1 -1
  8. package/dist/dashboard/index.html +1 -1
  9. package/dist/dashboard/routes/capabilities.d.ts +3 -0
  10. package/dist/dashboard/routes/capabilities.js +11 -0
  11. package/dist/dashboard/routes/health.d.ts +5 -1
  12. package/dist/dashboard/routes/health.js +15 -3
  13. package/dist/dashboard/routes/rules.js +4 -1
  14. package/dist/dashboard/routes/traces.d.ts +3 -0
  15. package/dist/dashboard/routes/traces.js +11 -30
  16. package/dist/dashboard/seed-demo-data.js +1 -1
  17. package/dist/dashboard/server.d.ts +2 -0
  18. package/dist/dashboard/server.js +6 -2
  19. package/dist/eval/accuracy.d.ts +41 -0
  20. package/dist/eval/accuracy.js +97 -0
  21. package/dist/eval/citation-verify/verifier.d.ts +16 -1
  22. package/dist/eval/citation-verify/verifier.js +14 -4
  23. package/dist/eval/compose.d.ts +57 -0
  24. package/dist/eval/compose.js +179 -0
  25. package/dist/eval/criticality.d.ts +15 -1
  26. package/dist/eval/criticality.js +6 -0
  27. package/dist/eval/decision-moment.js +33 -4
  28. package/dist/eval/dormant.d.ts +4 -0
  29. package/dist/eval/dormant.js +22 -0
  30. package/dist/eval/engine.d.ts +6 -2
  31. package/dist/eval/engine.js +126 -12
  32. package/dist/eval/failure-classes.d.ts +8 -0
  33. package/dist/eval/failure-classes.js +18 -0
  34. package/dist/eval/llm-judge/evaluator.d.ts +30 -0
  35. package/dist/eval/llm-judge/evaluator.js +26 -2
  36. package/dist/eval/published-accuracy.d.ts +230 -0
  37. package/dist/eval/published-accuracy.js +86 -0
  38. package/dist/eval/questions.d.ts +12 -0
  39. package/dist/eval/questions.js +14 -0
  40. package/dist/eval/response-schema.d.ts +652 -0
  41. package/dist/eval/response-schema.js +130 -0
  42. package/dist/eval/response.d.ts +12 -0
  43. package/dist/eval/response.js +30 -0
  44. package/dist/eval/risk.d.ts +60 -0
  45. package/dist/eval/risk.js +187 -0
  46. package/dist/eval/rules/completeness.js +36 -1
  47. package/dist/eval/rules/cost.d.ts +2 -2
  48. package/dist/eval/rules/cost.js +50 -6
  49. package/dist/eval/rules/custom.d.ts +0 -12
  50. package/dist/eval/rules/custom.js +22 -0
  51. package/dist/eval/rules/relevance.js +23 -2
  52. package/dist/eval/rules/safety.d.ts +6 -2
  53. package/dist/eval/rules/safety.js +224 -51
  54. package/dist/eval/seeded-random.d.ts +4 -0
  55. package/dist/eval/seeded-random.js +36 -0
  56. package/dist/eval/stamp.d.ts +14 -0
  57. package/dist/eval/stamp.js +89 -0
  58. package/dist/eval/stats.d.ts +33 -0
  59. package/dist/eval/stats.js +109 -0
  60. package/dist/eval/text/checksums.d.ts +23 -0
  61. package/dist/eval/text/checksums.js +97 -0
  62. package/dist/eval/text/normalise.d.ts +30 -0
  63. package/dist/eval/text/normalise.js +265 -0
  64. package/dist/eval/text/sentences.d.ts +15 -0
  65. package/dist/eval/text/sentences.js +149 -0
  66. package/dist/eval/verdict.d.ts +34 -0
  67. package/dist/eval/verdict.js +131 -0
  68. package/dist/index.js +5 -28
  69. package/dist/instructions.d.ts +17 -0
  70. package/dist/instructions.js +53 -0
  71. package/dist/judge-enablement.d.ts +34 -0
  72. package/dist/judge-enablement.js +78 -0
  73. package/dist/judge-enablement.json +10 -0
  74. package/dist/preferences.d.ts +1 -1
  75. package/dist/prompts.d.ts +3 -0
  76. package/dist/prompts.js +29 -0
  77. package/dist/resources/index.d.ts +5 -2
  78. package/dist/resources/index.js +65 -5
  79. package/dist/resources/uris.d.ts +12 -0
  80. package/dist/resources/uris.js +24 -0
  81. package/dist/retention.d.ts +20 -0
  82. package/dist/retention.js +44 -0
  83. package/dist/self-test.d.ts +1 -0
  84. package/dist/self-test.js +17 -3
  85. package/dist/server.d.ts +10 -1
  86. package/dist/server.js +34 -7
  87. package/dist/storage/index.js +1 -1
  88. package/dist/storage/migrations/007-eval-provenance.d.ts +3 -0
  89. package/dist/storage/migrations/007-eval-provenance.js +30 -0
  90. package/dist/storage/migrations/index.js +24 -4
  91. package/dist/storage/sqlite-adapter.d.ts +26 -1
  92. package/dist/storage/sqlite-adapter.js +149 -15
  93. package/dist/tools/delete-rule.d.ts +8 -0
  94. package/dist/tools/delete-rule.js +30 -38
  95. package/dist/tools/delete-trace.d.ts +5 -0
  96. package/dist/tools/delete-trace.js +24 -27
  97. package/dist/tools/deploy-rule.d.ts +13 -1
  98. package/dist/tools/deploy-rule.js +37 -34
  99. package/dist/tools/describe.d.ts +20 -0
  100. package/dist/tools/describe.js +36 -0
  101. package/dist/tools/errors.d.ts +36 -0
  102. package/dist/tools/errors.js +134 -0
  103. package/dist/tools/evaluate-output.d.ts +8 -1
  104. package/dist/tools/evaluate-output.js +39 -60
  105. package/dist/tools/evaluate-with-llm-judge.d.ts +34 -0
  106. package/dist/tools/evaluate-with-llm-judge.js +124 -69
  107. package/dist/tools/get-traces.d.ts +9 -0
  108. package/dist/tools/get-traces.js +29 -28
  109. package/dist/tools/index.d.ts +8 -0
  110. package/dist/tools/index.js +22 -1
  111. package/dist/tools/list-rules.d.ts +13 -0
  112. package/dist/tools/list-rules.js +43 -46
  113. package/dist/tools/log-trace.d.ts +4 -0
  114. package/dist/tools/log-trace.js +31 -29
  115. package/dist/tools/respond.d.ts +42 -0
  116. package/dist/tools/respond.js +90 -0
  117. package/dist/tools/strict-input.js +1 -1
  118. package/dist/tools/trace-link.d.ts +2 -0
  119. package/dist/tools/trace-link.js +13 -2
  120. package/dist/tools/verify-citations.d.ts +18 -2
  121. package/dist/tools/verify-citations.js +122 -96
  122. package/dist/types/config.d.ts +44 -0
  123. package/dist/types/eval.d.ts +309 -0
  124. package/dist/types/eval.js +2 -1
  125. package/dist/types/query.d.ts +2 -0
  126. package/package.json +1 -1
  127. package/server.json +2 -2
  128. package/dist/resources/dashboard-summary.d.ts +0 -3
  129. package/dist/resources/dashboard-summary.js +0 -16
  130. package/dist/resources/trace-detail.d.ts +0 -3
  131. package/dist/resources/trace-detail.js +0 -30
@@ -1,10 +1,15 @@
1
1
  import { z } from 'zod';
2
2
  import { LOCAL_TENANT } from '../types/tenant.js';
3
3
  import { evaluateWithLLMJudge } from '../eval/llm-judge/evaluator.js';
4
- import { findPricing } from '../eval/llm-judge/pricing.js';
4
+ import { findPricing, MODEL_PRICING } from '../eval/llm-judge/pricing.js';
5
5
  import { generateEvalId } from '../utils/ids.js';
6
+ import { JUDGE_COST_CAP_VAR, JUDGE_DEFAULT_COST_CAP_USD, JUDGE_KEY_VARS, judgeCostCapUsd, judgeRecovery } from '../judge-enablement.js';
6
7
  import { strictInput } from './strict-input.js';
7
8
  import { assertTraceExists, insertLinkedEvalResult } from './trace-link.js';
9
+ import { describeTool, ERROR_ENVELOPE_SENTENCE } from './describe.js';
10
+ import { irisError } from './errors.js';
11
+ import { evaluationLinks, guarded, respond } from './respond.js';
12
+ import { CAPABILITIES_RESOURCE_URI } from '../resources/uris.js';
8
13
  const inputSchema = {
9
14
  output: z.string().min(1).describe('The agent output text to evaluate'),
10
15
  template: z
@@ -12,77 +17,104 @@ const inputSchema = {
12
17
  .describe('Judge dimension: accuracy (factual correctness), helpfulness (does it address the ask), safety (harm potential), correctness (vs reference answer — requires `expected`), faithfulness (RAG grounding — requires `source_material`).'),
13
18
  model: z
14
19
  .string()
15
- .describe('Model ID. Supported: anthropic = claude-opus-4-7 | claude-sonnet-4-6 | claude-haiku-4-5 | claude-haiku-4-5-20251001; openai = gpt-4o | gpt-4o-mini | o1-mini.'),
20
+ .describe('Model ID. Supported: anthropic = claude-opus-4-7 | claude-sonnet-4-6 | claude-haiku-4-5 | claude-haiku-4-5-20251001; openai = gpt-4o | gpt-4o-mini | o1-mini. Required — cost varies a hundredfold across models'),
16
21
  provider: z.enum(['anthropic', 'openai']).optional().describe('Auto-detected from model when omitted'),
17
22
  input: z.string().optional().describe('User question / prompt that produced the output (improves accuracy for helpfulness/safety)'),
18
23
  expected: z.string().optional().describe('Reference answer (required for correctness template)'),
19
24
  source_material: z.string().optional().describe('Provided RAG sources (required for faithfulness template)'),
20
25
  trace_id: z.string().optional().describe('Link this evaluation to a stored trace (id from log_trace / get_traces); an unknown id is rejected BEFORE the judge is called'),
21
- max_cost_usd: z.number().positive().optional().describe('Cost cap in USD; defaults to IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL or 0.25'),
26
+ max_cost_usd: z.number().positive().optional().describe(`Cost cap in USD for this call; defaults to ${JUDGE_COST_CAP_VAR} or ${JUDGE_DEFAULT_COST_CAP_USD}. The worst case (two attempts, full max_output_tokens) is computed before the call and refused if it exceeds the cap`),
22
27
  max_output_tokens: z.number().int().positive().max(4096).optional().describe('Judge output token cap; default 512'),
23
28
  temperature: z.number().min(0).max(2).optional().describe('Sampling temperature; default 0 (deterministic)'),
24
29
  timeout_ms: z.number().int().positive().optional().describe('Per-request timeout; default 60_000'),
25
30
  };
26
- function inferProvider(model) {
31
+ /** The models the pricing table knows, so an unknown one can be refused with the valid list. */
32
+ export function supportedModels() {
33
+ return MODEL_PRICING.map((m) => m.model);
34
+ }
35
+ export function inferProvider(model) {
27
36
  const pricing = findPricing(model);
28
37
  if (!pricing) {
29
- throw new Error(`Unknown model "${model}". Provider cannot be inferred. Supported models are listed in src/eval/llm-judge/pricing.ts.`);
38
+ throw irisError('IRIS_JUDGE_UNKNOWN_MODEL', `Unknown model "${model}": its provider and price are not known, so the cost cap cannot be enforced.`, {
39
+ field: 'model',
40
+ valid: supportedModels(),
41
+ recovery: ['Pass one of the supported models (see valid).', 'Pass provider explicitly only for a model in the list whose id is ambiguous.'],
42
+ });
30
43
  }
31
44
  return pricing.provider;
32
45
  }
33
- function resolveApiKey(provider) {
34
- if (provider === 'anthropic') {
35
- const key = process.env.IRIS_ANTHROPIC_API_KEY;
36
- if (!key) {
37
- throw new Error('Anthropic judge requires IRIS_ANTHROPIC_API_KEY. Set it in the environment or use a different provider.');
38
- }
39
- return key;
40
- }
41
- const key = process.env.IRIS_OPENAI_API_KEY;
46
+ /**
47
+ * The key for the provider, from this process's environment. Missing is
48
+ * IRIS_JUDGE_NOT_ENABLED with the enable steps as recovery — the fact
49
+ * users get wrong is that a shell export does not reach the process an
50
+ * MCP client spawns, and the steps say so.
51
+ */
52
+ export function resolveApiKey(provider, toolName = 'evaluate_with_llm_judge') {
53
+ const variable = JUDGE_KEY_VARS[provider];
54
+ const key = process.env[variable];
42
55
  if (!key) {
43
- throw new Error('OpenAI judge requires IRIS_OPENAI_API_KEY. Set it in the environment or use a different provider.');
56
+ throw irisError('IRIS_JUDGE_NOT_ENABLED', `${toolName} needs ${variable} in the environment of the process that runs Iris; no key for ${provider} reached this process. Nothing was spent.`, {
57
+ field: variable,
58
+ recovery: judgeRecovery(provider),
59
+ see: CAPABILITIES_RESOURCE_URI,
60
+ });
44
61
  }
45
62
  return key;
46
63
  }
47
64
  function resolveMaxCost(paramValue) {
48
65
  if (paramValue !== undefined)
49
66
  return paramValue;
50
- const envRaw = process.env.IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL;
51
- if (envRaw) {
52
- const parsed = Number(envRaw);
53
- if (Number.isFinite(parsed) && parsed > 0)
54
- return parsed;
55
- }
56
- return 0.25;
67
+ return judgeCostCapUsd();
57
68
  }
69
+ export const judgeOutputSchema = z.looseObject({
70
+ id: z.string().describe('the evaluation id; read it back at iris://evaluations/{id}'),
71
+ trace_id: z.string().optional().describe('the linked trace, when one was named'),
72
+ score: z.number().describe('0..1 from the judge'),
73
+ passed: z.boolean().describe('the verdict: the score against the template\'s threshold, which is pass_threshold below. Not the model\'s own boolean — that is self_reported_pass'),
74
+ pass_threshold: z.number().describe('the threshold the score was read against, so you can check the arithmetic'),
75
+ self_reported_pass: z.boolean().optional().describe('what the model said about passing, when it said anything. Recorded, never obeyed'),
76
+ disagreement: z.boolean().optional().describe('true when the model\'s own boolean disagrees with the threshold verdict — its rubric and its judgement have come apart on this output'),
77
+ rationale: z.string().describe('the judge\'s reasoning, in its words'),
78
+ dimensions: z.record(z.string(), z.unknown()).describe('per-dimension sub-scores for the template'),
79
+ model: z.string().describe('the model that judged'),
80
+ provider: z.enum(['anthropic', 'openai']).describe('the provider called'),
81
+ template: z.string().describe('the template used'),
82
+ input_tokens: z.number().describe('tokens sent, across both attempts when a retry ran'),
83
+ output_tokens: z.number().describe('tokens received, across both attempts when a retry ran'),
84
+ cost_usd: z.number().nullable().describe('the exact spend from the pricing table'),
85
+ latency_ms: z.number().describe('wall time of the provider call(s)'),
86
+ raw_response_id: z.string().optional().describe('the provider\'s response id, for your own audit'),
87
+ });
58
88
  export function registerEvaluateWithLLMJudgeTool(server, storage) {
59
89
  server.registerTool('evaluate_with_llm_judge', {
60
90
  title: 'Evaluate With LLM Judge',
61
- description: [
62
- 'Score agent output using an LLM as the judge (Anthropic or OpenAI). Returns a 0..1 score with rationale, per-dimension breakdown, and exact cost. The judge\'s own accuracy is measurable on a key you supply and is not yet published — see https://iris-eval.com/proof.',
63
- '',
64
- 'Sibling tools evaluate_output runs heuristic rules (free, deterministic, no API key needed); this tool runs LLM-based semantic scoring (paid, requires an API key). verify_citations is a SPECIALIZED form of LLM judging that focuses on citation grounding only. log_trace / get_traces handle trace I/O; list_rules / deploy_rule / delete_rule manage heuristic-rule lifecycle. evaluate_with_llm_judge is the GENERAL semantic-scoring path.',
65
- '',
66
- 'Behavior. Calls an external LLM API (Anthropic or OpenAI) — costs money per call, takes as long as the provider takes, respects an IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL cap. Non-deterministic at temperature > 0; default temperature=0 gives near-deterministic scores. Writes one eval_result row to Iris storage (linked to trace_id if provided) plus captures provider response id + latency + token counts + cost in the rule_results payload. Rate-limited to 20 req/min on HTTP MCP; your LLM provider also enforces its own rate limits (we transparently retry once on 429).',
67
- '',
68
- 'Output shape. Returns JSON: `{ "id": "<uuid>", "score": 0..1, "passed": boolean, "rationale": string, "dimensions": {...}, "model": string, "provider": "anthropic"|"openai", "template": string, "input_tokens": number, "output_tokens": number, "cost_usd": number, "latency_ms": number }`. `dimensions` has per-dimension sub-scores (e.g., accuracy template returns `{factual_claims, citations, internal_consistency}`).',
69
- '',
70
- 'Use when heuristic rules (via evaluate_output) are too coarse for the quality signal you need semantic correctness, factual accuracy vs a reference, RAG faithfulness to sources, nuanced safety/helpfulness. Pick the template that matches: `accuracy` (hallucination detection), `helpfulness` (does it address the ask), `safety` (harm potential beyond regex PII), `correctness` (vs reference answer pass `expected`), `faithfulness` (RAG grounding — pass `source_material`).',
71
- '',
72
- "Don't use for simple regex/length/keyword checks (use evaluate_output with heuristic rules — they're free, deterministic and in-process). Don't use without an API key set (IRIS_ANTHROPIC_API_KEY or IRIS_OPENAI_API_KEY). Don't use on very large outputs (>8K tokens) without raising max_cost_usd — the pre-check will refuse the call.",
73
- '',
74
- 'Parameters. model is required (no default — pick consciously since cost varies 100x across models). provider is auto-detected from the model name; override only for ambiguous IDs. expected is REQUIRED when template="correctness" (the reference answer to compare against); ignored for other templates. source_material is REQUIRED when template="faithfulness" (the RAG sources to ground against); ignored otherwise. input is optional but improves scoring on helpfulness/safety templates (gives the judge the user prompt that produced the output). max_cost_usd defaults to env var IRIS_LLM_JUDGE_MAX_COST_USD_PER_EVAL or $0.25 — the worst-case cost is computed BEFORE the call (input_tokens × prompt_price + max_output_tokens × completion_price, PLUS the same for the one retry that fires if the judge\'s first reply is not valid JSON); call refused upfront if that two-attempt worst case would exceed. When a retry does run, the reported input_tokens / output_tokens / cost_usd / latency_ms are totals across both attempts. max_output_tokens caps the judge response (default 512, max 4096); higher = more rationale detail + more cost. temperature default 0 (deterministic). timeout_ms default 60000. trace_id optional but recommended (links eval to trace in dashboard). Defaults: temperature=0, max_output_tokens=512, max_cost_usd=$0.25, timeout_ms=60000.',
75
- '',
76
- 'Error modes. Throws when the required API key env var is missing. Throws when trace_id does not match a stored trace — checked before the provider call, so no money is spent and nothing is written. Throws when the estimated worst-case cost exceeds max_cost_usd (raise the cap or trim prompts). Throws LLMJudgeError on provider errors — kind=`auth` on 401/403, `rate_limit` on 429 (auto-retried once), `server_error` on 5xx, `timeout` on abort, `malformed_response` when the judge fails to emit valid JSON on both attempts. Throws "Unknown model" for unsupported model IDs — update src/eval/llm-judge/pricing.ts first.',
77
- ].join('\n'),
91
+ description: describeTool({
92
+ summary: 'Score an output with an LLM judge on your own provider key: a 0..1 score, a rationale, per-dimension sub-scores and the exact spend.',
93
+ does: `Calls Anthropic or OpenAI directly with the key in this process's environment (${JUDGE_KEY_VARS.anthropic} or ${JUDGE_KEY_VARS.openai}); Iris never proxies. ` +
94
+ 'template picks the question: accuracy, helpfulness, safety, correctness (needs expected) or faithfulness (needs source_material); input improves helpfulness and safety. model is required; provider is inferred from it. ' +
95
+ `The worst-case spend — both attempts, full max_output_tokens — is computed BEFORE the call and refused if it exceeds max_cost_usd (default ${JUDGE_COST_CAP_VAR} or ${JUDGE_DEFAULT_COST_CAP_USD}). ` +
96
+ 'temperature defaults to 0; a rate-limited call is retried once. One evaluation row is stored with the provider response id, tokens, cost and latency, linked to trace_id when given. ' +
97
+ "The judge's own accuracy is measurable on a key you supply and is not yet published (see iris://proof).",
98
+ whenNot: 'For length, keyword, PII, injection or cost checks: evaluate_output is free and deterministic. Without a key: the call returns IRIS_JUDGE_NOT_ENABLED with the enable steps do not search for them. On very large outputs without raising max_cost_usd: the pre-check refuses.',
99
+ returns: judgeOutputSchema,
100
+ errors: 'IRIS_JUDGE_NOT_ENABLED (no key for the provider reached this process; recovery carries the steps). IRIS_JUDGE_UNKNOWN_MODEL (valid lists the models). IRIS_UNKNOWN_TRACE, checked before any spend. ' +
101
+ 'IRIS_BUDGET_EXCEEDED (nothing spent; the message carries both numbers). IRIS_PROVIDER_ERROR with kind auth, rate_limit, bad_request, server_error, timeout or malformed_response, and retryable set. ' +
102
+ ERROR_ENVELOPE_SENTENCE,
103
+ siblings: {
104
+ evaluate_output: 'the free deterministic path',
105
+ verify_citations: 'citation grounding, the narrower judge',
106
+ log_trace: 'record the execution first',
107
+ },
108
+ }),
78
109
  inputSchema: strictInput(inputSchema),
110
+ outputSchema: judgeOutputSchema,
79
111
  annotations: {
80
112
  readOnlyHint: false, // Writes eval_result; also spends money (external API cost)
81
113
  destructiveHint: false, // Creates data; doesn't overwrite or delete
82
114
  idempotentHint: false, // Temperature > 0 may vary; even at T=0 provider non-determinism is possible; cost also varies per call
83
115
  openWorldHint: true, // Calls external APIs (Anthropic / OpenAI) — touches the world beyond local process
84
116
  },
85
- }, async (args) => {
117
+ }, guarded(async (args) => {
86
118
  const provider = args.provider ?? inferProvider(args.model);
87
119
  const apiKey = resolveApiKey(provider);
88
120
  const maxCostUsd = resolveMaxCost(args.max_cost_usd);
@@ -95,8 +127,8 @@ export function registerEvaluateWithLLMJudgeTool(server, storage) {
95
127
  const result = await evaluateWithLLMJudge({
96
128
  output: args.output,
97
129
  template: args.template,
98
- provider,
99
130
  model: args.model,
131
+ provider,
100
132
  apiKey,
101
133
  input: args.input,
102
134
  expected: args.expected,
@@ -106,12 +138,12 @@ export function registerEvaluateWithLLMJudgeTool(server, storage) {
106
138
  temperature: args.temperature,
107
139
  timeoutMs: args.timeout_ms,
108
140
  });
141
+ // Persist to eval_results so the dashboard can surface it.
142
+ // eval_type='custom' — LLM judge scores span all 4 heuristic
143
+ // categories (accuracy, helpfulness, safety, faithfulness); 'custom'
144
+ // is the honest bucket. rule_results[0] captures per-dimension
145
+ // breakdown + provider metadata for audit.
109
146
  const evalId = generateEvalId();
110
- // Persist as a normal eval_result so the dashboard picks it up
111
- // alongside heuristic scores. eval_type is 'custom' because LLM
112
- // judge doesn't fit completeness/relevance/safety/cost taxonomy
113
- // cleanly — it spans all four. The rule_results payload carries
114
- // the full judge provenance.
115
147
  await insertLinkedEvalResult(storage, LOCAL_TENANT, {
116
148
  id: evalId,
117
149
  trace_id: args.trace_id,
@@ -126,34 +158,57 @@ export function registerEvaluateWithLLMJudgeTool(server, storage) {
126
158
  passed: result.passed,
127
159
  score: result.score,
128
160
  message: result.rationale || 'LLM judge evaluation',
161
+ /*
162
+ * The row says what KIND of claim it is (0.10.0). Without it a
163
+ * stored judge evaluation read back through the composer had no
164
+ * layer to fall into — not a policy, not a detector with a
165
+ * published rate — and a FAILED judgement read back as clean.
166
+ * A judgment the caller asked and paid for decides.
167
+ */
168
+ kind: 'judgment',
169
+ role: 'gate',
170
+ saw: ['output'],
171
+ evidence: [
172
+ {
173
+ type: 'sample',
174
+ score: result.score,
175
+ ...(result.selfReportedPass !== undefined ? { selfReportedPass: result.selfReportedPass } : {}),
176
+ rationaleHash: '',
177
+ },
178
+ ],
179
+ uncertainty: {
180
+ basis: 'unmeasured',
181
+ why: 'the judge is user-keyed and its accuracy is measured only by a run on a key you or the maintainer supplies (npm run proof:judge)',
182
+ },
129
183
  },
130
184
  ],
131
185
  suggestions: result.passed ? [] : [result.rationale],
132
186
  rules_evaluated: 1,
133
187
  rules_skipped: 0,
134
188
  insufficient_data: false,
189
+ // What the evaluation itself cost — the description promised it was
190
+ // kept and the write path stored none of it (arc zero, G15).
191
+ eval_cost_usd: result.costUsd ?? undefined,
192
+ eval_tokens: result.inputTokens + result.outputTokens,
135
193
  });
136
- return {
137
- content: [
138
- {
139
- type: 'text',
140
- text: JSON.stringify({
141
- id: evalId,
142
- score: result.score,
143
- passed: result.passed,
144
- rationale: result.rationale,
145
- dimensions: result.dimensions,
146
- model: result.model,
147
- provider: result.provider,
148
- template: result.template,
149
- input_tokens: result.inputTokens,
150
- output_tokens: result.outputTokens,
151
- cost_usd: result.costUsd,
152
- latency_ms: result.latencyMs,
153
- raw_response_id: result.rawResponseId,
154
- }),
155
- },
156
- ],
157
- };
158
- });
194
+ return respond(judgeOutputSchema, {
195
+ id: evalId,
196
+ ...(args.trace_id ? { trace_id: args.trace_id } : {}),
197
+ score: result.score,
198
+ passed: result.passed,
199
+ pass_threshold: result.passThreshold,
200
+ ...(result.selfReportedPass !== undefined ? { self_reported_pass: result.selfReportedPass } : {}),
201
+ ...(result.disagreement ? { disagreement: true } : {}),
202
+ rationale: result.rationale,
203
+ dimensions: result.dimensions,
204
+ model: result.model,
205
+ provider: result.provider,
206
+ template: result.template,
207
+ input_tokens: result.inputTokens,
208
+ output_tokens: result.outputTokens,
209
+ cost_usd: result.costUsd,
210
+ latency_ms: result.latencyMs,
211
+ raw_response_id: result.rawResponseId,
212
+ }, evaluationLinks(evalId, args.trace_id));
213
+ }));
159
214
  }
@@ -27,4 +27,13 @@ export interface TraceRangeArgs {
27
27
  * bound the tool rejects is never one the HTTP query quietly accepts.
28
28
  */
29
29
  export declare function addTraceRangeIssues(args: TraceRangeArgs, ctx: z.RefinementCtx): void;
30
+ export declare const getTracesOutputSchema: z.ZodObject<{
31
+ traces: z.ZodArray<z.ZodObject<{
32
+ trace_id: z.ZodString;
33
+ }, z.core.$loose>>;
34
+ total: z.ZodNumber;
35
+ limit: z.ZodNumber;
36
+ offset: z.ZodNumber;
37
+ summary: z.ZodOptional<z.ZodObject<{}, z.core.$loose>>;
38
+ }, z.core.$loose>;
30
39
  export declare function registerGetTracesTool(server: McpServer, storage: IStorageAdapter): void;
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { LOCAL_TENANT } from '../types/tenant.js';
3
3
  import { strictInput } from './strict-input.js';
4
+ import { describeTool, ERROR_ENVELOPE_SENTENCE } from './describe.js';
5
+ import { guarded, respond } from './respond.js';
4
6
  /*
5
7
  * An ISO-8601 instant (2026-08-01T00:00:00Z, offsets allowed) or calendar
6
8
  * date (2026-08-01). Stored timestamps are ISO strings and the adapter
@@ -61,7 +63,7 @@ const inputSchema = {
61
63
  // Mirrors traceQuerySchema in dashboard/validation.ts — both capture paths
62
64
  // (MCP tool, HTTP query) enforce the same 1..1000 bound. Unclamped, limit:-1
63
65
  // meant "LIMIT -1" in SQLite, i.e. every row (#332).
64
- limit: z.number().int().min(1).max(1000).default(50).describe('Results per page (default 50, max 1000 — values >1000 return 400)'),
66
+ limit: z.number().int().min(1).max(1000).default(50).describe('Results per page (default 50, max 1000 — values above are rejected)'),
65
67
  offset: z.number().int().min(0).default(0).describe('Zero-based pagination offset — skip first N results (non-negative integer)'),
66
68
  sort_by: z.enum(['timestamp', 'latency_ms', 'cost_usd']).default('timestamp').describe('Sort by timestamp | latency_ms | cost_usd (default timestamp)'),
67
69
  sort_order: z.enum(['asc', 'desc']).default('desc').describe('Sort order: asc | desc (default desc — most recent / highest first)'),
@@ -69,34 +71,40 @@ const inputSchema = {
69
71
  };
70
72
  // Cross-field range checks — see addTraceRangeIssues above.
71
73
  const inputSchemaWithRanges = strictInput(inputSchema).superRefine(addTraceRangeIssues);
74
+ export const getTracesOutputSchema = z.looseObject({
75
+ traces: z.array(z.looseObject({ trace_id: z.string() })).describe('the page of traces: trace_id, agent_name, framework, input, output, tool_calls, latency_ms, token_usage, cost_usd, metadata, timestamp'),
76
+ total: z.number().int().describe('how many traces match the filters, across every page'),
77
+ limit: z.number().int().describe('the page size applied'),
78
+ offset: z.number().int().describe('the offset applied'),
79
+ summary: z.looseObject({}).optional().describe('the dashboard aggregates for the last hour, when include_summary was true'),
80
+ });
72
81
  export function registerGetTracesTool(server, storage) {
73
82
  server.registerTool('get_traces', {
74
83
  title: 'Get Traces',
75
- description: [
76
- 'Query stored agent-execution traces with filters, pagination, and optional dashboard summary.',
77
- '',
78
- 'Sibling tools log_trace creates traces, delete_trace removes a single trace, evaluate_output / evaluate_with_llm_judge / verify_citations score them, list_rules / deploy_rule / delete_rule manage the custom-rule lifecycle. get_traces is the READ path for historical agent executions — never mutates anything.',
79
- '',
80
- 'Behavior. Read-only: never mutates storage, never calls external services. Idempotent: repeated calls with the same args return consistent results (new traces logged after the call obviously show up on subsequent calls). Tenant-scoped: queries only the caller\'s tenant rows (LOCAL_TENANT in OSS). Paginates results (default limit 50, max 1000). Rate-limited to 20 req/min on HTTP MCP, unlimited on stdio.',
81
- '',
82
- 'Output shape. Returns JSON: `{ "traces": [{...traceRow}], "total": number, "limit": number, "offset": number, "summary"?: { total_traces, avg_latency_ms, total_cost_usd, error_rate, eval_pass_rate, traces_per_hour, top_agents } }`. Each trace row includes trace_id, agent_name, framework, input, output, tool_calls, latency_ms, token_usage, cost_usd, metadata, timestamp. `summary` only included when `include_summary: true`.',
83
- '',
84
- 'Use when you need historical data: investigating a past failure, computing quality trends, comparing agents, or feeding an analytics job. Set `agent_name` / `framework` / `since` / `until` to narrow the query. Set `min_score` / `max_score` to surface outliers. Set `sort_by: "cost_usd"` + `sort_order: "desc"` to find the most expensive traces. Set `include_summary: true` when you want dashboard-style aggregates in one round-trip.',
85
- '',
86
- 'Don\'t use to score a trace (use evaluate_output). Don\'t use to create a trace (use log_trace). Don\'t use as a live event stream — it\'s a query, not a subscription, and Iris has no event-stream endpoint; poll with exponential backoff.',
87
- '',
88
- 'Parameters. limit defaults to 50, max 1000 (anything higher returns 400). offset is zero-based pagination (non-negative integer). since / until must be ISO 8601 timestamps or dates — `since` is inclusive (timestamp >= since), `until` is inclusive (timestamp <= until), and `since` may not be later than `until`. min_score / max_score are 0..1 and filter on the LATEST eval per trace, not all evals (so a trace with one failing + one passing eval may or may not match depending on which landed last); min_score may not exceed max_score. Combining since + sort_by="latency_ms" + sort_order="desc" is the canonical "find slow recent traces" query. include_summary returns dashboard-style aggregates in the SAME response (saves a round-trip; use true for dashboard ingest, false for analytics queries that don\'t need them). agent_name and framework are exact-match (no wildcards). Defaults: limit=50, offset=0, sort_by="timestamp", sort_order="desc", include_summary=false.',
89
- '',
90
- 'Error modes. Returns 400 on invalid sort_by / sort_order (Zod enum). Returns 400 if limit > 1000 or offset < 0. Returns 400 — naming both values — on an empty range: min_score > max_score, since later than until, a score outside 0..1, or a since/until that is not an ISO 8601 timestamp or date (an unparseable bound is refused, never silently ignored). Returns 429 when HTTP rate limit exceeded. Storage failures propagate as 500. Empty result with `total: 0` on no matches (not an error).',
91
- ].join('\n'),
84
+ description: describeTool({
85
+ summary: 'Query stored traces with filters, pagination and sorting; optionally include the dashboard summary in the same response.',
86
+ does: 'Read-only, local storage only. Filters are exact-match (agent_name, framework), inclusive time bounds (since, until — an ISO 8601 timestamp or date) and a score range applied to the LATEST evaluation of each trace (min_score, max_score, 0..1). ' +
87
+ 'limit is 1..1000 (default 50), offset counts from 0, sort_by is timestamp, latency_ms or cost_usd, sort_order asc or desc (default: newest first). include_summary adds the one-hour dashboard aggregates. ' +
88
+ 'A crossed range (min above max, since after until) is refused naming both values rather than returning an empty page that reads as "no such traces".',
89
+ whenNot: 'To score a trace (evaluate_output). To create one (log_trace). As a live stream: this is a query, and Iris has no event stream poll with backoff.',
90
+ returns: getTracesOutputSchema,
91
+ errors: 'IRIS_STORAGE_ERROR when the database cannot be read. An out-of-range or crossed bound is refused before the handler runs, naming the values. An empty result is total 0, not an error. ' +
92
+ ERROR_ENVELOPE_SENTENCE,
93
+ siblings: {
94
+ log_trace: 'record an execution',
95
+ evaluate_output: 'score one output',
96
+ delete_trace: 'remove one trace',
97
+ },
98
+ }),
92
99
  inputSchema: inputSchemaWithRanges,
100
+ outputSchema: getTracesOutputSchema,
93
101
  annotations: {
94
102
  readOnlyHint: true, // Pure query: never writes, never deletes
95
103
  destructiveHint: false, // Inverse of readOnly — trivially false
96
104
  idempotentHint: true, // Same args → same result (modulo new traces that may have landed since)
97
105
  openWorldHint: false, // Queries local storage only; no external network
98
106
  },
99
- }, async (args) => {
107
+ }, guarded(async (args) => {
100
108
  // OSS single-tenant: MCP caller is the local user.
101
109
  const result = await storage.queryTraces(LOCAL_TENANT, {
102
110
  filter: {
@@ -121,13 +129,6 @@ export function registerGetTracesTool(server, storage) {
121
129
  if (args.include_summary) {
122
130
  response.summary = await storage.getDashboardSummary(LOCAL_TENANT);
123
131
  }
124
- return {
125
- content: [
126
- {
127
- type: 'text',
128
- text: JSON.stringify(response),
129
- },
130
- ],
131
- };
132
- });
132
+ return respond(getTracesOutputSchema, response);
133
+ }));
133
134
  }
@@ -2,4 +2,12 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { IStorageAdapter } from '../types/query.js';
3
3
  import type { EvalEngine } from '../eval/engine.js';
4
4
  import type { CustomRuleStore } from '../custom-rule-store.js';
5
+ /**
6
+ * Every tool this server registers, by name. The capabilities object
7
+ * lists it, the docs contract checks prose against it, and a test asserts
8
+ * it equals what tools/list returns — so a tool added below without a
9
+ * name here (or the reverse) fails before it ships.
10
+ */
11
+ export declare const TOOL_NAMES: readonly ["log_trace", "evaluate_output", "get_traces", "list_rules", "deploy_rule", "delete_rule", "delete_trace", "evaluate_with_llm_judge", "verify_citations"];
12
+ export type ToolName = (typeof TOOL_NAMES)[number];
5
13
  export declare function registerAllTools(server: McpServer, storage: IStorageAdapter, evalEngine: EvalEngine, customRuleStore: CustomRuleStore): void;
@@ -7,9 +7,30 @@ import { registerDeleteRuleTool } from './delete-rule.js';
7
7
  import { registerDeleteTraceTool } from './delete-trace.js';
8
8
  import { registerEvaluateWithLLMJudgeTool } from './evaluate-with-llm-judge.js';
9
9
  import { registerVerifyCitationsTool } from './verify-citations.js';
10
+ import { dormantRulesFrom } from '../eval/dormant.js';
11
+ import { LOCAL_TENANT } from '../types/tenant.js';
12
+ /**
13
+ * Every tool this server registers, by name. The capabilities object
14
+ * lists it, the docs contract checks prose against it, and a test asserts
15
+ * it equals what tools/list returns — so a tool added below without a
16
+ * name here (or the reverse) fails before it ships.
17
+ */
18
+ export const TOOL_NAMES = [
19
+ 'log_trace',
20
+ 'evaluate_output',
21
+ 'get_traces',
22
+ 'list_rules',
23
+ 'deploy_rule',
24
+ 'delete_rule',
25
+ 'delete_trace',
26
+ 'evaluate_with_llm_judge',
27
+ 'verify_citations',
28
+ ];
10
29
  export function registerAllTools(server, storage, evalEngine, customRuleStore) {
11
30
  registerLogTraceTool(server, storage);
12
- registerEvaluateOutputTool(server, storage, evalEngine);
31
+ registerEvaluateOutputTool(server, storage, evalEngine, {
32
+ dormant: () => dormantRulesFrom(customRuleStore.quarantined(LOCAL_TENANT)),
33
+ });
13
34
  registerGetTracesTool(server, storage);
14
35
  registerListRulesTool(server, customRuleStore, evalEngine);
15
36
  registerDeployRuleTool(server, customRuleStore, evalEngine);
@@ -1,4 +1,17 @@
1
+ import { z } from 'zod';
1
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
3
  import type { CustomRuleStore } from '../custom-rule-store.js';
3
4
  import type { EvalEngine } from '../eval/engine.js';
5
+ export declare const listRulesOutputSchema: z.ZodObject<{
6
+ rules: z.ZodArray<z.ZodObject<{
7
+ id: z.ZodString;
8
+ name: z.ZodString;
9
+ }, z.core.$loose>>;
10
+ total: z.ZodNumber;
11
+ enabled_count: z.ZodNumber;
12
+ built_in: z.ZodArray<z.ZodObject<{
13
+ name: z.ZodString;
14
+ }, z.core.$loose>>;
15
+ quarantined: z.ZodArray<z.ZodUnknown>;
16
+ }, z.core.$loose>;
4
17
  export declare function registerListRulesTool(server: McpServer, customRuleStore: CustomRuleStore, evalEngine: EvalEngine): void;
@@ -1,59 +1,66 @@
1
1
  /*
2
- * list_rules MCP tool — enumerate deployed custom rules.
2
+ * list_rules MCP tool — the rule inventory.
3
3
  *
4
- * Read-only view into the custom-rule store (~/.iris/custom-rules.json).
5
- * Lets agents discover what rules are deployed, what each one evaluates,
6
- * and which are enabled so an agent can decide whether to call
7
- * evaluate_output at all, and which eval_type to route through.
8
- *
9
- * Companion to deploy_rule / delete_rule. Together these replace the
10
- * dashboard-only Make-This-A-Rule composer when an agent (not a human)
11
- * needs to manage the rule set programmatically.
4
+ * Two halves. `built_in` is the shipped roster with everything a caller
5
+ * needs to trust a verdict: what each rule is (kind, mechanism), what it
6
+ * reads (needs absent means it skips), the question it answers, the
7
+ * criticality THIS server applies and who decided it, and its published
8
+ * accuracy. `rules` is the custom-rule store (~/.iris/custom-rules.json),
9
+ * the read path deploy_rule and delete_rule write to.
12
10
  */
13
11
  import { z } from 'zod';
14
12
  import { builtInRuleRoster } from '../eval/criticality.js';
13
+ import { ruleProof } from '../capabilities.js';
15
14
  import { LOCAL_TENANT } from '../types/tenant.js';
16
15
  import { strictInput } from './strict-input.js';
16
+ import { describeTool, ERROR_ENVELOPE_SENTENCE } from './describe.js';
17
+ import { guarded, respond } from './respond.js';
18
+ import { PROOF_RESOURCE_URI } from '../resources/uris.js';
17
19
  const inputSchema = {
18
20
  eval_type: z
19
21
  .enum(['completeness', 'relevance', 'safety', 'cost', 'custom'])
20
22
  .optional()
21
- .describe('Filter to rules of a specific eval category'),
23
+ .describe('Filter the custom rules to one eval category (exact match); built_in is never filtered'),
22
24
  enabled_only: z
23
25
  .boolean()
24
26
  .default(false)
25
- .describe('Return only enabled rules (excludes disabled ones)'),
27
+ .describe('Return only enabled custom rules (a rule disabled with delete_rule stays in the store and does not fire)'),
26
28
  };
29
+ export const listRulesOutputSchema = z.looseObject({
30
+ rules: z.array(z.looseObject({ id: z.string(), name: z.string() })).describe('the deployed custom rules after the filters: id, name, description, evalType, severity, definition, enabled, createdAt, updatedAt, version, sourceMomentId'),
31
+ total: z.number().int().describe('custom rules after the filters'),
32
+ enabled_count: z.number().int().describe('of those, how many are enabled'),
33
+ built_in: z.array(z.looseObject({ name: z.string() })).describe('the shipped roster, never filtered: name, category, description, weight, kind, mechanism, needs, question, classes, version, the EFFECTIVE critical flag with criticalSource, and proof (published precision, recall, intervals and ppvAt from https://iris-eval.com/proof; null where the proof is a conformance check)'),
34
+ quarantined: z.array(z.unknown()).describe('entries in the store this version could not validate; they do not fire and are never deleted by a deploy'),
35
+ });
27
36
  export function registerListRulesTool(server, customRuleStore, evalEngine) {
28
37
  server.registerTool('list_rules', {
29
- title: 'List Custom Rules',
30
- description: [
31
- 'Enumerate deployed custom evaluation rules from the local rule store.',
32
- '',
33
- 'Sibling tools deploy_rule adds custom rules, delete_rule removes them, evaluate_output runs them against agent output. log_trace / get_traces / delete_trace handle the trace lifecycle separately. list_rules is the READ path for the custom-rule store; nothing else exposes the inventory.',
34
- '',
35
- 'Behavior. Pure read of ~/.iris/custom-rules.json (in-memory cached; no disk read per call after server boot). No mutation, no external network. Returns the rules of the local tenant. Rate-limited to 20 req/min on HTTP MCP, unlimited on stdio.',
36
- '',
37
- 'Output shape. Returns JSON: `{ "rules": [{ "id": "rule-XXXX", "name", "description", "evalType", "severity", "definition": { name, type, config, weight? }, "enabled": boolean, "createdAt": ISO timestamp, "updatedAt": ISO timestamp, "version": number, "sourceMomentId?": string }], "total": number, "enabled_count": number, "built_in": [{ "name", "category", "weight", "critical": boolean, "criticalSource": "default" | "config" }] }`. Empty `rules` array + total=0 when no custom rules are deployed. A deployed rule fires only on evaluate_output calls whose eval_type equals its evalType (or eval_type="all", which runs every bundle). `built_in` is the shipped rule set, always present and NOT narrowed by the filters; `total` and `enabled_count` count custom rules only.',
38
- '',
39
- 'Why `built_in` carries criticality. A critical rule vetoes `passed` regardless of the weighted score, and which built-in rules are critical is configurable (`eval.criticalRules` / `eval.nonCriticalRules`). `critical` is the EFFECTIVE value this server applies and `criticalSource` says who decided it: `default` is the declaration on the rule itself, `config` means one of those lists named it. Read it before trusting a `passed: true` — it is how you tell "nothing was violated" from "the rule that would have vetoed is demoted on this server".',
40
- '',
41
- 'Use when you need to know what custom rules are currently live (before calling evaluate_output, before deploying a similar rule to avoid duplicates, or when building a dashboard view). Filter with `eval_type` to scope to a specific category, or `enabled_only: true` to exclude disabled rules. Use get_traces to see trace data; use evaluate_output to run scoring; use list_rules only when you need the RULE INVENTORY.',
42
- '',
43
- "Don't use to count traces or evals (that's get_traces). Don't use to deploy a rule (use deploy_rule); don't use to remove one (use delete_rule). Built-in rules are not in the store and cannot be deployed, deleted or disabled — they appear under `built_in` for reference, carrying the criticality this server applies.",
44
- '',
45
- 'Parameters. eval_type filter is exact-match against each rule\'s evalType field (no wildcards). enabled_only excludes rules that are deployed-but-disabled — a rule is disabled without deleting it via delete_rule with `enabled: false` (and re-enabled with `enabled: true`), or from the dashboard; disabled rules stay in the store with their history but do not fire. Both filters are AND-combined when both are set. Both are optional; with no filter, all rules return. Defaults: eval_type=undefined (no filter), enabled_only=false (returns all rules including disabled).',
46
- '',
47
- "Error modes. Returns empty list if the rule store file doesn't exist (first run). Returns 429 if HTTP rate limit exceeded. Never throws on valid input.",
48
- ].join('\n'),
38
+ title: 'List Rules',
39
+ description: describeTool({
40
+ summary: 'The rule inventory: the built-in roster with what each rule needs, the criticality this server applies and its published accuracy, plus every deployed custom rule.',
41
+ does: 'Read-only, no network. built_in is the shipped roster and is never narrowed by the filters. For each rule: kind (measurement, detection, inference, judgment, policy, verification), mechanism, needs (the inputs it reads — absent means the rule skips, never passes), question, classes, version, weight, ' +
42
+ 'the EFFECTIVE critical flag with criticalSource (default, or config when eval.criticalRules / eval.nonCriticalRules changed it on this server read it before trusting a passed: true), ' +
43
+ 'and proof: precision and recall with 95% intervals and the positive predictive value at four prevalences, the numbers published at https://iris-eval.com/proof. ' +
44
+ 'rules is the custom-rule store, filterable by eval_type and enabled_only; total and enabled_count count custom rules. quarantined lists store entries this version could not validate; they do not fire.',
45
+ whenNot: 'To count traces (get_traces). To add, remove or pause a rule (deploy_rule, delete_rule). Built-in rules are not in the store and cannot be deployed, deleted or disabled.',
46
+ returns: listRulesOutputSchema,
47
+ errors: 'IRIS_INTERNAL_ERROR if the store file cannot be read. A missing store file is an empty list, not an error. ' +
48
+ ERROR_ENVELOPE_SENTENCE,
49
+ siblings: {
50
+ deploy_rule: 'add a custom rule',
51
+ delete_rule: 'remove, disable or re-enable one',
52
+ evaluate_output: 'run the rules',
53
+ },
54
+ }),
49
55
  inputSchema: strictInput(inputSchema),
56
+ outputSchema: listRulesOutputSchema,
50
57
  annotations: {
51
58
  readOnlyHint: true,
52
59
  destructiveHint: false,
53
60
  idempotentHint: true,
54
61
  openWorldHint: false,
55
62
  },
56
- }, async (args) => {
63
+ }, guarded(async (args) => {
57
64
  // OSS: MCP tools operate under LOCAL_TENANT. Cloud multi-tenant
58
65
  // exposure is a v0.5 architectural item (MCP SDK doesn't pass
59
66
  // session/tenant context to tool handlers).
@@ -75,19 +82,9 @@ export function registerListRulesTool(server, customRuleStore, evalEngine) {
75
82
  * purpose — the filters describe the custom-rule store.
76
83
  */
77
84
  const built_in = builtInRuleRoster((rule) => evalEngine.effectiveCriticality(rule)).map((r) => ({
78
- name: r.name,
79
- category: r.category,
80
- weight: r.weight,
81
- critical: r.critical,
82
- criticalSource: r.criticalSource,
85
+ ...r,
86
+ proof: ruleProof(r.name),
83
87
  }));
84
- return {
85
- content: [
86
- {
87
- type: 'text',
88
- text: JSON.stringify({ rules, total, enabled_count, built_in }),
89
- },
90
- ],
91
- };
92
- });
88
+ return respond(listRulesOutputSchema, { rules, total, enabled_count, built_in, quarantined: customRuleStore.quarantined(LOCAL_TENANT) }, [{ uri: PROOF_RESOURCE_URI, name: 'proof', description: 'The published accuracy of every measured rule, with the corpus it was measured on' }]);
89
+ }));
93
90
  }
@@ -58,4 +58,8 @@ export declare const logTraceInputShape: {
58
58
  }, z.core.$strip>>>;
59
59
  timestamp: z.ZodOptional<z.ZodString>;
60
60
  };
61
+ export declare const logTraceOutputSchema: z.ZodObject<{
62
+ trace_id: z.ZodString;
63
+ status: z.ZodLiteral<"stored">;
64
+ }, z.core.$loose>;
61
65
  export declare function registerLogTraceTool(server: McpServer, storage: IStorageAdapter): void;