jev-decision-mcp 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/README.md +146 -0
- package/dist/config.js +24 -0
- package/dist/decision.js +60 -0
- package/dist/index.js +15 -0
- package/dist/schema.js +69 -0
- package/dist/server.js +23 -0
- package/examples/decision.json +30 -0
- package/package.json +61 -0
- package/server.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stefan (amidabuddha)
|
|
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,146 @@
|
|
|
1
|
+
# Jev Decision MCP
|
|
2
|
+
|
|
3
|
+
A local stdio MCP server exposing one tool, **`jev_decide`**, for typed decisions through the official TypeSafe API. Written in TypeScript with the official MCP and TypeSafe SDKs.
|
|
4
|
+
|
|
5
|
+
Independent community project; not affiliated with TypeSafe. Licensed under [MIT](LICENSE).
|
|
6
|
+
|
|
7
|
+
| Question | Use it for | Result |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `choice` | Choose a label or candidate | Choice, probabilities, confidence |
|
|
10
|
+
| `score` | Rate a single dimension on 2–10 ordered levels | 0-based score, probabilities, legend, confidence |
|
|
11
|
+
| `noul` | Judge whether a condition holds | Probability of yes, from 0 to 1 |
|
|
12
|
+
|
|
13
|
+
Batch independent questions over the same context in one call. Answers retain their question IDs. The MCP returns judgments and token usage; the caller owns thresholds, escalation, and action execution.
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Requires Node.js 22 or newer.
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
git clone https://github.com/amidabuddha/jev-decision-mcp.git
|
|
21
|
+
cd jev-decision-mcp
|
|
22
|
+
npm ci
|
|
23
|
+
cp -n .env.example .env
|
|
24
|
+
# Edit .env and set TYPESAFE_API_KEY.
|
|
25
|
+
npm run build
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Obtain the key from [TypeSafe Console](https://console.typesafe.ai). Use an official TypeSafe key, rather than an OpenRouter or third-party gateway key.
|
|
29
|
+
|
|
30
|
+
`.env` is excluded by `.gitignore`. The server reads the `.env` beside this README even when started from another working directory. Existing process environment variables take precedence. Restart the MCP server after editing `.env`.
|
|
31
|
+
|
|
32
|
+
| Variable | Default | Purpose |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `TYPESAFE_API_KEY` | Required for calls | TypeSafe API credential |
|
|
35
|
+
| `TYPESAFE_DEFAULT_MODEL` | `jev-latest` | Default model; each call can override it |
|
|
36
|
+
| `JEV_TIMEOUT_MS` | `30000` | Total deadline including retries; 1–120000 ms |
|
|
37
|
+
|
|
38
|
+
The server starts without a key so a host can discover the tool. Calls then return `MISSING_API_KEY`. API traffic is fixed to `https://api.typesafe.ai/v1/systemone`; `TYPESAFE_BASE_URL` does not override it. State and questions are sent to TypeSafe and may incur API charges. The server does not persist inputs or decisions and disables SDK logging. Upstream error bodies are not exposed because they may echo submitted data.
|
|
39
|
+
|
|
40
|
+
## Connect to Codex
|
|
41
|
+
|
|
42
|
+
After building, run:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
codex mcp add jev -- "$(command -v node)" "$PWD/dist/index.js"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Run this command from the repository root. It registers the absolute paths to your Node executable and built server.
|
|
49
|
+
|
|
50
|
+
Alternatively, merge this into your Codex `config.toml`, replacing the example paths with your absolute paths:
|
|
51
|
+
|
|
52
|
+
```toml
|
|
53
|
+
[mcp_servers.jev]
|
|
54
|
+
command = "/opt/homebrew/bin/node"
|
|
55
|
+
args = ["/absolute/path/to/jev-decision-mcp/dist/index.js"]
|
|
56
|
+
tool_timeout_sec = 45
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Keep the host tool timeout above `JEV_TIMEOUT_MS / 1000`. The key stays in `.env`; it need not appear in the command or MCP configuration. See [official Codex MCP configuration](https://developers.openai.com/codex/mcp).
|
|
60
|
+
|
|
61
|
+
For other stdio MCP hosts, configure your absolute paths similarly:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"jev": {
|
|
67
|
+
"command": "/opt/homebrew/bin/node",
|
|
68
|
+
"args": ["/absolute/path/to/jev-decision-mcp/dist/index.js"]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The host starts the process and communicates over stdin/stdout. For interactive local development use `npm run dev`; this is not an HTTP server. No host configuration is modified by setup or tests.
|
|
75
|
+
|
|
76
|
+
## Call the tool
|
|
77
|
+
|
|
78
|
+
Call `jev_decide` with the JSON in [examples/decision.json](examples/decision.json). It combines a team choice, a refund yes/no judgment, and an urgency score. Smaller example:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"state": { "request": "Please refund the duplicate charge." },
|
|
83
|
+
"questions": {
|
|
84
|
+
"route": {
|
|
85
|
+
"type": "choice",
|
|
86
|
+
"instructions": "Which team should handle `request`?",
|
|
87
|
+
"criteria": {
|
|
88
|
+
"billing": "Charges, invoices, refunds",
|
|
89
|
+
"technical": "Broken software or integrations",
|
|
90
|
+
"other": "Neither billing nor technical"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The response contains `model`, `answers`, and `usage`, both as MCP structured content and JSON text. Choice supports 1–255 named options. Instructions and descriptions may be strings, JSON objects, or arrays; choice descriptions may also be null. Noul accepts optional `criteria.true` and `criteria.false` descriptions.
|
|
98
|
+
|
|
99
|
+
Supply relevant facts, source text, and policies explicitly: Jev cannot see the caller's conversation or local files. Write complete judgments in `instructions`; IDs are only response keys. Include a no-match option when appropriate. Questions in one batch cannot see one another's answers. A noul near 0.5 is uncertainty about yes/no, not medium intensity. Confidence does not authorize actions or guarantee correctness; evaluate thresholds on representative data.
|
|
100
|
+
|
|
101
|
+
## Verify
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
npm run check # Type checking + mocked API tests + real stdio MCP handshake
|
|
105
|
+
npm run test:live # Explicit live API call using only the synthetic example
|
|
106
|
+
npm run test:codex # Installed Codex schema parser + native MCP discovery, no model turn
|
|
107
|
+
# Add -- --live to test:codex to also call Jev through Codex's native MCP interface.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Normal tests use synthetic credentials and mocked HTTP responses; they do not contact TypeSafe. Tests cover mixed decisions, input/output validation, missing keys, authentication errors, rate-limit retries, deadlines, cancellation, and MCP discovery/calls. The stdio smoke test starts from another working directory and checks that stdout remains valid MCP.
|
|
111
|
+
|
|
112
|
+
The live test requires your key, starts the built MCP server, calls `jev_decide` through an MCP client, validates the response, and prints judgments, elapsed time, and token usage. A passing live smoke test verifies integration, not general decision quality. SDK retries can make up to three HTTP attempts for transient failures, bounded by the total deadline.
|
|
113
|
+
|
|
114
|
+
`test:codex` requires the Codex CLI. It uses an ephemeral diagnostic context without starting a model turn. It verifies that Codex rejects the original tuple-style score schema, accepts the repaired array schema, and discovers the MCP tool. Passing generic MCP client tests alone does not establish Codex compatibility.
|
|
115
|
+
|
|
116
|
+
If an older session cannot see the tool after a server update, start a fresh task so it loads the rebuilt server. The MCP server is named `jev` and its tool is `jev_decide`; `$Jev` is not an installed skill.
|
|
117
|
+
|
|
118
|
+
Service/configuration failures are MCP tool errors (`isError: true`), never invented decisions. Codes include `MISSING_API_KEY`, `INVALID_INPUT`, `INVALID_RESPONSE`, `API_ERROR`, `CONNECTION_ERROR`, `TIMEOUT`, and `CANCELLED`. Invalid arguments may also be rejected directly by the MCP SDK.
|
|
119
|
+
|
|
120
|
+
## References
|
|
121
|
+
|
|
122
|
+
Built using the `typesafe-ai` skill and official documentation, checked September 22, 2026:
|
|
123
|
+
|
|
124
|
+
- [TypeSafe HTTP API](https://docs.typesafe.ai/api)
|
|
125
|
+
- [TypeSafe JavaScript SDK](https://docs.typesafe.ai/sdk/javascript)
|
|
126
|
+
- [Decision primitives and question design](https://docs.typesafe.ai/primitives)
|
|
127
|
+
- [Function-calling cookbook](https://docs.typesafe.ai/cookbooks/function_calling)
|
|
128
|
+
- [MCP server development](https://modelcontextprotocol.io/docs/develop/build-server)
|
|
129
|
+
|
|
130
|
+
`jev-latest` follows TypeSafe model updates. For repeatable evaluations, set a specific supported model version. SDK versions are recorded in `package-lock.json`.
|
|
131
|
+
|
|
132
|
+
## Releases and package publishing
|
|
133
|
+
|
|
134
|
+
GitHub releases provide source archives. The repository also includes npm packaging
|
|
135
|
+
and official MCP Registry metadata in `server.json`. npm and registry publication
|
|
136
|
+
are separate steps; a GitHub release does not imply that either listing is live.
|
|
137
|
+
|
|
138
|
+
To verify the publishable artifact locally, run `npm pack --dry-run`. The package
|
|
139
|
+
contains the compiled server, license, README, example input, and registry metadata.
|
|
140
|
+
Local `.env` files, tests, and development dependencies are not bundled.
|
|
141
|
+
|
|
142
|
+
After the npm package has been published, clients can launch it with
|
|
143
|
+
`npx -y jev-decision-mcp@0.1.0`. For that installation method, provide
|
|
144
|
+
`TYPESAFE_API_KEY` in the MCP host's environment; the package does not read a `.env`
|
|
145
|
+
from the caller's working directory. The clone-and-build setup above remains
|
|
146
|
+
available independently of npm publication.
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { loadEnvFile } from "node:process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
export function loadProjectEnv() {
|
|
4
|
+
try {
|
|
5
|
+
// Resolve beside the project, even when an MCP host starts us in another cwd.
|
|
6
|
+
// Existing process environment variables take precedence.
|
|
7
|
+
loadEnvFile(fileURLToPath(new URL("../.env", import.meta.url)));
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function readConfig(env = process.env) {
|
|
15
|
+
const timeoutMs = Number(env.JEV_TIMEOUT_MS ?? 30000);
|
|
16
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 120000) {
|
|
17
|
+
throw new Error("JEV_TIMEOUT_MS must be an integer from 1 to 120000");
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
apiKey: env.TYPESAFE_API_KEY?.trim() || undefined,
|
|
21
|
+
model: env.TYPESAFE_DEFAULT_MODEL?.trim() || "jev-latest",
|
|
22
|
+
timeoutMs,
|
|
23
|
+
};
|
|
24
|
+
}
|
package/dist/decision.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { APIConnectionError, APIError, APITimeoutError, APIUserAbortError, TypeSafeClient } from "@typesafe-ai/sdk";
|
|
2
|
+
import { decisionInput, validateResponse } from "./schema.js";
|
|
3
|
+
export class DecisionError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function createDecider(config, fetch) {
|
|
11
|
+
const client = config.apiKey ? new TypeSafeClient({
|
|
12
|
+
apiKey: config.apiKey,
|
|
13
|
+
// Always use the official origin; tool input cannot redirect credentials.
|
|
14
|
+
baseURL: "https://api.typesafe.ai",
|
|
15
|
+
defaultModel: config.model,
|
|
16
|
+
timeout: Math.min(config.timeoutMs, 10000),
|
|
17
|
+
logLevel: "off",
|
|
18
|
+
...(fetch ? { fetch } : {}),
|
|
19
|
+
}) : undefined;
|
|
20
|
+
return async (raw, signal) => {
|
|
21
|
+
const parsed = decisionInput.safeParse(raw);
|
|
22
|
+
if (!parsed.success)
|
|
23
|
+
throw new DecisionError("INVALID_INPUT", "Invalid decision request. Check state, instructions and criteria against the tool schema.");
|
|
24
|
+
if (!client)
|
|
25
|
+
throw new DecisionError("MISSING_API_KEY", "Set TYPESAFE_API_KEY in the project .env or process environment, then restart the MCP server.");
|
|
26
|
+
const deadline = AbortSignal.timeout(config.timeoutMs);
|
|
27
|
+
try {
|
|
28
|
+
// Runtime validation guarantees Score's minimum two entries, expressed
|
|
29
|
+
// as a tuple by the SDK but as a homogeneous array in our MCP schema.
|
|
30
|
+
const result = await client.systemOne(parsed.data, {
|
|
31
|
+
signal: signal ? AbortSignal.any([signal, deadline]) : deadline,
|
|
32
|
+
});
|
|
33
|
+
try {
|
|
34
|
+
return validateResponse(result, parsed.data);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new DecisionError("INVALID_RESPONSE", "TypeSafe returned a response that does not match the requested decision schema.");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof DecisionError)
|
|
42
|
+
throw error;
|
|
43
|
+
if (error instanceof APIUserAbortError || error instanceof APITimeoutError) {
|
|
44
|
+
throw new DecisionError(signal?.aborted ? "CANCELLED" : "TIMEOUT", signal?.aborted ? "Decision request cancelled." : "TypeSafe request exceeded its timeout.");
|
|
45
|
+
}
|
|
46
|
+
if (error instanceof APIError) {
|
|
47
|
+
// Upstream bodies can echo submitted data. Expose only a status-based message.
|
|
48
|
+
const guidance = error.status === 401 ? "Check TYPESAFE_API_KEY."
|
|
49
|
+
: error.status === 403 ? "Check account permissions."
|
|
50
|
+
: error.status === 422 ? "Check the model, question instructions and criteria."
|
|
51
|
+
: error.status === 429 ? "Rate limit reached; retry later."
|
|
52
|
+
: "The service rejected the request; retry later if temporary.";
|
|
53
|
+
throw new DecisionError("API_ERROR", `TypeSafe HTTP ${error.status}. ${guidance}`);
|
|
54
|
+
}
|
|
55
|
+
if (error instanceof APIConnectionError)
|
|
56
|
+
throw new DecisionError("CONNECTION_ERROR", "Could not connect to TypeSafe. Check network access.");
|
|
57
|
+
throw new DecisionError("INTERNAL_ERROR", "The decision request failed unexpectedly.");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { loadProjectEnv, readConfig } from "./config.js";
|
|
4
|
+
import { createDecider } from "./decision.js";
|
|
5
|
+
import { createServer } from "./server.js";
|
|
6
|
+
try {
|
|
7
|
+
loadProjectEnv();
|
|
8
|
+
const server = createServer(createDecider(readConfig()));
|
|
9
|
+
await server.connect(new StdioServerTransport());
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// stdout is reserved for MCP; never print environment values or error bodies.
|
|
13
|
+
console.error("Jev MCP could not start. Check .env readability and JEV_TIMEOUT_MS (1–120000).");
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const entry = z.union([z.string().min(1), z.record(z.string(), z.json()), z.array(z.json())]);
|
|
3
|
+
const description = entry.nullable();
|
|
4
|
+
const key = z.string().min(1);
|
|
5
|
+
const question = z.discriminatedUnion("type", [
|
|
6
|
+
z.strictObject({
|
|
7
|
+
type: z.literal("choice"),
|
|
8
|
+
instructions: entry,
|
|
9
|
+
criteria: z.record(key, description).refine((value) => Object.keys(value).length >= 1 && Object.keys(value).length <= 255, "Choice requires 1 to 255 options"),
|
|
10
|
+
}),
|
|
11
|
+
z.strictObject({
|
|
12
|
+
type: z.literal("score"),
|
|
13
|
+
instructions: entry,
|
|
14
|
+
// A tuple emits draft-07 `items: [...]`, which Codex cannot deserialize.
|
|
15
|
+
// Homogeneous array items preserve the same 2–10-level contract.
|
|
16
|
+
criteria: z.array(entry).min(2).max(10),
|
|
17
|
+
}),
|
|
18
|
+
z.strictObject({
|
|
19
|
+
type: z.literal("noul"),
|
|
20
|
+
instructions: entry,
|
|
21
|
+
criteria: z.strictObject({ true: description.optional(), false: description.optional() }).optional(),
|
|
22
|
+
}),
|
|
23
|
+
]);
|
|
24
|
+
export const decisionInput = z.strictObject({
|
|
25
|
+
state: entry.describe("Relevant source text or JSON context; include evidence, definitions and policies needed for the questions."),
|
|
26
|
+
questions: z.record(key, question).refine((value) => Object.keys(value).length > 0, "Provide at least one question")
|
|
27
|
+
.describe("Named independent questions over the same state. IDs are not model instructions; write each full judgment in instructions."),
|
|
28
|
+
model: z.string().trim().min(1).optional().describe("Optional TypeSafe model override; defaults to the configured model or jev-latest."),
|
|
29
|
+
});
|
|
30
|
+
const probability = z.number().min(0).max(1);
|
|
31
|
+
const probabilities = z.record(key, probability);
|
|
32
|
+
const answer = z.discriminatedUnion("type", [
|
|
33
|
+
z.object({ type: z.literal("noul"), noul: probability }),
|
|
34
|
+
z.object({ type: z.literal("choice"), choice: z.string(), probabilities, confidence: probability }),
|
|
35
|
+
z.object({ type: z.literal("score"), score: z.number(), legend: z.record(key, description), probabilities, confidence: probability }),
|
|
36
|
+
]);
|
|
37
|
+
export const decisionOutput = z.object({
|
|
38
|
+
model: z.string().min(1),
|
|
39
|
+
answers: z.record(key, answer),
|
|
40
|
+
usage: z.object({ input_tokens: z.number().int().nonnegative(), output_tokens: z.number().int().nonnegative() }),
|
|
41
|
+
});
|
|
42
|
+
function sameKeys(actual, expected) {
|
|
43
|
+
return Object.keys(actual).length === expected.length && expected.every((id) => Object.hasOwn(actual, id));
|
|
44
|
+
}
|
|
45
|
+
export function validateResponse(raw, input) {
|
|
46
|
+
const result = decisionOutput.parse(raw);
|
|
47
|
+
if (!sameKeys(result.answers, Object.keys(input.questions)))
|
|
48
|
+
throw new Error("Answer IDs do not match questions");
|
|
49
|
+
for (const [id, question] of Object.entries(input.questions)) {
|
|
50
|
+
const answer = result.answers[id];
|
|
51
|
+
if (!answer || answer.type !== question.type)
|
|
52
|
+
throw new Error("Answer type does not match question");
|
|
53
|
+
if (answer.type === "noul")
|
|
54
|
+
continue;
|
|
55
|
+
const levels = question.type === "choice" ? Object.keys(question.criteria)
|
|
56
|
+
: question.type === "score" ? question.criteria.map((_, index) => String(index)) : [];
|
|
57
|
+
if (!sameKeys(answer.probabilities, levels))
|
|
58
|
+
throw new Error("Probability labels do not match criteria");
|
|
59
|
+
const sum = Object.values(answer.probabilities).reduce((a, b) => a + b, 0);
|
|
60
|
+
if (Math.abs(sum - 1) > 0.001)
|
|
61
|
+
throw new Error("Probabilities do not sum to one");
|
|
62
|
+
if (answer.type === "choice" && !levels.includes(answer.choice))
|
|
63
|
+
throw new Error("Unknown choice");
|
|
64
|
+
if (answer.type === "score" && (answer.score < 0 || answer.score > levels.length - 1 || !sameKeys(answer.legend, levels))) {
|
|
65
|
+
throw new Error("Score or legend does not match criteria");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { DecisionError } from "./decision.js";
|
|
3
|
+
import { decisionInput, decisionOutput } from "./schema.js";
|
|
4
|
+
export function createServer(decide) {
|
|
5
|
+
const server = new McpServer({ name: "jev-decision-mcp", version: "0.1.0" });
|
|
6
|
+
server.registerTool("jev_decide", {
|
|
7
|
+
title: "Ask Jev for typed decisions",
|
|
8
|
+
description: "Evaluate supplied context using TypeSafe Jev. Batch independent, narrow questions in one call: choice selects a provided label; score returns a position on 2–10 ordered levels (0-based); noul returns probability of yes, not intensity. Supply relevant evidence and full instructions; question IDs are not sent to the model. Include an other/none choice when appropriate. Questions cannot use each other's answers. Returns raw judgments, probabilities, confidence for choice/score, and token usage. Use caller-defined policies for uncertainty. This sends the supplied state and questions to TypeSafe and may incur API charges. It does not execute selected actions.",
|
|
9
|
+
inputSchema: decisionInput,
|
|
10
|
+
outputSchema: decisionOutput,
|
|
11
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
12
|
+
}, async (input, extra) => {
|
|
13
|
+
try {
|
|
14
|
+
const result = await decide(input, extra.signal);
|
|
15
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result };
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const safe = error instanceof DecisionError ? error : new DecisionError("INTERNAL_ERROR", "Decision request failed.");
|
|
19
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: safe.code, message: safe.message }) }] };
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
return server;
|
|
23
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"state": {
|
|
3
|
+
"ticket": "I was charged twice for order A-104. Please refund the duplicate charge today.",
|
|
4
|
+
"policy": "Duplicate charges should be reviewed by billing. This tool only classifies the request."
|
|
5
|
+
},
|
|
6
|
+
"questions": {
|
|
7
|
+
"team": {
|
|
8
|
+
"type": "choice",
|
|
9
|
+
"instructions": "Which team should handle the issue in `ticket`?",
|
|
10
|
+
"criteria": {
|
|
11
|
+
"billing": "Charges, invoices, or refunds",
|
|
12
|
+
"technical": "Broken features or software failures",
|
|
13
|
+
"other": "Neither billing nor technical"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"refund_requested": {
|
|
17
|
+
"type": "noul",
|
|
18
|
+
"instructions": "Does the customer explicitly request a refund in `ticket`?"
|
|
19
|
+
},
|
|
20
|
+
"urgency": {
|
|
21
|
+
"type": "score",
|
|
22
|
+
"instructions": "How time-sensitive is the customer's requested response in `ticket`?",
|
|
23
|
+
"criteria": [
|
|
24
|
+
"No response deadline or time pressure is stated",
|
|
25
|
+
"A response is requested within several days",
|
|
26
|
+
"A response is requested today or immediately"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jev-decision-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A local MCP server for TypeSafe Jev typed decisions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.0.0"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"test": "npm run build && tsx --test test/*.test.ts",
|
|
14
|
+
"check": "tsc -p tsconfig.check.json && npm test",
|
|
15
|
+
"test:live": "npm run build && tsx scripts/live-test.ts",
|
|
16
|
+
"test:codex": "npm run build && node --import tsx scripts/check-codex-schema.ts",
|
|
17
|
+
"prepack": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
21
|
+
"@typesafe-ai/sdk": "0.6.0",
|
|
22
|
+
"zod": "^4.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^22.0.0",
|
|
26
|
+
"tsx": "^4.0.0",
|
|
27
|
+
"typescript": "^5.9.0"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"author": "Stefan (amidabuddha)",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/amidabuddha/jev-decision-mcp.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/amidabuddha/jev-decision-mcp#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/amidabuddha/jev-decision-mcp/issues"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"mcp",
|
|
41
|
+
"mcp-server",
|
|
42
|
+
"jev",
|
|
43
|
+
"typesafe",
|
|
44
|
+
"codex",
|
|
45
|
+
"classification"
|
|
46
|
+
],
|
|
47
|
+
"bin": {
|
|
48
|
+
"jev-decision-mcp": "dist/index.js"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"dist",
|
|
52
|
+
"LICENSE",
|
|
53
|
+
"README.md",
|
|
54
|
+
"examples/decision.json",
|
|
55
|
+
"server.json"
|
|
56
|
+
],
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"mcpName": "io.github.amidabuddha/jev-decision-mcp"
|
|
61
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.amidabuddha/jev-decision-mcp",
|
|
4
|
+
"description": "Typed Jev decisions in one batched tool: choice, score, and yes/no probabilities.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/amidabuddha/jev-decision-mcp",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "npm",
|
|
13
|
+
"identifier": "jev-decision-mcp",
|
|
14
|
+
"version": "0.1.0",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
},
|
|
18
|
+
"environmentVariables": [
|
|
19
|
+
{
|
|
20
|
+
"name": "TYPESAFE_API_KEY",
|
|
21
|
+
"description": "Your TypeSafe API key from console.typesafe.ai",
|
|
22
|
+
"isRequired": true,
|
|
23
|
+
"isSecret": true,
|
|
24
|
+
"format": "string"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "TYPESAFE_DEFAULT_MODEL",
|
|
28
|
+
"description": "Default Jev model",
|
|
29
|
+
"default": "jev-latest",
|
|
30
|
+
"format": "string"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "JEV_TIMEOUT_MS",
|
|
34
|
+
"description": "Total request deadline in milliseconds, including retries (1\u2013120000)",
|
|
35
|
+
"default": "30000",
|
|
36
|
+
"format": "number"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|