An MCP server for TypeSafe's System One API (Jev). You hand it an array of
choice/description pairs and some state; it makes the POST /v1/systemone call and returns the
typed answer — the selected option, the probability of every option, and a confidence score.
Built on TypeSafe's official @typesafe-ai/sdk, so the
wire format, retries and error classes are theirs rather than reimplemented here.
| Tool | What it does | Returns |
|---|---|---|
ask |
One or more typed questions over one state, in a single call | answers keyed by your ids |
list_models |
Models this account can use | models[] |
ask takes a questions array; each entry has an id you choose and a type:
| Type | What it does | The answer carries |
|---|---|---|
choice |
Pick one option from a set you define | choice, probabilities, confidence |
noul |
Yes/no question | noul (0–1 probability of yes) |
score |
Rate against ordered levels | score, legend, probabilities, confidence |
The response also carries the resolving model and token usage. ask takes an optional model
to override the server default for that call.
score is in legend-index units, not 0–1. With three levels it runs 0–2, and it can land
between levels — 1.03 against ["Calm", "Frustrated", "Very angry"] means "Frustrated". Round it
to name a level; divide by levels.length - 1 first if you want a fraction.
A choice needs at least 2 options and a score at least 2 levels; fewer is rejected by the input
schema before any API call. Question ids must be unique within a call.
Ask everything you want to know about a state in one call rather than several — TypeSafe bills per input token, so batching reuses the state instead of resending it per question, and is barely slower than a single question.
Node 20+ (the v2 SDK's floor).
npm install
npm run buildBuilt on SDK v2 (@modelcontextprotocol/server), serving protocol revision 2026-07-28 via
serveStdio. Clients that still open with the 2025 initialize handshake are served too — the
entry point handles both eras, so older hosts keep working.
Set TYPESAFE_API_KEY. Optional: TYPESAFE_BASE_URL (default https://api.typesafe.ai) and
TYPESAFE_DEFAULT_MODEL (default jev-latest), both read by the SDK. TYPESAFE_MODEL is this
server's older name for the model default and still wins when set.
Claude Code:
claude mcp add askjev --env TYPESAFE_API_KEY=sk-... -- node /home/cb/TOOLBOX/MCP/AskJev-MCP/dist/index.jsOr in mcp.json / claude_desktop_config.json:
{
"mcpServers": {
"askjev": {
"command": "node",
"args": ["/home/cb/TOOLBOX/MCP/AskJev-MCP/dist/index.js"],
"env": { "TYPESAFE_API_KEY": "sk-..." }
}
}
}A single question. Choice options are either plain strings or {option, description} pairs;
descriptions are the rubric and usually improve the answer:
{
"state": "Help! My payouts have been failing for 3 days.",
"questions": [
{
"id": "dept",
"type": "choice",
"instructions": "Which team should handle this?",
"options": [
{ "option": "billing", "description": "Payments, invoicing, refunds" },
{ "option": "technical", "description": "Bugs, outages, integrations" },
"sales"
]
}
]
}{
"answers": {
"dept": {
"type": "choice",
"choice": "technical",
"probabilities": { "billing": 0.08, "technical": 0.85, "sales": 0.07 },
"confidence": 0.82
}
},
"model": "jev-1.13.0",
"usage": { "input_tokens": 312, "output_tokens": 48 }
}Mixed question types over one state — the case ask exists for:
{
"state": { "ticket": "Help! My payouts have been failing for 3 days." },
"questions": [
{ "id": "dept", "type": "choice", "instructions": "Which team?",
"options": ["billing", "technical", "sales"] },
{ "id": "urgent", "type": "noul", "instructions": "Does this convey urgency?",
"criteria": { "true": "Explicitly time-sensitive", "false": "No urgency expressed" } },
{ "id": "frustration", "type": "score", "instructions": "How frustrated is the customer?",
"levels": ["Calm", "Frustrated", "Very angry"] }
]
}state accepts a string or any JSON structure (chat logs, records, application state).
confidence is a separate axis from the answer: the answer tells you what, confidence tells you
whether to act. Gate on it — act automatically above your threshold, route to a human below it.
API failures come back as isError tool results carrying the status and body.
Retries are the SDK's RetryPolicy defaults: 408, 429 and 500–599 plus connection and
timeout failures, 2 retries after the first attempt, 500ms backoff doubling to 5s with 25% jitter,
honoring Retry-After and retry-after-ms up to 60s. Per-attempt timeout is 10s with no total
budget.
Invalid arguments are rejected by the input schema before any request is made, and surface as a
protocol error rather than an isError result.
src/schemas.ts— zod input schemas and the mapping from caller-friendly options/levels to the SDK'scriteriasrc/index.ts— MCP server and tool registration;buildServer()is the per-connection factoryserveStdiopins to each connectionscripts/lib/mcp-client.mjs— the SDK client the harness scripts share, negotiating the era withmode: 'auto'
scripts/vault-test.mjs runs real notes from 40-RESOURCES through the server over stdio — the
same path a client takes, so it exercises the MCP layer and not just the HTTP client.
Each note becomes one batched ask call with three questions: a choice (primary topic, drawn
from the vault's own tag taxonomy), a noul (is it actionable?), and a score (how deep?).
The note's hand-written tags: act as ground truth, so the script reports how often Jev's pick
agrees with yours, split by whether confidence cleared 0.6.
export TYPESAFE_API_KEY=$(grep -oP '(?<=TYPESAFE_API_KEY=")[^"]+' ~/.bashrc)
node scripts/vault-test.mjs --n 8 --dry # show the extracted state, call nothing
node scripts/vault-test.mjs --n 8 # send to Jev
node scripts/vault-test.mjs --n 20 --dir /mnt/d/OBS/brncx-skills/30-CLIENTSOnly the title and TL;DR are sent, not the whole note — roughly 700 input tokens per note for all
three questions. Adjust the TOPICS table at the top of the script to change the taxonomy; each
entry maps an option to the vault tags that count as a match.
scripts/content-scorer-bench.mjs compares the vault's content-scorer skill against Jev on the
two dimensions that are judgment calls rather than counts.
seo_content_analyzer.py score computes Humanity (30%) and Specificity (25%) from a phrase
blacklist and regex counts — 55% of the composite. This script asks Jev the same two questions as
score questions, then checks which scorer better separates what you published from what you left
in draft, using the status: frontmatter as ground truth.
export TYPESAFE_API_KEY=$(grep -oP '(?<=TYPESAFE_API_KEY=")[^"]+' ~/.bashrc)
node scripts/content-scorer-bench.mjs --n 24 --dry # list the sample, call nothing
node scripts/content-scorer-bench.mjs --n 24 # score it
node scripts/content-scorer-bench.mjs --n 30 --out rows.jsonThree scorers are reported: analyzer (today's composite), jev (Jev's two dimensions alone), and
hybrid (Jev for judgment, the analyzer's arithmetic for structure/SEO/readability). The headline
metric is AUC — the chance a random published document outscores a random draft, where 0.5 is a
coin flip.
--words caps how much of each document is sent (default 1200); sd in the summary is the spread
of a dimension across the sample, which is how a saturated dimension gives itself away.
Result on 30 marketing documents: no scorer separated published from draft — analyzer AUC 0.52,
Jev 0.52, hybrid 0.49. status: draft turns out not to be a quality label, so this corpus can't
settle the question. What it did confirm is the saturation: analyzer Specificity has sd 3.1 around
a mean of 99, versus sd 15.8 for Jev's.
To benchmark against real quality labels instead of publish status:
node scripts/make-grading-sheet.mjs --n 30 # writes the sheet into the vault
# fill the grade column in Obsidian: ship / fix / scrap
node scripts/content-scorer-bench.mjs --labelsmake-grading-sheet.mjs draws a sample balanced on publish status and spread across content type
(at most --max-per-type from any one folder), shuffles it, and writes it to
99-TMP/content-grading-sheet.md with no scores and no status: — grading blind is the point, so
the labels stay independent of what the analyzer thought. grading-key.json in the repo root holds
the mapping back to paths and is what --labels reads; it is gitignored, since it lists paths and
titles from a private vault.
With --labels the bench reports two splits, ship vs scrap and ship vs fix+scrap. Rows left
blank in the sheet are dropped, so a partly-filled sheet still runs.
Both tools are verified working against the live API over stdio, on both the 2026-07-28 and
2025 eras, including the error paths (bad key → 401 as an isError result; bad arguments →
schema rejection; missing TYPESAFE_API_KEY → refuses to start).
What is not covered:
- No automated tests. There is no
npm test. The three scripts underscripts/are live integration checks that cost API tokens, which makes them useful by hand and useless in CI. @typesafe-ai/sdkis pre-1.0 (0.6.0at time of writing), so expect breaking changes on upgrade. The dependency is range-pinned to^0.6.0for that reason.noulquestions carry noconfidence. The API returns none for them, so unlike choice and score there is no gate to threshold on — only thenoulprobability itself.