@iris-eval/mcp-server 0.3.0 → 0.4.0-rc.1

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.
Files changed (103) hide show
  1. package/README.md +11 -3
  2. package/dist/audit-log-reader.d.ts +24 -0
  3. package/dist/audit-log-reader.js +87 -0
  4. package/dist/config/defaults.js +7 -1
  5. package/dist/custom-rule-store.d.ts +27 -0
  6. package/dist/custom-rule-store.js +188 -0
  7. package/dist/dashboard/assets/index-BEG5FYWH.css +1 -0
  8. package/dist/dashboard/assets/index-D9JHfSB2.js +12 -0
  9. package/dist/dashboard/index.html +2 -2
  10. package/dist/dashboard/routes/audit.d.ts +3 -0
  11. package/dist/dashboard/routes/audit.js +36 -0
  12. package/dist/dashboard/routes/eval-stats.js +9 -4
  13. package/dist/dashboard/routes/evaluations.js +3 -1
  14. package/dist/dashboard/routes/filters.js +5 -3
  15. package/dist/dashboard/routes/health.js +8 -1
  16. package/dist/dashboard/routes/index.d.ts +4 -0
  17. package/dist/dashboard/routes/index.js +4 -0
  18. package/dist/dashboard/routes/moments.d.ts +3 -0
  19. package/dist/dashboard/routes/moments.js +115 -0
  20. package/dist/dashboard/routes/preferences.d.ts +3 -0
  21. package/dist/dashboard/routes/preferences.js +52 -0
  22. package/dist/dashboard/routes/rules.d.ts +10 -0
  23. package/dist/dashboard/routes/rules.js +169 -0
  24. package/dist/dashboard/routes/summary.js +3 -1
  25. package/dist/dashboard/routes/traces.js +7 -4
  26. package/dist/dashboard/server.d.ts +9 -1
  27. package/dist/dashboard/server.js +52 -3
  28. package/dist/eval/citation-verify/extract.d.ts +11 -0
  29. package/dist/eval/citation-verify/extract.js +102 -0
  30. package/dist/eval/citation-verify/resolve.d.ts +26 -0
  31. package/dist/eval/citation-verify/resolve.js +237 -0
  32. package/dist/eval/citation-verify/verifier.d.ts +43 -0
  33. package/dist/eval/citation-verify/verifier.js +203 -0
  34. package/dist/eval/decision-moment.d.ts +12 -0
  35. package/dist/eval/decision-moment.js +181 -0
  36. package/dist/eval/llm-judge/client.d.ts +28 -0
  37. package/dist/eval/llm-judge/client.js +183 -0
  38. package/dist/eval/llm-judge/evaluator.d.ts +32 -0
  39. package/dist/eval/llm-judge/evaluator.js +138 -0
  40. package/dist/eval/llm-judge/pricing.d.ts +9 -0
  41. package/dist/eval/llm-judge/pricing.js +31 -0
  42. package/dist/eval/llm-judge/templates/index.d.ts +20 -0
  43. package/dist/eval/llm-judge/templates/index.js +170 -0
  44. package/dist/eval/rules/custom.js +13 -2
  45. package/dist/eval/rules/relevance.js +51 -6
  46. package/dist/eval/rules/safety.d.ts +1 -0
  47. package/dist/eval/rules/safety.js +87 -3
  48. package/dist/index.js +77 -14
  49. package/dist/middleware/index.d.ts +1 -0
  50. package/dist/middleware/index.js +1 -0
  51. package/dist/middleware/tenant.d.ts +17 -0
  52. package/dist/middleware/tenant.js +26 -0
  53. package/dist/otel/exporter.d.ts +24 -0
  54. package/dist/otel/exporter.js +116 -0
  55. package/dist/otel/lazy.d.ts +5 -0
  56. package/dist/otel/lazy.js +31 -0
  57. package/dist/otel/mapper.d.ts +24 -0
  58. package/dist/otel/mapper.js +208 -0
  59. package/dist/preferences.d.ts +129 -0
  60. package/dist/preferences.js +152 -0
  61. package/dist/resources/dashboard-summary.js +3 -1
  62. package/dist/resources/trace-detail.js +5 -3
  63. package/dist/server.d.ts +3 -1
  64. package/dist/server.js +9 -3
  65. package/dist/storage/migrations/004-tenant-id.d.ts +3 -0
  66. package/dist/storage/migrations/004-tenant-id.js +40 -0
  67. package/dist/storage/migrations/index.js +2 -1
  68. package/dist/storage/sqlite-adapter.d.ts +17 -15
  69. package/dist/storage/sqlite-adapter.js +130 -79
  70. package/dist/tools/delete-rule.d.ts +3 -0
  71. package/dist/tools/delete-rule.js +53 -0
  72. package/dist/tools/delete-trace.d.ts +3 -0
  73. package/dist/tools/delete-trace.js +54 -0
  74. package/dist/tools/deploy-rule.d.ts +3 -0
  75. package/dist/tools/deploy-rule.js +91 -0
  76. package/dist/tools/evaluate-output.js +23 -2
  77. package/dist/tools/evaluate-with-llm-judge.d.ts +3 -0
  78. package/dist/tools/evaluate-with-llm-judge.js +147 -0
  79. package/dist/tools/get-traces.js +23 -3
  80. package/dist/tools/index.d.ts +2 -1
  81. package/dist/tools/index.js +13 -1
  82. package/dist/tools/list-rules.d.ts +3 -0
  83. package/dist/tools/list-rules.js +66 -0
  84. package/dist/tools/log-trace.js +30 -2
  85. package/dist/tools/verify-citations.d.ts +3 -0
  86. package/dist/tools/verify-citations.js +157 -0
  87. package/dist/types/custom-rule.d.ts +70 -0
  88. package/dist/types/custom-rule.js +1 -0
  89. package/dist/types/decision-moment.d.ts +122 -0
  90. package/dist/types/decision-moment.js +17 -0
  91. package/dist/types/index.d.ts +2 -0
  92. package/dist/types/index.js +1 -1
  93. package/dist/types/query.d.ts +25 -15
  94. package/dist/types/tenant.d.ts +26 -0
  95. package/dist/types/tenant.js +58 -0
  96. package/dist/utils/open-browser.d.ts +1 -0
  97. package/dist/utils/open-browser.js +45 -0
  98. package/dist/utils/validate-port-config.d.ts +2 -0
  99. package/dist/utils/validate-port-config.js +9 -0
  100. package/package.json +4 -1
  101. package/server.json +2 -2
  102. package/dist/dashboard/assets/index-CnDg6bYi.js +0 -43
  103. package/dist/dashboard/assets/index-ZsBou2-c.css +0 -1
