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-mcpGiven the state, choose the best option from the list."
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
A small, self-contained MCP service that exposes three bounded TypeSafe Jev / System One decision primitives — jev_choice, jev_score, jev_noul — plus jev_decide for batching several of them against one shared state in a single upstream call, over remote HTTPS/MCP.
Open WebUI ──────┐
Codex ───────────┤
Claude Code ─────┼── HTTPS + Bearer auth ──> jev-mcp (Streamable HTTP) ──> TypeSafe Jev API
DocIntel ────────┤
Other agents ────┘Jev is used here for bounded decisions, not general text generation. Callers are responsible for gathering evidence (web search, DB lookups, etc.) and passing it in as state — this service never does its own research.
Why these design choices
Python + FastMCP, Streamable HTTP transport — matches the other MCP services already running on this host and is the current (non-deprecated) MCP HTTP transport.
No database, no queue, no OAuth server. Machine-to-machine auth is a single shared bearer token, compared in constant time. This is an experimental, low-traffic internal service — that's the right amount of mechanism for now.
A dedicated Caddy sidecar, not the host's existing Caddy instance. This host already runs a Caddy process (under a different local user, proxying Ollama) — reusing it would mean editing someone else's live shared config for an unrelated purpose. The sidecar here is fully self-contained and can't conflict with it.
No public DNS / Let's Encrypt. Access to this service is via Tailscale/WireGuard only, not the public internet, so Caddy terminates HTTPS with its own locally-trusted CA (
tls internal) instead. See HTTPS below for what that means for clients.
Related MCP server: Jev MCP
Repository layout
jev-mcp/
├── src/
│ ├── server.py # FastMCP app: the 4 tools + /health route + auth wiring
│ ├── jev_client.py # Thin async HTTP client for the TypeSafe System One API
│ ├── auth.py # Bearer-token middleware protecting the MCP endpoint
│ ├── config.py # Env-var settings, fails fast if secrets are missing
│ └── logging_setup.py # One-line structured JSON logging per request
├── tests/ # pytest unit tests (client, auth, tools)
├── scripts/test_client.py # Manual connectivity + example-decision test client
├── deploy/
│ ├── deploy.sh # Parameterized remote deploy (Oracle Cloud / any SSH host)
│ └── bootstrap_remote.sh # Remote-side: installs Docker, opens firewall (idempotent)
├── caddy/
│ ├── Caddyfile # Local/Tailscale variant (self-signed, built into an image)
│ ├── Caddyfile.public # Public variant (auto Let's Encrypt, bind-mounted)
│ └── Dockerfile # Builds the Caddyfile-baked image used by docker-compose.yml
├── Dockerfile
├── docker-compose.yml # Local deploy (Tailscale/WireGuard-only, self-signed)
├── docker-compose.prod.yml # Public deploy (real domain, Let's Encrypt)
├── .env.example
├── LICENSE
├── CONTRIBUTING.md
└── README.mdQuickstart
cp .env.example .env
# edit .env: set TYPESAFE_API_KEY (your real TypeSafe key) and
# MCP_AUTH_TOKEN (generate with: openssl rand -hex 32)
docker compose up -d --build
docker compose ps # both jev-mcp and jev-mcp-caddy should be healthy/running
curl -s http://127.0.0.1:9020/health # {"ok":true,...} — local, unauthenticatedThen from a remote machine on your Tailscale/WireGuard network:
curl -sk https://<this-host-tailscale-ip>:8443/healthThe MCP endpoint
https://<host>:8443/mcpAll requests to /mcp require Authorization: Bearer <MCP_AUTH_TOKEN>. GET /health is unauthenticated (for container/orchestrator health checks) and only ever returns {"ok": true/false, ...} — no secrets.
Fill in your actual reachable address once deployed (e.g. https://100.64.1.2:8443/mcp if using this host's current Tailscale IP — check with tailscale ip on the host, this can change if the machine is re-added to the tailnet).
Tools
All tools return {"ok": true, "request_id", ...} on success or {"ok": false, "request_id", "error": {"type", "message", ...}} on failure — including upstream Jev failures. Nothing is ever silently fabricated: if Jev fails or omits a field (e.g. Noul has no confidence), the response reflects that rather than inventing a value.
jev_choice — pick one of a fixed set of options
// input
{
"state": "AAPL: growth=high, valuation=high\nMSFT: growth=medium-high, valuation=medium-high\nGOOG: growth=medium-high, valuation=medium",
"question": "Which best matches high growth with reasonable valuation?",
"options": ["AAPL", "MSFT", "GOOG"]
// optional: "criteria": {"AAPL": "...", "MSFT": "...", "GOOG": "..."}
}
// output
{
"ok": true, "request_id": "...", "model": "jev-1.13.0", "usage": {...},
"type": "choice", "choice": "MSFT", "confidence": 0.7,
"probabilities": {"AAPL": 0.2, "MSFT": 0.7, "GOOG": 0.1}
}jev_score — rate something against ordered levels
// input
{
"state": "The export button crashes the settings page in Safari.",
"question": "How severe is the reported issue?",
"criteria": ["Cosmetic; no functional impact", "Degraded feature, workaround exists", "Blocking; no workaround"]
}
// output
{
"ok": true, "type": "score", "score": 1.43, "confidence": 0.35,
"probabilities": {"0": 0.0, "1": 0.57, "2": 0.43},
"legend": {"0": "Cosmetic...", "1": "Degraded...", "2": "Blocking..."}
}jev_noul — bounded yes/no
// input
{"state": "I have asked three times now. Can I please just talk to a real person?",
"question": "Is the customer asking for a human agent?"}
// output
{"ok": true, "type": "noul", "noul": 0.99}
// no "confidence" field — Jev doesn't return one for Noul, and none is invented herejev_decide — batch several questions against one state, one upstream call
Use this instead of multiple jev_choice/jev_score/jev_noul calls when you need more than one judgment about the same evidence. The TypeSafe API natively supports multiple typed questions per request; this tool is the only one that uses that instead of forcing N round trips.
// input
{
"state": "...",
"decisions": [
{"id": "dept", "type": "choice", "question": "Which team should handle this?", "options": ["returns", "shipping", "billing"]},
{"id": "urgent", "type": "noul", "question": "Does this need same-day attention?"}
]
}
// output
{"ok": true, "request_id": "...", "usage": {...}, "results": {"dept": {...}, "urgent": {...}}}Environment variables
See .env.example for the full annotated list. Required: TYPESAFE_API_KEY, MCP_AUTH_TOKEN (service refuses to start without either). Everything else has a sane default.
JEV_LOG_STATE=true enables verbose debug logging that includes the raw state payload — off by default since state may contain private information.
Logging
One JSON line per tool call to stdout (docker compose logs jev-mcp):
{"ts": "...", "tool": "jev_choice", "request_id": "...", "decision_type": "choice", "success": true, "jev_latency_ms": 340.2, "upstream_status": 200, "usage": {"input_tokens": 62, "output_tokens": 9}, "confidence": 0.7}state content is never included unless JEV_LOG_STATE=true.
Error handling
Condition | Result |
| Service refuses to start (config error logged, process exits) |
Invalid tool input (e.g. 1 option for a choice) |
|
Jev returns 401/403 |
|
Jev returns 429 |
|
Jev request times out |
|
Malformed/non-JSON Jev response |
|
Network failure reaching Jev |
|
Missing/wrong MCP bearer token | HTTP 401 at the transport layer, before any tool runs |
HTTPS
Caddy (the jev-mcp-caddy sidecar) terminates TLS on :8443 using its own internal CA (tls internal in caddy/Caddyfile) — no public domain or Let's Encrypt involved, matching an access pattern that's Tailscale/WireGuard-only.
Two things worth knowing:
The Caddyfile lists specific hostnames/IPs (
localhost,127.0.0.1, and the host's Tailscale IP) that it issues certificates for at startup, plus adefault_sniso IP-literal connections (which typically send no SNI at all — this is how most MCP/HTTP clients connect over Tailscale) still get a valid cert. If this host's Tailscale IP changes, update that list and rundocker compose up -d --build caddy.Clients will see an untrusted-certificate warning unless they trust Caddy's local root CA. For a private, Tailscale-only deployment this is normally fine to bypass (
-kin curl,verify=Falsein the test client) since the transport is already encrypted/authenticated by WireGuard. To avoid the warning instead, export Caddy's root and trust it on client machines:docker compose exec caddy cat /data/caddy/pki/authorities/local/root.crt > jev-mcp-ca.crt # then install jev-mcp-ca.crt in each client's trust store
Upgrade path to a publicly-trusted cert (optional, not required for the current access pattern): if this tailnet has Tailscale HTTPS Certificates enabled, you could instead run tailscale cert for this node's *.ts.net name and point Caddy at those files — real Let's Encrypt-backed certs, still no public exposure needed. Not set up here since it requires an admin-console change on your tailnet, which is your call to make.
The internal app (jev-mcp, port 9020) is only bound to 127.0.0.1 on the host — it is never reachable except through the Caddy sidecar or from the host itself.
Deploying to a public host (e.g. Oracle Cloud)
The Mac mini setup above is IP/Tailscale-only. On a real public cloud VM you have a public IP, so a real, publicly-trusted certificate is both possible and the better choice: Caddy obtains and auto-renews one from Let's Encrypt on its own — no manual cert files, no CDN/proxy in front.
This uses a different compose file (docker-compose.prod.yml) and Caddyfile (caddy/Caddyfile.public) than the Mac mini setup — stock caddy:2-alpine with the Caddyfile bind-mounted in (no custom image build needed; the earlier "bake the Caddyfile into the image" workaround was specifically for this Mac's Colima setup, which only auto-mounts $HOME — a normal Linux Docker host doesn't have that restriction).
One-time setup, before running the script
Provision the VM (you said you already have this) with SSH key access. Ubuntu and Oracle Linux are both supported by the bootstrap script.
DNS: add an A/AAAA record for your domain pointing directly at the VM's public IP — DNS-only, not proxied through Cloudflare or any other CDN (no orange cloud). Caddy needs to see real inbound connections itself to complete the Let's Encrypt domain-ownership challenge.
Cloud-level firewall: make sure the instance's Security List (or Network Security Group, if separately configured — check both) has ingress rules allowing TCP from
0.0.0.0/0on port 80 (needed for the ACME challenge, even though the service itself is reached over HTTPS) and your chosen--https-port(443 by default). This is a cloud-console setting the deploy script cannot reach from here — it only configures the VM's own OS firewall.
Deploy
deploy/deploy.sh \
--host <vm-ip-or-hostname> \
--user ubuntu \
--domain jev.yourdomain.com \
--acme-email you@example.com
# optional: --ssh-key ~/.ssh/id_ed25519 --remote-dir /opt/jev-mcp --https-port 443
# optional: --private-http-host 10.x.x.x --private-http-port 8080 (see below)This is idempotent — re-run it any time to push code updates; it will rsync the latest files, refresh .env, and restart the stack. It:
Bootstraps the VM over SSH (installs Docker + Compose plugin +
rsyncif missing, opens the OS firewall for ports 80 and--https-port) via deploy/bootstrap_remote.sh.rsyncs this project to
<remote-dir>/app.Writes a remote
.env— reusingTYPESAFE_API_KEY/MCP_AUTH_TOKENfrom your local.envif present (generating a freshMCP_AUTH_TOKENotherwise), plusDOMAIN/CADDY_PORT/ACME_EMAIL.Runs
docker compose -f docker-compose.prod.yml up -d --buildon the VM.Curls
https://<domain>:<port>/healthfrom your machine (retrying for ~30s — first-time cert issuance isn't instant) to confirm it's actually live before declaring success.
At the end it prints the MCP URL (https://<domain>:<port>/mcp) and the auth token to use.
Private/direct path (no public DNS dependency): pass --private-http-host <ip> (e.g. a WireGuard or Tailscale address already reachable from your machine) to also have Caddy serve plain HTTP on that IP, alongside the public HTTPS path — see the second site block in caddy/Caddyfile.public. No TLS there since the private tunnel already provides transport encryption; bearer-token auth still applies regardless of transport. The port is published bound to exactly that IP (never 0.0.0.0), so it's never reachable publicly, and it defaults to 8080 (not 80) so it doesn't collide with the public ACME listener. This is genuinely useful as a fallback for whenever the public path is unavailable for any reason.
Diagnostic history worth knowing about (both encountered on the Oracle Cloud deployment this was built against, neither is a flaw in this project's config):
A cloud provider's network can silently drop a CDN's traffic on the standard port while leaving direct connections untouched. The original design here used a Cloudflare-proxied setup with a Cloudflare Origin Certificate. It turned out Oracle Cloud's network was silently dropping Cloudflare's edge traffic on port 443 specifically — every console setting (Security List, DNS, SSL mode) was correct,
curlfrom an ordinary machine reached the origin fine, but Cloudflare's edge got 523 (origin unreachable), and Caddy's own access log showed zero requests ever arriving from Cloudflare's IP ranges (confirmed by watchingdocker compose -f docker-compose.prod.yml logs -f caddylive while triggering requests through the public URL — real diagnostic signal, not a config guess). Switching to a non-standard port worked around it, but going direct (no proxy in front at all, this repo's current approach) sidesteps the whole class of problem, since only the CDN's shared IP ranges were being filtered — direct visitor traffic was never affected.A VM's own outbound DNS resolution can silently break independent of anything in this repo. Oracle's link-local metadata DNS relay (
169.254.169.254) started failing mid-session, breakingdocker pull/aptwith "server misbehaving" errors. Ifdeploy.sh's Docker build step fails on a DNS-resolution error, check the VM can resolve names at all (getent ahosts registry-1.docker.io) before assuming it's a project issue — fix with a static/etc/resolv.confpointing at a public resolver (e.g. 8.8.8.8/1.1.1.1) if needed.
This was validated end-to-end against a real Oracle Cloud Ubuntu 24.04 VM in this session (deployed, real Jev decisions through both the public HTTPS path and the private WireGuard path, auth enforcement on both) — under the earlier Cloudflare-based design; the switch to direct Let's Encrypt was made for the architectural reasons above and validated with caddy validate/docker compose config locally, but treat the very first live run of this exact flow as worth watching.
Testing
Unit tests
pip install -r requirements-dev.txt
pytest -q26 tests covering: config validation (fails fast on missing secrets), the Jev client's request shape and error-mapping for every documented failure mode, the auth middleware, and all four tools' input validation / success / failure paths against a mocked upstream.
Manual / remote connectivity test
python scripts/test_client.py --url https://<host>:8443/mcp --token <MCP_AUTH_TOKEN> --insecure(Drop --insecure once you've installed Caddy's root CA locally, per HTTPS.) This checks /health, lists the registered tools, and runs the example decision from the project spec:
state: AAPL/MSFT/GOOG growth & valuation notes
question: Which best matches high growth with reasonable valuation?
options: AAPL, MSFT, GOOGThis is a connectivity/decision smoke test only — it does not fetch real market data (by design; Jev doesn't do its own research).
Open WebUI integration
Open WebUI's tool servers currently speak OpenAPI, not raw MCP — connecting an MCP server means bridging it through mcpo (the Open WebUI project's own MCP→OpenAPI adapter), rather than building a custom bridge for this service. (This wasn't verified against a live Open WebUI install in this session — no instance was running on this host — so double-check the exact admin-settings labels against your installed version.)
Run mcpo pointed at this remote MCP server, forwarding the bearer token as an upstream header:
pip install mcpo mcpo --port 8001 --api-key "<a separate key for Open WebUI to call mcpo with>" \ --server-type "streamable-http" \ --header '{"Authorization": "Bearer <MCP_AUTH_TOKEN>"}' \ -- https://<host>:8443/mcpOr via a config file (
mcpo --config mcpo.json):{ "mcpServers": { "jev-mcp": { "type": "streamable-http", "url": "https://<host>:8443/mcp", "headers": {"Authorization": "Bearer <MCP_AUTH_TOKEN>"} } } }In Open WebUI: Settings → Admin Settings → Tools, add an OpenAPI tool server pointing at
http://<mcpo-host>:8001with the mcpo--api-keyyou chose above.Enable the
jev-mcptools for the model(s) you want to use them, alongside a web-search tool. The intended flow per the project spec:search_web → collect evidence → jev_choice / jev_score / jev_noul → answer user
mcpo never sees TYPESAFE_API_KEY — only the MCP_AUTH_TOKEN you configured, which is exactly the credential jev-mcp expects.
Other clients (Codex, Claude Code, DocIntel, custom agents)
Nothing about this service is Open WebUI-specific — it's a standard MCP Streamable HTTP server with bearer auth. Any MCP-capable client can connect directly to https://<host>:8443/mcp with Authorization: Bearer <MCP_AUTH_TOKEN>, no adapter needed. scripts/test_client.py doubles as a minimal reference for wiring up a Python client (fastmcp.Client + StreamableHttpTransport).
Operations
Restart policy: both containers use
restart: unless-stopped; Colima (this host's Docker VM) is already configured to start on login via launchd, so the stack comes back up automatically after a reboot.Updating the Jev API key: edit
.env, thendocker compose up -d(no rebuild needed — it's read from the environment at process start).Rotating the MCP auth token: same — edit
MCP_AUTH_TOKENin.env, restart, and update every client's config.Logs:
docker compose logs -f jev-mcp/docker compose logs -f caddy.
Contributing
See CONTRIBUTING.md — setup, running tests, and what's in/out of scope for this project.
License
This server cannot be deployed
Maintenance
Related MCP Connectors
AI Reasoning Cache & Consensus Layer with 11 MCP tools via Streamable HTTP.
A paid remote MCP for Skybridge, built to return verdicts, receipts, usage logs, and audit-ready JSO
Decision Layer for AI Agents — 58+ tools, Advisor, MCP. Free key: POST /v1/register {}.
A paid remote MCP for Equibles, built to return verdicts, receipts, usage logs, and audit-ready JSON
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables MCP hosts to query Jev's typed decision model—yes/no, choice, and score—with calibrated probabilities, while defaulting to an offline mock and disclosing all egress unless explicitly enabled.13Apache 2.0
- AlicenseNot gradedqualityAmaintenanceEnables MCP clients to submit bounded semantic-uncertainty judgments to the pinned TypeSafe Jev API, with tools for yes/no, choice, and score evaluations plus optional evidence or context selection.MIT
- AlicenseNot gradedqualityBmaintenanceEnables MCP-capable harnesses to apply TypeSafe/Jev's System One decision model through judgment primitives and task-shaped tools, with local event logging and Prometheus metrics for impact tracking.1MIT
- AlicenseAqualityAmaintenanceEnables MCP-compatible agent hosts to interact with TypeSafe AI's Jev System One API through dependency-free STDIO tools for classification, scoring, verification, gating, routing, review, and health checks.9MIT