Architecture

September 18, 2026 ยท View on GitHub

askjev is an MCP server that gives an agent a fast, cheap judgment call. The agent asks free-text questions about some material it already has. The server asks Jev to decide what kind of question each one is, asks Jev to answer it, and returns calibrated probabilities the agent can act on. No generative model sits in the loop.

Typesafe AI calls Jev a "System One" model: fast, structured decisions rather than prose. This server is the System One path for agents. The agent's own reasoning, or a sub-agent, is the System Two path. The confidence figures in every response are what let the agent pick between the two.

The tool

One tool, ask.

Input

{
  "state": "...",                       // string | object | array: the material to judge
  "questions": [
    { "question": "Is this a bug report?" },
    { "question": "How severe is it?" },
    { "question": "Which team owns it?", "options": ["billing", "platform", "mobile"] },
    { "question": "How urgent is it?",    "options": ["can wait", "this week", "today"] }
  ]
}
  • question is free text. Jev decides whether it is a yes/no question, a scale, or a choice between the given options.
  • options is optional. Pass it when the question has named alternatives. Order matters when the options form a scale.
  • There is no way to force the question type. The whole point is that the agent does not have to think about it.

Output

One entry per question, in input order.

{
  "answers": [
    {
      "kind": "noul",
      "answer": 0.93,                               // probability of "yes"
      "probabilities": { "yes": 0.93, "no": 0.07 },
      "routing": { "kind": "noul", "confidence": 0.98 }
    },
    {
      "kind": "score",
      "answer": 3.4,                                // expected level, may be fractional
      "legend": { "0": "trivial", "1": "minor", "2": "moderate", "3": "major", "4": "critical" },
      "probabilities": { "0": 0.01, "1": 0.04, "2": 0.15, "3": 0.5, "4": 0.3 },
      "confidence": 0.71,
      "rubric": "severity",
      "note": "No options were given, so the built-in 'severity' rubric was used. Pass options for a rubric tailored to your question.",
      "routing": { "kind": "score", "confidence": 0.9, "rubric": { "name": "severity", "confidence": 0.84 } }
    },
    {
      "kind": "choice",
      "answer": "platform",
      "probabilities": { "billing": 0.05, "platform": 0.88, "mobile": 0.07 },
      "confidence": 0.88,
      "routing": { "kind": "choice", "confidence": 0.97 }
    },
    {
      "kind": "score",
      "answer": 1.8,
      "legend": { "0": "can wait", "1": "this week", "2": "today" },
      "probabilities": { "0": 0.1, "1": 0.2, "2": 0.7 },
      "confidence": 0.7,
      "routing": { "kind": "score", "confidence": 0.79 }
    }
  ]
}

A question that cannot be answered does not fail the batch. Its entry has kind: "error" and a message saying what to change, and every other question is still answered:

{
  "kind": "error",
  "message": "\"Which team owns it?\" asks to pick between alternatives, but no options were given. Pass options with the alternatives.",
  "routing": { "kind": "choice", "confidence": 0.95 }
}

Everything is raw. There is no threshold and no verdict. confidence comes straight from Jev; yes/no answers have no separate confidence because the probability is the signal. routing exposes how sure Jev was about the question type, and about the rubric when one was picked, so a misroute is visible rather than silent.

What else comes back is the operator's choice, not the agent's. ASKJEV_INCLUDE is a comma-separated list of the optional parts: routing (the default), usage (tokens summed over every Jev call), and model. Set it to usage,routing to add token counts, or to an empty string to get nothing but the answers. The tool description tells the agent what the server was configured to emit.

Pipeline

flowchart TD
    A[ask: state + questions] --> B[Call 1: route<br/>one Jev call, one choice question per input question]
    B --> C{per question}
    C -->|options given| D{choice or score?}
    C -->|no options| E{noul, score, or<br/>choice-without-options?}
    D -->|choice| F[choice with options as labels]
    D -->|score| G[score with options as ordered rubric]
    E -->|noul| H[noul]
    E -->|score| I[Call 2: pick rubric<br/>one Jev call, one choice question per such input question]
    E -->|choice| X[error: this question needs options]
    I --> J[score with built-in rubric]
    F --> K[Call 3: answer<br/>one Jev call with every typed question]
    G --> K
    H --> K
    J --> K
    K --> L[shape answers + routing metadata + summed usage]