@@ -0,0 +1,129 @@
1
+ import { z } from 'zod';
2
+ declare const MomentFiltersSchema: z.ZodObject<{
3
+ agentName: z.ZodOptional<z.ZodString>;
4
+ verdict: z.ZodOptional<z.ZodEnum<["pass", "fail", "partial", "unevaluated"]>>;
5
+ significanceKind: z.ZodOptional<z.ZodEnum<["safety-violation", "cost-spike", "first-failure", "novel-pattern", "rule-collision", "normal-pass", "normal-fail"]>>;
6
+ }, "strict", z.ZodTypeAny, {
7
+ verdict?: "pass" | "fail" | "partial" | "unevaluated" | undefined;
8
+ agentName?: string | undefined;
9
+ significanceKind?: "safety-violation" | "cost-spike" | "first-failure" | "novel-pattern" | "rule-collision" | "normal-pass" | "normal-fail" | undefined;
10
+ }, {
11
+ verdict?: "pass" | "fail" | "partial" | "unevaluated" | undefined;
12
+ agentName?: string | undefined;
13
+ significanceKind?: "safety-violation" | "cost-spike" | "first-failure" | "novel-pattern" | "rule-collision" | "normal-pass" | "normal-fail" | undefined;
14
+ }>;
15
+ export declare const PreferencesSchema: z.ZodObject<{
16
+ autoLaunch: z.ZodDefault<z.ZodBoolean>;
17
+ firstSeen: z.ZodOptional<z.ZodString>;
18
+ dismissedBanners: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
19
+ /** Theme override; "system" defers to prefers-color-scheme. v0.4. */
20
+ theme: z.ZodDefault<z.ZodEnum<["dark", "light", "system"]>>;
21
+ /** Last filter set used on /moments — applied on next visit when the URL has no filter params. v0.4. */
22
+ momentFilters: z.ZodDefault<z.ZodObject<{
23
+ agentName: z.ZodOptional<z.ZodString>;
24
+ verdict: z.ZodOptional<z.ZodEnum<["pass", "fail", "partial", "unevaluated"]>>;
25
+ significanceKind: z.ZodOptional<z.ZodEnum<["safety-violation", "cost-spike", "first-failure", "novel-pattern", "rule-collision", "normal-pass", "normal-fail"]>>;
26
+ }, "strict", z.ZodTypeAny, {
27
+ verdict?: "pass" | "fail" | "partial" | "unevaluated" | undefined;
28
+ agentName?: string | undefined;
29
+ significanceKind?: "safety-violation" | "cost-spike" | "first-failure" | "novel-pattern" | "rule-collision" | "normal-pass" | "normal-fail" | undefined;
30
+ }, {
31
+ verdict?: "pass" | "fail" | "partial" | "unevaluated" | undefined;
32
+ agentName?: string | undefined;
33
+ significanceKind?: "safety-violation" | "cost-spike" | "first-failure" | "novel-pattern" | "rule-collision" | "normal-pass" | "normal-fail" | undefined;
34
+ }>>;
35
+ /** Tour ids the user has completed or dismissed. v0.4. */
36
+ dismissedTours: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
37
+ /** Decision Moments hidden from the timeline by user action. v0.4. */
38
+ archivedMoments: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
39
+ /** Density mode for chrome (Design System v2.A). 'compact' default per R2.3. */
40
+ density: z.ZodDefault<z.ZodEnum<["compact", "comfortable"]>>;
41
+ /** Sidebar collapsed (icon-only at 64px) vs expanded (256px). Default expanded per R2.4. */
42
+ sidebarCollapsed: z.ZodDefault<z.ZodBoolean>;
43
+ /** ISO timestamp of last notifications-popover opened — drives unread badge. */
44
+ notificationsLastSeen: z.ZodOptional<z.ZodString>;
45
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
46
+ autoLaunch: z.ZodDefault<z.ZodBoolean>;
47
+ firstSeen: z.ZodOptional<z.ZodString>;
48
+ dismissedBanners: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
49
+ /** Theme override; "system" defers to prefers-color-scheme. v0.4. */
50
+ theme: z.ZodDefault<z.ZodEnum<["dark", "light", "system"]>>;
51
+ /** Last filter set used on /moments — applied on next visit when the URL has no filter params. v0.4. */
52
+ momentFilters: z.ZodDefault<z.ZodObject<{
53
+ agentName: z.ZodOptional<z.ZodString>;
54
+ verdict: z.ZodOptional<z.ZodEnum<["pass", "fail", "partial", "unevaluated"]>>;
55
+ significanceKind: z.ZodOptional<z.ZodEnum<["safety-violation", "cost-spike", "first-failure", "novel-pattern", "rule-collision", "normal-pass", "normal-fail"]>>;
56
+ }, "strict", z.ZodTypeAny, {
57
+ verdict?: "pass" | "fail" | "partial" | "unevaluated" | undefined;
58
+ agentName?: string | undefined;
59
+ significanceKind?: "safety-violation" | "cost-spike" | "first-failure" | "novel-pattern" | "rule-collision" | "normal-pass" | "normal-fail" | undefined;
60
+ }, {
61
+ verdict?: "pass" | "fail" | "partial" | "unevaluated" | undefined;
62
+ agentName?: string | undefined;
63
+ significanceKind?: "safety-violation" | "cost-spike" | "first-failure" | "novel-pattern" | "rule-collision" | "normal-pass" | "normal-fail" | undefined;
64
+ }>>;
65
+ /** Tour ids the user has completed or dismissed. v0.4. */
66
+ dismissedTours: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
67
+ /** Decision Moments hidden from the timeline by user action. v0.4. */
68
+ archivedMoments: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
69
+ /** Density mode for chrome (Design System v2.A). 'compact' default per R2.3. */
70
+ density: z.ZodDefault<z.ZodEnum<["compact", "comfortable"]>>;
71
+ /** Sidebar collapsed (icon-only at 64px) vs expanded (256px). Default expanded per R2.4. */
72
+ sidebarCollapsed: z.ZodDefault<z.ZodBoolean>;
73
+ /** ISO timestamp of last notifications-popover opened — drives unread badge. */
74
+ notificationsLastSeen: z.ZodOptional<z.ZodString>;
75
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
76
+ autoLaunch: z.ZodDefault<z.ZodBoolean>;
77
+ firstSeen: z.ZodOptional<z.ZodString>;
78
+ dismissedBanners: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
79
+ /** Theme override; "system" defers to prefers-color-scheme. v0.4. */
80
+ theme: z.ZodDefault<z.ZodEnum<["dark", "light", "system"]>>;
81
+ /** Last filter set used on /moments — applied on next visit when the URL has no filter params. v0.4. */
82
+ momentFilters: z.ZodDefault<z.ZodObject<{
83
+ agentName: z.ZodOptional<z.ZodString>;
84
+ verdict: z.ZodOptional<z.ZodEnum<["pass", "fail", "partial", "unevaluated"]>>;
85
+ significanceKind: z.ZodOptional<z.ZodEnum<["safety-violation", "cost-spike", "first-failure", "novel-pattern", "rule-collision", "normal-pass", "normal-fail"]>>;
86
+ }, "strict", z.ZodTypeAny, {
87
+ verdict?: "pass" | "fail" | "partial" | "unevaluated" | undefined;
88
+ agentName?: string | undefined;
89
+ significanceKind?: "safety-violation" | "cost-spike" | "first-failure" | "novel-pattern" | "rule-collision" | "normal-pass" | "normal-fail" | undefined;
90
+ }, {
91
+ verdict?: "pass" | "fail" | "partial" | "unevaluated" | undefined;
92
+ agentName?: string | undefined;
93
+ significanceKind?: "safety-violation" | "cost-spike" | "first-failure" | "novel-pattern" | "rule-collision" | "normal-pass" | "normal-fail" | undefined;
94
+ }>>;
95
+ /** Tour ids the user has completed or dismissed. v0.4. */
96
+ dismissedTours: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
97
+ /** Decision Moments hidden from the timeline by user action. v0.4. */
98
+ archivedMoments: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
99
+ /** Density mode for chrome (Design System v2.A). 'compact' default per R2.3. */
100
+ density: z.ZodDefault<z.ZodEnum<["compact", "comfortable"]>>;
101
+ /** Sidebar collapsed (icon-only at 64px) vs expanded (256px). Default expanded per R2.4. */
102
+ sidebarCollapsed: z.ZodDefault<z.ZodBoolean>;
103
+ /** ISO timestamp of last notifications-popover opened — drives unread badge. */
104
+ notificationsLastSeen: z.ZodOptional<z.ZodString>;
105
+ }, z.ZodTypeAny, "passthrough">>;
106
+ export type Preferences = z.infer<typeof PreferencesSchema>;
107
+ export type MomentFilters = z.infer<typeof MomentFiltersSchema>;
108
+ export interface PreferenceState {
109
+ /** True if `~/.iris/preferences.json` did not exist when iris-mcp started. */
110
+ isFirstRun: boolean;
111
+ /** Resolved preferences (defaulted + validated). */
112
+ preferences: Preferences;
113
+ /** Absolute path to the preferences file. */
114
+ path: string;
115
+ }
116
+ export declare function loadOrInitPreferences(customPath?: string): PreferenceState;
117
+ export declare function shouldAutoLaunchDashboard(state: PreferenceState): boolean;
118
+ export interface PreferenceStore {
119
+ readonly path: string;
120
+ /** Current preferences (always defaulted + validated). */
121
+ read(): Preferences;
122
+ /**
123
+ * Apply a partial update + persist atomically. Returns the merged result.
124
+ * Throws if the merge fails Zod validation (e.g., illegal verdict value).
125
+ */
126
+ patch(input: Partial<Preferences>): Preferences;
127
+ }
128
+ export declare function createPreferenceStore(customPath?: string): PreferenceStore;
129
+ export {};
@@ -0,0 +1,152 @@
1
+ /*
2
+ * preferences — first-run + per-user dashboard preferences for iris-mcp.
3
+ *
4
+ * State lives at ~/.iris/preferences.json. The file is created on first
5
+ * dashboard launch with `autoLaunch: true` and `firstSeen` set to the
6
+ * current ISO timestamp.
7
+ *
8
+ * Subsequent runs read the file and respect the user's autoLaunch setting.
9
+ * The env var IRIS_NO_AUTO_LAUNCH=1 overrides preferences and disables
10
+ * auto-launch unconditionally (useful for CI, headless servers, container
11
+ * runs, and remote SSH sessions).
12
+ *
13
+ * The file format is intentionally small — additional preferences will be
14
+ * added as the dashboard grows; the schema validates known keys but
15
+ * tolerates unknown ones forward-compatibly.
16
+ *
17
+ * v0.4 expansions (B8.2): theme, momentFilters (last filter set used on
18
+ * /moments), dismissedTours, archivedMoments. These let the dashboard
19
+ * remember the user's last view across reloads + across iris-mcp restarts.
20
+ */
21
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, renameSync } from 'node:fs';
22
+ import { join, dirname } from 'node:path';
23
+ import { homedir } from 'node:os';
24
+ import { z } from 'zod';
25
+ const MomentFiltersSchema = z
26
+ .object({
27
+ agentName: z.string().max(200).optional(),
28
+ verdict: z.enum(['pass', 'fail', 'partial', 'unevaluated']).optional(),
29
+ significanceKind: z
30
+ .enum([
31
+ 'safety-violation',
32
+ 'cost-spike',
33
+ 'first-failure',
34
+ 'novel-pattern',
35
+ 'rule-collision',
36
+ 'normal-pass',
37
+ 'normal-fail',
38
+ ])
39
+ .optional(),
40
+ })
41
+ .strict();
42
+ export const PreferencesSchema = z
43
+ .object({
44
+ autoLaunch: z.boolean().default(true),
45
+ firstSeen: z.string().datetime({ offset: true }).optional(),
46
+ dismissedBanners: z.array(z.string().max(80)).default([]),
47
+ /** Theme override; "system" defers to prefers-color-scheme. v0.4. */
48
+ theme: z.enum(['dark', 'light', 'system']).default('system'),
49
+ /** Last filter set used on /moments — applied on next visit when the URL has no filter params. v0.4. */
50
+ momentFilters: MomentFiltersSchema.default({}),
51
+ /** Tour ids the user has completed or dismissed. v0.4. */
52
+ dismissedTours: z.array(z.string().max(80)).default([]),
53
+ /** Decision Moments hidden from the timeline by user action. v0.4. */
54
+ archivedMoments: z.array(z.string().max(200)).default([]),
55
+ /** Density mode for chrome (Design System v2.A). 'compact' default per R2.3. */
56
+ density: z.enum(['compact', 'comfortable']).default('compact'),
57
+ /** Sidebar collapsed (icon-only at 64px) vs expanded (256px). Default expanded per R2.4. */
58
+ sidebarCollapsed: z.boolean().default(false),
59
+ /** ISO timestamp of last notifications-popover opened — drives unread badge. */
60
+ notificationsLastSeen: z.string().datetime({ offset: true }).optional(),
61
+ })
62
+ .passthrough();
63
+ function defaultPreferencesPath() {
64
+ return join(homedir(), '.iris', 'preferences.json');
65
+ }
66
+ function freshPreferences() {
67
+ return PreferencesSchema.parse({
68
+ autoLaunch: true,
69
+ firstSeen: new Date().toISOString(),
70
+ dismissedBanners: [],
71
+ theme: 'system',
72
+ momentFilters: {},
73
+ dismissedTours: [],
74
+ archivedMoments: [],
75
+ });
76
+ }
77
+ function writeAtomic(targetPath, contents) {
78
+ mkdirSync(dirname(targetPath), { recursive: true });
79
+ const tmp = `${targetPath}.tmp.${process.pid}`;
80
+ writeFileSync(tmp, contents, 'utf-8');
81
+ renameSync(tmp, targetPath);
82
+ }
83
+ export function loadOrInitPreferences(customPath) {
84
+ const path = customPath ?? defaultPreferencesPath();
85
+ if (!existsSync(path)) {
86
+ const fresh = freshPreferences();
87
+ try {
88
+ writeAtomic(path, JSON.stringify(fresh, null, 2));
89
+ }
90
+ catch {
91
+ // Filesystem may be read-only (containerized run); we still proceed
92
+ // with the defaults in memory but isFirstRun stays true so callers
93
+ // can decide whether to auto-launch anyway.
94
+ }
95
+ return { isFirstRun: true, preferences: fresh, path };
96
+ }
97
+ try {
98
+ const raw = readFileSync(path, 'utf-8');
99
+ const parsed = PreferencesSchema.safeParse(JSON.parse(raw));
100
+ if (parsed.success) {
101
+ return { isFirstRun: false, preferences: parsed.data, path };
102
+ }
103
+ // Malformed file — fall back to defaults but DON'T overwrite the
104
+ // existing file. The user may want to fix it manually.
105
+ return {
106
+ isFirstRun: false,
107
+ preferences: PreferencesSchema.parse({}),
108
+ path,
109
+ };
110
+ }
111
+ catch {
112
+ return {
113
+ isFirstRun: false,
114
+ preferences: PreferencesSchema.parse({}),
115
+ path,
116
+ };
117
+ }
118
+ }
119
+ export function shouldAutoLaunchDashboard(state) {
120
+ if (process.env.IRIS_NO_AUTO_LAUNCH === '1')
121
+ return false;
122
+ if (process.env.CI === 'true' || process.env.CI === '1')
123
+ return false;
124
+ return state.preferences.autoLaunch !== false;
125
+ }
126
+ export function createPreferenceStore(customPath) {
127
+ const path = customPath ?? defaultPreferencesPath();
128
+ let current = loadOrInitPreferences(path).preferences;
129
+ return {
130
+ path,
131
+ read() {
132
+ return current;
133
+ },
134
+ patch(input) {
135
+ const merged = { ...current, ...input };
136
+ // Re-parse so default coalescing + validation runs, and so passthrough
137
+ // forward-compat preserves any unknown keys present in the file.
138
+ const validated = PreferencesSchema.parse(merged);
139
+ try {
140
+ writeAtomic(path, JSON.stringify(validated, null, 2));
141
+ }
142
+ catch (err) {
143
+ // If persist fails (read-only fs, disk full), still update memory
144
+ // so the UI feels responsive — but rethrow so the API surfaces it.
145
+ current = validated;
146
+ throw err;
147
+ }
148
+ current = validated;
149
+ return validated;
150
+ },
151
+ };
152
+ }
@@ -1,6 +1,8 @@
1
+ import { LOCAL_TENANT } from '../types/tenant.js';
1
2
  export function registerDashboardSummaryResource(server, storage) {
2
3
  server.resource('dashboard-summary', 'iris://dashboard/summary', { description: 'Dashboard summary with key metrics and trends' }, async () => {
3
- const summary = await storage.getDashboardSummary();
4
+ // OSS single-tenant: summary scopes to the local user.
5
+ const summary = await storage.getDashboardSummary(LOCAL_TENANT);
4
6
  return {
5
7
  contents: [
6
8
  {
@@ -1,7 +1,9 @@
1
+ import { LOCAL_TENANT } from '../types/tenant.js';
1
2
  export function registerTraceDetailResource(server, storage) {
2
3
  server.resource('trace-detail', 'iris://traces/{trace_id}', { description: 'Full trace detail with spans and evaluation results' }, async (uri) => {
3
4
  const traceId = uri.pathname.split('/').pop();
4
- const trace = await storage.getTrace(traceId);
5
+ // OSS single-tenant: MCP caller is the local user.
6
+ const trace = await storage.getTrace(LOCAL_TENANT, traceId);
5
7
  if (!trace) {
6
8
  return {
7
9
  contents: [
@@ -13,8 +15,8 @@ export function registerTraceDetailResource(server, storage) {
13
15
  ],
14
16
  };
15
17
  }
16
- const spans = await storage.getSpansByTraceId(traceId);
17
- const evals = await storage.getEvalsByTraceId(traceId);
18
+ const spans = await storage.getSpansByTraceId(LOCAL_TENANT, traceId);
19
+ const evals = await storage.getEvalsByTraceId(LOCAL_TENANT, traceId);
18
20
  return {
19
21
  contents: [
20
22
  {
package/dist/server.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { IrisConfig } from './types/index.js';
3
3
  import type { IStorageAdapter } from './types/query.js';
4
+ import type { CustomRuleStore } from './custom-rule-store.js';
4
5
  import { EvalEngine } from './eval/engine.js';
5
6
  export interface IrisServer {
6
7
  mcpServer: McpServer;
7
8
  evalEngine: EvalEngine;
9
+ customRuleStore: CustomRuleStore;
8
10
  }
9
- export declare function createIrisServer(config: IrisConfig, storage: IStorageAdapter): IrisServer;
11
+ export declare function createIrisServer(config: IrisConfig, storage: IStorageAdapter, customRuleStore?: CustomRuleStore): IrisServer;
package/dist/server.js CHANGED
@@ -2,13 +2,19 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { EvalEngine } from './eval/engine.js';
3
3
  import { registerAllTools } from './tools/index.js';
4
4
  import { registerAllResources } from './resources/index.js';
5
- export function createIrisServer(config, storage) {
5
+ import { createCustomRuleStore } from './custom-rule-store.js';
6
+ export function createIrisServer(config, storage, customRuleStore) {
6
7
  const mcpServer = new McpServer({
7
8
  name: config.server.name,
8
9
  version: config.server.version,
9
10
  });
10
11
  const evalEngine = new EvalEngine(config.eval.defaultThreshold, config.eval.ruleThresholds);
11
- registerAllTools(mcpServer, storage, evalEngine);
12
+ // Caller can inject a shared rule store (e.g. index.ts passes the
13
+ // same instance the HTTP dashboard uses, so a rule deployed via MCP
14
+ // is immediately visible in the dashboard without a restart). If
15
+ // none provided, create a fresh one loading from the default path.
16
+ const ruleStore = customRuleStore ?? createCustomRuleStore();
17
+ registerAllTools(mcpServer, storage, evalEngine, ruleStore);
12
18
  registerAllResources(mcpServer, storage);
13
- return { mcpServer, evalEngine };
19
+ return { mcpServer, evalEngine, customRuleStore: ruleStore };
14
20
  }
@@ -0,0 +1,3 @@
1
+ import type Database from 'better-sqlite3';
2
+ export declare const id = "004-tenant-id";
3
+ export declare function up(db: Database.Database): void;
@@ -0,0 +1,40 @@
1
+ export const id = '004-tenant-id';
2
+ /*
3
+ * Migration 004 — tenant scaffolding.
4
+ *
5
+ * Adds `tenant_id TEXT NOT NULL DEFAULT 'local'` to every data table.
6
+ * Existing rows are backfilled to 'local' by the default. New rows
7
+ * (v0.4+) MUST supply a tenant_id via the storage adapter — the default
8
+ * only exists so the existing-row backfill is automatic.
9
+ *
10
+ * Design notes (threat model §5.1 + §5.2):
11
+ * - Not NULL. Never allow a row without a tenant.
12
+ * - DEFAULT 'local'. Backfill is automatic; existing OSS installs
13
+ * continue working with zero user action.
14
+ * - Composite indexes put tenant_id first so per-tenant queries use
15
+ * the leading index column. Existing single-column indexes stay
16
+ * for non-tenant lookups (e.g., trace_id by primary key is still
17
+ * fast).
18
+ *
19
+ * Forward compatibility: when Cloud goes live, the storage backend
20
+ * swaps from SQLite to Postgres. The tenant_id column + index
21
+ * conventions translate 1:1.
22
+ */
23
+ export function up(db) {
24
+ db.exec(`
25
+ -- traces
26
+ ALTER TABLE traces ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'local';
27
+ CREATE INDEX IF NOT EXISTS idx_traces_tenant_timestamp ON traces(tenant_id, timestamp);
28
+ CREATE INDEX IF NOT EXISTS idx_traces_tenant_agent ON traces(tenant_id, agent_name);
29
+
30
+ -- spans
31
+ ALTER TABLE spans ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'local';
32
+ CREATE INDEX IF NOT EXISTS idx_spans_tenant_trace ON spans(tenant_id, trace_id);
33
+
34
+ -- eval_results
35
+ ALTER TABLE eval_results ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'local';
36
+ CREATE INDEX IF NOT EXISTS idx_eval_results_tenant_created ON eval_results(tenant_id, created_at);
37
+ CREATE INDEX IF NOT EXISTS idx_eval_results_tenant_trace ON eval_results(tenant_id, trace_id);
38
+ CREATE INDEX IF NOT EXISTS idx_eval_results_tenant_type ON eval_results(tenant_id, eval_type);
39
+ `);
40
+ }
@@ -1,7 +1,8 @@
1
1
  import * as migration001 from './001-initial-schema.js';
2
2
  import * as migration002 from './002-eval-skip-fields.js';
3
3
  import * as migration003 from './003-eval-passed-index.js';
4
- const migrations = [migration001, migration002, migration003];
4
+ import * as migration004 from './004-tenant-id.js';
5
+ const migrations = [migration001, migration002, migration003, migration004];
5
6
  export function runMigrations(db) {
6
7
  db.exec(`
7
8
  CREATE TABLE IF NOT EXISTS _iris_migrations (
@@ -1,19 +1,20 @@
1
1
  import type { IStorageAdapter, DashboardSummary, TraceQueryOptions, TraceQueryResult, EvalStatsPeriod, EvalStats, EvalStatsTrendBucket, EvalStatsRuleBreakdown, EvalStatsFailure } from '../types/query.js';
2
2
  import type { Trace, Span } from '../types/trace.js';
3
3
  import type { EvalResult } from '../types/eval.js';
4
+ import type { TenantId } from '../types/tenant.js';
4
5
  export declare class SqliteAdapter implements IStorageAdapter {
5
6
  private db;
6
7
  constructor(dbPath: string);
7
8
  initialize(): Promise<void>;
8
9
  close(): Promise<void>;
9
- insertTrace(trace: Trace): Promise<void>;
10
- getTrace(traceId: string): Promise<Trace | null>;
11
- queryTraces(options: TraceQueryOptions): Promise<TraceQueryResult>;
12
- insertSpan(span: Span): Promise<void>;
13
- getSpansByTraceId(traceId: string): Promise<Span[]>;
14
- insertEvalResult(result: EvalResult): Promise<void>;
15
- getEvalsByTraceId(traceId: string): Promise<EvalResult[]>;
16
- queryEvalResults(options: {
10
+ insertTrace(tenantId: TenantId, trace: Trace): Promise<void>;
11
+ getTrace(tenantId: TenantId, traceId: string): Promise<Trace | null>;
12
+ queryTraces(tenantId: TenantId, options: TraceQueryOptions): Promise<TraceQueryResult>;
13
+ insertSpan(tenantId: TenantId, span: Span): Promise<void>;
14
+ getSpansByTraceId(tenantId: TenantId, traceId: string): Promise<Span[]>;
15
+ insertEvalResult(tenantId: TenantId, result: EvalResult): Promise<void>;
16
+ getEvalsByTraceId(tenantId: TenantId, traceId: string): Promise<EvalResult[]>;
17
+ queryEvalResults(tenantId: TenantId, options: {
17
18
  eval_type?: string;
18
19
  passed?: boolean;
19
20
  since?: string;
@@ -24,14 +25,15 @@ export declare class SqliteAdapter implements IStorageAdapter {
24
25
  results: EvalResult[];
25
26
  total: number;
26
27
  }>;
27
- getDashboardSummary(sinceHours?: number): Promise<DashboardSummary>;
28
+ getDashboardSummary(tenantId: TenantId, sinceHours?: number): Promise<DashboardSummary>;
28
29
  private periodToSince;
29
- getEvalStats(period: EvalStatsPeriod): Promise<EvalStats>;
30
- getEvalStatsTrend(period: EvalStatsPeriod): Promise<EvalStatsTrendBucket[]>;
31
- getEvalStatsRules(period: EvalStatsPeriod): Promise<EvalStatsRuleBreakdown[]>;
32
- getEvalStatsFailures(period: EvalStatsPeriod, limit: number): Promise<EvalStatsFailure[]>;
33
- deleteTracesOlderThan(days: number): Promise<number>;
34
- getDistinctValues(column: string): Promise<string[]>;
30
+ getEvalStats(tenantId: TenantId, period: EvalStatsPeriod): Promise<EvalStats>;
31
+ getEvalStatsTrend(tenantId: TenantId, period: EvalStatsPeriod): Promise<EvalStatsTrendBucket[]>;
32
+ getEvalStatsRules(tenantId: TenantId, period: EvalStatsPeriod): Promise<EvalStatsRuleBreakdown[]>;
33
+ getEvalStatsFailures(tenantId: TenantId, period: EvalStatsPeriod, limit: number): Promise<EvalStatsFailure[]>;
34
+ deleteTracesOlderThan(tenantId: TenantId, days: number): Promise<number>;
35
+ deleteTrace(tenantId: TenantId, traceId: string): Promise<boolean>;
36
+ getDistinctValues(tenantId: TenantId, column: string): Promise<string[]>;
35
37
  private rowToTrace;
36
38
  private rowToSpan;
37
39
  private rowToEvalResult;