@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.
- package/README.md +9 -2
- package/dist/capabilities.d.ts +64 -0
- package/dist/capabilities.js +65 -0
- package/dist/config/defaults.js +17 -0
- package/dist/custom-rule-store.d.ts +4 -0
- package/dist/custom-rule-store.js +8 -3
- package/dist/dashboard/assets/{index-CyzO6OC7.js → index-CeJbaq6m.js} +1 -1
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/routes/capabilities.d.ts +3 -0
- package/dist/dashboard/routes/capabilities.js +11 -0
- package/dist/dashboard/routes/health.d.ts +5 -1
- package/dist/dashboard/routes/health.js +15 -3
- package/dist/dashboard/routes/rules.js +4 -1
- package/dist/dashboard/routes/traces.d.ts +3 -0
- package/dist/dashboard/routes/traces.js +11 -30
- package/dist/dashboard/seed-demo-data.js +1 -1
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +6 -2
- package/dist/eval/accuracy.d.ts +41 -0
- package/dist/eval/accuracy.js +97 -0
- package/dist/eval/citation-verify/verifier.d.ts +16 -1
- package/dist/eval/citation-verify/verifier.js +14 -4
- package/dist/eval/compose.d.ts +57 -0
- package/dist/eval/compose.js +179 -0
- package/dist/eval/criticality.d.ts +15 -1
- package/dist/eval/criticality.js +6 -0
- package/dist/eval/decision-moment.js +33 -4
- package/dist/eval/dormant.d.ts +4 -0
- package/dist/eval/dormant.js +22 -0
- package/dist/eval/engine.d.ts +6 -2
- package/dist/eval/engine.js +126 -12
- package/dist/eval/failure-classes.d.ts +8 -0
- package/dist/eval/failure-classes.js +18 -0
- package/dist/eval/llm-judge/evaluator.d.ts +30 -0
- package/dist/eval/llm-judge/evaluator.js +26 -2
- package/dist/eval/published-accuracy.d.ts +230 -0
- package/dist/eval/published-accuracy.js +86 -0
- package/dist/eval/questions.d.ts +12 -0
- package/dist/eval/questions.js +14 -0
- package/dist/eval/response-schema.d.ts +652 -0
- package/dist/eval/response-schema.js +130 -0
- package/dist/eval/response.d.ts +12 -0
- package/dist/eval/response.js +30 -0
- package/dist/eval/risk.d.ts +60 -0
- package/dist/eval/risk.js +187 -0
- package/dist/eval/rules/completeness.js +36 -1
- package/dist/eval/rules/cost.d.ts +2 -2
- package/dist/eval/rules/cost.js +50 -6
- package/dist/eval/rules/custom.d.ts +0 -12
- package/dist/eval/rules/custom.js +22 -0
- package/dist/eval/rules/relevance.js +23 -2
- package/dist/eval/rules/safety.d.ts +6 -2
- package/dist/eval/rules/safety.js +224 -51
- package/dist/eval/seeded-random.d.ts +4 -0
- package/dist/eval/seeded-random.js +36 -0
- package/dist/eval/stamp.d.ts +14 -0
- package/dist/eval/stamp.js +89 -0
- package/dist/eval/stats.d.ts +33 -0
- package/dist/eval/stats.js +109 -0
- package/dist/eval/text/checksums.d.ts +23 -0
- package/dist/eval/text/checksums.js +97 -0
- package/dist/eval/text/normalise.d.ts +30 -0
- package/dist/eval/text/normalise.js +265 -0
- package/dist/eval/text/sentences.d.ts +15 -0
- package/dist/eval/text/sentences.js +149 -0
- package/dist/eval/verdict.d.ts +34 -0
- package/dist/eval/verdict.js +131 -0
- package/dist/index.js +5 -28
- package/dist/instructions.d.ts +17 -0
- package/dist/instructions.js +53 -0
- package/dist/judge-enablement.d.ts +34 -0
- package/dist/judge-enablement.js +78 -0
- package/dist/judge-enablement.json +10 -0
- package/dist/preferences.d.ts +1 -1
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.js +29 -0
- package/dist/resources/index.d.ts +5 -2
- package/dist/resources/index.js +65 -5
- package/dist/resources/uris.d.ts +12 -0
- package/dist/resources/uris.js +24 -0
- package/dist/retention.d.ts +20 -0
- package/dist/retention.js +44 -0
- package/dist/self-test.d.ts +1 -0
- package/dist/self-test.js +17 -3
- package/dist/server.d.ts +10 -1
- package/dist/server.js +34 -7
- package/dist/storage/index.js +1 -1
- package/dist/storage/migrations/007-eval-provenance.d.ts +3 -0
- package/dist/storage/migrations/007-eval-provenance.js +30 -0
- package/dist/storage/migrations/index.js +24 -4
- package/dist/storage/sqlite-adapter.d.ts +26 -1
- package/dist/storage/sqlite-adapter.js +149 -15
- package/dist/tools/delete-rule.d.ts +8 -0
- package/dist/tools/delete-rule.js +30 -38
- package/dist/tools/delete-trace.d.ts +5 -0
- package/dist/tools/delete-trace.js +24 -27
- package/dist/tools/deploy-rule.d.ts +13 -1
- package/dist/tools/deploy-rule.js +37 -34
- package/dist/tools/describe.d.ts +20 -0
- package/dist/tools/describe.js +36 -0
- package/dist/tools/errors.d.ts +36 -0
- package/dist/tools/errors.js +134 -0
- package/dist/tools/evaluate-output.d.ts +8 -1
- package/dist/tools/evaluate-output.js +39 -60
- package/dist/tools/evaluate-with-llm-judge.d.ts +34 -0
- package/dist/tools/evaluate-with-llm-judge.js +124 -69
- package/dist/tools/get-traces.d.ts +9 -0
- package/dist/tools/get-traces.js +29 -28
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +22 -1
- package/dist/tools/list-rules.d.ts +13 -0
- package/dist/tools/list-rules.js +43 -46
- package/dist/tools/log-trace.d.ts +4 -0
- package/dist/tools/log-trace.js +31 -29
- package/dist/tools/respond.d.ts +42 -0
- package/dist/tools/respond.js +90 -0
- package/dist/tools/strict-input.js +1 -1
- package/dist/tools/trace-link.d.ts +2 -0
- package/dist/tools/trace-link.js +13 -2
- package/dist/tools/verify-citations.d.ts +18 -2
- package/dist/tools/verify-citations.js +122 -96
- package/dist/types/config.d.ts +44 -0
- package/dist/types/eval.d.ts +309 -0
- package/dist/types/eval.js +2 -1
- package/dist/types/query.d.ts +2 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/dist/resources/dashboard-summary.d.ts +0 -3
- package/dist/resources/dashboard-summary.js +0 -16
- package/dist/resources/trace-detail.d.ts +0 -3
- 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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const key = process.env
|
|
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
|
|
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
|
-
|
|
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
|
|
63
|
-
'
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
'
|
|
69
|
-
|
|
70
|
-
'
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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;
|
package/dist/tools/get-traces.js
CHANGED
|
@@ -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
|
|
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
|
|
77
|
-
''
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
'
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
126
|
-
{
|
|
127
|
-
type: 'text',
|
|
128
|
-
text: JSON.stringify(response),
|
|
129
|
-
},
|
|
130
|
-
],
|
|
131
|
-
};
|
|
132
|
-
});
|
|
132
|
+
return respond(getTracesOutputSchema, response);
|
|
133
|
+
}));
|
|
133
134
|
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/tools/index.js
CHANGED
|
@@ -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;
|
package/dist/tools/list-rules.js
CHANGED
|
@@ -1,59 +1,66 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* list_rules MCP tool —
|
|
2
|
+
* list_rules MCP tool — the rule inventory.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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 (
|
|
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
|
|
30
|
-
description:
|
|
31
|
-
'
|
|
32
|
-
''
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
'',
|
|
37
|
-
|
|
38
|
-
''
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
weight: r.weight,
|
|
81
|
-
critical: r.critical,
|
|
82
|
-
criticalSource: r.criticalSource,
|
|
85
|
+
...r,
|
|
86
|
+
proof: ruleProof(r.name),
|
|
83
87
|
}));
|
|
84
|
-
return {
|
|
85
|
-
|
|
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;
|