token-optimizer-jev-mcp
# token-optimizer-jev-mcp
A read-only [Model Context Protocol](https://modelcontextprotocol.io) server for
[TypeSafe Jev](https://docs.typesafe.ai) — the System One model that answers **typed
questions** instead of writing prose.
Jev returns a decision your code can consume directly: a label, a score, or P(yes), each
with calibrated probabilities. This server exposes exactly that surface, with three
token-economy behaviours built in:
> Jev is a decision model, not a chat or code-completion model. It never writes prose, code,
> summaries, translations or explanations of its reasoning — every question must define its
> answer space up front. Point an agent here and it stays in control of the workflow, calling
> Jev for the decisions inside it; that is the whole point of the server.
* **One request, many questions.** `jev_ask` takes every question at once. Jev evaluates
them against the state in parallel, so extra questions cost a few tokens and almost no
latency — one call instead of N round trips.
* **A hard context-budget guard.** The request is measured locally and refused *before* it
is sent if it would exceed the model's budget, with the numbers in the error.
* **Compact answers by default.** You get the answer, its confidence, and whether it needs
review. Probability tables and the echoed rubric are opt-in.
* **No client lock-in.** A plain stdio MCP server with no vendor extensions: Claude Code and
Desktop, Cursor, VS Code, Cline, Windsurf, Zed, JetBrains, Codex, Gemini CLI, or an agent
you write against the Python/TypeScript SDK — same six tools, same answers.
## Install
```bash
npx -y token-optimizer-jev-mcp # run directly, no install
npm install -g token-optimizer-jev-mcp
```
Requires Node 20.12+ and a TypeSafe API key from
[console.typesafe.ai/settings/keys](https://console.typesafe.ai/settings/keys).
## Client setup
This is a stdio MCP server, so any host that speaks MCP can run it — nothing below is
Claude-specific and no vendor is special-cased. Most hosts take the same `mcpServers` block:
```json
{
"mcpServers": {
"jev": {
"command": "npx",
"args": ["-y", "token-optimizer-jev-mcp"],
"env": { "TYPESAFE_API_KEY": "your-key" }
}
}
}
```
| Host | Where that block goes |
|------|-----------------------|
| Claude Code | `claude mcp add --scope user jev --env TYPESAFE_API_KEY=your-key -- npx -y token-optimizer-jev-mcp`, or `.mcp.json` |
| Claude Desktop | `claude_desktop_config.json` (`<APPDATA>/Claude/` on Windows, `~/Library/Application Support/Claude/` on macOS) |
| Cursor | `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global) |
| Cline | `cline_mcp_settings.json`, via the extension's MCP Servers panel |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
| JetBrains / Junie | `mcp.json` (Settings → Tools → MCP Server), or `.junie/mcp/mcp.json` in the project |
| Gemini CLI | `~/.gemini/settings.json` |
Three hosts use their own shape instead:
VS Code (Copilot) — `.vscode/mcp.json`, under `servers` rather than `mcpServers`:
```json
{
"servers": {
"jev": {
"type": "stdio",
"command": "npx",
"args": ["-y", "token-optimizer-jev-mcp"],
"env": { "TYPESAFE_API_KEY": "your-key" }
}
}
}
```
Zed — `settings.json`, under `context_servers`:
```json
{
"context_servers": {
"jev": {
"command": "npx",
"args": ["-y", "token-optimizer-jev-mcp"],
"env": { "TYPESAFE_API_KEY": "your-key" }
}
}
}
```
Codex CLI — `~/.codex/config.toml`:
```toml
[mcp_servers.jev]
command = "npx"
args = ["-y", "token-optimizer-jev-mcp"]
env = { TYPESAFE_API_KEY = "your-key" }
```
And an agent you write yourself just needs an MCP client library:
```python
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
params = StdioServerParameters(
command="npx", args=["-y", "token-optimizer-jev-mcp"],
env={"TYPESAFE_API_KEY": "your-key"},
)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize() # server instructions arrive here
await session.call_tool("jev_ask", {"state": "...", "questions": {...}})
```
The TypeScript equivalent (`Client` + `StdioClientTransport` from
`@modelcontextprotocol/sdk`) is what `scripts/smoke.mjs` runs.
### What every host gets
Only standard MCP fields, so a client that does not understand one simply ignores it:
* `instructions` in the initialize result — how to use the tools, written for whatever agent
is asking (batch your questions, treat `needs_review` as unresolved, branch on error codes).
* A `title` and a `description` per tool, and an `inputSchema` on every tool — `{}` where a
tool takes no parameters, because the field is required by the spec.
* Self-contained JSON Schemas: no `$ref` for a host to resolve, so a client can hand the
schema to its model's function-calling API as-is.
* `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`,
`openWorldHint: true` on every tool. Hosts that gate approvals on annotations (Cursor,
VS Code, Cline, Windsurf, Zed) therefore see a read-only server, not one that needs a
prompt before each call.
* Typed failures: `isError` with `{ tool, code, message, suggestion? }`.
Transport is stdio only — this server opens no socket, so an agent that can only reach a URL
needs a stdio↔HTTP bridge in front of it.
The key is read from the server's environment only. It is never a tool argument, never
returned by a tool, and never logged.
## Tools
| Tool | What it answers |
|------|-----------------|
| `jev_ask` | A batch of questions — `noul`, `choice` and `score` mixed freely — against one state, in one request. |
| `jev_choice` | Pick exactly one label from a defined set: the label, its confidence, and optionally every label's probability. |
| `jev_score` | Rate a state against an ordered rubric: the expected score (may fall between levels), its confidence, optionally per-level probabilities. |
| `jev_noul` | A yes/no question, answered as calibrated P(yes). |
| `jev_list_models` | The model names this account can send, what `jev-latest` and `jev-preview` currently point to. |
| `jev_server_info` | Base URL, default model, whether a key is present (never the key), the context budget, the confidence threshold, the tool list. |
### Question shapes
```jsonc
// noul — yes/no
{ "type": "noul", "instructions": "Is this message about billing?" }
// choice — one label from a set
{ "type": "choice",
"instructions": "What is this about?",
"criteria": { "billing": "money, invoices or charges", "technical": "a broken feature", "other": null } }
// score — ordered rubric, lowest first, indexed from 0
{ "type": "score",
"instructions": "How urgent is this?",
"criteria": ["no rush", "normal", "urgent", "drop everything"] }
```
`instructions` and the criteria descriptions accept a string, a JSON object, or an array —
put the question in one field and the data it refers to in others.
### Answer shape
```json
{
"model": "jev-1.13.0",
"answers": {
"billing": { "type": "noul", "noul": 0.94, "uncertain": false },
"topic": { "type": "choice", "choice": "billing", "confidence": 0.88, "needs_review": false },
"urgency": { "type": "score", "score": 2.1, "confidence": 0.55, "needs_review": true }
},
"usage": { "input_tokens": 128, "output_tokens": 0 }
}
```
* `needs_review` (choice/score) is `true` when the reported confidence is below the
threshold — route those to a human, or to a second opinion.
* `uncertain` (noul) is `true` when P(yes) sits inside the mirrored threshold band
(0.4–0.6 by default). A noul answer has no separate confidence: the probability *is* the
answer.
* `include_probabilities: true` adds the full probability table; `include_legend: true`
adds the score rubric echoed back; `raw: true` returns the API result untouched.
## Context budget
The published model card allows 64k tokens per request, 32k of which may be `state` plus
the longest question. This server estimates the serialized request at roughly 4 characters
per token and refuses anything over `JEV_MAX_INPUT_TOKENS` / `JEV_MAX_STATE_TOKENS`
(defaults 60000 / 30000) **before sending it**:
```json
{ "tool": "jev_ask", "code": "BUDGET_STATE",
"message": "`state` is an estimated 31000 tokens, over the 30000-token budget for state (JEV_MAX_STATE_TOKENS). The request was not sent.",
"suggestion": "Send the part of the state the question actually needs, or split the state across several calls." }
```
The estimator is a heuristic guard rail, not a tokenizer — the headroom under the real
limits is deliberate.
## Error codes
Every failure comes back as `{ tool, code, message, suggestion? }` with `isError: true`, so
a caller can branch on the code instead of parsing prose.
| Code | Meaning |
|------|---------|
| `CONFIG_MISSING_API_KEY` | No key in the server environment. The fix is in `suggestion`. |
| `CONFIG_INVALID_API_KEY` | The API rejected the key (HTTP 401). |
| `CONFIG_INVALID_ENV` | A bad `JEV_*` / `TYPESAFE_*` value; the process exits at startup naming it. |
| `VALIDATION` / `VALIDATION_EMPTY_QUESTIONS` | The questions are malformed (a choice with fewer than 2 labels, a score with fewer than 2 levels, no questions at all). |
| `BUDGET_STATE` / `BUDGET_TOTAL` | The estimated request is over budget; it was not sent. |
| `RATE_LIMIT` | HTTP 429 after retries; includes `retry_after_ms` when the server sent one. |
| `MODEL_NOT_FOUND` | HTTP 404 — usually an unknown `model` name. Call `jev_list_models`. |
| `PERMISSION_DENIED` / `BAD_REQUEST` / `SERVER_ERROR` / `API_ERROR` | HTTP 403 / 400+422 / 5xx / anything else. A `BAD_REQUEST` in practice means an unknown `model` name or a `state` the API will not accept; the `suggestion` says which to check. |
| `TIMEOUT` / `CONNECTION` | Transport failure, after the SDK's own retries with backoff. |
| `TYPESAFE_ERROR` / `UNEXPECTED` | The SDK rejected a response shape, or something unexpected was thrown. |
## Read-only posture
There is nothing to write. Jev calls are stateless inference — no records, no files, no
server-side state — and this server adds no write path of its own:
* no filesystem access, no shell, no database;
* no tool accepts a file path, so nothing can be read off your disk either;
* the only network egress is the configured `TYPESAFE_BASE_URL`;
* the API key is environment-only and never appears in a tool call or a tool result.
## Environment variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `TYPESAFE_API_KEY` | Yes* | — | TypeSafe API key. Read by this server and by the SDK. |
| `JEV_API_KEY` | — | — | Fallback if `TYPESAFE_API_KEY` is unset. |
| `TYPESAFE_BASE_URL` | No | `https://api.typesafe.ai` | API root (for a proxy). |
| `TYPESAFE_DEFAULT_MODEL` | No | `jev-latest` | Model used when a call omits `model`. |
| `JEV_TIMEOUT_MS` | No | `30000` | Timeout per attempt, in milliseconds. |
| `JEV_MAX_INPUT_TOKENS` | No | `60000` | Estimated request budget before a local refusal. |
| `JEV_MAX_STATE_TOKENS` | No | `30000` | Estimated `state` budget before a local refusal. |
| `JEV_CONFIDENCE_THRESHOLD` | No | `0.6` | Below this, answers are flagged `needs_review` / `uncertain`. |
| `JEV_BUDGET_WARN_RATIO` | No | `0.8` | Fraction of the budget past which a response carries a warning. |
\* Without a key the server still starts and lists its tools; each judgment call returns
`CONFIG_MISSING_API_KEY` with the fix. An *invalid* environment value stops the process at
startup with a named reason on stderr instead of failing silently.
## Development
```bash
npm install
npm run build # tsc -> dist/
npm test # jest: config, budget, question schemas, formatting, error mapping, request/response wiring
# src/protocol.test.ts drives the real server through a plain MCP client over a
# linked transport: instructions, tool titles, annotations, and typed errors
npm run smoke # speak MCP to dist/index.js over stdio as a host does; skips the live call without a key
npm run test:live # tier B: the real API, the real server (needs TYPESAFE_API_KEY); see TEST-PLAN.md
python bench/run_three_way.py # Hermes vs +MCP vs +MCP+hooks on six check-verified tasks
# measured results: bench/THREE-WAY.md
```
## Publishing
```bash
npm login
npm publish # prepublishOnly runs build + tests
```
`jev-mcp` and `typesafe-jev-mcp` were already taken on npm; `token-optimizer-jev-mcp` is
this package's published name.
## Hooks — Jev decisions at every lifecycle step
`hooks/` ships six hooks that ask Jev for one judgement at each point where a judgement changes
what happens next, plus `agent-bridge.mjs`, which runs the same hooks under Claude Code, Gemini CLI
and Cursor. What each hook decides, its measured latency/cost, and its failure model:
`hooks/README.md`.
| Hook | Event | Decision | Effect |
|------|-------|----------|--------|
| H1 | pre_llm_call | is this request irreversible/destructive? | inject context when P ≥ 0.5 |
| H2 | pre_tool_call | deny-list, then "is this destructive or hard to reverse?" | block |
| H3 | post_tool_call | did this result actually fail? | record (this event cannot block) |
| H4 | pre_verify | complete / with caveats / incomplete | push back with `continue` |
| H5 | on_session_start | code / docs / data / ops session? | inject the profile |
| H6 | on_session_end | does a human need to be told? | append to the follow-up ledger |
### Files (all under [`hooks/`](hooks/README.md))
| File | What it is |
|------|------------|
| [`hooks/install.mjs`](hooks/install.mjs) | registers/unregisters the six hooks in Hermes (merges config + allowlist, backs the config up) |
| [`hooks/jev_pre_llm_call.mjs`](hooks/jev_pre_llm_call.mjs) | H1 — irreversible-request pre-read |
| [`hooks/jev_pre_tool_call.mjs`](hooks/jev_pre_tool_call.mjs) | H2 — deny-list + Jev risk gate |
| [`hooks/jev_post_tool_call.mjs`](hooks/jev_post_tool_call.mjs) | H3 — outcome triage (records failures) |
| [`hooks/jev_pre_verify.mjs`](hooks/jev_pre_verify.mjs) | H4 — completion gate |
| [`hooks/jev_on_session_start.mjs`](hooks/jev_on_session_start.mjs) | H5 — session profile |
| [`hooks/jev_on_session_end.mjs`](hooks/jev_on_session_end.mjs) | H6 — follow-up ledger |
| [`hooks/agent-bridge.mjs`](hooks/agent-bridge.mjs) | runs any of the above under Claude Code / Gemini CLI / Cursor |
| [`hooks/jev-client.mjs`](hooks/jev-client.mjs) | the MCP stdio client the hooks use (handshake, key resolution, cache, daily budget) |
| [`hooks/hook-common.mjs`](hooks/hook-common.mjs) | payload reading, the four emit shapes, the one-emitter wrapper |
| [`hooks/stub-mcp-server.mjs`](hooks/stub-mcp-server.mjs) | canned MCP server used by the tests |
| [`hooks/selftest.mjs`](hooks/selftest.mjs) | 33 offline cases — run before trusting an edit |
### Hermes — one command (verified end to end)
```bash
node hooks/install.mjs # merges 6 entries + allowlist rows; backs up config.yaml first
hermes hooks doctor # exec + allowlist + valid JSON per hook
hermes hooks test pre_tool_call --for-tool terminal --payload-file <payload.json>
node hooks/install.mjs --uninstall # removes exactly these six
```
Hooks are not hot-reloaded: they apply from the next session. `JEV_HOOKS_OFF=1` makes the whole set
a no-op. Measured: `DROP TABLE customers;` blocked in 0.16 s without an API call; a `docker system
prune -af` scored P(destructive)=0.95 and blocked in 1.6 s.
### Claude Code
Paths in these snippets are written relative to this repository: substitute `<REPO>` with the
checkout's absolute path where you paste them, because a hook is spawned from an arbitrary working
directory and cannot resolve a relative path from inside a config file.
`hooks/agent-bridge.mjs` normalises Claude's payload (its `tool_input`/`tool_name`/`session_id`/
`cwd` are the fields our hooks already read), maps Claude's tool names onto ours
(`Bash`→`terminal`, `Write`→`write_file`, `Edit`→`patch`) and translates the answer back: a block
becomes Claude's own `{"decision":"block","reason":…}` **and** exit-2-style stderr, context becomes
`hookSpecificOutput.additionalContext`, and a `Stop` push-back becomes `decision: "block"` so the
conversation continues.
`.claude/settings.json` (project) or `~/.claude/settings.json`, merged with whatever is already
there:
```json
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash|Write|Edit|MultiEdit",
"hooks": [{ "type": "command", "timeout": 20,
"command": "node \"<REPO>/hooks/agent-bridge.mjs\" claude jev_pre_tool_call.mjs" }] }
],
"PostToolUse": [
{ "matcher": "Bash|Write|Edit|MultiEdit",
"hooks": [{ "type": "command", "timeout": 20,
"command": "node \"<REPO>/hooks/agent-bridge.mjs\" claude jev_post_tool_call.mjs" }] }
],
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "timeout": 20,
"command": "node \"<REPO>/hooks/agent-bridge.mjs\" claude jev_pre_llm_call.mjs" }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "timeout": 20,
"command": "node \"<REPO>/hooks/agent-bridge.mjs\" claude jev_pre_verify.mjs" }] }
],
"SessionStart": [
{ "hooks": [{ "type": "command", "timeout": 20,
"command": "node \"<REPO>/hooks/agent-bridge.mjs\" claude jev_on_session_start.mjs" }] }
]
}
}
```
Notes: `PreToolUse`/`PostToolUse` take a `matcher`; the lifecycle events (`UserPromptSubmit`,
`Stop`, `SessionStart`, `SessionEnd`, `PreCompact`, …) do not. Blocking works through stdout JSON or
exit code 2 with the reason on stderr. Set `timeout` (seconds) — the default for a command hook is
600, which is far longer than a judgement needs. `${CLAUDE_PROJECT_DIR}` is available if you prefer
project-relative paths. Cursor loads Claude Code hooks natively, so this block can serve both.
### Gemini CLI
Gemini CLI's `settings.json` takes a `hooks` object with PascalCase events (`BeforeTool`,
`AfterTool`, `BeforeAgent`, `AfterAgent`, `SessionStart`, `SessionEnd`), a `matcher` (tool-name
regex for tool hooks), and hook entries of `{type: "command", name?, command, timeout}` — **timeout
in milliseconds**. Its stdout contract is close to ours: `decision: "deny"` (alias `"block"`) plus
`reason` refuses a tool, `hookSpecificOutput.additionalContext` appends context, `continue: false`
kills the loop, `systemMessage` shows a line to the user, and exit code 2 blocks with stderr as the
reason. The bridge emits exactly those.
```json
{
"hooks": {
"BeforeTool": [
{ "matcher": "run_shell_command|write_file|replace",
"hooks": [{ "type": "command", "name": "jev risk gate", "timeout": 20000,
"command": "node \"<REPO>/hooks/agent-bridge.mjs\" gemini jev_pre_tool_call.mjs" }] }
],
"AfterTool": [
{ "hooks": [{ "type": "command", "timeout": 20000,
"command": "node \"<REPO>/hooks/agent-bridge.mjs\" gemini jev_post_tool_call.mjs" }] }
],
"BeforeAgent": [
{ "hooks": [{ "type": "command", "timeout": 20000,
"command": "node \"<REPO>/hooks/agent-bridge.mjs\" gemini jev_pre_llm_call.mjs" }] }
]
}
}
```
### Cursor
`~/.cursor/hooks.json` (user) or `<project>/.cursor/hooks.json`, camelCase events, one `command` per
hook. Cursor's block signal for `preToolUse` / `beforeShellExecution` / `beforeMCPExecution` /
`beforeReadFile` / `subagentStart` is **exit code 2** with the reason on stderr — which is what the
bridge emits for a block; it is also committed to a config file Cursor auto-reloads:
```json
{
"version": 1,
"hooks": {
"preToolUse": [
{ "command": "node \"<REPO>/hooks/agent-bridge.mjs\" cursor jev_pre_tool_call.mjs", "timeout": 20 }
],
"postToolUse": [
{ "command": "node \"<REPO>/hooks/agent-bridge.mjs\" cursor jev_post_tool_call.mjs", "timeout": 20 }
],
"beforeSubmitPrompt": [
{ "command": "node \"<REPO>/hooks/agent-bridge.mjs\" cursor jev_pre_llm_call.mjs", "timeout": 20 }
],
"stop": [
{ "command": "node \"<REPO>/hooks/agent-bridge.mjs\" cursor jev_pre_verify.mjs", "timeout": 20 }
]
}
}
```
Project hooks run from the project root, user hooks from `~/.cursor/` — so keep the bridge path
absolute (as above) and you never think about it again. Other Cursor events you can wire the same
way: `afterFileEdit`, `beforeShellExecution`, `sessionStart`, `sessionEnd`, `preCompact`,
`afterAgentResponse`.
### Agents with no hook API (and what to do instead)
Windsurf, Cline, Zed, VS Code (Copilot agent mode), Aider, Continue and Codex CLI do not expose a
before-tool/after-tool shell hook I could verify on this machine, so there is no config to paste for
them. Three honest fallbacks, in order of how much they buy you:
1. **Register the MCP server itself** (they all speak MCP) and put one line in the agent's rules
file — `AGENTS.md`, `.windsurfrules`, `.clinerules`, `.github/copilot-instructions.md`, … —
telling it to call `jev_noul` / `jev_choice` for risky or uncertain decisions, and to treat
`needs_review: true` as "ask the human".
2. **Git-level gates**: the same bridge works as a `.git/hooks/pre-commit` script (read the staged
diff as the `state`, `jev_noul` "does this change look destructive or unfinished?"), which every
one of these tools respects because it is git, not the agent.
3. **Wrap the command** you launch the agent with, running `agent-bridge.mjs hermes
jev_pre_llm_call.mjs` on the prompt first if you want the pre-read regardless of the host.
### What each agent honours (and how this was verified)
| Agent | Block | Context injection | Continue / push-back |
|-------|-------|-------------------|----------------------|
| Hermes | `{"decision":"block","reason":…}` (and exit 2) | `{"context":"…"}` on every event | `{"action":"continue","message":…}` on `pre_verify` |
| Claude Code | `{"decision":"block","reason":…}` or exit 2 + stderr | `hookSpecificOutput.additionalContext` (verified for `Stop`) | `decision:"block"` on `Stop` keeps the conversation going |
| Gemini CLI | `{"decision":"deny","reason":…}` or exit 2 + stderr | `hookSpecificOutput.additionalContext` | `continue:true` + `systemMessage` |
| Cursor | exit 2 + stderr (→ `permission: deny`) | not wired — the response field is not documented for these events | `followup_message` on `stop` |
Verification status, stated plainly: the **Hermes** column is exercised end to end on this machine
(`hermes hooks doctor`, `hermes hooks test <event>` with real Jev answers, and the measurements in
`hooks/README.md`). The **Claude Code / Gemini CLI / Cursor** columns come from each vendor's hook
reference plus the bridge's own offline tests (`node hooks/selftest.mjs`, 33 cases, every agent's
block/allow/context path asserted against a stub MCP server) — I have not run those three apps with
the hooks installed, so treat their rows as "documented contract, adapter tested", not as exercised.
## License
MIT
TDQS
Scored across 6 tools
jev_ask can handle label sets, rubrics, and yes/no questions, so it overlaps with jev_choice, jev_score, and jev_noul for single-question use. The specialized tools are individually clear, but an agent could reasonably hesitate between the generic ask tool and the specialized one.
All tools share a consistent jev_ prefix and lowercase snake_case style, with list_models and server_info following a recognizable pattern. The names score, ask, choice are concise, though jev_noul is cryptic and not clearly verb- or noun-shaped.
Six tools is well-scoped for a decision-serving MCP: four distinct question modes plus two informational endpoints. Each tool earns its place, and the count is comfortably inside the expected range.
The surface covers all stated capabilities: rubric scoring, label choice, yes/no probability, mixed parallel questions, model metadata, and server configuration. The explicit design limitation that Jev does not generate text explains why generation/rewrite tools are absent, leaving no obvious dead ends.