@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
|
@@ -12,48 +12,45 @@
|
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
14
14
|
import { strictInput } from './strict-input.js';
|
|
15
|
+
import { describeTool, ERROR_ENVELOPE_SENTENCE } from './describe.js';
|
|
16
|
+
import { guarded, respond } from './respond.js';
|
|
15
17
|
const inputSchema = {
|
|
16
18
|
trace_id: z
|
|
17
19
|
.string()
|
|
18
20
|
.regex(/^[a-f0-9]{32}$/)
|
|
19
21
|
.describe('Trace id to delete (32-hex lowercase; obtained from log_trace response or get_traces)'),
|
|
20
22
|
};
|
|
23
|
+
export const deleteTraceOutputSchema = z.looseObject({
|
|
24
|
+
deleted: z.boolean().describe('true when a trace row was removed; false when no trace with that id existed for this tenant'),
|
|
25
|
+
trace_id: z.string().describe('the id that was asked for'),
|
|
26
|
+
});
|
|
21
27
|
export function registerDeleteTraceTool(server, storage) {
|
|
22
28
|
server.registerTool('delete_trace', {
|
|
23
29
|
title: 'Delete Trace',
|
|
24
|
-
description:
|
|
25
|
-
'Remove
|
|
26
|
-
'
|
|
27
|
-
|
|
28
|
-
'',
|
|
29
|
-
|
|
30
|
-
''
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
'',
|
|
39
|
-
"Error modes. Throws 400 on malformed trace_id (wrong format: not 32-char lowercase hex). Returns `{deleted: false}` when the id doesn't exist in the caller's tenant (not an error — the trace may simply have been deleted already). Returns 429 on HTTP rate limit. Storage failures propagate as 500.",
|
|
40
|
-
].join('\n'),
|
|
30
|
+
description: describeTool({
|
|
31
|
+
summary: 'Remove one stored trace by id; its spans go with it, and every evaluation linked to it keeps its verdict and loses its text.',
|
|
32
|
+
does: "Deletes the trace row for the caller's tenant. Spans cascade. Evaluations linked to it keep their verdict, scores, criticality and evidence offsets; their output text, expected text, suggestions and rule messages are erased in the same transaction and erased_at is stamped, so no text from the trace survives in any evaluation. " +
|
|
33
|
+
"deleted is false when no trace has that id — already removed, or not this tenant's — and that is not an error. No audit entry is written: traces are user data, not policy.",
|
|
34
|
+
whenNot: 'To expire old data in bulk (retention.days; the sweep runs at boot and every retention.sweepIntervalHours). To delete evaluations: they are not deleted per row; retention and --purge cover them. To pause anything: traces are immutable, there is nothing to pause.',
|
|
35
|
+
returns: deleteTraceOutputSchema,
|
|
36
|
+
errors: 'IRIS_STORAGE_ERROR when the delete cannot run. A malformed trace_id (not 32 lowercase hex) is refused before the handler runs. ' +
|
|
37
|
+
ERROR_ENVELOPE_SENTENCE,
|
|
38
|
+
siblings: {
|
|
39
|
+
log_trace: 'store a trace',
|
|
40
|
+
get_traces: 'find the trace to delete',
|
|
41
|
+
delete_rule: 'the equivalent for custom rules',
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
41
44
|
inputSchema: strictInput(inputSchema),
|
|
45
|
+
outputSchema: deleteTraceOutputSchema,
|
|
42
46
|
annotations: {
|
|
43
47
|
readOnlyHint: false,
|
|
44
48
|
destructiveHint: true,
|
|
45
49
|
idempotentHint: false,
|
|
46
50
|
openWorldHint: false,
|
|
47
51
|
},
|
|
48
|
-
}, async (args) => {
|
|
52
|
+
}, guarded(async (args) => {
|
|
49
53
|
const deleted = await storage.deleteTrace(LOCAL_TENANT, args.trace_id);
|
|
50
|
-
return {
|
|
51
|
-
|
|
52
|
-
{
|
|
53
|
-
type: 'text',
|
|
54
|
-
text: JSON.stringify({ deleted, trace_id: args.trace_id }),
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
};
|
|
58
|
-
});
|
|
54
|
+
return respond(deleteTraceOutputSchema, { deleted, trace_id: args.trace_id });
|
|
55
|
+
}));
|
|
59
56
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
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';
|
|
@@ -6,7 +7,8 @@ import { type TenantId } from '../types/tenant.js';
|
|
|
6
7
|
/**
|
|
7
8
|
* A rule with this name is already deployed and the caller did not ask to
|
|
8
9
|
* replace it. Carries the existing rule(s) so an HTTP surface can answer
|
|
9
|
-
* 409 with them beside the same message the MCP tool
|
|
10
|
+
* 409 with them beside the same message the MCP tool returns as
|
|
11
|
+
* IRIS_DUPLICATE_RULE (src/tools/errors.ts maps it by name).
|
|
10
12
|
*/
|
|
11
13
|
export declare class DuplicateRuleNameError extends Error {
|
|
12
14
|
readonly existing: DeployedCustomRule[];
|
|
@@ -34,4 +36,14 @@ export interface ReplacedRule {
|
|
|
34
36
|
export declare function retireSameNamedRules(store: CustomRuleStore, engine: EvalEngine, tenantId: TenantId, name: string, replace: boolean, user: string): ReplacedRule[];
|
|
35
37
|
/** The `warning` both deploy surfaces attach when a replace retired rules. */
|
|
36
38
|
export declare function replacedRulesWarning(name: string, replaced: ReplacedRule[]): string;
|
|
39
|
+
export declare const deployRuleOutputSchema: z.ZodObject<{
|
|
40
|
+
rule: z.ZodObject<{
|
|
41
|
+
id: z.ZodString;
|
|
42
|
+
name: z.ZodString;
|
|
43
|
+
}, z.core.$loose>;
|
|
44
|
+
replaced: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
45
|
+
id: z.ZodString;
|
|
46
|
+
}, z.core.$loose>>>;
|
|
47
|
+
warning: z.ZodOptional<z.ZodString>;
|
|
48
|
+
}, z.core.$loose>;
|
|
37
49
|
export declare function registerDeployRuleTool(server: McpServer, customRuleStore: CustomRuleStore, evalEngine: EvalEngine): void;
|
|
@@ -14,11 +14,14 @@ import { z } from 'zod';
|
|
|
14
14
|
import { createCustomRule } from '../eval/rules/custom.js';
|
|
15
15
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
16
16
|
import { strictInput, strictNested } from './strict-input.js';
|
|
17
|
+
import { describeTool, ERROR_ENVELOPE_SENTENCE } from './describe.js';
|
|
18
|
+
import { guarded, respond } from './respond.js';
|
|
17
19
|
const EvalTypeSchema = z.enum(['completeness', 'relevance', 'safety', 'cost', 'custom']);
|
|
18
20
|
/**
|
|
19
21
|
* A rule with this name is already deployed and the caller did not ask to
|
|
20
22
|
* replace it. Carries the existing rule(s) so an HTTP surface can answer
|
|
21
|
-
* 409 with them beside the same message the MCP tool
|
|
23
|
+
* 409 with them beside the same message the MCP tool returns as
|
|
24
|
+
* IRIS_DUPLICATE_RULE (src/tools/errors.ts maps it by name).
|
|
22
25
|
*/
|
|
23
26
|
export class DuplicateRuleNameError extends Error {
|
|
24
27
|
existing;
|
|
@@ -145,34 +148,41 @@ const inputSchemaWithAliases = strictInput(inputSchema).superRefine((args, ctx)
|
|
|
145
148
|
ctx.addIssue({ code: 'custom', path: ['sourceMomentId'], message: 'pass either source_moment_id or sourceMomentId, not both' });
|
|
146
149
|
}
|
|
147
150
|
});
|
|
151
|
+
export const deployRuleOutputSchema = z.looseObject({
|
|
152
|
+
rule: z.looseObject({ id: z.string(), name: z.string() }).describe('the rule as persisted: id (rule-<hex>, keep it for delete_rule), name, description, evalType, severity, definition, enabled, createdAt, updatedAt, version, sourceMomentId'),
|
|
153
|
+
replaced: z.array(z.looseObject({ id: z.string() })).optional().describe('with replace: true, the earlier rule(s) of the same name that were retired'),
|
|
154
|
+
warning: z.string().optional().describe('with replace: true, one sentence naming what was retired'),
|
|
155
|
+
});
|
|
148
156
|
export function registerDeployRuleTool(server, customRuleStore, evalEngine) {
|
|
149
157
|
server.registerTool('deploy_rule', {
|
|
150
158
|
title: 'Deploy Custom Rule',
|
|
151
|
-
description:
|
|
152
|
-
'Deploy a
|
|
153
|
-
''
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
'',
|
|
158
|
-
|
|
159
|
-
''
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
159
|
+
description: describeTool({
|
|
160
|
+
summary: 'Deploy a custom rule that fires on every future evaluate_output call of its bundle — persisted, active immediately, audited.',
|
|
161
|
+
does: 'Writes the rule to ~/.iris/custom-rules.json, appends a rule.deploy audit entry and registers it with the running engine, so it fires on the very next call and survives restarts. ' +
|
|
162
|
+
'eval_type says WHEN it fires (that bundle, and eval_type="all"); severity says what a failure DOES: low and medium only lower the weighted score, high and critical force passed to false and list the rule in critical_failures. ' +
|
|
163
|
+
'definition.type picks the check (regex_match, regex_no_match, min_length, max_length, contains_keywords, excludes_keywords, json_schema, cost_threshold) and definition.config carries its keys (pattern; min_length; max_length; keywords; max_cost). ' +
|
|
164
|
+
'Any bundle and type combine. Names are unique: a taken name is refused unless replace is true, which retires the earlier rule(s) first and reports them. Argument names are snake_case; the camelCase aliases evalType and sourceMomentId are accepted — pass one spelling of each.',
|
|
165
|
+
whenNot: 'To try a rule first: POST /api/v1/rules/custom/preview on the dashboard replays a definition against stored traces without deploying. For a one-off check on one call: the custom_rules argument of evaluate_output. To pause a rule: delete_rule with enabled: false.',
|
|
166
|
+
returns: deployRuleOutputSchema,
|
|
167
|
+
errors: 'IRIS_DUPLICATE_RULE when the name is deployed and replace is false (the message names the existing id). ' +
|
|
168
|
+
'IRIS_INVALID_RULE_CONFIG when the definition is rejected — a regex that fails the ReDoS check or exceeds 1000 characters, a missing config key — naming the field; nothing is deployed. ' +
|
|
169
|
+
'IRIS_STORAGE_ERROR when the store cannot be written. An unknown key in definition, a name over 80 characters, a non-positive weight or both spellings of an alias are refused before the handler runs. ' +
|
|
170
|
+
ERROR_ENVELOPE_SENTENCE,
|
|
171
|
+
siblings: {
|
|
172
|
+
list_rules: 'see what is deployed and the built-in roster',
|
|
173
|
+
delete_rule: 'remove, disable or re-enable',
|
|
174
|
+
evaluate_output: 'where the rule fires',
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
168
177
|
inputSchema: inputSchemaWithAliases,
|
|
178
|
+
outputSchema: deployRuleOutputSchema,
|
|
169
179
|
annotations: {
|
|
170
180
|
readOnlyHint: false,
|
|
171
181
|
destructiveHint: false,
|
|
172
182
|
idempotentHint: false,
|
|
173
183
|
openWorldHint: false,
|
|
174
184
|
},
|
|
175
|
-
}, async (args) => {
|
|
185
|
+
}, guarded(async (args) => {
|
|
176
186
|
const evalType = (args.eval_type ?? args.evalType);
|
|
177
187
|
const sourceMomentId = args.source_moment_id ?? args.sourceMomentId;
|
|
178
188
|
// Server overrides the inner definition's `name` so it always matches
|
|
@@ -186,10 +196,12 @@ export function registerDeployRuleTool(server, customRuleStore, evalEngine) {
|
|
|
186
196
|
};
|
|
187
197
|
// Same-name redeploy (#373) — shared with the dashboard's deploy
|
|
188
198
|
// route; see retireSameNamedRules above. Throws (nothing deployed)
|
|
189
|
-
// when the name is taken and replace is false
|
|
199
|
+
// when the name is taken and replace is false; `guarded` turns that
|
|
200
|
+
// into IRIS_DUPLICATE_RULE.
|
|
190
201
|
const replaced = retireSameNamedRules(customRuleStore, evalEngine, LOCAL_TENANT, args.name, args.replace, 'mcp');
|
|
191
202
|
// OSS: MCP tools operate under LOCAL_TENANT. See list-rules.ts for context.
|
|
192
203
|
const rule = customRuleStore.deploy(LOCAL_TENANT, {
|
|
204
|
+
replaces: replaced.map((r) => r.id),
|
|
193
205
|
name: args.name,
|
|
194
206
|
description: args.description,
|
|
195
207
|
evalType,
|
|
@@ -205,18 +217,9 @@ export function registerDeployRuleTool(server, customRuleStore, evalEngine) {
|
|
|
205
217
|
// Registered under its rule id so delete_rule can hot-remove it.
|
|
206
218
|
// Severity rides along: high/critical makes the rule hard-failing.
|
|
207
219
|
evalEngine.registerRule(rule.evalType, createCustomRule(rule.definition, rule.severity), rule.id);
|
|
208
|
-
return {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
rule,
|
|
214
|
-
...(replaced.length > 0
|
|
215
|
-
? { replaced, warning: replacedRulesWarning(args.name, replaced) }
|
|
216
|
-
: {}),
|
|
217
|
-
}),
|
|
218
|
-
},
|
|
219
|
-
],
|
|
220
|
-
};
|
|
221
|
-
});
|
|
220
|
+
return respond(deployRuleOutputSchema, {
|
|
221
|
+
rule,
|
|
222
|
+
...(replaced.length > 0 ? { replaced, warning: replacedRulesWarning(args.name, replaced) } : {}),
|
|
223
|
+
});
|
|
224
|
+
}));
|
|
222
225
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
export declare const DESCRIPTION_HEADINGS: readonly ["What it does.", "When not to use it.", "Returns.", "Errors.", "Siblings."];
|
|
3
|
+
export declare const DESCRIPTION_WORD_CAP = 450;
|
|
4
|
+
export interface ToolDescriptionSpec {
|
|
5
|
+
/** One sentence: what calling this does. */
|
|
6
|
+
summary: string;
|
|
7
|
+
does: string;
|
|
8
|
+
whenNot: string;
|
|
9
|
+
/** The tool's output schema; every top-level field must carry a description. */
|
|
10
|
+
returns: z.ZodObject<z.ZodRawShape>;
|
|
11
|
+
errors: string;
|
|
12
|
+
/** Sibling tool → one clause on when it is the better call. */
|
|
13
|
+
siblings: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
/** `field` (its description); … — from the schema, never retyped. */
|
|
16
|
+
export declare function returnsFrom(schema: z.ZodObject<z.ZodRawShape>): string;
|
|
17
|
+
export declare function wordCount(text: string): number;
|
|
18
|
+
export declare function describeTool(spec: ToolDescriptionSpec): string;
|
|
19
|
+
/** The sentence every "Errors" heading ends with, so the envelope is stated once. */
|
|
20
|
+
export declare const ERROR_ENVELOPE_SENTENCE = "Every failure returns {\"error\":{\"code\",\"message\",\"recovery\":[]}} with isError true; follow recovery before retrying.";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const DESCRIPTION_HEADINGS = ['What it does.', 'When not to use it.', 'Returns.', 'Errors.', 'Siblings.'];
|
|
2
|
+
export const DESCRIPTION_WORD_CAP = 450;
|
|
3
|
+
/** `field` (its description); … — from the schema, never retyped. */
|
|
4
|
+
export function returnsFrom(schema) {
|
|
5
|
+
const parts = [];
|
|
6
|
+
for (const [key, field] of Object.entries(schema.shape)) {
|
|
7
|
+
const description = field.description;
|
|
8
|
+
if (!description)
|
|
9
|
+
throw new Error(`output field "${key}" has no description; add .describe() on the schema`);
|
|
10
|
+
parts.push(`\`${key}\` (${description})`);
|
|
11
|
+
}
|
|
12
|
+
return `JSON with ${parts.join('; ')}.`;
|
|
13
|
+
}
|
|
14
|
+
export function wordCount(text) {
|
|
15
|
+
return text.split(/\s+/).filter(Boolean).length;
|
|
16
|
+
}
|
|
17
|
+
export function describeTool(spec) {
|
|
18
|
+
const siblings = Object.entries(spec.siblings)
|
|
19
|
+
.map(([name, when]) => `${name} — ${when}`)
|
|
20
|
+
.join('; ');
|
|
21
|
+
const text = [
|
|
22
|
+
spec.summary,
|
|
23
|
+
`${DESCRIPTION_HEADINGS[0]} ${spec.does}`,
|
|
24
|
+
`${DESCRIPTION_HEADINGS[1]} ${spec.whenNot}`,
|
|
25
|
+
`${DESCRIPTION_HEADINGS[2]} ${returnsFrom(spec.returns)}`,
|
|
26
|
+
`${DESCRIPTION_HEADINGS[3]} ${spec.errors}`,
|
|
27
|
+
`${DESCRIPTION_HEADINGS[4]} ${siblings}.`,
|
|
28
|
+
].join('\n\n');
|
|
29
|
+
const words = wordCount(text);
|
|
30
|
+
if (words > DESCRIPTION_WORD_CAP) {
|
|
31
|
+
throw new Error(`tool description is ${words} words; the cap is ${DESCRIPTION_WORD_CAP}`);
|
|
32
|
+
}
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
35
|
+
/** The sentence every "Errors" heading ends with, so the envelope is stated once. */
|
|
36
|
+
export const ERROR_ENVELOPE_SENTENCE = 'Every failure returns {"error":{"code","message","recovery":[]}} with isError true; follow recovery before retrying.';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { LLMJudgeError } from '../eval/llm-judge/client.js';
|
|
2
|
+
export declare const ERROR_CODE_CATALOGUE: readonly ["IRIS_INVALID_ARGUMENT", "IRIS_UNKNOWN_TRACE", "IRIS_DUPLICATE_RULE", "IRIS_INVALID_RULE_CONFIG", "IRIS_JUDGE_NOT_ENABLED", "IRIS_JUDGE_UNKNOWN_MODEL", "IRIS_BUDGET_EXCEEDED", "IRIS_PROVIDER_ERROR", "IRIS_JUDGE_FAILED", "IRIS_STORAGE_ERROR", "IRIS_INTERNAL_ERROR"];
|
|
3
|
+
export type IrisErrorCode = (typeof ERROR_CODE_CATALOGUE)[number];
|
|
4
|
+
export type ProviderErrorKind = LLMJudgeError['kind'];
|
|
5
|
+
export interface IrisErrorEnvelope {
|
|
6
|
+
code: IrisErrorCode;
|
|
7
|
+
/** One sentence a person can act on. */
|
|
8
|
+
message: string;
|
|
9
|
+
/** What to do, in order, before retrying. */
|
|
10
|
+
recovery: string[];
|
|
11
|
+
/** True when the same call can succeed later without a change from the caller. */
|
|
12
|
+
retryable: boolean;
|
|
13
|
+
/** The argument or environment variable at fault, when the failure is about one. */
|
|
14
|
+
field?: string;
|
|
15
|
+
/** The values the field accepts, when there is a closed list. */
|
|
16
|
+
valid?: string[];
|
|
17
|
+
/** A resource that explains the limit or the state. */
|
|
18
|
+
see?: string;
|
|
19
|
+
/** IRIS_PROVIDER_ERROR only: the provider failure class. */
|
|
20
|
+
kind?: ProviderErrorKind;
|
|
21
|
+
/** When the provider said how long to wait. */
|
|
22
|
+
retryAfterMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class IrisError extends Error {
|
|
25
|
+
readonly envelope: IrisErrorEnvelope;
|
|
26
|
+
constructor(envelope: Omit<IrisErrorEnvelope, 'recovery' | 'retryable'> & Partial<Pick<IrisErrorEnvelope, 'recovery' | 'retryable'>>);
|
|
27
|
+
}
|
|
28
|
+
export declare function irisError(code: IrisErrorCode, message: string, extra?: Partial<Omit<IrisErrorEnvelope, 'code' | 'message'>>): IrisError;
|
|
29
|
+
/**
|
|
30
|
+
* Every error a handler can throw, mapped to its code. Typed errors are
|
|
31
|
+
* matched by class or by name (the name check avoids importing the tool
|
|
32
|
+
* that defines the class, which would be a cycle); anything unrecognised
|
|
33
|
+
* is an internal error — reported as such, never dressed up as a caller
|
|
34
|
+
* mistake.
|
|
35
|
+
*/
|
|
36
|
+
export declare function toIrisError(err: unknown): IrisError;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Structured tool errors.
|
|
3
|
+
*
|
|
4
|
+
* A thrown Error reaches an MCP client as one line of text with isError
|
|
5
|
+
* set (the SDK flattens it), so the only thing an agent could do with
|
|
6
|
+
* "Anthropic judge requires IRIS_ANTHROPIC_API_KEY" was guess. Every
|
|
7
|
+
* failure a tool can produce is now a code from ONE catalogue with a
|
|
8
|
+
* message, the steps that clear it, whether a retry can help, and — when
|
|
9
|
+
* the failure is about one argument — the field and the valid values.
|
|
10
|
+
* Tools return these (src/tools/respond.ts wraps the handler); they never
|
|
11
|
+
* throw past the handler.
|
|
12
|
+
*
|
|
13
|
+
* The catalogue is exhaustive on purpose: tests/unit/tools/error-codes.test.ts
|
|
14
|
+
* provokes every code over a real transport and asserts the provoked set
|
|
15
|
+
* EQUALS this list, so a code nothing can raise cannot linger here and a
|
|
16
|
+
* new throw site cannot ship without a code.
|
|
17
|
+
*
|
|
18
|
+
* Two errors are not this module's: an argument the input schema rejects
|
|
19
|
+
* is refused by the SDK before the handler runs (the text names
|
|
20
|
+
* IRIS_INVALID_ARGUMENT and the valid arguments — see strict-input.ts), and
|
|
21
|
+
* an HTTP rate limit answers 429 at the transport before any tool runs.
|
|
22
|
+
*/
|
|
23
|
+
import { ZodError } from 'zod';
|
|
24
|
+
import { LLMJudgeError } from '../eval/llm-judge/client.js';
|
|
25
|
+
import { CAPABILITIES_RESOURCE_URI } from '../resources/uris.js';
|
|
26
|
+
export const ERROR_CODE_CATALOGUE = [
|
|
27
|
+
'IRIS_INVALID_ARGUMENT',
|
|
28
|
+
'IRIS_UNKNOWN_TRACE',
|
|
29
|
+
'IRIS_DUPLICATE_RULE',
|
|
30
|
+
'IRIS_INVALID_RULE_CONFIG',
|
|
31
|
+
'IRIS_JUDGE_NOT_ENABLED',
|
|
32
|
+
'IRIS_JUDGE_UNKNOWN_MODEL',
|
|
33
|
+
'IRIS_BUDGET_EXCEEDED',
|
|
34
|
+
'IRIS_PROVIDER_ERROR',
|
|
35
|
+
'IRIS_JUDGE_FAILED',
|
|
36
|
+
'IRIS_STORAGE_ERROR',
|
|
37
|
+
'IRIS_INTERNAL_ERROR',
|
|
38
|
+
];
|
|
39
|
+
export class IrisError extends Error {
|
|
40
|
+
envelope;
|
|
41
|
+
constructor(envelope) {
|
|
42
|
+
super(envelope.message);
|
|
43
|
+
this.name = 'IrisError';
|
|
44
|
+
this.envelope = { retryable: false, recovery: [], ...envelope };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function irisError(code, message, extra = {}) {
|
|
48
|
+
return new IrisError({ code, message, ...extra });
|
|
49
|
+
}
|
|
50
|
+
const CAPABILITIES = CAPABILITIES_RESOURCE_URI;
|
|
51
|
+
function isSqliteError(err) {
|
|
52
|
+
const code = err.code;
|
|
53
|
+
if (typeof code === 'string' && code.startsWith('SQLITE_'))
|
|
54
|
+
return true;
|
|
55
|
+
const message = err instanceof Error ? err.message : '';
|
|
56
|
+
return /SQLITE|database is locked|disk I\/O error|readonly database/i.test(message);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Every error a handler can throw, mapped to its code. Typed errors are
|
|
60
|
+
* matched by class or by name (the name check avoids importing the tool
|
|
61
|
+
* that defines the class, which would be a cycle); anything unrecognised
|
|
62
|
+
* is an internal error — reported as such, never dressed up as a caller
|
|
63
|
+
* mistake.
|
|
64
|
+
*/
|
|
65
|
+
export function toIrisError(err) {
|
|
66
|
+
if (err instanceof IrisError)
|
|
67
|
+
return err;
|
|
68
|
+
const name = err?.name;
|
|
69
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
70
|
+
if (name === 'DuplicateRuleNameError') {
|
|
71
|
+
return irisError('IRIS_DUPLICATE_RULE', message, {
|
|
72
|
+
field: 'name',
|
|
73
|
+
recovery: [
|
|
74
|
+
'Pass replace: true to retire the rule(s) with this name and deploy this one in their place.',
|
|
75
|
+
'Or call delete_rule with the existing rule id first.',
|
|
76
|
+
'Or choose a different name.',
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (name === 'CostCapError') {
|
|
81
|
+
const e = err;
|
|
82
|
+
return irisError('IRIS_BUDGET_EXCEEDED', message, {
|
|
83
|
+
field: 'max_cost_usd',
|
|
84
|
+
recovery: [
|
|
85
|
+
`Raise max_cost_usd on the call (the worst case was ${e.estimatedUsd?.toFixed(4) ?? '?'} USD against a cap of ${e.capUsd?.toFixed(4) ?? '?'} USD).`,
|
|
86
|
+
'Or trim the output, the input or max_output_tokens so the worst case fits.',
|
|
87
|
+
'Nothing was spent.',
|
|
88
|
+
],
|
|
89
|
+
see: CAPABILITIES,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (err instanceof LLMJudgeError) {
|
|
93
|
+
const retryable = err.kind === 'rate_limit' || err.kind === 'timeout' || err.kind === 'server_error';
|
|
94
|
+
const recovery = {
|
|
95
|
+
auth: ['The provider refused the key. Check that the key in the env block of your MCP config is valid and live, then restart the session.'],
|
|
96
|
+
rate_limit: ['The provider rate-limited the call. Wait and retry; Iris already retried once.'],
|
|
97
|
+
bad_request: ['The provider rejected the request. Check the model name and the template inputs.'],
|
|
98
|
+
server_error: ['The provider failed on its side. Retry later.'],
|
|
99
|
+
timeout: ['The provider did not answer within timeout_ms. Retry, or raise timeout_ms.'],
|
|
100
|
+
malformed_response: ['The judge did not return valid JSON on two attempts. Retry; if it recurs, pick another model.'],
|
|
101
|
+
unknown: ['Retry once; if it recurs, report the message with the provider and model.'],
|
|
102
|
+
};
|
|
103
|
+
return irisError('IRIS_PROVIDER_ERROR', message, {
|
|
104
|
+
kind: err.kind,
|
|
105
|
+
retryable,
|
|
106
|
+
recovery: recovery[err.kind],
|
|
107
|
+
...(err.retryAfterSeconds !== undefined ? { retryAfterMs: err.retryAfterSeconds * 1000 } : {}),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (err instanceof ZodError) {
|
|
111
|
+
// The SDK validates tool input before the handler runs, so a ZodError
|
|
112
|
+
// inside a handler is the rule store rejecting a definition.
|
|
113
|
+
const first = err.issues[0];
|
|
114
|
+
const field = first?.path?.length ? first.path.map(String).join('.') : undefined;
|
|
115
|
+
return irisError('IRIS_INVALID_RULE_CONFIG', `The rule definition was rejected: ${err.issues.map((i) => `${i.path.map(String).join('.') || 'definition'}: ${i.message}`).join('; ')}`, {
|
|
116
|
+
...(field ? { field } : {}),
|
|
117
|
+
recovery: ['Fix the named field and deploy again. Nothing was deployed.'],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (isSqliteError(err)) {
|
|
121
|
+
const code = err.code ?? '';
|
|
122
|
+
return irisError('IRIS_STORAGE_ERROR', `Iris storage failed: ${message}`, {
|
|
123
|
+
retryable: code === 'SQLITE_BUSY' || code === 'SQLITE_LOCKED',
|
|
124
|
+
recovery: [
|
|
125
|
+
'Retry once. If it recurs, check that the database path is writable and the disk is not full.',
|
|
126
|
+
'Run `npx @iris-eval/mcp-server --self-test` to probe the configured home and database.',
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return irisError('IRIS_INTERNAL_ERROR', message || 'Iris hit an unexpected error.', {
|
|
131
|
+
recovery: ['Retry once. If it recurs, report the message with the Iris version from iris://capabilities.'],
|
|
132
|
+
see: CAPABILITIES,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import type { DormantRule } from '../eval/dormant.js';
|
|
1
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
3
|
import type { IStorageAdapter } from '../types/query.js';
|
|
3
4
|
import type { EvalEngine } from '../eval/engine.js';
|
|
4
|
-
|
|
5
|
+
/** The most inline custom rules one call may carry (see the argument description). */
|
|
6
|
+
export declare const MAX_INLINE_CUSTOM_RULES = 10;
|
|
7
|
+
export interface EvaluateOutputOptions {
|
|
8
|
+
/** The quarantined gating rules on this server, for coverage.dormant. */
|
|
9
|
+
dormant?: () => DormantRule[];
|
|
10
|
+
}
|
|
11
|
+
export declare function registerEvaluateOutputTool(server: McpServer, storage: IStorageAdapter, evalEngine: EvalEngine, options?: EvaluateOutputOptions): void;
|