@iris-eval/mcp-server 0.4.4 → 0.4.5
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 +55 -1
- package/dist/audit-log-reader.js +1 -1
- package/dist/config/defaults.d.ts +1 -0
- package/dist/config/defaults.js +3 -0
- package/dist/custom-rule-store.js +109 -2
- package/dist/dashboard/assets/index-CIKsbEhq.js +10 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/routes/rules.js +1 -1
- package/dist/dashboard/server.js +16 -3
- package/dist/dashboard/validation.d.ts +36 -62
- package/dist/eval/citation-verify/resolve.js +101 -15
- package/dist/eval/rules/config-keys.d.ts +14 -0
- package/dist/eval/rules/config-keys.js +43 -0
- package/dist/eval/rules/custom.js +43 -14
- package/dist/eval/rules/relevance.d.ts +1 -0
- package/dist/eval/rules/relevance.js +3 -1
- package/dist/eval/rules/safety.d.ts +5 -0
- package/dist/eval/rules/safety.js +4 -2
- package/dist/index.js +3 -1
- package/dist/middleware/error-handler.js +19 -1
- package/dist/otel/mapper.js +2 -1
- package/dist/preferences.d.ts +43 -93
- package/dist/tools/deploy-rule.js +2 -2
- package/dist/tools/evaluate-output.js +1 -1
- package/dist/tools/log-trace.js +3 -3
- package/dist/transport/http.js +68 -4
- package/package.json +10 -5
- package/server.json +1 -1
- package/dist/dashboard/assets/index-DNflCqmJ.js +0 -12
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ Iris evaluates all of it.
|
|
|
41
41
|
|
|
42
42
|
## Quickstart
|
|
43
43
|
|
|
44
|
-
Add Iris to your MCP config. Works with Claude Desktop, Cursor, Windsurf, and any MCP-compatible agent.
|
|
44
|
+
Add Iris to your MCP config. Works with Claude Desktop, Claude Code, Cursor, Windsurf, Continue, VS Code, Cline, Zed, Codex CLI, Gemini CLI — and any other MCP-compatible agent.
|
|
45
45
|
|
|
46
46
|
```json
|
|
47
47
|
{
|
|
@@ -102,6 +102,60 @@ Then restart the session (`/clear` or relaunch) for tools to load.
|
|
|
102
102
|
|
|
103
103
|
Add to your workspace `.cursor/mcp.json` or global MCP settings using the JSON config above.
|
|
104
104
|
|
|
105
|
+
#### VS Code (native MCP)
|
|
106
|
+
|
|
107
|
+
Add to `.vscode/mcp.json` in your workspace (note: VS Code uses `servers`, not `mcpServers`):
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"servers": {
|
|
112
|
+
"iris-eval": {
|
|
113
|
+
"command": "npx",
|
|
114
|
+
"args": ["@iris-eval/mcp-server"]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Cline
|
|
121
|
+
|
|
122
|
+
Open Cline's MCP Servers panel → Configure MCP Servers, and add the `mcpServers` JSON config above to `cline_mcp_settings.json`.
|
|
123
|
+
|
|
124
|
+
#### Zed
|
|
125
|
+
|
|
126
|
+
Add to Zed `settings.json`:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"context_servers": {
|
|
131
|
+
"iris-eval": {
|
|
132
|
+
"command": {
|
|
133
|
+
"path": "npx",
|
|
134
|
+
"args": ["@iris-eval/mcp-server"]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### OpenAI Codex CLI
|
|
142
|
+
|
|
143
|
+
Add to `~/.codex/config.toml`:
|
|
144
|
+
|
|
145
|
+
```toml
|
|
146
|
+
[mcp_servers.iris-eval]
|
|
147
|
+
command = "npx"
|
|
148
|
+
args = ["@iris-eval/mcp-server"]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### Gemini CLI
|
|
152
|
+
|
|
153
|
+
Add the `mcpServers` JSON config above to `~/.gemini/settings.json`.
|
|
154
|
+
|
|
155
|
+
#### Anything else that speaks MCP
|
|
156
|
+
|
|
157
|
+
Iris is a standard stdio MCP server — one `npx @iris-eval/mcp-server` command, no SDK, no code changes. If your client supports MCP, it supports Iris. Client config formats change; when in doubt, check your client's MCP docs and point it at that command.
|
|
158
|
+
|
|
105
159
|
</details>
|
|
106
160
|
|
|
107
161
|
### Other Install Methods
|
package/dist/audit-log-reader.js
CHANGED
|
@@ -28,7 +28,7 @@ const EntrySchema = z.object({
|
|
|
28
28
|
user: z.string(),
|
|
29
29
|
ruleId: z.string(),
|
|
30
30
|
ruleName: z.string().optional(),
|
|
31
|
-
details: z.record(z.unknown()).optional(),
|
|
31
|
+
details: z.record(z.string(), z.unknown()).optional(),
|
|
32
32
|
});
|
|
33
33
|
function defaultAuditPath() {
|
|
34
34
|
return join(homedir(), '.iris', 'audit.log');
|
package/dist/config/defaults.js
CHANGED
|
@@ -11,6 +11,9 @@ try {
|
|
|
11
11
|
catch {
|
|
12
12
|
// Fallback if package.json isn't resolvable at runtime
|
|
13
13
|
}
|
|
14
|
+
// The single runtime source for the server's own version — import this
|
|
15
|
+
// instead of hardcoding a literal (OTel resource attrs, health, banners).
|
|
16
|
+
export const PKG_VERSION = pkgVersion;
|
|
14
17
|
export const defaultConfig = {
|
|
15
18
|
storage: {
|
|
16
19
|
type: 'sqlite',
|
|
@@ -28,6 +28,8 @@ import { join, dirname } from 'node:path';
|
|
|
28
28
|
import { homedir } from 'node:os';
|
|
29
29
|
import { randomBytes } from 'node:crypto';
|
|
30
30
|
import { z } from 'zod';
|
|
31
|
+
import isSafeRegex from 'safe-regex2';
|
|
32
|
+
import { CUSTOM_RULE_CONFIG_KEYS, readNumericConfig, describeKeys } from './eval/rules/config-keys.js';
|
|
31
33
|
import { LOCAL_TENANT } from './types/tenant.js';
|
|
32
34
|
const SEVERITY_VALUES = ['low', 'medium', 'high', 'critical'];
|
|
33
35
|
const EVAL_TYPE_VALUES = ['completeness', 'relevance', 'safety', 'cost', 'custom'];
|
|
@@ -41,11 +43,116 @@ const RULE_TYPE_VALUES = [
|
|
|
41
43
|
'json_schema',
|
|
42
44
|
'cost_threshold',
|
|
43
45
|
];
|
|
44
|
-
|
|
46
|
+
// Per-type config requirements, enforced at DEPLOY time.
|
|
47
|
+
//
|
|
48
|
+
// `config` was previously `z.record(z.unknown())` — any object passed. That
|
|
49
|
+
// let a rule like {type:'min_length', config:{}} deploy successfully and then
|
|
50
|
+
// fail on every single evaluation forever, silently dragging down aggregate
|
|
51
|
+
// scores with no indication the RULE (not the agent) was broken. Validating
|
|
52
|
+
// here means the failure surfaces once, at deploy, with an actionable message
|
|
53
|
+
// — instead of quietly corrupting every eval that follows.
|
|
54
|
+
const MAX_RULE_PATTERN_LENGTH = 1000;
|
|
55
|
+
function requirePositiveNumber(config, type, ctx) {
|
|
56
|
+
const value = readNumericConfig(config, type);
|
|
57
|
+
if (value == null || value <= 0) {
|
|
58
|
+
ctx.addIssue({
|
|
59
|
+
code: z.ZodIssueCode.custom,
|
|
60
|
+
path: ['config', CUSTOM_RULE_CONFIG_KEYS[type][0]],
|
|
61
|
+
message: `${type} rule requires ${describeKeys(type)} (positive number)`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function requireNonEmptyStringArray(config, key, ctx, hint) {
|
|
66
|
+
const value = config[key];
|
|
67
|
+
if (!Array.isArray(value) || value.length === 0 || !value.every((v) => typeof v === 'string')) {
|
|
68
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['config', key], message: hint });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const DefinitionSchema = z
|
|
72
|
+
.object({
|
|
45
73
|
name: z.string().min(1).max(80),
|
|
46
74
|
type: z.enum(RULE_TYPE_VALUES),
|
|
47
|
-
config: z.record(z.unknown()),
|
|
75
|
+
config: z.record(z.string(), z.unknown()),
|
|
48
76
|
weight: z.number().positive().optional(),
|
|
77
|
+
})
|
|
78
|
+
.superRefine((def, ctx) => {
|
|
79
|
+
const config = def.config ?? {};
|
|
80
|
+
switch (def.type) {
|
|
81
|
+
case 'regex_match':
|
|
82
|
+
case 'regex_no_match': {
|
|
83
|
+
const pattern = config.pattern;
|
|
84
|
+
if (typeof pattern !== 'string' || pattern.length === 0) {
|
|
85
|
+
ctx.addIssue({
|
|
86
|
+
code: z.ZodIssueCode.custom,
|
|
87
|
+
path: ['config', 'pattern'],
|
|
88
|
+
message: `${def.type} rule requires config.pattern (non-empty string)`,
|
|
89
|
+
});
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
if (pattern.length > MAX_RULE_PATTERN_LENGTH) {
|
|
93
|
+
ctx.addIssue({
|
|
94
|
+
code: z.ZodIssueCode.custom,
|
|
95
|
+
path: ['config', 'pattern'],
|
|
96
|
+
message: `Regex pattern too long (${pattern.length} > ${MAX_RULE_PATTERN_LENGTH})`,
|
|
97
|
+
});
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
// Strip a leading inline flag group the way the evaluator does, so a
|
|
101
|
+
// pattern that WILL run is not rejected here for syntax it tolerates.
|
|
102
|
+
const stripped = pattern.replace(/^\(\?[imsugy]+\)/, '');
|
|
103
|
+
// Syntax BEFORE safety: safe-regex2 returns false for anything it
|
|
104
|
+
// cannot parse, so checking it first reports a plainly broken pattern
|
|
105
|
+
// like `(` as "catastrophic backtracking" — an error that sends the
|
|
106
|
+
// author looking for a performance problem they do not have.
|
|
107
|
+
try {
|
|
108
|
+
new RegExp(stripped, typeof config.flags === 'string' ? config.flags : '');
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
ctx.addIssue({
|
|
112
|
+
code: z.ZodIssueCode.custom,
|
|
113
|
+
path: ['config', 'pattern'],
|
|
114
|
+
message: `Invalid regex syntax: ${e instanceof Error ? e.message : 'unknown error'}`,
|
|
115
|
+
});
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (!isSafeRegex(stripped)) {
|
|
119
|
+
ctx.addIssue({
|
|
120
|
+
code: z.ZodIssueCode.custom,
|
|
121
|
+
path: ['config', 'pattern'],
|
|
122
|
+
message: 'Regex pattern rejected: potentially unsafe (catastrophic backtracking)',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'min_length':
|
|
128
|
+
requirePositiveNumber(config, 'min_length', ctx);
|
|
129
|
+
break;
|
|
130
|
+
case 'max_length':
|
|
131
|
+
requirePositiveNumber(config, 'max_length', ctx);
|
|
132
|
+
break;
|
|
133
|
+
case 'contains_keywords':
|
|
134
|
+
requireNonEmptyStringArray(config, 'keywords', ctx, 'contains_keywords rule requires config.keywords (non-empty string array)');
|
|
135
|
+
break;
|
|
136
|
+
case 'excludes_keywords':
|
|
137
|
+
requireNonEmptyStringArray(config, 'keywords', ctx, 'excludes_keywords rule requires config.keywords (non-empty string array)');
|
|
138
|
+
break;
|
|
139
|
+
case 'cost_threshold': {
|
|
140
|
+
// 0 is a legitimate threshold ("must be free"), so only reject
|
|
141
|
+
// missing / non-numeric / negative.
|
|
142
|
+
const max = readNumericConfig(config, 'cost_threshold');
|
|
143
|
+
if (max == null || max < 0) {
|
|
144
|
+
ctx.addIssue({
|
|
145
|
+
code: z.ZodIssueCode.custom,
|
|
146
|
+
path: ['config', CUSTOM_RULE_CONFIG_KEYS.cost_threshold[0]],
|
|
147
|
+
message: `cost_threshold rule requires ${describeKeys('cost_threshold')} (non-negative number)`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case 'json_schema':
|
|
153
|
+
// No required config — validity is judged against the output itself.
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
49
156
|
});
|
|
50
157
|
const DeployedRuleSchema = z.object({
|
|
51
158
|
id: z.string().min(1),
|