@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/LICENSE +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1115 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
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
|