@jkudish/jev-mcp 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0
4
+
5
+ - Opt-in stateless Streamable HTTP: `jev-mcp --http` (or `JEV_MCP_TRANSPORT=http`) serves MCP 2026-07-28 per request and 2025-era clients through the SDK's stateless fallback, with no sessions, on `PORT` (default 8080) at `/mcp`, with `/health`. `HOST` defaults to loopback; binding beyond it is explicit. `JEV_MCP_AUTH_TOKEN` gates it with a bearer token and is required unless `HOST` is loopback; Host/Origin rebinding guards answer fixed-string 403s. Concurrent requests are capped at `JEV_MCP_MAX_CONCURRENCY` (default 16) with `429` backpressure, and a cancelled request aborts its in-flight regex worker and Jev calls. Stdio stays the default. Via [#37](https://github.com/jkudish/jev-mcp/pull/37) by shivasymbl.
6
+ - Stdio now also answers 2026-07-28 clients; 2025-era clients see no change.
7
+ - Client-visible provider errors are fixed strings (provider name and status only); upstream response text, including reflected credentials, never reaches tool error content.
8
+ - `jev_classify` keeps label ids collision-free and tallies on a null prototype, so labels like `constructor` or `__proto__` can no longer shadow `Object.prototype` or collide with generated ids. Via [#36](https://github.com/jkudish/jev-mcp/pull/36) by kittimzhe.
9
+ - `jev_verify` no longer mistakes an evidence item literally named `none` for its internal no-source option; the internal key now skips occupied `none_N` ids. Via [#38](https://github.com/jkudish/jev-mcp/pull/38) by xujiantop-crypto.
10
+ - Internal: `@modelcontextprotocol/sdk` 1.x replaced by the v2 packages `@modelcontextprotocol/server` and `@modelcontextprotocol/node` 2.1.0; tools are registered once and replayed onto a fresh server per connection or request.
11
+
12
+ ## 0.9.0
13
+
14
+ - Ships an agent skill inside the package: `skills/jev/` is included in the npm tarball, so coding agents get judgment-tool policy — which tool to call when, fail-closed handling, privacy — without hand-written prompts. Prompted by [#34](https://github.com/jkudish/jev-mcp/issues/34) by panaalexandrucristian.
15
+ - `jev_extract` keeps multi-letter regex flags (`gi`) intact instead of mangling them into `gig` and throwing. Found in [#34](https://github.com/jkudish/jev-mcp/issues/34).
16
+ - Internal: zod 4 and TypeScript 7; no tool behavior changes. `tools/list` input schemas gain a `propertyNames` field from zod 4's JSON Schema conversion. Via [#35](https://github.com/jkudish/jev-mcp/pull/35).
17
+ - Dependency updates: `@jkudish/jev-agent-tools` 0.1.2 and `@modelcontextprotocol/sdk` 1.30.1.
18
+
3
19
  ## 0.8.0
4
20
 
5
21
  - New `jev_noul` tool: batched calibrated probability for stated propositions, labeled likely / unlikely / uncertain. Prompted by [#31](https://github.com/jkudish/jev-mcp/issues/31).
package/README.md CHANGED
@@ -3,6 +3,10 @@
3
3
  [![CI](https://github.com/jkudish/jev-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/jkudish/jev-mcp/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
5
 
6
+ <p align="center">
7
+ <img src=".github/jev-mcp-og.png" alt="jev-mcp — Fast, cheap, typed judgments from TypeSafe's Jev model, as MCP tools." />
8
+ </p>
9
+
6
10
  Fast, cheap, typed judgments from TypeSafe's Jev model, as MCP tools.
7
11
 
8
12
  Give your agent eleven judgment tools:
@@ -124,6 +128,34 @@ args = ["-y", "@jkudish/jev-mcp"]
124
128
 
125
129
  Some MCP clients filter the environment before spawning servers, which silently drops `TYPESAFE_API_KEY`. If the server reports a missing key, pass it explicitly as shown above.
126
130
 
131
+ ### Remote / HTTP
132
+
133
+ Stdio is the default. To host one shared server for a team or a remote agent, run it in stateless HTTP mode:
134
+
135
+ ```bash
136
+ JEV_MCP_AUTH_TOKEN="$(openssl rand -hex 32)" TYPESAFE_API_KEY=ts_... npx -y @jkudish/jev-mcp --http
137
+ ```
138
+
139
+ It listens on `PORT` (default `8080`) and `HOST` (default `127.0.0.1` — set `HOST=0.0.0.0` explicitly to serve beyond your machine), serves MCP at `/mcp` and a health check at `/health`. It speaks MCP 2026-07-28 and falls back to stateless serving for 2025-era clients, so it keeps no sessions and scales behind any load balancer. Every call spends your Jev key, so `JEV_MCP_AUTH_TOKEN` is required unless `HOST` is loopback. Clients send it as a bearer token:
140
+
141
+ ```bash
142
+ claude mcp add --transport http jev https://jev.example.com/mcp --header "Authorization: Bearer $JEV_MCP_AUTH_TOKEN"
143
+ ```
144
+
145
+ The server itself speaks plain HTTP: terminate TLS at a reverse proxy or load balancer before exposing it beyond loopback, and put connection limits and request rate limits at that ingress — the process bounds admitted `/mcp` requests (`JEV_MCP_MAX_CONCURRENCY`, default 16; excess shed with `429`) and caps request bodies at 4 MiB, but it does not limit sockets waiting to finish headers or repeatedly rejected requests.
146
+
147
+ ### Agent skill
148
+
149
+ The package ships an agent skill (`skills/jev/`) that teaches coding agents when to reach for each tool instead of answering from their own reading — the difference between tools that sit registered-but-unused and tools that get called. Copy it into your client's skills directory:
150
+
151
+ ```bash
152
+ npm pack @jkudish/jev-mcp@latest
153
+ tar -xzf jkudish-jev-mcp-*.tgz
154
+ mkdir -p .claude/skills && cp -R package/skills/jev .claude/skills/
155
+ ```
156
+
157
+ Claude Code reads `.claude/skills`, OpenCode `.opencode/skills`, and Codex and generic agents `.agents/skills`. In Amp, the skill's frontmatter bundles the MCP server, so dropping it into a skills directory wires up both.
158
+
127
159
  ## The tools
128
160
 
129
161
  ### jev_verify
package/dist/http.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { type McpServer } from "@modelcontextprotocol/server";
2
+ export declare function serveHttp(factory: () => McpServer, env?: NodeJS.ProcessEnv): Promise<{
3
+ server: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
4
+ url: string;
5
+ }>;
package/dist/http.js ADDED
@@ -0,0 +1,101 @@
1
+ // Opt-in stateless Streamable HTTP transport (`jev-mcp --http`).
2
+ //
3
+ // Serves MCP 2026-07-28 per request and 2025-era clients through the SDK's
4
+ // stateless fallback: no sessions, no Mcp-Session-Id, nothing held between
5
+ // requests, so any number of replicas can sit behind a plain load balancer.
6
+ import { createServer as createNodeServer } from "node:http";
7
+ import { timingSafeEqual } from "node:crypto";
8
+ import { createMcpHandler } from "@modelcontextprotocol/server";
9
+ import { toNodeHandler } from "@modelcontextprotocol/node";
10
+ const LOOPBACK = new Set(["127.0.0.1", "::1", "localhost"]);
11
+ // Fixed-string rejection: never echo the rejected Host/Origin back (reflection
12
+ // of attacker-controlled values), unlike the SDK's guards.
13
+ const FORBIDDEN_BODY = JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Forbidden" }, id: null });
14
+ function forbidden(res) {
15
+ res.writeHead(403, { "content-type": "application/json" }).end(FORBIDDEN_BODY);
16
+ }
17
+ function isLoopbackHost(host) {
18
+ if (!host)
19
+ return false;
20
+ let hostname = host.toLowerCase();
21
+ const bracketed = hostname.match(/^\[(.+)\](?::\d+)?$/);
22
+ if (bracketed)
23
+ hostname = bracketed[1];
24
+ else
25
+ hostname = hostname.replace(/:\d+$/, "");
26
+ return hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost";
27
+ }
28
+ function isLoopbackOrigin(origin) {
29
+ if (origin === undefined)
30
+ return true;
31
+ try {
32
+ return isLoopbackHost(new URL(origin).host);
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ export async function serveHttp(factory, env = process.env) {
39
+ // Loopback by default: binding a public interface is an explicit act.
40
+ const host = env.HOST || "127.0.0.1";
41
+ const port = Number(env.PORT || 8080);
42
+ const token = Buffer.from(env.JEV_MCP_AUTH_TOKEN ?? "");
43
+ const maxInFlight = Number(env.JEV_MCP_MAX_CONCURRENCY ?? 16);
44
+ // The server spends the operator's Jev key on every call; never expose it unauthenticated.
45
+ if (token.length === 0 && !LOOPBACK.has(host)) {
46
+ throw new Error("JEV_MCP_AUTH_TOKEN is required when HTTP mode binds a non-loopback HOST");
47
+ }
48
+ if (!Number.isInteger(maxInFlight) || maxInFlight < 1) {
49
+ throw new Error("JEV_MCP_MAX_CONCURRENCY must be a positive integer");
50
+ }
51
+ const authorized = (header) => {
52
+ if (token.length === 0)
53
+ return true;
54
+ const given = Buffer.from(header?.startsWith("Bearer ") ? header.slice(7) : "");
55
+ return given.length === token.length && timingSafeEqual(given, token);
56
+ };
57
+ // On loopback, reject foreign Host/Origin headers so a web page cannot reach
58
+ // the server through DNS rebinding (the spec's Origin-validation MUST).
59
+ const onLoopback = LOOPBACK.has(host);
60
+ const mcp = toNodeHandler(createMcpHandler(factory), {
61
+ onerror: (error) => console.error(`[jev-mcp] http: ${error.message}`),
62
+ });
63
+ let inFlight = 0;
64
+ const server = createNodeServer((req, res) => {
65
+ const path = (req.url ?? "/").split("?")[0];
66
+ if (path === "/health") {
67
+ res.writeHead(200, { "content-type": "text/plain" }).end("ok");
68
+ }
69
+ else if (path !== "/mcp") {
70
+ res.writeHead(404).end();
71
+ }
72
+ else if (onLoopback && (!isLoopbackHost(req.headers.host) || !isLoopbackOrigin(req.headers.origin))) {
73
+ forbidden(res);
74
+ }
75
+ else if (!authorized(req.headers.authorization)) {
76
+ res.writeHead(401, { "www-authenticate": "Bearer" }).end();
77
+ }
78
+ else if (inFlight >= maxInFlight) {
79
+ // Backpressure: each in-flight request replays tools and may spend the
80
+ // operator's Jev key; shed load instead of queueing it.
81
+ res.writeHead(429, { "retry-after": "1" }).end();
82
+ }
83
+ else {
84
+ inFlight++;
85
+ void mcp(req, res).finally(() => inFlight--);
86
+ }
87
+ });
88
+ await new Promise((resolve) => server.listen(port, host, resolve));
89
+ const { port: bound, address: boundAddress } = server.address();
90
+ // "localhost" must actually resolve to loopback; a poisoned resolver must not
91
+ // silently turn a loopback bind into a public one.
92
+ if (onLoopback && boundAddress !== "127.0.0.1" && boundAddress !== "::1" && boundAddress !== "::ffff:127.0.0.1") {
93
+ server.close();
94
+ throw new Error(`HOST ${host} resolved to non-loopback ${boundAddress}; refusing to serve`);
95
+ }
96
+ const shutdown = () => server.close(() => process.exit(0));
97
+ process.once("SIGTERM", shutdown);
98
+ process.once("SIGINT", shutdown);
99
+ return { server, url: `http://${host.includes(":") ? `[${host}]` : host}:${bound}/mcp` };
100
+ }
101
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import { McpServer } from "@modelcontextprotocol/server";
3
+ export declare function createServer(): McpServer;