jev-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@jev-mcpSave a tool that classifies support tickets as urgent or not urgent"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
jev-mcp
jev-mcp is a self-hostable MCP server that lets AI agents save, document, and re-run TypeSafe Jev queries. Instead of an agent re-deriving the same "is this urgent?" or "which category?" judgment call from scratch in every session, it saves the judgment once as a named tool — with the questions, the supporting context, and docs explaining when to use it — and any agent with access can look it up and run it again with new inputs.
To run it you need a TypeSafe API key, somewhere to run one container with a small persistent disk (a Fly.io app, a VPS, or any Docker host), and an MCP client that supports remote servers with OAuth, such as Claude Desktop, claude.ai, or Claude Code.
How it works
Agents authenticate with OAuth 2.1; you approve each agent once by typing an owner password on a consent page, and it gets its own client id and tokens from then on.
A saved tool is
inputs + context + questions + docs: inputs are what callers supply at run time, context is constant background (policies, definitions, examples), questions are what to ask TypeSafe, and docs explain the tool for the next agent that finds it.Running a tool merges its context and the caller's inputs into a single TypeSafe
stateobject and returns typed answers (noul,choice, orscore).Every call — saved-tool runs and one-off
ask_jevcalls alike — is logged with its inputs, answers, token usage, latency, and the agent that made it.
Related MCP server: Dvarapala
Deploy on Fly.io
git clone <repo> && cd jev-mcp
fly launch --no-deploy --copy-config --name <your-app>
fly volumes create jev_data --size 1 --region <region>
fly secrets set TYPESAFE_API_KEY=... JEV_OWNER_PASSWORD=... JEV_PUBLIC_URL=https://<your-app>.fly.dev
fly deploy
curl https://<your-app>.fly.dev/healthzThis is a single-instance design: the database is one SQLite file on one volume, so scaling past one Fly machine gives each machine its own volume and forks the database. Keep the count at one.
Deploy with Docker
cp .env.example .env
# edit .env: set TYPESAFE_API_KEY, JEV_OWNER_PASSWORD, JEV_PUBLIC_URL
docker compose up -dJEV_PUBLIC_URL must be the HTTPS URL your MCP clients will actually use — OAuth issuer and redirect
checks depend on it matching exactly. docker compose does not terminate TLS for you, so put a
TLS-terminating proxy (Caddy, Traefik, or Cloudflare Tunnel) in front of the container and point it at
localhost:8080.
Connect an agent
Claude Desktop / claude.ai: Settings → Connectors → Add custom connector → URL https://<host>/mcp.
A browser tab opens the consent page; enter the owner password to approve the agent.
Claude Code:
claude mcp add --transport http jev https://<host>/mcpThen run /mcp inside Claude Code to complete authentication.
Every agent that connects gets its own client id and its own access/refresh tokens. To revoke one agent,
stop the server and delete its rows (matching client_id) from oauth_tokens, oauth_codes, and
oauth_pending in the SQLite database, then restart. To revoke every agent at once, stop the server,
run the following against the configured database, and restart:
BEGIN IMMEDIATE;
DELETE FROM oauth_tokens;
DELETE FROM oauth_codes;
DELETE FROM oauth_pending;
COMMIT;Rotate JEV_OWNER_PASSWORD before restarting if it may have been shared or compromised. Password
rotation alone does not revoke existing grants: refresh tokens continue to work and each successful
refresh issues a replacement with a new expiry, so an active agent can retain access indefinitely.
How agents use it
Agents don't need to be taught TypeSafe up front. The server ships TypeSafe's official
agent skill through the get_guide tool (and the jev://guide
resource), prefixed with a short section that maps the skill's concepts onto these tools. The startup
instructions tell agents to read it before designing a new tool. If you use Claude Code, also install the
skill locally with claude plugin marketplace add typesafe-ai/skills && claude plugin install typesafe@typesafe-ai;
the server-shipped copy is for hosted agents that cannot install plugins.
The server's instructions field tells connecting agents the workflow:
Call
list_toolsfirst. If a saved tool fits, callget_toolto read its docs, thenrun_toolwith the declared inputs.To design a new judgment, iterate with
ask_jev(state + questions, nothing is saved) until the answers look right.Persist it with
create_tool: declare inputs (what callers pass at run time), optional context (policies, definitions, examples that never change), the questions, and docs that explain purpose, when to use it, and how to read the answers.
Worked example: Gmail triage
An agent that has been iterating with ask_jev settles on a tool and saves it:
{
"name": "create_tool",
"arguments": {
"name": "gmail_triage",
"title": "Gmail triage",
"docs": "Triages one inbound Gmail message. Use this before deciding whether to reply, file, or escalate an email. `needs_reply` above 0.5 means draft a reply. `category` sorts the email into one of four buckets. `urgency` above 1.5 (top of a 0-2 scale) means handle it today; below 0.5 means it can wait.",
"inputs": {
"email": {
"type": "object",
"description": "Object with `subject`, `from`, and `body`.",
"required": true
}
},
"context": {
"policy": "Reply within 24 hours to anything from a paying customer. Newsletters and automated receipts never need a reply."
},
"questions": {
"needs_reply": {
"type": "noul",
"instructions": "Does `email` require a personal reply, per `policy`?"
},
"category": {
"type": "choice",
"instructions": "Which category best fits `email`?",
"criteria": {
"customer_support": "A question or complaint from someone outside the company",
"internal": "From a colleague or an internal system",
"newsletter": "A subscribed newsletter or marketing email",
"receipt": "An automated receipt, invoice, or shipping notice"
}
},
"urgency": {
"type": "score",
"instructions": "How urgent is `email`, given `policy`?",
"criteria": ["low", "medium", "high"]
}
}
}
}Later — this session or a future one — any connected agent can run it against a real message:
{
"name": "run_tool",
"arguments": {
"name": "gmail_triage",
"inputs": {
"email": {
"subject": "Payment failed again",
"from": "customer@example.com",
"body": "This is the third time my card has been declined even though it's valid. I need this fixed today."
}
}
}
}Which returns:
{
"tool": "gmail_triage",
"version": 1,
"model": "jev-latest",
"answers": {
"needs_reply": { "type": "noul", "noul": 0.97 },
"category": {
"type": "choice",
"choice": "customer_support",
"probabilities": { "customer_support": 0.91, "internal": 0.02, "newsletter": 0.01, "receipt": 0.06 },
"confidence": 0.9
},
"urgency": {
"type": "score",
"score": 1.8,
"legend": { "0": "low", "1": "medium", "2": "high" },
"probabilities": { "0": 0.02, "1": 0.16, "2": 0.82 },
"confidence": 0.85
}
},
"usage": { "input_tokens": 210, "output_tokens": 34 },
"run_id": "r_8f2a1c9b"
}needs_reply.noul is 0.97, well above the 0.5 threshold, and urgency.score is 1.8, above the "handle it
today" threshold — the agent drafts a reply now.
Tool reference
All nine tools require a valid bearer token.
Tool | Arguments | Returns |
|
|
|
|
| The saved tool. Error if name exists or validation fails. |
|
| The saved tool with bumped version. Full validation runs on the merged result. |
|
| Full definition including docs. |
|
|
|
|
|
|
|
|
|
|
| Recent TypeSafe calls newest first: |
| none |
|
Validation failures and not-found conditions come back as MCP tool errors with a single plain-English
message naming the offending field. TypeSafe HTTP errors are surfaced with the status code, the API's
message, and retry_after_seconds when the response carried a retry-after header.
Configuration
All configuration is by environment variable; .env.example documents every key.
Variable | Required | Default | Meaning |
| yes | Server-held key used for every TypeSafe call. | |
| yes | Password entered on the consent page. At least 12 characters; generate one with | |
| yes | Externally reachable HTTPS base URL, e.g. | |
| no |
| SQLite file path. |
| no |
| Bind address. |
| no |
| Bind port. |
| no |
| Default model for new tools and |
| no |
| Seconds. |
| no |
| Seconds. |
| no |
| One of |
| no |
| Run history older than this is deleted as new runs are recorded. |
Startup fails fast with a message naming the variable at fault: a required variable missing, an owner
password under 12 characters, a JEV_PUBLIC_URL that is not HTTPS (set JEV_ALLOW_INSECURE_URL=1 to
allow http:// for local development only) or that carries a path (the OAuth endpoints live at the
root), an unknown log level, a non-positive port or token TTL, or a negative retention.
Security notes
Self-contained OAuth 2.1 authorization server with PKCE and dynamic client registration; no external identity provider is required.
Access and refresh tokens are hashed at rest in SQLite — the raw token is never stored.
Client registrations are capped at 1,000. Registration, authorization, and token exchange clean up clients older than 24 hours that have no live tokens, pending consent, or unused authorization code. At capacity, new registrations are rejected until space is freed; existing grants remain usable.
One owner password gates every agent's consent; anyone who knows it can authorize a new agent, so treat it like any other server credential. It must be at least 12 characters, and guessing is bounded twice over: 5 attempts per consent request, and 10 failures server-wide lock the consent page for everyone (right password included) with an exponential backoff from 1 minute to 1 hour. A successful consent clears the lock.
Wrong passwords, rejected registrations, approvals, token issuance, and token revocations are logged with client and family identifiers. Token values, authorization codes, and the password never are.
The TypeSafe API key never leaves the server — agents send state and questions, and the server makes the TypeSafe call on their behalf.
Run only behind HTTPS in production;
JEV_PUBLIC_URLdoubles as the OAuth issuer, so it must be the exact externally reachable origin.jev_mcp/server.pycarries a small guarded patch for a bug in the mcp SDK version this project was built against (2.2.0, locked inuv.lock), where a missing default onRevocationRequest.client_secretmakes/revokereject the public clients dynamic registration creates. It checks the field at import time and only applies the fix if the bug is still present, so it self-disables once upstream ships a corrected release. The patch rebinds an attribute on an SDK module, which is process-wide, and any failure to find what it patches is swallowed so a moved module degrades to upstream behaviour.The container runs as root. Fly.io and Docker mount volumes root-owned and the slim base image has no
gosuorsu-execto drop privileges after fixing ownership, so a non-root default would fail to start on a freshly attached volume. Nothing but the app runs in the image and it listens on one port, so the exposure is small — but if your host lets you prepare the volume, run non-root by addinguser: "1000:1000"to thejev-mcpservice indocker-compose.ymlandchown -R 1000:1000the volume's/dataonce before starting.
Backups
The whole application state is one SQLite file. Copying jev.db on its own gives you a torn or stale
snapshot, because the database runs in WAL mode and recent commits live in the -wal sidecar. Let SQLite
write a consistent copy with VACUUM INTO, then fetch that:
# Fly.io
fly ssh console -C "python -c \"import sqlite3; sqlite3.connect('/data/jev.db').execute('VACUUM INTO \\\"/data/backup.db\\\"')\""
fly ssh sftp get /data/backup.db ./jev-backup.db
# Docker
docker compose exec jev-mcp python -c "import sqlite3; sqlite3.connect('/data/jev.db').execute('VACUUM INTO \"/data/backup.db\"')"
docker cp jev-mcp:/data/backup.db ./jev-backup.dbVACUUM INTO fails if the destination already exists, so delete the previous /data/backup.db between
runs. To restore, stop the server, copy the backup to /data/jev.db (the configured JEV_DB_PATH),
delete any leftover jev.db-wal and jev.db-shm files next to it, and start the server again.
Run history is self-limiting: each new run deletes rows older than JEV_RUN_RETENTION_DAYS (90 by
default), so back up before lowering it if you want to keep the older history.
Development
uv sync
uv run pytest
uv run ruff check .To run the server locally over plain HTTP:
TYPESAFE_API_KEY=... JEV_OWNER_PASSWORD=... JEV_PUBLIC_URL=http://localhost:8080 \
JEV_ALLOW_INSECURE_URL=1 uv run python -m jev_mcpscripts/smoke_live.py makes one real call against the TypeSafe API to catch integration drift that
mocked tests can't. It is opt-in and skipped in CI:
TYPESAFE_API_KEY=... uv run python scripts/smoke_live.pyWithout a key it prints a skip message and exits 0.
Refreshing the vendored skill
The guide's upstream half lives in jev_mcp/vendor/typesafe-ai/ (MIT, TypeSafe AI). Refresh it with
uv run python scripts/update_skill.py, review the diff, and commit; SOURCE.txt records the fetch time.
License
MIT — see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
OAuth 2.1 short-link tools for AI agents with scoped tokens, approvals, audit logs, and revocation.
Runtime permission, approval, and audit layer for AI agent tool execution.
Hosted AI agents and workflows with app OAuth, human approval gates, and a run ledger.
A read-only verified record of agent-operable GTM tools: search, fetch, compare, track changes.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceEnables controlled AI-agent access to enterprise-shaped tools with a deny-by-default gated write path, human approval, dry-run execution, and append-only audit logging.1-
- AlicenseNot gradedqualityCmaintenanceProvides permission gates and tamper-evident audit logging for AI agent tool executions, with declarative policies, consent ladders, and hash-chained verification.MIT
- AlicenseNot gradedqualityBmaintenanceEnables coding agents to run source-bound evidence checks and bounded batch judgments for classification, extraction, and decision tasks via TypeSafe Jev.1,480 npmMIT
- AlicenseAqualityCmaintenanceEnables coding or reasoning agents to request structured judgments from TypeSafe's Jev model at decision points, including choices, scores, claim verification, and code reviews, with probabilities and confidence returned as data.5173 npmMIT