@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
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;
|
package/dist/tools/log-trace.js
CHANGED
|
@@ -3,6 +3,9 @@ import { generateTraceId, generateSpanId } from '../utils/ids.js';
|
|
|
3
3
|
import { LOCAL_TENANT } from '../types/tenant.js';
|
|
4
4
|
import { bestEffortExport } from '../otel/lazy.js';
|
|
5
5
|
import { strictInput, strictNested } from './strict-input.js';
|
|
6
|
+
import { describeTool, ERROR_ENVELOPE_SENTENCE } from './describe.js';
|
|
7
|
+
import { guarded, respond } from './respond.js';
|
|
8
|
+
import { traceUri } from '../resources/uris.js';
|
|
6
9
|
/*
|
|
7
10
|
* The tool-call record — one entry of `tool_calls[]`.
|
|
8
11
|
*
|
|
@@ -58,47 +61,51 @@ export const logTraceInputShape = {
|
|
|
58
61
|
framework: z.string().optional().describe('Agent framework identifier (e.g., langchain, autogen, custom)'),
|
|
59
62
|
input: z.string().optional().describe('Agent input text — the user prompt or upstream input that produced this output'),
|
|
60
63
|
output: z.string().optional().describe('Agent output text — what the agent produced (pass to evaluate_output for scoring)'),
|
|
61
|
-
tool_calls: z.array(toolCallSchema).optional().describe('Tool calls made during execution
|
|
64
|
+
tool_calls: z.array(toolCallSchema).optional().describe('Tool calls made during execution, in order, each { tool_name, input?, output?, latency_ms?, error? } — what the trajectory rules judge; evaluate_output reuses them when given this trace_id'),
|
|
62
65
|
latency_ms: z.number().optional().describe('Total execution time in milliseconds (end-to-end agent latency)'),
|
|
63
66
|
token_usage: TokenUsageSchema.optional().describe('Token usage breakdown (prompt/completion/total — used for cost analysis)'),
|
|
64
67
|
cost_usd: z.number().optional().describe('Total cost in USD — overrides per-span aggregation when provided (treated as authoritative)'),
|
|
65
68
|
metadata: z.record(z.string(), z.unknown()).optional().describe('Opaque key-value tags (e.g. {requestId, userId, env}) — queryable in dashboard, not via get_traces filters'),
|
|
66
|
-
spans: z.array(SpanSchema).optional().describe('Detailed execution spans (hierarchical span tree with timings, attributes, events)'),
|
|
69
|
+
spans: z.array(SpanSchema).optional().describe('Detailed execution spans (hierarchical span tree with timings, attributes, events); a span without start_time takes the trace timestamp'),
|
|
67
70
|
timestamp: z.string().optional().describe('Trace timestamp (ISO 8601); defaults to now() when omitted'),
|
|
68
71
|
};
|
|
72
|
+
export const logTraceOutputSchema = z.looseObject({
|
|
73
|
+
trace_id: z.string().describe('the stored trace id, 32 hex — pass it to evaluate_output, get_traces or delete_trace'),
|
|
74
|
+
status: z.literal('stored').describe('always "stored" on success'),
|
|
75
|
+
});
|
|
69
76
|
export function registerLogTraceTool(server, storage) {
|
|
70
77
|
server.registerTool('log_trace', {
|
|
71
78
|
title: 'Log Trace',
|
|
72
|
-
description:
|
|
73
|
-
'
|
|
74
|
-
''
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
'',
|
|
79
|
-
|
|
80
|
-
''
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
].join('\n'),
|
|
79
|
+
description: describeTool({
|
|
80
|
+
summary: 'Store one agent execution — input, output, tool calls, spans, cost, latency, token usage — and get the trace_id every later call keys on.',
|
|
81
|
+
does: 'Writes one trace row to local SQLite and mints a fresh trace_id; nothing is deduplicated, so resubmitting the same payload stores a second trace. ' +
|
|
82
|
+
'Only agent_name is required. Store what you have: tool_calls so the trajectory rules can later judge what the agent did, cost_usd and token_usage so the cost rules can, input and output so everything else can. ' +
|
|
83
|
+
'When IRIS_OTEL_ENDPOINT is set the trace is also exported to that collector, best-effort and asynchronous; the local write never waits on it. ' +
|
|
84
|
+
'Traces are immutable: there is no update path. In stdio mode nothing authenticates the caller; over HTTP a Bearer token is required only when an API key is configured.',
|
|
85
|
+
whenNot: 'For a transient log line (use your logger). To score an output: log first, then call evaluate_output with the trace_id, which also lets it reuse the stored tool_calls. To change a stored trace: delete_trace and log again.',
|
|
86
|
+
returns: logTraceOutputSchema,
|
|
87
|
+
errors: 'IRIS_STORAGE_ERROR when the database cannot be written. An unknown argument or a malformed span or tool_calls entry is refused before the handler runs, naming the valid keys. ' +
|
|
88
|
+
ERROR_ENVELOPE_SENTENCE,
|
|
89
|
+
siblings: {
|
|
90
|
+
evaluate_output: 'score the stored output',
|
|
91
|
+
get_traces: 'query what was logged',
|
|
92
|
+
delete_trace: 'remove one trace',
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
89
95
|
// Strict at the MCP boundary (unknown args rejected, not stripped).
|
|
90
96
|
// The dashboard's HTTP ingest builds its own — equally strict —
|
|
91
97
|
// schema FROM this shape (dashboard/validation.ts): a client-supplied
|
|
92
98
|
// trace_id is rejected there with a 400 whose message says the server
|
|
93
99
|
// mints it, exactly as this tool mints its own in the handler below.
|
|
94
100
|
inputSchema: strictInput(logTraceInputShape),
|
|
101
|
+
outputSchema: logTraceOutputSchema,
|
|
95
102
|
annotations: {
|
|
96
103
|
readOnlyHint: false, // Writes a row to storage
|
|
97
104
|
destructiveHint: false, // Creates new data; doesn't overwrite or delete
|
|
98
105
|
idempotentHint: false, // Each call mints a fresh trace_id; duplicate payloads produce distinct traces
|
|
99
106
|
openWorldHint: false, // Local storage first. When IRIS_OTEL_ENDPOINT is set a best-effort async OTel export runs but is non-blocking (tool succeeds even if export fails).
|
|
100
107
|
},
|
|
101
|
-
}, async (args) => {
|
|
108
|
+
}, guarded(async (args) => {
|
|
102
109
|
const traceId = generateTraceId();
|
|
103
110
|
const timestamp = args.timestamp ?? new Date().toISOString();
|
|
104
111
|
const trace = {
|
|
@@ -128,13 +135,8 @@ export function registerLogTraceTool(server, storage) {
|
|
|
128
135
|
// eslint-disable-next-line no-console
|
|
129
136
|
console.warn(`[iris.otel] ${err.message}`);
|
|
130
137
|
});
|
|
131
|
-
return {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
text: JSON.stringify({ trace_id: traceId, status: 'stored' }),
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
};
|
|
139
|
-
});
|
|
138
|
+
return respond(logTraceOutputSchema, { trace_id: traceId, status: 'stored' }, [
|
|
139
|
+
{ uri: traceUri(traceId), name: `trace ${traceId}`, description: 'The stored trace with its spans and, later, its evaluations' },
|
|
140
|
+
]);
|
|
141
|
+
}));
|
|
140
142
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { IrisError } from './errors.js';
|
|
4
|
+
export declare const CAPABILITIES_URI = "iris://capabilities";
|
|
5
|
+
export interface ResourceLink {
|
|
6
|
+
uri: string;
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const CAPABILITIES_LINK: ResourceLink;
|
|
11
|
+
export declare function respond<S extends z.ZodType>(schema: S, payload: object, links?: ResourceLink[]): CallToolResult;
|
|
12
|
+
export declare const errorEnvelopeSchema: z.ZodObject<{
|
|
13
|
+
error: z.ZodObject<{
|
|
14
|
+
code: z.ZodEnum<{
|
|
15
|
+
IRIS_INVALID_ARGUMENT: "IRIS_INVALID_ARGUMENT";
|
|
16
|
+
IRIS_UNKNOWN_TRACE: "IRIS_UNKNOWN_TRACE";
|
|
17
|
+
IRIS_DUPLICATE_RULE: "IRIS_DUPLICATE_RULE";
|
|
18
|
+
IRIS_INVALID_RULE_CONFIG: "IRIS_INVALID_RULE_CONFIG";
|
|
19
|
+
IRIS_JUDGE_NOT_ENABLED: "IRIS_JUDGE_NOT_ENABLED";
|
|
20
|
+
IRIS_JUDGE_UNKNOWN_MODEL: "IRIS_JUDGE_UNKNOWN_MODEL";
|
|
21
|
+
IRIS_BUDGET_EXCEEDED: "IRIS_BUDGET_EXCEEDED";
|
|
22
|
+
IRIS_PROVIDER_ERROR: "IRIS_PROVIDER_ERROR";
|
|
23
|
+
IRIS_JUDGE_FAILED: "IRIS_JUDGE_FAILED";
|
|
24
|
+
IRIS_STORAGE_ERROR: "IRIS_STORAGE_ERROR";
|
|
25
|
+
IRIS_INTERNAL_ERROR: "IRIS_INTERNAL_ERROR";
|
|
26
|
+
}>;
|
|
27
|
+
message: z.ZodString;
|
|
28
|
+
recovery: z.ZodArray<z.ZodString>;
|
|
29
|
+
retryable: z.ZodBoolean;
|
|
30
|
+
field: z.ZodOptional<z.ZodString>;
|
|
31
|
+
valid: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
32
|
+
see: z.ZodOptional<z.ZodString>;
|
|
33
|
+
kind: z.ZodOptional<z.ZodString>;
|
|
34
|
+
retryAfterMs: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
}, z.core.$loose>;
|
|
36
|
+
}, z.core.$loose>;
|
|
37
|
+
export type ErrorEnvelope = z.infer<typeof errorEnvelopeSchema>;
|
|
38
|
+
export declare function errorResult(err: IrisError): CallToolResult;
|
|
39
|
+
/** Wrap a handler so every failure returns an envelope instead of a flattened line. */
|
|
40
|
+
export declare function guarded<A extends unknown[]>(fn: (...args: A) => Promise<CallToolResult> | CallToolResult): (...args: A) => Promise<CallToolResult>;
|
|
41
|
+
/** Links for what an evaluation created: the evaluation, and the trace when linked. */
|
|
42
|
+
export declare function evaluationLinks(evalId: string, traceId?: string): ResourceLink[];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* One way to answer a tool call.
|
|
3
|
+
*
|
|
4
|
+
* `respond` parses the payload through the tool's own output schema
|
|
5
|
+
* BEFORE serialising it — a field the schema does not describe fails a
|
|
6
|
+
* test, not a user — and emits the same object twice: as the text a
|
|
7
|
+
* client without structured-content support reads, and as
|
|
8
|
+
* `structuredContent` for one that has it. Beside the payload go
|
|
9
|
+
* `resource_link` items for what the call created and what explains its
|
|
10
|
+
* limits, so a client can follow them with resources/read instead of
|
|
11
|
+
* guessing a URI.
|
|
12
|
+
*
|
|
13
|
+
* `errorResult` is the failure shape: the IrisError envelope as the text
|
|
14
|
+
* and as structuredContent, `isError: true`, and a link to
|
|
15
|
+
* iris://capabilities. `guarded` wraps a handler so nothing thrown inside
|
|
16
|
+
* it reaches the SDK's flattener.
|
|
17
|
+
*/
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { ERROR_CODE_CATALOGUE, toIrisError } from './errors.js';
|
|
20
|
+
import { CAPABILITIES_RESOURCE_URI, evaluationUri, traceUri } from '../resources/uris.js';
|
|
21
|
+
export const CAPABILITIES_URI = CAPABILITIES_RESOURCE_URI;
|
|
22
|
+
export const CAPABILITIES_LINK = {
|
|
23
|
+
uri: CAPABILITIES_URI,
|
|
24
|
+
name: 'capabilities',
|
|
25
|
+
description: 'What this server can judge, what each rule needs, judge state, limits, tools and resources',
|
|
26
|
+
};
|
|
27
|
+
const linkItem = (l) => ({
|
|
28
|
+
type: 'resource_link',
|
|
29
|
+
uri: l.uri,
|
|
30
|
+
name: l.name,
|
|
31
|
+
...(l.description ? { description: l.description } : {}),
|
|
32
|
+
mimeType: 'application/json',
|
|
33
|
+
});
|
|
34
|
+
/** JSON round-trip: what the text carries is exactly what structuredContent carries. */
|
|
35
|
+
function normalise(payload) {
|
|
36
|
+
return JSON.parse(JSON.stringify(payload));
|
|
37
|
+
}
|
|
38
|
+
export function respond(schema, payload, links = []) {
|
|
39
|
+
const body = normalise(payload);
|
|
40
|
+
const parsed = schema.safeParse(body);
|
|
41
|
+
if (!parsed.success) {
|
|
42
|
+
// A programming error: the tool built a response its own schema does
|
|
43
|
+
// not describe. Loud on purpose — the drift-lock tests catch it.
|
|
44
|
+
const issues = parsed.error.issues.slice(0, 3).map((i) => `${i.path.map(String).join('.') || '(root)'}: ${i.message}`);
|
|
45
|
+
throw new Error(`response does not match the tool's output schema: ${issues.join('; ')}`);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: 'text', text: JSON.stringify(body) }, ...links.map(linkItem)],
|
|
49
|
+
structuredContent: body,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export const errorEnvelopeSchema = z.looseObject({
|
|
53
|
+
error: z.looseObject({
|
|
54
|
+
code: z.enum(ERROR_CODE_CATALOGUE),
|
|
55
|
+
message: z.string(),
|
|
56
|
+
recovery: z.array(z.string()),
|
|
57
|
+
retryable: z.boolean(),
|
|
58
|
+
field: z.string().optional(),
|
|
59
|
+
valid: z.array(z.string()).optional(),
|
|
60
|
+
see: z.string().optional(),
|
|
61
|
+
kind: z.string().optional(),
|
|
62
|
+
retryAfterMs: z.number().optional(),
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
export function errorResult(err) {
|
|
66
|
+
const body = normalise({ error: err.envelope });
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: 'text', text: JSON.stringify(body) }, linkItem(CAPABILITIES_LINK)],
|
|
69
|
+
structuredContent: body,
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Wrap a handler so every failure returns an envelope instead of a flattened line. */
|
|
74
|
+
export function guarded(fn) {
|
|
75
|
+
return async (...args) => {
|
|
76
|
+
try {
|
|
77
|
+
return await fn(...args);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return errorResult(toIrisError(err));
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Links for what an evaluation created: the evaluation, and the trace when linked. */
|
|
85
|
+
export function evaluationLinks(evalId, traceId) {
|
|
86
|
+
const links = [{ uri: evaluationUri(evalId), name: `evaluation ${evalId}`, description: 'The stored evaluation, as every reader sees it' }];
|
|
87
|
+
if (traceId)
|
|
88
|
+
links.push({ uri: traceUri(traceId), name: `trace ${traceId}`, description: 'The trace this evaluation is linked to, with its spans and every evaluation' });
|
|
89
|
+
return links;
|
|
90
|
+
}
|
|
@@ -29,7 +29,7 @@ export function strictInput(shape) {
|
|
|
29
29
|
`Valid arguments: ${validKeys}. ` +
|
|
30
30
|
'Unknown arguments are rejected rather than silently ignored, so a misspelled ' +
|
|
31
31
|
'argument name cannot change what gets evaluated — check the spelling against ' +
|
|
32
|
-
"the tool's input schema and retry."
|
|
32
|
+
"the tool's input schema and retry. Code IRIS_INVALID_ARGUMENT."
|
|
33
33
|
: undefined,
|
|
34
34
|
});
|
|
35
35
|
}
|
|
@@ -11,6 +11,8 @@ export declare function unknownTraceMessage(traceId: string): string;
|
|
|
11
11
|
* existence check had to load the row anyway. Fetching it twice would be
|
|
12
12
|
* two reads for one fact — and two chances for them to disagree.
|
|
13
13
|
*/
|
|
14
|
+
/** The IRIS_UNKNOWN_TRACE error, built once for both the pre-check and the insert race. */
|
|
15
|
+
export declare function unknownTraceError(traceId: string): import("./errors.js").IrisError;
|
|
14
16
|
export declare function getTraceOrThrow(storage: IStorageAdapter, tenantId: TenantId, traceId: string): Promise<Trace>;
|
|
15
17
|
export declare function assertTraceExists(storage: IStorageAdapter, tenantId: TenantId, traceId: string): Promise<void>;
|
|
16
18
|
/** insertEvalResult with the foreign-key race translated into the same clear message. */
|
package/dist/tools/trace-link.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { irisError } from './errors.js';
|
|
1
2
|
/*
|
|
2
3
|
* Linking an evaluation to a trace that does not exist.
|
|
3
4
|
*
|
|
@@ -26,10 +27,20 @@ export function unknownTraceMessage(traceId) {
|
|
|
26
27
|
* existence check had to load the row anyway. Fetching it twice would be
|
|
27
28
|
* two reads for one fact — and two chances for them to disagree.
|
|
28
29
|
*/
|
|
30
|
+
/** The IRIS_UNKNOWN_TRACE error, built once for both the pre-check and the insert race. */
|
|
31
|
+
export function unknownTraceError(traceId) {
|
|
32
|
+
return irisError('IRIS_UNKNOWN_TRACE', unknownTraceMessage(traceId), {
|
|
33
|
+
field: 'trace_id',
|
|
34
|
+
recovery: [
|
|
35
|
+
'Pass the trace_id that log_trace returned, or one listed by get_traces.',
|
|
36
|
+
'Or omit trace_id to store an unlinked evaluation.',
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
29
40
|
export async function getTraceOrThrow(storage, tenantId, traceId) {
|
|
30
41
|
const trace = await storage.getTrace(tenantId, traceId);
|
|
31
42
|
if (!trace)
|
|
32
|
-
throw
|
|
43
|
+
throw unknownTraceError(traceId);
|
|
33
44
|
return trace;
|
|
34
45
|
}
|
|
35
46
|
export async function assertTraceExists(storage, tenantId, traceId) {
|
|
@@ -44,7 +55,7 @@ export async function insertLinkedEvalResult(storage, tenantId, result) {
|
|
|
44
55
|
const code = err.code;
|
|
45
56
|
const message = err instanceof Error ? err.message : String(err);
|
|
46
57
|
if (result.trace_id && (code === 'SQLITE_CONSTRAINT_FOREIGNKEY' || /FOREIGN KEY constraint failed/i.test(message))) {
|
|
47
|
-
throw
|
|
58
|
+
throw unknownTraceError(result.trace_id);
|
|
48
59
|
}
|
|
49
60
|
throw err;
|
|
50
61
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
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';
|
|
3
4
|
/**
|
|
@@ -5,8 +6,8 @@ import type { IStorageAdapter } from '../types/query.js';
|
|
|
5
6
|
* was nothing to judge (no citations, none resolved). It is NOT the honest
|
|
6
7
|
* answer when citations resolved and the judge then failed on every one —
|
|
7
8
|
* a wrong API key, a model the provider refused, a parse failure — because
|
|
8
|
-
* the caller reads "passed" and ships. That case is
|
|
9
|
-
* cause; nothing is stored. (v0.6.0 acceptance pass, observation 3.)
|
|
9
|
+
* the caller reads "passed" and ships. That case is IRIS_JUDGE_FAILED
|
|
10
|
+
* naming the cause; nothing is stored. (v0.6.0 acceptance pass, observation 3.)
|
|
10
11
|
*/
|
|
11
12
|
export declare function assertJudgeRan(result: {
|
|
12
13
|
totalResolved: number;
|
|
@@ -19,4 +20,18 @@ export declare function assertJudgeRan(result: {
|
|
|
19
20
|
};
|
|
20
21
|
}>;
|
|
21
22
|
}): void;
|
|
23
|
+
export declare const verifyCitationsOutputSchema: z.ZodObject<{
|
|
24
|
+
id: z.ZodString;
|
|
25
|
+
trace_id: z.ZodOptional<z.ZodString>;
|
|
26
|
+
overall_score: z.ZodNullable<z.ZodNumber>;
|
|
27
|
+
passed: z.ZodBoolean;
|
|
28
|
+
total_citations_found: z.ZodNumber;
|
|
29
|
+
total_resolved: z.ZodNumber;
|
|
30
|
+
total_judged: z.ZodNumber;
|
|
31
|
+
total_supported: z.ZodNumber;
|
|
32
|
+
total_cost_usd: z.ZodNumber;
|
|
33
|
+
citations: z.ZodArray<z.ZodObject<{
|
|
34
|
+
resolve_status: z.ZodString;
|
|
35
|
+
}, z.core.$loose>>;
|
|
36
|
+
}, z.core.$loose>;
|
|
22
37
|
export declare function registerVerifyCitationsTool(server: McpServer, storage: IStorageAdapter): void;
|