askjev 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 +77 -0
- package/dist/ask.js +137 -0
- package/dist/cli.js +16 -0
- package/dist/jev.js +7 -0
- package/dist/router.js +57 -0
- package/dist/rubrics.js +14 -0
- package/dist/schema.js +92 -0
- package/dist/server.js +54 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pedro Zaccaria
|
|
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,77 @@
|
|
|
1
|
+
# askjev
|
|
2
|
+
|
|
3
|
+
Unofficial [MCP](https://modelcontextprotocol.io) server for [Jev](https://docs.typesafe.ai),
|
|
4
|
+
Typesafe AI's System One model. Not affiliated with Typesafe AI.
|
|
5
|
+
|
|
6
|
+
Give your agent a fast second opinion. It asks a plain question about material it already
|
|
7
|
+
has, Jev works out whether that is a yes/no, a scale, or a choice, and answers with
|
|
8
|
+
calibrated probabilities. No generative model in the loop, one round trip, a fraction of the
|
|
9
|
+
cost and latency of a sub-agent.
|
|
10
|
+
|
|
11
|
+
## Status
|
|
12
|
+
|
|
13
|
+
Not published yet. The pipeline works end to end against the live API and the router
|
|
14
|
+
scores 100% on the first evaluation dataset. See [docs/EVAL.md](docs/EVAL.md).
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
Requires Node 22+ and a Typesafe API key in `TYPESAFE_API_KEY`.
|
|
19
|
+
|
|
20
|
+
Claude Code:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
claude mcp add askjev -e TYPESAFE_API_KEY=your-key -- npx -y askjev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Claude Desktop, Cursor, and other clients that take a JSON config:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"askjev": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["-y", "askjev"],
|
|
34
|
+
"env": { "TYPESAFE_API_KEY": "your-key" }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
On Windows some hosts cannot launch `npx` directly. Use `"command": "cmd"` with
|
|
41
|
+
`"args": ["/c", "npx", "-y", "askjev"]`.
|
|
42
|
+
|
|
43
|
+
## The tool
|
|
44
|
+
|
|
45
|
+
`ask` takes the material and a list of questions. Options are only needed when the question
|
|
46
|
+
has named alternatives.
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"state": "Customer writes: charged twice for one order, wants it fixed today.",
|
|
51
|
+
"questions": [
|
|
52
|
+
{ "question": "Does the customer ask for a refund?" },
|
|
53
|
+
{ "question": "How frustrated is the customer?" },
|
|
54
|
+
{ "question": "Which team should handle this?", "options": ["billing", "platform", "mobile"] }
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Each answer comes back with probabilities, Jev's confidence, and how the question was
|
|
60
|
+
routed. Read the confidence before acting on the answer.
|
|
61
|
+
|
|
62
|
+
Full contract, pipeline diagrams, and the reasoning behind the design:
|
|
63
|
+
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). How the router is evaluated:
|
|
64
|
+
[docs/EVAL.md](docs/EVAL.md).
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npm ci
|
|
70
|
+
npm run lint
|
|
71
|
+
npm run typecheck
|
|
72
|
+
npm test
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
package/dist/ask.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { choice, noul, score, } from "@typesafe-ai/sdk";
|
|
2
|
+
import { buildRouting, buildRubricPick, key, readRouting, readRubricPick, } from "./router.js";
|
|
3
|
+
import { RUBRICS } from "./rubrics.js";
|
|
4
|
+
/**
|
|
5
|
+
* The pipeline: route each question, pick built-in rubrics where needed, answer everything
|
|
6
|
+
* in one Jev call, and shape the result. See docs/ARCHITECTURE.md, "Pipeline".
|
|
7
|
+
*/
|
|
8
|
+
export async function ask(jev, input) {
|
|
9
|
+
const { questions } = input;
|
|
10
|
+
const usage = { input_tokens: 0, output_tokens: 0 };
|
|
11
|
+
let model = "";
|
|
12
|
+
// Call 1: route.
|
|
13
|
+
const routingResult = await jev.systemOne(buildRouting(questions));
|
|
14
|
+
addUsage(usage, routingResult.usage);
|
|
15
|
+
model = routingResult.model;
|
|
16
|
+
const routed = questions.map((_, i) => {
|
|
17
|
+
const a = routingResult.answers[key(i)];
|
|
18
|
+
if (a?.type !== "choice")
|
|
19
|
+
throw new Error(`askjev: routing answer for ${key(i)} is missing or malformed`);
|
|
20
|
+
return readRouting(a);
|
|
21
|
+
});
|
|
22
|
+
// Call 2: pick a rubric for every scale question that came without options.
|
|
23
|
+
const needRubric = questions.flatMap((q, i) => !q.options && routed[i]?.kind === "score" ? [i] : []);
|
|
24
|
+
const picked = new Map();
|
|
25
|
+
if (needRubric.length > 0) {
|
|
26
|
+
const pickResult = await jev.systemOne(buildRubricPick(questions, needRubric));
|
|
27
|
+
addUsage(usage, pickResult.usage);
|
|
28
|
+
for (const i of needRubric) {
|
|
29
|
+
const a = pickResult.answers[key(i)];
|
|
30
|
+
if (a?.type !== "choice")
|
|
31
|
+
throw new Error(`askjev: rubric answer for ${key(i)} is missing or malformed`);
|
|
32
|
+
picked.set(i, readRubricPick(a));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Build the typed questions, or an error entry for questions that cannot be asked.
|
|
36
|
+
const typed = {};
|
|
37
|
+
const answers = questions.map((q, i) => {
|
|
38
|
+
const r = routed[i];
|
|
39
|
+
if (!r)
|
|
40
|
+
return null;
|
|
41
|
+
const routing = { kind: r.kind, confidence: r.confidence };
|
|
42
|
+
if (r.kind === "noul") {
|
|
43
|
+
typed[key(i)] = noul(q.question);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (r.kind === "choice") {
|
|
47
|
+
if (!q.options) {
|
|
48
|
+
return {
|
|
49
|
+
kind: "error",
|
|
50
|
+
message: `"${q.question}" asks to pick between alternatives, but no options were given. Pass options with the alternatives.`,
|
|
51
|
+
routing,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
typed[key(i)] = choice(q.question, Object.fromEntries(q.options.map((o) => [o, null])));
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
// score
|
|
58
|
+
if (q.options) {
|
|
59
|
+
typed[key(i)] = score(q.question, q.options);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const rubric = picked.get(i);
|
|
63
|
+
if (!rubric)
|
|
64
|
+
throw new Error(`askjev: no rubric was picked for ${key(i)}`);
|
|
65
|
+
typed[key(i)] = score(q.question, RUBRICS[rubric.name]);
|
|
66
|
+
return null;
|
|
67
|
+
});
|
|
68
|
+
// Call 3: answer.
|
|
69
|
+
if (Object.keys(typed).length > 0) {
|
|
70
|
+
const result = await jev.systemOne({ state: input.state, questions: typed });
|
|
71
|
+
addUsage(usage, result.usage);
|
|
72
|
+
model = result.model;
|
|
73
|
+
questions.forEach((q, i) => {
|
|
74
|
+
if (answers[i])
|
|
75
|
+
return;
|
|
76
|
+
const r = routed[i];
|
|
77
|
+
const a = result.answers[key(i)];
|
|
78
|
+
if (!r || !a)
|
|
79
|
+
throw new Error(`askjev: answer for ${key(i)} is missing`);
|
|
80
|
+
answers[i] = shape(a, r, q.options ? undefined : picked.get(i));
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
model,
|
|
85
|
+
usage,
|
|
86
|
+
answers: answers.map((a, i) => {
|
|
87
|
+
if (!a)
|
|
88
|
+
throw new Error(`askjev: no answer was produced for ${key(i)}`);
|
|
89
|
+
return a;
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function shape(a, r, rubric) {
|
|
94
|
+
const routing = rubric
|
|
95
|
+
? {
|
|
96
|
+
kind: r.kind,
|
|
97
|
+
confidence: r.confidence,
|
|
98
|
+
rubric: { name: rubric.name, confidence: rubric.confidence },
|
|
99
|
+
}
|
|
100
|
+
: { kind: r.kind, confidence: r.confidence };
|
|
101
|
+
switch (a.type) {
|
|
102
|
+
case "noul":
|
|
103
|
+
return {
|
|
104
|
+
kind: "noul",
|
|
105
|
+
answer: a.noul,
|
|
106
|
+
probabilities: { yes: a.noul, no: 1 - a.noul },
|
|
107
|
+
routing,
|
|
108
|
+
};
|
|
109
|
+
case "choice":
|
|
110
|
+
return {
|
|
111
|
+
kind: "choice",
|
|
112
|
+
answer: a.choice,
|
|
113
|
+
probabilities: { ...a.probabilities },
|
|
114
|
+
confidence: a.confidence,
|
|
115
|
+
routing,
|
|
116
|
+
};
|
|
117
|
+
case "score": {
|
|
118
|
+
const legend = Object.fromEntries(Object.entries(a.legend).map(([k, v]) => [k, String(v)]));
|
|
119
|
+
const base = {
|
|
120
|
+
kind: "score",
|
|
121
|
+
answer: a.score,
|
|
122
|
+
legend,
|
|
123
|
+
probabilities: { ...a.probabilities },
|
|
124
|
+
confidence: a.confidence,
|
|
125
|
+
routing,
|
|
126
|
+
};
|
|
127
|
+
return rubric ? { ...base, rubric: rubric.name, note: rubricNote(rubric.name) } : base;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function rubricNote(name) {
|
|
132
|
+
return `No options were given, so the built-in '${name}' rubric was used. Pass options for a rubric tailored to your question.`;
|
|
133
|
+
}
|
|
134
|
+
function addUsage(total, u) {
|
|
135
|
+
total.input_tokens += u.input_tokens;
|
|
136
|
+
total.output_tokens += u.output_tokens;
|
|
137
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { createJev } from "./jev.js";
|
|
5
|
+
import { createServer } from "./server.js";
|
|
6
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
7
|
+
async function main() {
|
|
8
|
+
const jev = createJev();
|
|
9
|
+
const server = createServer(jev, version);
|
|
10
|
+
await server.connect(new StdioServerTransport());
|
|
11
|
+
}
|
|
12
|
+
main().catch((error) => {
|
|
13
|
+
// stdout is the MCP protocol channel; diagnostics go to stderr.
|
|
14
|
+
console.error(`askjev: ${error instanceof Error ? error.message : String(error)}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
package/dist/jev.js
ADDED
package/dist/router.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { choice } from "@typesafe-ai/sdk";
|
|
2
|
+
import { RUBRICS } from "./rubrics.js";
|
|
3
|
+
/** Name used for question `i` in every Jev request, so answers map back by position. */
|
|
4
|
+
export function key(index) {
|
|
5
|
+
return `q${index}`;
|
|
6
|
+
}
|
|
7
|
+
// ---------- step 1: what kind of question is this? ----------
|
|
8
|
+
const WITH_OPTIONS = {
|
|
9
|
+
choice: "The options are distinct categories or alternatives with no inherent order between them. The answer is one of the options.",
|
|
10
|
+
score: "The options are ordered levels of a single quantity, listed from lowest to highest, such as degrees, ratings, grades, or urgency levels. The answer is a position on that scale.",
|
|
11
|
+
};
|
|
12
|
+
const WITHOUT_OPTIONS = {
|
|
13
|
+
noul: "A yes/no question. It can be answered with yes or no, true or false, or the probability that something holds.",
|
|
14
|
+
score: "A question about how much, how well, how likely, how often, or how strongly something applies. The answer is a degree on a scale.",
|
|
15
|
+
choice: "A question that asks to pick one of several named alternatives, such as which, who, or what kind, but the alternatives are not listed.",
|
|
16
|
+
};
|
|
17
|
+
/** Build the routing request: one choice question per input question, over the questions themselves. */
|
|
18
|
+
export function buildRouting(questions) {
|
|
19
|
+
const state = {};
|
|
20
|
+
const routing = {};
|
|
21
|
+
questions.forEach((q, i) => {
|
|
22
|
+
const k = key(i);
|
|
23
|
+
state[k] = q.options ? { question: q.question, options: q.options } : { question: q.question };
|
|
24
|
+
routing[k] = choice(`What kind of question is \`${k}\`? Judge by what the question asks for${q.options ? " and by the nature of its options" : ""}.`, q.options ? WITH_OPTIONS : WITHOUT_OPTIONS);
|
|
25
|
+
});
|
|
26
|
+
return { state, questions: routing };
|
|
27
|
+
}
|
|
28
|
+
export function readRouting(answer) {
|
|
29
|
+
return { kind: answer.choice, confidence: answer.confidence };
|
|
30
|
+
}
|
|
31
|
+
// ---------- step 2: which built-in rubric fits a scale question without options? ----------
|
|
32
|
+
const RUBRIC_CRITERIA = {
|
|
33
|
+
intensity: `How much, how strongly, or to what extent something applies. Levels: ${RUBRICS.intensity.join(", ")}.`,
|
|
34
|
+
quality: `How good or well done something is. Levels: ${RUBRICS.quality.join(", ")}.`,
|
|
35
|
+
severity: `How serious, harmful, or damaging something is. Levels: ${RUBRICS.severity.join(", ")}.`,
|
|
36
|
+
likelihood: `How probable something is. Levels: ${RUBRICS.likelihood.join(", ")}.`,
|
|
37
|
+
sentiment: `How positive or negative the tone or attitude is. Levels: ${RUBRICS.sentiment.join(", ")}.`,
|
|
38
|
+
frequency: `How often something happens. Levels: ${RUBRICS.frequency.join(", ")}.`,
|
|
39
|
+
agreement: `How much someone agrees with a statement. Levels: ${RUBRICS.agreement.join(", ")}.`,
|
|
40
|
+
};
|
|
41
|
+
/** Build the rubric-selection request for the given question indexes. */
|
|
42
|
+
export function buildRubricPick(questions, indexes) {
|
|
43
|
+
const state = {};
|
|
44
|
+
const picks = {};
|
|
45
|
+
for (const i of indexes) {
|
|
46
|
+
const k = key(i);
|
|
47
|
+
const q = questions[i];
|
|
48
|
+
if (!q)
|
|
49
|
+
continue;
|
|
50
|
+
state[k] = q.question;
|
|
51
|
+
picks[k] = choice(`Which rubric best fits the scale that \`${k}\` asks about? Pick by the quantity being measured, not by the topic.`, RUBRIC_CRITERIA);
|
|
52
|
+
}
|
|
53
|
+
return { state, questions: picks };
|
|
54
|
+
}
|
|
55
|
+
export function readRubricPick(answer) {
|
|
56
|
+
return { name: answer.choice, confidence: answer.confidence };
|
|
57
|
+
}
|
package/dist/rubrics.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in rubrics for scale questions that arrive without options.
|
|
3
|
+
* All have five levels, lowest to highest, so Jev can discriminate without the levels blurring.
|
|
4
|
+
*/
|
|
5
|
+
export const RUBRICS = {
|
|
6
|
+
intensity: ["not at all", "slightly", "moderately", "very", "extremely"],
|
|
7
|
+
quality: ["poor", "below average", "acceptable", "good", "excellent"],
|
|
8
|
+
severity: ["trivial", "minor", "moderate", "major", "critical"],
|
|
9
|
+
likelihood: ["very unlikely", "unlikely", "uncertain", "likely", "very likely"],
|
|
10
|
+
sentiment: ["very negative", "negative", "neutral", "positive", "very positive"],
|
|
11
|
+
frequency: ["never", "rarely", "sometimes", "often", "always"],
|
|
12
|
+
agreement: ["strongly disagree", "disagree", "neutral", "agree", "strongly agree"],
|
|
13
|
+
};
|
|
14
|
+
export const RUBRIC_NAMES = Object.keys(RUBRICS);
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ---------- input ----------
|
|
3
|
+
const jsonValue = z.lazy(() => z.union([
|
|
4
|
+
z.string(),
|
|
5
|
+
z.number(),
|
|
6
|
+
z.boolean(),
|
|
7
|
+
z.null(),
|
|
8
|
+
z.array(jsonValue),
|
|
9
|
+
z.record(z.string(), jsonValue),
|
|
10
|
+
]));
|
|
11
|
+
export const stateSchema = z
|
|
12
|
+
.union([z.string(), z.array(jsonValue), z.record(z.string(), jsonValue)])
|
|
13
|
+
.describe("The material to judge: text, a JSON object, or a JSON array.");
|
|
14
|
+
export const questionInputSchema = z.object({
|
|
15
|
+
question: z
|
|
16
|
+
.string()
|
|
17
|
+
.trim()
|
|
18
|
+
.min(1)
|
|
19
|
+
.describe("A free-text question about the state. Jev decides whether it is yes/no, a scale, or a choice."),
|
|
20
|
+
options: z
|
|
21
|
+
.array(z.string().trim().min(1))
|
|
22
|
+
.min(2)
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Named alternatives, when the question has them. Order matters if they form a scale. Omit for yes/no questions or for 'how X' questions that can use a built-in rubric."),
|
|
25
|
+
});
|
|
26
|
+
export const askInputShape = {
|
|
27
|
+
state: stateSchema,
|
|
28
|
+
questions: z
|
|
29
|
+
.array(questionInputSchema)
|
|
30
|
+
.min(1)
|
|
31
|
+
.describe("Questions about the same state, answered in one round trip."),
|
|
32
|
+
};
|
|
33
|
+
export const askInputSchema = z.object(askInputShape);
|
|
34
|
+
// ---------- output ----------
|
|
35
|
+
export const kindSchema = z.enum(["noul", "score", "choice"]);
|
|
36
|
+
const routingSchema = z.object({
|
|
37
|
+
kind: kindSchema.describe("The question type Jev routed this question to."),
|
|
38
|
+
confidence: z.number().describe("Jev's confidence in that routing."),
|
|
39
|
+
rubric: z
|
|
40
|
+
.object({ name: z.string(), confidence: z.number() })
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Present when a built-in rubric was picked for a scale question without options."),
|
|
43
|
+
});
|
|
44
|
+
const noulAnswerSchema = z.object({
|
|
45
|
+
kind: z.literal("noul"),
|
|
46
|
+
answer: z.number().describe("Probability that the answer is yes, from 0 to 1."),
|
|
47
|
+
probabilities: z.object({ yes: z.number(), no: z.number() }),
|
|
48
|
+
routing: routingSchema,
|
|
49
|
+
});
|
|
50
|
+
const scoreAnswerSchema = z.object({
|
|
51
|
+
kind: z.literal("score"),
|
|
52
|
+
answer: z.number().describe("Expected level on the rubric. May fall between integer levels."),
|
|
53
|
+
legend: z.record(z.string(), z.string()).describe("Level index to level description."),
|
|
54
|
+
probabilities: z.record(z.string(), z.number()).describe("Probability per level index."),
|
|
55
|
+
confidence: z.number(),
|
|
56
|
+
rubric: z.string().optional().describe("Name of the built-in rubric, when one was used."),
|
|
57
|
+
note: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("Advice when a built-in rubric was used instead of caller options."),
|
|
61
|
+
routing: routingSchema,
|
|
62
|
+
});
|
|
63
|
+
const choiceAnswerSchema = z.object({
|
|
64
|
+
kind: z.literal("choice"),
|
|
65
|
+
answer: z.string().describe("The selected option."),
|
|
66
|
+
probabilities: z.record(z.string(), z.number()).describe("Probability per option."),
|
|
67
|
+
confidence: z.number(),
|
|
68
|
+
routing: routingSchema,
|
|
69
|
+
});
|
|
70
|
+
const errorAnswerSchema = z.object({
|
|
71
|
+
kind: z.literal("error"),
|
|
72
|
+
message: z.string().describe("Why this question could not be answered, and what to change."),
|
|
73
|
+
routing: routingSchema
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Present when the question was routed before the error was detected."),
|
|
76
|
+
});
|
|
77
|
+
export const answerSchema = z.discriminatedUnion("kind", [
|
|
78
|
+
noulAnswerSchema,
|
|
79
|
+
scoreAnswerSchema,
|
|
80
|
+
choiceAnswerSchema,
|
|
81
|
+
errorAnswerSchema,
|
|
82
|
+
]);
|
|
83
|
+
export const askOutputShape = {
|
|
84
|
+
model: z.string().describe("The Jev model that answered."),
|
|
85
|
+
usage: z
|
|
86
|
+
.object({ input_tokens: z.number(), output_tokens: z.number() })
|
|
87
|
+
.describe("Token usage summed over every Jev call made."),
|
|
88
|
+
answers: z
|
|
89
|
+
.array(answerSchema)
|
|
90
|
+
.describe('One entry per question, in input order. A question that could not be answered has kind "error"; the others are still answered.'),
|
|
91
|
+
};
|
|
92
|
+
export const askOutputSchema = z.object(askOutputShape);
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { APIError, AuthenticationError, TypeSafeError } from "@typesafe-ai/sdk";
|
|
3
|
+
import { ask } from "./ask.js";
|
|
4
|
+
import { RUBRICS } from "./rubrics.js";
|
|
5
|
+
import { askInputShape, askOutputShape } from "./schema.js";
|
|
6
|
+
const rubricLines = Object.entries(RUBRICS)
|
|
7
|
+
.map(([name, levels]) => ` - ${name}: ${levels.join(" < ")}`)
|
|
8
|
+
.join("\n");
|
|
9
|
+
const description = `Ask Jev, a fast non-generative judgment model, one or more questions about some material you already have. Use it for a quick second opinion, a classification, a rating, or a yes/no check instead of reasoning it out yourself or spawning a sub-agent.
|
|
10
|
+
|
|
11
|
+
Write each question in plain language. Jev decides whether it is a yes/no question, a scale, or a choice:
|
|
12
|
+
- Yes/no: "Does this ticket ask for a refund?" You get the probability of yes.
|
|
13
|
+
- Choice: pass "options" with the named alternatives. "Which team owns this?" with options ["billing", "platform"].
|
|
14
|
+
- Scale: pass "options" as ordered levels, lowest first. "How urgent is this?" with options ["can wait", "this week", "today"]. Without options, a built-in rubric is picked for you:
|
|
15
|
+
${rubricLines}
|
|
16
|
+
|
|
17
|
+
Every answer carries probabilities and Jev's confidence, plus "routing" showing how the question was interpreted. Read the confidence: a low value means the material does not settle the question, so add context or decide another way.
|
|
18
|
+
|
|
19
|
+
Requires TYPESAFE_API_KEY in the server's environment.`;
|
|
20
|
+
export function createServer(jev, version) {
|
|
21
|
+
const server = new McpServer({ name: "askjev", version });
|
|
22
|
+
server.registerTool("ask", {
|
|
23
|
+
title: "Ask Jev",
|
|
24
|
+
description,
|
|
25
|
+
inputSchema: askInputShape,
|
|
26
|
+
outputSchema: askOutputShape,
|
|
27
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
28
|
+
}, async (input) => {
|
|
29
|
+
try {
|
|
30
|
+
const output = await ask(jev, input);
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: "text", text: JSON.stringify(output) }],
|
|
33
|
+
structuredContent: output,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return { isError: true, content: [{ type: "text", text: describeError(error) }] };
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return server;
|
|
41
|
+
}
|
|
42
|
+
export function describeError(error) {
|
|
43
|
+
if (error instanceof AuthenticationError) {
|
|
44
|
+
return "Jev rejected the API key. Set TYPESAFE_API_KEY in the environment of the askjev process.";
|
|
45
|
+
}
|
|
46
|
+
if (error instanceof APIError) {
|
|
47
|
+
const id = error.requestId ? ` (request ${error.requestId})` : "";
|
|
48
|
+
return `Jev returned HTTP ${error.status}${id}: ${error.message}`;
|
|
49
|
+
}
|
|
50
|
+
if (error instanceof TypeSafeError || error instanceof Error) {
|
|
51
|
+
return error.message;
|
|
52
|
+
}
|
|
53
|
+
return String(error);
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "askjev",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Unofficial MCP server for Jev, Typesafe AI's System One model. Ask free-text questions; Jev routes them to choice, score, or yes/no and answers with calibrated probabilities.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"jev",
|
|
9
|
+
"typesafe",
|
|
10
|
+
"typesafe-ai",
|
|
11
|
+
"system-one",
|
|
12
|
+
"agents"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Pedro Zaccaria",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/pZacca/askjev.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/pZacca/askjev#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/pZacca/askjev/issues"
|
|
23
|
+
},
|
|
24
|
+
"funding": "https://github.com/sponsors/pZacca",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"askjev": "dist/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.build.json",
|
|
37
|
+
"clean": "rm -rf dist",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"lint": "biome check .",
|
|
40
|
+
"lint:fix": "biome check --write .",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest",
|
|
43
|
+
"eval": "tsx eval/run.ts",
|
|
44
|
+
"smoke": "tsx test/smoke/run.ts",
|
|
45
|
+
"prepack": "npm run clean && npm run build"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
|
+
"@typesafe-ai/sdk": "^0.6.0",
|
|
50
|
+
"zod": "^4.6.5"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@biomejs/biome": "^2.5.14",
|
|
54
|
+
"@types/node": "^22.20.3",
|
|
55
|
+
"tsx": "^4.23.13",
|
|
56
|
+
"typescript": "^7.0.2",
|
|
57
|
+
"vitest": "^5.0.1"
|
|
58
|
+
}
|
|
59
|
+
}
|