askjev 0.1.0 → 0.2.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/README.md +269 -20
- package/dist/cli.js +3 -1
- package/dist/http.js +78 -0
- package/dist/include.js +43 -0
- package/dist/schema.js +8 -6
- package/dist/server.js +25 -9
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# askjev
|
|
2
2
|
|
|
3
|
+
[](https://github.com/pZacca/askjev/actions/workflows/ci.yml) [](https://github.com/pZacca/askjev/actions/workflows/eval.yml) [](https://www.npmjs.com/package/askjev) [](https://smithery.ai/servers/pzacca/askjev) [](LICENSE)
|
|
4
|
+
|
|
5
|
+
[](#claude-code) [](#claude-desktop) [](#cursor) [](#codex)
|
|
6
|
+
|
|
3
7
|
Unofficial [MCP](https://modelcontextprotocol.io) server for [Jev](https://docs.typesafe.ai),
|
|
4
8
|
Typesafe AI's System One model. Not affiliated with Typesafe AI.
|
|
5
9
|
|
|
@@ -8,22 +12,52 @@ has, Jev works out whether that is a yes/no, a scale, or a choice, and answers w
|
|
|
8
12
|
calibrated probabilities. No generative model in the loop, one round trip, a fraction of the
|
|
9
13
|
cost and latency of a sub-agent.
|
|
10
14
|
|
|
11
|
-
##
|
|
15
|
+
## Demo
|
|
16
|
+
|
|
17
|
+
An agent planning a workflow, asking Jev one question at a time. Every number is real.
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
scores 100% on the first evaluation dataset. See [docs/EVAL.md](docs/EVAL.md).
|
|
19
|
+

|
|
15
20
|
|
|
16
21
|
## Install
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
You need a Typesafe API key. The server runs in one of two places; every client below
|
|
24
|
+
supports at least one.
|
|
25
|
+
|
|
26
|
+
- **Hosted.** `https://jev.zacca.dev/mcp` runs this repository on Cloudflare Workers. It keeps
|
|
27
|
+
nothing: every call builds a Typesafe client from the key you send and forwards the
|
|
28
|
+
question. Send the key in the `x-api-key` header, or as a bearer token if your client
|
|
29
|
+
only has that field. The same server is listed on Smithery as
|
|
30
|
+
[pzacca/askjev](https://smithery.ai/servers/pzacca/askjev).
|
|
31
|
+
- **Local.** `npx -y askjev` runs it on your machine over stdio. Requires Node 22+ and the
|
|
32
|
+
key in `TYPESAFE_API_KEY`.
|
|
33
|
+
|
|
34
|
+
### Supported clients
|
|
19
35
|
|
|
20
|
-
|
|
36
|
+
| Client | Local (stdio) | Hosted (HTTP) |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| [Claude Code](#claude-code) | yes | yes |
|
|
39
|
+
| [Claude Desktop](#claude-desktop) | yes | no, custom connectors cannot send an API key header |
|
|
40
|
+
| [Cursor](#cursor) | yes | yes |
|
|
41
|
+
| [Codex](#codex) | yes | yes, as a bearer token |
|
|
42
|
+
|
|
43
|
+
Claude Code and Codex were exercised end to end on both transports. Cursor follows its
|
|
44
|
+
documented configuration format.
|
|
45
|
+
|
|
46
|
+
#### Claude Code
|
|
21
47
|
|
|
22
48
|
```sh
|
|
49
|
+
# hosted
|
|
50
|
+
claude mcp add --transport http askjev https://jev.zacca.dev/mcp --header "x-api-key: your-key"
|
|
51
|
+
|
|
52
|
+
# local
|
|
23
53
|
claude mcp add askjev -e TYPESAFE_API_KEY=your-key -- npx -y askjev
|
|
24
54
|
```
|
|
25
55
|
|
|
26
|
-
Claude Desktop
|
|
56
|
+
#### Claude Desktop
|
|
57
|
+
|
|
58
|
+
Open Settings, Developer, Edit Config. The file is
|
|
59
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS and
|
|
60
|
+
`%APPDATA%\Claude\claude_desktop_config.json` on Windows.
|
|
27
61
|
|
|
28
62
|
```json
|
|
29
63
|
{
|
|
@@ -37,31 +71,228 @@ Claude Desktop, Cursor, and other clients that take a JSON config:
|
|
|
37
71
|
}
|
|
38
72
|
```
|
|
39
73
|
|
|
40
|
-
On Windows some hosts cannot launch `npx` directly
|
|
41
|
-
`"args": ["/c", "npx", "-y", "askjev"]`.
|
|
74
|
+
Restart Claude Desktop after saving. On Windows some hosts cannot launch `npx` directly;
|
|
75
|
+
use `"command": "cmd"` with `"args": ["/c", "npx", "-y", "askjev"]`.
|
|
42
76
|
|
|
43
|
-
|
|
77
|
+
#### Cursor
|
|
44
78
|
|
|
45
|
-
|
|
46
|
-
has named alternatives.
|
|
79
|
+
Add to `~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` for one project.
|
|
47
80
|
|
|
48
81
|
```json
|
|
49
82
|
{
|
|
50
|
-
"
|
|
83
|
+
"mcpServers": {
|
|
84
|
+
"askjev": {
|
|
85
|
+
"url": "https://jev.zacca.dev/mcp",
|
|
86
|
+
"headers": { "x-api-key": "your-key" }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
For a local server use the same `command`, `args` and `env` block as Claude Desktop.
|
|
93
|
+
|
|
94
|
+
#### Codex
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
# hosted: Codex reads the key from an environment variable and sends it as a bearer token
|
|
98
|
+
export TYPESAFE_API_KEY=your-key
|
|
99
|
+
codex mcp add askjev --url https://jev.zacca.dev/mcp --bearer-token-env-var TYPESAFE_API_KEY
|
|
100
|
+
|
|
101
|
+
# local
|
|
102
|
+
codex mcp add askjev --env TYPESAFE_API_KEY=your-key -- npx -y askjev
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## The tool
|
|
106
|
+
|
|
107
|
+
One tool, `ask`. It takes the material and a list of free-text questions. Options are only
|
|
108
|
+
needed when the question has named alternatives.
|
|
109
|
+
|
|
110
|
+
### Input
|
|
111
|
+
|
|
112
|
+
```jsonc
|
|
113
|
+
{
|
|
114
|
+
"state": "...", // string | object | array: the material to judge
|
|
51
115
|
"questions": [
|
|
52
|
-
{ "question": "
|
|
53
|
-
{ "question": "How
|
|
54
|
-
{ "question": "Which team
|
|
116
|
+
{ "question": "Is this a bug report?" },
|
|
117
|
+
{ "question": "How severe is it?" },
|
|
118
|
+
{ "question": "Which team owns it?", "options": ["billing", "platform", "mobile"] },
|
|
119
|
+
{ "question": "How urgent is it?", "options": ["can wait", "this week", "today"] }
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- `question` is free text. Jev decides whether it is a yes/no question, a scale, or a
|
|
125
|
+
choice between the given options.
|
|
126
|
+
- `options` is optional. Pass it when the question has named alternatives. Order matters
|
|
127
|
+
when the options form a scale.
|
|
128
|
+
- There is no way to force the question type. The whole point is that the agent does not
|
|
129
|
+
have to think about it.
|
|
130
|
+
|
|
131
|
+
### Output
|
|
132
|
+
|
|
133
|
+
One entry per question, in input order.
|
|
134
|
+
|
|
135
|
+
```jsonc
|
|
136
|
+
{
|
|
137
|
+
"answers": [
|
|
138
|
+
{
|
|
139
|
+
"kind": "noul",
|
|
140
|
+
"answer": 0.93, // probability of "yes"
|
|
141
|
+
"probabilities": { "yes": 0.93, "no": 0.07 },
|
|
142
|
+
"routing": { "kind": "noul", "confidence": 0.98 }
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"kind": "score",
|
|
146
|
+
"answer": 3.4, // expected level, may be fractional
|
|
147
|
+
"legend": { "0": "trivial", "1": "minor", "2": "moderate", "3": "major", "4": "critical" },
|
|
148
|
+
"probabilities": { "0": 0.01, "1": 0.04, "2": 0.15, "3": 0.5, "4": 0.3 },
|
|
149
|
+
"confidence": 0.71,
|
|
150
|
+
"rubric": "severity",
|
|
151
|
+
"note": "No options were given, so the built-in 'severity' rubric was used. Pass options for a rubric tailored to your question.",
|
|
152
|
+
"routing": { "kind": "score", "confidence": 0.9, "rubric": { "name": "severity", "confidence": 0.84 } }
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"kind": "choice",
|
|
156
|
+
"answer": "platform",
|
|
157
|
+
"probabilities": { "billing": 0.05, "platform": 0.88, "mobile": 0.07 },
|
|
158
|
+
"confidence": 0.88,
|
|
159
|
+
"routing": { "kind": "choice", "confidence": 0.97 }
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"kind": "score",
|
|
163
|
+
"answer": 1.8,
|
|
164
|
+
"legend": { "0": "can wait", "1": "this week", "2": "today" },
|
|
165
|
+
"probabilities": { "0": 0.1, "1": 0.2, "2": 0.7 },
|
|
166
|
+
"confidence": 0.7,
|
|
167
|
+
"routing": { "kind": "score", "confidence": 0.79 }
|
|
168
|
+
}
|
|
55
169
|
]
|
|
56
170
|
}
|
|
57
171
|
```
|
|
58
172
|
|
|
59
|
-
|
|
60
|
-
|
|
173
|
+
A question that cannot be answered does not fail the batch. Its entry has `kind: "error"`
|
|
174
|
+
and a message saying what to change, and every other question is still answered:
|
|
175
|
+
|
|
176
|
+
```jsonc
|
|
177
|
+
{
|
|
178
|
+
"kind": "error",
|
|
179
|
+
"message": "\"Which team owns it?\" asks to pick between alternatives, but no options were given. Pass options with the alternatives.",
|
|
180
|
+
"routing": { "kind": "choice", "confidence": 0.95 }
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Everything is raw. There is no threshold and no verdict. `confidence` comes straight from
|
|
185
|
+
Jev; yes/no answers have no separate confidence because the probability is the signal.
|
|
186
|
+
`routing` exposes how sure Jev was about the question type, and about the rubric when one
|
|
187
|
+
was picked, so a misroute is visible rather than silent.
|
|
188
|
+
|
|
189
|
+
What else comes back is the operator's choice, not the agent's. `ASKJEV_INCLUDE` is a
|
|
190
|
+
comma-separated list of the optional parts: `routing` (the default), `usage` (tokens summed
|
|
191
|
+
over every Jev call), and `model`. Set it to `usage,routing` to add token counts, or to an
|
|
192
|
+
empty string to get nothing but the answers. The tool description tells the agent what the
|
|
193
|
+
server was configured to emit.
|
|
194
|
+
|
|
195
|
+
## How it works
|
|
196
|
+
|
|
197
|
+
```mermaid
|
|
198
|
+
flowchart TD
|
|
199
|
+
A[ask: state + questions] --> B[Call 1: route<br/>one Jev call, one choice question per input question]
|
|
200
|
+
B --> C{per question}
|
|
201
|
+
C -->|options given| D{choice or score?}
|
|
202
|
+
C -->|no options| E{noul, score, or<br/>choice-without-options?}
|
|
203
|
+
D -->|choice| F[choice with options as labels]
|
|
204
|
+
D -->|score| G[score with options as ordered rubric]
|
|
205
|
+
E -->|noul| H[noul]
|
|
206
|
+
E -->|score| I[Call 2: pick rubric<br/>one Jev call, one choice question per such input question]
|
|
207
|
+
E -->|choice| X[error: this question needs options]
|
|
208
|
+
I --> J[score with built-in rubric]
|
|
209
|
+
F --> K[Call 3: answer<br/>one Jev call with every typed question]
|
|
210
|
+
G --> K
|
|
211
|
+
H --> K
|
|
212
|
+
J --> K
|
|
213
|
+
K --> L[shape answers + routing metadata + summed usage]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Two Jev calls in the common case, three when at least one question is a scale without
|
|
217
|
+
options. The number of input questions does not change the number of calls: each step
|
|
218
|
+
batches every question that needs it into a single `systemone` request.
|
|
219
|
+
|
|
220
|
+
### Routing table
|
|
221
|
+
|
|
222
|
+
| `options` | Router decides between | Becomes |
|
|
223
|
+
|---|---|---|
|
|
224
|
+
| 2+ items | choice, score | `choice` with options as labels, or `score` with options as ordered rubric |
|
|
225
|
+
| absent | noul, score, choice | `noul`; `score` after picking a built-in rubric; or an error asking for options |
|
|
226
|
+
| 1 item | rejected by schema validation | never reaches the router |
|
|
227
|
+
|
|
228
|
+
The router's `state` is the question text itself (an object keyed by question when
|
|
229
|
+
batching), and its instructions ask which kind of question that text is. The routing
|
|
230
|
+
criteria are described in plain language so Jev discriminates on intent, not on keywords.
|
|
231
|
+
|
|
232
|
+
### Built-in rubrics
|
|
233
|
+
|
|
234
|
+
Used only for scale questions that arrive without options. A second Jev call picks the
|
|
235
|
+
closest one. All have five levels so Jev has room to discriminate without the levels
|
|
236
|
+
blurring together.
|
|
237
|
+
|
|
238
|
+
| Name | Levels, lowest to highest |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `intensity` | not at all, slightly, moderately, very, extremely |
|
|
241
|
+
| `quality` | poor, below average, acceptable, good, excellent |
|
|
242
|
+
| `severity` | trivial, minor, moderate, major, critical |
|
|
243
|
+
| `likelihood` | very unlikely, unlikely, uncertain, likely, very likely |
|
|
244
|
+
| `sentiment` | very negative, negative, neutral, positive, very positive |
|
|
245
|
+
| `frequency` | never, rarely, sometimes, often, always |
|
|
246
|
+
| `agreement` | strongly disagree, disagree, neutral, agree, strongly agree |
|
|
247
|
+
|
|
248
|
+
The list is fixed in code. Configurable rubrics are a possible later addition.
|
|
61
249
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
250
|
+
## Errors
|
|
251
|
+
|
|
252
|
+
| Situation | Behaviour |
|
|
253
|
+
|---|---|
|
|
254
|
+
| Input fails schema validation | Tool error with the Zod message. Jev is not called. |
|
|
255
|
+
| Question routed to `choice` with no options | That entry becomes `kind: "error"` asking for `options`. The rest of the batch is answered normally. |
|
|
256
|
+
| Jev returns 401 | Tool error: API key missing or invalid, with the env var name. |
|
|
257
|
+
| Jev returns 422, 429 after retries, 5xx | Tool error with status, Jev's message, and the request id when present. |
|
|
258
|
+
| Network or timeout after retries | Tool error with the SDK's message. |
|
|
259
|
+
|
|
260
|
+
Errors that affect the whole call are returned as MCP tool results with `isError: true`,
|
|
261
|
+
not as protocol errors, so the agent sees the message and can recover. Errors that affect
|
|
262
|
+
one question are entries in `answers`, so one bad question never costs the agent the
|
|
263
|
+
others.
|
|
264
|
+
|
|
265
|
+
## Configuration
|
|
266
|
+
|
|
267
|
+
Local, only what the Typesafe SDK already reads from the environment:
|
|
268
|
+
|
|
269
|
+
| Variable | Meaning |
|
|
270
|
+
|---|---|
|
|
271
|
+
| `TYPESAFE_API_KEY` | Required. |
|
|
272
|
+
| `TYPESAFE_DEFAULT_MODEL` | Optional, defaults to `jev-latest`. |
|
|
273
|
+
| `TYPESAFE_BASE_URL` | Optional, for proxies and the smoke test stub. |
|
|
274
|
+
| `TYPESAFE_LOG_LEVEL` | Optional. SDK logs go to stderr. |
|
|
275
|
+
|
|
276
|
+
Plus one setting of the server's own:
|
|
277
|
+
|
|
278
|
+
| Variable | Meaning |
|
|
279
|
+
|---|---|
|
|
280
|
+
| `ASKJEV_INCLUDE` | Optional parts of every result, comma-separated: `model`, `usage`, `routing`. Default `routing`. Empty string for answers only. An unknown name is a startup error. |
|
|
281
|
+
|
|
282
|
+
stdout carries MCP protocol messages only; every diagnostic goes to stderr.
|
|
283
|
+
|
|
284
|
+
Hosted, the key travels per request in the `x-api-key` header (or `Authorization:
|
|
285
|
+
Bearer`). `TYPESAFE_BASE_URL`, `TYPESAFE_DEFAULT_MODEL` and `ASKJEV_INCLUDE` can be set as
|
|
286
|
+
Worker vars and apply to every caller. The public instance at jev.zacca.dev runs the
|
|
287
|
+
defaults.
|
|
288
|
+
|
|
289
|
+
## Evaluation
|
|
290
|
+
|
|
291
|
+
The router is the part that can be wrong in a way unit tests cannot catch. A versioned
|
|
292
|
+
dataset of human-labelled questions runs against the live API weekly and on demand; the
|
|
293
|
+
first run scored 42/42 on routing and 16/16 on rubric selection. See
|
|
294
|
+
[docs/EVAL.md](docs/EVAL.md). Design decisions and rejected alternatives are in
|
|
295
|
+
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
65
296
|
|
|
66
297
|
## Development
|
|
67
298
|
|
|
@@ -72,6 +303,24 @@ npm run typecheck
|
|
|
72
303
|
npm test
|
|
73
304
|
```
|
|
74
305
|
|
|
306
|
+
The hosted variant is `src/worker.ts`, bundled and deployed by wrangler from
|
|
307
|
+
`wrangler.jsonc`. `npm run dev` serves it on localhost. Every push to `main` that passes
|
|
308
|
+
CI is deployed to jev.zacca.dev by the `deploy` job in `ci.yml`; `npm run deploy` does
|
|
309
|
+
the same by hand (needs `wrangler login`).
|
|
310
|
+
|
|
311
|
+
Releases are tag-driven. Bump the version in `package.json`, commit, then:
|
|
312
|
+
|
|
313
|
+
```sh
|
|
314
|
+
git tag v0.2.0 && git push origin main v0.2.0
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
`release.yml` checks that the tag matches `package.json`, runs the full test suite,
|
|
318
|
+
publishes to npm with provenance, and creates the GitHub release with generated notes.
|
|
319
|
+
|
|
320
|
+
The Smithery listing points at that domain and takes its configuration form from
|
|
321
|
+
`smithery.schema.json`. After changing the schema, republish with
|
|
322
|
+
`smithery mcp publish https://jev.zacca.dev/mcp -n pzacca/askjev --config-schema smithery.schema.json`.
|
|
323
|
+
|
|
75
324
|
## License
|
|
76
325
|
|
|
77
326
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { INCLUDE_ENV, parseInclude } from "./include.js";
|
|
4
5
|
import { createJev } from "./jev.js";
|
|
5
6
|
import { createServer } from "./server.js";
|
|
6
7
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
7
8
|
async function main() {
|
|
9
|
+
const include = parseInclude(process.env[INCLUDE_ENV]);
|
|
8
10
|
const jev = createJev();
|
|
9
|
-
const server = createServer(jev, version);
|
|
11
|
+
const server = createServer(jev, version, { include });
|
|
10
12
|
await server.connect(new StdioServerTransport());
|
|
11
13
|
}
|
|
12
14
|
main().catch((error) => {
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
2
|
+
import { createJev } from "./jev.js";
|
|
3
|
+
import { createServer } from "./server.js";
|
|
4
|
+
/** Header that carries the caller's Typesafe API key. */
|
|
5
|
+
export const API_KEY_HEADER = "x-api-key";
|
|
6
|
+
/** How a remote caller supplies the key; shown in the tool description and in auth errors. */
|
|
7
|
+
export const KEY_HINT = `Send your Typesafe API key in the ${API_KEY_HEADER} header, or as a bearer token in Authorization.`;
|
|
8
|
+
/**
|
|
9
|
+
* The remote, multi-tenant entry point over Streamable HTTP, on web-standard Request and
|
|
10
|
+
* Response so it runs on Cloudflare Workers, Deno, Bun, or Node 22+.
|
|
11
|
+
*
|
|
12
|
+
* Stateless: every request gets a fresh server and transport, and the Typesafe client is
|
|
13
|
+
* built from the key that request carried. The key is never stored. Initialize and
|
|
14
|
+
* tools/list work without a key so registries can scan the server; only `ask` needs one.
|
|
15
|
+
*/
|
|
16
|
+
export function createHttpHandler(options) {
|
|
17
|
+
const path = options.path ?? "/mcp";
|
|
18
|
+
const jevFor = options.jevFor ??
|
|
19
|
+
((apiKey) => createJev({
|
|
20
|
+
apiKey,
|
|
21
|
+
...(options.baseURL ? { baseURL: options.baseURL } : {}),
|
|
22
|
+
...(options.defaultModel ? { defaultModel: options.defaultModel } : {}),
|
|
23
|
+
}));
|
|
24
|
+
return async (request) => {
|
|
25
|
+
const url = new URL(request.url);
|
|
26
|
+
if (url.pathname === "/" && request.method === "GET") {
|
|
27
|
+
return Response.json({
|
|
28
|
+
name: "askjev",
|
|
29
|
+
version: options.version,
|
|
30
|
+
mcp: path,
|
|
31
|
+
auth: KEY_HINT,
|
|
32
|
+
docs: "https://github.com/pZacca/askjev",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (url.pathname !== path) {
|
|
36
|
+
return new Response("Not found", { status: 404 });
|
|
37
|
+
}
|
|
38
|
+
// Stateless: no sessions to resume (GET) or terminate (DELETE). A GET would otherwise open
|
|
39
|
+
// an SSE stream that nothing can ever write to, and hold it open with keep-alives.
|
|
40
|
+
if (request.method !== "POST") {
|
|
41
|
+
return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
|
|
42
|
+
}
|
|
43
|
+
const apiKey = readApiKey(request.headers);
|
|
44
|
+
const jev = apiKey ? lazyJev(() => jevFor(apiKey)) : missingKeyJev;
|
|
45
|
+
const server = createServer(jev, options.version, {
|
|
46
|
+
keyHint: KEY_HINT,
|
|
47
|
+
...(options.include ? { include: options.include } : {}),
|
|
48
|
+
});
|
|
49
|
+
// No sessionIdGenerator: stateless, one transport per request.
|
|
50
|
+
const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
|
|
51
|
+
await server.connect(transport);
|
|
52
|
+
return transport.handleRequest(request);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** The dedicated header wins; a bearer token is accepted for clients that only have that field. */
|
|
56
|
+
export function readApiKey(headers) {
|
|
57
|
+
const direct = headers.get(API_KEY_HEADER)?.trim();
|
|
58
|
+
if (direct)
|
|
59
|
+
return direct;
|
|
60
|
+
const auth = headers.get("authorization")?.trim();
|
|
61
|
+
const match = auth?.match(/^bearer\s+(.+)$/i);
|
|
62
|
+
return match?.[1]?.trim() || undefined;
|
|
63
|
+
}
|
|
64
|
+
/** Builds the client on first use, so initialize and tools/list never construct one. */
|
|
65
|
+
function lazyJev(build) {
|
|
66
|
+
let jev;
|
|
67
|
+
return {
|
|
68
|
+
systemOne: (request) => {
|
|
69
|
+
jev ??= build();
|
|
70
|
+
return jev.systemOne(request);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const missingKeyJev = {
|
|
75
|
+
systemOne: async () => {
|
|
76
|
+
throw new Error(`No Typesafe API key was sent. ${KEY_HINT}`);
|
|
77
|
+
},
|
|
78
|
+
};
|
package/dist/include.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Optional parts of the result. The operator picks them once, in configuration. */
|
|
2
|
+
export const INCLUDE_FIELDS = ["model", "usage", "routing"];
|
|
3
|
+
/** What goes out when nothing is configured: the answers, plus how each was interpreted. */
|
|
4
|
+
export const DEFAULT_INCLUDE = ["routing"];
|
|
5
|
+
/** Environment variable that overrides the default, as a comma-separated list. */
|
|
6
|
+
export const INCLUDE_ENV = "ASKJEV_INCLUDE";
|
|
7
|
+
/**
|
|
8
|
+
* Parses `ASKJEV_INCLUDE`. Unset keeps the default; an empty string strips everything
|
|
9
|
+
* optional; unknown names are a startup error, not a silent no-op.
|
|
10
|
+
*/
|
|
11
|
+
export function parseInclude(raw) {
|
|
12
|
+
if (raw === undefined)
|
|
13
|
+
return [...DEFAULT_INCLUDE];
|
|
14
|
+
const fields = new Set();
|
|
15
|
+
for (const part of raw.split(",")) {
|
|
16
|
+
const name = part.trim().toLowerCase();
|
|
17
|
+
if (name === "")
|
|
18
|
+
continue;
|
|
19
|
+
if (!isIncludeField(name)) {
|
|
20
|
+
throw new Error(`${INCLUDE_ENV}: unknown field "${part.trim()}". Valid fields: ${INCLUDE_FIELDS.join(", ")}.`);
|
|
21
|
+
}
|
|
22
|
+
fields.add(name);
|
|
23
|
+
}
|
|
24
|
+
return INCLUDE_FIELDS.filter((f) => fields.has(f));
|
|
25
|
+
}
|
|
26
|
+
function isIncludeField(name) {
|
|
27
|
+
return INCLUDE_FIELDS.includes(name);
|
|
28
|
+
}
|
|
29
|
+
/** Drops every optional part that is not in `include`. The pipeline always produces them all. */
|
|
30
|
+
export function trimOutput(output, include) {
|
|
31
|
+
const keep = new Set(include);
|
|
32
|
+
const { model, usage, answers } = output;
|
|
33
|
+
return {
|
|
34
|
+
...(keep.has("model") && model !== undefined ? { model } : {}),
|
|
35
|
+
...(keep.has("usage") && usage !== undefined ? { usage } : {}),
|
|
36
|
+
answers: keep.has("routing")
|
|
37
|
+
? answers
|
|
38
|
+
: answers.map((a) => {
|
|
39
|
+
const { routing: _routing, ...rest } = a;
|
|
40
|
+
return rest;
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/schema.js
CHANGED
|
@@ -33,6 +33,7 @@ export const askInputShape = {
|
|
|
33
33
|
export const askInputSchema = z.object(askInputShape);
|
|
34
34
|
// ---------- output ----------
|
|
35
35
|
export const kindSchema = z.enum(["noul", "score", "choice"]);
|
|
36
|
+
/** How Jev interpreted the question. Emitted when the server is configured to include routing. */
|
|
36
37
|
const routingSchema = z.object({
|
|
37
38
|
kind: kindSchema.describe("The question type Jev routed this question to."),
|
|
38
39
|
confidence: z.number().describe("Jev's confidence in that routing."),
|
|
@@ -45,7 +46,7 @@ const noulAnswerSchema = z.object({
|
|
|
45
46
|
kind: z.literal("noul"),
|
|
46
47
|
answer: z.number().describe("Probability that the answer is yes, from 0 to 1."),
|
|
47
48
|
probabilities: z.object({ yes: z.number(), no: z.number() }),
|
|
48
|
-
routing: routingSchema,
|
|
49
|
+
routing: routingSchema.optional(),
|
|
49
50
|
});
|
|
50
51
|
const scoreAnswerSchema = z.object({
|
|
51
52
|
kind: z.literal("score"),
|
|
@@ -58,21 +59,21 @@ const scoreAnswerSchema = z.object({
|
|
|
58
59
|
.string()
|
|
59
60
|
.optional()
|
|
60
61
|
.describe("Advice when a built-in rubric was used instead of caller options."),
|
|
61
|
-
routing: routingSchema,
|
|
62
|
+
routing: routingSchema.optional(),
|
|
62
63
|
});
|
|
63
64
|
const choiceAnswerSchema = z.object({
|
|
64
65
|
kind: z.literal("choice"),
|
|
65
66
|
answer: z.string().describe("The selected option."),
|
|
66
67
|
probabilities: z.record(z.string(), z.number()).describe("Probability per option."),
|
|
67
68
|
confidence: z.number(),
|
|
68
|
-
routing: routingSchema,
|
|
69
|
+
routing: routingSchema.optional(),
|
|
69
70
|
});
|
|
70
71
|
const errorAnswerSchema = z.object({
|
|
71
72
|
kind: z.literal("error"),
|
|
72
73
|
message: z.string().describe("Why this question could not be answered, and what to change."),
|
|
73
74
|
routing: routingSchema
|
|
74
75
|
.optional()
|
|
75
|
-
.describe("Present when the question was routed before the error was detected."),
|
|
76
|
+
.describe("Present when routing is configured and the question was routed before the error was detected."),
|
|
76
77
|
});
|
|
77
78
|
export const answerSchema = z.discriminatedUnion("kind", [
|
|
78
79
|
noulAnswerSchema,
|
|
@@ -81,10 +82,11 @@ export const answerSchema = z.discriminatedUnion("kind", [
|
|
|
81
82
|
errorAnswerSchema,
|
|
82
83
|
]);
|
|
83
84
|
export const askOutputShape = {
|
|
84
|
-
model: z.string().describe("The Jev model that answered."),
|
|
85
|
+
model: z.string().optional().describe("The Jev model that answered. Present when configured."),
|
|
85
86
|
usage: z
|
|
86
87
|
.object({ input_tokens: z.number(), output_tokens: z.number() })
|
|
87
|
-
.
|
|
88
|
+
.optional()
|
|
89
|
+
.describe("Token usage summed over every Jev call made. Present when configured."),
|
|
88
90
|
answers: z
|
|
89
91
|
.array(answerSchema)
|
|
90
92
|
.describe('One entry per question, in input order. A question that could not be answered has kind "error"; the others are still answered.'),
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { APIError, AuthenticationError, TypeSafeError } from "@typesafe-ai/sdk";
|
|
3
3
|
import { ask } from "./ask.js";
|
|
4
|
+
import { DEFAULT_INCLUDE, trimOutput } from "./include.js";
|
|
4
5
|
import { RUBRICS } from "./rubrics.js";
|
|
5
6
|
import { askInputShape, askOutputShape } from "./schema.js";
|
|
6
7
|
const rubricLines = Object.entries(RUBRICS)
|
|
@@ -14,34 +15,49 @@ Write each question in plain language. Jev decides whether it is a yes/no questi
|
|
|
14
15
|
- 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
16
|
${rubricLines}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
`;
|
|
19
|
+
/** The sentence about the result shape depends on what the operator chose to include. */
|
|
20
|
+
function describeOutput(include) {
|
|
21
|
+
const keep = new Set(include);
|
|
22
|
+
const parts = ["Every answer carries probabilities and Jev's confidence"];
|
|
23
|
+
if (keep.has("routing")) {
|
|
24
|
+
parts.push('plus "routing" showing how the question was interpreted and how sure Jev was about that');
|
|
25
|
+
}
|
|
26
|
+
const extras = [keep.has("model") ? '"model"' : "", keep.has("usage") ? '"usage" (tokens)' : ""]
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.join(" and ");
|
|
29
|
+
const extraLine = extras ? ` The result also carries ${extras}.` : "";
|
|
30
|
+
return `${parts.join(", ")}.${extraLine} Read the confidence: a low value means the material does not settle the question, so add context or decide another way.`;
|
|
31
|
+
}
|
|
32
|
+
/** How the caller supplies the Typesafe API key when nothing else is said: the stdio way. */
|
|
33
|
+
export const ENV_KEY_HINT = "Set TYPESAFE_API_KEY in the environment of the askjev process.";
|
|
34
|
+
export function createServer(jev, version, options = {}) {
|
|
35
|
+
const keyHint = options.keyHint ?? ENV_KEY_HINT;
|
|
36
|
+
const include = options.include ?? DEFAULT_INCLUDE;
|
|
21
37
|
const server = new McpServer({ name: "askjev", version });
|
|
22
38
|
server.registerTool("ask", {
|
|
23
39
|
title: "Ask Jev",
|
|
24
|
-
description
|
|
40
|
+
description: `${description}\n${describeOutput(include)}\n\nRequires a Typesafe API key. ${keyHint}`,
|
|
25
41
|
inputSchema: askInputShape,
|
|
26
42
|
outputSchema: askOutputShape,
|
|
27
43
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
28
44
|
}, async (input) => {
|
|
29
45
|
try {
|
|
30
|
-
const output = await ask(jev, input);
|
|
46
|
+
const output = trimOutput(await ask(jev, input), include);
|
|
31
47
|
return {
|
|
32
48
|
content: [{ type: "text", text: JSON.stringify(output) }],
|
|
33
49
|
structuredContent: output,
|
|
34
50
|
};
|
|
35
51
|
}
|
|
36
52
|
catch (error) {
|
|
37
|
-
return { isError: true, content: [{ type: "text", text: describeError(error) }] };
|
|
53
|
+
return { isError: true, content: [{ type: "text", text: describeError(error, keyHint) }] };
|
|
38
54
|
}
|
|
39
55
|
});
|
|
40
56
|
return server;
|
|
41
57
|
}
|
|
42
|
-
export function describeError(error) {
|
|
58
|
+
export function describeError(error, keyHint = ENV_KEY_HINT) {
|
|
43
59
|
if (error instanceof AuthenticationError) {
|
|
44
|
-
return
|
|
60
|
+
return `Jev rejected the API key. ${keyHint}`;
|
|
45
61
|
}
|
|
46
62
|
if (error instanceof APIError) {
|
|
47
63
|
const id = error.requestId ? ` (request ${error.requestId})` : "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "askjev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
"test:watch": "vitest",
|
|
43
43
|
"eval": "tsx eval/run.ts",
|
|
44
44
|
"smoke": "tsx test/smoke/run.ts",
|
|
45
|
+
"dev": "wrangler dev --define \"ASKJEV_VERSION:'$npm_package_version'\"",
|
|
46
|
+
"deploy": "wrangler deploy --define \"ASKJEV_VERSION:'$npm_package_version'\"",
|
|
45
47
|
"prepack": "npm run clean && npm run build"
|
|
46
48
|
},
|
|
47
49
|
"dependencies": {
|
|
@@ -54,6 +56,10 @@
|
|
|
54
56
|
"@types/node": "^22.20.3",
|
|
55
57
|
"tsx": "^4.23.13",
|
|
56
58
|
"typescript": "^7.0.2",
|
|
57
|
-
"vitest": "^5.0.1"
|
|
59
|
+
"vitest": "^5.0.1",
|
|
60
|
+
"wrangler": "^4.134.0"
|
|
61
|
+
},
|
|
62
|
+
"allowScripts": {
|
|
63
|
+
"workerd@1.20260917.1": true
|
|
58
64
|
}
|
|
59
65
|
}
|