@run-iq/mcp-server 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1115 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/engine.ts
8
+ import { PPEEngine } from "@run-iq/core";
9
+ import { FiscalPlugin } from "@run-iq/plugin-fiscal";
10
+ import { JsonLogicEvaluator } from "@run-iq/dsl-jsonlogic";
11
+ import { fiscalDescriptor } from "@run-iq/plugin-fiscal";
12
+
13
+ // src/descriptors/registry.ts
14
+ var DescriptorRegistry = class {
15
+ descriptors = [];
16
+ register(descriptor) {
17
+ this.descriptors.push(descriptor);
18
+ }
19
+ getAll() {
20
+ return this.descriptors;
21
+ }
22
+ getRuleExtensions() {
23
+ return this.descriptors.flatMap((d) => d.ruleExtensions);
24
+ }
25
+ getInputFields() {
26
+ const seen = /* @__PURE__ */ new Set();
27
+ const fields = [];
28
+ for (const d of this.descriptors) {
29
+ for (const f of d.inputFields) {
30
+ if (!seen.has(f.name)) {
31
+ seen.add(f.name);
32
+ fields.push(f);
33
+ }
34
+ }
35
+ }
36
+ return fields;
37
+ }
38
+ getExamples() {
39
+ return this.descriptors.flatMap((d) => d.examples);
40
+ }
41
+ isEmpty() {
42
+ return this.descriptors.length === 0;
43
+ }
44
+ };
45
+
46
+ // src/engine.ts
47
+ function createEngine(bundles2) {
48
+ const descriptorRegistry2 = new DescriptorRegistry();
49
+ const allPlugins = [];
50
+ const allDsls = [];
51
+ if (bundles2 && bundles2.length > 0) {
52
+ for (const bundle of bundles2) {
53
+ allPlugins.push(bundle.plugin);
54
+ descriptorRegistry2.register(bundle.descriptor);
55
+ if (bundle.dsls) {
56
+ allDsls.push(...bundle.dsls);
57
+ }
58
+ }
59
+ } else {
60
+ allPlugins.push(new FiscalPlugin());
61
+ allDsls.push(new JsonLogicEvaluator());
62
+ descriptorRegistry2.register(fiscalDescriptor);
63
+ }
64
+ const engine2 = new PPEEngine({
65
+ plugins: allPlugins,
66
+ dsls: allDsls,
67
+ dryRun: true,
68
+ strict: false,
69
+ onConflict: "first",
70
+ onChecksumMismatch: "skip"
71
+ });
72
+ const models2 = /* @__PURE__ */ new Map();
73
+ for (const plugin of allPlugins) {
74
+ const pluginWithModels = plugin;
75
+ if (Array.isArray(pluginWithModels.models)) {
76
+ for (const model of pluginWithModels.models) {
77
+ models2.set(model.name, model);
78
+ }
79
+ }
80
+ }
81
+ return { engine: engine2, models: models2, descriptorRegistry: descriptorRegistry2, plugins: allPlugins, dsls: allDsls };
82
+ }
83
+
84
+ // src/loader/plugin-loader.ts
85
+ import { readdir } from "fs/promises";
86
+ import { resolve } from "path";
87
+ async function loadPluginsFromDir(dir) {
88
+ const bundles2 = [];
89
+ const absoluteDir = resolve(dir);
90
+ const entries = await readdir(absoluteDir, { withFileTypes: true });
91
+ for (const entry of entries) {
92
+ if (!entry.isFile()) continue;
93
+ if (!entry.name.endsWith(".js") && !entry.name.endsWith(".mjs")) continue;
94
+ const filePath = resolve(absoluteDir, entry.name);
95
+ const mod = await import(filePath);
96
+ const bundle = mod["default"] ?? mod;
97
+ if (bundle["plugin"] && typeof bundle["plugin"] === "object" && bundle["descriptor"] && typeof bundle["descriptor"] === "object") {
98
+ bundles2.push(bundle);
99
+ }
100
+ }
101
+ return bundles2;
102
+ }
103
+
104
+ // src/tools/create-checksum.ts
105
+ import { createHash } from "crypto";
106
+ import { z } from "zod";
107
+ function registerCreateChecksumTool(server2) {
108
+ server2.tool(
109
+ "create_checksum",
110
+ "Compute the SHA-256 checksum of a params object. Used to generate the checksum field for Run-IQ rules.",
111
+ { params: z.record(z.unknown()).describe("The params object to hash") },
112
+ (args) => {
113
+ const checksum = createHash("sha256").update(JSON.stringify(args.params)).digest("hex");
114
+ return {
115
+ content: [{ type: "text", text: JSON.stringify({ checksum }, null, 2) }]
116
+ };
117
+ }
118
+ );
119
+ }
120
+
121
+ // src/tools/create-rule.ts
122
+ import { createHash as createHash2 } from "crypto";
123
+
124
+ // src/tools/schema-builder.ts
125
+ import { z as z2 } from "zod";
126
+ function buildFieldSchema(field) {
127
+ let schema;
128
+ if (field.enum && field.enum.length > 0) {
129
+ schema = z2.enum(field.enum);
130
+ } else {
131
+ switch (field.type) {
132
+ case "string":
133
+ schema = z2.string();
134
+ break;
135
+ case "number":
136
+ schema = z2.number();
137
+ break;
138
+ case "boolean":
139
+ schema = z2.boolean();
140
+ break;
141
+ }
142
+ }
143
+ if (!field.required) {
144
+ schema = schema.optional();
145
+ }
146
+ return schema.describe(field.description);
147
+ }
148
+ function buildCreateRuleSchema(descriptors2) {
149
+ const shape = {
150
+ id: z2.string().describe("Unique rule identifier"),
151
+ model: z2.string().describe("Calculation model name (e.g. FLAT_RATE, PROGRESSIVE_BRACKET)"),
152
+ params: z2.record(z2.unknown()).describe("Model-specific parameters"),
153
+ priority: z2.number().int().optional().describe("Rule priority (may be auto-computed by plugins, e.g. from jurisdiction+scope)"),
154
+ effectiveFrom: z2.string().describe("ISO 8601 date string for when the rule becomes active"),
155
+ effectiveUntil: z2.string().nullable().optional().describe("ISO 8601 date string for when the rule expires (null = no expiry)"),
156
+ tags: z2.array(z2.string()).optional().describe("Optional tags for filtering"),
157
+ condition: z2.object({
158
+ dsl: z2.string().describe('DSL identifier (e.g. "jsonlogic")'),
159
+ value: z2.unknown().describe("DSL-specific condition expression")
160
+ }).optional().describe("Optional condition expression")
161
+ };
162
+ for (const descriptor of descriptors2) {
163
+ for (const field of descriptor.ruleExtensions) {
164
+ shape[field.name] = buildFieldSchema(field);
165
+ }
166
+ }
167
+ return shape;
168
+ }
169
+ function buildValidateExtensionErrors(rule, descriptors2) {
170
+ const errors = [];
171
+ for (const descriptor of descriptors2) {
172
+ for (const field of descriptor.ruleExtensions) {
173
+ const value = rule[field.name];
174
+ if (field.required && (value === void 0 || value === null)) {
175
+ errors.push(`"${field.name}" is required by ${descriptor.name}: ${field.description}`);
176
+ continue;
177
+ }
178
+ if (value === void 0 || value === null) continue;
179
+ if (field.enum && !field.enum.includes(String(value))) {
180
+ errors.push(
181
+ `"${field.name}" must be one of: ${field.enum.join(", ")} (got "${String(value)}")`
182
+ );
183
+ }
184
+ if (field.type === "string" && typeof value !== "string") {
185
+ errors.push(`"${field.name}" must be a string`);
186
+ }
187
+ if (field.type === "number" && typeof value !== "number") {
188
+ errors.push(`"${field.name}" must be a number`);
189
+ }
190
+ if (field.type === "boolean" && typeof value !== "boolean") {
191
+ errors.push(`"${field.name}" must be a boolean`);
192
+ }
193
+ }
194
+ }
195
+ return errors;
196
+ }
197
+
198
+ // src/tools/create-rule.ts
199
+ function registerCreateRuleTool(server2, descriptors2) {
200
+ const schema = buildCreateRuleSchema(descriptors2);
201
+ const extensionFields = descriptors2.flatMap((d) => d.ruleExtensions.map((f) => f.name));
202
+ server2.tool(
203
+ "create_rule",
204
+ "Generate a valid Run-IQ Rule JSON object with auto-computed SHA-256 checksum. Includes plugin-specific fields based on loaded plugins.",
205
+ schema,
206
+ (args) => {
207
+ const params = args["params"];
208
+ const checksum = createHash2("sha256").update(JSON.stringify(params)).digest("hex");
209
+ const rule = {
210
+ id: args["id"],
211
+ version: 1,
212
+ model: args["model"],
213
+ params,
214
+ priority: args["priority"] ?? 1e3,
215
+ effectiveFrom: args["effectiveFrom"],
216
+ effectiveUntil: args["effectiveUntil"] ?? null,
217
+ tags: args["tags"] ?? [],
218
+ checksum
219
+ };
220
+ if (args["condition"]) {
221
+ rule["condition"] = args["condition"];
222
+ }
223
+ for (const field of extensionFields) {
224
+ if (args[field] !== void 0) {
225
+ rule[field] = args[field];
226
+ }
227
+ }
228
+ return {
229
+ content: [{ type: "text", text: JSON.stringify({ rule }, null, 2) }]
230
+ };
231
+ }
232
+ );
233
+ }
234
+
235
+ // src/tools/validate.ts
236
+ import { createHash as createHash3 } from "crypto";
237
+ import { z as z3 } from "zod";
238
+ var RuleSchema = z3.object({
239
+ id: z3.string(),
240
+ version: z3.number(),
241
+ model: z3.string(),
242
+ params: z3.unknown(),
243
+ priority: z3.number().optional(),
244
+ effectiveFrom: z3.string(),
245
+ effectiveUntil: z3.string().nullable(),
246
+ tags: z3.array(z3.string()),
247
+ checksum: z3.string(),
248
+ condition: z3.object({ dsl: z3.string(), value: z3.unknown() }).optional()
249
+ });
250
+ function registerValidateRulesTool(server2, models2, descriptors2) {
251
+ server2.tool(
252
+ "validate_rules",
253
+ "Validate the structure, checksum, model params, and plugin-specific fields of Run-IQ rules.",
254
+ {
255
+ rules: z3.array(z3.record(z3.unknown())).describe("Array of Rule JSON objects to validate")
256
+ },
257
+ (args) => {
258
+ const entries = [];
259
+ for (const raw of args.rules) {
260
+ const parsed = RuleSchema.safeParse(raw);
261
+ const ruleId = typeof raw["id"] === "string" ? raw["id"] : "unknown";
262
+ if (!parsed.success) {
263
+ entries.push({
264
+ ruleId,
265
+ valid: false,
266
+ errors: parsed.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`)
267
+ });
268
+ continue;
269
+ }
270
+ const rule = parsed.data;
271
+ const errors = [];
272
+ const computed = createHash3("sha256").update(JSON.stringify(rule.params)).digest("hex");
273
+ if (computed !== rule.checksum) {
274
+ errors.push(`Checksum mismatch: expected ${computed}, got ${rule.checksum}`);
275
+ }
276
+ const model = models2.get(rule.model);
277
+ if (!model) {
278
+ errors.push(
279
+ `Model "${rule.model}" not found. Available: ${[...models2.keys()].join(", ")}`
280
+ );
281
+ } else {
282
+ const validation = model.validateParams(rule.params);
283
+ if (!validation.valid && validation.errors) {
284
+ errors.push(...validation.errors);
285
+ }
286
+ }
287
+ const extensionErrors = buildValidateExtensionErrors(raw, descriptors2);
288
+ errors.push(...extensionErrors);
289
+ entries.push({
290
+ ruleId: rule.id,
291
+ valid: errors.length === 0,
292
+ ...errors.length > 0 ? { errors } : {}
293
+ });
294
+ }
295
+ return {
296
+ content: [{ type: "text", text: JSON.stringify({ entries }, null, 2) }]
297
+ };
298
+ }
299
+ );
300
+ }
301
+
302
+ // src/tools/list-models.ts
303
+ function registerListModelsTool(server2, models2) {
304
+ server2.tool(
305
+ "list_models",
306
+ "List all available calculation models with their parameter schemas. Shows model name, version, and expected parameters.",
307
+ {},
308
+ () => {
309
+ const result = [];
310
+ for (const [, model] of models2) {
311
+ const validation = model.validateParams({});
312
+ const paramsSchema = {};
313
+ if (validation.errors) {
314
+ for (const error of validation.errors) {
315
+ const match = error.match(/^"(\w+)"\s+must be (?:a |an )?(.+)$/);
316
+ if (match?.[1] && match[2]) {
317
+ paramsSchema[match[1]] = match[2];
318
+ }
319
+ }
320
+ }
321
+ result.push({
322
+ name: model.name,
323
+ version: model.version,
324
+ paramsSchema
325
+ });
326
+ }
327
+ return {
328
+ content: [{ type: "text", text: JSON.stringify({ models: result }, null, 2) }]
329
+ };
330
+ }
331
+ );
332
+ }
333
+
334
+ // src/tools/evaluate.ts
335
+ import { z as z4 } from "zod";
336
+ import { hydrateRules } from "@run-iq/core";
337
+ function registerEvaluateTool(server2, engine2) {
338
+ server2.tool(
339
+ "evaluate",
340
+ "Evaluate Run-IQ rules against input data in dry-run mode. Returns the computed value, breakdown per rule, applied/skipped rules, and execution trace.",
341
+ {
342
+ rules: z4.array(z4.record(z4.unknown())).describe("Array of Rule JSON objects"),
343
+ input: z4.object({
344
+ data: z4.record(z4.unknown()).describe("Input data for evaluation"),
345
+ requestId: z4.string().describe("Unique request identifier"),
346
+ meta: z4.object({
347
+ tenantId: z4.string().describe("Tenant identifier"),
348
+ userId: z4.string().optional().describe("User identifier"),
349
+ tags: z4.array(z4.string()).optional().describe("Tags for rule filtering"),
350
+ context: z4.record(z4.unknown()).optional().describe("Additional context"),
351
+ effectiveDate: z4.string().optional().describe("ISO 8601 date for effective date evaluation")
352
+ })
353
+ }).describe("Evaluation input")
354
+ },
355
+ async (args) => {
356
+ try {
357
+ const rules = hydrateRules(args.rules);
358
+ const input = {
359
+ data: args.input.data,
360
+ requestId: args.input.requestId,
361
+ meta: {
362
+ ...args.input.meta,
363
+ effectiveDate: args.input.meta.effectiveDate ? new Date(args.input.meta.effectiveDate) : void 0
364
+ }
365
+ };
366
+ const result = await engine2.evaluate(rules, input);
367
+ const serializable = {
368
+ value: result.value,
369
+ breakdown: result.breakdown,
370
+ appliedRules: result.appliedRules.map((r) => ({
371
+ id: r.id,
372
+ model: r.model,
373
+ priority: r.priority
374
+ })),
375
+ skippedRules: result.skippedRules.map((s) => ({
376
+ ruleId: s.rule.id,
377
+ reason: s.reason
378
+ })),
379
+ trace: {
380
+ steps: result.trace.steps.map((s) => ({
381
+ ruleId: s.ruleId,
382
+ modelUsed: s.modelUsed,
383
+ contribution: s.contribution,
384
+ durationMs: s.durationMs
385
+ })),
386
+ totalDurationMs: result.trace.totalDurationMs
387
+ }
388
+ };
389
+ return {
390
+ content: [{ type: "text", text: JSON.stringify(serializable, null, 2) }]
391
+ };
392
+ } catch (error) {
393
+ const message = error instanceof Error ? error.message : String(error);
394
+ return {
395
+ content: [{ type: "text", text: JSON.stringify({ error: message }) }],
396
+ isError: true
397
+ };
398
+ }
399
+ }
400
+ );
401
+ }
402
+
403
+ // src/tools/inspect-rule.ts
404
+ import { createHash as createHash4 } from "crypto";
405
+ import { z as z5 } from "zod";
406
+ function registerInspectRuleTool(server2, models2, descriptors2) {
407
+ server2.tool(
408
+ "inspect_rule",
409
+ "Analyze a single Run-IQ rule in detail: checksum, model, active dates, params, and plugin-specific fields.",
410
+ {
411
+ rule: z5.record(z5.unknown()).describe("A single Rule JSON object to inspect")
412
+ },
413
+ (args) => {
414
+ const rule = args.rule;
415
+ const id = typeof rule["id"] === "string" ? rule["id"] : "unknown";
416
+ const modelName = typeof rule["model"] === "string" ? rule["model"] : "";
417
+ const params = rule["params"];
418
+ const checksum = typeof rule["checksum"] === "string" ? rule["checksum"] : "";
419
+ const computed = createHash4("sha256").update(JSON.stringify(params)).digest("hex");
420
+ const checksumMatch = computed === checksum;
421
+ const model = models2.get(modelName);
422
+ const modelFound = model !== void 0;
423
+ let paramErrors;
424
+ if (model) {
425
+ const validation = model.validateParams(params);
426
+ if (!validation.valid && validation.errors) {
427
+ paramErrors = [...validation.errors];
428
+ }
429
+ }
430
+ const extensionErrors = buildValidateExtensionErrors(rule, descriptors2);
431
+ const now = /* @__PURE__ */ new Date();
432
+ const effectiveFrom = typeof rule["effectiveFrom"] === "string" ? new Date(rule["effectiveFrom"]) : null;
433
+ const effectiveUntil = typeof rule["effectiveUntil"] === "string" ? new Date(rule["effectiveUntil"]) : null;
434
+ const isActive = effectiveFrom !== null && effectiveFrom <= now && (effectiveUntil === null || effectiveUntil > now);
435
+ const effectivePeriod = {
436
+ from: effectiveFrom?.toISOString() ?? null,
437
+ until: effectiveUntil?.toISOString() ?? null
438
+ };
439
+ const allErrors = [...paramErrors ?? [], ...extensionErrors];
440
+ const valid = checksumMatch && modelFound && allErrors.length === 0 && isActive;
441
+ const result = {
442
+ ruleId: id,
443
+ valid,
444
+ checksumMatch,
445
+ modelFound,
446
+ isActive,
447
+ effectivePeriod
448
+ };
449
+ if (paramErrors && paramErrors.length > 0) {
450
+ result["paramErrors"] = paramErrors;
451
+ }
452
+ if (extensionErrors.length > 0) {
453
+ result["extensionErrors"] = extensionErrors;
454
+ }
455
+ return {
456
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
457
+ };
458
+ }
459
+ );
460
+ }
461
+
462
+ // src/tools/explain.ts
463
+ import { z as z6 } from "zod";
464
+ var EvaluationResultSchema = {
465
+ result: z6.object({
466
+ value: z6.unknown(),
467
+ breakdown: z6.array(
468
+ z6.object({
469
+ ruleId: z6.string(),
470
+ contribution: z6.unknown(),
471
+ modelUsed: z6.string(),
472
+ label: z6.string().optional()
473
+ })
474
+ ),
475
+ appliedRules: z6.array(
476
+ z6.object({
477
+ id: z6.string(),
478
+ model: z6.string(),
479
+ priority: z6.number()
480
+ })
481
+ ),
482
+ skippedRules: z6.array(
483
+ z6.object({
484
+ ruleId: z6.string(),
485
+ reason: z6.string()
486
+ })
487
+ ),
488
+ trace: z6.object({
489
+ totalDurationMs: z6.number(),
490
+ steps: z6.array(z6.record(z6.unknown())).optional()
491
+ }).optional()
492
+ }).describe("An EvaluationResult (or simplified result from the evaluate tool)")
493
+ };
494
+ function registerExplainResultTool(server2) {
495
+ server2.tool(
496
+ "explain_result",
497
+ "Generate a human-readable explanation of an evaluation result. Summarizes the total value, applied rules with their contributions, skipped rules with reasons, and execution timing.",
498
+ EvaluationResultSchema,
499
+ (args) => {
500
+ const { result } = args;
501
+ const lines = [];
502
+ lines.push(`## Evaluation Result Summary`);
503
+ lines.push("");
504
+ lines.push(`**Total Value**: ${String(result.value)}`);
505
+ lines.push("");
506
+ if (result.appliedRules.length > 0) {
507
+ lines.push(`### Applied Rules (${result.appliedRules.length})`);
508
+ lines.push("");
509
+ for (const applied of result.appliedRules) {
510
+ const breakdown = result.breakdown.find((b) => b.ruleId === applied.id);
511
+ const contribution = breakdown ? String(breakdown.contribution) : "N/A";
512
+ const label = breakdown?.label ? ` (${breakdown.label})` : "";
513
+ lines.push(
514
+ `- **${applied.id}**${label}: model=${applied.model}, priority=${applied.priority}, contribution=${contribution}`
515
+ );
516
+ }
517
+ lines.push("");
518
+ } else {
519
+ lines.push("### No rules were applied.");
520
+ lines.push("");
521
+ }
522
+ if (result.skippedRules.length > 0) {
523
+ lines.push(`### Skipped Rules (${result.skippedRules.length})`);
524
+ lines.push("");
525
+ for (const skipped of result.skippedRules) {
526
+ lines.push(`- **${skipped.ruleId}**: ${skipped.reason}`);
527
+ }
528
+ lines.push("");
529
+ }
530
+ if (result.trace?.totalDurationMs !== void 0) {
531
+ lines.push(`### Timing`);
532
+ lines.push("");
533
+ lines.push(`Total duration: ${result.trace.totalDurationMs}ms`);
534
+ }
535
+ const explanation = lines.join("\n");
536
+ return {
537
+ content: [{ type: "text", text: JSON.stringify({ explanation }, null, 2) }]
538
+ };
539
+ }
540
+ );
541
+ }
542
+
543
+ // src/tools/simulate.ts
544
+ import { z as z7 } from "zod";
545
+ import { hydrateRules as hydrateRules2 } from "@run-iq/core";
546
+ var ScenarioSchema = z7.object({
547
+ label: z7.string().describe("Human-readable scenario label"),
548
+ input: z7.object({
549
+ data: z7.record(z7.unknown()),
550
+ requestId: z7.string(),
551
+ meta: z7.object({
552
+ tenantId: z7.string(),
553
+ userId: z7.string().optional(),
554
+ tags: z7.array(z7.string()).optional(),
555
+ context: z7.record(z7.unknown()).optional(),
556
+ effectiveDate: z7.string().optional()
557
+ })
558
+ })
559
+ });
560
+ function registerSimulateTool(server2, engine2) {
561
+ server2.tool(
562
+ "simulate",
563
+ "Compare N scenarios using the same rules. Evaluates each scenario independently and returns side-by-side results for comparison.",
564
+ {
565
+ rules: z7.array(z7.record(z7.unknown())).describe("Array of Rule JSON objects (shared across all scenarios)"),
566
+ scenarios: z7.array(ScenarioSchema).min(1).describe("Array of scenarios to compare")
567
+ },
568
+ async (args) => {
569
+ try {
570
+ const rules = hydrateRules2(args.rules);
571
+ const results = [];
572
+ for (const scenario of args.scenarios) {
573
+ const input = {
574
+ data: scenario.input.data,
575
+ requestId: scenario.input.requestId,
576
+ meta: {
577
+ ...scenario.input.meta,
578
+ effectiveDate: scenario.input.meta.effectiveDate ? new Date(scenario.input.meta.effectiveDate) : void 0
579
+ }
580
+ };
581
+ const result = await engine2.evaluate(rules, input);
582
+ results.push({
583
+ label: scenario.label,
584
+ value: result.value,
585
+ appliedCount: result.appliedRules.length,
586
+ skippedCount: result.skippedRules.length,
587
+ breakdown: result.breakdown.map((b) => ({
588
+ ruleId: b.ruleId,
589
+ contribution: b.contribution,
590
+ modelUsed: b.modelUsed
591
+ }))
592
+ });
593
+ }
594
+ return {
595
+ content: [{ type: "text", text: JSON.stringify({ results }, null, 2) }]
596
+ };
597
+ } catch (error) {
598
+ const message = error instanceof Error ? error.message : String(error);
599
+ return {
600
+ content: [{ type: "text", text: JSON.stringify({ error: message }) }],
601
+ isError: true
602
+ };
603
+ }
604
+ }
605
+ );
606
+ }
607
+
608
+ // src/resources/models.ts
609
+ function buildModelsCatalog(models2) {
610
+ const lines = [];
611
+ lines.push("# Run-IQ Calculation Models");
612
+ lines.push("");
613
+ for (const [, model] of models2) {
614
+ lines.push(`## ${model.name} (v${model.version})`);
615
+ lines.push("");
616
+ const validation = model.validateParams({});
617
+ if (validation.errors) {
618
+ lines.push("### Parameters");
619
+ lines.push("");
620
+ for (const error of validation.errors) {
621
+ lines.push(`- ${error}`);
622
+ }
623
+ lines.push("");
624
+ }
625
+ switch (model.name) {
626
+ case "FLAT_RATE":
627
+ lines.push("Applies a flat rate to a base value: `base_value * rate`.");
628
+ lines.push("");
629
+ lines.push('**Example params**: `{ "rate": 0.18, "base": "revenue" }`');
630
+ lines.push("**Use case**: TVA, flat tax rates.");
631
+ break;
632
+ case "PROGRESSIVE_BRACKET":
633
+ lines.push(
634
+ "Applies progressive tax brackets cumulatively. Each bracket taxes only the portion within its range."
635
+ );
636
+ lines.push("");
637
+ lines.push(
638
+ '**Example params**: `{ "base": "income", "brackets": [{ "from": 0, "to": 500000, "rate": 0 }, { "from": 500000, "to": 1000000, "rate": 0.1 }] }`'
639
+ );
640
+ lines.push("**Use case**: IRPP (income tax), progressive taxes.");
641
+ break;
642
+ case "MINIMUM_TAX":
643
+ lines.push("Computes `MAX(base_value * rate, minimum)`. Ensures a minimum tax amount.");
644
+ lines.push("");
645
+ lines.push('**Example params**: `{ "rate": 0.01, "base": "revenue", "minimum": 50000 }`');
646
+ lines.push("**Use case**: Minimum corporate tax.");
647
+ break;
648
+ case "THRESHOLD_BASED":
649
+ lines.push(
650
+ "Applies a rate only when the base value exceeds a threshold. If `above_only` is true, taxes only the amount above the threshold."
651
+ );
652
+ lines.push("");
653
+ lines.push(
654
+ '**Example params**: `{ "base": "revenue", "threshold": 1000000, "rate": 0.05, "above_only": true }`'
655
+ );
656
+ lines.push("**Use case**: Luxury tax, excess profit tax.");
657
+ break;
658
+ case "FIXED_AMOUNT":
659
+ lines.push("Returns a fixed amount regardless of input values.");
660
+ lines.push("");
661
+ lines.push('**Example params**: `{ "amount": 25000, "currency": "XOF" }`');
662
+ lines.push("**Use case**: Fixed registration fees, stamps.");
663
+ break;
664
+ case "COMPOSITE":
665
+ lines.push(
666
+ "Combines multiple sub-models using an aggregation function (SUM, MAX, or MIN)."
667
+ );
668
+ lines.push("");
669
+ lines.push(
670
+ '**Example params**: `{ "steps": [{ "model": "FLAT_RATE", "params": { "rate": 0.18, "base": "revenue" } }], "aggregation": "SUM" }`'
671
+ );
672
+ lines.push("**Use case**: Complex tax calculations combining multiple methods.");
673
+ break;
674
+ }
675
+ lines.push("");
676
+ lines.push("---");
677
+ lines.push("");
678
+ }
679
+ return lines.join("\n");
680
+ }
681
+ function registerModelsResource(server2, models2) {
682
+ server2.resource(
683
+ "models-catalog",
684
+ "models://catalog",
685
+ {
686
+ description: "Documentation of all available calculation models with parameter schemas and usage examples"
687
+ },
688
+ () => {
689
+ const catalog = buildModelsCatalog(models2);
690
+ return {
691
+ contents: [
692
+ {
693
+ uri: "models://catalog",
694
+ mimeType: "text/markdown",
695
+ text: catalog
696
+ }
697
+ ]
698
+ };
699
+ }
700
+ );
701
+ }
702
+
703
+ // src/resources/plugins.ts
704
+ function registerPluginsResource(server2, plugins2, dsls2, registry) {
705
+ server2.resource(
706
+ "plugins-loaded",
707
+ "plugins://loaded",
708
+ { description: "Information about loaded plugins, DSL evaluators, and their descriptors" },
709
+ () => {
710
+ const descriptors2 = registry.getAll();
711
+ const info = {
712
+ plugins: plugins2.map((p) => {
713
+ const desc = descriptors2.find((d) => d.name === p.name);
714
+ const pluginWithModels = p;
715
+ return {
716
+ name: p.name,
717
+ version: p.version,
718
+ models: Array.isArray(pluginWithModels.models) ? pluginWithModels.models.map((m) => m.name) : [],
719
+ hasDescriptor: desc !== void 0,
720
+ ruleExtensions: desc?.ruleExtensions.map((f) => f.name) ?? []
721
+ };
722
+ }),
723
+ dsls: dsls2.map((d) => ({
724
+ name: d.dsl,
725
+ version: d.version
726
+ }))
727
+ };
728
+ return {
729
+ contents: [
730
+ {
731
+ uri: "plugins://loaded",
732
+ mimeType: "application/json",
733
+ text: JSON.stringify(info, null, 2)
734
+ }
735
+ ]
736
+ };
737
+ }
738
+ );
739
+ }
740
+
741
+ // src/resources/schema.ts
742
+ function buildSchemaDocument(models2, registry) {
743
+ const lines = [];
744
+ lines.push("# Run-IQ Rule Schema");
745
+ lines.push("");
746
+ lines.push("Complete schema for creating valid rules with the currently loaded plugins.");
747
+ lines.push("");
748
+ lines.push("## Base Rule Fields (always required)");
749
+ lines.push("");
750
+ lines.push("| Field | Type | Required | Description |");
751
+ lines.push("|-------|------|----------|-------------|");
752
+ lines.push("| id | string | yes | Unique rule identifier |");
753
+ lines.push("| version | number | auto (=1) | Rule version |");
754
+ lines.push("| model | string | yes | Calculation model name |");
755
+ lines.push("| params | object | yes | Model-specific parameters |");
756
+ lines.push(
757
+ "| priority | number | optional | Auto-computed by plugins (e.g. jurisdiction+scope) |"
758
+ );
759
+ lines.push("| effectiveFrom | string | yes | ISO 8601 date |");
760
+ lines.push("| effectiveUntil | string\\|null | optional | ISO 8601 date or null |");
761
+ lines.push("| tags | string[] | optional | Filtering tags |");
762
+ lines.push("| checksum | string | auto | SHA-256 of params (auto-computed by create_rule) |");
763
+ lines.push("| condition | {dsl,value} | optional | DSL condition (e.g. jsonlogic) |");
764
+ lines.push("");
765
+ const descriptors2 = registry.getAll();
766
+ if (descriptors2.length > 0) {
767
+ lines.push("## Plugin Extension Fields");
768
+ lines.push("");
769
+ for (const desc of descriptors2) {
770
+ lines.push(`### ${desc.name} (v${desc.version})`);
771
+ lines.push("");
772
+ lines.push(desc.description);
773
+ lines.push("");
774
+ lines.push("| Field | Type | Required | Values | Description |");
775
+ lines.push("|-------|------|----------|--------|-------------|");
776
+ for (const field of desc.ruleExtensions) {
777
+ const values = field.enum ? field.enum.join(", ") : "-";
778
+ lines.push(
779
+ `| ${field.name} | ${field.type} | ${field.required ? "yes" : "no"} | ${values} | ${field.description} |`
780
+ );
781
+ }
782
+ lines.push("");
783
+ }
784
+ }
785
+ lines.push("## Available Calculation Models");
786
+ lines.push("");
787
+ for (const [, model] of models2) {
788
+ lines.push(`### ${model.name} (v${model.version})`);
789
+ lines.push("");
790
+ const validation = model.validateParams({});
791
+ if (validation.errors) {
792
+ lines.push("**Required params:**");
793
+ for (const error of validation.errors) {
794
+ lines.push(`- ${error}`);
795
+ }
796
+ }
797
+ lines.push("");
798
+ }
799
+ const inputFields = registry.getInputFields();
800
+ if (inputFields.length > 0) {
801
+ lines.push("## Input Data Fields (input.data)");
802
+ lines.push("");
803
+ lines.push('Variables available for use in JSONLogic conditions (`{"var": "fieldName"}`)');
804
+ lines.push("and as `base` parameter in calculation models.");
805
+ lines.push("");
806
+ lines.push("| Field | Type | Description | Examples |");
807
+ lines.push("|-------|------|-------------|----------|");
808
+ for (const field of inputFields) {
809
+ const examples2 = field.examples ? field.examples.join(", ") : "-";
810
+ lines.push(`| ${field.name} | ${field.type} | ${field.description} | ${examples2} |`);
811
+ }
812
+ lines.push("");
813
+ }
814
+ lines.push("## JSONLogic Condition Syntax");
815
+ lines.push("");
816
+ lines.push("Conditions use the `jsonlogic` DSL. Format:");
817
+ lines.push("```json");
818
+ lines.push('{ "dsl": "jsonlogic", "value": <expression> }');
819
+ lines.push("```");
820
+ lines.push("");
821
+ lines.push("**Common operators:**");
822
+ lines.push('- `{">=": [{"var": "revenue"}, 1000000]}` - comparison');
823
+ lines.push('- `{"and": [expr1, expr2]}` - logical AND');
824
+ lines.push('- `{"or": [expr1, expr2]}` - logical OR');
825
+ lines.push('- `{"!": expr}` - logical NOT');
826
+ lines.push('- `{"in": ["value", {"var": "tags"}]}` - membership');
827
+ lines.push('- `{"==": [{"var": "type"}, "enterprise"]}` - equality');
828
+ lines.push("");
829
+ const examples = registry.getExamples();
830
+ if (examples.length > 0) {
831
+ lines.push("## Complete Rule Examples");
832
+ lines.push("");
833
+ for (const example of examples) {
834
+ lines.push(`### ${example.title}`);
835
+ lines.push("");
836
+ lines.push(example.description);
837
+ lines.push("");
838
+ lines.push("**Rule:**");
839
+ lines.push("```json");
840
+ lines.push(JSON.stringify(example.rule, null, 2));
841
+ lines.push("```");
842
+ if (example.input) {
843
+ lines.push("");
844
+ lines.push("**Input data:**");
845
+ lines.push("```json");
846
+ lines.push(JSON.stringify(example.input, null, 2));
847
+ lines.push("```");
848
+ }
849
+ lines.push("");
850
+ }
851
+ }
852
+ return lines.join("\n");
853
+ }
854
+ function registerSchemaResource(server2, models2, registry) {
855
+ server2.resource(
856
+ "rule-schema",
857
+ "schema://rules",
858
+ {
859
+ description: "Complete rule schema including base fields, plugin extensions, model params, input variables, DSL syntax, and examples. THE reference for creating valid rules."
860
+ },
861
+ () => ({
862
+ contents: [
863
+ {
864
+ uri: "schema://rules",
865
+ mimeType: "text/markdown",
866
+ text: buildSchemaDocument(models2, registry)
867
+ }
868
+ ]
869
+ })
870
+ );
871
+ }
872
+
873
+ // src/prompts/analyze-text.ts
874
+ import { z as z8 } from "zod";
875
+ function buildDomainLabel(registry) {
876
+ const descriptors2 = registry.getAll();
877
+ if (descriptors2.length === 0) return "policy";
878
+ return descriptors2.map((d) => d.domainLabel).join(" / ");
879
+ }
880
+ function buildModelDocs(models2) {
881
+ const lines = [];
882
+ for (const [, model] of models2) {
883
+ const validation = model.validateParams({});
884
+ const paramHints = validation.errors ? validation.errors.join("; ") : "none";
885
+ lines.push(`- **${model.name}** (v${model.version}): ${paramHints}`);
886
+ }
887
+ return lines.join("\n");
888
+ }
889
+ function buildExtensionDocs(registry) {
890
+ const descriptors2 = registry.getAll();
891
+ if (descriptors2.length === 0) return "";
892
+ const lines = [];
893
+ for (const desc of descriptors2) {
894
+ lines.push(`
895
+ ### Plugin: ${desc.name}`);
896
+ lines.push(desc.description);
897
+ lines.push("\n**Required fields on each rule:**");
898
+ for (const field of desc.ruleExtensions) {
899
+ const values = field.enum ? ` (values: ${field.enum.join(", ")})` : "";
900
+ const req = field.required ? " [REQUIRED]" : " [optional]";
901
+ lines.push(`- \`${field.name}\`${req}: ${field.description}${values}`);
902
+ }
903
+ }
904
+ return lines.join("\n");
905
+ }
906
+ function buildExampleDocs(registry) {
907
+ const examples = registry.getExamples();
908
+ if (examples.length === 0) return "";
909
+ const lines = ["\n## Concrete Examples"];
910
+ for (const ex of examples) {
911
+ lines.push(`
912
+ ### ${ex.title}`);
913
+ lines.push(ex.description);
914
+ lines.push("```json");
915
+ lines.push(JSON.stringify(ex.rule, null, 2));
916
+ lines.push("```");
917
+ }
918
+ return lines.join("\n");
919
+ }
920
+ function buildInputDocs(registry) {
921
+ const fields = registry.getInputFields();
922
+ if (fields.length === 0) return "";
923
+ const lines = ["\n## Available input.data Fields"];
924
+ lines.push('Use these as `{"var": "fieldName"}` in JSONLogic conditions');
925
+ lines.push('and as `"base"` parameter in calculation models:\n');
926
+ for (const f of fields) {
927
+ const examples = f.examples ? ` (e.g. ${f.examples.join(", ")})` : "";
928
+ lines.push(`- \`${f.name}\` (${f.type}): ${f.description}${examples}`);
929
+ }
930
+ return lines.join("\n");
931
+ }
932
+ function buildGuidelinesSection(registry) {
933
+ const descriptors2 = registry.getAll();
934
+ const lines = [];
935
+ for (const desc of descriptors2) {
936
+ if (desc.promptGuidelines.length > 0) {
937
+ lines.push(`
938
+ ## ${desc.domainLabel} Guidelines`);
939
+ for (const g of desc.promptGuidelines) {
940
+ lines.push(`- ${g}`);
941
+ }
942
+ }
943
+ }
944
+ return lines.join("\n");
945
+ }
946
+ function registerAnalyzeTextPrompt(server2, models2, registry) {
947
+ const domainLabel = buildDomainLabel(registry);
948
+ server2.prompt(
949
+ "analyze-text",
950
+ `Translate a ${domainLabel} text (law, regulation, policy) into Run-IQ rule definitions. Plugin-aware: includes all required fields.`,
951
+ {
952
+ source_text: z8.string().describe("The regulatory, legal, or policy text to analyze and convert into rules"),
953
+ country: z8.string().optional().describe("ISO country code (e.g. TG, FR, US, IN)")
954
+ },
955
+ (args) => {
956
+ return {
957
+ messages: [
958
+ {
959
+ role: "user",
960
+ content: {
961
+ type: "text",
962
+ text: `You are an expert at translating regulatory and policy texts into structured Run-IQ rules.
963
+
964
+ ## Available Calculation Models
965
+ ${buildModelDocs(models2)}
966
+ ${buildExtensionDocs(registry)}
967
+ ${buildInputDocs(registry)}
968
+ ${buildGuidelinesSection(registry)}
969
+
970
+ ## Rule Creation Workflow
971
+ 1. Read the source text carefully
972
+ 2. Identify each rule, rate, threshold, bracket, condition, or exemption
973
+ 3. Map each identified element to the appropriate calculation model
974
+ 4. Use the \`create_rule\` tool \u2014 it knows ALL required fields for loaded plugins
975
+ 5. Use the \`validate_rules\` tool to verify your rules are valid
976
+ 6. Read \`schema://rules\` for the complete field reference if needed
977
+
978
+ ## JSONLogic Conditions
979
+ \`\`\`json
980
+ { "dsl": "jsonlogic", "value": { ">=": [{ "var": "revenue" }, 100000] } }
981
+ \`\`\`
982
+ ${buildExampleDocs(registry)}
983
+ ${args.country ? `
984
+ ## Target Country: ${args.country}
985
+ ` : ""}
986
+ ## Source Text to Analyze
987
+ ${args.source_text}`
988
+ }
989
+ }
990
+ ]
991
+ };
992
+ }
993
+ );
994
+ }
995
+
996
+ // src/prompts/domain-expert.ts
997
+ import { z as z9 } from "zod";
998
+ function buildGuidelinesSection2(registry) {
999
+ const descriptors2 = registry.getAll();
1000
+ const allGuidelines = [];
1001
+ for (const desc of descriptors2) {
1002
+ if (desc.promptGuidelines.length > 0) {
1003
+ allGuidelines.push(`### ${desc.name} (${desc.domainLabel})`);
1004
+ for (const g of desc.promptGuidelines) {
1005
+ allGuidelines.push(`- ${g}`);
1006
+ }
1007
+ }
1008
+ }
1009
+ if (allGuidelines.length === 0) return "";
1010
+ return `## Domain-Specific Guidelines
1011
+ ${allGuidelines.join("\n")}`;
1012
+ }
1013
+ function buildDomainLabel2(registry) {
1014
+ const descriptors2 = registry.getAll();
1015
+ if (descriptors2.length === 0) return "general-purpose";
1016
+ return descriptors2.map((d) => d.domainLabel).join(" + ");
1017
+ }
1018
+ function registerDomainExpertPrompt(server2, registry) {
1019
+ const descriptors2 = registry.getAll();
1020
+ const domainLabel = buildDomainLabel2(registry);
1021
+ const pluginList = descriptors2.map((d) => `- ${d.name}: ${d.description}`).join("\n");
1022
+ server2.prompt(
1023
+ "domain-expert",
1024
+ `${domainLabel} calculation expertise: scenario comparison, result explanation, recommendations`,
1025
+ {
1026
+ question: z9.string().describe("The question or scenario to analyze")
1027
+ },
1028
+ (args) => {
1029
+ return {
1030
+ messages: [
1031
+ {
1032
+ role: "user",
1033
+ content: {
1034
+ type: "text",
1035
+ text: `You are a ${domainLabel} expert assistant powered by the Run-IQ PPE (Parametric Policy Engine).
1036
+
1037
+ ## Loaded Plugins
1038
+ ${pluginList || "No plugins loaded."}
1039
+
1040
+ ## Your Tools
1041
+ - **evaluate**: evaluate rules against input data (always dry-run)
1042
+ - **simulate**: compare N scenarios side-by-side
1043
+ - **validate_rules**: verify rule structure, checksum, and plugin-specific fields
1044
+ - **explain_result**: human-readable result explanation
1045
+ - **create_rule**: generate rules with ALL required plugin fields
1046
+ - **inspect_rule**: analyze a single rule in detail
1047
+ - **list_models**: show available calculation models
1048
+ - **create_checksum**: compute SHA-256 for params
1049
+
1050
+ ## Key Resources
1051
+ - \`schema://rules\` \u2014 THE complete rule schema reference
1052
+ - \`models://catalog\` \u2014 model documentation with examples
1053
+ - \`plugins://loaded\` \u2014 loaded plugins and their capabilities
1054
+
1055
+ ## General Guidelines
1056
+ 1. Always read \`schema://rules\` before creating rules \u2014 it has ALL required fields
1057
+ 2. When comparing scenarios, use the \`simulate\` tool with clear labels
1058
+ 3. Always validate rules after creating them
1059
+
1060
+ ${buildGuidelinesSection2(registry)}
1061
+
1062
+ ## Question
1063
+ ${args.question}`
1064
+ }
1065
+ }
1066
+ ]
1067
+ };
1068
+ }
1069
+ );
1070
+ }
1071
+
1072
+ // src/index.ts
1073
+ var pluginsDir;
1074
+ var argv = process.argv.slice(2);
1075
+ for (let i = 0; i < argv.length; i++) {
1076
+ if (argv[i] === "--plugins-dir" && argv[i + 1]) {
1077
+ pluginsDir = argv[i + 1];
1078
+ i++;
1079
+ }
1080
+ }
1081
+ var bundles;
1082
+ if (pluginsDir) {
1083
+ bundles = await loadPluginsFromDir(pluginsDir);
1084
+ }
1085
+ var { engine, models, descriptorRegistry, plugins, dsls } = createEngine(bundles);
1086
+ var descriptors = descriptorRegistry.getAll();
1087
+ var server = new McpServer(
1088
+ {
1089
+ name: "@run-iq/mcp-server",
1090
+ version: "0.1.0"
1091
+ },
1092
+ {
1093
+ capabilities: {
1094
+ tools: {},
1095
+ resources: {},
1096
+ prompts: {}
1097
+ }
1098
+ }
1099
+ );
1100
+ registerCreateChecksumTool(server);
1101
+ registerCreateRuleTool(server, descriptors);
1102
+ registerValidateRulesTool(server, models, descriptors);
1103
+ registerListModelsTool(server, models);
1104
+ registerEvaluateTool(server, engine);
1105
+ registerInspectRuleTool(server, models, descriptors);
1106
+ registerExplainResultTool(server);
1107
+ registerSimulateTool(server, engine);
1108
+ registerModelsResource(server, models);
1109
+ registerPluginsResource(server, plugins, dsls, descriptorRegistry);
1110
+ registerSchemaResource(server, models, descriptorRegistry);
1111
+ registerAnalyzeTextPrompt(server, models, descriptorRegistry);
1112
+ registerDomainExpertPrompt(server, descriptorRegistry);
1113
+ var transport = new StdioServerTransport();
1114
+ await server.connect(transport);
1115
+ //# sourceMappingURL=index.js.map