Two Jev calls in the common case, three when at least one question is a scale without options. The number of input questions does not change the number of calls: each step batches every question that needs it into a single systemone request.

Sequence

sequenceDiagram
    participant Agent
    participant askjev
    participant Jev

    Agent->>askjev: ask(state, questions[])
    askjev->>Jev: systemone(state = {q0, q1, ...}, questions = {q0: choice(kind), q1: choice(kind), ...})
    Jev-->>askjev: kind + confidence per question
    opt any scale without options
        askjev->>Jev: systemone(state = {q1, ...}, questions = {q1: choice(rubric), ...})
        Jev-->>askjev: rubric + confidence per question
    end
    askjev->>Jev: systemone(state = agent's state, questions = typed questions)
    Jev-->>askjev: answers + usage
    askjev-->>Agent: answers[] with routing metadata

Routing table

optionsRouter decides betweenBecomes
2+ itemschoice, scorechoice with options as labels, or score with options as ordered rubric
absentnoul, score, choicenoul; score after picking a built-in rubric; or an error asking for options
1 itemrejected by schema validationnever reaches the router

The router's state is the question text itself (an object keyed by question when batching), and its instructions ask which kind of question that text is. The routing criteria are described in plain language so Jev discriminates on intent, not on keywords.

Built-in rubrics

Used only for scale questions that arrive without options. A second Jev call picks the closest one. All have five levels so Jev has room to discriminate without the levels blurring together.

NameLevels, lowest to highest
intensitynot at all, slightly, moderately, very, extremely
qualitypoor, below average, acceptable, good, excellent
severitytrivial, minor, moderate, major, critical
likelihoodvery unlikely, unlikely, uncertain, likely, very likely
sentimentvery negative, negative, neutral, positive, very positive
frequencynever, rarely, sometimes, often, always
agreementstrongly disagree, disagree, neutral, agree, strongly agree

The list is fixed in code. Configurable rubrics are a possible later addition.

Errors

SituationBehaviour
Input fails schema validationTool error with the Zod message. Jev is not called.
Question routed to choice with no optionsThat entry becomes kind: "error" asking for options. The rest of the batch is answered normally.
Jev returns 401Tool error: API key missing or invalid, with the env var name.
Jev returns 422, 429 after retries, 5xxTool error with status, Jev's message, and the request id when present.
Network or timeout after retriesTool error with the SDK's message.

Errors that affect the whole call are returned as MCP tool results with isError: true, not as protocol errors, so the agent sees the message and can recover. Errors that affect one question are entries in answers, so one bad question never costs the agent the others.

Transports

Two entry points share everything from createServer down.

EntryTransportWhere the key comes from
src/cli.tsstdio, one process per clientTYPESAFE_API_KEY in the process environment
src/worker.tsStreamable HTTP on Cloudflare Workers, at https://jev.zacca.dev/mcpx-api-key header, or Authorization: Bearer, per request

The HTTP handler (src/http.ts) is stateless: every request gets a fresh McpServer and transport, replies as JSON rather than SSE, and builds the Typesafe client lazily from the key that request carried. The key is never stored. Initialize and tools/list succeed without a key so registries can scan the server; only ask fails, with a tool error that names the header. The handler is written on web-standard Request and Response, so it also runs on Deno, Bun, or Node 22+ behind any adapter.

The credential hint in the tool description and in auth errors is a createServer option, so each transport tells the agent the right way to supply the key.

Configuration

Local, only what the Typesafe SDK already reads from the environment:

VariableMeaning
TYPESAFE_API_KEYRequired.
TYPESAFE_DEFAULT_MODELOptional, defaults to jev-latest.
TYPESAFE_BASE_URLOptional, for proxies and the smoke test stub.
TYPESAFE_LOG_LEVELOptional. SDK logs go to stderr.

Plus one setting of the server's own:

VariableMeaning
ASKJEV_INCLUDEOptional parts of every result, comma-separated: model, usage, routing. Default routing. Empty string for answers only. An unknown name is a startup error.

stdout carries MCP protocol messages only; every diagnostic goes to stderr.

Hosted, the key travels per request (see Transports). TYPESAFE_BASE_URL, TYPESAFE_DEFAULT_MODEL and ASKJEV_INCLUDE can be set as Worker vars and apply to every caller.

Modules

FileResponsibility
src/cli.tsShebang entry. Builds the server, connects stdio, exits non-zero on startup failure.
src/http.tsWeb-standard HTTP handler: routes /mcp, reads the key per request, serves a stateless server.
src/worker.tsCloudflare Workers entry over http.ts. Bundled by wrangler, not by tsc.
src/server.tsCreates the McpServer, registers ask, maps thrown errors to tool errors.
src/schema.tsZod schemas for the tool input and output, and the TypeScript types derived from them.
src/include.tsParses ASKJEV_INCLUDE and trims the pipeline's full output down to what was configured.
src/ask.tsThe pipeline: route, pick rubrics, answer, shape. Pure orchestration over a Jev adapter.
src/router.tsBuilds the routing and rubric-selection questions and interprets Jev's answers.
src/rubrics.tsThe built-in rubric table.
src/jev.tsThe Jev adapter interface and its SDK-backed implementation.

The Jev adapter is one method, systemOne(request). Tests inject a fake; production wraps TypeSafeClient. Question-assembly tests instead inject a fetch into the real client and assert on the request bodies, so serialization is exercised too.

Decisions and rejected alternatives

One free-text question, routed by Jev, over one tool per primitive. Twenty-odd Jev MCP servers appeared on the day Jev launched. Most expose classify, score, check. The value here is that the agent does not learn Jev's type system.

A minimal structural contract over pure free text. Jev is not generative. It can say a question is a choice but cannot extract the options from the sentence. Passing options is the smallest thing the agent must do to keep a generative model out of the loop.

Raw output over a built-in verdict. A threshold with verdict: "confident" | "uncertain" was considered. Every agent and task has a different cost of being wrong, so the threshold belongs to the caller. Routing metadata is included by default for the same reason: the caller interprets.

Output shape is server configuration, not a call parameter. model, usage and routing are useful to an operator measuring cost or debugging prompts, and noise to an agent deciding what to do next. A per-call include argument was considered and rejected: it would put that choice in front of the agent on every call, cost input tokens to describe, and make results differ between calls for no reason the agent cares about. The operator sets ASKJEV_INCLUDE once; the pipeline always produces the full result and the server trims it, so the trimming has no reach into the routing logic.

No kind override. It would save one call when the agent already knows the type, at the cost of a second code path and an invitation to misuse. If the router eval shows it is needed, it can be added with evidence.

Batched questions over one question per call. One Jev request carries many questions about the same state. That is a structural advantage over a sub-agent and it would be wasted by a single-question tool.

Built-in rubrics chosen by Jev over an instructive error. Most scale questions from an agent are "how X is this", and a generic five-level rubric answers them. The note field says a generic rubric was used so the agent can do better next time.

Per-question errors over failing the batch. A batch is one request, but the questions are independent from the agent's point of view. Failing everything because one question lacked options would make the agent re-send the whole batch. An error entry in the array keeps input and output aligned by position and lets the agent fix one thing.

One tool over ask plus list_rubrics and list_models. Both lists are static and fit in the tool description. Every extra tool costs context in every client that connects.

stdio first, HTTP without touching the pipeline. The first release was stdio only, because remote hosting needs infrastructure and per-user auth. The HTTP transport arrived as a separate entry point over the same createServer; the pipeline did not change.