@criterionx/mcp 0.3.3

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Tomas Maritano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # @criterionx/mcp
2
+
3
+ MCP (Model Context Protocol) server for Criterion decisions. Exposes business rules as MCP tools for use with LLM applications like Claude Desktop.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @criterionx/mcp @criterionx/core zod
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { createMcpServer } from "@criterionx/mcp";
15
+ import { defineDecision } from "@criterionx/core";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import { z } from "zod";
18
+
19
+ // Define a decision
20
+ const pricingDecision = defineDecision({
21
+ id: "pricing-tier",
22
+ version: "1.0.0",
23
+ inputSchema: z.object({ revenue: z.number() }),
24
+ outputSchema: z.object({ tier: z.string(), discount: z.number() }),
25
+ profileSchema: z.object({
26
+ tiers: z.array(z.object({ min: z.number(), name: z.string(), discount: z.number() }))
27
+ }),
28
+ rules: [
29
+ {
30
+ id: "enterprise",
31
+ when: (ctx, profile) => ctx.revenue >= profile.tiers[2].min,
32
+ emit: (ctx, profile) => ({ tier: profile.tiers[2].name, discount: profile.tiers[2].discount }),
33
+ explain: () => "Revenue qualifies for enterprise tier",
34
+ },
35
+ {
36
+ id: "growth",
37
+ when: (ctx, profile) => ctx.revenue >= profile.tiers[1].min,
38
+ emit: (ctx, profile) => ({ tier: profile.tiers[1].name, discount: profile.tiers[1].discount }),
39
+ explain: () => "Revenue qualifies for growth tier",
40
+ },
41
+ {
42
+ id: "starter",
43
+ when: () => true,
44
+ emit: (ctx, profile) => ({ tier: profile.tiers[0].name, discount: profile.tiers[0].discount }),
45
+ explain: () => "Default starter tier",
46
+ },
47
+ ],
48
+ });
49
+
50
+ // Create MCP server
51
+ const mcpServer = createMcpServer({
52
+ name: "my-decisions",
53
+ decisions: [pricingDecision],
54
+ profiles: {
55
+ "pricing-tier": {
56
+ tiers: [
57
+ { min: 0, name: "Starter", discount: 0 },
58
+ { min: 100000, name: "Growth", discount: 10 },
59
+ { min: 1000000, name: "Enterprise", discount: 25 },
60
+ ]
61
+ }
62
+ }
63
+ });
64
+
65
+ // Connect via stdio transport
66
+ const transport = new StdioServerTransport();
67
+ await mcpServer.server.connect(transport);
68
+ ```
69
+
70
+ ## MCP Tools
71
+
72
+ The server exposes four MCP tools:
73
+
74
+ ### `list_decisions`
75
+
76
+ List all registered decisions with their metadata.
77
+
78
+ **Input:** None
79
+
80
+ **Output:**
81
+ ```json
82
+ {
83
+ "decisions": [
84
+ {
85
+ "id": "pricing-tier",
86
+ "version": "1.0.0",
87
+ "description": "Determine pricing tier based on revenue",
88
+ "rulesCount": 3
89
+ }
90
+ ]
91
+ }
92
+ ```
93
+
94
+ ### `get_decision_schema`
95
+
96
+ Get the JSON schemas for a specific decision.
97
+
98
+ **Input:**
99
+ - `decisionId` (string): The ID of the decision
100
+
101
+ **Output:**
102
+ ```json
103
+ {
104
+ "id": "pricing-tier",
105
+ "version": "1.0.0",
106
+ "inputSchema": { "type": "object", "properties": { "revenue": { "type": "number" } } },
107
+ "outputSchema": { "type": "object", "properties": { "tier": { "type": "string" } } },
108
+ "profileSchema": { ... }
109
+ }
110
+ ```
111
+
112
+ ### `evaluate_decision`
113
+
114
+ Evaluate a decision with the given input and optional profile.
115
+
116
+ **Input:**
117
+ - `decisionId` (string): The ID of the decision to evaluate
118
+ - `input` (object): Input data matching the decision's input schema
119
+ - `profile` (object, optional): Profile to use (overrides default)
120
+
121
+ **Output:**
122
+ ```json
123
+ {
124
+ "status": "OK",
125
+ "data": { "tier": "Growth", "discount": 10 },
126
+ "meta": {
127
+ "decisionId": "pricing-tier",
128
+ "decisionVersion": "1.0.0",
129
+ "matchedRule": "growth",
130
+ "explanation": "Revenue qualifies for growth tier",
131
+ "evaluatedRules": [
132
+ { "ruleId": "enterprise", "matched": false },
133
+ { "ruleId": "growth", "matched": true, "explanation": "Revenue qualifies for growth tier" }
134
+ ],
135
+ "evaluatedAt": "2024-12-29T22:00:00.000Z"
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### `explain_result`
141
+
142
+ Get a human-readable explanation of a decision result.
143
+
144
+ **Input:**
145
+ - `result` (object): The evaluation result to explain
146
+
147
+ **Output:**
148
+ ```
149
+ Decision: pricing-tier v1.0.0
150
+ Status: OK
151
+ Matched: growth
152
+ Reason: Revenue qualifies for growth tier
153
+
154
+ Evaluation trace:
155
+ ✗ enterprise
156
+ ✓ growth
157
+ ```
158
+
159
+ ## Claude Desktop Configuration
160
+
161
+ Add to `~/.claude/config.json`:
162
+
163
+ ```json
164
+ {
165
+ "mcpServers": {
166
+ "criterion": {
167
+ "command": "node",
168
+ "args": ["/path/to/your/criterion-mcp-server.js"]
169
+ }
170
+ }
171
+ }
172
+ ```
173
+
174
+ ## API Reference
175
+
176
+ ### `createMcpServer(options)`
177
+
178
+ Create a new Criterion MCP server.
179
+
180
+ **Options:**
181
+ - `name` (string, optional): Server name exposed to MCP clients
182
+ - `version` (string, optional): Server version
183
+ - `decisions` (Decision[]): Decisions to expose as MCP tools
184
+ - `profiles` (Record<string, unknown>, optional): Default profiles keyed by decision ID
185
+
186
+ **Returns:** `CriterionMcpServer`
187
+
188
+ ### `CriterionMcpServer`
189
+
190
+ The MCP server class.
191
+
192
+ **Properties:**
193
+ - `server`: The underlying MCP server instance (for transport connection)
194
+ - `decisionRegistry`: Map of registered decisions
195
+ - `profileRegistry`: Map of registered profiles
196
+
197
+ ## License
198
+
199
+ MIT
@@ -0,0 +1,100 @@
1
+ import { Decision } from '@criterionx/core';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+
4
+ /**
5
+ * MCP Server configuration options
6
+ */
7
+ interface McpServerOptions {
8
+ /** Server name exposed to MCP clients */
9
+ name?: string;
10
+ /** Server version */
11
+ version?: string;
12
+ /** Decisions to expose as MCP tools */
13
+ decisions: Decision<any, any, any>[];
14
+ /** Default profiles for decisions (keyed by decision ID) */
15
+ profiles?: Record<string, unknown>;
16
+ }
17
+ /**
18
+ * Decision info returned by list_decisions tool
19
+ */
20
+ interface DecisionListItem {
21
+ id: string;
22
+ version: string;
23
+ description?: string;
24
+ rulesCount: number;
25
+ meta?: Record<string, unknown>;
26
+ }
27
+ /**
28
+ * Schema response for get_decision_schema tool
29
+ */
30
+ interface DecisionSchemaResponse {
31
+ id: string;
32
+ version: string;
33
+ inputSchema: Record<string, unknown>;
34
+ outputSchema: Record<string, unknown>;
35
+ profileSchema: Record<string, unknown>;
36
+ }
37
+ /**
38
+ * Error codes for MCP tool responses
39
+ */
40
+ type McpErrorCode = "DECISION_NOT_FOUND" | "MISSING_PROFILE" | "EVALUATION_ERROR" | "EXPLAIN_ERROR";
41
+
42
+ /**
43
+ * Criterion MCP Server
44
+ *
45
+ * Exposes Criterion decisions as MCP tools for use with LLM applications.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * import { createMcpServer } from "@criterionx/mcp";
50
+ * import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
51
+ *
52
+ * const mcpServer = createMcpServer({
53
+ * decisions: [myDecision],
54
+ * profiles: { "my-decision": { threshold: 100 } },
55
+ * });
56
+ *
57
+ * const transport = new StdioServerTransport();
58
+ * await mcpServer.server.connect(transport);
59
+ * ```
60
+ */
61
+ declare class CriterionMcpServer {
62
+ private mcpServer;
63
+ private engine;
64
+ private decisions;
65
+ private profiles;
66
+ constructor(options: McpServerOptions);
67
+ private registerTools;
68
+ /**
69
+ * Get the underlying MCP server instance
70
+ */
71
+ get server(): McpServer;
72
+ /**
73
+ * Get the decision registry (for testing/introspection)
74
+ */
75
+ get decisionRegistry(): Map<string, Decision<any, any, any>>;
76
+ /**
77
+ * Get the profile registry (for testing/introspection)
78
+ */
79
+ get profileRegistry(): Map<string, unknown>;
80
+ }
81
+ /**
82
+ * Create a new Criterion MCP server
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * import { createMcpServer } from "@criterionx/mcp";
87
+ * import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
88
+ *
89
+ * const server = createMcpServer({
90
+ * decisions: [myDecision],
91
+ * profiles: { "my-decision": { threshold: 100 } },
92
+ * });
93
+ *
94
+ * const transport = new StdioServerTransport();
95
+ * await server.server.connect(transport);
96
+ * ```
97
+ */
98
+ declare function createMcpServer(options: McpServerOptions): CriterionMcpServer;
99
+
100
+ export { CriterionMcpServer, type DecisionListItem, type DecisionSchemaResponse, type McpErrorCode, type McpServerOptions, createMcpServer };
package/dist/index.js ADDED
@@ -0,0 +1,241 @@
1
+ // src/server.ts
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { z } from "zod";
4
+ import {
5
+ Engine,
6
+ extractDecisionSchema
7
+ } from "@criterionx/core";
8
+ var PACKAGE_VERSION = "0.3.3";
9
+ var CriterionMcpServer = class {
10
+ mcpServer;
11
+ engine;
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ decisions;
14
+ profiles;
15
+ constructor(options) {
16
+ this.mcpServer = new McpServer({
17
+ name: options.name ?? "criterion-mcp-server",
18
+ version: options.version ?? PACKAGE_VERSION
19
+ });
20
+ this.engine = new Engine();
21
+ this.decisions = /* @__PURE__ */ new Map();
22
+ this.profiles = /* @__PURE__ */ new Map();
23
+ for (const decision of options.decisions) {
24
+ this.decisions.set(decision.id, decision);
25
+ }
26
+ if (options.profiles) {
27
+ for (const [id, profile] of Object.entries(options.profiles)) {
28
+ this.profiles.set(id, profile);
29
+ }
30
+ }
31
+ this.registerTools();
32
+ }
33
+ registerTools() {
34
+ this.mcpServer.tool(
35
+ "list_decisions",
36
+ "List all registered Criterion decisions with their metadata. Returns decision IDs, versions, descriptions, and rule counts.",
37
+ {},
38
+ async () => {
39
+ const decisions = Array.from(
40
+ this.decisions.values()
41
+ ).map((d) => ({
42
+ id: d.id,
43
+ version: d.version,
44
+ description: d.meta?.description,
45
+ rulesCount: d.rules.length,
46
+ meta: d.meta
47
+ }));
48
+ return {
49
+ content: [
50
+ {
51
+ type: "text",
52
+ text: JSON.stringify({ decisions }, null, 2)
53
+ }
54
+ ]
55
+ };
56
+ }
57
+ );
58
+ this.mcpServer.tool(
59
+ "get_decision_schema",
60
+ "Get the JSON schemas for a specific decision. Returns input, output, and profile schemas that define the decision's contract.",
61
+ {
62
+ decisionId: z.string().describe("The ID of the decision to get schemas for")
63
+ },
64
+ async (args) => {
65
+ const decision = this.decisions.get(args.decisionId);
66
+ if (!decision) {
67
+ return {
68
+ content: [
69
+ {
70
+ type: "text",
71
+ text: JSON.stringify({
72
+ error: "DECISION_NOT_FOUND",
73
+ message: `Decision not found: ${args.decisionId}`,
74
+ availableDecisions: Array.from(this.decisions.keys())
75
+ })
76
+ }
77
+ ],
78
+ isError: true
79
+ };
80
+ }
81
+ const schema = extractDecisionSchema(decision);
82
+ return {
83
+ content: [
84
+ {
85
+ type: "text",
86
+ text: JSON.stringify(schema, null, 2)
87
+ }
88
+ ]
89
+ };
90
+ }
91
+ );
92
+ this.mcpServer.tool(
93
+ "evaluate_decision",
94
+ "Evaluate a Criterion decision with the given input and optional profile. Returns the decision result including status, data, and evaluation metadata with full explainability.",
95
+ {
96
+ decisionId: z.string().describe("The ID of the decision to evaluate"),
97
+ input: z.record(z.unknown()).describe("Input data matching the decision's input schema"),
98
+ profile: z.record(z.unknown()).optional().describe("Optional profile to use (overrides default profile)")
99
+ },
100
+ async (args) => {
101
+ const decision = this.decisions.get(args.decisionId);
102
+ if (!decision) {
103
+ return {
104
+ content: [
105
+ {
106
+ type: "text",
107
+ text: JSON.stringify({
108
+ error: "DECISION_NOT_FOUND",
109
+ message: `Decision not found: ${args.decisionId}`,
110
+ availableDecisions: Array.from(this.decisions.keys())
111
+ })
112
+ }
113
+ ],
114
+ isError: true
115
+ };
116
+ }
117
+ let profile = args.profile;
118
+ if (!profile) {
119
+ profile = this.profiles.get(args.decisionId);
120
+ if (!profile) {
121
+ return {
122
+ content: [
123
+ {
124
+ type: "text",
125
+ text: JSON.stringify({
126
+ error: "MISSING_PROFILE",
127
+ message: `No profile provided and no default profile for decision: ${args.decisionId}`
128
+ })
129
+ }
130
+ ],
131
+ isError: true
132
+ };
133
+ }
134
+ }
135
+ try {
136
+ const result = this.engine.run(decision, args.input, { profile });
137
+ return {
138
+ content: [
139
+ {
140
+ type: "text",
141
+ text: JSON.stringify(result, null, 2)
142
+ }
143
+ ]
144
+ };
145
+ } catch (error) {
146
+ const err = error instanceof Error ? error : new Error(String(error));
147
+ return {
148
+ content: [
149
+ {
150
+ type: "text",
151
+ text: JSON.stringify({
152
+ error: "EVALUATION_ERROR",
153
+ message: err.message
154
+ })
155
+ }
156
+ ],
157
+ isError: true
158
+ };
159
+ }
160
+ }
161
+ );
162
+ this.mcpServer.tool(
163
+ "explain_result",
164
+ "Get a human-readable explanation of a decision evaluation result. Formats the result metadata, matched rules, and evaluation trace into an easy-to-understand summary.",
165
+ {
166
+ result: z.object({
167
+ status: z.enum(["OK", "NO_MATCH", "INVALID_INPUT", "INVALID_OUTPUT"]),
168
+ data: z.unknown().nullable(),
169
+ meta: z.object({
170
+ decisionId: z.string(),
171
+ decisionVersion: z.string(),
172
+ profileId: z.string().optional(),
173
+ matchedRule: z.string().optional(),
174
+ evaluatedRules: z.array(
175
+ z.object({
176
+ ruleId: z.string(),
177
+ matched: z.boolean(),
178
+ explanation: z.string().optional()
179
+ })
180
+ ),
181
+ explanation: z.string(),
182
+ evaluatedAt: z.string()
183
+ })
184
+ }).describe("The evaluation result to explain")
185
+ },
186
+ async (args) => {
187
+ try {
188
+ const explanation = this.engine.explain(args.result);
189
+ return {
190
+ content: [
191
+ {
192
+ type: "text",
193
+ text: explanation
194
+ }
195
+ ]
196
+ };
197
+ } catch (error) {
198
+ const err = error instanceof Error ? error : new Error(String(error));
199
+ return {
200
+ content: [
201
+ {
202
+ type: "text",
203
+ text: JSON.stringify({
204
+ error: "EXPLAIN_ERROR",
205
+ message: err.message
206
+ })
207
+ }
208
+ ],
209
+ isError: true
210
+ };
211
+ }
212
+ }
213
+ );
214
+ }
215
+ /**
216
+ * Get the underlying MCP server instance
217
+ */
218
+ get server() {
219
+ return this.mcpServer;
220
+ }
221
+ /**
222
+ * Get the decision registry (for testing/introspection)
223
+ */
224
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
225
+ get decisionRegistry() {
226
+ return this.decisions;
227
+ }
228
+ /**
229
+ * Get the profile registry (for testing/introspection)
230
+ */
231
+ get profileRegistry() {
232
+ return this.profiles;
233
+ }
234
+ };
235
+ function createMcpServer(options) {
236
+ return new CriterionMcpServer(options);
237
+ }
238
+ export {
239
+ CriterionMcpServer,
240
+ createMcpServer
241
+ };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@criterionx/mcp",
3
+ "version": "0.3.3",
4
+ "description": "MCP server for Criterion decisions - expose business rules as LLM tools",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "LICENSE",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format esm --dts --clean",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "test:coverage": "vitest run --coverage",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "keywords": [
27
+ "criterion",
28
+ "decision-engine",
29
+ "mcp",
30
+ "model-context-protocol",
31
+ "llm",
32
+ "ai-tools",
33
+ "business-rules"
34
+ ],
35
+ "author": {
36
+ "name": "Tomas Maritano",
37
+ "url": "https://github.com/tomymaritano"
38
+ },
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/tomymaritano/criterionx.git",
43
+ "directory": "packages/mcp"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/tomymaritano/criterionx/issues"
47
+ },
48
+ "homepage": "https://github.com/tomymaritano/criterionx#readme",
49
+ "dependencies": {
50
+ "@criterionx/core": "workspace:*",
51
+ "@modelcontextprotocol/sdk": "^1.0.0"
52
+ },
53
+ "peerDependencies": {
54
+ "zod": "^3.22.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^20.0.0",
58
+ "tsup": "^8.0.0",
59
+ "typescript": "^5.3.0",
60
+ "vitest": "^4.0.16",
61
+ "zod": "^3.22.0"
62
+ },
63
+ "engines": {
64
+ "node": ">=18"
65
+ }
66
+ }