@iris-eval/mcp-server 0.4.3-rc.0 → 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 CHANGED
@@ -7,6 +7,7 @@
7
7
  [![GitHub stars](https://img.shields.io/github/stars/iris-eval/mcp-server?style=social)](https://github.com/iris-eval/mcp-server)
8
8
  [![CI](https://github.com/iris-eval/mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/iris-eval/mcp-server/actions/workflows/ci.yml)
9
9
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/iris-eval/mcp-server/badge)](https://securityscorecards.dev/viewer/?uri=github.com/iris-eval/mcp-server)
10
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12849/badge)](https://www.bestpractices.dev/projects/12849)
10
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
11
12
  [![Docker](https://img.shields.io/badge/Docker-ghcr.io-blue?logo=docker)](https://github.com/iris-eval/mcp-server/pkgs/container/mcp-server)
12
13
  [![PulseMCP](https://img.shields.io/badge/PulseMCP-Listed-blue?style=flat-square)](https://www.pulsemcp.com/servers/iris-eval)
@@ -40,7 +41,7 @@ Iris evaluates all of it.
40
41
 
41
42
  ## Quickstart
42
43
 
43
- 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.
44
45
 
45
46
  ```json
46
47
  {
@@ -101,6 +102,60 @@ Then restart the session (`/clear` or relaunch) for tools to load.
101
102
 
102
103
  Add to your workspace `.cursor/mcp.json` or global MCP settings using the JSON config above.
103
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
+
104
159
  </details>
105
160
 
106
161
  ### Other Install Methods
@@ -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');
@@ -1,2 +1,3 @@
1
1
  import type { IrisConfig } from '../types/index.js';
2
+ export declare const PKG_VERSION: string;
2
3
  export declare const defaultConfig: IrisConfig;
@@ -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
- const DefinitionSchema = z.object({
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),