@iris-eval/mcp-server 0.8.1 → 0.9.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 +2 -0
- package/dist/custom-rule-store.d.ts +4 -0
- package/dist/custom-rule-store.js +8 -3
- package/dist/dashboard/assets/{index-BfMShR3p.js → index-Cz8_oOqG.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 +9 -28
- 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/criticality.d.ts +8 -1
- package/dist/eval/criticality.js +6 -0
- package/dist/eval/dormant.d.ts +4 -0
- package/dist/eval/dormant.js +22 -0
- package/dist/eval/engine.d.ts +1 -0
- package/dist/eval/engine.js +49 -3
- 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 +10 -0
- package/dist/eval/llm-judge/evaluator.js +16 -1
- 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/rules/completeness.js +31 -0
- package/dist/eval/rules/cost.d.ts +1 -1
- package/dist/eval/rules/cost.js +44 -0
- package/dist/eval/rules/custom.d.ts +0 -12
- package/dist/eval/rules/custom.js +21 -0
- package/dist/eval/rules/relevance.js +16 -0
- package/dist/eval/rules/safety.js +178 -1
- package/dist/eval/stamp.d.ts +14 -0
- package/dist/eval/stamp.js +88 -0
- package/dist/eval/stats.d.ts +33 -0
- package/dist/eval/stats.js +109 -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 +14 -0
- 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 +135 -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 +37 -58
- package/dist/tools/evaluate-with-llm-judge.d.ts +31 -0
- package/dist/tools/evaluate-with-llm-judge.js +96 -69
- package/dist/tools/get-traces.d.ts +9 -0
- package/dist/tools/get-traces.js +30 -29
- 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 +17 -2
- package/dist/tools/verify-citations.js +98 -93
- package/dist/types/config.d.ts +9 -0
- package/dist/types/eval.d.ts +258 -0
- package/dist/types/eval.js +2 -1
- package/dist/types/query.d.ts +2 -0
- package/package.json +1 -1
- package/server.json +72 -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,16 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { toEvaluationResponse } from '../eval/response.js';
|
|
3
|
+
import { evaluateOutputResponseSchema } from '../eval/response-schema.js';
|
|
2
4
|
import { DEFAULT_EVAL_TYPE, DEFAULT_EVAL_TYPE_NOTE } from '../eval/engine.js';
|
|
3
5
|
import { INJECTION_SCOPE_SENTENCE } from '../eval/rules/safety.js';
|
|
4
6
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
5
7
|
import { strictInput, strictNested } from './strict-input.js';
|
|
6
8
|
import { toolCallSchema } from './log-trace.js';
|
|
7
9
|
import { getTraceOrThrow, insertLinkedEvalResult } from './trace-link.js';
|
|
10
|
+
import { describeTool, ERROR_ENVELOPE_SENTENCE } from './describe.js';
|
|
11
|
+
import { evaluationLinks, guarded, respond } from './respond.js';
|
|
12
|
+
/** The most inline custom rules one call may carry (see the argument description). */
|
|
13
|
+
export const MAX_INLINE_CUSTOM_RULES = 10;
|
|
8
14
|
/*
|
|
9
15
|
* Strict one level down (#376): `{ name, type, config, wieght: 5 }` used to
|
|
10
16
|
* parse with `wieght` silently discarded, so the rule ran at weight 1 and
|
|
@@ -31,13 +37,13 @@ const inputSchema = {
|
|
|
31
37
|
eval_type: z.enum(['completeness', 'relevance', 'safety', 'cost', 'custom', 'all']).optional().describe('Rule bundle to apply: completeness | relevance | safety | cost | custom | all — picks which built-in rules fire. "all" runs every bundle in one call and adds a per-category breakdown. Defaults to "all" when omitted — every bundle runs, safety included, and the response carries a note saying the default ran'),
|
|
32
38
|
expected: z.string().optional().describe('Expected output for comparison — consulted only by the completeness bundle\'s expected_coverage rule; NOT used by relevance (the relevance rules compare the output against `input`)'),
|
|
33
39
|
input: z.string().optional().describe('Original input for context (the ask + any source material the agent was given) — REQUIRED when eval_type="relevance" (keyword_overlap and topic_consistency compare the output against it and skip without it); also grounds the safety bundle\'s hallucination signals'),
|
|
34
|
-
trace_id: z.string().optional().describe('Link evaluation to a trace — surfaces this eval in the dashboard\'s trace drill-through. Must be the id of a stored trace (from log_trace / get_traces); an unknown id is rejected before anything is evaluated'),
|
|
40
|
+
trace_id: z.string().optional().describe('Link evaluation to a trace — surfaces this eval in the dashboard\'s trace drill-through and lets the tool reuse the trace\'s stored tool_calls. Must be the id of a stored trace (from log_trace / get_traces); an unknown id is rejected before anything is evaluated'),
|
|
35
41
|
// .max(10): inline rules skip the deploy-time probe, and the engine runs
|
|
36
42
|
// rules synchronously — without a cap, one request carrying N sandbox-
|
|
37
43
|
// defeating regex rules stalls the server linearly in N (measured 9.3s at
|
|
38
44
|
// N=50). Ten is ample for per-call rules; persistent sets belong in
|
|
39
45
|
// deploy_rule, where deploy-time validation probes each pattern.
|
|
40
|
-
custom_rules: z.array(CustomRuleSchema).max(
|
|
46
|
+
custom_rules: z.array(CustomRuleSchema).max(MAX_INLINE_CUSTOM_RULES).optional().describe('Custom evaluation rules, max 10 per call (deploy persistent rule sets via deploy_rule instead) — fires REGARDLESS of eval_type; pass eval_type="custom" if you want ONLY these. Each entry accepts exactly name, type, config, weight — an unknown key (e.g. a misspelled weight) is rejected'),
|
|
41
47
|
cost_usd: z.number().optional().describe('Cost in USD — consulted by the cost bundle (eval_type="cost" or "all") AND by any cost_threshold custom rule regardless of eval_type; omit it and such a rule skips rather than passes (a critical one is listed in critical_skipped)'),
|
|
42
48
|
token_usage: z.object({
|
|
43
49
|
prompt_tokens: z.number().optional(),
|
|
@@ -49,38 +55,38 @@ const inputSchema = {
|
|
|
49
55
|
// is how that field goes missing on one path and not the other.
|
|
50
56
|
tool_calls: z.array(toolCallSchema).optional().describe('What the agent DID — the tool calls it made, in order, each { tool_name, input?, output?, latency_ms?, error? } exactly as log_trace records them. Read by the trajectory rules — the rules that judge what the agent DID rather than what it wrote. Omit it and those rules SKIP rather than pass — an evaluation with no trajectory data reports "not judged", never "clean". When trace_id names a stored trace and this argument is omitted, the tool_calls stored on that trace are loaded and used, so a caller who already logged them need not resend them'),
|
|
51
57
|
};
|
|
52
|
-
export function registerEvaluateOutputTool(server, storage, evalEngine) {
|
|
58
|
+
export function registerEvaluateOutputTool(server, storage, evalEngine, options) {
|
|
53
59
|
server.registerTool('evaluate_output', {
|
|
54
60
|
title: 'Evaluate Output',
|
|
55
|
-
description:
|
|
56
|
-
'Score agent output against
|
|
57
|
-
''
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
''
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
''
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
].join('\n'),
|
|
61
|
+
description: describeTool({
|
|
62
|
+
summary: 'Score an agent output against the deterministic rule bundles: the ship verdict with its basis, every rule result with evidence and uncertainty, and what was not judged.',
|
|
63
|
+
does: 'In-process, no network, no key. eval_type picks one bundle (completeness, relevance, safety, cost, custom) or all (the default): every bundle plus deployed and inline custom rules, with a per-bundle breakdown in categories. ' +
|
|
64
|
+
'Inputs decide what can be judged: input is REQUIRED when eval_type="relevance" (keyword_overlap and topic_consistency compare the output against it and skip without it) and grounds the hallucination signals; ' +
|
|
65
|
+
'tool_calls, or a trace_id whose stored tool_calls are reused, feed the trajectory rules; cost_usd and token_usage feed the cost rules; expected feeds only expected_coverage. ' +
|
|
66
|
+
'A rule without its input SKIPS, is named, and never counts as a pass. custom_rules always fire. One row is stored, linked to trace_id when given.',
|
|
67
|
+
whenNot: 'To validate arbitrary JSON Schema (the json_schema custom type asserts an output\'s shape only). ' +
|
|
68
|
+
`To screen inputs before they reach an agent: ${INJECTION_SCOPE_SENTENCE} ` +
|
|
69
|
+
'For semantic judgment, evaluate_with_llm_judge and verify_citations need a key you supply.',
|
|
70
|
+
returns: evaluateOutputResponseSchema,
|
|
71
|
+
errors: 'IRIS_UNKNOWN_TRACE when trace_id names no stored trace — checked first, nothing scored or written. IRIS_STORAGE_ERROR when the row cannot be written. ' +
|
|
72
|
+
'Unknown arguments or keys are refused before the handler runs, naming the valid ones; a regex rule over its budget or with a broken config reports skipped, not an error. ' +
|
|
73
|
+
ERROR_ENVELOPE_SENTENCE,
|
|
74
|
+
siblings: {
|
|
75
|
+
log_trace: 'record the execution first',
|
|
76
|
+
evaluate_with_llm_judge: 'semantic scoring on your key',
|
|
77
|
+
verify_citations: 'citation grounding on your key',
|
|
78
|
+
list_rules: 'the roster, needs and published accuracy',
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
76
81
|
inputSchema: strictInput(inputSchema),
|
|
82
|
+
outputSchema: evaluateOutputResponseSchema,
|
|
77
83
|
annotations: {
|
|
78
84
|
readOnlyHint: false, // Writes an eval_result row
|
|
79
85
|
destructiveHint: false, // Creates new data; doesn't overwrite or delete
|
|
80
86
|
idempotentHint: true, // Deterministic: same inputs → same score (each call writes a distinct result row, but the SCORE is stable)
|
|
81
87
|
openWorldHint: false, // No external network in heuristic mode; LLM-as-judge has its own tool with openWorldHint:true
|
|
82
88
|
},
|
|
83
|
-
}, async (args) => {
|
|
89
|
+
}, guarded(async (args) => {
|
|
84
90
|
// Refuse an unknown trace_id up front (#376): the old path ran the
|
|
85
91
|
// evaluation and then surfaced SQLite's "FOREIGN KEY constraint
|
|
86
92
|
// failed", which names neither the field nor the fix.
|
|
@@ -118,37 +124,10 @@ export function registerEvaluateOutputTool(server, storage, evalEngine) {
|
|
|
118
124
|
// OSS single-tenant: MCP tool callers are the local user. Cloud
|
|
119
125
|
// will derive tenant from the authenticated MCP session.
|
|
120
126
|
await insertLinkedEvalResult(storage, LOCAL_TENANT, result);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// Echo which bundle actually ran. Without this, a caller who
|
|
128
|
-
// omitted eval_type could not tell a "safety pass" from a
|
|
129
|
-
// completeness eval that never ran a single safety rule.
|
|
130
|
-
eval_type: result.eval_type,
|
|
131
|
-
score: result.score,
|
|
132
|
-
passed: result.passed,
|
|
133
|
-
...(result.critical_failures ? { critical_failures: result.critical_failures } : {}),
|
|
134
|
-
// The other half of the veto contract. The engine names every
|
|
135
|
-
// critical rule that SKIPPED (budget-killed regex, missing cost
|
|
136
|
-
// data) so a fail-closed gate can treat the eval as unknown;
|
|
137
|
-
// this response used to drop the field, so the gate the
|
|
138
|
-
// description tells users to write keyed on something that
|
|
139
|
-
// never arrived and read passed:true as clean.
|
|
140
|
-
...(result.critical_skipped ? { critical_skipped: result.critical_skipped } : {}),
|
|
141
|
-
rule_results: result.rule_results,
|
|
142
|
-
suggestions: result.suggestions,
|
|
143
|
-
rules_evaluated: result.rules_evaluated,
|
|
144
|
-
rules_skipped: result.rules_skipped,
|
|
145
|
-
insufficient_data: result.insufficient_data,
|
|
146
|
-
// Per-bundle breakdown — eval_type="all" only.
|
|
147
|
-
...(result.categories ? { categories: result.categories } : {}),
|
|
148
|
-
...(evalTypeOmitted ? { note: DEFAULT_EVAL_TYPE_NOTE } : {}),
|
|
149
|
-
}),
|
|
150
|
-
},
|
|
151
|
-
],
|
|
152
|
-
};
|
|
153
|
-
});
|
|
127
|
+
// One serializer for every evaluation surface (src/eval/response.ts):
|
|
128
|
+
// the tool, the HTTP ingest route, the resources and the drift-lock
|
|
129
|
+
// all read the same object, so a field added there reaches every
|
|
130
|
+
// reader at once.
|
|
131
|
+
return respond(evaluateOutputResponseSchema, toEvaluationResponse(result, { traceId: args.trace_id, dormant: options?.dormant?.(), ...(evalTypeOmitted ? { note: DEFAULT_EVAL_TYPE_NOTE } : {}) }), evaluationLinks(result.id, args.trace_id));
|
|
132
|
+
}));
|
|
154
133
|
}
|
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
3
|
import type { IStorageAdapter } from '../types/query.js';
|
|
4
|
+
import type { LLMProvider } from '../eval/llm-judge/client.js';
|
|
5
|
+
/** The models the pricing table knows, so an unknown one can be refused with the valid list. */
|
|
6
|
+
export declare function supportedModels(): string[];
|
|
7
|
+
export declare function inferProvider(model: string): LLMProvider;
|
|
8
|
+
/**
|
|
9
|
+
* The key for the provider, from this process's environment. Missing is
|
|
10
|
+
* IRIS_JUDGE_NOT_ENABLED with the enable steps as recovery — the fact
|
|
11
|
+
* users get wrong is that a shell export does not reach the process an
|
|
12
|
+
* MCP client spawns, and the steps say so.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveApiKey(provider: LLMProvider, toolName?: string): string;
|
|
15
|
+
export declare const judgeOutputSchema: z.ZodObject<{
|
|
16
|
+
id: z.ZodString;
|
|
17
|
+
trace_id: z.ZodOptional<z.ZodString>;
|
|
18
|
+
score: z.ZodNumber;
|
|
19
|
+
passed: z.ZodBoolean;
|
|
20
|
+
rationale: z.ZodString;
|
|
21
|
+
dimensions: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
22
|
+
model: z.ZodString;
|
|
23
|
+
provider: z.ZodEnum<{
|
|
24
|
+
anthropic: "anthropic";
|
|
25
|
+
openai: "openai";
|
|
26
|
+
}>;
|
|
27
|
+
template: z.ZodString;
|
|
28
|
+
input_tokens: z.ZodNumber;
|
|
29
|
+
output_tokens: z.ZodNumber;
|
|
30
|
+
cost_usd: z.ZodNullable<z.ZodNumber>;
|
|
31
|
+
latency_ms: z.ZodNumber;
|
|
32
|
+
raw_response_id: z.ZodOptional<z.ZodString>;
|
|
33
|
+
}, z.core.$loose>;
|
|
3
34
|
export declare function registerEvaluateWithLLMJudgeTool(server: McpServer, storage: IStorageAdapter): void;
|
|
@@ -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,101 @@ 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 judge\'s own pass verdict for the template'),
|
|
74
|
+
rationale: z.string().describe('the judge\'s reasoning, in its words'),
|
|
75
|
+
dimensions: z.record(z.string(), z.unknown()).describe('per-dimension sub-scores for the template'),
|
|
76
|
+
model: z.string().describe('the model that judged'),
|
|
77
|
+
provider: z.enum(['anthropic', 'openai']).describe('the provider called'),
|
|
78
|
+
template: z.string().describe('the template used'),
|
|
79
|
+
input_tokens: z.number().describe('tokens sent, across both attempts when a retry ran'),
|
|
80
|
+
output_tokens: z.number().describe('tokens received, across both attempts when a retry ran'),
|
|
81
|
+
cost_usd: z.number().nullable().describe('the exact spend from the pricing table'),
|
|
82
|
+
latency_ms: z.number().describe('wall time of the provider call(s)'),
|
|
83
|
+
raw_response_id: z.string().optional().describe('the provider\'s response id, for your own audit'),
|
|
84
|
+
});
|
|
58
85
|
export function registerEvaluateWithLLMJudgeTool(server, storage) {
|
|
59
86
|
server.registerTool('evaluate_with_llm_judge', {
|
|
60
87
|
title: 'Evaluate With LLM Judge',
|
|
61
|
-
description:
|
|
62
|
-
'Score
|
|
63
|
-
'
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
'
|
|
69
|
-
|
|
70
|
-
'
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
description: describeTool({
|
|
89
|
+
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.',
|
|
90
|
+
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. ` +
|
|
91
|
+
'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. ' +
|
|
92
|
+
`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}). ` +
|
|
93
|
+
'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. ' +
|
|
94
|
+
"The judge's own accuracy is measurable on a key you supply and is not yet published (see iris://proof).",
|
|
95
|
+
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.',
|
|
96
|
+
returns: judgeOutputSchema,
|
|
97
|
+
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. ' +
|
|
98
|
+
'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. ' +
|
|
99
|
+
ERROR_ENVELOPE_SENTENCE,
|
|
100
|
+
siblings: {
|
|
101
|
+
evaluate_output: 'the free deterministic path',
|
|
102
|
+
verify_citations: 'citation grounding, the narrower judge',
|
|
103
|
+
log_trace: 'record the execution first',
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
78
106
|
inputSchema: strictInput(inputSchema),
|
|
107
|
+
outputSchema: judgeOutputSchema,
|
|
79
108
|
annotations: {
|
|
80
109
|
readOnlyHint: false, // Writes eval_result; also spends money (external API cost)
|
|
81
110
|
destructiveHint: false, // Creates data; doesn't overwrite or delete
|
|
82
111
|
idempotentHint: false, // Temperature > 0 may vary; even at T=0 provider non-determinism is possible; cost also varies per call
|
|
83
112
|
openWorldHint: true, // Calls external APIs (Anthropic / OpenAI) — touches the world beyond local process
|
|
84
113
|
},
|
|
85
|
-
}, async (args) => {
|
|
114
|
+
}, guarded(async (args) => {
|
|
86
115
|
const provider = args.provider ?? inferProvider(args.model);
|
|
87
116
|
const apiKey = resolveApiKey(provider);
|
|
88
117
|
const maxCostUsd = resolveMaxCost(args.max_cost_usd);
|
|
@@ -95,8 +124,8 @@ export function registerEvaluateWithLLMJudgeTool(server, storage) {
|
|
|
95
124
|
const result = await evaluateWithLLMJudge({
|
|
96
125
|
output: args.output,
|
|
97
126
|
template: args.template,
|
|
98
|
-
provider,
|
|
99
127
|
model: args.model,
|
|
128
|
+
provider,
|
|
100
129
|
apiKey,
|
|
101
130
|
input: args.input,
|
|
102
131
|
expected: args.expected,
|
|
@@ -106,12 +135,12 @@ export function registerEvaluateWithLLMJudgeTool(server, storage) {
|
|
|
106
135
|
temperature: args.temperature,
|
|
107
136
|
timeoutMs: args.timeout_ms,
|
|
108
137
|
});
|
|
138
|
+
// Persist to eval_results so the dashboard can surface it.
|
|
139
|
+
// eval_type='custom' — LLM judge scores span all 4 heuristic
|
|
140
|
+
// categories (accuracy, helpfulness, safety, faithfulness); 'custom'
|
|
141
|
+
// is the honest bucket. rule_results[0] captures per-dimension
|
|
142
|
+
// breakdown + provider metadata for audit.
|
|
109
143
|
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
144
|
await insertLinkedEvalResult(storage, LOCAL_TENANT, {
|
|
116
145
|
id: evalId,
|
|
117
146
|
trace_id: args.trace_id,
|
|
@@ -132,28 +161,26 @@ export function registerEvaluateWithLLMJudgeTool(server, storage) {
|
|
|
132
161
|
rules_evaluated: 1,
|
|
133
162
|
rules_skipped: 0,
|
|
134
163
|
insufficient_data: false,
|
|
164
|
+
// What the evaluation itself cost — the description promised it was
|
|
165
|
+
// kept and the write path stored none of it (arc zero, G15).
|
|
166
|
+
eval_cost_usd: result.costUsd ?? undefined,
|
|
167
|
+
eval_tokens: result.inputTokens + result.outputTokens,
|
|
135
168
|
});
|
|
136
|
-
return {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
raw_response_id: result.rawResponseId,
|
|
154
|
-
}),
|
|
155
|
-
},
|
|
156
|
-
],
|
|
157
|
-
};
|
|
158
|
-
});
|
|
169
|
+
return respond(judgeOutputSchema, {
|
|
170
|
+
id: evalId,
|
|
171
|
+
...(args.trace_id ? { trace_id: args.trace_id } : {}),
|
|
172
|
+
score: result.score,
|
|
173
|
+
passed: result.passed,
|
|
174
|
+
rationale: result.rationale,
|
|
175
|
+
dimensions: result.dimensions,
|
|
176
|
+
model: result.model,
|
|
177
|
+
provider: result.provider,
|
|
178
|
+
template: result.template,
|
|
179
|
+
input_tokens: result.inputTokens,
|
|
180
|
+
output_tokens: result.outputTokens,
|
|
181
|
+
cost_usd: result.costUsd,
|
|
182
|
+
latency_ms: result.latencyMs,
|
|
183
|
+
raw_response_id: result.rawResponseId,
|
|
184
|
+
}, evaluationLinks(evalId, args.trace_id));
|
|
185
|
+
}));
|
|
159
186
|
}
|
|
@@ -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
|
|
@@ -52,7 +54,7 @@ export function addTraceRangeIssues(args, ctx) {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
const inputSchema = {
|
|
55
|
-
agent_name: z.string().optional().describe('Filter by agent name — exact match (no wildcards
|
|
57
|
+
agent_name: z.string().optional().describe('Filter by agent name — exact match (no wildcards)'),
|
|
56
58
|
framework: z.string().optional().describe('Filter by agent framework — exact match (e.g., langchain, autogen)'),
|
|
57
59
|
since: isoTimestamp.optional().describe('ISO 8601 timestamp (or date) lower bound — return traces with timestamp >= this; anything that is not an ISO timestamp is rejected, never treated as "no bound"'),
|
|
58
60
|
until: isoTimestamp.optional().describe('ISO 8601 timestamp (or date) upper bound — return traces with timestamp <= this; must not be earlier than `since`'),
|
|
@@ -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;
|